[FIX] document_webdav : Replace <TAB> with four <WHITE SPACE>
[odoo/odoo.git] / addons / document_webdav / dav_fs.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 # Copyright (c) 2004 TINY SPRL. (http://tiny.be) All Rights Reserved.
5 #                    Fabien Pinckaers <fp@tiny.Be>
6 #
7 # WARNING: This program as such is intended to be used by professional
8 # programmers who take the whole responsability of assessing all potential
9 # consequences resulting from its eventual inadequacies and bugs
10 # End users who are looking for a ready-to-use solution with commercial
11 # garantees and support are strongly adviced to contract a Free Software
12 # Service Company
13 #
14 # This program is Free Software; you can redistribute it and/or
15 # modify it under the terms of the GNU General Public License
16 # as published by the Free Software Foundation; either version 2
17 # of the License, or (at your option) any later version.
18 #
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
22 # GNU General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program; if not, write to the Free Software
26 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
27 #
28 ##############################################################################
29
30 import pooler
31
32 import base64
33 import sys
34 import os
35 import time
36 from string import joinfields, split, lower
37
38 from service import security
39
40 import netsvc
41 import urlparse
42
43 from DAV.constants import COLLECTION, OBJECT
44 from DAV.errors import *
45 from DAV.iface import *
46 import urllib
47
48 from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
49
50 from cache import memoize
51
52 CACHE_SIZE=20000
53
54 #hack for urlparse: add webdav in the net protocols
55 urlparse.uses_netloc.append('webdav')
56 urlparse.uses_netloc.append('webdavs')
57
58 class tinydav_handler(dav_interface):
59     """
60     This class models a Tiny ERP interface for the DAV server
61     """
62     PROPS={'DAV:': dav_interface.PROPS['DAV:'], }
63
64     M_NS={ "DAV:" : dav_interface.M_NS['DAV:'], }
65
66     def __init__(self,  parent, verbose=False):
67         self.db_name = False
68         self.directory_id=False
69         self.db_name_list=[]
70         self.parent = parent
71         self.baseuri = parent.baseuri
72
73         def get_propnames(self,uri):
74             props = self.PROPS
75             self.parent.log_message('get propnames: %s' % uri)
76             if uri[-1]=='/':uri=uri[:-1]
77             cr, uid, pool, dbname, uri2 = self.get_cr(uri)
78             if not dbname:
79                 cr.close()
80                 return props
81             node = self.uri2object(cr,uid,pool, uri2)
82             if node:
83                 props.update(node.get_dav_props(cr))
84             cr.close()
85             return props
86
87         def get_prop(self,uri,ns,propname):
88             """ return the value of a given property
89
90                 uri        -- uri of the object to get the property of
91                 ns        -- namespace of the property
92                 pname        -- name of the property
93              """
94             if self.M_NS.has_key(ns):
95                return dav_interface.get_prop(self,uri,ns,propname)
96
97             if uri[-1]=='/':uri=uri[:-1]
98             cr, uid, pool, dbname, uri2 = self.get_cr(uri)
99             if not dbname:
100                 cr.close()
101                 raise DAV_NotFound
102             node = self.uri2object(cr,uid,pool, uri2)
103             if not node:
104                 cr.close()
105                 raise DAV_NotFound
106             res = node.get_dav_eprop(cr,ns,propname)
107             cr.close()
108             return res
109
110 #
111 #    def get_db(self,uri):
112 #        names=self.uri2local(uri).split('/')
113 #        self.db_name=False
114 #        if len(names) > 1:
115 #            self.db_name=self.uri2local(uri).split('/')[1]
116 #            if self.db_name=='':
117 #                raise Exception,'Plese specify Database name in folder'
118 #        return self.db_name
119 #
120
121     def later_get_db_from_path(self,path):
122         return "aaa"
123
124     def urijoin(self,*ajoin):
125         """ Return the base URI of this request, or even join it with the
126             ajoin path elements
127         """
128         return self.baseuri+ '/'.join(ajoin)
129
130     @memoize(4)
131     def db_list(self):
132         s = netsvc.LocalService('db')
133         result = s.list()
134         self.db_name_list=[]
135         for db_name in result:
136             db = pooler.get_db_only(db_name)
137             cr = db.cursor()
138             cr.execute("select id from ir_module_module where name = 'document' and state='installed' ")
139             res=cr.fetchone()
140             if res and len(res):
141                 self.db_name_list.append(db_name)
142             cr.close()
143         return self.db_name_list
144
145     def get_childs(self,uri):
146         """ return the child objects as self.baseuris for the given URI """
147         self.parent.log_message('get childs: %s' % uri)
148         if uri[-1]=='/':uri=uri[:-1]
149         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
150         
151         if not dbname:
152             s = netsvc.LocalService('db')
153             return map(lambda x: self.urijoin(x), self.db_list())
154         result = []
155         node = self.uri2object(cr,uid,pool, uri2[:])
156         if not node:
157             cr.close()
158             raise DAV_NotFound(uri2)
159         else:
160             fp = node.full_path()
161             if fp and len(fp):
162                 self.parent.log_message('childs: @%s' % fp)
163                 fp = '/'.join(fp)
164             else:
165                 fp = None
166             for d in node.children(cr):
167                 self.parent.log_message('child: %s' % d.path)
168                 if fp:
169                     result.append( self.urijoin(dbname,fp,d.path) )
170                 else:
171                     result.append( self.urijoin(dbname,d.path) )
172         cr.close()
173         return result
174
175     def uri2local(self, uri):
176         uparts=urlparse.urlparse(uri)
177         reluri=uparts[2]
178         if reluri and reluri[-1]=="/":
179             reluri=reluri[:-1]
180         return reluri
181
182     #
183     # pos: -1 to get the parent of the uri
184     #
185     def get_cr(self, uri):
186         pdb = self.parent.auth_proxy.last_auth
187         reluri = self.uri2local(uri)
188         try:
189             dbname = reluri.split('/')[2]
190         except:
191             dbname = False
192         if not dbname:
193             return None, None, None, False, None
194         if not pdb and dbname:
195             # if dbname was in our uri, we should have authenticated
196             # against that.
197             raise Exception("Programming error")
198         assert pdb == dbname, " %s != %s" %(pdb, dbname)
199         user, passwd, dbn2, uid = self.parent.auth_proxy.auth_creds[pdb]
200         db,pool = pooler.get_db_and_pool(dbname)
201         cr = db.cursor()
202         uri2 = reluri.split('/')[3:]
203         return cr, uid, pool, dbname, uri2
204
205     def uri2object(self, cr,uid, pool,uri):
206         if not uid:
207             return None
208         return pool.get('document.directory').get_object(cr, uid, uri)
209
210     def get_data(self,uri):
211         self.parent.log_message('GET: %s' % uri)
212         if uri[-1]=='/':uri=uri[:-1]
213         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
214         try:
215             if not dbname:
216                 raise DAV_Error, 409
217             node = self.uri2object(cr,uid,pool, uri2)
218             if not node:
219                 raise DAV_NotFound(uri2)
220             try:
221                 datas = node.get_data(cr)
222             except TypeError,e:
223                 import traceback
224                 self.parent.log_error("GET typeError: %s", str(e))
225                 self.parent.log_message("Exc: %s",traceback.format_exc())
226                 raise DAV_Forbidden
227             except IndexError,e :
228                 self.parent.log_error("GET IndexError: %s", str(e))
229                 raise DAV_NotFound(uri2)
230             except Exception,e:
231                 import traceback
232                 self.parent.log_error("GET exception: %s",str(e))
233                 self.parent.log_message("Exc: %s", traceback.format_exc())
234                 raise DAV_Error, 409
235             return datas
236         finally:
237             cr.close()
238
239     @memoize(CACHE_SIZE)
240     def _get_dav_resourcetype(self,uri):
241         """ return type of object """
242         self.parent.log_message('get RT: %s' % uri)
243         if uri[-1]=='/':uri=uri[:-1]
244         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
245         try:
246             if not dbname:
247                 return COLLECTION
248             node = self.uri2object(cr,uid,pool, uri2)
249             if not node:
250                 raise DAV_NotFound(uri2)
251             if node.type in ('collection','database'):
252                 return COLLECTION
253             return OBJECT
254         finally:
255             cr.close()
256
257     def _get_dav_displayname(self,uri):
258         self.parent.log_message('get DN: %s' % uri)
259         if uri[-1]=='/':uri=uri[:-1]
260         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
261         if not dbname:
262             cr.close()
263             return COLLECTION
264         node = self.uri2object(cr,uid,pool, uri2)
265         if not node:
266             cr.close()
267             raise DAV_NotFound(uri2)
268         cr.close()
269         return node.displayname
270
271     @memoize(CACHE_SIZE)
272     def _get_dav_getcontentlength(self,uri):
273         """ return the content length of an object """
274         self.parent.log_message('get length: %s' % uri)
275         if uri[-1]=='/':uri=uri[:-1]
276         result = 0
277         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
278         if not dbname:
279             cr.close()
280             return '0'
281         node = self.uri2object(cr, uid, pool, uri2)
282         if not node:
283             cr.close()
284             raise DAV_NotFound(uri2)
285         result = node.content_length or 0
286         cr.close()
287         return str(result)
288
289     @memoize(CACHE_SIZE)
290     def _get_dav_getetag(self,uri):
291         """ return the ETag of an object """
292         self.parent.log_message('get etag: %s' % uri)
293         if uri[-1]=='/':uri=uri[:-1]
294         result = 0
295         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
296         if not dbname:
297             cr.close()
298             return '0'
299         node = self.uri2object(cr, uid, pool, uri2)
300         if not node:
301             cr.close()
302             raise DAV_NotFound(uri2)
303         result = node.get_etag(cr)
304         cr.close()
305         return str(result)
306
307     @memoize(CACHE_SIZE)
308     def get_lastmodified(self,uri):
309         """ return the last modified date of the object """
310         if uri[-1]=='/':uri=uri[:-1]
311         today = time.time()
312         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
313         try:
314             if not dbname:
315                 return today
316             node = self.uri2object(cr,uid,pool, uri2)
317             if not node:
318                 raise DAV_NotFound(uri2)
319             if node.write_date:
320                 return time.mktime(time.strptime(node.write_date,'%Y-%m-%d %H:%M:%S'))
321             else:
322                 return today
323         finally:
324             cr.close()
325
326     @memoize(CACHE_SIZE)
327     def get_creationdate(self,uri):
328         """ return the last modified date of the object """
329
330         if uri[-1]=='/':uri=uri[:-1]
331         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
332         try:
333             if not dbname:
334                 raise DAV_Error, 409
335             node = self.uri2object(cr,uid,pool, uri2)
336             if not node:
337                 raise DAV_NotFound(uri2)
338             if node.create_date:
339                 result = time.strptime(node.create_date,'%Y-%m-%d %H:%M:%S')
340             else:
341                 result = time.gmtime()
342             return result
343         finally:
344             cr.close()
345
346     @memoize(CACHE_SIZE)
347     def _get_dav_getcontenttype(self,uri):
348         self.parent.log_message('get contenttype: %s' % uri)
349         if uri[-1]=='/':uri=uri[:-1]
350         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
351         try:
352             if not dbname:
353                 return 'httpd/unix-directory'
354             node = self.uri2object(cr,uid,pool, uri2)
355             if not node:
356                 raise DAV_NotFound(uri2)
357             
358             result = 'application/octet-stream'
359             #if node.type=='collection':
360                 #result ='httpd/unix-directory'
361             #else:
362             result = node.mimetype
363             return result
364             #raise DAV_NotFound, 'Could not find %s' % path
365         finally:
366             cr.close()
367
368     def mkcol(self,uri):
369         """ create a new collection """
370         self.parent.log_message('MKCOL: %s' % uri)
371         if uri[-1]=='/':uri=uri[:-1]
372         parent='/'.join(uri.split('/')[:-1])
373         if not parent.startswith(self.baseuri):
374             parent=self.baseuri + ''.join(parent[1:])
375         if not uri.startswith(self.baseuri):
376             uri=self.baseuri + ''.join(uri[1:])
377
378
379         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
380         if not dbname:
381             raise DAV_Error, 409
382         node = self.uri2object(cr,uid,pool, uri2[:-1])
383         object2=node and node.object2 or False
384         object=node and node.object or False
385
386         objname = uri2[-1]
387         if not object:
388             pool.get('document.directory').create(cr, uid, {
389                 'name': objname,
390                 'parent_id': False,
391                 'ressource_type_id': False,
392                 'ressource_id': False
393             })
394         else:
395             pool.get('document.directory').create(cr, uid, {
396                 'name': objname,
397                 'parent_id': object.id,
398                 'ressource_type_id': object.ressource_type_id.id,
399                 'ressource_id': object2 and object2.id or False
400             })
401
402         cr.commit()
403         cr.close()
404         return True
405
406     def put(self,uri,data,content_type=None):
407         """ put the object into the filesystem """
408         self.parent.log_message('Putting %s (%d), %s'%( unicode(uri,'utf8'), len(data), content_type))
409         parent='/'.join(uri.split('/')[:-1])
410         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
411         if not dbname:
412             raise DAV_Forbidden
413         try:
414             node = self.uri2object(cr,uid,pool, uri2[:])
415         except:
416             node = False
417         objname = uri2[-1]
418         ext = objname.find('.') >0 and objname.split('.')[1] or False
419
420         if not node:
421             dir_node = self.uri2object(cr,uid,pool, uri2[:-1])
422             if not dir_node:
423                 raise DAV_NotFound('Parent folder not found')
424             try:
425                 dir_node.create_child(cr,objname,data)
426             except Exception,e:
427                 import traceback
428                 self.parent.log_error("Cannot create %s: %s", objname, str(e))
429                 self.parent.log_message("Exc: %s",traceback.format_exc())
430                 raise DAV_Forbidden
431         else:
432             try:
433                 node.set_data(cr,data)
434             except Exception,e:
435                 import traceback
436                 self.parent.log_error("Cannot save %s: %s", objname, str(e))
437                 self.parent.log_message("Exc: %s",traceback.format_exc())
438                 raise DAV_Forbidden
439             
440         cr.commit()
441
442         return 201
443
444     def rmcol(self,uri):
445         """ delete a collection """
446         if uri[-1]=='/':uri=uri[:-1]
447
448         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
449         if True or not dbname: # *-*
450             raise DAV_Error, 409
451         node = self.uri2object(cr,uid,pool, uri2)
452         object2=node and node.object2 or False
453         object=node and node.object or False
454         if object._table_name=='document.directory':
455             if object.child_ids:
456                 raise DAV_Forbidden # forbidden
457             if object.file_ids:
458                 raise DAV_Forbidden # forbidden
459             res = pool.get('document.directory').unlink(cr, uid, [object.id])
460
461         cr.commit()
462         cr.close()
463         return 204
464
465     def rm(self,uri):
466         if uri[-1]=='/':uri=uri[:-1]
467
468         object=False
469         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
470         #if not dbname:
471         if True:
472             raise DAV_Error, 409
473         node = self.uri2object(cr,uid,pool, uri2)
474         object2=node and node.object2 or False
475         object=node and node.object or False
476         if not object:
477             raise DAV_NotFound
478
479         self.parent.log_message(' rm %s "%s"'%(object._table_name,uri))
480         if object._table_name=='ir.attachment':
481             res = pool.get('ir.attachment').unlink(cr, uid, [object.id])
482         else:
483             raise DAV_Forbidden # forbidden
484         parent='/'.join(uri.split('/')[:-1])
485         cr.commit()
486         cr.close()
487         return 204
488
489     ### DELETE handlers (examples)
490     ### (we use the predefined methods in davcmd instead of doing
491     ### a rm directly
492     ###
493
494     def delone(self,uri):
495         """ delete a single resource
496
497         You have to return a result dict of the form
498         uri:error_code
499         or None if everything's ok
500
501         """
502         if uri[-1]=='/':uri=uri[:-1]
503         res=delone(self,uri)
504         parent='/'.join(uri.split('/')[:-1])
505         return res
506
507     def deltree(self,uri):
508         """ delete a collection
509
510         You have to return a result dict of the form
511         uri:error_code
512         or None if everything's ok
513         """
514         if uri[-1]=='/':uri=uri[:-1]
515         res=deltree(self,uri)
516         parent='/'.join(uri.split('/')[:-1])
517         return res
518
519
520     ###
521     ### MOVE handlers (examples)
522     ###
523
524     def moveone(self,src,dst,overwrite):
525         """ move one resource with Depth=0
526
527         an alternative implementation would be
528
529         result_code=201
530         if overwrite:
531             result_code=204
532             r=os.system("rm -f '%s'" %dst)
533             if r: return 412
534         r=os.system("mv '%s' '%s'" %(src,dst))
535         if r: return 412
536         return result_code
537
538         (untested!). This would not use the davcmd functions
539         and thus can only detect errors directly on the root node.
540         """
541         res=moveone(self,src,dst,overwrite)
542         return res
543
544     def movetree(self,src,dst,overwrite):
545         """ move a collection with Depth=infinity
546
547         an alternative implementation would be
548
549         result_code=201
550         if overwrite:
551             result_code=204
552             r=os.system("rm -rf '%s'" %dst)
553             if r: return 412
554         r=os.system("mv '%s' '%s'" %(src,dst))
555         if r: return 412
556         return result_code
557
558         (untested!). This would not use the davcmd functions
559         and thus can only detect errors directly on the root node"""
560
561         res=movetree(self,src,dst,overwrite)
562         return res
563
564     ###
565     ### COPY handlers
566     ###
567
568     def copyone(self,src,dst,overwrite):
569         """ copy one resource with Depth=0
570
571         an alternative implementation would be
572
573         result_code=201
574         if overwrite:
575             result_code=204
576             r=os.system("rm -f '%s'" %dst)
577             if r: return 412
578         r=os.system("cp '%s' '%s'" %(src,dst))
579         if r: return 412
580         return result_code
581
582         (untested!). This would not use the davcmd functions
583         and thus can only detect errors directly on the root node.
584         """
585         res=copyone(self,src,dst,overwrite)
586         return res
587
588     def copytree(self,src,dst,overwrite):
589         """ copy a collection with Depth=infinity
590
591         an alternative implementation would be
592
593         result_code=201
594         if overwrite:
595             result_code=204
596             r=os.system("rm -rf '%s'" %dst)
597             if r: return 412
598         r=os.system("cp -r '%s' '%s'" %(src,dst))
599         if r: return 412
600         return result_code
601
602         (untested!). This would not use the davcmd functions
603         and thus can only detect errors directly on the root node"""
604         res=copytree(self,src,dst,overwrite)
605         return res
606
607     ###
608     ### copy methods.
609     ### This methods actually copy something. low-level
610     ### They are called by the davcmd utility functions
611     ### copytree and copyone (not the above!)
612     ### Look in davcmd.py for further details.
613     ###
614
615     def copy(self,src,dst):
616         src=urllib.unquote(src)
617         dst=urllib.unquote(dst)
618         ct = self._get_dav_getcontenttype(src)
619         data = self.get_data(src)
620         self.put(dst,data,ct)
621         return 201
622
623     def copycol(self,src,dst):
624         """ copy a collection.
625
626         As this is not recursive (the davserver recurses itself)
627         we will only create a new directory here. For some more
628         advanced systems we might also have to copy properties from
629         the source to the destination.
630         """
631         print " copy a collection."
632         return self.mkcol(dst)
633
634
635     def exists(self,uri):
636         """ test if a resource exists """
637         result = False
638         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
639         if not dbname:
640             return True
641         try:
642             node = self.uri2object(cr,uid,pool, uri2)
643             if node:
644                 result = True
645         except:
646             pass
647         cr.close()
648         return result
649
650     @memoize(CACHE_SIZE)
651     def is_collection(self,uri):
652         """ test if the given uri is a collection """
653         return self._get_dav_resourcetype(uri)==COLLECTION