diff --git a/assetkit/files/storage.py b/assetkit/files/storage.py index 3c1090b..68589f3 100644 --- a/assetkit/files/storage.py +++ b/assetkit/files/storage.py @@ -13,12 +13,7 @@ from django.utils.encoding import filepath_to_uri from django.utils.six.moves.urllib.parse import urljoin -__all__ = ('MediaAssetStorage',) - - -ORIGINAL_FILE_PREFIX = 'original' -CACHED_VARIANT_PREFIX = 'cache' -MANUAL_VARIANT_PREFIX = 'manual' +__all__ = ('ProtectedMediaAssetStorage',) """ @@ -29,9 +24,17 @@ Storage setup: /manual//. TODO Add snapshot (versioning) to path after asset_uuid. - """ +FILENAME_MAX_LENGTH = getattr(settings, 'ASSETKIT_FILENAME_MAX_LENGTH', 255) + +ORIGINAL_FILE_PREFIX = 'original' +CACHED_VARIANT_PREFIX = 'cache' +MANUAL_VARIANT_PREFIX = 'manual' + +PROTECTED_MEDIA_ROOT = os.path.join(os.path.dirname(settings.MEDIA_ROOT, 'protected')) +PROTECTED_MEDIA_URL = '/protected/' + class BaseVariantSubStorage(FileSystemStorage): def delete(self, parent_storage, name): @@ -42,23 +45,25 @@ class BaseVariantSubStorage(FileSystemStorage): @deconstructible -class MediaAssetStorage(FileSystemStorage): +class ProtectedMediaAssetStorage(FileSystemStorage): """ Standard media assets filesystem storage """ - def __init__(self, location=None, base_url=None, file_permissions_mode=None, - directory_permissions_mode=None): + ORIGINAL_FILE_PREFIX = ORIGINAL_FILE_PREFIX + FILENAME_MAX_LENGTH = FILENAME_MAX_LENGTH + + def __init__(self, location=None, base_url=None, **kwargs): if location is None: - location = getattr(settings, 'MEDIA_ASSETS_ROOT', settings.MEDIA_ROOT) + location = getattr(settings, 'PROTECTED_MEDIA_ROOT', PROTECTED_MEDIA_ROOT) if base_url is None: - base_url = getattr(settings, 'MEDIA_ASSETS_URL', settings.MEDIA_URL) + base_url = getattr(settings, 'PROTECTED_MEDIA_URL', PROTECTED_MEDIA_URL) if not base_url.endswith('/'): base_url += '/' - super(MediaAssetStorage, self).__init__(self) + super(ProtectedMediaAssetStorage, self).__init__(location=location, base_url=base_url, **kwargs) def delete(self, name): - super(MediaAssetStorage, self).delete(name) + super(ProtectedMediaAssetStorage, self).delete(name) # FIXME Delete all cached files, too warnings.warn("Cached files for asset \"%s\" are not deleted." % name) @@ -66,12 +71,10 @@ class MediaAssetStorage(FileSystemStorage): """ `name` must already contain the whole asset filesystem path. """ - return safe_join(self.location, ORIGINAL_FILE_PREFIX, name) + return safe_join(self.location, self.ORIGINAL_FILE_PREFIX, name) def size(self, name): return os.path.getsize(self.path(name)) def url(self, name): - if self.base_url is None: - raise ValueError("This file is not accessible via a URL.") - return urljoin(self.base_url, ORIGINAL_FILE_PREFIX, filepath_to_uri(name)) + return urljoin(self.base_url, filepath_to_uri(name)) diff --git a/example_site/main/medialibrary/fields.py b/example_site/main/medialibrary/fields.py index a3c11cf..898079e 100644 --- a/example_site/main/medialibrary/fields.py +++ b/example_site/main/medialibrary/fields.py @@ -2,42 +2,18 @@ from __future__ import unicode_literals # Erik Stein , 2016 -import os from django import forms from django.contrib.admin.widgets import AdminFileWidget from django.db import models from django.db.models.fields.files import FileDescriptor -from django.utils.html import conditional_escape -from django.utils.text import slugify +from django.utils.functional import curry -from ..storages import ProtectedMediaAssetStorage +from assetkit.files.storage import ProtectedMediaAssetStorage +from .utils import get_upload_path # TODO Define the central storage somewhere more central -ORIGINALS_STORAGE = ProtectedMediaAssetStorage() -# TODO Move filename max length to storage -FILENAME_MAX_LENGTH = 255 - - -# TOD Move get_upload_path to utils -def get_upload_path(instance, filename): - """ - Returns //original/ - where - - uuid is taken from instance, - - filename is slugified and shortened to a max length including the extension. - """ - name, ext = os.path.splitext(filename) - name = slugify(name) - name = name[:(FILENAME_MAX_LENGTH - len(ext))] - filename = "%s%s" % (name, ext) - return os.path.join( - instance.get_uuid(), - instance.STORAGE.ORIGINAL_FILE_PREFIX, - filename - ) - - +MEDIA_ASSET_STORAGE = ProtectedMediaAssetStorage() DEFAULT_UPLOAD_TO = get_upload_path @@ -46,24 +22,24 @@ class MediaAssetFileWidget(AdminFileWidget): Widget which understands ProtectedMediaAssetStorage (knows that the url property is not relevant), also does not provide a link to the original file. - # TODO Add admin access to the original file, if permissions apply + # TODO Only admin access to the original file, if permissions apply # TODO Add permission "allowed to view original file" """ - template_with_initial = ( - '%(initial_text)s: %(initial)s ' - '%(clear_template)s
%(input_text)s: %(input)s' - ) + # template_with_initial = ( + # '%(initial_text)s: %(initial)s ' + # '%(clear_template)s
%(input_text)s: %(input)s' + # ) def is_initial(self, value): # Checks for 'name' instead of 'url' property return bool(value and hasattr(value, 'name')) - def get_template_substitution_values(self, value): - # Does not use value.url - return { - 'initial': conditional_escape(value), - } + # def get_template_substitution_values(self, value): + # # Does not use value.url + # return { + # 'initial': conditional_escape(value), + # } class MediaAssetFileDescriptor(FileDescriptor): @@ -87,6 +63,7 @@ class MediaAssetFileDescriptor(FileDescriptor): pass # self.field.update_dimension_fields(instance, force=True) +# TODO MediaAssetFormField unused class MediaAssetFormField(forms.FileField): widget = MediaAssetFileWidget @@ -95,16 +72,31 @@ class MediaAssetField(models.FileField): # The descriptor to use for accessing the attribute off of the class. description_class = MediaAssetFileDescriptor - def _init__(self, verbose_name=None, name=None, upload_to='', storage=None, *args, **kwargs): - kwargs['max_length'] = kwargs.get('max_length', FILENAME_MAX_LENGTH) - kwargs['upload_to'] = kwargs.get('upload_to', DEFAULT_UPLOAD_TO) - kwargs['storage'] = kwargs.get('storage', ORIGINALS_STORAGE) + def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, *args, **kwargs): + storage = kwargs.get('storage', MEDIA_ASSET_STORAGE) + kwargs['storage'] = storage + + upload_func = kwargs.get('upload_to', DEFAULT_UPLOAD_TO) + if callable(upload_func): + kwargs['upload_to'] = curry(upload_func, storage=storage) + else: + kwargs['upload_to'] = upload_func + + # Field max length is the length of the file name plus the + # string ORIGINAL_FILE_PREFIX, two slashes and the uuid. The filename + # is shortened in the get_upload_path function. + field_max_length = storage.FILENAME_MAX_LENGTH + 2 \ + + len(storage.ORIGINAL_FILE_PREFIX) \ + + 32 # uuid_hex + kwargs['max_length'] = kwargs.get('max_length', field_max_length) + super(MediaAssetField, self).__init__(verbose_name, name, **kwargs) def formfield(self, **kwargs): defaults = { - 'form_class': MediaAssetFormField, - 'max_length': self.max_length + # 'form_class': MediaAssetFormField, + 'widget': MediaAssetFileWidget, + 'max_length': self.storage.FILENAME_MAX_LENGTH, } - defaults.update(kwargs) - return super(MediaAssetField, self).formfield(**defaults) + kwargs.update(defaults) # Force our values + return super(MediaAssetField, self).formfield(**kwargs) diff --git a/example_site/main/medialibrary/models.py b/example_site/main/medialibrary/models.py index 87d5b45..2802ce9 100644 --- a/example_site/main/medialibrary/models.py +++ b/example_site/main/medialibrary/models.py @@ -1,33 +1,23 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -import uuid from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from ..medialibrary.fields import MediaAssetField - - -class UUIDMixin(models.Model): - uuid_hex = models.CharField(max_length=32, null=False, editable=False) - - class Meta: - abstract = True - - # TODO Make auto-initializing UUID-field - def get_uuid(self): - if not self.uuid_hex: - self.uuid_hex = uuid.uuid4().hex - return str(uuid.UUID(self.uuid_hex)) +from ..medialibrary.fields import MediaAssetField, MEDIA_ASSET_STORAGE +from .utils import UUIDMixin @python_2_unicode_compatible class MediaAsset(UUIDMixin, models.Model): - original_file = MediaAssetField(_("original file")) name = models.CharField(_('name'), max_length=50) - simple_file = models.FileField(null=True, blank=True) + # TODO Add slug = SlugField + + storage = MEDIA_ASSET_STORAGE + original_file = MediaAssetField(_("original file"), storage=MEDIA_ASSET_STORAGE) + # TODO Add thumbnail = Thumbnail + # TODO Add preview = ImageSpec def __str__(self): return self.name - diff --git a/example_site/main/medialibrary/utils.py b/example_site/main/medialibrary/utils.py index 6d2db66..0fbe2ce 100644 --- a/example_site/main/medialibrary/utils.py +++ b/example_site/main/medialibrary/utils.py @@ -2,24 +2,43 @@ # from __future__ import unicode_literals # # Erik Stein , 2016 -# import os -# from django.utils.text import slugify - - -# def get_upload_path(instance, filename): -# """ -# Returns //original/ -# where -# - uuid is taken from instance, -# - filename is slugified and shortened to a max length including the extension. -# """ -# name, ext = os.path.splitext(filename) -# name = slugify(name) -# name = name[:(FILENAME_MAX_LENGTH - len(ext))] -# filename = "%s%s" % (name, ext) -# return os.path.join( -# instance.get_uuid(), -# instance.STORAGE.ORIGINAL_FILE_PREFIX, -# filename -# ) +import os +import uuid +from django.conf import settings +from django.db import models +# TODO Use improved slugify +from django.utils.text import slugify + + +class UUIDMixin(models.Model): + uuid_hex = models.CharField(max_length=32, null=False, editable=False) + + class Meta: + abstract = True + + # TODO Make auto-initializing UUID-field + def get_uuid(self): + if not self.uuid_hex: + self.uuid_hex = uuid.uuid4().hex + return str(uuid.UUID(self.uuid_hex)) + + +def get_upload_path(instance, filename, storage): + """ + Returns //original/. + where + - uuid and name are fields from instance, + - filename is slugified and shortened to a max length including the extension. + - extension is preserved from original filename + """ + name, ext = os.path.splitext(filename) + name = slugify(name) + name = name[:(storage.FILENAME_MAX_LENGTH - len(ext))] + filename = "%s%s" % (name, ext) + return os.path.join( + instance.get_uuid(), + instance.storage.ORIGINAL_FILE_PREFIX, + filename + ) + diff --git a/example_site/main/settings.py b/example_site/main/settings.py index 2f1193a..490fe85 100644 --- a/example_site/main/settings.py +++ b/example_site/main/settings.py @@ -31,6 +31,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', + 'assetkit', 'imagekit', 'main.medialibrary.apps.MedialibraryConfig', # 'main', @@ -119,29 +120,21 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ -# MEDIA_ASSETS_URL = '/assets/' -# MEDIA_ASSETS_ROOT = os.path.join(BASE_DIR, 'media') - - STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' - STATIC_URL = '/static/' -MEDIA_URL = '/media/' - STATIC_ROOT = os.path.join(ENV_DIR, 'var', 'static') + + +MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(ENV_DIR, 'var', 'public') -PROTECTED_MEDIA_ASSETS_ROOT = os.path.join(ENV_DIR, 'var', 'protected') +PROTECTED_MEDIA_URL = '/protected/' +PROTECTED_MEDIA_ROOT = os.path.join(ENV_DIR, 'var', 'protected') +PROTECTED_MEDIA_INTERNAL_URL = '/protected-x-accel-redirect/' +PROTECTED_MEDIA_SERVER = 'private_media.servers.NginxXAccelRedirectServer' +# PROTECTED_MEDIA_PERMISSIONS = 'm1web.multimedia.permissions.AssetPermissions' # IMAGEKIT_DEFAULT_CACHEFILE_BACKEND = 'assetkit.cachefiles.backend.MediaAssetCacheBackend' # IMAGEKIT_DEFAULT_FILE_STORAGE = 'private_media.storages.PrivateMediaStorage' -# ORIGINALFILE_ROOT = os.path.join(ENV_DIR, 'var', 'protected') # IMAGEKIT_CACHEFILE_DIR = '' # Directly in media root -# PRIVATE_MEDIA_URL = '/media-archive/' -# PRIVATE_MEDIA_INTERNAL_URL = '/media-archive-x-accel-redirect/' -# PRIVATE_MEDIA_ROOT = os.path.join(ROOT_DIR, 'var', 'media-private') - -# PRIVATE_MEDIA_SERVER = 'private_media.servers.NginxXAccelRedirectServer' -# PRIVATE_MEDIA_PERMISSIONS = 'm1web.multimedia.permissions.AssetPermissions' - diff --git a/example_site/main/storages.py b/example_site/main/storages.py index 883df06..e6127b0 100644 --- a/example_site/main/storages.py +++ b/example_site/main/storages.py @@ -1,52 +1,49 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals +# # -*- coding: utf-8 -*- +# from __future__ import unicode_literals -import os -import warnings -from django.conf import settings -from django.core.files.storage import FileSystemStorage -from django.utils._os import safe_join -from django.utils.six.moves.urllib.parse import urljoin +# import os +# import warnings +# from django.conf import settings +# from django.core.files.storage import FileSystemStorage +# from django.utils._os import safe_join -ORIGINAL_FILE_PREFIX = 'original' -CACHED_VARIANT_PREFIX = 'cache' -MANUAL_VARIANT_PREFIX = 'manual' +# ORIGINAL_FILE_PREFIX = 'original' +# CACHED_VARIANT_PREFIX = 'cache' +# MANUAL_VARIANT_PREFIX = 'manual' -# TODO ? @deconstructible -class ProtectedMediaAssetStorage(FileSystemStorage): - """ - Alllows uploads to /original/-subdirectories, but never provides URLs - to those files, instead raising an error. - """ - ORIGINAL_FILE_PREFIX = ORIGINAL_FILE_PREFIX +# # TODO ? @deconstructible +# class ProtectedMediaAssetStorage(FileSystemStorage): +# """ +# Alllows uploads to /original/-subdirectories, but never provides URLs +# to those files, instead raising an error. +# """ - def __init__(self, location=None, base_url=None, **kwargs): - if location is None: - location = getattr(settings, 'PROTECTED_MEDIA_ASSETS_ROOT', settings.PROTECTED_MEDIA_ASSETS_ROOT) - super(ProtectedMediaAssetStorage, self).__init__(location=location, base_url=None, **kwargs) - self.base_url = None +# ORIGINAL_FILE_PREFIX = ORIGINAL_FILE_PREFIX +# FILENAME_MAX_LENGTH = FILENAME_MAX_LENGTH - def delete(self, name): - super(ProtectedMediaAssetStorage, self).delete(name) - # FIXME Delete all cached files, too - warnings.warn("Cached files for asset \"%s\" are not deleted." % name) +# def __init__(self, location=None, base_url=None, **kwargs): +# if location is None: +# location = getattr(settings, 'PROTECTED_MEDIA_ROOT', settings.PROTECTED_MEDIA_ROOT) +# super(ProtectedMediaAssetStorage, self).__init__(location=location, base_url=None, **kwargs) - def path(self, name): - """ - `name` must already contain the whole asset filesystem path. - """ - return safe_join(self.location, ORIGINAL_FILE_PREFIX, name) +# def delete(self, name): +# super(ProtectedMediaAssetStorage, self).delete(name) +# # FIXME Delete all cached files, too +# warnings.warn("Cached files for asset \"%s\" are not deleted." % name) - def size(self, name): - return os.path.getsize(self.path(name)) +# def path(self, name): +# """ +# `name` must already contain the whole asset filesystem path. +# """ +# return safe_join(self.location, ORIGINAL_FILE_PREFIX, name) - def url(self, name): - raise ValueError("This file storage is not accessible via a URL.") +# def size(self, name): +# return os.path.getsize(self.path(name)) - -# TODO ? @deconstructible -# class -# return urljoin(self.base_url, ORIGINAL_FILE_PREFIX, filepath_to_uri(name)) +# # def url(self, name): +# # print "storage.url" +# # # Returns the URL for fetching the original file +# # raise ValueError("This file storage is not accessible via a URL.") diff --git a/example_site/main/urls.py b/example_site/main/urls.py index 6f18904..b156fd9 100644 --- a/example_site/main/urls.py +++ b/example_site/main/urls.py @@ -17,4 +17,5 @@ urlpatterns = [ # For unprotected uploads: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +urlpatterns += static(settings.PROTECTED_MEDIA_URL, document_root=settings.PROTECTED_MEDIA_ROOT)