Edgewall Software

Ticket #2304: backup.3.2.patch

File backup.3.2.patch, 13.7 KB (added by Shane Caraveo <shanec@…>, 3 years ago)

fixed full patch with new backup

  • trac/db/api.py

    diff --git a/trac/db/api.py b/trac/db/api.py
    index 829b7e6..715c32d 100644
    a b  
    1616 
    1717import os 
    1818import urllib 
     19import time 
    1920 
    2021from trac.config import Option, IntOption 
    2122from trac.core import * 
    class IDatabaseConnector(Interface): 
    4849    def to_sql(table): 
    4950        """Return the DDL statements necessary to create the specified table, 
    5051        including indices.""" 
     52         
     53    def backup(dest): 
     54        """Backup the database to a location defined by trac.backup_dir""" 
    5155 
    5256 
    5357class DatabaseManager(Component): 
    class DatabaseManager(Component): 
    5963        [wiki:TracEnvironment#DatabaseConnectionStrings string] for this 
    6064        project""") 
    6165 
     66    backup_dir = Option('trac', 'backup_dir', 'db', 
     67        """Database backup location""") 
     68 
    6269    timeout = IntOption('trac', 'timeout', '20', 
    6370        """Timeout value for database connection, in seconds. 
    6471        Use '0' to specify ''no timeout''. ''(Since 0.11)''""") 
    class DatabaseManager(Component): 
    8188            self._cnx_pool.shutdown(tid) 
    8289            if not tid: 
    8390                self._cnx_pool = None 
     91                 
     92    def backup(self, dest=None): 
     93        connector, args = self._get_connector() 
     94        if not dest: 
     95            backup_dir = self.backup_dir 
     96            if backup_dir[0] != "/": 
     97                backup_dir = os.path.join(self.env.path, backup_dir) 
     98            db_str = self.config.get('trac', 'database') 
     99            db_name, db_path = db_str.split(":",1) 
     100            dest_name = '%s.%i.%d.bak' % (db_name, self.env.get_version(),int(time.time())) 
     101            dest = os.path.join(backup_dir, dest_name) 
     102        else: 
     103            backup_dir = os.path.dirname(dest) 
     104        if not os.path.exists(backup_dir): 
     105            os.makedirs(backup_dir) 
     106        connector.backup(dest) 
    84107 
    85108    def _get_connector(self): ### FIXME: Make it public? 
    86109        scheme, args = _parse_db_str(self.connection_uri) 
  • trac/db/mysql_backend.py

    diff --git a/trac/db/mysql_backend.py b/trac/db/mysql_backend.py
    index e69a83c..e03272d 100644
    a b  
    1414# individuals. For the exact contribution history, see the revision 
    1515# history and logs, available at http://trac.edgewall.org/log/. 
    1616 
    17 import re 
     17import re, sys, os, time 
    1818 
    1919from trac.core import * 
    20 from trac.db.api import IDatabaseConnector 
     20from trac.config import Option 
     21from trac.db.api import IDatabaseConnector, _parse_db_str 
    2122from trac.db.util import ConnectionWrapper 
    2223from trac.util import get_pkginfo 
    2324from trac.util.translation import _ 
     25from subprocess import Popen, PIPE 
    2426 
    2527_like_escape_re = re.compile(r'([/_%])') 
    2628 
    class MySQLConnector(Component): 
    5658 
    5759    implements(IDatabaseConnector) 
    5860 
     61    dump_bin = Option('trac', 'mysqldump_bin', 'mysqldump', 
     62        """Location of mysqldump for MySQL database backups""") 
     63 
    5964    def __init__(self): 
    6065        self._version = None 
    6166 
    class MySQLConnector(Component): 
    143148                  self._collist(table, index.columns)) 
    144149 
    145150 
     151    def backup(self, dest_file): 
     152        # msyqldump -n schemaname dbname | gzip > filename.gz 
     153        db_url = self.env.config.get('trac', 'database') 
     154        scheme, db_prop = _parse_db_str(db_url) 
     155        db_name = os.path.basename(db_prop['path']) 
     156        args = [self.dump_bin,  
     157                '-u%s' % db_prop['user'], 
     158                '-h%s' % db_prop['host'], 
     159                '-P%s' % str(db_prop['port']), db_name] 
     160         
     161        args.extend(['>', dest_file]) 
     162        if sys.platform == 'win': 
     163            # XXX TODO verify on windows 
     164            args = ['cmd', '/c', ' '.join(args)] 
     165        else: 
     166            args = ['bash', '-c', ' '.join(args)] 
     167         
     168        environ = os.environ.copy() 
     169        environ['MYSQL_PWD'] = db_prop['password'] 
     170        #print >> sys.stderr, "backup command %r" % (args,) 
     171        #print >> sys.stderr, "backup props %r" % (db_prop,) 
     172        #print >> sys.stderr, "backup to %s" % dest_file 
     173        p = Popen(args, env=environ, shell=False, bufsize=0, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=True) 
     174        p.wait() 
     175        p.stdout.close() 
     176        p.stderr.close() 
     177        if not os.path.exists(dest_file): 
     178            raise TracError("Backup attempt failed") 
     179 
    146180class MySQLConnection(ConnectionWrapper): 
    147181    """Connection wrapper for MySQL.""" 
    148182 
  • trac/db/postgres_backend.py

    diff --git a/trac/db/postgres_backend.py b/trac/db/postgres_backend.py
    index 9c7b3b2..cc5447c 100644
    a b  
    1414# 
    1515# Author: Christopher Lenz <cmlenz@gmx.de> 
    1616 
    17 import re 
     17import re, sys, os, time 
    1818 
    1919from trac.core import * 
    20 from trac.db.api import IDatabaseConnector 
     20from trac.config import Option 
     21from trac.db.api import IDatabaseConnector, _parse_db_str 
    2122from trac.db.util import ConnectionWrapper 
    2223from trac.util import get_pkginfo 
     24from subprocess import Popen, PIPE 
    2325 
    2426psycopg = None 
    2527PgSQL = None 
    class PostgreSQLConnector(Component): 
    3335 
    3436    implements(IDatabaseConnector) 
    3537 
     38    pg_dump_bin = Option('trac', 'pg_dump_bin', 'pg_dump', 
     39        """Location of pg_dump for Postgres database backups""") 
     40    compression = Option('trac', 'backup_compression', '8', 
     41        """Gzip backup compression level (if supported by backend)""") 
     42 
    3643    def __init__(self): 
    3744        self._version = None 
    3845 
    class PostgreSQLConnector(Component): 
    93100            yield 'CREATE %s INDEX "%s_%s_idx" ON "%s" ("%s")' % (unique, table.name,  
    94101                   '_'.join(index.columns), table.name, '","'.join(index.columns)) 
    95102 
     103    def backup(self, dest_file): 
     104        # pg_dump -n schemaname dbname | gzip > filename.gz 
     105        db_url = self.env.config.get('trac', 'database') 
     106        scheme, db_prop = _parse_db_str(db_url) 
     107        db_name = os.path.basename(db_prop['path']) 
     108        args = [self.pg_dump_bin, '-C', '-d', '-x', '-Z', self.compression, 
     109                '-U', db_prop['user'], 
     110                '-h', db_prop['host'], 
     111                '-p', str(db_prop['port'])] 
     112 
     113        if 'schema' in db_prop['params']: 
     114            args.extend(['-n', db_prop['params']['schema'], db_name]) 
     115        else: 
     116            args.extend([db_name]) 
     117 
     118        dest_file = "%s.gz" % (dest_file,) 
     119        args.extend(['>', dest_file]) 
     120        if sys.platform == 'win': 
     121            # XXX TODO verify on windows 
     122            args = ['cmd', '/c', ' '.join(args)] 
     123        else: 
     124            args = ['bash', '-c', ' '.join(args)] 
     125         
     126        environ = os.environ.copy() 
     127        environ['PGPASSWORD'] = db_prop['password'] 
     128        #print >> sys.stderr, "backup command %r" % (args,) 
     129        #print >> sys.stderr, "backup props %r" % (db_prop,) 
     130        #print >> sys.stderr, "backup to %s" % dest_file 
     131        p = Popen(args, env=environ, shell=False, bufsize=0, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=True) 
     132        p.wait() 
     133        p.stdout.close() 
     134        p.stderr.close() 
     135        if not os.path.exists(dest_file): 
     136            raise TracError("Backup attempt failed") 
    96137 
    97138class PostgreSQLConnection(ConnectionWrapper): 
    98139    """Connection wrapper for PostgreSQL.""" 
  • trac/db/sqlite_backend.py

    diff --git a/trac/db/sqlite_backend.py b/trac/db/sqlite_backend.py
    index 84a8dd9..bc69ee1 100644
    a b class SQLiteConnector(Component): 
    146146    def to_sql(cls, table): 
    147147        return _to_sql(table) 
    148148 
     149    def backup(self, dest_file): 
     150        """Simple SQLite-specific backup of the database. 
     151 
     152        @param dest: Destination file; if not specified, the backup is stored in 
     153                     a file called db_name.trac_version.bak 
     154        """ 
     155        import shutil 
     156        db_str = self.config.get('trac', 'database') 
     157        db_name = os.path.join(self.env.path, db_str[7:]) 
     158        shutil.copy(db_name, dest_file) 
     159        if not os.path.exists(dest_file): 
     160            raise TracError("Backup attempt failed") 
    149161 
    150162class SQLiteConnection(ConnectionWrapper): 
    151163    """Connection wrapper for SQLite.""" 
  • trac/env.py

    diff --git a/trac/env.py b/trac/env.py
    index a579397..c0ec4c2 100644
    a b class Environment(Component, ComponentManager): 
    414414        @param dest: Destination file; if not specified, the backup is stored in 
    415415                     a file called db_name.trac_version.bak 
    416416        """ 
    417         import shutil 
    418  
    419         db_str = self.config.get('trac', 'database') 
    420         if not db_str.startswith('sqlite:'): 
    421             raise TracError(_('Can only backup sqlite databases')) 
    422         db_name = os.path.join(self.path, db_str[7:]) 
    423         if not dest: 
    424             dest = '%s.%i.bak' % (db_name, self.get_version()) 
    425         shutil.copy (db_name, dest) 
     417        DatabaseManager(self).backup(dest) 
    426418 
    427419    def needs_upgrade(self): 
    428420        """Return whether the environment needs to be upgraded.""" 
  • new file trac/tests/backup.py

    diff --git a/trac/tests/backup.py b/trac/tests/backup.py
    new file mode 100644
    index 0000000..96e22eb
    - +  
     1import trac 
     2from trac import db_default 
     3from trac.db import sqlite_backend 
     4from trac.env import Environment 
     5 
     6import os.path 
     7import unittest 
     8import tempfile 
     9import shutil 
     10 
     11from trac.tests.functional.testenv import FunctionalTestEnvironment 
     12 
     13class DatabaseBackupTestCase(unittest.TestCase): 
     14 
     15    env_class = FunctionalTestEnvironment 
     16 
     17    def setUp(self): 
     18        trac_source_tree = os.path.normpath(os.path.join(trac.__file__, '..', 
     19                                                     '..')) 
     20        port = 8000 + os.getpid() % 1000 
     21        dirname = os.path.join(trac_source_tree, "testenv") 
     22 
     23        baseurl = "http://127.0.0.1:%s" % port 
     24        self._testenv = self.env_class(dirname, port, baseurl) 
     25 
     26    def tearDown(self): 
     27        """leave the test environment for later examination, 
     28        FunctionalTestEnvironment will cleanup on the next run""" 
     29 
     30    def test_backup(self): 
     31        """Testing backup""" 
     32        # raises TracError if backup fails 
     33        env = self._testenv.get_trac_environment() 
     34        env.backup() 
     35 
     36 
     37 
     38def suite(): 
     39    return unittest.makeSuite(DatabaseBackupTestCase,'test') 
     40 
     41if __name__ == '__main__': 
     42    unittest.main(defaultTest='suite') 
  • trac/tests/functional/testenv.py

    diff --git a/trac/tests/functional/testenv.py b/trac/tests/functional/testenv.py
    index 6514e36..69ee88a 100755
    a b from trac.tests.functional.compat import rmtree, close_fds 
    1515from trac.tests.functional import logfile 
    1616from trac.tests.functional.better_twill import tc, ConnectError 
    1717from trac.env import open_environment 
     18from trac.db.api import _parse_db_str 
    1819 
    1920# TODO: refactor to support testing multiple frontends, backends (and maybe 
    2021# repositories and authentication). 
    class FunctionalTestEnvironment(object): 
    7172        return 'sqlite:db/trac.db' 
    7273    dburi = property(get_dburi) 
    7374 
     75    def destroy_mysqldb(self): 
     76        # NOTE: mysqldump and mysql must be on path 
     77        # for mysql, we'll drop all the tables in the database and reuse 
     78        # the same database 
     79        # mysqldump -u[USERNAME] -p[PASSWORD] --add-drop-table --no-data [DATABASE] | grep ^DROP | mysql -u[USERNAME] -p[PASSWORD] [DATABASE] 
     80        import sys 
     81        scheme, db_prop = _parse_db_str(self.dburi) 
     82        db_prop['dbname'] = os.path.basename(db_prop['path']) 
     83        cmd = "mysqldump -u%(user)s -h%(host)s -P%(port)s --add-drop-table --no-data %(dbname)s | grep ^DROP | mysql -u%(user)s -h%(host)s -P%(port)s %(dbname)s" \ 
     84              % db_prop 
     85        print cmd 
     86        if sys.platform == 'win': 
     87            # XXX TODO verify on windows 
     88            args = ['cmd', '/c', cmd] 
     89        else: 
     90            args = ['bash', '-c', cmd] 
     91         
     92        environ = os.environ.copy() 
     93        environ['MYSQL_PWD'] = db_prop['password'] 
     94        print >> sys.stderr, "command %r" % (args,) 
     95        p = Popen(args, env=environ, shell=False, bufsize=0, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=True) 
     96        p.wait() 
     97        p.stdout.close() 
     98        p.stderr.close() 
     99 
    74100    def destroy(self): 
    75101        """Remove all of the test environment data.""" 
    76         if self.dburi.startswith("postgres"): 
    77             # We'll remove the schema automatically for Postgres if it exists. 
    78             # With this, you can run functional tests multiple times without 
    79             # running external tools (just like when running against sqlite) 
    80             import trac.db.api as db_api 
    81             env = self.get_trac_environment() 
    82             env_db = env.get_db_cnx() 
    83             if env_db.schema: 
    84                 cursor = env_db.cursor() 
    85                 try: 
    86                     cursor.execute('DROP SCHEMA %s CASCADE'%(env_db.schema)) 
    87                     env_db.commit() 
    88                 except: #TODO decide if this can swallow important errors 
    89                     env_db.rollback() 
     102        if os.path.exists(self.dirname): 
     103            if self.dburi.startswith("postgres"): 
     104                # We'll remove the schema automatically for Postgres if it exists. 
     105                # With this, you can run functional tests multiple times without 
     106                # running external tools (just like when running against sqlite) 
     107                import trac.db.api as db_api 
     108                env = self.get_trac_environment() 
     109                env_db = env.get_db_cnx() 
     110                if env_db.schema: 
     111                    cursor = env_db.cursor() 
     112                    try: 
     113                        cursor.execute('DROP SCHEMA %s CASCADE'%(env_db.schema)) 
     114                        env_db.commit() 
     115                    except: #TODO decide if this can swallow important errors 
     116                        env_db.rollback() 
     117            elif self.dburi.startswith("mysql"): 
     118                self.destroy_mysqldb() 
    90119 
    91120        self.destroy_repo() 
    92121        if os.path.exists(self.dirname):