filedepot 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
depot/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,54 @@
1
+ from depot.io import utils
2
+ from depot.manager import DepotManager
3
+ from ..interfaces import FileFilter
4
+ from PIL import Image
5
+ from io import BytesIO
6
+
7
+
8
+ class WithThumbnailFilter(FileFilter):
9
+ """Uploads a thumbnail together with the file.
10
+
11
+ Takes for granted that the file is an image.
12
+ The resulting uploaded file will provide three additional
13
+ properties named:
14
+
15
+ - ``thumb_X_id`` -> The depot file id
16
+ - ``thumb_X_path`` -> Where the file is available in depot
17
+ - ``thumb_X_url`` -> Where the file is served.
18
+
19
+ Where ``X`` is the resolution specified as ``size`` in the
20
+ filter initialization. By default this is ``(128, 128)``?so
21
+ you will get ``thumb_128x128_id``, ``thumb_128x128_url`` and
22
+ so on.
23
+
24
+ .. warning::
25
+
26
+ Requires Pillow library
27
+
28
+ """
29
+ def __init__(self, size=(128,128), format='PNG'):
30
+ self.thumbnail_size = size
31
+ self.thumbnail_format = format
32
+
33
+ def on_save(self, uploaded_file):
34
+ close_content, content = utils.file_from_content(uploaded_file.original_content)
35
+
36
+ try:
37
+ thumbnail = Image.open(content)
38
+ thumbnail.thumbnail(self.thumbnail_size, Image.BILINEAR)
39
+ thumbnail = thumbnail.convert('RGBA')
40
+ thumbnail.format = self.thumbnail_format
41
+
42
+ output = BytesIO()
43
+ thumbnail.save(output, self.thumbnail_format)
44
+ output.seek(0)
45
+
46
+ thumb_name = 'thumb_%sx%s' % self.thumbnail_size
47
+ thumb_file_name = '%s.%s' % (thumb_name, self.thumbnail_format.lower())
48
+ thumb_path, thumb_id = uploaded_file.store_content(output, thumb_file_name)
49
+ uploaded_file[thumb_name + '_id'] = thumb_id
50
+ uploaded_file[thumb_name + '_path'] = thumb_path
51
+ uploaded_file[thumb_name + '_url'] = DepotManager.get_middleware().url_for(thumb_path)
52
+ finally:
53
+ if close_content:
54
+ content.close()
@@ -0,0 +1,108 @@
1
+ from abc import ABCMeta, abstractmethod
2
+ from depot.manager import DepotManager
3
+
4
+
5
+ class FileFilter(object, metaclass=ABCMeta):
6
+ """Interface that must be implemented by file filters.
7
+
8
+ File filters get executed whenever a file is stored on the database
9
+ using one of the supported fields. Can be used to add additional data
10
+ to the stored file or change it. When file filters are run the file
11
+ has already been stored.
12
+
13
+ """
14
+ @abstractmethod
15
+ def on_save(self, uploaded_file): # pragma: no cover
16
+ """Filters are required to provide their own implementation"""
17
+ return
18
+
19
+
20
+ class DepotFileInfo(dict, metaclass=ABCMeta):
21
+ """Keeps information on a content related to a specific depot.
22
+
23
+ By itself the DepotFileInfo does nothing, it is required to implement
24
+ a :meth:`process_content` method that actually saves inside the
25
+ file info the information related to the content. The only information
26
+ which is saved by default is the depot name itself.
27
+
28
+ It is a specialized dictionary that provides also attribute style access,
29
+ the dictionary parent permits easy encoding/decoding to most marshalling
30
+ systems.
31
+
32
+ """
33
+ def __init__(self, content, depot_name=None):
34
+ super(DepotFileInfo, self).__init__()
35
+ self._thaw()
36
+
37
+ if isinstance(content, dict):
38
+ object.__setattr__(self, 'original_content', None)
39
+ self.update(content)
40
+ else:
41
+ object.__setattr__(self, 'original_content', content)
42
+ if depot_name is None:
43
+ depot_name = DepotManager.get_default()
44
+
45
+ depot_name = DepotManager.resolve_alias(depot_name)
46
+ if not depot_name:
47
+ raise ValueError('Storage has not been found in DEPOT')
48
+
49
+ self['depot_name'] = depot_name
50
+ self['files'] = []
51
+ self.process_content(content)
52
+
53
+ self._freeze()
54
+
55
+ @abstractmethod
56
+ def process_content(self, content, filename=None, content_type=None): # pragma: no cover
57
+ """Process content in the given depot.
58
+
59
+ This is implemented by subclasses to provide some kind of behaviour on the
60
+ content in the related Depot. The default implementation is provided by
61
+ :class:`depot.fields.upload.UploadedFile` which stores the content into
62
+ the depot.
63
+
64
+ """
65
+ return
66
+
67
+ def __getitem__(self, key):
68
+ return dict.__getitem__(self, key)
69
+
70
+ def __getattr__(self, name):
71
+ try:
72
+ return self[name]
73
+ except KeyError:
74
+ raise AttributeError(name)
75
+
76
+ def __setitem__(self, key, value):
77
+ if object.__getattribute__(self, '_frozen'):
78
+ raise TypeError('Already saved files are immutable')
79
+ return dict.__setitem__(self, key, value)
80
+
81
+ __setattr__ = __setitem__
82
+
83
+ def __delattr__(self, name):
84
+ if object.__getattribute__(self, '_frozen'):
85
+ raise TypeError('Already saved files are immutable')
86
+
87
+ try:
88
+ del self[name]
89
+ except KeyError:
90
+ raise AttributeError(name)
91
+
92
+ def __delitem__(self, key):
93
+ if object.__getattribute__(self, '_frozen'):
94
+ raise TypeError('Already saved files are immutable')
95
+ dict.__delitem__(self, key)
96
+
97
+ def _apply_filters(self, filters):
98
+ if self.original_content is not None:
99
+ self._thaw()
100
+ for filt in filters:
101
+ filt.on_save(self)
102
+ self._freeze()
103
+
104
+ def _freeze(self):
105
+ object.__setattr__(self, '_frozen', True)
106
+
107
+ def _thaw(self):
108
+ object.__setattr__(self, '_frozen', False)
depot/fields/ming.py ADDED
@@ -0,0 +1,168 @@
1
+ from ming.odm import mapper
2
+ from ming.odm.property import FieldProperty
3
+ from ming.odm.base import session, state, ObjectState
4
+ from ming.odm.odmsession import SessionExtension
5
+ from ming.schema import Anything
6
+
7
+ from depot.manager import DepotManager
8
+ from .upload import UploadedFile
9
+
10
+
11
+ class _UploadedFileSchema(Anything):
12
+ pass
13
+
14
+
15
+ class UploadedFileProperty(FieldProperty):
16
+ """Provides support for storing attachments to **Ming** MongoDB models.
17
+
18
+ ``UploadedFileProperty`` can be used as a field type to store files
19
+ into the model. The actual file itself will be uploaded to the
20
+ default Storage, and only the :class:`depot.fields.upload.UploadedFile`
21
+ information will be stored on the database.
22
+
23
+ The ``UploadedFileProperty`` is UnitOfWork aware, so it will delete
24
+ every uploaded file whenever unit of work is flushed and deletes a Document
25
+ that stored files or changes the field of a document storing files. This is
26
+ the reason you should never associate the same :class:`depot.fields.upload.UploadedFile`
27
+ to two different ``UploadedFileProperty``, otherwise you might delete a file
28
+ already used by another document. It is usually best to just set the ``file``
29
+ or ``bytes`` as content of the column and let the ``UploadedFileProperty``
30
+ create the :class:`depot.fields.upload.UploadedFile` by itself whenever it's content is set.
31
+
32
+ .. warning::
33
+
34
+ As the Ming UnitOfWork does not notify any event in case it gets cleared instead
35
+ of being flushed all the files uploaded before clearing the unit of work will be
36
+ already uploaded but won't have a document referencing them anymore, so DEPOT will
37
+ be unable to delete them for you.
38
+
39
+ """
40
+ def __init__(self, filters=tuple(), upload_type=UploadedFile, upload_storage=None):
41
+ FieldProperty.__init__(self, _UploadedFileSchema())
42
+ self._filters = filters
43
+ self._upload_type = upload_type
44
+ self._upload_storage = upload_storage
45
+
46
+ def __set__(self, instance, value):
47
+ if value is not None and not isinstance(value, UploadedFile):
48
+ upload_type = self._upload_type
49
+ value = upload_type(value, self._upload_storage)
50
+
51
+ if isinstance(value, UploadedFile):
52
+ value._apply_filters(self._filters)
53
+
54
+ old_value = self.__get__(instance, instance.__class__)
55
+ DepotExtension.get_depot_history(instance).swap(old_value, value)
56
+ return FieldProperty.__set__(self, instance, value)
57
+
58
+ def __get__(self, instance, owner=None):
59
+ try:
60
+ value = FieldProperty.__get__(self, instance, owner)
61
+ except AttributeError:
62
+ value = None
63
+
64
+ if not value:
65
+ return None
66
+
67
+ if instance is None:
68
+ return value
69
+
70
+ return self._upload_type(value)
71
+
72
+ """
73
+ # Looks like this should do nothing on ming.
74
+ def __delete__(self, instance, owner=None):
75
+ old_value = self.__get__(instance, instance.__class__)
76
+ DepotExtension.get_depot_history(instance).delete(old_value)
77
+ return FieldProperty.__delete__(self, instance, owner)
78
+ """
79
+
80
+
81
+ class DepotExtension(SessionExtension):
82
+ """Extends the Ming Session to track files.
83
+
84
+ Deletes old files when an entry gets removed or replaced,
85
+ apply this as a Ming ``SessionExtension`` according to Ming
86
+ documentation.
87
+ """
88
+ @classmethod
89
+ def get_depot_history(cls, instance):
90
+ istate = state(instance)
91
+ if not hasattr(istate, '_depot_history'):
92
+ istate._depot_history = _DepotHistory()
93
+ return istate._depot_history
94
+
95
+ def _check_object_deleted(self, obj):
96
+ hist = self.get_depot_history(obj)
97
+ if state(obj).status == ObjectState.deleted:
98
+ for prop in mapper(obj).properties:
99
+ if isinstance(prop, UploadedFileProperty):
100
+ current_value = prop.__get__(obj, obj.__class__)
101
+ hist.delete(current_value)
102
+ self._flush_object(obj)
103
+
104
+ def _flush_object(self, obj):
105
+ history = self.get_depot_history(obj)
106
+ for entry in history.deleted:
107
+ depot, fileid = entry.split('/', 1)
108
+ depot = DepotManager.get(depot)
109
+ depot.delete(fileid)
110
+ history.clear()
111
+
112
+ def before_flush(self, obj=None):
113
+ if obj:
114
+ self._check_object_deleted(obj)
115
+ else:
116
+ for class_, id_, obj in self.session.imap:
117
+ self._check_object_deleted(obj)
118
+
119
+ def after_flush(self, obj=None):
120
+ if obj:
121
+ self._flush_object(obj)
122
+ else:
123
+ for class_, id_, obj in self.session.imap:
124
+ self._flush_object(obj)
125
+
126
+
127
+ class _DepotHistory(object):
128
+ def __init__(self):
129
+ self.clear()
130
+
131
+ def _extract_files(self, obj):
132
+ return obj['files']
133
+
134
+ def add(self, obj):
135
+ if obj is None:
136
+ return
137
+
138
+ files = self._extract_files(obj)
139
+ self.deleted.difference_update(obj)
140
+ self.new.update(files)
141
+
142
+ def delete(self, obj):
143
+ if obj is None:
144
+ return
145
+
146
+ files = self._extract_files(obj)
147
+ self.new.difference_update(obj)
148
+ self.deleted.update(files)
149
+
150
+ def swap(self, old, new):
151
+ self.delete(old)
152
+ self.add(new)
153
+
154
+ def clear(self):
155
+ self.deleted = set()
156
+ self.new = set()
157
+
158
+
159
+ try: # pragma: no cover
160
+ from sprox.mg.widgetselector import MingWidgetSelector
161
+ from tw2.forms import FileField as TW2FileField
162
+ MingWidgetSelector.default_widgets.setdefault(_UploadedFileSchema, TW2FileField)
163
+
164
+ from sprox.mg.validatorselector import MingValidatorSelector
165
+ from ..validators import TW2FileIntentValidator
166
+ MingValidatorSelector.default_validators.setdefault(_UploadedFileSchema, TW2FileIntentValidator)
167
+ except ImportError: # pragma: no cover
168
+ pass
File without changes
@@ -0,0 +1,76 @@
1
+ from depot.io import utils
2
+ from depot.manager import DepotManager
3
+ from ..upload import UploadedFile
4
+ from depot.io.interfaces import FileStorage
5
+ from PIL import Image
6
+ from depot.io.utils import INMEMORY_FILESIZE
7
+ from tempfile import SpooledTemporaryFile
8
+
9
+
10
+ class UploadedImageWithThumb(UploadedFile):
11
+ """Uploads an Image with thumbnail.
12
+
13
+ The default thumbnail format and size are PNG@128x128, those can be changed
14
+ by inheriting the ``UploadedImageWithThumb`` and replacing the
15
+ ``thumbnail_format`` and ``thumbnail_size`` class properties.
16
+
17
+ The Thumbnail file is accessible as ``.thumb_file`` while the
18
+ thumbnail url is ``.thumb_url``.
19
+
20
+ .. warning::
21
+
22
+ Requires Pillow library
23
+
24
+ """
25
+
26
+ max_size = 1024
27
+ thumbnail_format = 'PNG'
28
+ thumbnail_size = (128, 128)
29
+
30
+ def process_content(self, content, filename=None, content_type=None):
31
+ orig_content = content
32
+ temporary_content = False
33
+ __, filename, content_type = FileStorage.fileinfo(orig_content)
34
+ close_content, content = utils.file_from_content(content)
35
+
36
+ try:
37
+ uploaded_image = Image.open(content)
38
+ if max(uploaded_image.size) >= self.max_size:
39
+ uploaded_image.thumbnail((self.max_size, self.max_size), Image.BILINEAR)
40
+ content = SpooledTemporaryFile(INMEMORY_FILESIZE)
41
+ close_content = True
42
+ uploaded_image.save(content, uploaded_image.format)
43
+
44
+ content.seek(0)
45
+ super(UploadedImageWithThumb, self).process_content(content, filename, content_type)
46
+ finally:
47
+ if close_content:
48
+ content.close()
49
+
50
+ thumbnail = uploaded_image.copy()
51
+ thumbnail.thumbnail(self.thumbnail_size, Image.LANCZOS)
52
+ thumbnail = thumbnail.convert('RGBA')
53
+ thumbnail.format = self.thumbnail_format
54
+
55
+ with SpooledTemporaryFile(INMEMORY_FILESIZE) as output:
56
+ thumbnail.save(output, self.thumbnail_format)
57
+ output.seek(0)
58
+
59
+ thumb_path, thumb_id = self.store_content(output,
60
+ 'thumb.%s' % self.thumbnail_format.lower())
61
+ self['thumb_id'] = thumb_id
62
+ self['thumb_path'] = thumb_path
63
+
64
+ thumbnail_file = self.thumb_file
65
+ self['_thumb_public_url'] = thumbnail_file.public_url
66
+
67
+ @property
68
+ def thumb_file(self):
69
+ return self.depot.get(self.thumb_id)
70
+
71
+ @property
72
+ def thumb_url(self):
73
+ public_url = self['_thumb_public_url']
74
+ if public_url:
75
+ return public_url
76
+ return DepotManager.get_middleware().url_for(self['thumb_path'])
@@ -0,0 +1,193 @@
1
+ import itertools
2
+
3
+ from sqlalchemy import types
4
+ from sqlalchemy import event
5
+ from sqlalchemy import orm
6
+ from sqlalchemy import inspect
7
+ from sqlalchemy.orm import ColumnProperty
8
+ from sqlalchemy.orm import Session
9
+ from sqlalchemy.orm.attributes import get_history
10
+
11
+ from depot.manager import DepotManager
12
+ from .upload import UploadedFile
13
+
14
+
15
+ class UploadedFileField(types.TypeDecorator):
16
+ """Provides support for storing attachments to **SQLAlchemy** models.
17
+
18
+ ``UploadedFileField`` can be used as a Column type to store files
19
+ into the model. The actual file itself will be uploaded to the
20
+ default Storage, and only the :class:`depot.fields.upload.UploadedFile`
21
+ information will be stored on the database.
22
+
23
+ The ``UploadedFileField`` is transaction aware, so it will delete
24
+ every uploaded file whenever the transaction is rolled back and will
25
+ delete any old file whenever the transaction is committed. This is
26
+ the reason you should never associate the same :class:`depot.fields.upload.UploadedFile`
27
+ to two different ``UploadedFileField``, otherwise you might delete a file
28
+ already used by another model. It is usually best to just set the ``file``
29
+ or ``bytes`` as content of the column and let the ``UploadedFileField``
30
+ create the :class:`depot.fields.upload.UploadedFile` by itself whenever it's content is set.
31
+
32
+ """
33
+ impl = types.Unicode
34
+ cache_ok = False
35
+
36
+ def __init__(self, filters=tuple(), upload_type=UploadedFile, upload_storage=None, *args, **kw):
37
+ super(UploadedFileField, self).__init__(*args, **kw)
38
+ self._filters = filters
39
+ self._upload_type = upload_type
40
+ self._upload_storage = upload_storage
41
+
42
+ def load_dialect_impl(self, dialect):
43
+ return dialect.type_descriptor(types.VARCHAR(4000))
44
+
45
+ def process_bind_param(self, value, dialect):
46
+ if not value:
47
+ return None
48
+
49
+ if not isinstance(value, self._upload_type):
50
+ raise ValueError('AttachmentField requires %s, '
51
+ 'got %s instead' % (self._upload_type, type(value)))
52
+
53
+ return value.encode()
54
+
55
+ def process_result_value(self, value, dialect):
56
+ if not value:
57
+ return None
58
+ return self._upload_type.decode(value)
59
+
60
+
61
+ class _SQLAMutationTracker(object):
62
+ mapped_entities = {}
63
+
64
+ @classmethod
65
+ def _field_set(cls, target, value, oldvalue, initiator):
66
+ if value is None or isinstance(value, UploadedFile):
67
+ return value
68
+
69
+ inspection = inspect(target)
70
+ set_property = inspection.mapper.get_property(initiator.key)
71
+ column_type = set_property.columns[0].type
72
+ assert(isinstance(column_type, UploadedFileField))
73
+
74
+ upload_type = column_type._upload_type
75
+ value = upload_type(value, column_type._upload_storage)
76
+ value._apply_filters(column_type._filters)
77
+
78
+ # Mark newly added files on assignment, so that in case of a rollback
79
+ # without going through flush we have track of the files
80
+ # that need to be rolled back.
81
+ session = inspection.session
82
+ if session is not None:
83
+ session._depot_new = getattr(session, '_depot_new', set())
84
+ session._depot_new.update(value.files)
85
+ else:
86
+ # If object is not attached to a session preserve the files
87
+ # until it gets added to one.
88
+ target._depot_new = getattr(target, '_depot_new', set())
89
+ target._depot_new.update(value.files)
90
+
91
+ return value
92
+
93
+ @classmethod
94
+ def _mapper_configured(cls, mapper, class_):
95
+ for mapper_property in mapper.iterate_properties:
96
+ if isinstance(mapper_property, ColumnProperty):
97
+ for idx, col in enumerate(mapper_property.columns):
98
+ if isinstance(col.type, UploadedFileField):
99
+ if idx > 0:
100
+ # Not clear when this might happen, but ColumnProperty can have
101
+ # multiple columns assigned. We should probably take the first one
102
+ # as is done by ColumnProperty.expression but there is no guarantee
103
+ # that it would be the right thing to do.
104
+ raise TypeError('UploadedFileField currently supports a single column')
105
+ cls.mapped_entities.setdefault(class_, []).append(mapper_property.key)
106
+ event.listen(mapper_property, 'set', cls._field_set, retval=True, propagate=True)
107
+
108
+ @classmethod
109
+ def _session_rollback(cls, session, previous_transaction):
110
+ if hasattr(session, '_depot_old'):
111
+ del session._depot_old
112
+ if hasattr(session, '_depot_new'):
113
+ for entry in session._depot_new:
114
+ depot, fileid = entry.split('/', 1)
115
+ depot = DepotManager.get(depot)
116
+ depot.delete(fileid)
117
+ del session._depot_new
118
+
119
+ @classmethod
120
+ def _session_committed(cls, session):
121
+ if hasattr(session, '_depot_old'):
122
+ for entry in session._depot_old:
123
+ depot, fileid = entry.split('/', 1)
124
+ depot = DepotManager.get(depot)
125
+ depot.delete(fileid)
126
+ del session._depot_old
127
+ if hasattr(session, '_depot_new'):
128
+ del session._depot_new
129
+
130
+ @classmethod
131
+ def _session_flush(cls, session, flush_context, instances):
132
+ for obj in session.deleted:
133
+ class_ = obj.__class__
134
+ tracked_columns = cls.mapped_entities.get(class_, tuple())
135
+ for col in tracked_columns:
136
+ value = getattr(obj, col)
137
+ if value is not None:
138
+ session._depot_old = getattr(session, '_depot_old', set())
139
+ session._depot_old.update(value.files)
140
+
141
+ for obj in session.new.union(session.dirty):
142
+ class_ = obj.__class__
143
+ tracked_columns = cls.mapped_entities.get(class_, tuple())
144
+ for col in tracked_columns:
145
+ history = get_history(obj, col)
146
+ added_files = itertools.chain(*(f.files for f in history.added
147
+ if f is not None))
148
+ deleted_files = itertools.chain(*(f.files for f in history.deleted
149
+ if f is not None))
150
+
151
+ session._depot_new = getattr(session, '_depot_new', set())
152
+ session._depot_new.update(added_files)
153
+ session._depot_old = getattr(session, '_depot_old', set())
154
+ session._depot_old.update(deleted_files)
155
+
156
+ @classmethod
157
+ def _session_deleted_obj(cls, session, obj):
158
+ # Tracking indirectly deleted objects
159
+ # a relationship.remove, or a query().filter().delete()
160
+ class_ = obj.__class__
161
+ tracked_columns = cls.mapped_entities.get(class_, tuple())
162
+ for col in tracked_columns:
163
+ value = getattr(obj, col)
164
+ if value is not None:
165
+ session._depot_old = getattr(session, '_depot_old', set())
166
+ session._depot_old.update(value.files)
167
+
168
+ @classmethod
169
+ def _session_attached(cls, session, instance):
170
+ session._depot_new = getattr(session, '_depot_new', set())
171
+ session._depot_new.update(getattr(instance, '_depot_new', set()))
172
+
173
+ @classmethod
174
+ def setup(cls):
175
+ event.listen(orm.Mapper, 'mapper_configured', cls._mapper_configured)
176
+ event.listen(Session, 'after_soft_rollback', cls._session_rollback)
177
+ event.listen(Session, 'after_commit', cls._session_committed)
178
+ event.listen(Session, 'before_attach', cls._session_attached)
179
+ event.listen(Session, 'before_flush', cls._session_flush)
180
+ event.listen(Session, 'persistent_to_deleted', cls._session_deleted_obj)
181
+
182
+ _SQLAMutationTracker.setup()
183
+
184
+ try: # pragma: no cover
185
+ from sprox.sa.widgetselector import SAWidgetSelector
186
+ from tw2.forms import FileField as TW2FileField
187
+ SAWidgetSelector.default_widgets.setdefault(UploadedFileField, TW2FileField)
188
+
189
+ from sprox.sa.validatorselector import SAValidatorSelector
190
+ from ..validators import TW2FileIntentValidator
191
+ SAValidatorSelector.default_validators.setdefault(UploadedFileField, TW2FileIntentValidator)
192
+ except ImportError: # pragma: no cover
193
+ pass
depot/fields/upload.py ADDED
@@ -0,0 +1,73 @@
1
+ from depot.manager import DepotManager
2
+ from .interfaces import DepotFileInfo
3
+ import json
4
+
5
+
6
+ class UploadedFile(DepotFileInfo):
7
+ """Simple :class:`depot.fields.interfaces.DepotFileInfo` implementation that stores files.
8
+
9
+ Takes a file as content and uploads it to the depot while saving around
10
+ most file information. Pay attention that if the file gets replaced
11
+ through depot manually the ``UploadedFile`` will continue to have the old data.
12
+
13
+ Also provides support for encoding/decoding using JSON for storage inside
14
+ databases as a plain string.
15
+
16
+ Default attributes provided for all ``UploadedFile`` include:
17
+ - filename - This is the name of the uploaded file
18
+ - file_id - This is the ID of the uploaded file
19
+ - path - This is a depot_name/file_id path which can
20
+ be used with :meth:`DepotManager.get_file` to retrieve the file
21
+ - content_type - This is the content type of the uploaded file
22
+ - uploaded_at - This is the upload date in YYYY-MM-DD HH:MM:SS format
23
+ - url - Public url of the uploaded file
24
+ - file - The :class:`depot.io.interfaces.StoredFile` instance of the stored file
25
+ """
26
+ def process_content(self, content, filename=None, content_type=None):
27
+ """Standard implementation of :meth:`.DepotFileInfo.process_content`
28
+
29
+ This is the standard depot implementation of files upload, it will
30
+ store the file on the default depot and will provide the standard
31
+ attributes.
32
+
33
+ Subclasses will need to call this method to ensure the standard
34
+ set of attributes is provided.
35
+ """
36
+
37
+ file_path, file_id = self.store_content(content, filename, content_type)
38
+ self['file_id'] = file_id
39
+ self['path'] = file_path
40
+
41
+ saved_file = self.file
42
+ self['filename'] = saved_file.filename
43
+ self['content_type'] = saved_file.content_type
44
+ self['uploaded_at'] = saved_file.last_modified.strftime('%Y-%m-%d %H:%M:%S')
45
+ self['_public_url'] = saved_file.public_url
46
+
47
+ def store_content(self, content, filename=None, content_type=None):
48
+ file_id = self.depot.create(content, filename, content_type)
49
+ file_path = '%s/%s' % (self.depot_name, file_id)
50
+ self.files.append(file_path)
51
+ return file_path, file_id
52
+
53
+ def encode(self):
54
+ return json.dumps(self)
55
+
56
+ @classmethod
57
+ def decode(cls, data):
58
+ return cls(json.loads(data))
59
+
60
+ @property
61
+ def url(self):
62
+ public_url = self['_public_url']
63
+ if public_url:
64
+ return public_url
65
+ return DepotManager.get_middleware().url_for(self['path'])
66
+
67
+ @property
68
+ def depot(self):
69
+ return DepotManager.get(self.depot_name)
70
+
71
+ @property
72
+ def file(self):
73
+ return self.depot.get(self.file_id)
depot/io/__init__.py ADDED
@@ -0,0 +1 @@
1
+