diff --git a/.coveragerc b/.coveragerc index e227db0..b34a9e1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,3 +11,4 @@ exclude_lines = ignore_errors = True omit = tests/* + dbfilestorage/migrations/* diff --git a/README.md b/README.md index 0fdb402..3d93d72 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ I will sign everything with 0x769A1BC78A2DDEE2 ## CHANGELOG +- 2017-01-20 [Tyrel Souza] Split filename and filehash. - 2016-12-09 [Tyrel Souza] Add signing key to readme. - 2016-12-09 [Tyrel Souza] Update Tests, add some cleanup. - 2016-12-08 [Tyrel Souza] Add more documentation. diff --git a/dbfilestorage/migrations/0002_add_filehash_rename_files.py b/dbfilestorage/migrations/0002_add_filehash_rename_files.py new file mode 100644 index 0000000..4eca219 --- /dev/null +++ b/dbfilestorage/migrations/0002_add_filehash_rename_files.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-01-20 18:44 +from __future__ import unicode_literals + +from django.db import migrations, models + +def copy_filehash(apps, schema_editor): + DBFile = apps.get_model('dbfilestorage', 'DBFile') + for dbf in DBFile.objects.all(): + dbf.filehash = dbf.name + dbf.save() + +def fix_filename(apps, schema_editor): + DBFile = apps.get_model('dbfilestorage', 'DBFile') + for dbf in DBFile.objects.all(): + ext = dbf.content_type.split("/")[0] + dbf.name = "{name}.{ext}".format( + name=dbf.name, + ext=ext) + dbf.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('dbfilestorage', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='dbfile', + name='filehash', + field=models.CharField(default='filehash', max_length=32, primary_key=True, serialize=False), + preserve_default=False, + ), + migrations.AlterField( + model_name='dbfile', + name='name', + field=models.CharField(max_length=100), + ), + migrations.RunPython(copy_filehash), + migrations.RunPython(fix_filename), + ] diff --git a/dbfilestorage/models.py b/dbfilestorage/models.py index f318795..600a66d 100644 --- a/dbfilestorage/models.py +++ b/dbfilestorage/models.py @@ -7,15 +7,16 @@ class DBFile(models.Model): # This is kept as `name` and not something like `md5` because the file # operations pass around `name` as the identifier, so it's kept the same # to make sense. - name = models.CharField(max_length=100, primary_key=True) + name = models.CharField(max_length=100) + filehash = models.CharField(max_length=32, primary_key=True) # file data content_type = models.CharField(max_length=100) b64 = models.TextField() def __unicode__(self): - return u"{name} <{content_type}>".format( - name=self.name, content_type=self.content_type) + return u"{filehash} <{content_type}>".format( + filehash=self.filehash, content_type=self.content_type) def save(self, **kwargs): if self.content_type is None: diff --git a/dbfilestorage/storage.py b/dbfilestorage/storage.py index 55d8fce..ab95411 100644 --- a/dbfilestorage/storage.py +++ b/dbfilestorage/storage.py @@ -1,6 +1,7 @@ import mimetypes import logging import hashlib +import os from django.db.transaction import atomic from django.core.files.base import ContentFile @@ -50,16 +51,17 @@ class DBFileStorage(Storage): ct = mimetypes.guess_type(name)[0] # After we get the mimetype by name potentially, mangle it. - name = hashlib.md5(read_data).hexdigest() + filehash = hashlib.md5(read_data).hexdigest() # create the file, or just return name if the exact file already exists if not DBFile.objects.filter(pk=name).exists(): the_file = DBFile( - name=name, + name=os.path.basename(name), + filehash=filehash, content_type=ct, b64=b64) the_file.save() - return name + return filehash def get_available_name(self, name, max_length=None): return name diff --git a/dbfilestorage/views.py b/dbfilestorage/views.py index 8f04488..77c311d 100644 --- a/dbfilestorage/views.py +++ b/dbfilestorage/views.py @@ -1,4 +1,5 @@ -from django.http import HttpResponse +from django.http import HttpResponse, Http404 +from django.db.models import Q from django.shortcuts import get_object_or_404 from .models import DBFile @@ -12,7 +13,12 @@ def show_file(request, name): :return HttpResponse: Rendered file """ - dbf = get_object_or_404(DBFile, pk=name) - return HttpResponse( - dbf.b64.decode('base64'), - content_type=dbf.content_type) + dbf = DBFile.objects.filter(Q(name=name)|Q(filehash=name)) + if dbf.exists(): + response = HttpResponse( + dbf[0].b64.decode('base64'), + content_type=dbf[0].content_type) + response['Content-Disposition'] = 'attachment; filename="{}"'.format( + dbf[0].name) + return response + raise Http404 diff --git a/docs/conf.py b/docs/conf.py index 89a0921..5cde4f3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -54,9 +54,9 @@ author = u'Tyrel Souza' # built documents. # # The short X.Y version. -version = u'0.1.3' +version = u'0.1.4' # The full version, including alpha/beta/rc tags. -release = u'0.1.3' +release = u'0.1.4' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 49bbf79..d4dbf82 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ class CleanCommand(Command): setup( name="django-dbfilestorage", - version="0.1.3", + version="0.1.4", description="Database backed file storage for testing.", long_description="Database backed file storage for testing. Stores files as base64 encoded textfields.", author="Tyrel Souza", diff --git a/tests/tests.py b/tests/tests.py index 7a08051..0b00f22 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -30,14 +30,14 @@ class DBFileTest(TestCase): def test_upload(self): """ Test that the file storage uploads and puts in DB Properly """ - self.assertTrue(DBFile.objects.filter(name=self.md5).exists()) + self.assertTrue(DBFile.objects.filter(filehash=self.md5).exists()) def test_content_file(self): """ Test that this code works with ContentFile as well """ content_file = ContentFile(u"ΑΔΔGΕΝΕ") content_file_md5 = hashlib.md5(u"ΑΔΔGΕΝΕ".encode('utf8')).hexdigest() default_storage.save("unicode", content_file) - unicode_file = DBFile.objects.get(name=content_file_md5) + unicode_file = DBFile.objects.get(filehash=content_file_md5) self.assertEqual(unicode(unicode_file), "{} ".format(content_file_md5)) @@ -50,7 +50,7 @@ class DBFileTest(TestCase): def test_equality(self): """ Test that the DB entry matches what is expected from the file """ with open(self.filepath, 'rb') as f: - dbf = DBFile.objects.get(name=self.md5) + dbf = DBFile.objects.get(filehash=self.md5) self.assertEqual(dbf.b64.decode("base64"), f.read()) self.assertEqual(dbf.content_type, 'image/jpeg') @@ -66,9 +66,9 @@ class DBFileTest(TestCase): def test_delete(self): """ Test Deletion """ - self.assertTrue(DBFile.objects.filter(name=self.md5).exists()) + self.assertTrue(DBFile.objects.filter(filehash=self.md5).exists()) default_storage.delete(self.md5) - self.assertFalse(DBFile.objects.filter(name=self.md5).exists()) + self.assertFalse(DBFile.objects.filter(filehash=self.md5).exists()) # Also test that calling delete on something that doesn't exist, # errors silently self.assertFalse(DBFile.objects.filter(name="Nothing").exists()) @@ -91,9 +91,18 @@ class DBFileTest(TestCase): def test_view(self): client = Client() - url = default_storage.url(self.md5) + # check it works for both md5 and filename + for param in (self.md5, self.filename): + url = default_storage.url(param) + resp = client.get(url) + self.assertEqual(resp.status_code, 200) + + def test_view_fails(self): + client = Client() + url = default_storage.url("failure") resp = client.get(url) - self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.status_code, 404) + def test_admin(self): my_admin = User.objects.create_superuser(