Launchpad automatic translations update.
[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         boo = self.browse(cr, uid, id, context=context)
426         if not boo.online:
427             raise IOError(errno.EREMOTE, 'medium offline')
428         
429         if fil_obj:
430             ira = fil_obj
431         else:
432             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
433         return self.__get_data_3(cr, uid, boo, ira, context)
434
435     def get_file(self, cr, uid, id, file_node, mode, context=None):
436         """ Return a file-like object for the contents of some node
437         """
438         if context is None:
439             context = {}
440         boo = self.browse(cr, uid, id, context=context)
441         if not boo.online:
442             raise IOError(errno.EREMOTE, 'medium offline')
443         
444         if boo.readonly and mode not in ('r', 'rb'):
445             raise IOError(errno.EPERM, "Readonly medium")
446         
447         ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
448         if boo.type == 'filestore':
449             if not ira.store_fname:
450                 # On a migrated db, some files may have the wrong storage type
451                 # try to fix their directory.
452                 if mode in ('r','r+'):
453                     if ira.file_size:
454                         self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
455                     raise IOError(errno.ENOENT, 'No file can be located')
456                 else:
457                     store_fname = self.__get_random_fname(boo.path)
458                     cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
459                                 (store_fname, ira.id))
460                     fpath = os.path.join(boo.path, store_fname)
461             else:
462                 fpath = os.path.join(boo.path, ira.store_fname)
463             return nodefd_file(file_node, path=fpath, mode=mode)
464
465         elif boo.type == 'db':
466             # TODO: we need a better api for large files
467             return nodefd_db(file_node, ira_browse=ira, mode=mode)
468
469         elif boo.type == 'db64':
470             return nodefd_db64(file_node, ira_browse=ira, mode=mode)
471
472         elif boo.type == 'realstore':
473             path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path,
474                             do_create = (mode[0] in ('w','a'))  )
475             fpath = os.path.join(path, npath[-1])
476             if (not os.path.exists(fpath)) and mode[0] == 'r':
477                 raise IOError("File not found: %s" % fpath)
478             elif mode[0] in ('w', 'a') and not ira.store_fname:
479                 store_fname = os.path.join(*npath)
480                 cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
481                                 (store_fname, ira.id))
482             return nodefd_file(file_node, path=fpath, mode=mode)
483
484         elif boo.type == 'virtual':
485             raise ValueError('Virtual storage does not support static files')
486         
487         else:
488             raise TypeError("No %s storage" % boo.type)
489
490     def __get_data_3(self, cr, uid, boo, ira, context):
491         if boo.type == 'filestore':
492             if not ira.store_fname:
493                 # On a migrated db, some files may have the wrong storage type
494                 # try to fix their directory.
495                 if ira.file_size:
496                     self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
497                 return None
498             fpath = os.path.join(boo.path, ira.store_fname)
499             return file(fpath, 'rb').read()
500         elif boo.type == 'db64':
501             # TODO: we need a better api for large files
502             if ira.db_datas:
503                 out = base64.decodestring(ira.db_datas)
504             else:
505                 out = ''
506             return out
507         elif boo.type == 'db':
508             # We do an explicit query, to avoid type transformations.
509             cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s', (ira.id,))
510             res = cr.fetchone()
511             if res:
512                 return res[0]
513             else:
514                 return ''
515         elif boo.type == 'realstore':
516             if not ira.store_fname:
517                 # On a migrated db, some files may have the wrong storage type
518                 # try to fix their directory.
519                 if ira.file_size:
520                     self._doclog.warning("ir.attachment #%d does not have a filename, trying the name." %ira.id)
521                 # sfname = ira.name
522             fpath = os.path.join(boo.path,ira.store_fname or ira.name)
523             if os.path.exists(fpath):
524                 return file(fpath,'rb').read()
525             elif not ira.store_fname:
526                 return None
527             else:
528                 raise IOError(errno.ENOENT, "File not found: %s" % fpath)
529
530         elif boo.type == 'virtual':
531             raise ValueError('Virtual storage does not support static files')
532
533         else:
534             raise TypeError("No %s storage" % boo.type)
535
536     def set_data(self, cr, uid, id, file_node, data, context=None, fil_obj=None):
537         """ store the data.
538             This function MUST be used from an ir.attachment. It wouldn't make sense
539             to store things persistently for other types (dynamic).
540         """
541         boo = self.browse(cr, uid, id, context=context)
542         if fil_obj:
543             ira = fil_obj
544         else:
545             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
546
547         if not boo.online:
548             raise IOError(errno.EREMOTE, 'medium offline')
549         
550         if boo.readonly:
551             raise IOError(errno.EPERM, "Readonly medium")
552
553         self._doclog.debug( "Store data for ir.attachment #%d" % ira.id)
554         store_fname = None
555         fname = None
556         if boo.type == 'filestore':
557             path = boo.path
558             try:
559                 store_fname = self.__get_random_fname(path)
560                 fname = os.path.join(path, store_fname)
561                 fp = open(fname, 'wb')
562                 try:
563                     fp.write(data)
564                 finally:    
565                     fp.close()
566                 self._doclog.debug( "Saved data to %s" % fname)
567                 filesize = len(data) # os.stat(fname).st_size
568                 
569                 # TODO Here, an old file would be left hanging.
570
571             except Exception, e:
572                 self._doclog.warning( "Couldn't save data to %s", path, exc_info=True)
573                 raise except_orm(_('Error!'), str(e))
574         elif boo.type == 'db':
575             filesize = len(data)
576             # will that work for huge data?
577             out = psycopg2.Binary(data)
578             cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
579                 (out, file_node.file_id))
580         elif boo.type == 'db64':
581             filesize = len(data)
582             # will that work for huge data?
583             out = base64.encodestring(data)
584             cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
585                 (out, file_node.file_id))
586         elif boo.type == 'realstore':
587             try:
588                 path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path, do_create=True)
589                 fname = os.path.join(path, npath[-1])
590                 fp = open(fname,'wb')
591                 try:
592                     fp.write(data)
593                 finally:    
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
789
790 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: