diff --git a/.gitignore b/.gitignore index 57e683c..af2f537 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,104 @@ -*.pyc -/build +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +.static_storage/ +.media/ +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ddd71dd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: python +python: +- '3.5' +- '3.6' +install: +- pip install tox +script: +- tox +after_success: +- coveralls +deploy: + provider: pypi + user: "Samuel.Spencer" + password: + secure: IpJuYrTkutnCuIzI61JnXoP9qkCGvvmc0wE8oBIn9HFyhWVyPw2qbNTQp58lOMOf2UVm24c3iA3Pax+1J8g9q9N144rMkgBpg5dot8KT3mt9u5TDupnKiLLl66IFh2/B68MTbx33ZCu6rCNAE49wlKGdRAf7EWIXVMJ1D7bzjtC0L5aTtML1yOc0sbsXxpFr3D0jkxP5VsTulPkKIbUrjYkphNWSLc4kobapqCwRE8tLvd50obJjl/IqGv1K+19l2TNiniDETB7R48fRenZkOIVG97y0yUP4EM71mxATU4pb/UpbenpzkhpUM+VsnUMTRht8p85Yn736m/wEmef6rCz5wccdusRW5pwy/+fJcGjci6f3Y+nWM7ZNsFmV9TvKYEhRbHWknNPWohRqSuuBHYEnX5jzosBIij1TDSpNJUXvs9GTQfSGwlEyviJUmX6f2Km1K2fdqw5FkXR9tUJyt8ezdfm4bc13DfDpeb5W1m/8dkE8ulO7TS4yp9KXqBYjkn5CduhU75lwZ+lhKfEbEzUrqoAyf4p/6wLV6J8YeEn7+BdmNp8m7lXUxZ21sIv6XacihdsGG1pqoqNNjlTLx2zm7ygtm3OyVFvz21aXBZAaGzlVeWaOcgyDMlEW+rveQ6MVgAydevX+ztzFpUCJEi2xYDTCZwt+1emFrjNWWms= + on: + branch: master + condition: $TRAVIS_PYTHON_VERSION == 3.6 \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..f982fa9 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE +include README.rst +recursive-include database_files/ * +exclude MANIFEST.in diff --git a/README.md b/README.md deleted file mode 100644 index 3cad5eb..0000000 --- a/README.md +++ /dev/null @@ -1,40 +0,0 @@ -django-database-files -===================== - -django-database-files is a storage system for Django that stores uploaded files -in the database. - -WARNING: It is generally a bad idea to serve static files from Django, -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 ------------- - - $ python setup.py install - -Usage ------ - -In ``settings.py``, add ``database_files`` to your ``INSTALLED_APPS`` and add this line: - - DEFAULT_FILE_STORAGE = 'database_files.storage.DatabaseStorage' - -Although ``upload_to`` is a required argument on ``FileField``, it is not used for -storing files in the database. Just set it to a dummy value: - - upload = models.FileField(upload_to='not required') - -All your ``FileField`` and ``ImageField`` files will now be stored in the -database. - -Test suite ----------- - - $ ./run_tests.sh - diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..d225bb9 --- /dev/null +++ b/README.rst @@ -0,0 +1,48 @@ +django-database-files +===================== + +.. image:: https://travis-ci.org/LegoStormtroopr/django-database-files.svg?branch=master + :target: https://travis-ci.org/LegoStormtroopr/django-database-files + +``django-database-files`` is a storage system for Django that stores files in the database. +It can act as a storage class anywhere that accepts the `Django storage API `_. + +WARNING: There are serious downsides to storing and serving static files from Django, +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. + +Likewise, when using systems like Heroku where no easy and persistent file system +exists, storing files in the database can provide a quick way to add file management. + + +Requires: + + * Django 1.11 or above only + * Python 3.5 or above only + +Installation +------------ + + $ pip install django-database-files-ny + +Usage +----- + +In ``settings.py``, add ``database_files`` to your ``INSTALLED_APPS`` and add this line:: + + DEFAULT_FILE_STORAGE = 'database_files.storage.DatabaseStorage' + +In your ``urls.py`` add a path to the database files:: + + url(r'^db_static/', include("database_files.urls")), + +All your ``FileField`` and ``ImageField`` files will now be stored in the +database. + +Test suite +---------- + + $ tox + diff --git a/database_files/manager.py b/database_files/manager.py index 3271a4e..8e66693 100644 --- a/database_files/manager.py +++ b/database_files/manager.py @@ -1,6 +1,7 @@ from django.db import models import os + class FileManager(models.Manager): def get_from_name(self, name): return self.get(pk=os.path.splitext(os.path.split(name)[1])[0]) diff --git a/database_files/migrations/0001_initial.py b/database_files/migrations/0001_initial.py index 7d0ebfc..bf6aada 100644 --- a/database_files/migrations/0001_initial.py +++ b/database_files/migrations/0001_initial.py @@ -1,35 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.2 on 2016-02-23 02:40 +from __future__ import unicode_literals -from south.db import db -from django.db import models -from database_files.models import * +from django.db import migrations, models -class Migration: - - def forwards(self, orm): - - # Adding model 'File' - db.create_table('database_files_file', ( - ('id', orm['database_files.File:id']), - ('content', orm['database_files.File:content']), - ('size', orm['database_files.File:size']), - )) - db.send_create_signal('database_files', ['File']) - - - - def backwards(self, orm): - - # Deleting model 'File' - db.delete_table('database_files_file') - - - - models = { - 'database_files.file': { - 'content': ('django.db.models.fields.TextField', [], {}), - 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), - 'size': ('django.db.models.fields.IntegerField', [], {}) - } - } - - complete_apps = ['database_files'] + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='FileInDatabase', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField(unique=True)), + ('size', models.IntegerField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('content', models.BinaryField()), + ], + ), + ] diff --git a/database_files/models.py b/database_files/models.py index 496ae46..1a8a1ae 100644 --- a/database_files/models.py +++ b/database_files/models.py @@ -1,9 +1,12 @@ from django.db import models from database_files.manager import FileManager -class File(models.Model): - content = models.TextField() + +class FileInDatabase(models.Model): + name = models.TextField(unique=True) size = models.IntegerField() - - objects = FileManager() + created_at = models.DateTimeField(auto_now_add=True) + modified_at = models.DateTimeField(auto_now=True) + content = models.BinaryField() + objects = FileManager() diff --git a/database_files/storage.py b/database_files/storage.py index 681d2e3..2a7bbaa 100644 --- a/database_files/storage.py +++ b/database_files/storage.py @@ -1,57 +1,121 @@ import base64 from database_files import models -from django.core import files +from django.core.files.base import File from django.core.files.storage import Storage -from django.core.urlresolvers import reverse -import os -import StringIO +from django.urls import reverse +from io import StringIO, BytesIO + + +class DBFile(File): + + """ + A file returned from the database. + """ + + def __init__(self, file, name, storage): + super().__init__(file, name) + self._storage = storage + + def open(self, mode="rb"): + # if self.closed: + self.file = self._storage.open(self.name, mode).file + return super().open(mode) + class DatabaseStorage(Storage): - def _generate_name(self, name, pk): - """ - Replaces the filename with the specified pk and removes any dir - """ - dir_name, file_name = os.path.split(name) - file_root, file_ext = os.path.splitext(file_name) - return '%s%s' % (pk, file_ext) - + + def _make_name(self, name): + name = "/" + name.lstrip("/") + return name + def _open(self, name, mode='rb'): + name = self._make_name(name) try: - f = models.File.objects.get_from_name(name) - except models.File.DoesNotExist: + f = models.FileInDatabase.objects.get(name=name) + except models.FileInDatabase.DoesNotExist: return None - fh = StringIO.StringIO(base64.b64decode(f.content)) + + if 'b' in mode: + fh = BytesIO(base64.b64decode(f.content)) + else: + fh = StringIO(base64.b64decode(f.content).decode('utf-8')) fh.name = name fh.mode = mode fh.size = f.size - return files.File(fh) - + return DBFile(fh, name, self) + def _save(self, name, content): - f = models.File.objects.create( - content=base64.b64encode(content.read()), + name = self._make_name(name) + # if 'b' in content.mode: + _content = content.read() + # else: + # _content = content.read().encode('utf-8') + + models.FileInDatabase.objects.create( + content=base64.b64encode(_content), size=content.size, + name=name, ) - return self._generate_name(name, f.pk) - + + return name + def exists(self, name): - """ - We generate a new filename for each file, so it will never already - exist. - """ - return False - + name = self._make_name(name) + return models.FileInDatabase.objects.filter(name=name).exists() + def delete(self, name): + name = self._make_name(name) try: - models.File.objects.get_from_name(name).delete() - except models.File.DoesNotExist: + models.FileInDatabase.objects.get(name=name).delete() + except models.FileInDatabase.DoesNotExist: pass - + def url(self, name): return reverse('database_file', kwargs={'name': name}) - + def size(self, name): + name = self._make_name(name) try: - return models.File.objects.get_from_name(name).size - except models.File.DoesNotExist: + return models.FileInDatabase.objects.get(name=name).size + except models.FileInDatabase.DoesNotExist: return 0 + def listdir(self, path): + path = self._make_name(path) + + path = path.rstrip("/") + "/" + results = list( + models.FileInDatabase.objects.filter(name__startswith=path).values_list("name", flat=True) + ) + + _files = [] + dirs = [] + path = path.rstrip("/") + sub_path = len(path.split("/")) + for r in results: + parts = r.split("/")[sub_path:] + # If there is only one more part its a file + if len(parts) == 1: + _files.append(parts[0]) + else: + dirs.append(parts[0]) + dirs = list(set(dirs)) + _files.sort() + dirs.sort() + return dirs, _files + + def get_created_time(self, name): + name = self._make_name(name) + try: + f = models.FileInDatabase.objects.get(name=name) + return f.created_at + except models.FileInDatabase.DoesNotExist: + return None + + def get_modified_time(self, name): + name = self._make_name(name) + try: + f = models.FileInDatabase.objects.get(name=name) + return f.modified_at + except models.FileInDatabase.DoesNotExist: + return None diff --git a/database_files/tests/files/lena.png b/database_files/tests/files/lena.png new file mode 100644 index 0000000..59ef68a Binary files /dev/null and b/database_files/tests/files/lena.png differ diff --git a/database_files/tests/files/lorem.txt b/database_files/tests/files/lorem.txt new file mode 100644 index 0000000..4897035 --- /dev/null +++ b/database_files/tests/files/lorem.txt @@ -0,0 +1,2 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + diff --git a/database_files/tests/files/other_files/ipsum.txt b/database_files/tests/files/other_files/ipsum.txt new file mode 100644 index 0000000..4897035 --- /dev/null +++ b/database_files/tests/files/other_files/ipsum.txt @@ -0,0 +1,2 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + diff --git a/database_files/tests/files/other_files/lena.png b/database_files/tests/files/other_files/lena.png new file mode 100644 index 0000000..59ef68a Binary files /dev/null and b/database_files/tests/files/other_files/lena.png differ diff --git a/database_files/tests/files/qq/qqqq.qqq b/database_files/tests/files/qq/qqqq.qqq new file mode 100644 index 0000000..b984abc --- /dev/null +++ b/database_files/tests/files/qq/qqqq.qqq @@ -0,0 +1,39 @@ +Qqqq Qqqqqqq, + +Qqqq qqqq qq qqqq, qq, QQQ, qqqq qq qqqq qq'q q qqq qq qqqqqq. Q qqqq qqqq qqqqq qqqqq qqq qqq qqqq qqqqqqq qq'q qqq qqqqq qq qq qqqqqqqq qqqq qqq qqqqqq q qqq qq qqqq qqqqqq. + + + +Qqqqqqqq qqqqq qqqqqqqqq qq qqqqq qq qqqqq qqqqq qqqqqqqqq qqq qqqqqqqq qqqq, qqqqq qqqqqqqqq qqq qqqqq qqqqq qqqqq qqqqqqqq qqqqqqqq qqq qqqqqq qqq q qqqqqqqqq qqqqqq qq qqqqqqq qqqq qqqq qqqqq qqqq qq qqqqqq qqqqqqqqqq qq qqq-qqqqqqqq. + +Qq, Q'q qqqqqqq qqqq qqq qqqq qqqqqqqq. Q qqqq qqqqqq qqq qq qqq. Q qqqq qqqq Q'q qqqqqq qqqq qqq. Qqqqqqq qqqq qqqqqq qqq qqqq qqqq Q qq qqq q qqq qqqqqqq qqqqqqq qq q qqqqqqq qq qqqq qq qqqq, qqq qqqqqqqqqq qqqqqqq qqqqqqq qq q qqqq qqq qqqqq qqqq q qqq. Qqq qq Q qqqqq qq qqqq qqqqqqq qqqq qqqqqq. + + + +Qq qqqq, Q qqqq qq qqqqq qqqq qqq. Q'qq qqqqqqq qq qqqqq qqq qq qqqqq qqqqqq, qqq qqq qq qqqq qqq qqq qq qqqqqq qqqqq qqqq Q qqqqqqq Q qqq qqqq qqqqqqqqq qq qqqqq qqq. Q qqq'q qqqqqqq qqq qqqqqqqqqq Q qqq qq qqqqq qqqq qqqq qqq qqqqq qqq qqqqq! Qq qqqq qqq qqqqq qqqq qqqq, Q'q qqqqq qqqqqqqqq qqq qq qqqqqqqqqqqqq, qqqqqqq qq'q qqq qqqqq qq. + + + +Qq Q qqqq qq qqq qqq qqqqqq qq qqq qqqq qq qqqqqqq qqqqqq qq qqqqq qqqqqq qq qqqq qqqq, Q'q qqqq qq. Qqqqqqq, Q'q qqqqqqq qq qqqqq qq qqqq qqqqqqqqqq qqqq. Q qqqq q qqqqq qqqqqq, q qqqqqqqqqq qqqq qqqq qqqq qqqqqq qq q qqqqqqqqq qqqqqqqqqq qqq qq "qq" qqqq qqqqqq qq qqqqqq qq qq qqqqq, qqqqqqqqq qqqqqqqqqqqqqqq qqqqqqqq qqqq. Qqq qq Q qqq qqq qqqqqq qqq qq qqqq? + +Qqqqqqqqq qqq qqqqqqq qq qq. Q qqqq qqqqq qq qqq Q'q qqqqqqq qqqq qqqq qqqqqq qqqq. Qq qqqqqq qqqq Q qq qqqqqq qqqqqqq Q'q q qqqqq qq qqq qqqqqqqq qqq qqqqq qq qqqq qqq qqq qqqq qqqqqq qqqq qqqq qqq qqqqqq qq qqq qqqqqqqqqq qqqqqqq. Qqq qqqqqqqq qq qqqqq qqqqqq qq qqqqqqqqqq, qqq Q qqqq qqqq qqqq qq qqq qqqq qqqq qqqqqqqq qq qqqqqqq qqqqqqqqq. + + + +Qq, qqq. QQQ QQQ QQ!! Qq'q qq qqqq qqqq qqqqqqqq, Qqqq Qqqqqq. QQQ Q qqqq qqq qqq qqqqqq qqqqqqq, qqq qqqq qqqqqqqqqqq qqqq qqq qqqq'q qqqqqqqqq. + +Qq qqqq'q qq qqqqqqq qqq qqq qqq: + +QQQQ QQQ, QQQQ QQQ QQQQQQQ "QQQQQQ" QQQQQQ, QQQQ QQQ QQQQQQQQQQQQ, QQQQ QQQ QQQQQQQQ, QQQQQQ, QQQQQQQ, QQQ QQQQQQQ QQQ QQQQ QQQQ QQQQQ!!!!! + +QQQQ QQQ QQQ QQQ!!!! + +Q'Q QQQQQ QQQQ, QQQ QQ QQQ QQQQQQ QQQQQQQ!!!! + + + +QQQQ + +QQQ!!!!!!!!! + +(USER WAS BANNED FOR THIS POST) \ No newline at end of file diff --git a/database_files/tests/models.py b/database_files/tests/models.py index 91ec2b1..a7ee168 100644 --- a/database_files/tests/models.py +++ b/database_files/tests/models.py @@ -1,5 +1,5 @@ from django.db import models -class Thing(models.Model): - upload = models.FileField(upload_to='not required') +class Thing(models.Model): + upload = models.FileField(upload_to='documents') diff --git a/database_files/tests/settings.py b/database_files/tests/settings.py index e454934..626e39e 100644 --- a/database_files/tests/settings.py +++ b/database_files/tests/settings.py @@ -9,3 +9,11 @@ 'database_files.tests', ] DEFAULT_FILE_STORAGE = 'database_files.storage.DatabaseStorage' +SECRET_KEY = "not a secret" + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ":memory:" + } +} diff --git a/database_files/tests/tests.py b/database_files/tests/tests.py index 237bdec..a65ed28 100644 --- a/database_files/tests/tests.py +++ b/database_files/tests/tests.py @@ -1,34 +1,109 @@ +from tempfile import NamedTemporaryFile from django.core import files from django.test import TestCase -from database_files.models import File +from database_files.models import FileInDatabase from database_files.tests.models import Thing -import StringIO +import os +from django.core.files.storage import default_storage + +this_dir = os.path.dirname(os.path.abspath(__file__)) + class DatabaseFilesTestCase(TestCase): - def test_adding_file(self): - test_file = files.temp.NamedTemporaryFile( - suffix='.txt', - dir=files.temp.gettempdir() + def test_adding_text_file(self): + test_file = NamedTemporaryFile( + mode="w+", suffix='.txt', delete=False ) test_file.write('1234567890') - test_file.seek(0) + test_file.close() + + f = open(test_file.name, mode="r+b") + t = Thing.objects.create( - upload=files.File(test_file), + upload=files.File(f, name=f.name.lstrip('/')), ) - self.assertEqual(File.objects.count(), 1) + self.assertEqual(FileInDatabase.objects.count(), 1) t = Thing.objects.get(pk=t.pk) + self.assertEqual(t.upload.file.size, 10) - self.assertEqual(t.upload.file.name[-4:], '.txt') + self.assertTrue(t.upload.file.name.endswith('.txt')) + + t.upload.file.open(mode="rb") + + self.assertEqual(t.upload.file.read(), b'1234567890') + + t.upload.file.open(mode="rb") + self.assertEqual(t.upload.file.read().decode('utf-8'), '1234567890') + + t.upload.file.open(mode="r") self.assertEqual(t.upload.file.read(), '1234567890') + self.assertTrue(default_storage.exists(t.upload.file.name)) t.upload.delete() - self.assertEqual(File.objects.count(), 0) + self.assertEqual(FileInDatabase.objects.count(), 0) + + # discard file + os.unlink(f.name) + + def test_adding_binary_file(self): + lena = os.path.join(this_dir, "files", "lena.png") + f = open(lena, mode="rb") + size = os.path.getsize(lena) -class DatabaseFilesViewTestCase(TestCase): - fixtures = ['test_data.json'] - - def test_reading_file(self): - response = self.client.get('/1.txt') - self.assertEqual(response.content, '1234567890') - self.assertEqual(response['content-type'], 'text/plain') - self.assertEqual(unicode(response['content-length']), '10') + t = Thing.objects.create( + upload=files.File(f, name="images/lena.png"), + ) + self.assertEqual(FileInDatabase.objects.count(), 1) + t = Thing.objects.get(pk=t.pk) + self.assertEqual(t.upload.file.size, size) + self.assertTrue(t.upload.file.name.endswith('.png')) + self.assertEqual(t.upload.file.name, "/documents/images/lena.png") + self.assertTrue(default_storage.exists("/documents/images/lena.png")) + + from PIL import Image + + t.upload.file.open(mode="rb") + i1 = Image.open(t.upload.file) + i2 = Image.open(lena) + self.assertEqual(i1.size, i2.size) + + def test_listing_directory(self): + files_dir = os.path.join(this_dir, "files") + num_files = 0 + for subdir, dirs, _files in os.walk(files_dir): + subdir = subdir[len(files_dir) + 1:] + for fn in _files: + num_files += 1 + with open(os.path.join(files_dir, subdir, fn), 'rb') as f: + path = os.path.join("mydocs", subdir, fn) + default_storage.save(name=path, content=f) + + for subdir, dirs, _files in os.walk(files_dir): + subdir = subdir[len(files_dir) + 1:] + path = os.path.join("mydocs", subdir) + listed_dirs, listed_files = default_storage.listdir(path) + self.assertEqual(listed_dirs, sorted(dirs)) + self.assertEqual(listed_files, sorted(_files)) + + def test_access_times(self): + qqq = os.path.join(this_dir, "files", "qq", "qqqq.qqq") + f = open(qqq, mode="rb") + + t = Thing.objects.create( + upload=files.File(f, name="qqqq.qqq"), + ) + self.assertEqual(FileInDatabase.objects.count(), 1) + t = Thing.objects.get(pk=t.pk) + + file_in_db = FileInDatabase.objects.get(name=t.upload.name) + created = file_in_db.created_at + modifed = file_in_db.modified_at + + self.assertEqual( + default_storage.get_created_time(t.upload.name), + created + ) + self.assertEqual( + default_storage.get_modified_time(t.upload.name), + modifed + ) diff --git a/database_files/urls.py b/database_files/urls.py index 5f575c9..29a9aea 100644 --- a/database_files/urls.py +++ b/database_files/urls.py @@ -1,5 +1,8 @@ -from django.conf.urls.defaults import * +from django.conf.urls import url +from django.views.decorators.cache import cache_page +from database_files.views import serve # , serve_thumb -urlpatterns = patterns('', - url(r'^(?P.+)$', 'database_files.views.serve', name='database_file'), -) +urlpatterns = [ + # url(r'^t/(?P\d+)/(?P\d+)/(?P.+)$', serve_thumb, name='database_file_thumb'), + url(r'^/?(?P.+)$', cache_page(60 * 15)(serve), name='database_file'), +] diff --git a/database_files/views.py b/database_files/views.py index 2f08960..31e5421 100644 --- a/database_files/views.py +++ b/database_files/views.py @@ -1,20 +1,39 @@ import base64 -from django.http import Http404, HttpResponse +from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.views.decorators.cache import cache_control import mimetypes -from database_files.models import File -import os +from database_files.models import FileInDatabase +# import os +# from PIL import Image +# from io import StringIO + + +def get_file(name): + name = "/" + name.lstrip("/") + f = get_object_or_404(FileInDatabase, name=name) + return f + @cache_control(max_age=86400) def serve(request, name): - pk, file_ext = os.path.splitext(name) - try: - pk = int(pk) - except ValueError: - raise Http404('Filename is not an integer') - f = get_object_or_404(File, pk=pk) + f = get_file(name) + return return_image_response(name, base64.b64decode(f.content), f.size) + + +# @cache_control(max_age=86400) +# def serve_thumb(request, name, x, y): +# f = get_image(name) +# stream = StringIO(base64.b64decode(f.content)) +# output = StringIO() +# thumb = Image.open(stream) +# thumb.thumbnail((int(x),int(y))) +# thumb.save(output, format='JPEG') +# return return_image_response(name, output.getvalue(), len(output.getvalue())) + + +def return_image_response(name, content, size): mimetype = mimetypes.guess_type(name)[0] or 'application/octet-stream' - response = HttpResponse(base64.b64decode(f.content), mimetype=mimetype) - response['Content-Length'] = f.size + response = HttpResponse(content, content_type=mimetype) + response['Content-Length'] = size return response diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 36c3d78..0000000 --- a/run_tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -PYTHONPATH=. DJANGO_SETTINGS_MODULE="database_files.tests.settings" django-admin.py test tests - diff --git a/setup.py b/setup.py index 52eda75..eeec396 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,24 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from distutils.core import setup - +from setuptools import setup, find_packages + + setup( - name='django-database-files', - version='0.1', + name='django-database-files-ny', + version='0.2.3', + packages=find_packages(), description='A storage system for Django that stores uploaded files in the database.', - author='Ben Firshman', - author_email='ben@firshman.co.uk', - url='http://github.com/bfirsh/django-database-files/', - packages=[ - 'database_files', + author='Sam Spencer', + author_email='sam@aristotlemetadata.com', + url='http://github.com/legostormtroopr/django-database-files/', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', ], - classifiers=['Development Status :: 4 - Beta', - 'Framework :: Django', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - ], + install_requires=[ + ] ) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b127fd6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,26 @@ +[tox] +skipsdist = True +envlist = + ddf-dj{1.11,2.0} + flake8 + +[testenv] +setenv = + PYTHONPATH= {toxinidir}:{toxinidir} + DJANGO_SETTINGS_MODULE= database_files.tests.settings +platform = + windows: win32 + linux: linux +passenv = + LC_ALL + LANG +deps = + dj1.11: django>=1.11,<2.0 + dj2.0: django>=1.11,<2.1 + flake8: flake8>=2.0,<3.0 + ddf: coverage + ddf: Pillow + ddf: . +commands = + flake8: flake8 --ignore=E501 + ddf: coverage run --branch --parallel-mode --source=database_files {envbindir}/django-admin test database_files.tests \ No newline at end of file