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