1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 from __future__
import unicode_literals
21 from contextlib
import contextmanager
22 from shutil
import copyfileobj
23 from tempfile
import NamedTemporaryFile
25 from werkzeug
.security
import safe_join
27 from indico
.core
import signals
28 from indico
.core
.config
import Config
29 from indico
.util
.signals
import named_objects_from_signal
30 from indico
.util
.string
import return_ascii
31 from indico
.web
.flask
.util
import send_file
34 def get_storage(backend_name
):
35 """Returns an FS object for the given backend.
37 The backend must be defined in the StorageBackends dict in the
38 indico config. Once a backend has been used it is assumed to
39 stay there forever or at least as long as it is referenced
42 Each backend definition uses the ``name:data`` notation, e.g.
43 ``fs:/some/folder/`` or ``foo:host=foo.host,token=secret``.
46 definition
= Config
.getInstance().getStorageBackends()[backend_name
]
48 raise RuntimeError('Storage backend does not exist: {}'.format(backend_name
))
49 name
, data
= definition
.split(':', 1)
51 backend
= get_storage_backends()[name
]
53 raise RuntimeError('Storage backend {} has invalid type {}'.format(backend_name
, name
))
57 def get_storage_backends():
58 return named_objects_from_signal(signals
.get_storage_backends
.send(), plugin_attr
='plugin')
61 class StorageError(Exception):
62 """Exception used when a storage operation fails for any reason"""
65 class Storage(object):
66 """Base class for storage backends
68 To create a new storage backend, subclass this class and register
69 it using the `get_storage_backends` signal.
71 In case you wonder why both `save` and `send_file` require certain
72 file metadata: Depending on the storage backend, the information
73 needs to be set when saving the file (usually with external storage
74 services such as S3) or provided when sending the file back to the
75 client (for example if the file is accessed through the local file
78 :param data: A string of data used to initialize the backend.
79 This could be a path, an url, or any other kind of
80 information needed by the backend. If `simple_data`
81 is set, it should be a plain string. Otherwise it is
82 expected to be a string containing comma-separatey
83 key-value pairs: ``key=value,key2=value2,..``
86 #: unique name of the storage backend
88 #: plugin containing this backend - assigned automatically
90 #: if the backend uses a simple data string instead of key-value pairs
93 def __init__(self
, data
): # pragma: no cover
96 def _parse_data(self
, data
):
97 """Util to parse a key=value data string to a dict"""
98 return dict((x
.strip() for x
in item
.split('=', 1)) for item
in data
.split(',')) if data
else {}
100 def open(self
, file_id
): # pragma: no cover
101 """Opens a file in the storage for reading.
103 This returns a file-like object which contains the content of
106 :param file_id: The ID of the file within the storage backend.
108 raise NotImplementedError
111 def get_local_path(self
, file_id
):
112 """Returns a local path for the file.
114 While this path MAY point to the permanent location of the
115 stored file, it MUST NOT be used for anything but read
116 operations and MUST NOT be used after existing this function's
119 :param file_id: The ID of the file within the storage backend.
121 with self
.open(file_id
) as fd
:
122 with
NamedTemporaryFile(suffix
='indico.tmp', dir=Config
.getInstance().getUploadedFilesTempDir()) as tmpfile
:
123 copyfileobj(fd
, tmpfile
, 1024 * 1024)
127 def save(self
, name
, content_type
, filename
, fileobj
): # pragma: no cover
128 """Creates a new file in the storage.
130 This returns a a string identifier which can be used later to
131 retrieve the file from the storage.
133 :param name: A unique name for the file. This must be usable
134 as a filesystem path even though it depends on
135 the backend whether this name is used in such a
136 way or used at all. It SHOULD not contain ``..``
137 as this could result in two apparently-different
138 names to actually end up being the same on
139 storage backends that use the regular file system.
140 Using slashes in the name is allowed, but when
141 doing so extra caution is needed to avoid cases
142 which fail on a filesystem backend such as trying
143 to save 'foo' and 'foo/bar.txt'
144 :param content_type: The content-type of the file (may or may
145 not be used depending on the backend).
146 :param filename: The original filename of the file, used e.g.
147 when sending the file to a client (may or may
148 not be used depending on the backend).
149 :param fileobj: A file-like object containing the file data as
151 :return: unicode -- A unique identifier for the file.
153 raise NotImplementedError
155 def delete(self
, file_id
): # pragma: no cover
156 """Deletes a file from the storage.
158 :param file_id: The ID of the file within the storage backend.
160 raise NotImplementedError
162 def getsize(self
, file_id
): # pragma: no cover
163 """Gets the size in bytes of a file
165 :param file_id: The ID of the file within the storage backend.
167 raise NotImplementedError
169 def send_file(self
, file_id
, content_type
, filename
): # pragma: no cover
170 """Sends the file to the client.
172 This returns a flask response that will eventually result in
173 the user being offered to download the file (or view it in the
174 browser). Depending on the storage backend it may actually
175 send a redirect to an external URL where the file is available.
177 :param file_id: The ID of the file within the storage backend.
178 :param content_type: The content-type of the file (may or may
179 not be used depending on the backend)
180 :param filename: The file name to use when sending the file to
181 the client (may or may not be used depending
184 raise NotImplementedError
187 return '<{}()>'.format(type(self
).__name
__)
190 class ReadOnlyStorageMixin(object):
191 """Mixin that makes write operations fail with an error"""
193 def save(self
, name
, content_type
, filename
, fileobj
):
194 raise StorageError('Cannot write to read-only storage')
196 def delete(self
, file_id
):
197 raise StorageError('Cannot delete from read-only storage')
200 class FileSystemStorage(Storage
):
204 def __init__(self
, data
):
207 def _resolve_path(self
, path
):
208 full_path
= safe_join(self
.path
, path
)
209 if full_path
is None:
210 raise ValueError('Invalid path: {}'.format(path
))
213 def open(self
, file_id
):
215 return open(self
._resolve
_path
(file_id
), 'rb')
216 except Exception as e
:
217 raise StorageError('Could not open "{}": {}'.format(file_id
, e
)), None, sys
.exc_info()[2]
220 def get_local_path(self
, file_id
):
221 yield self
._resolve
_path
(file_id
)
223 def save(self
, name
, content_type
, filename
, fileobj
):
225 filepath
= self
._resolve
_path
(name
)
226 if os
.path
.exists(filepath
):
227 raise ValueError('A file with this name already exists')
228 basedir
= os
.path
.dirname(filepath
)
229 if not os
.path
.isdir(basedir
):
231 with
open(filepath
, 'wb') as f
:
232 copyfileobj(fileobj
, f
, 1024 * 1024)
234 except Exception as e
:
235 raise StorageError('Could not save "{}": {}'.format(name
, e
)), None, sys
.exc_info()[2]
237 def delete(self
, file_id
):
239 os
.remove(self
._resolve
_path
(file_id
))
240 except Exception as e
:
241 raise StorageError('Could not delete "{}": {}'.format(file_id
, e
)), None, sys
.exc_info()[2]
243 def getsize(self
, file_id
):
245 return os
.path
.getsize(self
._resolve
_path
(file_id
))
246 except Exception as e
:
247 raise StorageError('Could not get size of "{}": {}'.format(file_id
, e
)), None, sys
.exc_info()[2]
249 def send_file(self
, file_id
, content_type
, filename
):
251 return send_file(filename
, self
._resolve
_path
(file_id
), content_type
)
252 except Exception as e
:
253 raise StorageError('Could not send "{}": {}'.format(file_id
, e
)), None, sys
.exc_info()[2]
257 return '<FileSystemStorage: {}>'.format(self
.path
)
260 class ReadOnlyFileSystemStorage(ReadOnlyStorageMixin
, FileSystemStorage
):
265 return '<ReadOnlyFileSystemStorage: {}>'.format(self
.path
)
268 @signals.get_storage_backends
.connect
269 def _get_storage_backends(sender
, **kwargs
):
270 yield FileSystemStorage
271 yield ReadOnlyFileSystemStorage
274 @signals.app_created
.connect
275 def _check_storage_backends(app
, **kwargs
):
276 # This will raise RuntimeError if the backend names are not unique
277 get_storage_backends()