[FIX] res_lang : if value of thousands_sep is not present in language,method will...
[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 from tools.misc import ustr
28 from tools.translate import _
29
30 from osv.orm import except_orm
31
32 import random
33 import string
34 import netsvc
35 from content_index import cntIndex
36
37 DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config.get('root_path'), 'filestore'))
38
39
40 """ The algorithm of data storage
41
42 We have to consider 3 cases of data /retrieval/:
43  Given (context,path) we need to access the file (aka. node).
44  given (directory, context), we need one of its children (for listings, views)
45  given (ir.attachment, context), we needs its data and metadata (node).
46
47 For data /storage/ we have the cases:
48  Have (ir.attachment, context), we modify the file (save, update, rename etc).
49  Have (directory, context), we create a file.
50  Have (path, context), we create or modify a file.
51  
52 Note that in all above cases, we don't explicitly choose the storage media,
53 but always require a context to be present.
54
55 Note that a node will not always have a corresponding ir.attachment. Dynamic
56 nodes, for once, won't. Their metadata will be computed by the parent storage
57 media + directory.
58
59 The algorithm says that in any of the above cases, our first goal is to locate
60 the node for any combination of search criteria. It would be wise NOT to 
61 represent each node in the path (like node[/] + node[/dir1] + node[/dir1/dir2])
62 but directly jump to the end node (like node[/dir1/dir2]) whenever possible.
63
64 We also contain all the parenting loop code in one function. This is intentional,
65 because one day this will be optimized in the db (Pg 8.4).
66
67
68 """
69
70 def random_name():
71     random.seed()
72     d = [random.choice(string.ascii_letters) for x in xrange(10) ]
73     name = "".join(d)
74     return name
75
76 INVALID_CHARS = {'*':str(hash('*')), '|':str(hash('|')) , "\\":str(hash("\\")), '/':'__', ':':str(hash(':')), '"':str(hash('"')), '<':str(hash('<')) , '>':str(hash('>')) , '?':str(hash('?'))}
77
78
79 def create_directory(path):
80     dir_name = random_name()
81     path = os.path.join(path, dir_name)
82     os.makedirs(path)
83     return dir_name
84
85
86 class document_storage(osv.osv):
87     """ The primary object for data storage.
88     Each instance of this object is a storage media, in which our application
89     can store contents. The object here controls the behaviour of the storage
90     media.
91     The referring document.directory-ies will control the placement of data
92     into the storage.
93     
94     It is a bad idea to have multiple document.storage objects pointing to
95     the same tree of filesystem storage.
96     """
97     _name = 'document.storage'
98     _description = 'Document storage media'
99     _columns = {
100         'name': fields.char('Name', size=64, required=True, select=1),
101         'write_date': fields.datetime('Date Modified', readonly=True),
102         'write_uid':  fields.many2one('res.users', 'Last Modification User', readonly=True),
103         'create_date': fields.datetime('Date Created', readonly=True),
104         'create_uid':  fields.many2one('res.users', 'Creator', readonly=True),
105         'user_id': fields.many2one('res.users', 'Owner'),
106         'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
107         'dir_ids': fields.one2many('document.directory', 'parent_id', 'Directories'),
108         'type': fields.selection([('db', 'Database'), ('filestore', 'Internal File storage'),
109             ('realstore', 'External file storage'), ('virtual', 'Virtual storage')], 'Type', required=True),
110         'path': fields.char('Path', size=250, select=1, help="For file storage, the root path of the storage"),
111         'online': fields.boolean('Online', help="If not checked, media is currently offline and its contents not available", required=True),
112         'readonly': fields.boolean('Read Only', help="If set, media is for reading only"),
113     }
114
115     def _get_rootpath(self, cr, uid, context=None):
116         return os.path.join(DMS_ROOT_PATH, cr.dbname)
117
118     _defaults = {
119         'user_id': lambda self, cr, uid, ctx: uid,
120         'online': lambda *args: True,
121         'readonly': lambda *args: False,
122         # Note: the defaults below should only be used ONCE for the default
123         # storage media. All other times, we should create different paths at least.
124         'type': lambda *args: 'filestore',
125         'path': _get_rootpath,
126     }
127     _sql_constraints = [
128         # SQL note: a path = NULL doesn't have to be unique.
129         ('path_uniq', 'UNIQUE(type,path)', "The storage path must be unique!")
130         ]
131
132     def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
133         """ retrieve the contents of some file_node having storage_id = id
134             optionally, fil_obj could point to the browse object of the file
135             (ir.attachment)
136         """
137         if not context:
138             context = {}
139         boo = self.browse(cr, uid, id, context)
140         if fil_obj:
141             ira = fil_obj
142         else:
143             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
144         return self.__get_data_3(cr, uid, boo, ira, context)
145
146     def __get_data_3(self, cr, uid, boo, ira, context):
147         if not boo.online:
148             raise RuntimeError('media offline')
149         if boo.type == 'filestore':
150             if not ira.store_fname:
151                 # On a migrated db, some files may have the wrong storage type
152                 # try to fix their directory.
153                 if ira.file_size:
154                     netsvc.Logger().notifyChannel('document', netsvc.LOG_WARNING, "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
155                 return None
156             fpath = os.path.join(boo.path, ira.store_fname)
157             print "Trying to read \"%s\".." % fpath
158             return file(fpath, 'rb').read()
159         elif boo.type == 'db':
160             # TODO: we need a better api for large files
161             if ira.db_datas:
162                 out = base64.decodestring(ira.db_datas)
163             else:
164                 out = ''
165             return out
166         elif boo.type == 'realstore':
167             # fpath = os.path.join(boo.path,
168             return None
169         else:
170             raise TypeError("No %s storage" % boo.type)
171
172     def set_data(self, cr, uid, id, file_node, data, context=None, fil_obj=None):
173         """ store the data.
174             This function MUST be used from an ir.attachment. It wouldn't make sense
175             to store things persistently for other types (dynamic).
176         """
177         if not context:
178             context = {}
179         boo = self.browse(cr, uid, id, context)
180         logger = netsvc.Logger()
181         if fil_obj:
182             ira = fil_obj
183         else:
184             ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
185
186         if not boo.online:
187             raise RuntimeError('media offline')
188         logger.notifyChannel('document', netsvc.LOG_DEBUG, "Store data for ir.attachment #%d" % ira.id)
189         store_fname = None
190         fname = None
191         if boo.type == 'filestore':
192             path = boo.path
193             try:
194                 flag = None
195                 # This can be improved  
196                 if os.path.isdir(path):
197                     for dirs in os.listdir(path):
198                         if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000:
199                             flag = dirs
200                             break
201                 flag = flag or create_directory(path)
202                 filename = random_name()
203                 fname = os.path.join(path, flag, filename)
204                 fp = file(fname, 'wb')
205                 fp.write(data)
206                 fp.close()
207                 logger.notifyChannel('document', netsvc.LOG_DEBUG, "Saved data to %s" % fname)
208                 filesize = len(data) # os.stat(fname).st_size
209                 store_fname = os.path.join(flag, filename)
210
211                 # TODO Here, an old file would be left hanging.
212
213             except Exception, e :
214                 netsvc.Logger().notifyChannel('document', netsvc.LOG_WARNING, "Couldn't save data: %s" % str(e))
215                 raise except_orm(_('Error!'), str(e))
216         elif boo.type == 'db':
217             filesize = len(data)
218             # will that work for huge data? TODO
219             out = base64.encodestring(data)
220             cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
221                 (out, file_node.file_id))
222         else:
223             raise TypeError("No %s storage" % boo.type)
224
225         # 2nd phase: store the metadata
226         try:
227             icont = ''
228             mime = ira.file_type
229             try:
230                 mime, icont = cntIndex.doIndex(data, ira.datas_fname,
231                 ira.file_type or None, fname)
232             except Exception, e:
233                 logger.notifyChannel('document', netsvc.LOG_DEBUG, 'Cannot index file: %s' % str(e))
234                 pass
235
236             # a hack: /assume/ that the calling write operation will not try
237             # to write the fname and size, and update them in the db concurrently.
238             # We cannot use a write() here, because we are already in one.
239             cr.execute('UPDATE ir_attachment SET store_fname = %s, file_size = %s, index_content = %s, file_type = %s WHERE id = %s',
240                 (store_fname, filesize, ustr(icont), mime, file_node.file_id))
241             file_node.content_length = filesize
242             file_node.content_type = mime
243             return True
244         except Exception, e :
245             netsvc.Logger().notifyChannel('document', netsvc.LOG_WARNING, "Couldn't save data: %s" % str(e))
246             # should we really rollback once we have written the actual data?
247             # at the db case (only), that rollback would be safe
248             raise except_orm(_('Error at doc write!'), str(e))
249
250     def prepare_unlink(self, cr, uid, storage_bo, fil_bo):
251         """ Before we unlink a file (fil_boo), prepare the list of real
252         files that have to be removed, too. """
253
254         if not storage_bo.online:
255             raise RuntimeError('media offline')
256
257         if storage_bo.type == 'filestore':
258             fname = fil_bo.store_fname
259             if not fname:
260                 return None
261             path = storage_bo.path
262             return (storage_bo.id, 'file', os.path.join(path, fname))
263         elif storage_bo.type == 'db':
264             return None
265         else:
266             raise TypeError("No %s storage" % boo.type)
267
268     def do_unlink(self, cr, uid, unres):
269         for id, ktype, fname in unres:
270             if ktype == 'file':
271                 try:
272                     os.unlink(fname)
273                 except Exception, e:
274                     netsvc.Logger().notifyChannel('document', netsvc.LOG_WARNING, "Could not remove file %s, please remove manually." % fname)
275             else:
276                 netsvc.Logger().notifyChannel('document', netsvc.LOG_WARNING, "Unknown unlink key %s" % ktype)
277
278         return True
279
280
281 document_storage()
282
283
284 #eof