Document: remove debug calls from trunk code.
[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
31 from tools.misc import ustr
32 from tools.translate import _
33
34 from osv.orm import except_orm
35
36 import random
37 import string
38 import pooler
39 import netsvc
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         
101         for attr in ('closed', 'read', 'write', 'seek', 'tell'):
102             setattr(self,attr, getattr(self.__file, attr))
103
104     def close(self):
105         # TODO: locking in init, close()
106         self.__file.close()
107
108     
109 class nodefd_db(StringIO, nodes.node_descriptor):
110     """ A descriptor to db data
111     """
112     def __init__(self, parent, ira_browse, mode):
113         nodes.node_descriptor.__init__(self, parent)
114         if mode.endswith('b'):
115             mode = mode[:-1]
116         
117         if mode in ('r', 'r+'):
118             cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s', ira_browse.id)
119             data = cr.fetchone()[0]
120             StringIO.__init__(self, data)
121         elif mode in ('w', 'w+'):
122             StringIO.__init__(self, None)
123             # at write, we start at 0 (= overwrite), but have the original
124             # data available, in case of a seek()
125         elif mode == 'a':
126             StringIO.__init__(self, None)
127         else:
128             logging.getLogger('document.storage').error("Incorrect mode %s specified", mode)
129             raise IOError(errno.EINVAL, "Invalid file mode")
130         self.mode = mode
131
132     def close(self):
133         # we now open a *separate* cursor, to update the data.
134         # FIXME: this may be improved, for concurrency handling
135         par = self._get_parent()
136         uid = par.context.uid
137         cr = pooler.get_db(par.context.dbname).cursor()
138         try:
139             if self.mode in ('w', 'w+', 'r+'):
140                 out = self.getvalue()
141                 cr.execute("UPDATE ir_attachment SET db_datas = decode(%s,'escape'), file_size=%s WHERE id = %s",
142                     (out, len(out), par.file_id))
143             elif self.mode == 'a':
144                 out = self.getvalue()
145                 cr.execute("UPDATE ir_attachment " \
146                     "SET db_datas = COALESCE(db_datas,'') || decode(%s, 'escape'), " \
147                     "    file_size = COALESCE(file_size, 0) + %s " \
148                     " WHERE id = %s",
149                     (out, len(out), par.file_id))
150             cr.commit()
151         except Exception, e:
152             logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id)
153             raise
154         finally:
155             cr.close()
156         StringIO.close(self)
157
158 class nodefd_db64(StringIO, nodes.node_descriptor):
159     """ A descriptor to db data, base64 (the old way)
160     
161         It stores the data in base64 encoding at the db. Not optimal, but
162         the transparent compression of Postgres will save the day.
163     """
164     def __init__(self, parent, ira_browse, mode):
165         nodes.node_descriptor.__init__(self, parent)
166         if mode.endswith('b'):
167             mode = mode[:-1]
168         
169         if mode in ('r', 'r+'):
170             StringIO.__init__(self, base64.decodestring(ira_browse.db_datas))
171         elif mode in ('w', 'w+'):
172             StringIO.__init__(self, None)
173             # at write, we start at 0 (= overwrite), but have the original
174             # data available, in case of a seek()
175         elif mode == 'a':
176             StringIO.__init__(self, None)
177         else:
178             logging.getLogger('document.storage').error("Incorrect mode %s specified", mode)
179             raise IOError(errno.EINVAL, "Invalid file mode")
180         self.mode = mode
181
182     def close(self):
183         # we now open a *separate* cursor, to update the data.
184         # FIXME: this may be improved, for concurrency handling
185         par = self._get_parent()
186         uid = par.context.uid
187         cr = pooler.get_db(par.context.dbname).cursor()
188         try:
189             if self.mode in ('w', 'w+', 'r+'):
190                 out = self.getvalue()
191                 cr.execute('UPDATE ir_attachment SET db_datas = %s::bytea, file_size=%s WHERE id = %s',
192                     (base64.encodestring(out), len(out), par.file_id))
193             elif self.mode == 'a':
194                 out = self.getvalue()
195                 # Yes, we're obviously using the wrong representation for storing our
196                 # data as base64-in-bytea
197                 cr.execute("UPDATE ir_attachment " \
198                     "SET db_datas = encode( (COALESCE(decode(encode(db_datas,'escape'),'base64'),'') || decode(%s, 'base64')),'base64')::bytea , " \
199                     "    file_size = COALESCE(file_size, 0) + %s " \
200                     " WHERE id = %s",
201                     (base64.encodestring(out), len(out), par.file_id))
202             cr.commit()
203         except Exception, e:
204             logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id)
205             raise
206         finally:
207             cr.close()
208         StringIO.close(self)
209
210 class document_storage(osv.osv):
211     """ The primary object for data storage.
212     Each instance of this object is a storage media, in which our application
213     can store contents. The object here controls the behaviour of the storage
214     media.
215     The referring document.directory-ies will control the placement of data
216     into the storage.
217     
218     It is a bad idea to have multiple document.storage objects pointing to
219     the same tree of filesystem storage.
220     """
221     _name = 'document.storage'
222     _description = 'Storage Media'
223     _doclog = logging.getLogger('document')
224
225     _columns = {
226         'name': fields.char('Name', size=64, required=True, select=1),
227         'write_date': fields.datetime('Date Modified', readonly=True),
228         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
229         'create_date': fields.datetime('Date Created', readonly=True),
230         'create_uid':  fields.many2one('res.users', 'Creator', readonly=True),
231         'user_id': fields.many2one('res.users', 'Owner'),
232         'group_ids': fields.many2many('res.groups', 'document_storage_group_rel', 'item_id', 'group_id', 'Groups'),
233         'dir_ids': fields.one2many('document.directory', 'parent_id', 'Directories'),
234         'type': fields.selection([('db', 'Database'), ('filestore', 'Internal File storage'),
235             ('realstore', 'External file storage'), ('virtual', 'Virtual storage')], 'Type', required=True),
236         'path': fields.char('Path', size=250, select=1, help="For file storage, the root path of the storage"),
237         'online': fields.boolean('Online', help="If not checked, media is currently offline and its contents not available", required=True),
238         'readonly': fields.boolean('Read Only', help="If set, media is for reading only"),
239     }
240
241     def _get_rootpath(self, cr, uid, context=None):
242         return os.path.join(DMS_ROOT_PATH, cr.dbname)
243
244     _defaults = {
245         'user_id': lambda self, cr, uid, ctx: uid,
246         'online': lambda *args: True,
247         'readonly': lambda *args: False,
248         # Note: the defaults below should only be used ONCE for the default
249         # storage media. All other times, we should create different paths at least.
250         'type': lambda *args: 'filestore',
251         'path': _get_rootpath,
252     }
253     _sql_constraints = [
254         # SQL note: a path = NULL doesn't have to be unique.
255         ('path_uniq', 'UNIQUE(type,path)', "The storage path must be unique!")
256         ]
257
258     def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
259         """ retrieve the contents of some file_node having storage_id = id
260             optionally, fil_obj could point to the browse object of the file
261             (ir.attachment)
262         """
263         if not context:
264             context = {}
265         boo = self.browse(cr, uid, id, context)
266         if fil_obj:
267             ira = fil_obj
268         else:
269             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
270         return self.__get_data_3(cr, uid, boo, ira, context)
271
272     def get_file(self, cr, uid, id, file_node, mode, context=None):
273         if context is None:
274             context = {}
275         boo = self.browse(cr, uid, id, context)
276         if not boo.online:
277             raise RuntimeError('media offline')
278         
279         ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
280         if boo.type == 'filestore':
281             if not ira.store_fname:
282                 # On a migrated db, some files may have the wrong storage type
283                 # try to fix their directory.
284                 if ira.file_size:
285                     self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
286                 raise IOError(errno.ENOENT, 'No file can be located')
287             fpath = os.path.join(boo.path, ira.store_fname)
288             return nodefd_file(file_node, path=fpath, mode=mode)
289
290         elif boo.type == 'db':
291             # TODO: we need a better api for large files
292             return nodefd_db64(file_node, ira_browse=ira, mode=mode)
293
294         elif boo.type == 'realstore':
295             if not ira.store_fname:
296                 # On a migrated db, some files may have the wrong storage type
297                 # try to fix their directory.
298                 if ira.file_size:
299                     self._doclog.warning("ir.attachment #%d does not have a filename, trying the name." %ira.id)
300                 sfname = ira.name
301             fpath = os.path.join(boo.path,ira.store_fname or ira.name)
302             if not os.path.exists(fpath):
303                 raise IOError("File not found: %s" % fpath)
304             return nodefd_file(file_node, path=fpath, mode=mode)
305
306         else:
307             raise TypeError("No %s storage" % boo.type)
308
309     def __get_data_3(self, cr, uid, boo, ira, context):
310         if not boo.online:
311             raise RuntimeError('media offline')
312         if boo.type == 'filestore':
313             if not ira.store_fname:
314                 # On a migrated db, some files may have the wrong storage type
315                 # try to fix their directory.
316                 if ira.file_size:
317                     self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
318                 return None
319             fpath = os.path.join(boo.path, ira.store_fname)
320             return file(fpath, 'rb').read()
321         elif boo.type == 'db':
322             # TODO: we need a better api for large files
323             if ira.db_datas:
324                 out = base64.decodestring(ira.db_datas)
325             else:
326                 out = ''
327             return out
328         elif boo.type == 'realstore':
329             if not ira.store_fname:
330                 # On a migrated db, some files may have the wrong storage type
331                 # try to fix their directory.
332                 if ira.file_size:
333                     self._doclog.warning("ir.attachment #%d does not have a filename, trying the name." %ira.id)
334                 sfname = ira.name
335             fpath = os.path.join(boo.path,ira.store_fname or ira.name)
336             if os.path.exists(fpath):
337                 return file(fpath,'rb').read()
338             elif not ira.store_fname:
339                 return None
340             else:
341                 raise IOError("File not found: %s" % fpath)
342         else:
343             raise TypeError("No %s storage" % boo.type)
344
345     def set_data(self, cr, uid, id, file_node, data, context=None, fil_obj=None):
346         """ store the data.
347             This function MUST be used from an ir.attachment. It wouldn't make sense
348             to store things persistently for other types (dynamic).
349         """
350         if not context:
351             context = {}
352         boo = self.browse(cr, uid, id, context)
353         if fil_obj:
354             ira = fil_obj
355         else:
356             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
357
358         if not boo.online:
359             raise RuntimeError('media offline')
360         self._doclog.debug( "Store data for ir.attachment #%d" % ira.id)
361         store_fname = None
362         fname = None
363         if boo.type == 'filestore':
364             path = boo.path
365             try:
366                 flag = None
367                 # This can be improved  
368                 if os.path.isdir(path):
369                     for dirs in os.listdir(path):
370                         if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000:
371                             flag = dirs
372                             break
373                 flag = flag or create_directory(path)
374                 filename = random_name()
375                 fname = os.path.join(path, flag, filename)
376                 fp = file(fname, 'wb')
377                 fp.write(data)
378                 fp.close()
379                 self._doclog.debug( "Saved data to %s" % fname)
380                 filesize = len(data) # os.stat(fname).st_size
381                 store_fname = os.path.join(flag, filename)
382
383                 # TODO Here, an old file would be left hanging.
384
385             except Exception, e:
386                 self._doclog.warning( "Couldn't save data to %s", path, exc_info=True)
387                 raise except_orm(_('Error!'), str(e))
388         elif boo.type == 'db':
389             filesize = len(data)
390             # will that work for huge data? TODO
391             out = base64.encodestring(data)
392             cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
393                 (out, file_node.file_id))
394         elif boo.type == 'realstore':
395             try:
396                 file_node.fix_ppath(cr, ira)
397                 npath = file_node.full_path() or []
398                 # npath may contain empty elements, for root directory etc.
399                 for i, n in enumerate(npath):
400                     if n == None:
401                         del npath[i]
402                 for n in npath:
403                     for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'):
404                         if ch in n:
405                             raise ValueError("Invalid char %s in path %s" %(ch, n))
406                 dpath = [boo.path,]
407                 dpath += npath[:-1]
408                 path = os.path.join(*dpath)
409                 if not os.path.isdir(path):
410                     os.makedirs(path)
411                 fname = os.path.join(path, npath[-1])
412                 fp = file(fname,'wb')
413                 fp.write(data)
414                 fp.close()
415                 self._doclog.debug("Saved data to %s", fname)
416                 filesize = len(data) # os.stat(fname).st_size
417                 store_fname = os.path.join(*npath)
418                 # TODO Here, an old file would be left hanging.
419             except Exception,e :
420                 self._doclog.warning("Couldn't save data:", exc_info=True)
421                 raise except_orm(_('Error!'), str(e))
422         else:
423             raise TypeError("No %s storage" % boo.type)
424
425         # 2nd phase: store the metadata
426         try:
427             icont = ''
428             mime = ira.file_type
429             if not mime:
430                 mime = ""
431             try:
432                 mime, icont = cntIndex.doIndex(data, ira.datas_fname,
433                 ira.file_type or None, fname)
434             except Exception:
435                 self._doclog.debug('Cannot index file:', exc_info=True)
436                 pass
437
438             try:
439                 icont_u = ustr(icont)
440             except UnicodeError:
441                 icont_u = ''
442
443             # a hack: /assume/ that the calling write operation will not try
444             # to write the fname and size, and update them in the db concurrently.
445             # We cannot use a write() here, because we are already in one.
446             cr.execute('UPDATE ir_attachment SET store_fname = %s, file_size = %s, index_content = %s, file_type = %s WHERE id = %s',
447                 (store_fname, filesize, icont_u, mime, file_node.file_id))
448             file_node.content_length = filesize
449             file_node.content_type = mime
450             return True
451         except Exception, e :
452             self._doclog.warning("Couldn't save data:", exc_info=True)
453             # should we really rollback once we have written the actual data?
454             # at the db case (only), that rollback would be safe
455             raise except_orm(_('Error at doc write!'), str(e))
456
457     def prepare_unlink(self, cr, uid, storage_bo, fil_bo):
458         """ Before we unlink a file (fil_boo), prepare the list of real
459         files that have to be removed, too. """
460
461         if not storage_bo.online:
462             raise RuntimeError('media offline')
463
464         if storage_bo.type == 'filestore':
465             fname = fil_bo.store_fname
466             if not fname:
467                 return None
468             path = storage_bo.path
469             return (storage_bo.id, 'file', os.path.join(path, fname))
470         elif storage_bo.type == 'db':
471             return None
472         elif storage_bo.type == 'realstore':
473             fname = fil_bo.store_fname
474             if not fname:
475                 return None
476             path = storage_bo.path
477             return ( storage_bo.id, 'file', os.path.join(path, fname))
478         else:
479             raise TypeError("No %s storage" % storage_bo.type)
480
481     def do_unlink(self, cr, uid, unres):
482         for id, ktype, fname in unres:
483             if ktype == 'file':
484                 try:
485                     os.unlink(fname)
486                 except Exception, e:
487                     self._doclog.warning("Could not remove file %s, please remove manually.", fname, exc_info=True)
488             else:
489                 self._doclog.warning("Unknown unlink key %s" % ktype)
490
491         return True
492
493     def simple_rename(self, cr, uid, file_node, new_name, context=None):
494         """ A preparation for a file rename.
495             It will not affect the database, but merely check and perhaps
496             rename the realstore file.
497             
498             @return the dict of values that can safely be be stored in the db.
499         """
500         sbro = self.browse(cr, uid, file_node.storage_id, context=context)
501         assert sbro, "The file #%d didn't provide storage" % file_node.file_id
502         
503         if sbro.type in ('filestore', 'db'):
504             # nothing to do for a rename, allow to change the db field
505             return { 'name': new_name, 'datas_fname': new_name }
506         elif sbro.type == 'realstore':
507             fname = fil_bo.store_fname
508             if not fname:
509                 return ValueError("Tried to rename a non-stored file")
510             path = storage_bo.path
511             oldpath = os.path.join(path, fname)
512             
513             for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'):
514                 if ch in new_name:
515                     raise ValueError("Invalid char %s in name %s" %(ch, new_name))
516                 
517             file_node.fix_ppath(cr, ira)
518             npath = file_node.full_path() or []
519             dpath = [path,]
520             dpath.extend(npath[:-1])
521             dpath.append(new_name)
522             newpath = os.path.join(*dpath)
523             # print "old, new paths:", oldpath, newpath
524             os.rename(oldpath, newpath)
525             return { 'name': new_name, 'datas_fname': new_name, 'store_fname': new_name }
526         else:
527             raise TypeError("No %s storage" % boo.type)
528
529
530 document_storage()
531
532
533 #eof