Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple backup storage backends #235 #357

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ __pycache__/
# Distribution / packaging
.Python
env/
venv/
build/
develop-eggs/
dist/
Expand Down Expand Up @@ -44,6 +45,7 @@ coverage.xml
*,cover
tests/media/
coverage_html_report/
file:memorydb_default?mode=memory&cache=shared

# Translations
*.mo
Expand Down
15 changes: 12 additions & 3 deletions dbbackup/management/commands/dbbackup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
from __future__ import (absolute_import, division,
print_function, unicode_literals)

from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import CommandError

from ._base import BaseDbBackupCommand, make_option
from ...db.base import get_connector
from ...storage import get_storage, StorageError
from ...storage import get_storage, get_backup_storage, StorageError
from ... import utils, settings


Expand All @@ -32,7 +33,9 @@ class Command(BaseDbBackupCommand):
make_option("-o", "--output-filename", default=None,
help="Specify filename on storage"),
make_option("-O", "--output-path", default=None,
help="Specify where to store on local filesystem")
help="Specify where to store on local filesystem"),
make_option("--storage", default=None,
help="Specify storage from DBACKUP_STORAGES to use"),
)

@utils.email_uncaught_exception
Expand All @@ -47,9 +50,15 @@ def handle(self, **options):
self.compress = options.get('compress')
self.encrypt = options.get('encrypt')

self.db_storage = options.get('storage')

self.filename = options.get('output_filename')
self.path = options.get('output_path')
self.storage = get_storage()

if self.db_storage:
self.storage = get_backup_storage(self.db_storage)
else:
self.storage = get_storage()

self.database = options.get('database') or ''
database_keys = self.database.split(',') or settings.DATABASES
Expand Down
20 changes: 15 additions & 5 deletions dbbackup/management/commands/dbrestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
print_function, unicode_literals)

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.management.base import CommandError

from django.db import connection

from ._base import BaseDbBackupCommand, make_option
from ... import utils
from ...db.base import get_connector
from ...storage import get_storage, StorageError
from ...storage import get_storage, get_backup_storage, StorageError


class Command(BaseDbBackupCommand):
Expand All @@ -30,7 +32,9 @@ class Command(BaseDbBackupCommand):
help="Decrypt data before restoring"),
make_option("-p", "--passphrase", help="Passphrase for decrypt file", default=None),
make_option("-z", "--uncompress", action='store_true', default=False,
help="Uncompress gzip data before restoring")
help="Uncompress gzip data before restoring"),
make_option("--storage", default=None,
help="Specify storage from DBACKUP_STORAGES to use"),
)

def handle(self, *args, **options):
Expand All @@ -48,8 +52,14 @@ def handle(self, *args, **options):
self.uncompress = options.get('uncompress')
self.passphrase = options.get('passphrase')
self.interactive = options.get('interactive')
self.db_storage = options.get('storage')
self.database_name, self.database = self._get_database(options)
self.storage = get_storage()

if self.db_storage:
self.storage = get_backup_storage(self.db_storage)
else:
self.storage = get_storage()

self._restore_backup()
except StorageError as err:
raise CommandError(err)
Expand All @@ -59,8 +69,8 @@ def _get_database(self, options):
database_name = options.get('database')
if not database_name:
if len(settings.DATABASES) > 1:
errmsg = "Because this project contains more than one database, you"\
" must specify the --database option."
errmsg = "Because this project contains more than one database, you" \
" must specify the --database option."
raise CommandError(errmsg)
database_name = list(settings.DATABASES.keys())[0]
if database_name not in settings.DATABASES:
Expand Down
5 changes: 5 additions & 0 deletions dbbackup/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@

STORAGE = getattr(settings, 'DBBACKUP_STORAGE', 'django.core.files.storage.FileSystemStorage')
STORAGE_OPTIONS = getattr(settings, 'DBBACKUP_STORAGE_OPTIONS', {})
STORAGES = getattr(settings, 'DBBACKUP_STORAGES', {
'default': {
'storage': 'django.core.files.storage.FileSystemStorage'
}
})

CONNECTORS = getattr(settings, 'DBBACKUP_CONNECTORS', {})

Expand Down
14 changes: 14 additions & 0 deletions dbbackup/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ def get_storage(path=None, options=None):
return Storage(path, **options)


def get_backup_storage(db_storage):
if db_storage not in settings.STORAGES:
raise ImproperlyConfigured('You must specify a storage class using '
'DBBACKUP_STORAGES settings.')
storage_options = settings.STORAGES[db_storage]
if 'storage' in storage_options:
storage = storage_options.pop('storage', None)
options = storage_options
return get_storage(path=storage, options=options)
raise ImproperlyConfigured('You must specify a storage class "storage" using '
'DBBACKUP_STORAGES settings.')


class StorageError(Exception):
pass

Expand All @@ -44,6 +57,7 @@ class Storage(object):
list and filter files. It uses a Django storage object for low-level
operations.
"""

@property
def logger(self):
if not hasattr(self, '_logger'):
Expand Down
15 changes: 15 additions & 0 deletions dbbackup/tests/commands/test_dbbackup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
Tests for dbbackup command.
"""
import os

from django.core.exceptions import ImproperlyConfigured
from six import StringIO
from django.core.management import execute_from_command_line

from mock import patch

from django.test import TestCase
Expand Down Expand Up @@ -50,6 +55,16 @@ def test_path(self):
# tearDown
os.remove(self.command.path)

def test_fallback(self):
stdout = StringIO()
with self.assertRaises(ImproperlyConfigured) as ic:
with patch('sys.stdout', stdout):
execute_from_command_line(['', 'dbbackup', '--storage=s3'])
self.assertEqual(str(ic.exception),
'You must specify a storage class using DBBACKUP_STORAGES settings.')

# TODO: Update DBBACKUP_FALLBACK_STORAGE and verify successful backup.


@patch('dbbackup.settings.GPG_RECIPIENT', 'test@test')
@patch('sys.stdout', DEV_NULL)
Expand Down
13 changes: 13 additions & 0 deletions dbbackup/tests/commands/test_dbrestore.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Tests for dbrestore command.
"""
from django.core.exceptions import ImproperlyConfigured
from django.core.management import execute_from_command_line
from mock import patch
from tempfile import mktemp
from shutil import copyfileobj
Expand All @@ -9,6 +11,7 @@
from django.core.management.base import CommandError
from django.core.files import File
from django.conf import settings
from six import StringIO

from dbbackup import utils
from dbbackup.db.base import get_connector
Expand Down Expand Up @@ -90,6 +93,16 @@ def test_path(self, *args):
)
self.command._restore_backup()

def test_fallback(self, *args):
stdout = StringIO()
with self.assertRaises(ImproperlyConfigured) as ic:
with patch('sys.stdout', stdout):
execute_from_command_line(['', 'dbrestore', '--storage=s3'])
self.assertEqual(str(ic.exception),
'You must specify a storage class using DBBACKUP_STORAGES settings.')

# TODO: Update DBBACKUP_FALLBACK_STORAGE and verify successful restore.


class DbrestoreCommandGetDatabaseTest(TestCase):
def setUp(self):
Expand Down