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
__all__ = ('MediaAssetStorage',)
ORIGINAL_FILE_PREFIX = 'original'
CACHED_VARIANT_PREFIX = 'cache'
MANUAL_VARIANT_PREFIX = 'manual'
__all__ = ('ProtectedMediaAssetStorage',)
"""
@ -29,9 +24,17 @@ Storage setup:
/manual/<format_slug>/<hash>.<variant_ext>
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))

84
example_site/main/medialibrary/fields.py

@ -2,42 +2,18 @@
from __future__ import unicode_literals
# Erik Stein <code@classlibrary.net>, 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 /<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
)
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<br />%(input_text)s: %(input)s'
)
# template_with_initial = (
# '%(initial_text)s: %(initial)s '
# '%(clear_template)s<br />%(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)

26
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

59
example_site/main/medialibrary/utils.py

@ -2,24 +2,43 @@
# from __future__ import unicode_literals
# # Erik Stein <code@classlibrary.net>, 2016
# import os
# from django.utils.text import slugify
# 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
# )
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 /<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.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'

77
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.")

1
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)

Loading…
Cancel
Save