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