source: main/waeup.sirp/trunk/src/waeup/sirp/imagestorage.py @ 6996

Last change on this file since 6996 was 6980, checked in by Henrik Bettermann, 13 years ago

We have to create a real (deep) list copy of self.keys() when deleting items of self. Otherwise the deletion of some of the items might fail.

I have no idea how to write regression tests with justifiable expenditure.

File size: 7.2 KB
RevLine 
[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$
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"""
24import grok
25import hashlib
26import os
[6528]27import transaction
28import warnings
[6519]29from StringIO import StringIO
30from ZODB.blob import Blob
31from persistent import Persistent
32from hurry.file.interfaces import IFileRetrieval
33from waeup.sirp.image import WAeUPImageFile
34from waeup.sirp.utils.helpers import cmp_files
35
36def md5digest(fd):
37    """Get an MD5 hexdigest for the file stored in `fd`.
38
39    `fd`
40      a file object open for reading.
41
42    """
43    return hashlib.md5(fd.read()).hexdigest()
44
45class Basket(grok.Container):
46    """A basket holds a set of image files with same hash.
47    """
[6528]48
[6519]49    def _del(self):
50        """Remove temporary files associated with local blobs.
51
52        A basket holds files as Blob objects. Unfortunately, if a
53        basket was not committed (put into ZODB), those blobs linger
54        around as real files in some temporary directory and won't be
55        removed.
56
57        This is a helper function to remove all those uncommitted
58        blobs that has to be called explicitly, for instance in tests.
59        """
[6980]60        key_list = list(self.keys())
[6519]61        for key in key_list:
62            item = self[key]
63            if getattr(item, '_p_oid', None):
64                # Don't mess around with blobs in ZODB
65                continue
66            fd = item.open('r')
67            name = getattr(fd, 'name', None)
68            fd.close()
69            if name is not None and os.path.exists(name):
70                os.unlink(name)
71            del self[key]
72        return
73
74    def getInternalId(self, fd):
[6528]75        """Get the basket-internal id for the file stored in `fd`.
76
77        `fd` must be a file open for reading. If an (byte-wise) equal
78        file can be found in the basket, its internal id (basket id)
79        is returned, ``None`` otherwise.
80        """
81        fd.seek(0)
[6519]82        for key, val in self.items():
83            fd_stored = val.open('r')
[6528]84            file_len = os.stat(fd_stored.name)[6]
85            if file_len == 0:
86                # Nasty workaround. Blobs seem to suffer from being emptied
87                # accidentally.
88                site = grok.getSite()
89                if site is not None:
90                    site.logger.warn(
91                        'Empty Blob detected: %s' % fd_stored.name)
92                warnings.warn("EMPTY BLOB DETECTED: %s" % fd_stored.name)
93                fd_stored.close()
94                val.open('w').write(fd.read())
95                return key
96            fd_stored.seek(0)
[6519]97            if cmp_files(fd, fd_stored):
98                fd_stored.close()
99                return key
100            fd_stored.close()
101        return None
102
103    @property
104    def curr_id(self):
[6528]105        """The current basket id.
106
107        An integer number which is not yet in use. If there are
108        already `maxint` entries in the basket, a :exc:`ValueError` is
109        raised. The latter is _highly_ unlikely. It would mean to have
110        more than 2**32 hash collisions, i.e. so many files with the
111        same MD5 sum.
112        """
[6519]113        num = 1
114        while True:
115            if str(num) not in self.keys():
116                return str(num)
117            num += 1
118            if num <= 0:
119                name = getattr(self, '__name__', None)
120                raise ValueError('Basket full: %s' % name)
121
122    def storeFile(self, fd, filename):
[6528]123        """Store the file in `fd` into the basket.
124
125        The file will be stored in a Blob.
126        """
127        fd.seek(0)
128        internal_id = self.getInternalId(fd) # Moves file pointer!
[6519]129        if internal_id is None:
130            internal_id = self.curr_id
[6528]131            fd.seek(0)
132            self[internal_id] = Blob()
133            transaction.commit() # Urgently needed to make the Blob
134                                 # persistent. Took me ages to find
135                                 # out that solution, which makes some
136                                 # design flaw in ZODB Blobs likely.
137            self[internal_id].open('w').write(fd.read())
138            fd.seek(0)
139            self._p_changed = True
[6519]140        return internal_id
141
142    def retrieveFile(self, basket_id):
[6528]143        """Retrieve a file open for reading with basket id `basket_id`.
144
145        If there is no such id, ``None`` is returned. It is the
146        callers responsibility to close the open file.
147        """
[6519]148        if basket_id in self.keys():
149            return self[basket_id].open('r')
150        return None
151
152class ImageStorage(grok.Container):
153    """A container for image files.
154    """
155    def _del(self):
156        for basket in self.values():
157            try:
158                basket._del()
159            except:
160                pass
161
162    def storeFile(self, fd, filename):
163        fd.seek(0)
164        digest = md5digest(fd)
165        fd.seek(0)
166        if not digest in self.keys():
167            self[digest] = Basket()
168        basket_id = self[digest].storeFile(fd, filename)
169        full_id = "%s-%s" % (digest, basket_id)
170        return full_id
171
172    def retrieveFile(self, file_id):
173        if not '-' in file_id:
174            return None
175        full_id, basket_id = file_id.split('-', 1)
176        if not full_id in self.keys():
177            return None
178        return self[full_id].retrieveFile(basket_id)
179
180class ImageStorageFileRetrieval(Persistent):
181    grok.implements(IFileRetrieval)
182
183    def getImageStorage(self):
184        site = grok.getSite()
185        if site is None:
186            return None
187        return site.get('images', None)
188
189    def isImageStorageEnabled(self):
190        site = grok.getSite()
191        if site is None:
192            return False
193        if site.get('images', None) is None:
194            return False
195        return True
196
197    def getFile(self, data):
198        # ImageStorage is disabled, so give fall-back behaviour for
199        # testing without ImageStorage
200        if not self.isImageStorageEnabled():
201            return StringIO(data)
202        storage = self.getImageStorage()
203        if storage is None:
204            raise ValueError('Cannot find an image storage')
[6528]205        result = storage.retrieveFile(data)
206        if result is None:
207            return StringIO(data)
[6519]208        return storage.retrieveFile(data)
209
210    def createFile(self, filename, f):
211        if not self.isImageStorageEnabled():
212            return WAeUPImageFile(filename, f.read())
213        storage = self.getImageStorage()
214        if storage is None:
215            raise ValueError('Cannot find an image storage')
216        file_id = storage.storeFile(f, filename)
217        return WAeUPImageFile(filename, file_id)
Note: See TracBrowser for help on using the repository browser.