Ticket #2304: backup.3.2.patch
| File backup.3.2.patch, 13.7 KB (added by Shane Caraveo <shanec@…>, 3 years ago) |
|---|
-
trac/db/api.py
diff --git a/trac/db/api.py b/trac/db/api.py index 829b7e6..715c32d 100644
a b 16 16 17 17 import os 18 18 import urllib 19 import time 19 20 20 21 from trac.config import Option, IntOption 21 22 from trac.core import * … … class IDatabaseConnector(Interface): 48 49 def to_sql(table): 49 50 """Return the DDL statements necessary to create the specified table, 50 51 including indices.""" 52 53 def backup(dest): 54 """Backup the database to a location defined by trac.backup_dir""" 51 55 52 56 53 57 class DatabaseManager(Component): … … class DatabaseManager(Component): 59 63 [wiki:TracEnvironment#DatabaseConnectionStrings string] for this 60 64 project""") 61 65 66 backup_dir = Option('trac', 'backup_dir', 'db', 67 """Database backup location""") 68 62 69 timeout = IntOption('trac', 'timeout', '20', 63 70 """Timeout value for database connection, in seconds. 64 71 Use '0' to specify ''no timeout''. ''(Since 0.11)''""") … … class DatabaseManager(Component): 81 88 self._cnx_pool.shutdown(tid) 82 89 if not tid: 83 90 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) 84 107 85 108 def _get_connector(self): ### FIXME: Make it public? 86 109 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 14 14 # individuals. For the exact contribution history, see the revision 15 15 # history and logs, available at http://trac.edgewall.org/log/. 16 16 17 import re 17 import re, sys, os, time 18 18 19 19 from trac.core import * 20 from trac.db.api import IDatabaseConnector 20 from trac.config import Option 21 from trac.db.api import IDatabaseConnector, _parse_db_str 21 22 from trac.db.util import ConnectionWrapper 22 23 from trac.util import get_pkginfo 23 24 from trac.util.translation import _ 25 from subprocess import Popen, PIPE 24 26 25 27 _like_escape_re = re.compile(r'([/_%])') 26 28 … … class MySQLConnector(Component): 56 58 57 59 implements(IDatabaseConnector) 58 60 61 dump_bin = Option('trac', 'mysqldump_bin', 'mysqldump', 62 """Location of mysqldump for MySQL database backups""") 63 59 64 def __init__(self): 60 65 self._version = None 61 66 … … class MySQLConnector(Component): 143 148 self._collist(table, index.columns)) 144 149 145 150 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 146 180 class MySQLConnection(ConnectionWrapper): 147 181 """Connection wrapper for MySQL.""" 148 182 -
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 14 14 # 15 15 # Author: Christopher Lenz <cmlenz@gmx.de> 16 16 17 import re 17 import re, sys, os, time 18 18 19 19 from trac.core import * 20 from trac.db.api import IDatabaseConnector 20 from trac.config import Option 21 from trac.db.api import IDatabaseConnector, _parse_db_str 21 22 from trac.db.util import ConnectionWrapper 22 23 from trac.util import get_pkginfo 24 from subprocess import Popen, PIPE 23 25 24 26 psycopg = None 25 27 PgSQL = None … … class PostgreSQLConnector(Component): 33 35 34 36 implements(IDatabaseConnector) 35 37 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 36 43 def __init__(self): 37 44 self._version = None 38 45 … … class PostgreSQLConnector(Component): 93 100 yield 'CREATE %s INDEX "%s_%s_idx" ON "%s" ("%s")' % (unique, table.name, 94 101 '_'.join(index.columns), table.name, '","'.join(index.columns)) 95 102 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") 96 137 97 138 class PostgreSQLConnection(ConnectionWrapper): 98 139 """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): 146 146 def to_sql(cls, table): 147 147 return _to_sql(table) 148 148 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") 149 161 150 162 class SQLiteConnection(ConnectionWrapper): 151 163 """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): 414 414 @param dest: Destination file; if not specified, the backup is stored in 415 415 a file called db_name.trac_version.bak 416 416 """ 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) 426 418 427 419 def needs_upgrade(self): 428 420 """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
- + 1 import trac 2 from trac import db_default 3 from trac.db import sqlite_backend 4 from trac.env import Environment 5 6 import os.path 7 import unittest 8 import tempfile 9 import shutil 10 11 from trac.tests.functional.testenv import FunctionalTestEnvironment 12 13 class 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 38 def suite(): 39 return unittest.makeSuite(DatabaseBackupTestCase,'test') 40 41 if __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 15 15 from trac.tests.functional import logfile 16 16 from trac.tests.functional.better_twill import tc, ConnectError 17 17 from trac.env import open_environment 18 from trac.db.api import _parse_db_str 18 19 19 20 # TODO: refactor to support testing multiple frontends, backends (and maybe 20 21 # repositories and authentication). … … class FunctionalTestEnvironment(object): 71 72 return 'sqlite:db/trac.db' 72 73 dburi = property(get_dburi) 73 74 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 74 100 def destroy(self): 75 101 """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() 90 119 91 120 self.destroy_repo() 92 121 if os.path.exists(self.dirname):
