20 import os |
20 import os |
21 import sys |
21 import sys |
22 from os import unlink, path as osp |
22 from os import unlink, path as osp |
23 from contextlib import contextmanager |
23 from contextlib import contextmanager |
24 import tempfile |
24 import tempfile |
25 |
|
26 from six import PY2, PY3, text_type, binary_type |
|
27 |
25 |
28 from logilab.common import nullobject |
26 from logilab.common import nullobject |
29 |
27 |
30 from yams.schema import role_name |
28 from yams.schema import role_name |
31 |
29 |
111 |
109 |
112 |
110 |
113 class BytesFileSystemStorage(Storage): |
111 class BytesFileSystemStorage(Storage): |
114 """store Bytes attribute value on the file system""" |
112 """store Bytes attribute value on the file system""" |
115 def __init__(self, defaultdir, fsencoding=_marker, wmode=0o444): |
113 def __init__(self, defaultdir, fsencoding=_marker, wmode=0o444): |
116 if PY3: |
114 if fsencoding is not _marker: |
117 if not isinstance(defaultdir, text_type): |
115 raise ValueError('fsencoding is no longer supported in python 3') |
118 raise TypeError('defaultdir must be a unicode object in python 3') |
|
119 if fsencoding is not _marker: |
|
120 raise ValueError('fsencoding is no longer supported in python 3') |
|
121 else: |
|
122 self.fsencoding = fsencoding or 'utf-8' |
|
123 if isinstance(defaultdir, text_type): |
|
124 defaultdir = defaultdir.encode(fsencoding) |
|
125 self.default_directory = defaultdir |
116 self.default_directory = defaultdir |
126 # extra umask to use when creating file |
117 # extra umask to use when creating file |
127 # 0444 as in "only allow read bit in permission" |
118 # 0444 as in "only allow read bit in permission" |
128 self._wmode = wmode |
119 self._wmode = wmode |
129 |
120 |
158 else: |
149 else: |
159 binary = entity.cw_edited.pop(attr) |
150 binary = entity.cw_edited.pop(attr) |
160 if binary is not None: |
151 if binary is not None: |
161 fd, fpath = self.new_fs_path(entity, attr) |
152 fd, fpath = self.new_fs_path(entity, attr) |
162 # bytes storage used to store file's path |
153 # bytes storage used to store file's path |
163 binary_obj = Binary(fpath if PY2 else fpath.encode('utf-8')) |
154 binary_obj = Binary(fpath.encode('utf-8')) |
164 entity.cw_edited.edited_attribute(attr, binary_obj) |
155 entity.cw_edited.edited_attribute(attr, binary_obj) |
165 self._writecontent(fd, binary) |
156 self._writecontent(fd, binary) |
166 AddFileOp.get_instance(entity._cw).add_data(fpath) |
157 AddFileOp.get_instance(entity._cw).add_data(fpath) |
167 return binary |
158 return binary |
168 |
159 |
202 # reinstall poped value |
193 # reinstall poped value |
203 if fpath is None: |
194 if fpath is None: |
204 entity.cw_edited.edited_attribute(attr, None) |
195 entity.cw_edited.edited_attribute(attr, None) |
205 else: |
196 else: |
206 # register the new location for the file. |
197 # register the new location for the file. |
207 binary_obj = Binary(fpath if PY2 else fpath.encode('utf-8')) |
198 binary_obj = Binary(fpath.encode('utf-8')) |
208 entity.cw_edited.edited_attribute(attr, binary_obj) |
199 entity.cw_edited.edited_attribute(attr, binary_obj) |
209 if oldpath is not None and oldpath != fpath: |
200 if oldpath is not None and oldpath != fpath: |
210 # Mark the old file as useless so the file will be removed at |
201 # Mark the old file as useless so the file will be removed at |
211 # commit. |
202 # commit. |
212 DeleteFileOp.get_instance(entity._cw).add_data(oldpath) |
203 DeleteFileOp.get_instance(entity._cw).add_data(oldpath) |
222 # We try to get some hint about how to name the file using attribute's |
213 # We try to get some hint about how to name the file using attribute's |
223 # name metadata, so we use the real file name and extension when |
214 # name metadata, so we use the real file name and extension when |
224 # available. Keeping the extension is useful for example in the case of |
215 # available. Keeping the extension is useful for example in the case of |
225 # PIL processing that use filename extension to detect content-type, as |
216 # PIL processing that use filename extension to detect content-type, as |
226 # well as providing more understandable file names on the fs. |
217 # well as providing more understandable file names on the fs. |
227 if PY2: |
|
228 attr = attr.encode('ascii') |
|
229 basename = [str(entity.eid), attr] |
218 basename = [str(entity.eid), attr] |
230 name = entity.cw_attr_metadata(attr, 'name') |
219 name = entity.cw_attr_metadata(attr, 'name') |
231 if name is not None: |
220 if name is not None: |
232 basename.append(name.encode(self.fsencoding) if PY2 else name) |
221 basename.append(name) |
233 fd, fspath = uniquify_path(self.default_directory, |
222 fd, fspath = uniquify_path(self.default_directory, |
234 '_'.join(basename)) |
223 '_'.join(basename)) |
235 if fspath is None: |
224 if fspath is None: |
236 msg = entity._cw._('failed to uniquify path (%s, %s)') % ( |
225 msg = entity._cw._('failed to uniquify path (%s, %s)') % ( |
237 self.default_directory, '_'.join(basename)) |
226 self.default_directory, '_'.join(basename)) |
238 raise ValidationError(entity.eid, {role_name(attr, 'subject'): msg}) |
227 raise ValidationError(entity.eid, {role_name(attr, 'subject'): msg}) |
239 assert isinstance(fspath, str) # bytes on py2, unicode on py3 |
228 assert isinstance(fspath, str) |
240 return fd, fspath |
229 return fd, fspath |
241 |
230 |
242 def current_fs_path(self, entity, attr): |
231 def current_fs_path(self, entity, attr): |
243 """return the current fs_path of the attribute, or None is the attr is |
232 """return the current fs_path of the attribute, or None is the attr is |
244 not stored yet. |
233 not stored yet. |
249 attr, entity.cw_etype, entity.eid)) |
238 attr, entity.cw_etype, entity.eid)) |
250 rawvalue = cu.fetchone()[0] |
239 rawvalue = cu.fetchone()[0] |
251 if rawvalue is None: # no previous value |
240 if rawvalue is None: # no previous value |
252 return None |
241 return None |
253 fspath = sysource._process_value(rawvalue, cu.description[0], |
242 fspath = sysource._process_value(rawvalue, cu.description[0], |
254 binarywrap=binary_type) |
243 binarywrap=bytes) |
255 if PY3: |
244 return fspath.decode('utf-8') |
256 fspath = fspath.decode('utf-8') |
|
257 assert isinstance(fspath, str) # bytes on py2, unicode on py3 |
|
258 return fspath |
|
259 |
245 |
260 def migrate_entity(self, entity, attribute): |
246 def migrate_entity(self, entity, attribute): |
261 """migrate an entity attribute to the storage""" |
247 """migrate an entity attribute to the storage""" |
262 entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache) |
248 entity.cw_edited = EditedEntity(entity, **entity.cw_attr_cache) |
263 binary = self.entity_added(entity, attribute) |
249 binary = self.entity_added(entity, attribute) |
272 |
258 |
273 |
259 |
274 class AddFileOp(hook.DataOperationMixIn, hook.Operation): |
260 class AddFileOp(hook.DataOperationMixIn, hook.Operation): |
275 def rollback_event(self): |
261 def rollback_event(self): |
276 for filepath in self.get_data(): |
262 for filepath in self.get_data(): |
277 assert isinstance(filepath, str) # bytes on py2, unicode on py3 |
263 assert isinstance(filepath, str) |
278 try: |
264 try: |
279 unlink(filepath) |
265 unlink(filepath) |
280 except Exception as ex: |
266 except Exception as ex: |
281 self.error("can't remove %s: %s" % (filepath, ex)) |
267 self.error("can't remove %s: %s" % (filepath, ex)) |
282 |
268 |
283 class DeleteFileOp(hook.DataOperationMixIn, hook.Operation): |
269 class DeleteFileOp(hook.DataOperationMixIn, hook.Operation): |
284 def postcommit_event(self): |
270 def postcommit_event(self): |
285 for filepath in self.get_data(): |
271 for filepath in self.get_data(): |
286 assert isinstance(filepath, str) # bytes on py2, unicode on py3 |
272 assert isinstance(filepath, str) |
287 try: |
273 try: |
288 unlink(filepath) |
274 unlink(filepath) |
289 except Exception as ex: |
275 except Exception as ex: |
290 self.error("can't remove %s: %s" % (filepath, ex)) |
276 self.error("can't remove %s: %s" % (filepath, ex)) |