Skip to content

Instantly share code, notes, and snippets.

@NanoDano
Last active August 11, 2020 05:13
Show Gist options
  • Save NanoDano/57fb9674b4b9d16598521211ff7e5351 to your computer and use it in GitHub Desktop.
Save NanoDano/57fb9674b4b9d16598521211ff7e5351 to your computer and use it in GitHub Desktop.
Django management command to database backup, upload over FTP, and rotate files
# appname/management/commands/django_backup_db.py
# Then run with manage.py django_backup_db
from ftplib import FTP_TLS
from glob import glob
from os import system, remove
from os.path import join, getmtime, exists
from socket import gethostname
from django.core.management import BaseCommand, call_command
from django.utils.datetime_safe import datetime
from MyApp.settings import DB_BACKUP_LOCAL_DIR, DB_MAX_NUM_LOCAL_BACKUPS, DB_BACKUP_FTP_HOST, DB_BACKUP_FTP_USER, \
DB_BACKUP_FTP_PASS
class Command(BaseCommand):
def __init__(self):
super().__init__()
self.json_filename = f'{datetime.today().strftime("%Y-%m-%d")}-cathydb-{gethostname()}.json'
self.full_json_filepath = join(DB_BACKUP_LOCAL_DIR, self.json_filename)
def handle(self, *args, **kwargs):
"""
Dumps database using Django's dumpdata in JSON format.
Zips file and uploads it over FTP for unlimited/permanent bakcup.
Rotates backups so it only keeps X number of backups before rotating.
:return:
"""
self.stdout.write(self.style.SUCCESS(f'Backing up database. {self.full_json_filepath}.gz'))
if exists(self.full_json_filepath) or exists(f'{self.full_json_filepath}.gz'):
self.stdout.write(f'File already exists. Aborting database backup.')
exit(1)
self.dump_database()
self.zip_dump()
self.rotate_backups()
self.upload_over_ftp()
def dump_database(self):
# call_command('dumpdata', exclude=['chatbot']) # To exclude chat messages
self.stdout.write(self.style.SUCCESS(f'Dumping database to {self.full_json_filepath}'))
call_command('dumpdata', format='json', output=self.full_json_filepath)
def zip_dump(self):
self.stdout.write(self.style.SUCCESS(f'Gzipping {self.full_json_filepath}'))
system(f'gzip {self.full_json_filepath}') # turn x.json into x.json.gz
def rotate_backups(self):
"""Delete old backups so it doesn't fill disk space"""
self.stdout.write(self.style.SUCCESS('Rotating backups'))
files = glob(join(DB_BACKUP_LOCAL_DIR, "*.gz"))
# Oldest files are at the front. Newest files at the end
files.sort(key=getmtime)
self.stdout.write(self.style.SUCCESS(f'Found {len(files)} files: {files}'))
# print(f'Oldest 2 files: {files[:2]}')
# print(f'Newest 2 files: {files[-2:]}')
backup_overflow_count = len(files) - DB_MAX_NUM_LOCAL_BACKUPS
self.stdout.write(self.style.SUCCESS(f'Found {backup_overflow_count} too many backups.'))
if backup_overflow_count > 0:
for file in files[0:backup_overflow_count]:
self.stdout.write(self.style.SUCCESS(f'Will remove: {file}'))
remove(file)
else:
self.stdout.write(self.style.SUCCESS(f'Backup limit not reached: {backup_overflow_count}. Not deleting anything.'))
def upload_over_ftp(self):
self.stdout.write(self.style.SUCCESS(f'Uploading {self.full_json_filepath}.gz over FTP'))
with FTP_TLS(DB_BACKUP_FTP_HOST, DB_BACKUP_FTP_USER, DB_BACKUP_FTP_PASS) as ftp:
with open(f'{self.full_json_filepath}.gz', 'rb') as f:
ftp.storbinary(f'STOR {self.json_filename}.gz', f)
self.stdout.write(self.style.SUCCESS('Done uploading over FTP'))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment