Commit b9ccfed8 authored by chris's avatar chris
Browse files

Added support for Python3.

parent 6a12e92a
*.pyc
/build
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
.settings
/dist
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
# Sphinx documentation
docs/_build/
# Local virtualenvs used for testing.
/.env*
# PIP install version files.
/=*
......@@ -9,51 +9,52 @@ but there are some valid use cases. If your Django app is behind a caching
reverse proxy and you need to scale your application servers, it may be
simpler to store files in the database.
Requires:
* Django 1.1
Installation
------------
sudo python setup.py install
python setup.py install
Or via pip with:
sudo pip install https://github.com/chrisspen/django-database-files/zipball/master
pip install django-database-files
You can run unittests with:
python setup.py test
You can run unittests for a specific Python version using the `pv` parameter
like:
python setup.py test --pv=3
Usage
-----
In ``settings.py``, add ``database_files`` to your ``INSTALLED_APPS`` and add
In `settings.py`, add `database_files` to your `INSTALLED_APPS` and add
this line:
DEFAULT_FILE_STORAGE = 'database_files.storage.DatabaseStorage'
Note, the ``upload_to`` parameter is still used to synchronize the files stored
Note, the `upload_to` parameter is still used to synchronize the files stored
in the database with those on the file system, so new and existing fields
should still have a value that makes sense from your base media directory.
If you're using South, the initial model migrations will scan through all
existing models for ``FileFields`` or ``ImageFields`` and will automatically
existing models for `FileFields` or `ImageFields` and will automatically
load them into the database.
If for any reason you want to re-run this bulk import task, run:
$ python manage.py database_files_load
python manage.py database_files_load
Additionally, if you want to export all files in the database back to the file
system, run:
$ python manage.py database_files_dump
python manage.py database_files_dump
Note, that when a field referencing a file is cleared, the corresponding file
in the database and on the file system will not be automatically deleted.
To delete all files in the database and file system not referenced by any model
fields, run:
$ python manage.py database_files_cleanup
python manage.py database_files_cleanup
from __future__ import print_function
import os
from optparse import make_option
......@@ -28,7 +30,7 @@ class Command(BaseCommand):
dryrun = options['dryrun']
try:
for model in get_models():
print 'Checking model %s...' % (model,)
print('Checking model %s...' % (model,))
for field in model._meta.fields:
if not isinstance(field, (FileField, ImageField)):
continue
......@@ -41,7 +43,7 @@ class Command(BaseCommand):
for row in subq.iterator():
subq_i += 1
if subq_i == 1 or not subq_i % 100:
print '%i of %i' % (subq_i, subq_total)
print('%i of %i' % (subq_i, subq_total))
file = getattr(row, field.name)
if file is None:
continue
......@@ -49,22 +51,22 @@ class Command(BaseCommand):
continue
names.add(file.name)
# Find all database files with names not in our list.
print 'Finding orphaned files...'
print('Finding orphaned files...')
orphan_files = File.objects.exclude(name__in=names).only('name', 'size')
total_bytes = 0
orphan_total = orphan_files.count()
orphan_i = 0
print 'Deleting %i orphaned files...' % (orphan_total,)
print('Deleting %i orphaned files...' % (orphan_total,))
for f in orphan_files.iterator():
orphan_i += 1
if orphan_i == 1 or not orphan_i % 100:
print '%i of %i' % (orphan_i, orphan_total)
print('%i of %i' % (orphan_i, orphan_total))
total_bytes += f.size
if dryrun:
print 'File %s is orphaned.' % (f.name,)
print('File %s is orphaned.' % (f.name,))
else:
print 'Deleting orphan file %s...' % (f.name,)
print('Deleting orphan file %s...' % (f.name,))
default_storage.delete(f.name)
print '%i total bytes in orphan files.' % total_bytes
print('%i total bytes in orphan files.' % total_bytes)
finally:
settings.DEBUG = tmp_debug
from __future__ import print_function
import os
from django.conf import settings
......@@ -36,7 +38,8 @@ class Command(BaseCommand):
for field in model._meta.fields:
if not isinstance(field, (FileField, ImageField)):
continue
print model.__name__, field.name
if show_files:
print(model.__name__, field.name)
# Ignore records with null or empty string values.
q = {'%s__isnull'%field.name:False}
xq = {field.name:''}
......@@ -48,10 +51,10 @@ class Command(BaseCommand):
if not file.name:
continue
if show_files:
print "\t",file.name
print("\t",file.name)
if file.path and not os.path.isfile(file.path):
if show_files:
print "Broken:",file.name
print("Broken:",file.name)
broken += 1
continue
file.read()
......@@ -59,7 +62,7 @@ class Command(BaseCommand):
except IOError:
broken += 1
if show_files:
print '-'*80
print '%i broken' % (broken,)
print('-'*80)
print('%i broken' % (broken,))
finally:
settings.DEBUG = tmp_debug
from __future__ import print_function
from optparse import make_option
from django.conf import settings
......@@ -28,7 +30,7 @@ class Command(BaseCommand):
total = q.count()
i = 1
for f in q.iterator():
print '%i of %i: %s' % (i, total, f.name)
print('%i of %i: %s' % (i, total, f.name))
f._content_hash = None
f.save()
i += 1
......
from __future__ import print_function
import base64
import settings as _settings
import six
from . import settings as _settings
from django.conf import settings
from django.db import models
......@@ -57,7 +61,10 @@ class File(models.Model):
@property
def content(self):
return base64.b64decode(self._content)
c = self._content
if not isinstance(c, six.binary_type):
c = c.encode('utf-8')
return base64.b64decode(c)
@content.setter
def content(self, v):
......@@ -94,19 +101,20 @@ class File(models.Model):
tmp_debug = settings.DEBUG
settings.DEBUG = False
try:
q = cls.objects.only('id', 'name', '_content_hash').values_list('id', 'name', '_content_hash')
q = cls.objects.only('id', 'name', '_content_hash')\
.values_list('id', 'name', '_content_hash')
total = q.count()
if verbose:
print 'Checking %i total files...' % (total,)
print('Checking %i total files...' % (total,))
i = 0
for (file_id, name, content_hash) in q.iterator():
i += 1
if verbose and not i % 100:
print '%i of %i' % (i, total)
print('%i of %i' % (i, total))
if not is_fresh(name=name, content_hash=content_hash):
if verbose:
print 'File %i-%s is stale. Writing to local file system...' \
% (file_id, name)
print(('File %i-%s is stale. Writing to local file '
'system...') % (file_id, name))
file = File.objects.get(id=file_id)
write_file(
file.name,
......
......@@ -3,6 +3,13 @@ import os
from django.conf import settings
from django.core.urlresolvers import reverse
# If true, when file objects are created, they will be automatically copied
# to the local file system for faster serving.
settings.DB_FILES_AUTO_EXPORT_DB_TO_FS = getattr(
settings,
'DB_FILES_AUTO_EXPORT_DB_TO_FS',
True)
def URL_METHOD_1(name):
"""
Construct file URL based on media URL.
......@@ -20,17 +27,27 @@ URL_METHODS = (
('URL_METHOD_2', URL_METHOD_2),
)
DATABASE_FILES_URL_METHOD_NAME = getattr(
settings.DATABASE_FILES_URL_METHOD_NAME = getattr(
settings,
'DATABASE_FILES_URL_METHOD',
'URL_METHOD_1')
if callable(DATABASE_FILES_URL_METHOD_NAME):
method = DATABASE_FILES_URL_METHOD_NAME
if callable(settings.DATABASE_FILES_URL_METHOD_NAME):
method = settings.DATABASE_FILES_URL_METHOD_NAME
else:
method = dict(URL_METHODS)[DATABASE_FILES_URL_METHOD_NAME]
method = dict(URL_METHODS)[settings.DATABASE_FILES_URL_METHOD_NAME]
settings.DATABASE_FILES_URL_METHOD = method
settings.DB_FILES_DEFAULT_ENFORCE_ENCODING = getattr(
settings, 'DB_FILES_DEFAULT_ENFORCE_ENCODING', True)
DATABASE_FILES_URL_METHOD = setattr(
settings.DB_FILES_DEFAULT_ENCODING = getattr(
settings,
'DATABASE_FILES_URL_METHOD',
method)
'DB_FILES_DEFAULT_ENCODING',
'ascii')
settings.DB_FILES_DEFAULT_ERROR_METHOD = getattr(
settings, 'DB_FILES_DEFAULT_ERROR_METHOD', 'ignore')
settings.DB_FILES_DEFAULT_HASH_FN_TEMPLATE = getattr(
settings, 'DB_FILES_DEFAULT_HASH_FN_TEMPLATE', '%s.hash')
from __future__ import print_function
import os
import StringIO
import six
from six import StringIO
from django.conf import settings
from django.core import files
......@@ -9,8 +12,6 @@ from django.core.urlresolvers import reverse
from database_files import models
from database_files import utils
AUTO_EXPORT_DB_TO_FS = getattr(settings, 'DB_FILES_AUTO_EXPORT_DB_TO_FS', True)
class DatabaseStorage(FileSystemStorage):
def _generate_name(self, name, pk):
......@@ -31,7 +32,7 @@ class DatabaseStorage(FileSystemStorage):
f = models.File.objects.get_from_name(name)
content = f.content
size = f.size
if AUTO_EXPORT_DB_TO_FS \
if settings.DB_FILES_AUTO_EXPORT_DB_TO_FS \
and not utils.is_fresh(f.name, f.content_hash):
# Automatically write the file to the filesystem
# if it's missing and exists in the database.
......@@ -45,6 +46,7 @@ class DatabaseStorage(FileSystemStorage):
# and load it into the database if present.
fqfn = self.path(name)
if os.path.isfile(fqfn):
#print('Loading file into database.')
self._save(name, open(fqfn, mode))
fh = super(DatabaseStorage, self)._open(name, mode)
content = fh.read()
......@@ -53,7 +55,8 @@ class DatabaseStorage(FileSystemStorage):
# Otherwise we don't know where the file is.
return
# Normalize the content to a new file object.
fh = StringIO.StringIO(content)
#fh = StringIO(content)
fh = six.BytesIO(content)
fh.name = name
fh.mode = mode
fh.size = size
......@@ -77,7 +80,7 @@ class DatabaseStorage(FileSystemStorage):
name=name,
)
# Automatically write the change to the local file system.
if getattr(settings, 'DATABASE_FILES_FS_AUTO_WRITE', True):
if settings.DB_FILES_AUTO_EXPORT_DB_TO_FS:
utils.write_file(name, content, overwrite=True)
#TODO:add callback to handle custom save behavior?
return self._generate_name(name, f.pk)
......
import os, sys
PROJECT_DIR = os.path.dirname(__file__)
DATABASES = {
'default':{
'ENGINE': 'django.db.backends.sqlite3',
......@@ -8,7 +10,9 @@ DATABASES = {
# 'TEST_NAME': '/tmp/database_files.db',
}
}
ROOT_URLCONF = 'database_files.urls'
INSTALLED_APPS = [
'django.contrib.auth',
'django.contrib.contenttypes',
......@@ -16,6 +20,16 @@ INSTALLED_APPS = [
'django.contrib.sites',
'database_files',
'database_files.tests',
'south',
]
DEFAULT_FILE_STORAGE = 'database_files.storage.DatabaseStorage'
MEDIA_ROOT = os.path.join(PROJECT_DIR, 'media')
\ No newline at end of file
MEDIA_ROOT = os.path.join(PROJECT_DIR, 'media')
# Run our South migrations during unittesting.
SOUTH_TESTS_MIGRATE = True
USE_TZ = True
SECRET_KEY = 'secret'
# -*- coding: utf-8 -*-
import os
import StringIO
import shutil
import six
from six import StringIO
from django.core import files
from django.test import TestCase
......@@ -14,14 +17,17 @@ DIR = os.path.abspath(os.path.split(__file__)[0])
class DatabaseFilesTestCase(TestCase):
def setUp(self):
self.media_dir = os.path.join(DIR, 'media/i/special')
if os.path.isdir(self.media_dir):
shutil.rmtree(self.media_dir)
os.makedirs(self.media_dir)
def test_adding_file(self):
# Create default thing storing reference to file
# in the local media directory.
media_dir = os.path.join(DIR,'media/i/special')
if not os.path.isdir(media_dir):
os.makedirs(media_dir)
test_fqfn = os.path.join(media_dir,'test.txt')
test_fqfn = os.path.join(self.media_dir, 'test.txt')
open(test_fqfn,'w').write('hello there')
o1 = o = Thing()
test_fn = 'i/special/test.txt'
......@@ -42,21 +48,21 @@ class DatabaseFilesTestCase(TestCase):
# Verify we can read the contents of thing.
o = Thing.objects.get(id=id)
self.assertEqual(o.upload.read(), "hello there")
self.assertEqual(o.upload.read(), b"hello there")
# Verify that by attempting to read the file, we've automatically
# loaded it into the database.
File.objects.update()
q = File.objects.all()
self.assertEqual(q.count(), 1)
self.assertEqual(q[0].content, "hello there")
self.assertEqual(q[0].content, b"hello there")
# Load a dynamically created file outside /media.
test_file = files.temp.NamedTemporaryFile(
suffix='.txt',
dir=files.temp.gettempdir()
)
test_file.write('1234567890')
test_file.write(b'1234567890')
test_file.seek(0)
t = Thing.objects.create(
upload=files.File(test_file),
......@@ -65,7 +71,7 @@ class DatabaseFilesTestCase(TestCase):
t = Thing.objects.get(pk=t.pk)
self.assertEqual(t.upload.file.size, 10)
self.assertEqual(t.upload.file.name[-4:], '.txt')
self.assertEqual(t.upload.file.read(), '1234567890')
self.assertEqual(t.upload.file.read(), b'1234567890')
t.upload.delete()
self.assertEqual(File.objects.count(), 1)
......@@ -87,53 +93,41 @@ class DatabaseFilesTestCase(TestCase):
self.assertEqual(os.path.isfile(test_fqfn), False)
def test_hash(self):
verbose = 0
verbose = 1
# Create test file.
media_dir = os.path.join(DIR, 'media/i/special')
if not os.path.isdir(media_dir):
os.makedirs(media_dir)
image_content = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x002\x00\x00\x009\x08\x06\x00\x00\x00t\xf8xr\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\tpHYs\x00\x00\x1e\xc2\x00\x00\x1e\xc2\x01n\xd0u>\x00\x00\x00\x07tIME\x07\xdc\x08\x15\r0\x02\xc7\x0f^\x14\x00\x00\x0eNIDATh\xde\xd5\x9aytTU\x9e\xc7?\xf7\xbdWkRd\x0f\x01\x12\x02!\xc8\x120,\r6\xb2#\x10\xa2(\xeeK\xdb:n\xd3\xca\xd0\xb46\x8b\xf6\xc09=\xd3v\xdb\xa7\x91\xb8\xb5L\xab\xe3\xb8\xb5\x8c\xbd\x88\x83\xda-\x84\xc4\xb84\x13\x10F\x01\r\xb2%\x01C \tY\x8a$UIj{\xef\xce\x1f\xaf\xaa\x92\x90\x80@\xc0\xa6\xef9uRU\xa9\xba\xf7\xf7\xbd\xdf\xdf\xfe+\xb8Xk\xc1\x8aD\xe6/K\x03 \x7f%\x17{)\x17kcUU/s%\xa6\xbce[\xf8h6\x9b\xd7B\xfe\x8a\x7fL \xe9)I\t\xd3\'O\x9c-\xe2\x07\xfc\x89\x9c\xd9N6\x17\\Tf.\x1a\x90\xe1\x19\xa9\xa3\xe6\x8d\x19\x84\x96>f\x02#\xe6l\x02`\xf3ZX\xb0\xe2\x1f\x08\xc8\x84\xfb\xfb\xa5\xc6\xc7.\x9c;&\x1d\xabE\x95d\x8c\x9f\xc1\r\xbfx\x95\xf4\\\x85\xc2\x82\x8b\x02F\xb9\x80\xc6M\xcc\xf5\xab\x01p\x0eI\x9f1kl\xd6\xeca)\xfd\xb0)\x8a@\xb3\n\x06\xe4\xdc\xcb\x15\xb7\xad\xe1\xca\x1f:L0\xcb\xb9\xf4V\xd7\x1b\xce\xbec\xfe\xea7\x8bO\x18R\xca\x0f\xcb\xdd\x86kU\x89d\xd9\x16\x83\xe5E\x92%\xefHnyr#\xdf\xbb%\xbd\xf3\xbb\x17\x06\x90\xda\xe7\x1d\xf2W@a\x81\xf9|\xe6OV\xad\x7fz\xc9\xb3?\xbe\xe6\xfbI\xfb\x1b\xda\xe5\xafK\xbe\x11_\xd7y\x91\x08\x01H4\x9b\xc0\x95<\x12W\xf2\x1d$\x0fm\xe6\xc8\xce\xddTl7/\xa2b[\x9f\xc4\x10\xe7\xcd@D\xf8\xa1\xb7*\xd6\x9c\xac\x01\xb9C\x07\xbc\xf8\xdc\xe2E\x0bs\xb2\xd2)>\xe4f\xcb\xc1&^\xdfy\x8c\xa0n`\xe2\x08/)%B\x08:Z\xe1D\xf9\x0e\x02\xde\xd5H\xb6q\xb2\xdaO\xe9\xef\x8d\xef\x96\x91\xc8\xed\r\xbfcX\xde\xeds\xef\xbf\xe1\xca\xb1\x7f~n\xf1\xf5cGe\xa4\xcaV\x7f\x88=5mB\x02\xfb\xeb\xdb\xf0\xfaC\xdd\x81\x880;\x16\xbb ~`:V\xe7\xdd(\xea-\xc4$\xda\xc9\xc8m\xa1b[C\xf4\xb2\xce\x81%q.*\xe4Tm\xb4\xff\xf5\t`J\xf2e\xf7\xdf\xf0\xf0\xe2k\xa7\xdc>m\xf4\x90\xec\xef\rO\x07\x90\x80\x08\xea\x06\xaf}QGY\x8d\x87\xff\xfa\xec\x18\xbe\xa0\xde\x1dH\'3 \x84\x0c\x8b \xf0{\xc1\xd7Z\x83\xbbz\x03u\x07\xd6\xb2m\xfd1~\xf0[p\x1f\xedd\xbf\xcf@\xf2W\x9a1\x00`\xd2\x8f\xee[ro\xfe\x13\x8f\xdd<+e@\xa2K\xd5T5\n\x02\xe0\xbd\xaf\x1b\xa9h\xea\xe0\xc5\xd2\xa3T4\xb4\x99\xef\x8a3\x1c\x13\x05\x84@J0t\x89\xbb\xba\x9e\xe3e\x8fQ\xb2\xee\r\xf3\xfc\xe5\xb0\xf9\xa9>\x00\xe9\n`\xce\xc3\xc9C3\x07\xbc\xf9\xc6\xb2[\x17L\x1f3T\x86\xd5]tj\x0b|\\\xd9\xcc\xd7\'\xdax\xb7\xac\x8e\x92\xfd\x8d\xa0\x9d\x87w7\x81A[3\xd4\xee[G[\xd3c|\xf8|;\xf9\x8f\xc2\xe6\'\xcf\xc3F\xba\x80p,Z5\xfb\x9f\x17^\xf9\xde\x7f?z\xc7\xe4Q\x83S\x91R\n\x11\x96^\x08AP\x97|v\xb4\x95/k\xbd|x\xa8\x91\xc2\xb2z\xb0\x9c\xa7C\x8c\xb0gs\x803~20\x82\xd4\xecR\x8a\x9f\xf5\x90\xff(T\x94\x9e\x03\x90\xaeL,X\xb1d\xdd\x92\x1b^\\~\xe3\x8cA\t\xb1\x0e\xd9\x95E!\x04\xed\x01\x9d\x8f\x0f7SV\xd7\xc6\xa6\x03\rl\xda\xd7\x00\xea\x05\x88\xb3RJ\xacvp\xb8r\xd0\xacS\x89M\xfa\x94\x8f_p\x9f\xce\t\xa8\xbd\xc6\x85\xcda\xe3\x9a\xb1\xf4\xf1\xa2\xb5K~y\xf3\xb4\xb1\xb16\x8b&\xc3L\x10yT7\xfb\xd8t\xc0MM\xab\x9f\xff\xf9\xea\x04\x1f\x957u\xbf\xd5>\x05\x06a\xda\x8cj\x01g|\x06\xd6\x98\xd98b?\xe0\xd3\x97[X\xb0\x1c*\xb6\x7f\x0b\x90\x08\xda\xbc\xe5K\x8b\n\x96<>o|\xb6\x85\x88\xc9\x86=g\xc8\x80]\xc7\xbd|\\\xd9\x8c\xc7\xa7\xf3\xf2\xf6j>\xafj\x06E\\\x18\x10\xdd]\xb5\x00!q\xc6\xf5\xc7\xea\x1cMB\xfa\x07|\xf4\x1f\xbeS\xc1\xa8=\x02]\xc56\x1c\x8bV\xcd}\xe6\xa1\xeb_\xbcm\xfa\xe51\x11\x00B\x08tCR\xe7\t\xf0Ie3\xbbk\xbcT6\xb5\xf3\xfc\xd6*\xaa\xdd\x1d\x17F\x9d\xce\x04HQA\xb3g#uH\xc9,\xc1\xea\xeaf/j7\xbb(,@,X\x9e\xb4h\xfa\xc4\xf7V\xdf6{\x80\xd3\xa6\xc9\x88Q\xd7\xb4\xf8\xf9\xbfc\x1evV{\xa8hj\xa7p\x7f\x03\x1b\xbf:A{P\x07\xe5"\x82\xe8b5Xl\x02_\xcb4B\xa1=\x14\xae=\xc0\x82\x95Q\r\xd2\xba~\x0e\xc0\xe9\x8a{\xfd\xba9S\x86%\xc4\xd8$ \xaaN\xfa\xf8\xaa\xd6K\xbd7H\xc8\x90\x94\x947Rt\xb0\x89\x96\x8eP\xf4;\xdf\xd1\x12\x08Eb\x8b\x15\xd8b~\xcfU\xff\x92I\xe1\xda\x93\x91t\xa9\xbbB\xcfX|\xfd\xac\xebn\xda\xf8\x83\xe99RS\xa4\xe8\x08\x1a\x84\x0cS\xd8\xc3M\xed\xbc\xf5E\r\r\xde\xc0\x85\xb5\x83s\x89/\x8a\nMU\x12_\xb3\xa0\xa5\xfe\x1d6\xad\xb9\xd94\x89\x95(]\xd2h\x8d\xb4\xe1\x05\xce\x84\x14\xfc\xc1\x90hj\x0bR\xef\r\xf0u\xad\x97u[\xabx\xee\xa3\xc34x\xfeN \xa2\x9eP\x824L\xe3w\xc4/`\xfe#SM\x90\x06\x1a"\xac\xdfW?v\x1f\xfd\xb32>)o\x94\r\x1e\x9f\xf0\xeb\x065->\x1a[\xfcf\xf9\xa5*\x7f?\x10\x91e\x18 u\x90R`\xb1\xc4`s=H\xca\x88mly\xcaL1\x18\xb70\x86\xb1W\xbfM\xf2\xd0|\x84\xd2\xa9\xfa\x82\x0b\x17\x17.\xc4\n\x05\xa0\xe9\x1b\x08\xf9\xcd\xd7A\xdfQ<\x8d\x0b)z\xa6\xcc\xa4\xa3\xff\x8818\xe2\'\xa1\xa8\xa6\xd0\x8a\xe8\x8c\t\x97\n\x08\x80\x90\x0f\x8c`\xe7\xe5*\xea`l\xb1S\x99\xf6O(dM\x82\xf8\x81\x13\xd0\xac\xc9\\\xaaKJ\xf3\xe1o\x03#Z\x16HT\x0b(\xcau8\x13\x1d\nC&\xd9\xd1,Wa\x8b\xe5\xbb\xf6\xa7\xe7d\xe8z\x10\xda\x9b;\xf5]J\x11f\xe5*\xac\x0e\xa7B\xbf\xfe6T\xeb,T\x15$\xe2\x92d\x03\x01\xdeFS\xad"\xaa\x1eaE\xb3X\x91r\xa2\x82\xaa%a\xb1\'\xf5\xa1\x82\xbf\xf8l\xf8\xbd\xe0m \xeaa\xbb\x06I\xd5\x02\xd2\x98\xa1\xa0\x07Ga\x8b\x01y\x89\xa9\x95\x0c\x8b\xa3\x07\xc0]\xd5\x1b\x88p\x11\xa6\x82\xa2\xe6*\x18F&\x16; \xc5%i\x17\xee\xea\xae\x06\xde{\x11\xa6hY\nR&\xa3Z/AOe@s\r\x04\xda\xbf5\x97DQ\x924\x90j\x9fc\x85\x94=\xf6\xeefo\xe7\xb2\x7f\xa4f?Y\r\xbe\x96p9"z\xdf7\x9a\xc3k\x0e\xed\xbc-\xdc\x90(\xaa`p\x82\x9d\x1bF\xa72wD"\xd9\xc9N\x12\x9d\x164E\xe1PC\x1b\x7f\xfd\xba\x9e\re\r|s\xb2\x83\x0e\x9fn\xa6:\xa7o\ru2\xe1>\n\x1d\x9e\xce\xf2 |Qi\t1\xf4\xb3\xdbh\x0f\x049\xd1\xd2N0\xa8\x9b\x81\x1bEj\x08\xbc\xe8!P\xb5\xb3\x07\xa1\x1b$\xbbl,\x9d\x96\xc1\xe2\xa9\x19\xa4\xc4\xf4T\xcd\xc9\x83\xe3\x98<8\x8eU\xf3\x86\xf1\xa7\xdd\xb5\xbc\xbc\xa3\x86\xd2\n\xb7Y\x01\x9d\n&\xf2:\xe8\x83\xe6\xe3\x10h\xeb\x04\xe1\x0f\x92\x96\x1a\xc7\x8f\xe6\x8c\xe5\xa6+\x863b`"\xc7\xdc^6|v\x88\x7f\xdb\xb0\x1d\x7f0\x04\xe0S\x199{(\xae\x94\x1b\xd1\xac\xf2\xac\x1c\xb0\x94\x0cJp\xb0\xf1\x9e\\~8q 1V5\xda\x05\xed\xad\x7f`Q\x15\xc6\r\xea\xc7\xa2\x9cT\xfa\xc7\xd9()w#u#|\x93\x116\x14\xf0yLu\n\xf9\xcd\xd7R\x82?\xc8M\xd3G\xf3\xdaCy\xdc:e\x04\xfd\xe3b\xd0\x14\x85\xc4\x18;S/\x1bHK{\x80\xed\xfb\x8e\x81\xe0\x1b\x05E\xad4\rJ\x9c\x15\x08!\x04\x1f\xdc?\x9e)C\xe2\xbb\xb5rO_r\x9b+)\xc6\xc2\xb2\x99\x99\x1c^5\x9d\t\x83\xe3 \x14n\xf3*\nxN\x98\xc9\xa0\x1e\x0exRb\xb7j\xac{(\x8f?,\xbd\x9a\x9c\xf4\xa4\xe8Y]\xcf\xbcf\xc2\x10\x12R\xe2!\xd0Q\xa9\xa0h&\x90\xf0\x06g4\xc2\x90\xc1\xbf\xe7e\x91;\xd0\x15\xddT\x9c\x83!K)\x19\x9c`\xe7\x8b\x9f~\x9f\x9f\xe7g\x13g\x05\xea\x8f@Km\xd4\xee\xac\x9a\xc2\x15\xc3\x07\xf0\xf9\x13w\xb2d\xfe8,\x9a\xda\xe3\xac\xc8\xdf\xf4\x04\x97L\x8dsA\xc8\xbfK\xa3\xbd\xb9\x03G\xdc\x1e\xa41\xee\xcc\xadMHK\xb0\xb3\xfa\xaa\xacs\x06\xd0\xbd\xc3c\xde\xe6/\xf2\x861g\xb0\x9d\x17\x8a\xfcl\xaf\x80@\xc8`tz\x12\xb7^q\x19\x0f\xce\xbd\xbc\xdb\xcd\x9f\xee,\x87U\xc5*$\xa8\x96\x8f5\xbc\x8d>\xe2\x07}H\xd0?\x0e\x8b\xed\xf4v\x122\xb8}|\x1a\xaa"\xce\xa8N\xe7\x02f\xe6\xa8AL\xcaJ\xe5h\xa3\x07\xdd0\x18\x98\x10KB\xac\xfd[U6\xb2\xdauEH#\xd4\x82\xa1\xeeU\xd8\xfaj\x08\xbfg+\x81\xb63g[\x86d\xea\xd0\x84\xf3f\xa370\x00N\x9b\x85\x91\x83\x12\xc9\xc9H\x8e\x828\xcb3\xe4\t\x8f\x1f\x9f\xaf\xa3\x98\xc6\xea\x0e\xd3\xe76T\x96\x91\x90\xbe\x17)\xc7\x9c6\x80II\xb2\xd3\xd2\xe7.hDH\xdd\x90\x1c\xacu\xf3R\xc9Wl\xd8q\x08\xab\xaa\xf2\x93\x05\xe3\xb9gf\x0e\x89\xb1\xf6\xb3aD\x1c\xabk\xa0\xaa\xee\xe4\xfb\xecx\xc5g\x02\xd9\xfa\xda\x11R\x87\x97\xe0J\x19\x8d\xa2\x89^\x99\x11\x82\x86\xb6`\x9f@D\x04\xab\xa8;\xc9K%e\xfc\xae\xf8K\xda=\x1d\xa0\x99\xed\xb5\xe5/\x17\xb3i\xcf\x11\xd6\xdd;\x87\x91\x03\x13M\xe0B\xf4\x10FJI dp\xa4\xaei\x7fp\xfb\xe7\xa5\x00Jt\x90\xd9P\xf1\x9fx\x1a\xda\xa2>\xbcG\x1a \xf80\xdc\xdb\x95R\x9e\xb7:\xfdj\xe3\x0e\xf2\xd7\xbcK\xc1{;i\xef\x08\x98 "\x91\xddi\xa3d\xcf\x11\xae]\xfb.\xa5\x07\x8f\x9b\xc0Os\x96\xd7\x170\xde(\xdaQD\xf5\xdb\x87\xcdNc\xa4\xd7[\xb5\xbb\x81\x01\xa3\xd2I\xcc\x98\x8c\xa2\xc8h\x05\x16Y\x8a`\xd7\xb1VV\xce\x1e\x82US\xce\xca\x18\xbb~\xa6\xf4`\r\xf9\xbf\xd9\xc8\x86\x9d\x87h\xf2t\x80\x94\xb8\x9c6r\x87\xa4\x90\xd9?\x0e_ \x14\x05\xe6\xf6\xfa\xd8\xf2\xd5Q\xa6\x8d\x18Dz\x92\xab\xdb>\xe1\xe7\xf2\x95\xe2]-\xeb\xd7\xac_L\xfb\xc1F\xf2V\x9c\xc2\xda\xe5\xf3,\x0c\x9bYK\xc6\xe5Ih\xb6\x9e\x92\xea\x06\xf3F&\xf3\xee}\xe3p\xf62\xff8\x15\\ \xa4Ss\xb2\x8d_\xbe\xb3\x9dW\x8b\xf6\x80U\x03!\xc8\xee\x1f\xcf3w\xcfb\xe1\x84\xacn\xdf\x7ff\xd3.\xfe\xf5\x8f\xff\x8b?\xa4\x83n02#\x89w\x1e\xb9\x96\xd1\xe1\x80\x18I\x1b\xf7V70\xf6\xfe\'\xd7P\\\xf03\xf2V\xc2\x96\xb5]{\xbf+\xa0t\xbdA\xe6\xb8]\xf8=w!\x10\x18\xba\xc4\xd0\x05F\x08\xf4\x10\xc8\x10\x87k\xdc\x94\xec?N\xbaK%#\xc9\x85\xd6\xa5y\x1d\x01\xd1\xd0\xda\xce\xd6\x03\xc7y~\xcb\x1e\xee{\xa9\x88\xcf\x0f\x1c\x07\x9b\x05$\xdc~\xe5H6\xfd\xecFl\x16\x957\xff\xb6\x8f?\x94\x1e\xa0\xb8\xac\n\xb7\xd7\xc7=3G\xb3\xb7\xba\x89}\xc7\xdd\xa0\x08\x1a\x1b=\xfc\xad\xbc\x961\xe9Id\xa6\xf4\x03\xe0\xa3\x83\'\xc4\xddk\xff\xf8I\xd3\x9f\x7f~\x17\x00\xc3\xa7@\xc5\xf6S\x18\x89\xccF\x16\xaeZ\x8d\xc3\xf5+4\x9b\x99\xf7D\xab\xb3p7C\xd7M\xb5\x18\x9c\xcc\x98A\x89$\xba\x1ch\x9a\x8a\xdf\x1f\xa4\xb2\xbe\x85\xc3\r\xad\x94\xd7\x9d\xc4\xe3\xf1\x99\xe37! \xa43/w\x08E\xabo\xe6\xad\xd2\x03,_\xff)u\x8d\xad\x9d\x9ak\xd5x\xfb\xe1\x85\xe8Rr\xebs\x1ft\x89_:\xa9\x89\xb1\x8c\xcfL\x95zL\xb2\xd8{\xa0\xfcH\xdd\xde=3\xd8\xba\xeeX\xd71yw a\x9a\x98\xbb\xd4\x89#\xeeu\x9c\xf1\xb7D\x07\x95g0\xe2Nu\x92\x18\x86\xec=\xb35$)qN&g\xf5\xa7\xb0\xac\n]7:\xff\x1f6\xe8\xf4\xc4X\xfc\xbaNC\xab\xafg\x91\xd5/\r\xf4\x90\x87\xda\x03\xd7\xb2\xe97\x9f\x92\xb7\x02\xb6\x14\x9ca\x18\x1a\x19\xbbM\xbd\xd7E\xd2\xc0\xcd8\x13\xa6\x86=\x99\t\xe8l\x83\xa1\x94\x9d\x85\x90\xe8\x92\xe9\x1a\xf2\xf4\xb3\x94\x88\x87\x92\xd2L&#\x13_W\xaa@\xb5z8\xb2\xf3\xa7\x14\x16\xbc\xd2\xdbW{\xee\xb8y-\xe4\xad\x80\xd2\xd7<4\x1e\xcf\xa7\xad\xf9/\xe1\xacT\x84\xd99+\x10\x0e\xab\x85I\xc3\xd3\xb0j*\x04\xf5N\x17\xdb\x15DW\xc1u\x03\x02!\x92\\\x0e\xee\x9c>\n\x0cC\xa2j\x92\xb8\x01\x02\xc3h\xe1\xc8\x8eeQ\x10\xbd\xfc\xee\xab\xf7ah\xe56\xc8[\x0e\x9f\xbc\x10`p\xee\xfb\xe6\xcd\x8a\xe9\xa8\x96\xb3\xa3C\xc2\xfc\xdc!lxd!\xb3G\xa5S\xef\xf5QY{\x12:\xfc\xa6\xc0\xba\x0e!\xddL\xe5\x03A\xd0%\x99\x03\x13y`n.O\xdf5\x93E\x13\x87\xf1\xf4\xa62A|\x9a\xa0\xad\xe9\x10\xf5\xe5w\xb2\xb9`cd\x84@\xe1\xda^\x86\'gZ\x11c\x9a\xbfTA\xb1/\xc2\xee|\x1dG\\?\xa0g\x9c9\x85\x91\xac\xfe\xf1\xbc\xbfb\x119\xe9I\xf8\x02!Z;\x02\xec?\xee\xa6\xba\xa9\x15\xb7\xc7\x87aH\xfa\xc5X\xc9L\x89#;-\x9e\xfeqN\xec\x16\x8d\x17\x8bw\xcbU\x9b\xca\xc5I\xbf\x02\xf5\xe5\xef\xe3mZ\xcc\x96\xa7j\xba\xc9\xd3\xeb\x14\xe8\xdbV\xfe\n\x90\n\x14>\t\xb3\x7f\x9c\x8c3\xf65\x1cqW\xa1Y\x1d\xdd:~\xa7\x023\x0c\x84\x10\xac\xbcv\x12\x0f\xcc\x19K\x8a\xcb\x81USP\x15%j\xdf\xbaa\x10\xd4\r\xdcm~\xde\xff\xe20\xcf\x17\xef\xa5\xd2c\t\xe1on\xa2v\xdf\xe3\x14\xff\xf6w\xe1\xc1,l\xe9\xcb/\x1f\xba\xb1\xd3\x85\xd2\xf9\xcb\xe6a\x8fy\x00\x8bc\x06\x8a\x9a\x86\xaau\x02\xea\xda\x97\x95\x12\x02!\x90\x92!\xe9I\x0cK\x8d\'\xc6n&\x9e\x1d\x81\xa0lm\x0f\x88\x1ao\x90\xea\x9a\x16\xd3\xb8m\xf6]\xb4T\x7f\xc07\xbb\x9f\xe5\xcb\xbf\xb8\xcf\xa4J\xe7\x0f\x04\xe8\xe6\xf2.\x9b\xa90db.\xf6\x98\x19\x08\xe5\x1aTm\x0e\x8aEE\xb3t\x8f;]\xca\x80hW]\xb3\x85\xdf\x0b\x814\xaa\x11\xca\x16\x8c\xc0fj\x0el\xa7\xf4\xf5\xda\x1eg\x9d\xd5\x80\xf1|\xd6\xfceP\xf4\xb4\xf9|\xd6\x83\x02\xab\xd3\x81\xd5\x11\x83\x94\x93\x90\xc6,T\xcb8\x145\x1bEI\x02\xe1D\x08\x15)C\x08\xc5\x83P\xeb\x08u\xec#\x14\xdc\x89\xc5\xbe\x05\xa8\xa2\xb9\xa6\x83O^\n\xf4\xa5D\xf8\x7fM\xdd#\x1d\xf5i\xe0U\x00\x00\x00\x00IEND\xaeB`\x82'
fqfn = os.path.join(media_dir, 'image.png')
image_content = open(os.path.join(DIR, 'fixtures/test_image.png'), 'rb').read()
fqfn = os.path.join(self.media_dir, 'image.png')
open(fqfn, 'wb').write(image_content)
hashes = set()
# Calculate hash from various sources and confirm they all match.
expected_hash = '35830221efe45ab0dc3d91ca23c29d2d3c20d00c9afeaa096ab256ec322a7a0b3293f07a01377e31060e65b4e5f6f8fdb4c0e56bc586bba5a7ab3e6d6d97a192'
h = utils.get_text_hash(image_content)
hashes.add(h)
if verbose: print h
self.assertEqual(h, expected_hash)
h = utils.get_file_hash(fqfn)
hashes.add(h)
if verbose: print h
self.assertEqual(h, expected_hash)
h = utils.get_text_hash(open(fqfn, 'rb').read())
hashes.add(h)
if verbose: print h
h = utils.get_text_hash(open(fqfn, 'r').read())
hashes.add(h)
if verbose: print h
#print 'Hashes:', len(hashes)
self.assertEqual(h, expected_hash)
# h = utils.get_text_hash(open(fqfn, 'r').read())#not supported in py3
# self.assertEqual(h, expected_hash)
# Create test file.
self.assertEqual(len(hashes), 1)
image_content = u'aあä'
fqfn = os.path.join(media_dir, 'test.txt')
if six.PY3:
image_content = six.text_type('aあä')#, encoding='utf-8')
else:
image_content = six.text_type('aあä', encoding='utf-8')
fqfn = os.path.join(self.media_dir, 'test.txt')
open(fqfn, 'wb').write(image_content.encode('utf-8'))
hashes = set()
expected_hash = '1f40fc92da241694750979ee6cf582f2d5d7d28e18335de05abc54d0560e0f5302860c652bf08d560252aa5e74210546f369fbbbce8c12cfc7957b2652fe9a75'
h = utils.get_text_hash(image_content)
hashes.add(h)
if verbose: print h
self.assertEqual(h, expected_hash)
h = utils.get_file_hash(fqfn)
hashes.add(h)
if verbose: print h
self.assertEqual(h, expected_hash)
h = utils.get_text_hash(open(fqfn, 'rb').read())
hashes.add(h)
if verbose: print h
h = utils.get_text_hash(open(fqfn, 'r').read())
hashes.add(h)
if verbose: print h
#print 'Hashes:', len(hashes)
self.assertEqual(len(hashes), 1)
self.assertEqual(h, expected_hash)
# h = utils.get_text_hash(open(fqfn, 'r').read())
# self.assertEqual(h, expected_hash)
class DatabaseFilesViewTestCase(TestCase):
fixtures = ['test_data.json']
......@@ -141,6 +135,6 @@ class DatabaseFilesViewTestCase(TestCase):
def test_reading_file(self):
self.assertEqual(File.objects.count(), 1)
response = self.client.get('/files/1.txt')
self.assertEqual(response.content, '1234567890')
self.assertEqual(response.content, b'1234567890')
self.assertEqual(response['content-type'], 'text/plain')
self.assertEqual(unicode(response['content-length']), '10')
self.assertEqual(response['content-length'], '10')
from django.conf.urls.defaults import *
try:
# Removed in Django 1.6
from django.conf.urls.defaults import patterns, url
except ImportError:
from django.conf.urls import patterns, url
urlpatterns = patterns('',
url(r'^files/(?P<name>.+)$', 'database_files.views.serve', name='database_file'),
......
......@@ -3,12 +3,9 @@
import os
import hashlib