document: fix regressions at storage and node_descriptor
[odoo/odoo.git] / addons / document / document_storage.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #
6 #    Copyright (C) P. Christeas, 2009, all rights reserved
7 #
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 3 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, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 from osv import osv, fields
24 import os
25 import tools
26 import base64
27 import errno
28 import logging
29 import shutil
30 from StringIO import StringIO
31 import psycopg2
32
33 from tools.misc import ustr
34 from tools.translate import _
35
36 from osv.orm import except_orm
37
38 import random
39 import string
40 import pooler
41 import nodes
42 from content_index import cntIndex
43
44 DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config.get('root_path'), 'filestore'))
45
46
47 """ The algorithm of data storage
48
49 We have to consider 3 cases of data /retrieval/:
50  Given (context,path) we need to access the file (aka. node).
51  given (directory, context), we need one of its children (for listings, views)
52  given (ir.attachment, context), we need its data and metadata (node).
53
54 For data /storage/ we have the cases:
55  Have (ir.attachment, context), we modify the file (save, update, rename etc).
56  Have (directory, context), we create a file.
57  Have (path, context), we create or modify a file.
58  
59 Note that in all above cases, we don't explicitly choose the storage media,
60 but always require a context to be present.
61
62 Note that a node will not always have a corresponding ir.attachment. Dynamic
63 nodes, for once, won't. Their metadata will be computed by the parent storage
64 media + directory.
65
66 The algorithm says that in any of the above cases, our first goal is to locate
67 the node for any combination of search criteria. It would be wise NOT to 
68 represent each node in the path (like node[/] + node[/dir1] + node[/dir1/dir2])
69 but directly jump to the end node (like node[/dir1/dir2]) whenever possible.
70
71 We also contain all the parenting loop code in one function. This is intentional,
72 because one day this will be optimized in the db (Pg 8.4).
73
74
75 """
76
77 def random_name():
78     random.seed()
79     d = [random.choice(string.ascii_letters) for x in xrange(10) ]
80     name = "".join(d)
81     return name
82
83 INVALID_CHARS = {'*':str(hash('*')), '|':str(hash('|')) , "\\":str(hash("\\")), '/':'__', ':':str(hash(':')), '"':str(hash('"')), '<':str(hash('<')) , '>':str(hash('>')) , '?':str(hash('?'))}
84
85
86 def create_directory(path):
87     dir_name = random_name()
88     path = os.path.join(path, dir_name)
89     os.makedirs(path)
90     return dir_name
91
92 class nodefd_file(nodes.node_descriptor):
93     """ A descriptor to a real file
94
95     Inheriting directly from file doesn't work, since file exports
96     some read-only attributes (like 'name') that we don't like.
97     """
98     def __init__(self, parent, path, mode):
99         nodes.node_descriptor.__init__(self, parent)
100         self.__file = open(path, mode)
101         if mode.endswith('b'):
102             mode = mode[:-1]
103         self.mode = mode
104         self._size = os.stat(path).st_size
105         
106         for attr in ('closed', 'read', 'write', 'seek', 'tell', 'next'):
107             setattr(self,attr, getattr(self.__file, attr))
108
109     def size(self):
110         return self._size
111         
112     def __iter__(self):
113         return self
114
115     def close(self):
116         # TODO: locking in init, close()
117         fname = self.__file.name
118         self.__file.close()
119
120         if self.mode in ('w', 'w+', 'r+'):
121             par = self._get_parent()
122             cr = pooler.get_db(par.context.dbname).cursor()
123             icont = ''
124             mime = ''
125             filename = par.path
126             if isinstance(filename, (tuple, list)):
127                 filename = '/'.join(filename)
128             
129             try:
130                 mime, icont = cntIndex.doIndex(None, filename=filename,
131                         content_type=None, realfname=fname)
132             except Exception:
133                 logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
134                 pass
135
136             try:
137                 icont_u = ustr(icont)
138             except UnicodeError:
139                 icont_u = ''
140
141             try:
142                 fsize = os.stat(fname).st_size
143                 cr.execute("UPDATE ir_attachment " \
144                             " SET index_content = %s, file_type = %s, " \
145                             " file_size = %s " \
146                             "  WHERE id = %s",
147                             (icont_u, mime, fsize, par.file_id))
148                 par.content_length = fsize
149                 par.content_type = mime
150                 cr.commit()
151                 cr.close()
152             except Exception:
153                 logging.getLogger('document.storage').warning('Cannot save file indexed content:', exc_info=True)
154
155         elif self.mode in ('a', 'a+' ):
156             try:
157                 par = self._get_parent()
158                 cr = pooler.get_db(par.context.dbname).cursor()
159                 fsize = os.stat(fname).st_size
160                 cr.execute("UPDATE ir_attachment SET file_size = %s " \
161                             "  WHERE id = %s",
162                             (fsize, par.file_id))
163                 par.content_length = fsize
164                 cr.commit()
165                 cr.close()
166             except Exception:
167                 logging.getLogger('document.storage').warning('Cannot save file appended content:', exc_info=True)
168
169
170
171 class nodefd_db(StringIO, nodes.node_descriptor):
172     """ A descriptor to db data
173     """
174     def __init__(self, parent, ira_browse, mode):
175         nodes.node_descriptor.__init__(self, parent)
176         self._size = 0L
177         if mode.endswith('b'):
178             mode = mode[:-1]
179         
180         if mode in ('r', 'r+'):
181             cr = ira_browse._cr # reuse the cursor of the browse object, just now
182             cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s',(ira_browse.id,))
183             data = cr.fetchone()[0]
184             if data:
185                 self._size = len(data)
186             StringIO.__init__(self, data)
187         elif mode in ('w', 'w+'):
188             StringIO.__init__(self, None)
189             # at write, we start at 0 (= overwrite), but have the original
190             # data available, in case of a seek()
191         elif mode == 'a':
192             StringIO.__init__(self, None)
193         else:
194             logging.getLogger('document.storage').error("Incorrect mode %s specified", mode)
195             raise IOError(errno.EINVAL, "Invalid file mode")
196         self.mode = mode
197
198     def size(self):
199         return self._size
200
201     def close(self):
202         # we now open a *separate* cursor, to update the data.
203         # FIXME: this may be improved, for concurrency handling
204         par = self._get_parent()
205         # uid = par.context.uid
206         cr = pooler.get_db(par.context.dbname).cursor()
207         try:
208             if self.mode in ('w', 'w+', 'r+'):
209                 data = self.getvalue()
210                 icont = ''
211                 mime = ''
212                 filename = par.path
213                 if isinstance(filename, (tuple, list)):
214                     filename = '/'.join(filename)
215             
216                 try:
217                     mime, icont = cntIndex.doIndex(data, filename=filename,
218                             content_type=None, realfname=None)
219                 except Exception:
220                     logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
221                     pass
222
223                 try:
224                     icont_u = ustr(icont)
225                 except UnicodeError:
226                     icont_u = ''
227
228                 out = psycopg2.Binary(data)
229                 cr.execute("UPDATE ir_attachment " \
230                             "SET db_datas = %s, file_size=%s, " \
231                             " index_content= %s, file_type=%s " \
232                             " WHERE id = %s",
233                     (out, len(data), icont_u, mime, par.file_id))
234             elif self.mode == 'a':
235                 data = self.getvalue()
236                 out = psycopg2.Binary(data)
237                 cr.execute("UPDATE ir_attachment " \
238                     "SET db_datas = COALESCE(db_datas,'') || %s, " \
239                     "    file_size = COALESCE(file_size, 0) + %s " \
240                     " WHERE id = %s",
241                     (out, len(data), par.file_id))
242             cr.commit()
243         except Exception:
244             logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id)
245             raise
246         finally:
247             cr.close()
248         StringIO.close(self)
249
250 class nodefd_db64(StringIO, nodes.node_descriptor):
251     """ A descriptor to db data, base64 (the old way)
252     
253         It stores the data in base64 encoding at the db. Not optimal, but
254         the transparent compression of Postgres will save the day.
255     """
256     def __init__(self, parent, ira_browse, mode):
257         nodes.node_descriptor.__init__(self, parent)
258         self._size = 0L
259         if mode.endswith('b'):
260             mode = mode[:-1]
261         
262         if mode in ('r', 'r+'):
263             data = base64.decodestring(ira_browse.db_datas)
264             if data:
265                 self._size = len(data)
266             StringIO.__init__(self, data)
267         elif mode in ('w', 'w+'):
268             StringIO.__init__(self, None)
269             # at write, we start at 0 (= overwrite), but have the original
270             # data available, in case of a seek()
271         elif mode == 'a':
272             StringIO.__init__(self, None)
273         else:
274             logging.getLogger('document.storage').error("Incorrect mode %s specified", mode)
275             raise IOError(errno.EINVAL, "Invalid file mode")
276         self.mode = mode
277
278     def size(self):
279         return self._size
280
281     def close(self):
282         # we now open a *separate* cursor, to update the data.
283         # FIXME: this may be improved, for concurrency handling
284         par = self._get_parent()
285         # uid = par.context.uid
286         cr = pooler.get_db(par.context.dbname).cursor()
287         try:
288             if self.mode in ('w', 'w+', 'r+'):
289                 data = self.getvalue()
290                 icont = ''
291                 mime = ''
292                 filename = par.path
293                 if isinstance(filename, (tuple, list)):
294                     filename = '/'.join(filename)
295             
296                 try:
297                     mime, icont = cntIndex.doIndex(data, filename=filename,
298                             content_type=None, realfname=None)
299                 except Exception:
300                     logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
301                     pass
302
303                 try:
304                     icont_u = ustr(icont)
305                 except UnicodeError:
306                     icont_u = ''
307
308                 cr.execute('UPDATE ir_attachment SET db_datas = %s::bytea, file_size=%s, ' \
309                         'index_content = %s, file_type = %s ' \
310                         'WHERE id = %s',
311                         (base64.encodestring(data), len(data), icont_u, mime, par.file_id))
312             elif self.mode == 'a':
313                 data = self.getvalue()
314                 # Yes, we're obviously using the wrong representation for storing our
315                 # data as base64-in-bytea
316                 cr.execute("UPDATE ir_attachment " \
317                     "SET db_datas = encode( (COALESCE(decode(encode(db_datas,'escape'),'base64'),'') || decode(%s, 'base64')),'base64')::bytea , " \
318                     "    file_size = COALESCE(file_size, 0) + %s " \
319                     " WHERE id = %s",
320                     (base64.encodestring(data), len(data), par.file_id))
321             cr.commit()
322         except Exception:
323             logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id)
324             raise
325         finally:
326             cr.close()
327         StringIO.close(self)
328
329 class document_storage(osv.osv):
330     """ The primary object for data storage.
331     Each instance of this object is a storage media, in which our application
332     can store contents. The object here controls the behaviour of the storage
333     media.
334     The referring document.directory-ies will control the placement of data
335     into the storage.
336     
337     It is a bad idea to have multiple document.storage objects pointing to
338     the same tree of filesystem storage.
339     """
340     _name = 'document.storage'
341     _description = 'Storage Media'
342     _doclog = logging.getLogger('document')
343
344     _columns = {
345         'name': fields.char('Name', size=64, required=True, select=1),
346         'write_date': fields.datetime('Date Modified', readonly=True),
347         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
348         'create_date': fields.datetime('Date Created', readonly=True),
349         'create_uid':  fields.many2one('res.users', 'Creator', readonly=True),
350         'user_id': fields.many2one('res.users', 'Owner'),
351         'group_ids': fields.many2many('res.groups', 'document_storage_group_rel', 'item_id', 'group_id', 'Groups'),
352         'dir_ids': fields.one2many('document.directory', 'parent_id', 'Directories'),
353         'type': fields.selection([('db', 'Database'), ('filestore', 'Internal File storage'),
354                 ('realstore','External file storage'),], 'Type', required=True),
355         'path': fields.char('Path', size=250, select=1, help="For file storage, the root path of the storage"),
356         'online': fields.boolean('Online', help="If not checked, media is currently offline and its contents not available", required=True),
357         'readonly': fields.boolean('Read Only', help="If set, media is for reading only"),
358     }
359
360     def _get_rootpath(self, cr, uid, context=None):
361         return os.path.join(DMS_ROOT_PATH, cr.dbname)
362
363     _defaults = {
364         'user_id': lambda self, cr, uid, ctx: uid,
365         'online': lambda *args: True,
366         'readonly': lambda *args: False,
367         # Note: the defaults below should only be used ONCE for the default
368         # storage media. All other times, we should create different paths at least.
369         'type': lambda *args: 'filestore',
370         'path': _get_rootpath,
371     }
372     _sql_constraints = [
373         # SQL note: a path = NULL doesn't have to be unique.
374         ('path_uniq', 'UNIQUE(type,path)', "The storage path must be unique!")
375         ]
376
377     def __get_random_fname(self, path):
378         flag = None
379         # This can be improved
380         if os.path.isdir(path):
381             for dirs in os.listdir(path):
382                 if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000:
383                     flag = dirs
384                     break
385         flag = flag or create_directory(path)
386         filename = random_name()
387         return os.path.join(flag, filename)
388
389     def __prepare_realpath(self, cr, file_node, ira, store_path, do_create=True):
390         """ Cleanup path for realstore, create dirs if needed
391         
392             @param file_node  the node
393             @param ira    ir.attachment browse of the file_node
394             @param store_path the path of the parent storage object, list
395             @param do_create  create the directories, if needed
396             
397             @return tuple(path "/var/filestore/real/dir/", npath ['dir','fname.ext'] )
398         """
399         file_node.fix_ppath(cr, ira)
400         npath = file_node.full_path() or []
401         # npath may contain empty elements, for root directory etc.
402         npath = filter(lambda x: x is not None, npath)
403
404         # if self._debug:
405         #     self._doclog.debug('Npath: %s', npath)
406         for n in npath:
407             if n == '..':
408                 raise ValueError("Invalid '..' element in path")
409             for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?',):
410                 if ch in n:
411                     raise ValueError("Invalid char %s in path %s" %(ch, n))
412         dpath = [store_path,]
413         dpath += npath[:-1]
414         path = os.path.join(*dpath)
415         if not os.path.isdir(path):
416             self._doclog.debug("Create dirs: %s", path)
417             os.makedirs(path)
418         return path, npath
419
420     def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
421         """ retrieve the contents of some file_node having storage_id = id
422             optionally, fil_obj could point to the browse object of the file
423             (ir.attachment)
424         """
425         if not context:
426             context = {}
427         boo = self.browse(cr, uid, id, context)
428         if not boo.online:
429             raise IOError(errno.EREMOTE, 'medium offline')
430         
431         if fil_obj:
432             ira = fil_obj
433         else:
434             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
435         return self.__get_data_3(cr, uid, boo, ira, context)
436
437     def get_file(self, cr, uid, id, file_node, mode, context=None):
438         """ Return a file-like object for the contents of some node
439         """
440         if context is None:
441             context = {}
442         boo = self.browse(cr, uid, id, context)
443         if not boo.online:
444             raise IOError(errno.EREMOTE, 'medium offline')
445         
446         if boo.readonly and mode not in ('r', 'rb'):
447             raise IOError(errno.EPERM, "Readonly medium")
448         
449         ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
450         if boo.type == 'filestore':
451             if not ira.store_fname:
452                 # On a migrated db, some files may have the wrong storage type
453                 # try to fix their directory.
454                 if mode in ('r','r+'):
455                     if ira.file_size:
456                         self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
457                     raise IOError(errno.ENOENT, 'No file can be located')
458                 else:
459                     store_fname = self.__get_random_fname(boo.path)
460                     cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
461                                 (store_fname, ira.id))
462                     fpath = os.path.join(boo.path, store_fname)
463             else:
464                 fpath = os.path.join(boo.path, ira.store_fname)
465             return nodefd_file(file_node, path=fpath, mode=mode)
466
467         elif boo.type == 'db':
468             # TODO: we need a better api for large files
469             return nodefd_db(file_node, ira_browse=ira, mode=mode)
470
471         elif boo.type == 'db64':
472             return nodefd_db64(file_node, ira_browse=ira, mode=mode)
473
474         elif boo.type == 'realstore':
475             path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path,
476                             do_create = (mode[0] in ('w','a'))  )
477             fpath = os.path.join(path, npath[-1])
478             if (not os.path.exists(fpath)) and mode[0] == 'r':
479                 raise IOError("File not found: %s" % fpath)
480             elif mode[0] in ('w', 'a') and not ira.store_fname:
481                 store_fname = os.path.join(*npath)
482                 cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
483                                 (store_fname, ira.id))
484             return nodefd_file(file_node, path=fpath, mode=mode)
485
486         elif boo.type == 'virtual':
487             raise ValueError('Virtual storage does not support static files')
488         
489         else:
490             raise TypeError("No %s storage" % boo.type)
491
492     def __get_data_3(self, cr, uid, boo, ira, context):
493         if boo.type == 'filestore':
494             if not ira.store_fname:
495                 # On a migrated db, some files may have the wrong storage type
496                 # try to fix their directory.
497                 if ira.file_size:
498                     self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
499                 return None
500             fpath = os.path.join(boo.path, ira.store_fname)
501             return file(fpath, 'rb').read()
502         elif boo.type == 'db64':
503             # TODO: we need a better api for large files
504             if ira.db_datas:
505                 out = base64.decodestring(ira.db_datas)
506             else:
507                 out = ''
508             return out
509         elif boo.type == 'db':
510             # We do an explicit query, to avoid type transformations.
511             cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s', (ira.id,))
512             res = cr.fetchone()
513             if res:
514                 return res[0]
515             else:
516                 return ''
517         elif boo.type == 'realstore':
518             if not ira.store_fname:
519                 # On a migrated db, some files may have the wrong storage type
520                 # try to fix their directory.
521                 if ira.file_size:
522                     self._doclog.warning("ir.attachment #%d does not have a filename, trying the name." %ira.id)
523                 # sfname = ira.name
524             fpath = os.path.join(boo.path,ira.store_fname or ira.name)
525             if os.path.exists(fpath):
526                 return file(fpath,'rb').read()
527             elif not ira.store_fname:
528                 return None
529             else:
530                 raise IOError(errno.ENOENT, "File not found: %s" % fpath)
531
532         elif boo.type == 'virtual':
533             raise ValueError('Virtual storage does not support static files')
534
535         else:
536             raise TypeError("No %s storage" % boo.type)
537
538     def set_data(self, cr, uid, id, file_node, data, context=None, fil_obj=None):
539         """ store the data.
540             This function MUST be used from an ir.attachment. It wouldn't make sense
541             to store things persistently for other types (dynamic).
542         """
543         if not context:
544             context = {}
545         boo = self.browse(cr, uid, id, context)
546         if fil_obj:
547             ira = fil_obj
548         else:
549             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
550
551         if not boo.online:
552             raise IOError(errno.EREMOTE, 'medium offline')
553         
554         if boo.readonly:
555             raise IOError(errno.EPERM, "Readonly medium")
556
557         self._doclog.debug( "Store data for ir.attachment #%d" % ira.id)
558         store_fname = None
559         fname = None
560         if boo.type == 'filestore':
561             path = boo.path
562             try:
563                 store_fname = self.__get_random_fname(path)
564                 fname = os.path.join(path, store_fname)
565                 fp = file(fname, 'wb')
566                 fp.write(data)
567                 fp.close()
568                 self._doclog.debug( "Saved data to %s" % fname)
569                 filesize = len(data) # os.stat(fname).st_size
570                 
571                 # TODO Here, an old file would be left hanging.
572
573             except Exception, e:
574                 self._doclog.warning( "Couldn't save data to %s", path, exc_info=True)
575                 raise except_orm(_('Error!'), str(e))
576         elif boo.type == 'db':
577             filesize = len(data)
578             # will that work for huge data?
579             out = psycopg2.Binary(data)
580             cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
581                 (out, file_node.file_id))
582         elif boo.type == 'db64':
583             filesize = len(data)
584             # will that work for huge data?
585             out = base64.encodestring(data)
586             cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
587                 (out, file_node.file_id))
588         elif boo.type == 'realstore':
589             try:
590                 path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path, do_create=True)
591                 fname = os.path.join(path, npath[-1])
592                 fp = file(fname,'wb')
593                 fp.write(data)
594                 fp.close()
595                 self._doclog.debug("Saved data to %s", fname)
596                 filesize = len(data) # os.stat(fname).st_size
597                 store_fname = os.path.join(*npath)
598                 # TODO Here, an old file would be left hanging.
599             except Exception,e :
600                 self._doclog.warning("Couldn't save data:", exc_info=True)
601                 raise except_orm(_('Error!'), str(e))
602
603         elif boo.type == 'virtual':
604             raise ValueError('Virtual storage does not support static files')
605
606         else:
607             raise TypeError("No %s storage" % boo.type)
608
609         # 2nd phase: store the metadata
610         try:
611             icont = ''
612             mime = ira.file_type
613             if not mime:
614                 mime = ""
615             try:
616                 mime, icont = cntIndex.doIndex(data, ira.datas_fname,
617                 ira.file_type or None, fname)
618             except Exception:
619                 self._doclog.debug('Cannot index file:', exc_info=True)
620                 pass
621
622             try:
623                 icont_u = ustr(icont)
624             except UnicodeError:
625                 icont_u = ''
626
627             # a hack: /assume/ that the calling write operation will not try
628             # to write the fname and size, and update them in the db concurrently.
629             # We cannot use a write() here, because we are already in one.
630             cr.execute('UPDATE ir_attachment SET store_fname = %s, file_size = %s, index_content = %s, file_type = %s WHERE id = %s',
631                 (store_fname, filesize, icont_u, mime, file_node.file_id))
632             file_node.content_length = filesize
633             file_node.content_type = mime
634             return True
635         except Exception, e :
636             self._doclog.warning("Couldn't save data:", exc_info=True)
637             # should we really rollback once we have written the actual data?
638             # at the db case (only), that rollback would be safe
639             raise except_orm(_('Error at doc write!'), str(e))
640
641     def prepare_unlink(self, cr, uid, storage_bo, fil_bo):
642         """ Before we unlink a file (fil_boo), prepare the list of real
643         files that have to be removed, too. """
644
645         if not storage_bo.online:
646             raise IOError(errno.EREMOTE, 'medium offline')
647         
648         if storage_bo.readonly:
649             raise IOError(errno.EPERM, "Readonly medium")
650
651         if storage_bo.type == 'filestore':
652             fname = fil_bo.store_fname
653             if not fname:
654                 return None
655             path = storage_bo.path
656             return (storage_bo.id, 'file', os.path.join(path, fname))
657         elif storage_bo.type in ('db', 'db64'):
658             return None
659         elif storage_bo.type == 'realstore':
660             fname = fil_bo.store_fname
661             if not fname:
662                 return None
663             path = storage_bo.path
664             return ( storage_bo.id, 'file', os.path.join(path, fname))
665         else:
666             raise TypeError("No %s storage" % storage_bo.type)
667
668     def do_unlink(self, cr, uid, unres):
669         for id, ktype, fname in unres:
670             if ktype == 'file':
671                 try:
672                     os.unlink(fname)
673                 except Exception:
674                     self._doclog.warning("Could not remove file %s, please remove manually.", fname, exc_info=True)
675             else:
676                 self._doclog.warning("Unknown unlink key %s" % ktype)
677
678         return True
679
680     def simple_rename(self, cr, uid, file_node, new_name, context=None):
681         """ A preparation for a file rename.
682             It will not affect the database, but merely check and perhaps
683             rename the realstore file.
684             
685             @return the dict of values that can safely be be stored in the db.
686         """
687         sbro = self.browse(cr, uid, file_node.storage_id, context=context)
688         assert sbro, "The file #%d didn't provide storage" % file_node.file_id
689
690         if not sbro.online:
691             raise IOError(errno.EREMOTE, 'medium offline')
692         
693         if sbro.readonly:
694             raise IOError(errno.EPERM, "Readonly medium")
695
696         if sbro.type in ('filestore', 'db', 'db64'):
697             # nothing to do for a rename, allow to change the db field
698             return { 'name': new_name, 'datas_fname': new_name }
699         elif sbro.type == 'realstore':
700             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
701
702             path, npath = self.__prepare_realpath(cr, file_node, ira, sbro.path, do_create=False)
703             fname = ira.store_fname
704
705             if not fname:
706                 self._doclog.warning("Trying to rename a non-stored file")
707             if fname != os.path.join(*npath):
708                 self._doclog.warning("inconsistency in realstore: %s != %s" , fname, repr(npath))
709
710             oldpath = os.path.join(path, npath[-1])
711             newpath = os.path.join(path, new_name)
712             os.rename(oldpath, newpath)
713             store_path = npath[:-1]
714             store_path.append(new_name)
715             store_fname = os.path.join(*store_path)
716             return { 'name': new_name, 'datas_fname': new_name, 'store_fname': store_fname }
717         else:
718             raise TypeError("No %s storage" % sbro.type)
719
720     def simple_move(self, cr, uid, file_node, ndir_bro, context=None):
721         """ A preparation for a file move.
722             It will not affect the database, but merely check and perhaps
723             move the realstore file.
724             
725             @param ndir_bro a browse object of document.directory, where this
726                     file should move to.
727             @return the dict of values that can safely be be stored in the db.
728         """
729         sbro = self.browse(cr, uid, file_node.storage_id, context=context)
730         assert sbro, "The file #%d didn't provide storage" % file_node.file_id
731
732         if not sbro.online:
733             raise IOError(errno.EREMOTE, 'medium offline')
734         
735         if sbro.readonly:
736             raise IOError(errno.EPERM, "Readonly medium")
737
738         par = ndir_bro
739         psto = None
740         while par:
741             if par.storage_id:
742                 psto = par.storage_id.id
743                 break
744             par = par.parent_id
745         if file_node.storage_id != psto:
746             self._doclog.debug('Cannot move file %r from %r to %r', file_node, file_node.parent, ndir_bro.name)
747             raise NotImplementedError('Cannot move files between storage media')
748
749         if sbro.type in ('filestore', 'db', 'db64'):
750             # nothing to do for a rename, allow to change the db field
751             return { 'parent_id': ndir_bro.id }
752         elif sbro.type == 'realstore':
753             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
754
755             path, opath = self.__prepare_realpath(cr, file_node, ira, sbro.path, do_create=False)
756             fname = ira.store_fname
757
758             if not fname:
759                 self._doclog.warning("Trying to rename a non-stored file")
760             if fname != os.path.join(*opath):
761                 self._doclog.warning("inconsistency in realstore: %s != %s" , fname, repr(opath))
762
763             oldpath = os.path.join(path, opath[-1])
764             
765             npath = [sbro.path,] + (ndir_bro.get_full_path() or [])
766             npath = filter(lambda x: x is not None, npath)
767             newdir = os.path.join(*npath)
768             if not os.path.isdir(newdir):
769                 self._doclog.debug("Must create dir %s", newdir)
770                 os.makedirs(newdir)
771             npath.append(opath[-1])
772             newpath = os.path.join(*npath)
773             
774             self._doclog.debug("Going to move %s from %s to %s", opath[-1], oldpath, newpath)
775             shutil.move(oldpath, newpath)
776             
777             store_path = npath[1:] + [opath[-1],]
778             store_fname = os.path.join(*store_path)
779             
780             return { 'store_fname': store_fname }
781         else:
782             raise TypeError("No %s storage" % sbro.type)
783
784
785 document_storage()
786
787
788 #eof