separate filename and filehash (#21)
* separate filename and filehash * rename migration * test url * bump version * update changelog * test fail url * update coveragerc * add coveragerc * EOFNL * update covrc * filename
This commit is contained in:
parent
e50dc36942
commit
09d721df0d
@ -11,3 +11,4 @@ exclude_lines =
|
|||||||
ignore_errors = True
|
ignore_errors = True
|
||||||
omit =
|
omit =
|
||||||
tests/*
|
tests/*
|
||||||
|
dbfilestorage/migrations/*
|
||||||
|
@ -54,6 +54,7 @@ I will sign everything with 0x769A1BC78A2DDEE2
|
|||||||
|
|
||||||
## CHANGELOG
|
## 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] Add signing key to readme.
|
||||||
- 2016-12-09 [Tyrel Souza] Update Tests, add some cleanup.
|
- 2016-12-09 [Tyrel Souza] Update Tests, add some cleanup.
|
||||||
- 2016-12-08 [Tyrel Souza] Add more documentation.
|
- 2016-12-08 [Tyrel Souza] Add more documentation.
|
||||||
|
43
dbfilestorage/migrations/0002_add_filehash_rename_files.py
Normal file
43
dbfilestorage/migrations/0002_add_filehash_rename_files.py
Normal file
@ -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),
|
||||||
|
]
|
@ -7,15 +7,16 @@ class DBFile(models.Model):
|
|||||||
# This is kept as `name` and not something like `md5` because the file
|
# 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
|
# operations pass around `name` as the identifier, so it's kept the same
|
||||||
# to make sense.
|
# 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
|
# file data
|
||||||
content_type = models.CharField(max_length=100)
|
content_type = models.CharField(max_length=100)
|
||||||
b64 = models.TextField()
|
b64 = models.TextField()
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u"{name} <{content_type}>".format(
|
return u"{filehash} <{content_type}>".format(
|
||||||
name=self.name, content_type=self.content_type)
|
filehash=self.filehash, content_type=self.content_type)
|
||||||
|
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
if self.content_type is None:
|
if self.content_type is None:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
|
|
||||||
from django.db.transaction import atomic
|
from django.db.transaction import atomic
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
@ -50,16 +51,17 @@ class DBFileStorage(Storage):
|
|||||||
ct = mimetypes.guess_type(name)[0]
|
ct = mimetypes.guess_type(name)[0]
|
||||||
|
|
||||||
# After we get the mimetype by name potentially, mangle it.
|
# 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
|
# create the file, or just return name if the exact file already exists
|
||||||
if not DBFile.objects.filter(pk=name).exists():
|
if not DBFile.objects.filter(pk=name).exists():
|
||||||
the_file = DBFile(
|
the_file = DBFile(
|
||||||
name=name,
|
name=os.path.basename(name),
|
||||||
|
filehash=filehash,
|
||||||
content_type=ct,
|
content_type=ct,
|
||||||
b64=b64)
|
b64=b64)
|
||||||
the_file.save()
|
the_file.save()
|
||||||
return name
|
return filehash
|
||||||
|
|
||||||
def get_available_name(self, name, max_length=None):
|
def get_available_name(self, name, max_length=None):
|
||||||
return name
|
return name
|
||||||
|
@ -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 django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from .models import DBFile
|
from .models import DBFile
|
||||||
@ -12,7 +13,12 @@ def show_file(request, name):
|
|||||||
|
|
||||||
:return HttpResponse: Rendered file
|
:return HttpResponse: Rendered file
|
||||||
"""
|
"""
|
||||||
dbf = get_object_or_404(DBFile, pk=name)
|
dbf = DBFile.objects.filter(Q(name=name)|Q(filehash=name))
|
||||||
return HttpResponse(
|
if dbf.exists():
|
||||||
dbf.b64.decode('base64'),
|
response = HttpResponse(
|
||||||
content_type=dbf.content_type)
|
dbf[0].b64.decode('base64'),
|
||||||
|
content_type=dbf[0].content_type)
|
||||||
|
response['Content-Disposition'] = 'attachment; filename="{}"'.format(
|
||||||
|
dbf[0].name)
|
||||||
|
return response
|
||||||
|
raise Http404
|
||||||
|
@ -54,9 +54,9 @@ author = u'Tyrel Souza'
|
|||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = u'0.1.3'
|
version = u'0.1.4'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# 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
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
2
setup.py
2
setup.py
@ -24,7 +24,7 @@ class CleanCommand(Command):
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="django-dbfilestorage",
|
name="django-dbfilestorage",
|
||||||
version="0.1.3",
|
version="0.1.4",
|
||||||
description="Database backed file storage for testing.",
|
description="Database backed file storage for testing.",
|
||||||
long_description="Database backed file storage for testing. Stores files as base64 encoded textfields.",
|
long_description="Database backed file storage for testing. Stores files as base64 encoded textfields.",
|
||||||
author="Tyrel Souza",
|
author="Tyrel Souza",
|
||||||
|
@ -30,14 +30,14 @@ class DBFileTest(TestCase):
|
|||||||
|
|
||||||
def test_upload(self):
|
def test_upload(self):
|
||||||
""" Test that the file storage uploads and puts in DB Properly """
|
""" 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):
|
def test_content_file(self):
|
||||||
""" Test that this code works with ContentFile as well """
|
""" Test that this code works with ContentFile as well """
|
||||||
content_file = ContentFile(u"ΑΔΔGΕΝΕ")
|
content_file = ContentFile(u"ΑΔΔGΕΝΕ")
|
||||||
content_file_md5 = hashlib.md5(u"ΑΔΔGΕΝΕ".encode('utf8')).hexdigest()
|
content_file_md5 = hashlib.md5(u"ΑΔΔGΕΝΕ".encode('utf8')).hexdigest()
|
||||||
default_storage.save("unicode", content_file)
|
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),
|
self.assertEqual(unicode(unicode_file),
|
||||||
"{} <application/octet-stream>".format(content_file_md5))
|
"{} <application/octet-stream>".format(content_file_md5))
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class DBFileTest(TestCase):
|
|||||||
def test_equality(self):
|
def test_equality(self):
|
||||||
""" Test that the DB entry matches what is expected from the file """
|
""" Test that the DB entry matches what is expected from the file """
|
||||||
with open(self.filepath, 'rb') as f:
|
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.b64.decode("base64"), f.read())
|
||||||
self.assertEqual(dbf.content_type, 'image/jpeg')
|
self.assertEqual(dbf.content_type, 'image/jpeg')
|
||||||
|
|
||||||
@ -66,9 +66,9 @@ class DBFileTest(TestCase):
|
|||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
""" Test Deletion """
|
""" 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)
|
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,
|
# Also test that calling delete on something that doesn't exist,
|
||||||
# errors silently
|
# errors silently
|
||||||
self.assertFalse(DBFile.objects.filter(name="Nothing").exists())
|
self.assertFalse(DBFile.objects.filter(name="Nothing").exists())
|
||||||
@ -91,9 +91,18 @@ class DBFileTest(TestCase):
|
|||||||
|
|
||||||
def test_view(self):
|
def test_view(self):
|
||||||
client = Client()
|
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)
|
resp = client.get(url)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
|
||||||
def test_admin(self):
|
def test_admin(self):
|
||||||
my_admin = User.objects.create_superuser(
|
my_admin = User.objects.create_superuser(
|
||||||
|
Loading…
Reference in New Issue
Block a user