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