├── .gitignore ├── .travis.yml ├── MANIFEST.in ├── README.mdown ├── icybackup ├── __init__.py ├── admin.py ├── components │ ├── __init__.py │ ├── db.py │ └── glacier.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── backup.py │ │ └── restore.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto__add_glacierinventory.py │ ├── 0003_auto__add_field_glacierinventory_requested_date.py │ ├── 0004_auto__chg_field_glacierbackup_date.py │ └── __init__.py └── models.py ├── setup.py └── test_django_project ├── icytests ├── __init__.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── icytests.py ├── models.py └── views.py ├── manage.py ├── media └── test_image.jpg └── test_django_project ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | test_django_project/database.sqlite3 2 | test_django_project/backup.tgz -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '2.7' 3 | install: 4 | - 'pip install -q django psycopg2 mysql-python --use-mirrors' 5 | - 'pip install -q -e . --use-mirrors' 6 | before_script: 7 | - mysql -e 'create database app_test' 8 | - psql -c 'create database app_test' -U postgres 9 | - cd test_django_project 10 | - python manage.py syncdb --noinput 11 | - python manage.py syncdb --noinput --database=postgres 12 | - python manage.py syncdb --noinput --database=mysql 13 | script: python manage.py icytests -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | prune test_django_project -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | **This project is not actively maintained.** The last commit (other than the commit adding this message) was on 1 March, 2014. The source code and PyPI packages are still available, but I strongly recommend against using this for new projects. 2 | 3 | --- 4 | 5 | # django-icybackup 6 | 7 | Back up your databases and media directory to a local file/directory or Amazon Glacier. Works with PostgreSQL, MySQL and sqlite. 8 | 9 | A fork of (a fork of) django-backup which adds: 10 | 11 | - Multiple database support 12 | - Optionally save your backups to Amazon Glacier 13 | - Less assumptions about presence of a Bourne-like shell and GNU tools 14 | - Less tarballception (no more redundant tarballs inside tarballs) 15 | - Postgres-specific improvements: 16 | - Uses the custom archive type for `pg_dump`, for maximum flexibility at restore time, so there's no need to worry about having to hand-edit or craft cryptic regexes for a many-thousand-line SQL file when restoring just because the wrong command line flags were used when making the backup. 17 | - Doesn't pass PostgreSQL database password via command line argument, as doing so can reveal your database password to other users on the same machine. 18 | 19 | **Warning to MySQL Users:** This script may expose your MySQL database password to other users on the same machine, as it is passed in via a command line argument. Pull requests to fix this problem are welcome. 20 | 21 | ## Installation 22 | 23 | Run `pip install django-icybackup`, then add `icybackup` to your `INSTALLED_APPS`. 24 | 25 | ## Usage 26 | 27 | ### Backing up 28 | 29 | - `manage.py backup -o backup.tgz` - back up to `backup.tgz` 30 | - `manage.py backup -d backups` - back up to `backups/[CURRENT_DATETIME].tgz` 31 | - `manage.py backup -g arn:was:glacier:us-east-1:2584098540980:vaults/my-website-backups` - backup to the `my-website-backups` Amazon Glacier vault, given by it's ARN 32 | - `manage.py backup --stdout` - back up to stdout 33 | 34 | Supports the following optional flags: 35 | 36 | - `-e extra_file` - Add an extra file or directory to the backup (can be specified multiple times) 37 | 38 | ### Restoring 39 | 40 | - `manage.py restore -i backup.tgz` - restore from `backup.tgz` 41 | - `manage.py restore --stdin` - restore from stdin 42 | 43 | Optional flags: 44 | 45 | - `--pg-restore-flags` - flags to pass to the `pg_restore` process (default is `Oxc`). If the database you're restoring into exists and is empty, use `--pg-restore-flags=Ox`. 46 | 47 | Tip: If you want to move servers, or copy your production database/media exactly to development or staging, it is possible to do something like this: 48 | 49 | ssh other-server 'cd path/to/app && manage.py backup --stdout' | manage.py restore --stdin 50 | 51 | ### `settings.py` settings 52 | 53 | - `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` - Optional, but required for Amazon Glacier upload to work. 54 | - `PG_DUMP_BIN`, `PG_RESTORE_BIN`, `MYSQLDUMP_BIN`, `MYSQL_BIN` - Optional. Set these to the full paths to the `pg_dump` and `pg_restore`, `mysqldump` and `mysql` binaries. If not set, icybackup will try to find them on the search path. (Note that cron jobs often run with a much smaller search path, so it's a good idea to set these if you're backing up using cron.) 55 | 56 | ## To do 57 | 58 | - Backup directory cleanup command 59 | - Tests 60 | - remove password from `mysqldump` process name 61 | - Restore command 62 | - Commands to reconcile and prune Glacier backups 63 | 64 | ## Contributors 65 | 66 | The following people contributed code to this project or its ancestors, in chronological order by first commit: 67 | 68 | - Dmitriy Kovalev 69 | - Ted Tieken 70 | - Chris Cohoat 71 | - Jamie Matthews 72 | - Yar Kravtsov 73 | - Adam Brenecki, St Barnabas' Theological College 74 | 75 | ## License 76 | 77 | Copyright © 2012, St Barnabas' Theological College 78 | Copyright © 2011, Ted Tieken 79 | Copyright © 2011, http://code.google.com/p/django-backup/ 80 | All rights reserved. 81 | 82 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 83 | 84 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 85 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 86 | 87 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 88 | -------------------------------------------------------------------------------- /icybackup/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.1" -------------------------------------------------------------------------------- /icybackup/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import GlacierBackup 3 | 4 | class GlacierBackupAdmin (admin.ModelAdmin): 5 | list_display = ('date', 'glacier_id') 6 | readonly_fields = ('glacier_id', 'date') 7 | 8 | def has_add_permission(self, request): 9 | return False 10 | 11 | def has_delete_permission(self, request, obj=None): 12 | return False 13 | 14 | admin.site.register(GlacierBackup, GlacierBackupAdmin) -------------------------------------------------------------------------------- /icybackup/components/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stbarnabas/django-icybackup/a5fbd4b6dd064f3a7e2166a38b9a3138f0a8cdc9/icybackup/components/__init__.py -------------------------------------------------------------------------------- /icybackup/components/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.management.base import CommandError 3 | from tempfile import mkstemp 4 | from subprocess import check_call 5 | from shutil import copy 6 | from django.conf import settings 7 | 8 | BACKUP = 1 9 | RESTORE = 2 10 | 11 | MYSQL_BIN = getattr(settings, 'MYSQL_BIN', 'mysql') 12 | MYSQLDUMP_BIN = getattr(settings, 'MYSQLDUMP_BIN', 'mysqldump') 13 | PG_DUMP_BIN = getattr(settings, 'PG_DUMP_BIN', 'pg_dump') 14 | PG_RESTORE_BIN = getattr(settings, 'PG_RESTORE_BIN', 'pg_restore') 15 | 16 | def _database_dict_from_settings(settings): 17 | if hasattr(settings, 'DATABASES'): 18 | database_list = settings.DATABASES 19 | else: 20 | # database details are in the old format, so convert to the new one 21 | database_list = { 22 | 'default': { 23 | 'ENGINE': settings.DATABASE_ENGINE, 24 | 'NAME': settings.DATABASE_NAME, 25 | 'USER': settings.DATABASE_USER, 26 | 'PASSWORD': settings.DATABASE_PASSWORD, 27 | 'HOST': settings.DATABASE_HOST, 28 | 'PORT': settings.DATABASE_PORT, 29 | } 30 | } 31 | return database_list 32 | 33 | def backup_to(settings, dir, **kwargs): 34 | for name, database in _database_dict_from_settings(settings).iteritems(): 35 | do(BACKUP, database, os.path.join(dir, name), **kwargs) 36 | 37 | def restore_from(settings, dir, **kwargs): 38 | for name, database in _database_dict_from_settings(settings).iteritems(): 39 | do(RESTORE, database, os.path.join(dir, name), **kwargs) 40 | 41 | def do(action, database, f, **kwargs): 42 | engine = database['ENGINE'] 43 | if 'mysql' in engine: 44 | __mysql(action, database, f) 45 | elif 'postgresql' in engine or 'postgis' in engine: 46 | if 'postgres_flags' not in kwargs: 47 | __postgresql(action, database, f) 48 | else: 49 | __postgresql(action, database, f, **kwargs) 50 | elif 'sqlite3' in engine: 51 | __sqlite(action, database, f) 52 | else: 53 | raise CommandError('{} in {} engine not implemented'.format('Backup' if action == BACKUP else 'Restore', engine)) 54 | 55 | def __sqlite(action, database, f): 56 | if action == BACKUP: 57 | copy(database['NAME'], f) 58 | elif action == RESTORE: 59 | copy(f, database['NAME']) 60 | 61 | def __mysql(action, database, f): 62 | if action == BACKUP: 63 | command = [MYSQLDUMP_BIN] 64 | elif action == RESTORE: 65 | command = [MYSQL_BIN] 66 | 67 | if 'USER' in database: 68 | command += ["--user=%s" % database['USER']] 69 | if 'PASSWORD' in database: 70 | command += ["--password=%s" % database['PASSWORD']] 71 | if 'HOST' in database: 72 | command += ["--host=%s" % database['HOST']] 73 | if 'PORT' in database: 74 | command += ["--port=%s" % database['PORT']] 75 | command += [database['NAME']] 76 | 77 | if action == BACKUP: 78 | with open(f, 'w') as fo: 79 | check_call(command, stdout=fo) 80 | elif action == RESTORE: 81 | with open(f, 'r') as fo: 82 | check_call(command, stdin=fo) 83 | 84 | def __postgresql(action, database, f, **kwargs): 85 | if action == BACKUP: 86 | command = [PG_DUMP_BIN, '--format=c'] 87 | elif action == RESTORE: 88 | command = [PG_RESTORE_BIN, '-{}'.format(kwargs.get('postgres_flags', 'Oxc'))] 89 | 90 | if 'USER' in database and database['USER']: 91 | command.append("--username={}".format(database['USER'])) 92 | if 'HOST' in database and database['HOST']: 93 | command.append("--host={}".format(database['HOST'])) 94 | if 'PORT' in database and database['PORT']: 95 | command.append("--port={}".format(database['PORT'])) 96 | if 'NAME' in database and database['NAME']: 97 | if action == RESTORE: 98 | command.append('--dbname={}'.format(database['NAME'])) 99 | else: 100 | command.append(database['NAME']) 101 | 102 | if 'PASSWORD' in database and database['PASSWORD']: 103 | # create a pgpass file that always returns the same password, as a secure temp file 104 | password_fd, password_path = mkstemp() 105 | password_file = os.fdopen(password_fd, 'w') 106 | password_file.write('*:*:*:*:{}'.format(database['PASSWORD'])) 107 | password_file.close() 108 | os.environ['PGPASSFILE'] = password_path 109 | else: 110 | command.append('-w') 111 | 112 | if action == BACKUP: 113 | with open(f, 'w') as fo: 114 | check_call(command, stdout=fo) 115 | elif action == RESTORE: 116 | with open(f, 'r') as fo: 117 | check_call(command, stdin=fo) 118 | 119 | # clean up 120 | if 'PASSWORD' in database and database['PASSWORD']: 121 | os.remove(password_path) 122 | -------------------------------------------------------------------------------- /icybackup/components/glacier.py: -------------------------------------------------------------------------------- 1 | from .. import models 2 | from datetime import timedelta, datetime 3 | from boto.glacier.layer2 import Layer2 as Glacier 4 | from dateutil.parser import parse 5 | from django.core.management import CommandError 6 | 7 | # upload to amazon glacier 8 | 9 | def _get_vault_from_arn(arn, settings): 10 | g = Glacier(aws_access_key_id=settings.AWS_ACCESS_KEY_ID, aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY) 11 | for i in g.list_vaults(): 12 | if arn == i.arn: 13 | return i 14 | else: 15 | raise CommandError('The specified vault could not be accessed.') 16 | 17 | def upload(arn, output_file, settings): 18 | vault = _get_vault_from_arn(arn, settings) 19 | id = vault.upload_archive(output_file) 20 | 21 | # record backup internally 22 | # we don't need this record in order to restore from backup (obviously!) 23 | # but it makes pruning the backup set easier, and amazon reccomends it 24 | record = models.GlacierBackup.objects.create(glacier_id=id, date=datetime.now()) 25 | record.save() 26 | 27 | def reconcile(arn, settings): 28 | vault = _get_vault_from_arn(arn, settings) 29 | 30 | # check any inventory requests that have not been collected, 31 | # and if they are finished, collect them 32 | to_be_collected = models.GlacierInventory.objects.filter(collected_date=None) 33 | if len(to_be_collected) > 0: 34 | for record in to_be_collected: 35 | job = vault.get_job(record.inventory_id) 36 | if job.completed: 37 | print "Reconciling inventory", record.inventory_id 38 | _do_reconcile(job.get_output()) 39 | record.collected_date = datetime.now() 40 | record.save() 41 | 42 | # if there are no collected inventories in the last 14 days, 43 | # and no requested inventories in the last 3, 44 | # request another inventory 45 | max_requested_date = datetime.now() - timedelta(days=3) 46 | max_collected_date = datetime.now() - timedelta(days=14) 47 | if models.GlacierInventory.objects.exclude(collected_date__lte=max_collected_date).exclude(requested_date__lte=max_requested_date).count() == 0: 48 | job_id = vault.retrieve_inventory() 49 | record = models.GlacierInventory.objects.create(inventory_id=job_id) 50 | record.save() 51 | 52 | def _do_reconcile(inventory): 53 | for archive in inventory['ArchiveList']: 54 | id = archive['ArchiveId'] 55 | creation_date = parse(archive['CreationDate']) 56 | if not models.GlacierBackup.objects.filter(glacier_id=id).exists(): 57 | models.GlacierBackup.objects.create(glacier_id=id, date=creation_date).save() 58 | 59 | def prune(arn, settings): 60 | vault = _get_vault_from_arn(arn, settings) 61 | keep_all_before = datetime.now() - timedelta(days=31) 62 | keep_daily_before = datetime.now() - timedelta(days=90) 63 | keep_weekly_before = datetime.now() - timedelta(days=365) 64 | oldest_date = models.GlacierBackup.objects.all().order_by('date')[0].date 65 | _do_delete(vault, 1, keep_all_before, keep_daily_before) 66 | _do_delete(vault, 30, keep_daily_before, keep_weekly_before) 67 | _do_delete(vault, 30, keep_weekly_before, oldest_date) 68 | 69 | def _do_delete(vault, day_count, from_date, to_date): 70 | begin_date = from_date 71 | while begin_date >= to_date: 72 | end_date = begin_date - timedelta(days=day_count) 73 | if end_date < to_date: 74 | end_date = to_date 75 | qs = models.GlacierBackup.objects.filter(date__lt=end_date, date__gte=begin_date) 76 | # delete all but the most recent 77 | for record in qs[1:]: 78 | print "Deleting", record.glacier_id 79 | vault.delete(record.glacier_id) 80 | record.delete() 81 | -------------------------------------------------------------------------------- /icybackup/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stbarnabas/django-icybackup/a5fbd4b6dd064f3a7e2166a38b9a3138f0a8cdc9/icybackup/management/__init__.py -------------------------------------------------------------------------------- /icybackup/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stbarnabas/django-icybackup/a5fbd4b6dd064f3a7e2166a38b9a3138f0a8cdc9/icybackup/management/commands/__init__.py -------------------------------------------------------------------------------- /icybackup/management/commands/backup.py: -------------------------------------------------------------------------------- 1 | import os, sys, time, shutil 2 | from optparse import make_option 3 | from tempfile import mkdtemp, NamedTemporaryFile 4 | import tarfile 5 | from django.conf import settings 6 | from django.core.management.base import BaseCommand, CommandError 7 | 8 | from ...components import db, glacier 9 | 10 | # Based on: http://code.google.com/p/django-backup/ 11 | # Based on: http://www.djangosnippets.org/snippets/823/ 12 | # Based on: http://www.yashh.com/blog/2008/sep/05/django-database-backup-view/ 13 | class Command(BaseCommand): 14 | option_list = BaseCommand.option_list + ( 15 | make_option('-o', '--output', default=None, dest='output', 16 | help='Write backup to file'), 17 | make_option('-d', '--outdir', default=None, dest='outdir', 18 | help='Write backup to timestamped file in a directory'), 19 | make_option('-g', '--glacier', default=None, dest='glacier', 20 | help='Upload backup to the Amazon Glacier vault with the given ARN'), 21 | make_option('-O', '--stdout', action='store_true', dest='stdout', 22 | help='Output backup tarball to standard output'), 23 | make_option('--extra', '-e', action='append', default=[], dest='extras', 24 | help='Include extra directories or files in the backup tarball'), 25 | ) 26 | help = "Back up a Django installation (database and media directory)." 27 | 28 | def handle(self, *args, **options): 29 | extras = options.get('extras') 30 | 31 | output_file = options.get('output') 32 | output_dir = options.get('outdir') 33 | glacier_vault = options.get('glacier') 34 | output_to_stdout = options.get('stdout') 35 | output_file_temporary = False 36 | 37 | # glacier backups go to a temporary file 38 | if glacier_vault is not None or output_to_stdout: 39 | output_file_temporary = True 40 | output_file_obj = NamedTemporaryFile(delete=False) 41 | output_file_obj.close() # we'll open it later 42 | output_file = output_file_obj.name 43 | 44 | if output_file is None: 45 | if output_dir is None: 46 | raise CommandError('You must specify an output file') 47 | else: 48 | output_file = os.path.join(output_dir, '{}.tgz'.format(_time())) 49 | 50 | media_root = settings.MEDIA_ROOT 51 | 52 | # Create a temporary directory to perform our backup in 53 | backup_root = mkdtemp() 54 | database_root = os.path.join(backup_root, 'databases') 55 | os.mkdir(database_root) 56 | 57 | # Back up databases 58 | db.backup_to(settings, database_root) 59 | 60 | # create backup gzipped tarball 61 | with tarfile.open(output_file, 'w:gz') as tf: 62 | tf.add(database_root, arcname='backup/databases') 63 | tf.add(media_root, arcname='backup/media') 64 | if len(extras) > 0: 65 | extras_mf = NamedTemporaryFile(delete=False) 66 | for count, extra in enumerate(extras): 67 | tf.add(extra, arcname='backup/extras/{}'.format(count)) 68 | extras_mf.write('{},{}\n'.format(count, extra.replace(',','\\,'))) 69 | extras_mf.close() 70 | tf.add(extras_mf.name, arcname='backup/extras/manifest') 71 | os.unlink(extras_mf.name) 72 | 73 | # upload to glacier 74 | if glacier_vault is not None: 75 | glacier.upload(glacier_vault, output_file, settings) 76 | glacier.reconcile(glacier_vault, settings) 77 | glacier.prune(glacier_vault, settings) 78 | 79 | # output to stdout 80 | if output_to_stdout: 81 | with open(output_file, 'r') as f: 82 | sys.stdout.write(f.read()) 83 | 84 | # clean up 85 | shutil.rmtree(backup_root, ignore_errors=True) 86 | if output_file_temporary: 87 | os.unlink(output_file) 88 | 89 | def _time(): 90 | return time.strftime('%Y%m%d-%H%M%S') 91 | -------------------------------------------------------------------------------- /icybackup/management/commands/restore.py: -------------------------------------------------------------------------------- 1 | import os, sys, time, shutil 2 | from optparse import make_option 3 | from tempfile import mkdtemp, NamedTemporaryFile 4 | import tarfile 5 | from boto.glacier.layer2 import Layer2 as Glacier 6 | from django.conf import settings 7 | from django.core.management.base import BaseCommand, CommandError 8 | from distutils.dir_util import copy_tree 9 | 10 | from ...components import db 11 | 12 | # Based on: http://code.google.com/p/django-backup/ 13 | # Based on: http://www.djangosnippets.org/snippets/823/ 14 | # Based on: http://www.yashh.com/blog/2008/sep/05/django-database-backup-view/ 15 | class Command(BaseCommand): 16 | option_list = BaseCommand.option_list + ( 17 | make_option('-i', '--file', default=None, dest='input', 18 | help='Read backup from file'), 19 | make_option('--pg-restore-flags', default=None, dest='postgres_flags', 20 | help='Flags to pass to pg_restore'), 21 | make_option('-I', '--stdin', action='store_true', dest='stdin', 22 | help='Read backup from standard input'), 23 | ) 24 | help = "Restore a Django installation (database and media directory)." 25 | 26 | def handle(self, *args, **options): 27 | extras = options.get('extras') 28 | 29 | input_file = options.get('input') 30 | input_from_stdin = options.get('stdin') 31 | input_file_temporary = False 32 | 33 | if input_file is None and input_from_stdin is None: 34 | raise CommandError('You must specify an input file') 35 | 36 | media_root = settings.MEDIA_ROOT 37 | 38 | # read from stdin 39 | if input_from_stdin: 40 | input_file_temporary = True 41 | input_file_obj = NamedTemporaryFile(delete=False) 42 | input_file_obj.write(sys.stdin.read()) 43 | input_file_obj.close() 44 | input_file = input_file_obj.name 45 | 46 | # Create a temporary directory to extract our backup to 47 | extract_root = mkdtemp() 48 | backup_root = os.path.join(extract_root, 'backup') 49 | database_root = os.path.join(backup_root, 'databases') 50 | 51 | # extract the gzipped tarball 52 | with tarfile.open(input_file, 'r') as tf: 53 | tf.extractall(extract_root) 54 | 55 | # Restore databases 56 | db_options = {} 57 | if options.get('postgres_flags') is not None: 58 | db_options['postgres_flags'] = options['postgres_flags'] 59 | db.restore_from(settings, database_root, **db_options) 60 | 61 | # Restore media directory 62 | copy_tree(os.path.join(backup_root, 'media'), media_root) 63 | 64 | # clean up 65 | shutil.rmtree(extract_root, ignore_errors=True) 66 | if input_file_temporary: 67 | os.unlink(input_file) 68 | -------------------------------------------------------------------------------- /icybackup/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'GlacierBackup' 12 | db.create_table('icybackup_glacierbackup', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('glacier_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=138)), 15 | ('date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 16 | )) 17 | db.send_create_signal('icybackup', ['GlacierBackup']) 18 | 19 | 20 | def backwards(self, orm): 21 | # Deleting model 'GlacierBackup' 22 | db.delete_table('icybackup_glacierbackup') 23 | 24 | 25 | models = { 26 | 'icybackup.glacierbackup': { 27 | 'Meta': {'object_name': 'GlacierBackup'}, 28 | 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 29 | 'glacier_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '138'}), 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 31 | } 32 | } 33 | 34 | complete_apps = ['icybackup'] -------------------------------------------------------------------------------- /icybackup/migrations/0002_auto__add_glacierinventory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding model 'GlacierInventory' 12 | db.create_table('icybackup_glacierinventory', ( 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 14 | ('inventory_id', self.gf('django.db.models.fields.CharField')(unique=True, max_length=92)), 15 | ('collected_date', self.gf('django.db.models.fields.DateTimeField')(default=None, null=True, blank=True)), 16 | )) 17 | db.send_create_signal('icybackup', ['GlacierInventory']) 18 | 19 | 20 | def backwards(self, orm): 21 | # Deleting model 'GlacierInventory' 22 | db.delete_table('icybackup_glacierinventory') 23 | 24 | 25 | models = { 26 | 'icybackup.glacierbackup': { 27 | 'Meta': {'object_name': 'GlacierBackup'}, 28 | 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 29 | 'glacier_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '138'}), 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 31 | }, 32 | 'icybackup.glacierinventory': { 33 | 'Meta': {'object_name': 'GlacierInventory'}, 34 | 'collected_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), 35 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 36 | 'inventory_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '92'}) 37 | } 38 | } 39 | 40 | complete_apps = ['icybackup'] -------------------------------------------------------------------------------- /icybackup/migrations/0003_auto__add_field_glacierinventory_requested_date.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | # Adding field 'GlacierInventory.requested_date' 12 | db.add_column('icybackup_glacierinventory', 'requested_date', 13 | self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, default=datetime.datetime(2013, 1, 9, 0, 0), blank=True), 14 | keep_default=False) 15 | 16 | 17 | def backwards(self, orm): 18 | # Deleting field 'GlacierInventory.requested_date' 19 | db.delete_column('icybackup_glacierinventory', 'requested_date') 20 | 21 | 22 | models = { 23 | 'icybackup.glacierbackup': { 24 | 'Meta': {'object_name': 'GlacierBackup'}, 25 | 'date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 26 | 'glacier_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '138'}), 27 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 28 | }, 29 | 'icybackup.glacierinventory': { 30 | 'Meta': {'object_name': 'GlacierInventory'}, 31 | 'collected_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), 32 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 33 | 'inventory_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '92'}), 34 | 'requested_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 35 | } 36 | } 37 | 38 | complete_apps = ['icybackup'] -------------------------------------------------------------------------------- /icybackup/migrations/0004_auto__chg_field_glacierbackup_date.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from south.db import db 4 | from south.v2 import SchemaMigration 5 | from django.db import models 6 | 7 | 8 | class Migration(SchemaMigration): 9 | 10 | def forwards(self, orm): 11 | 12 | # Changing field 'GlacierBackup.date' 13 | db.alter_column('icybackup_glacierbackup', 'date', self.gf('django.db.models.fields.DateTimeField')()) 14 | 15 | def backwards(self, orm): 16 | 17 | # Changing field 'GlacierBackup.date' 18 | db.alter_column('icybackup_glacierbackup', 'date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True)) 19 | 20 | models = { 21 | 'icybackup.glacierbackup': { 22 | 'Meta': {'object_name': 'GlacierBackup'}, 23 | 'date': ('django.db.models.fields.DateTimeField', [], {}), 24 | 'glacier_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '138'}), 25 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) 26 | }, 27 | 'icybackup.glacierinventory': { 28 | 'Meta': {'object_name': 'GlacierInventory'}, 29 | 'collected_date': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), 30 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 31 | 'inventory_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '92'}), 32 | 'requested_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) 33 | } 34 | } 35 | 36 | complete_apps = ['icybackup'] -------------------------------------------------------------------------------- /icybackup/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stbarnabas/django-icybackup/a5fbd4b6dd064f3a7e2166a38b9a3138f0a8cdc9/icybackup/migrations/__init__.py -------------------------------------------------------------------------------- /icybackup/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class GlacierBackup (models.Model): 4 | glacier_id = models.CharField(max_length=138, unique=True, verbose_name="Glacier backup ID") 5 | date = models.DateTimeField() 6 | class Meta: 7 | verbose_name = "Glacier backup" 8 | verbose_name_plural = "Glacier backups" 9 | 10 | class GlacierInventory (models.Model): 11 | inventory_id = models.CharField(max_length=92, unique=True, verbose_name="Glacier inventory ID") 12 | collected_date = models.DateTimeField(blank=True, null=True, default=None) 13 | requested_date = models.DateTimeField(auto_now_add=True) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import subprocess 3 | from icybackup import __version__ 4 | 5 | def get_long_desc(): 6 | """Use Pandoc to convert the readme to ReST for the PyPI.""" 7 | try: 8 | return subprocess.check_output(['pandoc', '-f', 'markdown', '-t', 'rst', 'README.mdown']) 9 | except: 10 | print "WARNING: The long readme wasn't converted properly" 11 | 12 | setup(name='django-icybackup', 13 | version=__version__, 14 | description='A Django database/media backup tool with Amazon Glacier and local folder support', 15 | long_description=get_long_desc(), 16 | author='Adam Brenecki, St Barnabas\' Theological College', 17 | author_email='abrenecki@sbtc.org.au', 18 | url='https://github.com/stbarnabas/django-icybackup', 19 | packages=find_packages(), 20 | include_package_data=True, 21 | setup_requires=[ 22 | 'setuptools_git>=0.3', 23 | ], 24 | install_requires=[ 25 | 'boto', 26 | 'python-dateutil', 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /test_django_project/icytests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stbarnabas/django-icybackup/a5fbd4b6dd064f3a7e2166a38b9a3138f0a8cdc9/test_django_project/icytests/__init__.py -------------------------------------------------------------------------------- /test_django_project/icytests/management/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Adam' 2 | -------------------------------------------------------------------------------- /test_django_project/icytests/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Adam' 2 | -------------------------------------------------------------------------------- /test_django_project/icytests/management/commands/icytests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This runs the tests for icybackup. 3 | 4 | We're not using Django's unit test framework because this is really only one big test, 5 | and Django likes to use an in-memory sqlite database, which can't be backed up. 6 | """ 7 | import hashlib 8 | from optparse import make_option 9 | from django.conf import settings 10 | import os 11 | from ...models import Blah 12 | from django.core.management import call_command, BaseCommand 13 | 14 | IMAGE_SHA1 = "65c31668e9a889849e7ee8797f50e41d01a4bdcc" 15 | 16 | TEST_POSTGRES = "This is the test content for the Postgres database" 17 | TEST_MYSQL = "This is the test content for the MySQL database" 18 | TEST_SQLITE = "This is the test content for the SQLite database" 19 | 20 | class Command(BaseCommand): 21 | option_list = BaseCommand.option_list + ( 22 | make_option('--without-mysql', action='store_false', dest='mysql'), 23 | make_option('--without-postgres', action='store_false', dest='postgres'), 24 | make_option('--without-sqlite', action='store_false', dest='sqlite'), 25 | ) 26 | def handle(self, *args, **options): 27 | try: 28 | self._handle(*args, **options) 29 | except: 30 | if os.path.exists('backup.tgz'): 31 | os.unlink('backup.tgz') 32 | raise 33 | def _handle(self, *args, **options): 34 | postgres = true_if_none(options.get('postgres')) 35 | mysql = true_if_none(options.get('mysql')) 36 | sqlite = true_if_none(options.get('sqlite')) 37 | 38 | print postgres, mysql, sqlite 39 | 40 | # Create DB objects 41 | if postgres: 42 | call_command('syncdb', input=False, database='postgres') 43 | Blah.objects.using('postgres').all().delete() 44 | pg_test = Blah.objects.create(text=TEST_POSTGRES) 45 | pg_test.save(using='postgres') 46 | if mysql: 47 | call_command('syncdb', input=False, database='mysql') 48 | Blah.objects.using('mysql').all().delete() 49 | mysql_test = Blah.objects.create(text=TEST_MYSQL) 50 | mysql_test.save(using='mysql') 51 | if sqlite: 52 | call_command('syncdb', input=False) 53 | Blah.objects.using('default').all().delete() 54 | sqlite_test = Blah.objects.create(text=TEST_SQLITE) 55 | sqlite_test.save() 56 | 57 | # remove databases from the DB dict if we're not testing them 58 | if not postgres: del settings.DATABASES['postgres'] 59 | if not mysql: del settings.DATABASES['mysql'] 60 | if not sqlite: del settings.DATABASES['default'] 61 | 62 | # perform backup 63 | call_command('backup', output='backup.tgz') 64 | 65 | # delete everything 66 | if postgres: pg_test.delete() 67 | if mysql: mysql_test.delete() 68 | if sqlite: os.unlink(settings.DATABASES['default']['NAME']) 69 | os.unlink(os.path.join(settings.MEDIA_ROOT, 'test_image.jpg')) 70 | 71 | # perform restore 72 | call_command('restore', input='backup.tgz') 73 | 74 | # check that the file is present 75 | img_path = os.path.join(settings.MEDIA_ROOT, 'test_image.jpg') 76 | assert os.path.exists(img_path) 77 | assert hashfile(img_path) == IMAGE_SHA1 78 | 79 | # check that the postgres db entry is there 80 | if postgres: 81 | qs = Blah.objects.using('postgres').all() 82 | assert len(qs) == 1 83 | assert qs[0].text == TEST_POSTGRES 84 | 85 | # check that the mysql db entry is there 86 | if mysql: 87 | qs = Blah.objects.using('mysql').all() 88 | assert len(qs) == 1 89 | assert qs[0].text == TEST_MYSQL 90 | 91 | # check that the sqlite db entry is there 92 | if sqlite: 93 | qs = Blah.objects.using('default').all() 94 | assert len(qs) == 1 95 | assert qs[0].text == TEST_SQLITE 96 | 97 | def true_if_none(a): 98 | if a is None: 99 | return True 100 | else: 101 | return a 102 | 103 | def hashfile(filepath): 104 | sha1 = hashlib.sha1() 105 | f = open(filepath, 'rb') 106 | try: 107 | sha1.update(f.read()) 108 | finally: 109 | f.close() 110 | return sha1.hexdigest() 111 | -------------------------------------------------------------------------------- /test_django_project/icytests/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Blah (models.Model): 4 | text = models.TextField() -------------------------------------------------------------------------------- /test_django_project/icytests/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /test_django_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_django_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /test_django_project/media/test_image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stbarnabas/django-icybackup/a5fbd4b6dd064f3a7e2166a38b9a3138f0a8cdc9/test_django_project/media/test_image.jpg -------------------------------------------------------------------------------- /test_django_project/test_django_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stbarnabas/django-icybackup/a5fbd4b6dd064f3a7e2166a38b9a3138f0a8cdc9/test_django_project/test_django_project/__init__.py -------------------------------------------------------------------------------- /test_django_project/test_django_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for test_django_project project. 2 | 3 | import os 4 | 5 | DEBUG = True 6 | TEMPLATE_DEBUG = DEBUG 7 | 8 | ADMINS = ( 9 | # ('Your Name', 'your_email@example.com'), 10 | ) 11 | 12 | MANAGERS = ADMINS 13 | 14 | DATABASES = { 15 | 'default': { 16 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 17 | 'NAME': 'database.sqlite3', # Or path to database file if using sqlite3. 18 | }, 19 | 'postgres': { 20 | 'ENGINE': 'django.db.backends.postgresql_psycopg2', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 21 | 'NAME': 'app_test', # Or path to database file if using sqlite3. 22 | 'USER': 'postgres', # Not used with sqlite3. 23 | }, 24 | 'mysql': { 25 | 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 26 | 'NAME': 'app_test', # Or path to database file if using sqlite3. 27 | 'USER': 'root', # Not used with sqlite3. 28 | } 29 | } 30 | 31 | # Local time zone for this installation. Choices can be found here: 32 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 33 | # although not all choices may be available on all operating systems. 34 | # In a Windows environment this must be set to your system time zone. 35 | TIME_ZONE = 'America/Chicago' 36 | 37 | # Language code for this installation. All choices can be found here: 38 | # http://www.i18nguy.com/unicode/language-identifiers.html 39 | LANGUAGE_CODE = 'en-us' 40 | 41 | SITE_ID = 1 42 | 43 | # If you set this to False, Django will make some optimizations so as not 44 | # to load the internationalization machinery. 45 | USE_I18N = True 46 | 47 | # If you set this to False, Django will not format dates, numbers and 48 | # calendars according to the current locale. 49 | USE_L10N = True 50 | 51 | # If you set this to False, Django will not use timezone-aware datetimes. 52 | USE_TZ = True 53 | 54 | # Absolute filesystem path to the directory that will hold user-uploaded files. 55 | # Example: "/home/media/media.lawrence.com/media/" 56 | MEDIA_ROOT = os.path.join(os.path.split(os.path.dirname(__file__))[0], 'media') 57 | 58 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 59 | # trailing slash. 60 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 61 | MEDIA_URL = '' 62 | 63 | # Absolute path to the directory static files should be collected to. 64 | # Don't put anything in this directory yourself; store your static files 65 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 66 | # Example: "/home/media/media.lawrence.com/static/" 67 | STATIC_ROOT = '' 68 | 69 | # URL prefix for static files. 70 | # Example: "http://media.lawrence.com/static/" 71 | STATIC_URL = '/static/' 72 | 73 | # Additional locations of static files 74 | STATICFILES_DIRS = ( 75 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 76 | # Always use forward slashes, even on Windows. 77 | # Don't forget to use absolute paths, not relative paths. 78 | ) 79 | 80 | # List of finder classes that know how to find static files in 81 | # various locations. 82 | STATICFILES_FINDERS = ( 83 | 'django.contrib.staticfiles.finders.FileSystemFinder', 84 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 85 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 86 | ) 87 | 88 | # Make this unique, and don't share it with anybody. 89 | SECRET_KEY = '*=l2&n_2=ccm_4ovv3$uhs6(!-mt1%vk!%8)i19j!h7d@pe!i+' 90 | 91 | # List of callables that know how to import templates from various sources. 92 | TEMPLATE_LOADERS = ( 93 | 'django.template.loaders.filesystem.Loader', 94 | 'django.template.loaders.app_directories.Loader', 95 | # 'django.template.loaders.eggs.Loader', 96 | ) 97 | 98 | MIDDLEWARE_CLASSES = ( 99 | 'django.middleware.common.CommonMiddleware', 100 | 'django.contrib.sessions.middleware.SessionMiddleware', 101 | 'django.middleware.csrf.CsrfViewMiddleware', 102 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 103 | 'django.contrib.messages.middleware.MessageMiddleware', 104 | # Uncomment the next line for simple clickjacking protection: 105 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 106 | ) 107 | 108 | ROOT_URLCONF = 'test_django_project.urls' 109 | 110 | # Python dotted path to the WSGI application used by Django's runserver. 111 | WSGI_APPLICATION = 'test_django_project.wsgi.application' 112 | 113 | TEMPLATE_DIRS = ( 114 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 115 | # Always use forward slashes, even on Windows. 116 | # Don't forget to use absolute paths, not relative paths. 117 | ) 118 | 119 | INSTALLED_APPS = ( 120 | 'django.contrib.auth', 121 | 'django.contrib.contenttypes', 122 | 'django.contrib.sessions', 123 | 'django.contrib.sites', 124 | 'django.contrib.messages', 125 | 'django.contrib.staticfiles', 126 | 'icybackup', 127 | 'icytests', 128 | # Uncomment the next line to enable the admin: 129 | # 'django.contrib.admin', 130 | # Uncomment the next line to enable admin documentation: 131 | # 'django.contrib.admindocs', 132 | ) 133 | 134 | # A sample logging configuration. The only tangible logging 135 | # performed by this configuration is to send an email to 136 | # the site admins on every HTTP 500 error when DEBUG=False. 137 | # See http://docs.djangoproject.com/en/dev/topics/logging for 138 | # more details on how to customize your logging configuration. 139 | LOGGING = { 140 | 'version': 1, 141 | 'disable_existing_loggers': False, 142 | 'filters': { 143 | 'require_debug_false': { 144 | '()': 'django.utils.log.RequireDebugFalse' 145 | } 146 | }, 147 | 'handlers': { 148 | 'mail_admins': { 149 | 'level': 'ERROR', 150 | 'filters': ['require_debug_false'], 151 | 'class': 'django.utils.log.AdminEmailHandler' 152 | } 153 | }, 154 | 'loggers': { 155 | 'django.request': { 156 | 'handlers': ['mail_admins'], 157 | 'level': 'ERROR', 158 | 'propagate': True, 159 | }, 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /test_django_project/test_django_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Examples: 9 | # url(r'^$', 'test_django_project.views.home', name='home'), 10 | # url(r'^test_django_project/', include('test_django_project.foo.urls')), 11 | 12 | # Uncomment the admin/doc line below to enable admin documentation: 13 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | # url(r'^admin/', include(admin.site.urls)), 17 | ) 18 | -------------------------------------------------------------------------------- /test_django_project/test_django_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for test_django_project project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_django_project.settings") 19 | 20 | # This application object is used by any WSGI server configured to use this 21 | # file. This includes Django's development server, if the WSGI_APPLICATION 22 | # setting points here. 23 | from django.core.wsgi import get_wsgi_application 24 | application = get_wsgi_application() 25 | 26 | # Apply WSGI middleware here. 27 | # from helloworld.wsgi import HelloWorldApplication 28 | # application = HelloWorldApplication(application) 29 | --------------------------------------------------------------------------------