Browse Source

Refactored ProtectedMediaAssetStorage.

master
Erik Stein 9 years ago
parent
commit
235e24a4b0
  1. 39
      assetkit/files/storage.py
  2. 84
      example_site/main/medialibrary/fields.py
  3. 26
      example_site/main/medialibrary/models.py
  4. 59
      example_site/main/medialibrary/utils.py
  5. 25
      example_site/main/settings.py
  6. 77
      example_site/main/storages.py
  7. 1
      example_site/main/urls.py

39
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 from django.utils.six.moves.urllib.parse import urljoin
__all__ = ('MediaAssetStorage',) __all__ = ('ProtectedMediaAssetStorage',)
ORIGINAL_FILE_PREFIX = 'original'
CACHED_VARIANT_PREFIX = 'cache'
MANUAL_VARIANT_PREFIX = 'manual'
""" """
@ -29,9 +24,17 @@ Storage setup:
/manual/<format_slug>/<hash>.<variant_ext> /manual/<format_slug>/<hash>.<variant_ext>
TODO Add snapshot (versioning) to path after asset_uuid. 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): class BaseVariantSubStorage(FileSystemStorage):
def delete(self, parent_storage, name): def delete(self, parent_storage, name):
@ -42,23 +45,25 @@ class BaseVariantSubStorage(FileSystemStorage):
@deconstructible @deconstructible
class MediaAssetStorage(FileSystemStorage): class ProtectedMediaAssetStorage(FileSystemStorage):
""" """
Standard media assets filesystem storage Standard media assets filesystem storage
""" """
def __init__(self, location=None, base_url=None, file_permissions_mode=None, ORIGINAL_FILE_PREFIX = ORIGINAL_FILE_PREFIX
directory_permissions_mode=None): FILENAME_MAX_LENGTH = FILENAME_MAX_LENGTH
def __init__(self, location=None, base_url=None, **kwargs):
if location is None: 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: 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('/'): if not base_url.endswith('/'):
base_url += '/' base_url += '/'
super(MediaAssetStorage, self).__init__(self) super(ProtectedMediaAssetStorage, self).__init__(location=location, base_url=base_url, **kwargs)
def delete(self, name): def delete(self, name):
super(MediaAssetStorage, self).delete(name) super(ProtectedMediaAssetStorage, self).delete(name)
# FIXME Delete all cached files, too # FIXME Delete all cached files, too
warnings.warn("Cached files for asset \"%s\" are not deleted." % name) 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. `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): def size(self, name):
return os.path.getsize(self.path(name)) return os.path.getsize(self.path(name))
def url(self, name): def url(self, name):
if self.base_url is None: return urljoin(self.base_url, filepath_to_uri(name))
raise ValueError("This file is not accessible via a URL.")
return urljoin(self.base_url, ORIGINAL_FILE_PREFIX, filepath_to_uri(name))

84
example_site/main/medialibrary/fields.py

@ -2,42 +2,18 @@
from __future__ import unicode_literals from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 2016 # Erik Stein <code@classlibrary.net>, 2016
import os
from django import forms from django import forms
from django.contrib.admin.widgets import AdminFileWidget from django.contrib.admin.widgets import AdminFileWidget
from django.db import models from django.db import models
from django.db.models.fields.files import FileDescriptor from django.db.models.fields.files import FileDescriptor
from django.utils.html import conditional_escape from django.utils.functional import curry
from django.utils.text import slugify
from ..storages import ProtectedMediaAssetStorage from assetkit.files.storage import ProtectedMediaAssetStorage
from .utils import get_upload_path
# TODO Define the central storage somewhere more central # TODO Define the central storage somewhere more central
ORIGINALS_STORAGE = ProtectedMediaAssetStorage() MEDIA_ASSET_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 /<uuid_hex>/original/<slugified_filename.ext>
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
)
DEFAULT_UPLOAD_TO = get_upload_path DEFAULT_UPLOAD_TO = get_upload_path
@ -46,24 +22,24 @@ class MediaAssetFileWidget(AdminFileWidget):
Widget which understands ProtectedMediaAssetStorage Widget which understands ProtectedMediaAssetStorage
(knows that the url property is not relevant), also (knows that the url property is not relevant), also
does not provide a link to the original file. 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" # TODO Add permission "allowed to view original file"
""" """
template_with_initial = ( # template_with_initial = (
'%(initial_text)s: %(initial)s ' # '%(initial_text)s: %(initial)s '
'%(clear_template)s<br />%(input_text)s: %(input)s' # '%(clear_template)s<br />%(input_text)s: %(input)s'
) # )
def is_initial(self, value): def is_initial(self, value):
# Checks for 'name' instead of 'url' property # Checks for 'name' instead of 'url' property
return bool(value and hasattr(value, 'name')) return bool(value and hasattr(value, 'name'))
def get_template_substitution_values(self, value): # def get_template_substitution_values(self, value):
# Does not use value.url # # Does not use value.url
return { # return {
'initial': conditional_escape(value), # 'initial': conditional_escape(value),
} # }
class MediaAssetFileDescriptor(FileDescriptor): class MediaAssetFileDescriptor(FileDescriptor):
@ -87,6 +63,7 @@ class MediaAssetFileDescriptor(FileDescriptor):
pass # self.field.update_dimension_fields(instance, force=True) pass # self.field.update_dimension_fields(instance, force=True)
# TODO MediaAssetFormField unused
class MediaAssetFormField(forms.FileField): class MediaAssetFormField(forms.FileField):
widget = MediaAssetFileWidget widget = MediaAssetFileWidget
@ -95,16 +72,31 @@ class MediaAssetField(models.FileField):
# The descriptor to use for accessing the attribute off of the class. # The descriptor to use for accessing the attribute off of the class.
description_class = MediaAssetFileDescriptor description_class = MediaAssetFileDescriptor
def _init__(self, verbose_name=None, name=None, upload_to='', storage=None, *args, **kwargs): def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', FILENAME_MAX_LENGTH) storage = kwargs.get('storage', MEDIA_ASSET_STORAGE)
kwargs['upload_to'] = kwargs.get('upload_to', DEFAULT_UPLOAD_TO) kwargs['storage'] = storage
kwargs['storage'] = kwargs.get('storage', ORIGINALS_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) super(MediaAssetField, self).__init__(verbose_name, name, **kwargs)
def formfield(self, **kwargs): def formfield(self, **kwargs):
defaults = { defaults = {
'form_class': MediaAssetFormField, # 'form_class': MediaAssetFormField,
'max_length': self.max_length 'widget': MediaAssetFileWidget,
'max_length': self.storage.FILENAME_MAX_LENGTH,
} }
defaults.update(kwargs) kwargs.update(defaults) # Force our values
return super(MediaAssetField, self).formfield(**defaults) return super(MediaAssetField, self).formfield(**kwargs)

26
example_site/main/medialibrary/models.py

@ -1,33 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import uuid
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from ..medialibrary.fields import MediaAssetField from ..medialibrary.fields import MediaAssetField, MEDIA_ASSET_STORAGE
from .utils import UUIDMixin
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))
@python_2_unicode_compatible @python_2_unicode_compatible
class MediaAsset(UUIDMixin, models.Model): class MediaAsset(UUIDMixin, models.Model):
original_file = MediaAssetField(_("original file"))
name = models.CharField(_('name'), max_length=50) 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): def __str__(self):
return self.name return self.name

59
example_site/main/medialibrary/utils.py

@ -2,24 +2,43 @@
# from __future__ import unicode_literals # from __future__ import unicode_literals
# # Erik Stein <code@classlibrary.net>, 2016 # # Erik Stein <code@classlibrary.net>, 2016
# import os import os
# from django.utils.text import slugify import uuid
from django.conf import settings
from django.db import models
# def get_upload_path(instance, filename): # TODO Use improved slugify
# """ from django.utils.text import slugify
# Returns /<uuid_hex>/original/<slugified_filename.ext>
# where
# - uuid is taken from instance, class UUIDMixin(models.Model):
# - filename is slugified and shortened to a max length including the extension. uuid_hex = models.CharField(max_length=32, null=False, editable=False)
# """
# name, ext = os.path.splitext(filename) class Meta:
# name = slugify(name) abstract = True
# name = name[:(FILENAME_MAX_LENGTH - len(ext))]
# filename = "%s%s" % (name, ext) # TODO Make auto-initializing UUID-field
# return os.path.join( def get_uuid(self):
# instance.get_uuid(), if not self.uuid_hex:
# instance.STORAGE.ORIGINAL_FILE_PREFIX, self.uuid_hex = uuid.uuid4().hex
# filename return str(uuid.UUID(self.uuid_hex))
# )
def get_upload_path(instance, filename, storage):
"""
Returns /<uuid_hex>/original/<slugified name>.<extension>
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
)

25
example_site/main/settings.py

@ -31,6 +31,7 @@ INSTALLED_APPS = [
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'assetkit',
'imagekit', 'imagekit',
'main.medialibrary.apps.MedialibraryConfig', 'main.medialibrary.apps.MedialibraryConfig',
# 'main', # 'main',
@ -119,29 +120,21 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/ # 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' STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
STATIC_URL = '/static/' STATIC_URL = '/static/'
MEDIA_URL = '/media/'
STATIC_ROOT = os.path.join(ENV_DIR, 'var', 'static') STATIC_ROOT = os.path.join(ENV_DIR, 'var', 'static')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(ENV_DIR, 'var', 'public') 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_CACHEFILE_BACKEND = 'assetkit.cachefiles.backend.MediaAssetCacheBackend'
# IMAGEKIT_DEFAULT_FILE_STORAGE = 'private_media.storages.PrivateMediaStorage' # IMAGEKIT_DEFAULT_FILE_STORAGE = 'private_media.storages.PrivateMediaStorage'
# ORIGINALFILE_ROOT = os.path.join(ENV_DIR, 'var', 'protected')
# IMAGEKIT_CACHEFILE_DIR = '' # Directly in media root # 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'

77
example_site/main/storages.py

@ -1,52 +1,49 @@
# -*- coding: utf-8 -*- # # -*- coding: utf-8 -*-
from __future__ import unicode_literals # from __future__ import unicode_literals
import os # import os
import warnings # import warnings
from django.conf import settings # from django.conf import settings
from django.core.files.storage import FileSystemStorage # from django.core.files.storage import FileSystemStorage
from django.utils._os import safe_join # from django.utils._os import safe_join
from django.utils.six.moves.urllib.parse import urljoin
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): # ORIGINAL_FILE_PREFIX = ORIGINAL_FILE_PREFIX
if location is None: # FILENAME_MAX_LENGTH = FILENAME_MAX_LENGTH
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
def delete(self, name): # def __init__(self, location=None, base_url=None, **kwargs):
super(ProtectedMediaAssetStorage, self).delete(name) # if location is None:
# FIXME Delete all cached files, too # location = getattr(settings, 'PROTECTED_MEDIA_ROOT', settings.PROTECTED_MEDIA_ROOT)
warnings.warn("Cached files for asset \"%s\" are not deleted." % name) # super(ProtectedMediaAssetStorage, self).__init__(location=location, base_url=None, **kwargs)
def path(self, name): # def delete(self, name):
""" # super(ProtectedMediaAssetStorage, self).delete(name)
`name` must already contain the whole asset filesystem path. # # FIXME Delete all cached files, too
""" # warnings.warn("Cached files for asset \"%s\" are not deleted." % name)
return safe_join(self.location, ORIGINAL_FILE_PREFIX, name)
def size(self, name): # def path(self, name):
return os.path.getsize(self.path(name)) # """
# `name` must already contain the whole asset filesystem path.
# """
# return safe_join(self.location, ORIGINAL_FILE_PREFIX, name)
def url(self, name): # def size(self, name):
raise ValueError("This file storage is not accessible via a URL.") # return os.path.getsize(self.path(name))
# # def url(self, name):
# TODO ? @deconstructible # # print "storage.url"
# class # # # Returns the URL for fetching the original file
# return urljoin(self.base_url, ORIGINAL_FILE_PREFIX, filepath_to_uri(name)) # # raise ValueError("This file storage is not accessible via a URL.")

1
example_site/main/urls.py

@ -17,4 +17,5 @@ urlpatterns = [
# For unprotected uploads: # For unprotected uploads:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.PROTECTED_MEDIA_URL, document_root=settings.PROTECTED_MEDIA_ROOT)

Loading…
Cancel
Save