Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 8 additions & 0 deletions .env-example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# env variables in dbbackup/tests/settings.py
DB_ENGINE
DB_NAME
DB_USER
DB_PASSWORD
DB_HOST
CONNECTOR
DJANGO_LOG_LEVEL=DEBUG
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ __pycache__/

# Distribution / packaging
.Python
.env
env/
build/
develop-eggs/
Expand Down Expand Up @@ -62,3 +63,4 @@ target/
.idea/
*.sw[po]
test-sqlite
venv
5 changes: 0 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
language: python

python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
Expand Down Expand Up @@ -71,10 +70,6 @@ matrix:
env: DJANGO=3.0
- python: "pypy"
env: DJANGO=master
- python: "3.5"
env: DJANGO=3.0
- python: "3.5"
env: DJANGO=master
allow_failures:
- python: "nightly"
- env: DJANGO=master
Expand Down
7 changes: 7 additions & 0 deletions dbbackup/db/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
from tempfile import SpooledTemporaryFile
from subprocess import Popen
from importlib import import_module
import logging

from dbbackup import settings, utils
from . import exceptions

logger = logging.getLogger('dbbackup.command')
logger.setLevel(logging.DEBUG)


CONNECTOR_MAPPING = {
'django.db.backends.sqlite3': 'dbbackup.db.sqlite.SqliteConnector',
'django.db.backends.mysql': 'dbbackup.db.mysql.MysqlDumpConnector',
Expand Down Expand Up @@ -128,6 +134,7 @@ def run_command(self, command, stdin=None, env=None):
:return: Standard output of command
:rtype: file
"""
logger.debug(command)
cmd = shlex.split(command)
stdout = SpooledTemporaryFile(max_size=settings.TMP_FILE_MAX_SIZE,
dir=settings.TMP_DIR)
Expand Down
2 changes: 2 additions & 0 deletions dbbackup/db/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Exceptions for database connectors."""


class ConnectorError(Exception):
"""Base connector error"""

Expand Down
67 changes: 36 additions & 31 deletions dbbackup/db/postgresql.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
from urllib.parse import quote
import logging

from dbbackup import utils
from .base import BaseCommandDBConnector
from .exceptions import DumpError

logger = logging.getLogger('dbbackup.command')


def create_postgres_uri(self):
host = self.settings.get('HOST')
if not host:
raise DumpError('A host name is required')

dbname = self.settings.get('NAME') or ''
user = quote(self.settings.get('USER') or '')
password = self.settings.get('PASSWORD') or ''
password = ':{}'.format(quote(password)) if password else ''
if not user:
password = ''
else:
host = '@' + host

port = ':{}'.format(self.settings.get('PORT')) if self.settings.get('PORT') else ''
dbname = f'--dbname=postgresql://{user}{password}{host}{port}/{dbname}'
return dbname


class PgDumpConnector(BaseCommandDBConnector):
Expand All @@ -22,31 +47,21 @@ def run_command(self, *args, **kwargs):

def _create_dump(self):
cmd = '{} '.format(self.dump_cmd)
if self.settings.get('HOST'):
cmd += ' --host={}'.format(self.settings['HOST'])
if self.settings.get('PORT'):
cmd += ' --port={}'.format(self.settings['PORT'])
if self.settings.get('USER'):
cmd += ' --username={}'.format(self.settings['USER'])
cmd += ' --no-password'
cmd = cmd + create_postgres_uri(self)

for table in self.exclude:
cmd += ' --exclude-table={}'.format(table)
if self.drop:
cmd += ' --clean'
cmd += ' {}'.format(self.settings['NAME'])

cmd = '{} {} {}'.format(self.dump_prefix, cmd, self.dump_suffix)
stdout, stderr = self.run_command(cmd, env=self.dump_env)
return stdout

def _restore_dump(self, dump):
cmd = '{} '.format(self.restore_cmd)
if self.settings.get('HOST'):
cmd += ' --host={}'.format(self.settings['HOST'])
if self.settings.get('PORT'):
cmd += ' --port={}'.format(self.settings['PORT'])
if self.settings.get('USER'):
cmd += ' --username={}'.format(self.settings['USER'])
cmd += ' --no-password'
cmd = cmd + create_postgres_uri(self)

# without this, psql terminates with an exit value of 0 regardless of errors
cmd += ' --set ON_ERROR_STOP=on'
if self.single_transaction:
Expand Down Expand Up @@ -93,14 +108,9 @@ class PgDumpBinaryConnector(PgDumpConnector):
drop = True

def _create_dump(self):
cmd = '{} {}'.format(self.dump_cmd, self.settings['NAME'])
if self.settings.get('HOST'):
cmd += ' --host={}'.format(self.settings['HOST'])
if self.settings.get('PORT'):
cmd += ' --port={}'.format(self.settings['PORT'])
if self.settings.get('USER'):
cmd += ' --user={}'.format(self.settings['USER'])
cmd += ' --no-password'
cmd = '{} '.format(self.dump_cmd)
cmd = cmd + create_postgres_uri(self)

cmd += ' --format=custom'
for table in self.exclude:
cmd += ' --exclude-table={}'.format(table)
Expand All @@ -109,14 +119,9 @@ def _create_dump(self):
return stdout

def _restore_dump(self, dump):
cmd = '{} --dbname={}'.format(self.restore_cmd, self.settings['NAME'])
if self.settings.get('HOST'):
cmd += ' --host={}'.format(self.settings['HOST'])
if self.settings.get('PORT'):
cmd += ' --port={}'.format(self.settings['PORT'])
if self.settings.get('USER'):
cmd += ' --user={}'.format(self.settings['USER'])
cmd += ' --no-password'
dbname = create_postgres_uri(self)
cmd = '{} {}'.format(self.restore_cmd, dbname)

if self.single_transaction:
cmd += ' --single-transaction'
if self.drop:
Expand Down
54 changes: 47 additions & 7 deletions dbbackup/tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
"""
import os
import tempfile
import sys
from dotenv import load_dotenv

test = len(sys.argv) <= 1 or sys.argv[1] == 'test'
if not test:
load_dotenv()

DEBUG = False

Expand All @@ -24,13 +30,15 @@
'dbbackup.tests.testapp',
)

DATABASES = {'default': {
"ENGINE": os.environ.get('DB_ENGINE', "django.db.backends.sqlite3"),
"NAME": os.environ.get('DB_NAME', ":memory:"),
"USER": os.environ.get('DB_USER'),
"PASSWORD": os.environ.get('DB_PASSWORD'),
"HOST": os.environ.get('DB_HOST'),
}}
DATABASES = {
'default': {
"ENGINE": os.environ.get('DB_ENGINE', "django.db.backends.sqlite3"),
"NAME": os.environ.get('DB_NAME', ":memory:"),
"USER": os.environ.get('DB_USER'),
"PASSWORD": os.environ.get('DB_PASSWORD'),
"HOST": os.environ.get('DB_HOST'),
}
}
if os.environ.get('CONNECTOR'):
CONNECTOR = {'CONNECTOR': os.environ['CONNECTOR']}
DBBACKUP_CONNECTORS = {'default': CONNECTOR}
Expand All @@ -50,3 +58,35 @@
DBBACKUP_STORAGE_OPTIONS = dict([keyvalue.split('=') for keyvalue in
os.environ.get('STORAGE_OPTIONS', '').split(',')
if keyvalue])

LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'handlers': ['console'],
'level': 'DEBUG'
},
'handlers': {
'console': {
'level': os.getenv('DJANGO_LOG_LEVEL', 'INFO'),
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'formatters': {
'verbose': {
'format': "[%(asctime)s] %(levelname)s [%(name)s:%(lineno)s] %(message)s",
'datefmt': "%d/%b/%Y %H:%M:%S"
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
'loggers': {
'django.db.backends': {
# uncomment to see all queries
# 'level': 'DEBUG',
'handlers': ['console'],
}
}
}
Loading