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

Last change on this file since 6613 was 6528, checked in by uli, 13 years ago

Fix imagestorage. It took ages to find out, that strange behaviour of
saved image files (content disappearing, etc.) was not due to the
imagestorage implementation, but most probably caused by some design
problem in ZODB Blob storage. Nevertheless the new implementation
seems to work properly.

File size: 7.2 KB
Line 
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
27import transaction
28import warnings
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    """
48
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        """
60        key_list = self.keys()
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):
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)
82        for key, val in self.items():
83            fd_stored = val.open('r')
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)
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):
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        """
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):
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!
129        if internal_id is None:
130            internal_id = self.curr_id
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
140        return internal_id
141
142    def retrieveFile(self, basket_id):
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        """
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')
205        result = storage.retrieveFile(data)
206        if result is None:
207            return StringIO(data)
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.