1 | ## $Id: imagestorage.py 11954 2014-11-13 16:54:17Z henrik $ |
---|
2 | ## |
---|
3 | ## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann |
---|
4 | ## This program is free software; you can redistribute it and/or modify |
---|
5 | ## it under the terms of the GNU General Public License as published by |
---|
6 | ## the Free Software Foundation; either version 2 of the License, or |
---|
7 | ## (at your option) any later version. |
---|
8 | ## |
---|
9 | ## This program is distributed in the hope that it will be useful, |
---|
10 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
11 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
12 | ## GNU General Public License for more details. |
---|
13 | ## |
---|
14 | ## You should have received a copy of the GNU General Public License |
---|
15 | ## along with this program; if not, write to the Free Software |
---|
16 | ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
---|
17 | ## |
---|
18 | """A storage for image (and other) files. |
---|
19 | |
---|
20 | A few words about storing files with ``waeup.ikoba``. The need for this |
---|
21 | feature arised initially from the need to store passport files for |
---|
22 | customers. These files are dynamic (can be changed |
---|
23 | anytime), mean a lot of traffic and cost a lot of memory/disk space. |
---|
24 | |
---|
25 | **Design Basics** |
---|
26 | |
---|
27 | While one *can* store images and similar 'large binary objects' aka |
---|
28 | blobs in the ZODB, this approach quickly becomes cumbersome and |
---|
29 | difficult to understand. The worst approach here would be to store |
---|
30 | images as regular byte-stream objects. ZODB supports this but |
---|
31 | obviously access is slow (data must be looked up in the one |
---|
32 | ``Data.fs`` file, each file has to be sent to the ZEO server and back, |
---|
33 | etc.). |
---|
34 | |
---|
35 | A bit less worse is the approach to store images in the ZODB but as |
---|
36 | Blobs. ZODB supports storing blobs in separate files in order to |
---|
37 | accelerate lookup/retrieval of these files. The files, however, have |
---|
38 | to be sent to the ZEO server (and back on lookups) which means a |
---|
39 | bottleneck and will easily result in an increased number of |
---|
40 | ``ConflictErrors`` even on simple reads. |
---|
41 | |
---|
42 | The advantage of both ZODB-geared approaches is, of course, complete |
---|
43 | database consistency. ZODB will guarantee that your files are |
---|
44 | available under some object name and can be handled as any other |
---|
45 | Python object. |
---|
46 | |
---|
47 | Another approach is to leave the ZODB behind and to store images and |
---|
48 | other files in filesystem directly. This is faster (no ZEO contacts, |
---|
49 | etc.), reduces probability of `ConflictErrors`, keeps the ZODB |
---|
50 | smaller, and enables direct access (over filesystem) to the |
---|
51 | files. Furthermore steps might be better understandable for |
---|
52 | third-party developers. We opted for this last option. |
---|
53 | |
---|
54 | **External File Store** |
---|
55 | |
---|
56 | Our implementation for storing-files-API is defined in |
---|
57 | :class:`ExtFileStore`. An instance of this file storage (which is also |
---|
58 | able to store non-image files) is available at runtime as a global |
---|
59 | utility implementing :class:`waeup.ikoba.interfaces.IExtFileStore`. |
---|
60 | |
---|
61 | The main task of this central component is to maintain a filesystem |
---|
62 | root path for all files to be stored. It also provides methods to |
---|
63 | store/get files under certain file ids which identify certain files |
---|
64 | locally. |
---|
65 | |
---|
66 | So, to store a file away, you can do something like this: |
---|
67 | |
---|
68 | >>> from StringIO import StringIO |
---|
69 | >>> from zope.component import getUtility |
---|
70 | >>> from waeup.ikoba.interfaces import IExtFileStore |
---|
71 | >>> store = getUtility(IExtFileStore) |
---|
72 | >>> store.createFile('myfile.txt', StringIO('some file content')) |
---|
73 | |
---|
74 | All you need is a filename and the file-like object containing the |
---|
75 | real file data. |
---|
76 | |
---|
77 | This will store the file somewhere (you shouldn't make too much |
---|
78 | assumptions about the real filesystem path here). |
---|
79 | |
---|
80 | Later, we can get the file back like this: |
---|
81 | |
---|
82 | >>> store.getFile('myfile') |
---|
83 | <open file ...> |
---|
84 | |
---|
85 | Please note, that we ask for ``myfile`` instead of ``myfile.jpg`` as |
---|
86 | the file id should not make a difference for different filename |
---|
87 | extensions. The file id for ``sample.jpg`` thus could simply be |
---|
88 | ``sample``. |
---|
89 | |
---|
90 | What we get back is a file or file-like object already opened for |
---|
91 | reading: |
---|
92 | |
---|
93 | >>> store.getFile('myfile').read() |
---|
94 | 'some file content' |
---|
95 | |
---|
96 | **Handlers: Special Places for Special Files** |
---|
97 | |
---|
98 | The file store supports special handling for certain files. For |
---|
99 | example we want some customer images to be stored in a different directory |
---|
100 | than other customer images, etc. Because the file store cannot know all |
---|
101 | details about these special treatment of certain files, it looks up |
---|
102 | helpers (handlers) to provide the information it needs for really |
---|
103 | storing the files at the correct location. |
---|
104 | |
---|
105 | That a file stored in filestore needs special handling can be |
---|
106 | indicated by special filenames. These filenames start with a marker like |
---|
107 | this:: |
---|
108 | |
---|
109 | __<MARKER-STRING>__real-filename |
---|
110 | |
---|
111 | Please note the double underscores before and after the marker |
---|
112 | string. They indicate that all in between is a marker. |
---|
113 | |
---|
114 | If you store a file in file store with such a filename (we call this a |
---|
115 | `file_id` to distuingish it from real world filenames), the file store |
---|
116 | will look up a handler for ``<MARKER-STRING>`` and pass it the file to |
---|
117 | store. The handler then will return the internal path to store the |
---|
118 | file and possibly do additional things as well like validating the |
---|
119 | file or similar. |
---|
120 | |
---|
121 | Examples for such a file store handler can be found in the |
---|
122 | :mod:`waeup.ikoba.customers.customer` module. Please see also the |
---|
123 | :class:`DefaultFileStoreHandler` class below for more details. |
---|
124 | |
---|
125 | The file store looks up handlers by utility lookups: it looks for a |
---|
126 | named utiliy providing |
---|
127 | :class:`waeup.ikoba.interfaces.IFileStoreHandler` and named like the |
---|
128 | marker string (without leading/trailing underscores) in lower |
---|
129 | case. For example if the file id would be |
---|
130 | |
---|
131 | ``__IMG_USER__manfred`` |
---|
132 | |
---|
133 | then the looked up utility should be registered under name |
---|
134 | |
---|
135 | ``img_user`` |
---|
136 | |
---|
137 | and provide :class:`waeup.ikoba.interfaces.IFileStoreHandler`. If no |
---|
138 | such utility can be found, a default handler is used instead |
---|
139 | (see :class:`DefaultFileStoreHandler`). |
---|
140 | |
---|
141 | **About File IDs and Filenames** |
---|
142 | |
---|
143 | In the waeup.ikoba package we want to store documents like CVs, |
---|
144 | photographs, and similar. Each of this documents might come into the |
---|
145 | system with different filename extensions. This could be a problem as |
---|
146 | the browser components might have to set different response headers |
---|
147 | for different filetypes and we nevertheless want to make sure that |
---|
148 | only one file is stored per document. For instance we don't want |
---|
149 | ``passport.jpg`` *and* ``passport.png`` but only one of them. |
---|
150 | |
---|
151 | The default components like :class:`DefaultFileStoreHandler` take care |
---|
152 | of this by searching the filesystem for already existing files with |
---|
153 | same file id and eventually removing them. |
---|
154 | |
---|
155 | Therefore file ids should never include filename extensions (except if |
---|
156 | you only support exactly one filename extension for a certain |
---|
157 | document). The only part where you should add an extension (and it is |
---|
158 | important to do so) is when creating new files: when a file was |
---|
159 | uploaded you can pass in the filename (including the filename |
---|
160 | extension) and the file stored in external file store will (most |
---|
161 | probably) have a different name but the same extension as the original |
---|
162 | file. |
---|
163 | |
---|
164 | When looking for the file, you however only have to give the file id |
---|
165 | and the handlers should find the right file for you, regardless of the |
---|
166 | filename extension it has. |
---|
167 | |
---|
168 | **Context Adapters: Knowing Your Family** |
---|
169 | |
---|
170 | Often the internal filename or file id of a file depends on a |
---|
171 | context. For example when we store passport photographs of customers, |
---|
172 | then each image belongs to a certain customer instance. It is not |
---|
173 | difficult to maintain such a connection manually: Say every customer |
---|
174 | had an id, then we could put this id into the filename as well and |
---|
175 | would build the filename to store/get the connected file by using that |
---|
176 | filename. You then would create filenames of a format like this:: |
---|
177 | |
---|
178 | __<MARKER-STRING>__customer0001 |
---|
179 | |
---|
180 | where ``customer0001`` would tell exactly which customer you can see |
---|
181 | on the photograph. You notice that the internal file id might have |
---|
182 | nothing to do with once uploaded filenames. The id above could have |
---|
183 | been uploaded with filename ``manfred.jpg`` but with the new file id |
---|
184 | we are able to find the file again later. |
---|
185 | |
---|
186 | Unfortunately it might soon get boring or cumbersome to retype this |
---|
187 | building of filenames for a certain type of context, especially if |
---|
188 | your filenames take more of the context into account than only a |
---|
189 | simple id. |
---|
190 | |
---|
191 | Therefore you can define filename building for a context as an adapter |
---|
192 | that then could be looked up by other components simply by doing |
---|
193 | something like: |
---|
194 | |
---|
195 | >>> from waeup.ikoba.interfaces import IFileStoreNameChooser |
---|
196 | >>> file_id = IFileStoreNameChooser(my_context_obj) |
---|
197 | |
---|
198 | If you later want to change the way file ids are created from a |
---|
199 | certain context, you only have to change the adapter implementation |
---|
200 | accordingly. |
---|
201 | |
---|
202 | Note, that this is only a convenience component. You don't have to |
---|
203 | define context adapters but it makes things easier for others if you |
---|
204 | do, as you don't have to remember the exact file id creation method |
---|
205 | all the time and can change things quick and in only one location if |
---|
206 | you need to do so. |
---|
207 | |
---|
208 | Please see the :class:`FileStoreNameChooser` default implementation |
---|
209 | below for details. |
---|
210 | |
---|
211 | """ |
---|
212 | import glob |
---|
213 | import grok |
---|
214 | import os |
---|
215 | import tempfile |
---|
216 | from hurry.file import HurryFile |
---|
217 | from hurry.file.interfaces import IFileRetrieval |
---|
218 | from zope.component import queryUtility |
---|
219 | from zope.interface import Interface |
---|
220 | from waeup.ikoba.interfaces import ( |
---|
221 | IFileStoreNameChooser, IExtFileStore, IFileStoreHandler,) |
---|
222 | |
---|
223 | class FileStoreNameChooser(grok.Adapter): |
---|
224 | """Default file store name chooser. |
---|
225 | |
---|
226 | File store name choosers pick a file id, a string, for a certain |
---|
227 | context object. They are normally registered as adapters for a |
---|
228 | certain content type and know how to build the file id for this |
---|
229 | special type of context. |
---|
230 | |
---|
231 | Provides the :class:`waeup.ikoba.interfaces.IFileStoreNameChooser` |
---|
232 | interface. |
---|
233 | |
---|
234 | This default file name chosser accepts almost every name as long |
---|
235 | as it is a string or unicode object. |
---|
236 | """ |
---|
237 | grok.context(Interface) |
---|
238 | grok.implements(IFileStoreNameChooser) |
---|
239 | |
---|
240 | def checkName(self, name, attr=None): |
---|
241 | """Check whether a given name (file id) is valid. |
---|
242 | |
---|
243 | Raises a user error if the name is not valid. |
---|
244 | |
---|
245 | For the default file store name chooser any name is valid as |
---|
246 | long as it is a string. |
---|
247 | |
---|
248 | The `attr` is not taken into account here. |
---|
249 | """ |
---|
250 | if isinstance(name, basestring): |
---|
251 | return True |
---|
252 | return False |
---|
253 | |
---|
254 | def chooseName(self, name, attr=None): |
---|
255 | """Choose a unique valid file id for the object. |
---|
256 | |
---|
257 | The given name may be taken into account when choosing the |
---|
258 | name (file id). |
---|
259 | |
---|
260 | chooseName is expected to always choose a valid name (that |
---|
261 | would pass the checkName test) and never raise an error. |
---|
262 | |
---|
263 | For this default name chooser we return the given name if it |
---|
264 | is valid or ``unknown_file`` else. The `attr` param is not |
---|
265 | taken into account here. |
---|
266 | """ |
---|
267 | if self.checkName(name): |
---|
268 | return name |
---|
269 | return u'unknown_file' |
---|
270 | |
---|
271 | class ExtFileStore(object): |
---|
272 | """External file store. |
---|
273 | |
---|
274 | External file stores are meant to store files 'externally' of the |
---|
275 | ZODB, i.e. in filesystem. |
---|
276 | |
---|
277 | Most important attribute of the external file store is the `root` |
---|
278 | path which gives the path to the location where files will be |
---|
279 | stored within. |
---|
280 | |
---|
281 | By default `root` is a ``'media/'`` directory in the root of the |
---|
282 | datacenter root of a site. |
---|
283 | |
---|
284 | The `root` attribute is 'read-only' because you normally don't |
---|
285 | want to change this path -- it is dynamic. That means, if you call |
---|
286 | the file store from 'within' a site, the root path will be located |
---|
287 | inside this site (a :class:`waeup.ikoba.Company` instance). If |
---|
288 | you call it from 'outside' a site some temporary dir (always the |
---|
289 | same during lifetime of the file store instance) will be used. The |
---|
290 | term 'temporary' tells what you can expect from this path |
---|
291 | persistence-wise. |
---|
292 | |
---|
293 | If you insist, you can pass a root path on initialization to the |
---|
294 | constructor but when calling from within a site afterwards, the |
---|
295 | site will override your setting for security measures. This way |
---|
296 | you can safely use one file store for different sites in a Zope |
---|
297 | instance simultanously and files from one site won't show up in |
---|
298 | another. |
---|
299 | |
---|
300 | An ExtFileStore instance is available as a global utility |
---|
301 | implementing :class:`waeup.ikoba.interfaces.IExtFileStore`. |
---|
302 | |
---|
303 | To add and retrieve files from the storage, use the appropriate |
---|
304 | methods below. |
---|
305 | """ |
---|
306 | |
---|
307 | grok.implements(IExtFileStore) |
---|
308 | |
---|
309 | _root = None |
---|
310 | |
---|
311 | @property |
---|
312 | def root(self): |
---|
313 | """Root dir of this storage. |
---|
314 | |
---|
315 | The root dir is a readonly value determined dynamically. It |
---|
316 | holds media files for sites or other components. |
---|
317 | |
---|
318 | If a site is available we return a ``media/`` dir in the |
---|
319 | datacenter storage dir. |
---|
320 | |
---|
321 | Otherwise we create a temporary dir which will be remembered |
---|
322 | on next call. |
---|
323 | |
---|
324 | If a site exists and has a datacenter, it has always |
---|
325 | precedence over temporary dirs, also after a temporary |
---|
326 | directory was created. |
---|
327 | |
---|
328 | Please note that retrieving `root` is expensive. You might |
---|
329 | want to store a copy once retrieved in order to minimize the |
---|
330 | number of calls to `root`. |
---|
331 | |
---|
332 | """ |
---|
333 | site = grok.getSite() |
---|
334 | if site is not None: |
---|
335 | root = os.path.join(site['datacenter'].storage, 'media') |
---|
336 | return root |
---|
337 | if self._root is None: |
---|
338 | self._root = tempfile.mkdtemp() |
---|
339 | return self._root |
---|
340 | |
---|
341 | def __init__(self, root=None): |
---|
342 | self._root = root |
---|
343 | return |
---|
344 | |
---|
345 | def _pathFromFileID(self, file_id): |
---|
346 | """Helper method to create filesystem path from FileID. |
---|
347 | |
---|
348 | Used class-internally. Do not rely on this method when working |
---|
349 | with an :class:`ExtFileStore` instance from other components. |
---|
350 | """ |
---|
351 | marker, filename, base, ext = self.extractMarker(file_id) |
---|
352 | handler = queryUtility(IFileStoreHandler, name=marker, |
---|
353 | default=DefaultFileStoreHandler()) |
---|
354 | path = handler.pathFromFileID(self, self.root, file_id) |
---|
355 | return path |
---|
356 | |
---|
357 | def getFile(self, file_id): |
---|
358 | """Get a file stored under file ID `file_id`. |
---|
359 | |
---|
360 | Returns a file already opened for reading. |
---|
361 | |
---|
362 | If the file cannot be found ``None`` is returned. |
---|
363 | |
---|
364 | This methods takes into account registered handlers for any |
---|
365 | marker put into the file_id. |
---|
366 | |
---|
367 | .. seealso:: :class:`DefaultFileStoreHandler` |
---|
368 | """ |
---|
369 | path = self._pathFromFileID(file_id) |
---|
370 | if not os.path.exists(path): |
---|
371 | return None |
---|
372 | fd = open(path, 'rb') |
---|
373 | return fd |
---|
374 | |
---|
375 | def getFileByContext(self, context, attr=None): |
---|
376 | """Get a file for given context. |
---|
377 | |
---|
378 | Returns a file already opened for reading. |
---|
379 | |
---|
380 | If the file cannot be found ``None`` is returned. |
---|
381 | |
---|
382 | This method takes into account registered handlers and file |
---|
383 | name choosers for context types to build an intermediate file |
---|
384 | id for the context and `attr` given. |
---|
385 | |
---|
386 | Both, `context` and `attr` are used to find (`context`) |
---|
387 | and feed (`attr`) an appropriate file name chooser. |
---|
388 | |
---|
389 | This is a convenience method that internally calls |
---|
390 | :meth:`getFile`. |
---|
391 | |
---|
392 | .. seealso:: :class:`FileStoreNameChooser`, |
---|
393 | :class:`DefaultFileStoreHandler`. |
---|
394 | """ |
---|
395 | file_id = IFileStoreNameChooser(context).chooseName(attr=attr) |
---|
396 | return self.getFile(file_id) |
---|
397 | |
---|
398 | def deleteFile(self, file_id): |
---|
399 | """Delete file stored under `file_id` in storage. |
---|
400 | |
---|
401 | The file is physically removed from filesystem. |
---|
402 | """ |
---|
403 | path = self._pathFromFileID(file_id) |
---|
404 | if not os.path.exists(path) or not os.path.isfile(path): |
---|
405 | return |
---|
406 | os.unlink(path) |
---|
407 | return |
---|
408 | |
---|
409 | def deleteFileByContext(self, context, attr=None): |
---|
410 | """Remove file identified by `context` and `attr` if it exists. |
---|
411 | |
---|
412 | This method takes into account registered handlers and file |
---|
413 | name choosers for context types to build an intermediate file |
---|
414 | id for the context and `attr` given. |
---|
415 | |
---|
416 | Both, `context` and `attr` are used to find (`context`) |
---|
417 | and feed (`attr`) an appropriate file name chooser. |
---|
418 | |
---|
419 | This is a convenience method that internally calls |
---|
420 | :meth:`getFile`. |
---|
421 | |
---|
422 | .. seealso:: :class:`FileStoreNameChooser`, |
---|
423 | :class:`DefaultFileStoreHandler`. |
---|
424 | |
---|
425 | """ |
---|
426 | file_id = IFileStoreNameChooser(context).chooseName(attr=attr) |
---|
427 | return self.deleteFile(file_id) |
---|
428 | |
---|
429 | def createFile(self, filename, f): |
---|
430 | """Store a file. |
---|
431 | """ |
---|
432 | root = self.root # Calls to self.root are expensive |
---|
433 | file_id = os.path.splitext(filename)[0] |
---|
434 | marker, filename, base, ext = self.extractMarker(filename) |
---|
435 | handler = queryUtility(IFileStoreHandler, name=marker, |
---|
436 | default=DefaultFileStoreHandler()) |
---|
437 | f, path, file_obj = handler.createFile( |
---|
438 | self, root, filename, file_id, f) |
---|
439 | dirname = os.path.dirname(path) |
---|
440 | if not os.path.exists(dirname): |
---|
441 | os.makedirs(dirname, 0755) |
---|
442 | open(path, 'wb').write(f.read()) |
---|
443 | return file_obj |
---|
444 | |
---|
445 | def extractMarker(self, file_id): |
---|
446 | """split filename into marker, filename, basename, and extension. |
---|
447 | |
---|
448 | A marker is a leading part of a string of form |
---|
449 | ``__MARKERNAME__`` followed by the real filename. This way we |
---|
450 | can put markers into a filename to request special processing. |
---|
451 | |
---|
452 | Returns a quadruple |
---|
453 | |
---|
454 | ``(marker, filename, basename, extension)`` |
---|
455 | |
---|
456 | where ``marker`` is the marker in lowercase, filename is the |
---|
457 | complete trailing real filename, ``basename`` is the basename |
---|
458 | of the filename and ``extension`` the filename extension of |
---|
459 | the trailing filename. See examples below. |
---|
460 | |
---|
461 | Example: |
---|
462 | |
---|
463 | >>> extractMarker('__MaRkEr__sample.jpg') |
---|
464 | ('marker', 'sample.jpg', 'sample', '.jpg') |
---|
465 | |
---|
466 | If no marker is contained, we assume the whole string to be a |
---|
467 | real filename: |
---|
468 | |
---|
469 | >>> extractMarker('no-marker.txt') |
---|
470 | ('', 'no-marker.txt', 'no-marker', '.txt') |
---|
471 | |
---|
472 | Filenames without extension give an empty extension string: |
---|
473 | |
---|
474 | >>> extractMarker('no-marker') |
---|
475 | ('', 'no-marker', 'no-marker', '') |
---|
476 | |
---|
477 | """ |
---|
478 | if not isinstance(file_id, basestring) or not file_id: |
---|
479 | return ('', '', '', '') |
---|
480 | parts = file_id.split('__', 2) |
---|
481 | marker = '' |
---|
482 | if len(parts) == 3 and parts[0] == '': |
---|
483 | marker = parts[1].lower() |
---|
484 | file_id = parts[2] |
---|
485 | basename, ext = os.path.splitext(file_id) |
---|
486 | return (marker, file_id, basename, ext) |
---|
487 | |
---|
488 | grok.global_utility(ExtFileStore, provides=IExtFileStore) |
---|
489 | |
---|
490 | class DefaultStorage(ExtFileStore): |
---|
491 | """Default storage for files. |
---|
492 | |
---|
493 | Registered globally as utility for |
---|
494 | :class:`hurry.file.interfaces.IFileRetrieval`. |
---|
495 | """ |
---|
496 | grok.provides(IFileRetrieval) |
---|
497 | |
---|
498 | grok.global_utility(DefaultStorage, provides=IFileRetrieval) |
---|
499 | |
---|
500 | class DefaultFileStoreHandler(grok.GlobalUtility): |
---|
501 | """A default handler for external file store. |
---|
502 | |
---|
503 | This handler is the fallback called by external file stores when |
---|
504 | there is no or an unknown marker in the file id. |
---|
505 | |
---|
506 | Registered globally as utility for |
---|
507 | :class:`waeup.ikoba.interfaces.IFileStoreHandler`. |
---|
508 | """ |
---|
509 | grok.implements(IFileStoreHandler) |
---|
510 | |
---|
511 | def _searchInPath(self, path): |
---|
512 | """Get complete path of any existing file starting with `path`. |
---|
513 | |
---|
514 | If no such file can be found, return input path. |
---|
515 | |
---|
516 | If multiple such files exist, return the first one. |
---|
517 | |
---|
518 | **Example:** |
---|
519 | |
---|
520 | Looking for a `path`:: |
---|
521 | |
---|
522 | '/tmp/myfile' |
---|
523 | |
---|
524 | will find any file like ``'/tmp/myfile.txt'``, |
---|
525 | ``'/tmp/myfile.jpg'`` but also ``'/tmp/myfile_any_attribute.pdf'``, |
---|
526 | if it exists. Therefore we must be careful. File attributes |
---|
527 | must come first: ``'/tmp/any_attribute_myfile.pdf'`` |
---|
528 | """ |
---|
529 | result = path |
---|
530 | if os.path.isdir(os.path.dirname(path)): |
---|
531 | file_iter = glob.iglob('%s*' % (path,)) |
---|
532 | try: |
---|
533 | result = file_iter.next() |
---|
534 | except StopIteration: |
---|
535 | pass |
---|
536 | return result |
---|
537 | |
---|
538 | def pathFromFileID(self, store, root, file_id): |
---|
539 | """Return a path for getting/storing a file with given file id. |
---|
540 | |
---|
541 | If there is already a file stored for the given file id, the |
---|
542 | path to this file is returned. |
---|
543 | |
---|
544 | If no such file exists yet (or the the only file existing has |
---|
545 | no filename extension at all) a path to store the file but |
---|
546 | without any filename extension is returned. |
---|
547 | """ |
---|
548 | path = os.path.join(root, file_id) |
---|
549 | return self._searchInPath(path) |
---|
550 | |
---|
551 | def createFile(self, store, root, filename, file_id, f): |
---|
552 | """Infos about what to store exactly and where. |
---|
553 | |
---|
554 | When a file should be handled by an external file storage, it |
---|
555 | looks up any handlers (like this one), passes runtime infos |
---|
556 | like the storage object, root path, filename, file_id, and the |
---|
557 | raw file object itself. |
---|
558 | |
---|
559 | The handler can then change the file, raise exceptions or |
---|
560 | whatever and return the result. |
---|
561 | |
---|
562 | This handler returns the input file as-is, a path returned by |
---|
563 | :meth:`pathFromFileID` and an instance of |
---|
564 | :class:`hurry.file.HurryFile` for further operations. |
---|
565 | |
---|
566 | Please note: although a handler has enough infos to store the |
---|
567 | file itself, it should leave that task to the calling file |
---|
568 | store. |
---|
569 | |
---|
570 | This method does, however, remove any existing files stored |
---|
571 | under the given file id. |
---|
572 | """ |
---|
573 | ext = os.path.splitext(filename)[1] |
---|
574 | path = self.pathFromFileID(store, root, file_id) |
---|
575 | base, old_ext = os.path.splitext(path) |
---|
576 | if old_ext != ext: |
---|
577 | if os.path.exists(path): |
---|
578 | os.unlink(path) |
---|
579 | path = base + ext |
---|
580 | return f, path, HurryFile(filename, file_id + ext) |
---|