Edgewall Software

Ticket #2304: backup.2.patch

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

revised backup patch with test case

  • 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..724030c 100644
    a b class MySQLConnector(Component): 
    142142                  '_'.join(index.columns), table.name, 
    143143                  self._collist(table, index.columns)) 
    144144 
     145    def backup(self, dest): 
     146        raise TracError("MySQL backup not implemented") 
    145147 
    146148class MySQLConnection(ConnectionWrapper): 
    147149    """Connection wrapper for MySQL.""" 
  • trac/db/postgres_backend.py

    diff --git a/trac/db/postgres_backend.py b/trac/db/postgres_backend.py
    index 9c7b3b2..2df6e02 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): 
    6572        cnx = self.get_connection(path, user, password, host, port, params) 
    6673        cursor = cnx.cursor() 
    6774        if cnx.schema: 
    68             cursor.execute('CREATE SCHEMA "%s"' % cnx.schema) 
    69             cursor.execute('SET search_path TO %s', (cnx.schema,)) 
     75            try: 
     76                cursor.execute('CREATE SCHEMA "%s"' % cnx.schema) 
     77                cursor.execute('SET search_path TO %s', (cnx.schema,)) 
     78                cnx.commit() 
     79            except Exception, e: 
     80                cnx.rollback() 
    7081        from trac.db_default import schema 
    7182        for table in schema: 
    7283            for stmt in self.to_sql(table): 
    class PostgreSQLConnector(Component): 
    93104            yield 'CREATE %s INDEX "%s_%s_idx" ON "%s" ("%s")' % (unique, table.name,  
    94105                   '_'.join(index.columns), table.name, '","'.join(index.columns)) 
    95106 
     107    def backup(self, dest_file): 
     108        # pg_dump -n schemaname dbname | gzip > filename.gz 
     109        db_url = self.env.config.get('trac', 'database') 
     110        scheme, db_prop = _parse_db_str(db_url) 
     111        db_name = os.path.basename(db_prop['path']) 
     112        args = [self.pg_dump_bin, '-C', '-d', '-x', '-Z', self.compression, 
     113                '-U', db_prop['user'], 
     114                '-h', db_prop['host'], 
     115                '-p', str(db_prop['port'])] 
     116 
     117        if 'schema' in db_prop['params']: 
     118            args.extend(['-n', db_prop['params']['schema'], db_name]) 
     119        else: 
     120            args.extend([db_name]) 
     121 
     122        dest_file = "%s.gz" % (dest_file,) 
     123        args.extend(['>', dest_file]) 
     124        if sys.platform == 'win': 
     125            # XXX TODO verify on windows 
     126            args = ['cmd', '/c', ' '.join(args)] 
     127        else: 
     128            args = ['bash', '-c', ' '.join(args)] 
     129         
     130        environ = os.environ.copy() 
     131        environ['PGPASSWORD'] = db_prop['password'] 
     132        #print >> sys.stderr, "backup command %r" % (args,) 
     133        #print >> sys.stderr, "backup props %r" % (db_prop,) 
     134        #print >> sys.stderr, "backup to %s" % dest_file 
     135        p = Popen(args, env=environ, shell=False, bufsize=0, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=True) 
     136        p.wait() 
     137        p.stdout.close() 
     138        p.stderr.close() 
     139        if not os.path.exists(dest_file): 
     140            raise TracError("Backup attempt failed") 
    96141 
    97142class PostgreSQLConnection(ConnectionWrapper): 
    98143    """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..53f8b56
    - +  
     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 = "testenv%s" % port 
     22        dirname = os.path.join(trac_source_tree, dirname) 
     23 
     24        baseurl = "http://127.0.0.1:%s" % port 
     25        self._testenv = self.env_class(dirname, port, baseurl) 
     26 
     27    def tearDown(self): 
     28        """remove the test environment""" 
     29        self._testenv.destroy() 
     30 
     31    def test_backup(self): 
     32        """Testing backup""" 
     33        # raises TracError if backup fails 
     34        env = self._testenv.get_trac_environment() 
     35        env.backup() 
     36 
     37 
     38 
     39def suite(): 
     40    return unittest.makeSuite(DatabaseBackupTestCase,'test') 
     41 
     42if __name__ == '__main__': 
     43    unittest.main(defaultTest='suite') 
  • trac/tests/functional/testenv.py

    diff --cc trac/tests/functional/testenv.py
    index 6514e36,f1921c7..0000000