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 shutil
import copyfileobj
23 from werkzeug
.security
import safe_join
25 from indico
.core
import signals
26 from indico
.core
.config
import Config
27 from indico
.util
.signals
import named_objects_from_signal
28 from indico
.util
.string
import return_ascii
29 from indico
.web
.flask
.util
import send_file
32 def get_storage(backend_name
):
33 """Returns an FS object for the given backend.
35 The backend must be defined in the StorageBackends dict in the
36 indico config. Once a backend has been used it is assumed to
37 stay there forever or at least as long as it is referenced
40 Each backend definition uses the ``name:data`` notation, e.g.
41 ``fs:/some/folder/`` or ``foo:host=foo.host,token=secret``.
44 definition
= Config
.getInstance().getStorageBackends()[backend_name
]
46 raise RuntimeError('Storage backend does not exist: {}'.format(backend_name
))
47 name
, data
= definition
.split(':', 1)
49 backend
= get_storage_backends()[name
]
51 raise RuntimeError('Storage backend does not exist: {}'.format(backend_name
))
55 def get_storage_backends():
56 return named_objects_from_signal(signals
.get_storage_backends
.send(), plugin_attr
='plugin')
59 class StorageError(Exception):
60 """Exception used when a storage operation fails for any reason"""
63 class Storage(object):
64 """Base class for storage backends
66 To create a new storage backend, subclass this class and register
67 it using the `get_storage_backends` signal.
69 In case you wonder why both `save` and `send_file` require certain
70 file metadata: Depending on the storage backend, the information
71 needs to be set when saving the file (usually with external storage
72 services such as S3) or provided when sending the file back to the
73 client (for example if the file is accessed through the local file
76 :param data: A string of data used to initialize the backend.
77 This could be a path, an url, or any other kind of
78 information needed by the backend. If `simple_data`
79 is set, it should be a plain string. Otherwise it is
80 expected to be a string containing comma-separatey
81 key-value pairs: ``key=value,key2=value2,..``
84 #: unique name of the storage backend
86 #: plugin containing this backend - assigned automatically
88 #: if the backend uses a simple data string instead of key-value pairs
91 def __init__(self
, data
): # pragma: no cover
94 def _parse_data(self
, data
):
95 """Util to parse a key=value data string to a dict"""
96 return dict((x
.strip() for x
in item
.split('=', 1)) for item
in data
.split(',')) if data
else {}
98 def open(self
, file_id
): # pragma: no cover
99 """Opens a file in the storage for reading.
101 This returns a file-like object which contains the content of
104 :param file_id: The ID of the file within the storage backend.
106 raise NotImplementedError
108 def save(self
, name
, content_type
, filename
, fileobj
): # pragma: no cover
109 """Creates a new file in the storage.
111 This returns a a string identifier which can be used later to
112 retrieve the file from the storage.
114 :param name: A unique name for the file. This must be usable
115 as a filesystem path even though it depends on
116 the backend whether this name is used in such a
117 way or used at all. It SHOULD not contain ``..``
118 as this could result in two apparently-different
119 names to actually end up being the same on
120 storage backends that use the regular file system.
121 Using slashes in the name is allowed, but when
122 doing so extra caution is needed to avoid cases
123 which fail on a filesystem backend such as trying
124 to save 'foo' and 'foo/bar.txt'
125 :param content_type: The content-type of the file (may or may
126 not be used depending on the backend).
127 :param filename: The original filename of the file, used e.g.
128 when sending the file to a client (may or may
129 not be used depending on the backend).
130 :param fileobj: A file-like object containing the file data as
132 :return: unicode -- A unique identifier for the file.
134 raise NotImplementedError
136 def delete(self
, file_id
): # pragma: no cover
137 """Deletes a file from the storage.
139 :param file_id: The ID of the file within the storage backend.
141 raise NotImplementedError
143 def getsize(self
, file_id
): # pragma: no cover
144 """Gets the size in bytes of a file
146 :param file_id: The ID of the file within the storage backend.
148 raise NotImplementedError
150 def send_file(self
, file_id
, content_type
, filename
): # pragma: no cover
151 """Sends the file to the client.
153 This returns a flask response that will eventually result in
154 the user being offered to download the file (or view it in the
155 browser). Depending on the storage backend it may actually
156 send a redirect to an external URL where the file is available.
158 :param file_id: The ID of the file within the storage backend.
159 :param content_type: The content-type of the file (may or may
160 not be used depending on the backend)
161 :param filename: The file name to use when sending the file to
162 the client (may or may not be used depending
165 raise NotImplementedError
168 return '<{}()>'.format(type(self
).__name
__)
171 class FileSystemStorage(Storage
):
175 def __init__(self
, data
):
178 def _resolve_path(self
, path
):
179 full_path
= safe_join(self
.path
, path
)
180 if full_path
is None:
181 raise ValueError('Invalid path: {}'.format(path
))
184 def open(self
, file_id
):
186 return open(self
._resolve
_path
(file_id
), 'rb')
187 except Exception as e
:
188 raise StorageError('Could not open "{}": {}'.format(file_id
, e
)), None, sys
.exc_info()[2]
190 def save(self
, name
, content_type
, filename
, fileobj
):
192 filepath
= self
._resolve
_path
(name
)
193 if os
.path
.exists(filepath
):
194 raise ValueError('A file with this name already exists')
195 basedir
= os
.path
.dirname(filepath
)
196 if not os
.path
.isdir(basedir
):
198 with
open(filepath
, 'wb') as f
:
199 copyfileobj(fileobj
, f
, 1024 * 1024)
201 except Exception as e
:
202 raise StorageError('Could not save "{}": {}'.format(name
, e
)), None, sys
.exc_info()[2]
204 def delete(self
, file_id
):
206 os
.remove(self
._resolve
_path
(file_id
))
207 except Exception as e
:
208 raise StorageError('Could not delete "{}": {}'.format(file_id
, e
)), None, sys
.exc_info()[2]
210 def getsize(self
, file_id
):
212 return os
.path
.getsize(self
._resolve
_path
(file_id
))
213 except Exception as e
:
214 raise StorageError('Could not get size of "{}": {}'.format(file_id
, e
)), None, sys
.exc_info()[2]
216 def send_file(self
, file_id
, content_type
, filename
):
218 return send_file(filename
, self
._resolve
_path
(file_id
), content_type
)
219 except Exception as e
:
220 raise StorageError('Could not send "{}": {}'.format(file_id
, e
)), None, sys
.exc_info()[2]
224 return '<FileSystemStorage: {}>'.format(self
.path
)
227 @signals.get_storage_backends
.connect
228 def _get_storage_backends(sender
, **kwargs
):
229 return FileSystemStorage
232 @signals.app_created
.connect
233 def _check_storage_backends(app
, **kwargs
):
234 # This will raise RuntimeError if the backend names are not unique
235 get_storage_backends()