├── .gitignore ├── CHANGES.txt ├── LICENSE ├── MANIFEST.in ├── README.rst ├── dbdump ├── __init__.py └── management │ ├── __init__.py │ └── commands │ ├── __init__.py │ └── dbdump.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.egg-info -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | 2016-11-29 - Version 1.2 2 | - Adjusted script to be Django >=1.8 compatible 3 | 4 | 2014-04-16 - Version 1.1 5 | - Added --raw-args option that passes arguments as-is to dump command (dk-dkopen) 6 | - Made use of ~/.pgpass possible (JocelynDelalande) 7 | 8 | 2013-04-12 - Version 1.0.1 9 | - Added missing README/CHANGES files to MANIFEST.in 10 | 11 | 2013-04-10 - Version 1.0 12 | - Added option to specify output filename (bedingue) 13 | - Added ability to dump to stdout (madssj) 14 | - Fixed PostgreSQL support (madssj) 15 | 16 | 2011-09-03 - Version 0.1 17 | - Initial release. 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Vitaliy Fuks. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of Haystack nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include CHANGES.txt 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ------------- 2 | django-dbdump 3 | ------------- 4 | 5 | This tiny Django application provides a "dbdump" management command that wraps 6 | mysqldump or pg_dump and allows you to create SQL dumps of your configured 7 | databases. 8 | 9 | To use, add 'dbdump' to your INSTALLED_APPS. 10 | 11 | Usage: 12 | 13 | - `$ manage.py dbdump` 14 | - `$ manage.py dbdump --db-name=mydb --debug --compress=gzip` 15 | 16 | You can dump only table schema without data or exclude tables completely 17 | using DB_DUMP_EMPTY_TABLES and DB_DUMP_EXCLUDED_TABLES settings inside 18 | your settings.DATABASES definition. For example:: 19 | 20 | DATABASES = { 21 | 'default': { 22 | 'ENGINE': 'mysql', 23 | ... 24 | 'DB_DUMP_EMPTY_TABLES': ['table1', 'table2'], 25 | } 26 | } 27 | 28 | will not include contents of table1 and table2 in your output. 29 | 30 | See http://github.com/vitaliyf/django-dbdump for more information. 31 | -------------------------------------------------------------------------------- /dbdump/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitaliyf/django-dbdump/5a76ef93a895f49568bfc7928a4fced11991ae37/dbdump/__init__.py -------------------------------------------------------------------------------- /dbdump/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitaliyf/django-dbdump/5a76ef93a895f49568bfc7928a4fced11991ae37/dbdump/management/__init__.py -------------------------------------------------------------------------------- /dbdump/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitaliyf/django-dbdump/5a76ef93a895f49568bfc7928a4fced11991ae37/dbdump/management/commands/__init__.py -------------------------------------------------------------------------------- /dbdump/management/commands/dbdump.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Command to do a database dump using database's native tools. 4 | 5 | Originally inspired by http://djangosnippets.org/snippets/823/ 6 | """ 7 | 8 | import os 9 | import time 10 | import sys 11 | import subprocess 12 | 13 | from django.core.management.base import BaseCommand, CommandError 14 | from django.conf import settings 15 | 16 | 17 | class Command(BaseCommand): 18 | help = 'Dump database into a file. Only MySQL and PostgreSQL engines are supported.' 19 | 20 | def add_arguments(self, parser): 21 | parser.add_argument( 22 | '--destination', 23 | dest='backup_directory', 24 | default='backups', 25 | help='Destination (path) where to place database dump file.' 26 | ) 27 | 28 | parser.add_argument( 29 | '--filename', 30 | dest='filename', 31 | default=False, 32 | help='Name of the file, or - for stdout' 33 | ) 34 | 35 | parser.add_argument( 36 | '--db-name', 37 | dest='database_name', 38 | default='default', 39 | help='Name of database (as defined in settings.DATABASES[]) to dump.' 40 | ) 41 | 42 | parser.add_argument( 43 | '--compress', 44 | dest='compression_command', 45 | help='Optional command to run (e.g., gzip) to compress output file.' 46 | ) 47 | 48 | parser.add_argument( 49 | '--quiet', 50 | dest='quiet', 51 | action='store_true', 52 | default=False, 53 | help='Be silent.') 54 | 55 | parser.add_argument( 56 | '--debug', 57 | dest='debug', 58 | action='store_true', 59 | default=False, 60 | help='Show commands that are being executed.' 61 | ) 62 | 63 | parser.add_argument( 64 | '--pgpass', 65 | dest='pgpass', 66 | action='store_true', 67 | default=False, 68 | help='Use the ~/.pgdump file for password instead of prompting (PostgreSQL only).' 69 | ) 70 | 71 | parser.add_argument( 72 | '--raw-args', 73 | dest='raw_args', 74 | default='', 75 | help='Argument(s) to pass to database dump command as is' 76 | ) 77 | 78 | def __init__(self, *args, **kwargs): 79 | super(Command, self).__init__(*args, **kwargs) 80 | self.db_name = None 81 | self.compress = None 82 | self.quiet = None 83 | self.debug = None 84 | self.pgpass = None 85 | self.engine = None 86 | self.db = None 87 | self.user = None 88 | self.password = None 89 | self.host = None 90 | self.port = None 91 | self.excluded_tables = None 92 | self.empty_tables = None 93 | self.output_stdout = object() 94 | 95 | def handle(self, *args, **options): 96 | self.db_name = options.get('database_name', 'default') 97 | self.compress = options.get('compression_command') 98 | self.quiet = options.get('quiet') 99 | self.debug = options.get('debug') 100 | self.pgpass = options.get('pgpass') 101 | 102 | if self.db_name not in settings.DATABASES: 103 | raise CommandError('Database %s is not defined in settings.DATABASES' % self.db_name) 104 | 105 | self.engine = settings.DATABASES[self.db_name].get('ENGINE') 106 | self.db = settings.DATABASES[self.db_name].get('NAME') 107 | self.user = settings.DATABASES[self.db_name].get('USER') 108 | self.password = settings.DATABASES[self.db_name].get('PASSWORD') 109 | self.host = settings.DATABASES[self.db_name].get('HOST') 110 | self.port = settings.DATABASES[self.db_name].get('PORT') 111 | self.excluded_tables = settings.DATABASES[self.db_name].get('DB_DUMP_EXCLUDED_TABLES', []) 112 | self.empty_tables = settings.DATABASES[self.db_name].get('DB_DUMP_EMPTY_TABLES', []) 113 | 114 | backup_directory = options['backup_directory'] 115 | filename = options['filename'] 116 | 117 | if not os.path.exists(backup_directory): 118 | os.makedirs(backup_directory) 119 | 120 | if not filename: 121 | outfile = self.destination_filename(backup_directory, self.db) 122 | elif filename == "-": 123 | outfile = self.output_stdout 124 | self.quiet = True 125 | else: 126 | outfile = os.path.join(backup_directory, filename) 127 | 128 | raw_args = options['raw_args'] 129 | 130 | if 'mysql' in self.engine: 131 | self.do_mysql_backup(outfile, raw_args=raw_args) 132 | elif 'postgresql' in self.engine: 133 | self.do_postgresql_backup(outfile, raw_args=raw_args) 134 | else: 135 | raise CommandError('Backups of %s engine are not implemented.' % self.engine) 136 | 137 | if self.compress: 138 | self.run_command('%s %s' % (self.compress, outfile)) 139 | 140 | @classmethod 141 | def destination_filename(cls, backup_directory, database_name): 142 | return os.path.join(backup_directory, '%s_backup_%s.sql' % (database_name, time.strftime('%Y%m%d-%H%M%S'))) 143 | 144 | def do_mysql_backup(self, outfile, raw_args=''): 145 | if not self.quiet: 146 | print('Doing MySQL backup of database "%s" into %s' % (self.db, outfile)) 147 | 148 | main_args = [] 149 | 150 | if self.user: 151 | main_args.append('--user=%s' % self.user) 152 | 153 | if self.password: 154 | main_args.append('--password=%s' % self.password) 155 | 156 | if self.host: 157 | main_args.append('--host=%s' % self.host) 158 | 159 | if self.port: 160 | main_args.append('--port=%s' % self.port) 161 | 162 | if raw_args: 163 | main_args.append(raw_args) 164 | 165 | excluded_args = main_args[:] 166 | 167 | if self.excluded_tables or self.empty_tables: 168 | excluded_args += ['--ignore-table=%s.%s' % (self.db, excluded_table) 169 | for excluded_table in self.excluded_tables + self.empty_tables] 170 | 171 | command = 'mysqldump %s' % (' '.join(excluded_args + [self.db])) 172 | 173 | if outfile != self.output_stdout: 174 | command += " > %s" % outfile 175 | 176 | self.run_command(command) 177 | 178 | if self.empty_tables: 179 | no_data_args = main_args[:] + ['--no-data', self.db] 180 | no_data_args += [empty_table for empty_table in self.empty_tables] 181 | 182 | command = 'mysqldump %s' % (' '.join(no_data_args)) 183 | 184 | if outfile != self.output_stdout: 185 | command += " >> %s" % outfile 186 | 187 | self.run_command(command) 188 | 189 | def run_command(self, command): 190 | if self.debug: 191 | print(command) 192 | 193 | os.system(command) 194 | 195 | def do_postgresql_backup(self, outfile, raw_args=''): 196 | if not self.quiet: 197 | print('Doing PostgreSQL backup of database "%s" into %s' % (self.db, outfile)) 198 | 199 | main_args = [] 200 | 201 | if self.user: 202 | main_args.append('--username=%s' % self.user) 203 | 204 | if self.password and not self.pgpass: 205 | main_args.append('--password') 206 | 207 | if self.host: 208 | main_args.append('--host=%s' % self.host) 209 | 210 | if self.port: 211 | main_args.append('--port=%s' % self.port) 212 | 213 | if raw_args: 214 | main_args.append(raw_args) 215 | 216 | excluded_args = main_args[:] 217 | 218 | if self.excluded_tables or self.empty_tables: 219 | excluded_args += ['--exclude-table=%s' % excluded_table 220 | for excluded_table in self.excluded_tables + self.empty_tables] 221 | 222 | command = 'pg_dump %s %s' % (' '.join(excluded_args), self.db) 223 | 224 | if outfile != self.output_stdout: 225 | command += ' > %s' % outfile 226 | 227 | self.run_postgresql_command(command, outfile) 228 | 229 | if self.empty_tables: 230 | no_data_args = main_args[:] + ['--schema-only'] 231 | no_data_args += ['--table=%s' % empty_table for empty_table in self.empty_tables] 232 | no_data_args += [self.db] 233 | 234 | command = 'pg_dump %s %s' % (' '.join(no_data_args), self.db) 235 | 236 | if outfile != self.output_stdout: 237 | command += ' >> %s' % outfile 238 | 239 | self.run_postgresql_command(command, outfile) 240 | 241 | def run_postgresql_command(self, command, outfile): 242 | if self.debug: 243 | print(command) 244 | 245 | if outfile == self.output_stdout: 246 | kwargs = {'stdout': sys.stdout, 'stderr': sys.stderr} 247 | else: 248 | kwargs = {} 249 | 250 | process = subprocess.Popen( 251 | command, shell=True, 252 | stdin=subprocess.PIPE, **kwargs) 253 | 254 | process.wait() 255 | 256 | if self.password: 257 | process.stdin.write('%s\n' % self.password) 258 | process.stdin.close() 259 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | 6 | try: 7 | from setuptools import setup, find_packages 8 | except ImportError: 9 | import ez_setup 10 | ez_setup.use_setuptools() 11 | from setuptools import setup, find_packages 12 | 13 | def read(fname): 14 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 15 | 16 | setup( 17 | name = 'django-dbdump', 18 | version = '1.2', 19 | url = 'https://github.com/vitaliyf/django-dbdump/', 20 | download_url = 'https://github.com/vitaliyf/django-dbdump/', 21 | license = 'BSD', 22 | description = 'Database backup management command.', 23 | long_description=read('README.rst'), 24 | author = 'Vitaliy Fuks', 25 | author_email = 'vitaliyf@gmail.com', 26 | packages = find_packages(), 27 | include_package_data = True, 28 | platforms='any', 29 | classifiers = [ 30 | 'Framework :: Django', 31 | 'Intended Audience :: Developers', 32 | 'Intended Audience :: System Administrators', 33 | 'License :: OSI Approved :: BSD License', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | ], 37 | install_requires=[ 38 | 'Django>=1.8', 39 | ], 40 | ) 41 | --------------------------------------------------------------------------------