[FIX] document_webdav: fixed regration in mkcol, rmcol, rm
[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 from document.nodes import node_res_dir, node_res_obj
50 from cache import memoize
51 from tools import misc
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             cr.close()
154             return map(lambda x: self.urijoin(x), self.db_list())
155         result = []
156         node = self.uri2object(cr,uid,pool, uri2[:])
157         if not node:
158             cr.close()
159             raise DAV_NotFound(uri2)
160         else:
161             fp = node.full_path()
162             if fp and len(fp):
163                 self.parent.log_message('childs: @%s' % fp)
164                 fp = '/'.join(fp)
165             else:
166                 fp = None
167             for d in node.children(cr):
168                 self.parent.log_message('child: %s' % d.path)
169                 if fp:
170                     result.append( self.urijoin(dbname,fp,d.path) )
171                 else:
172                     result.append( self.urijoin(dbname,d.path) )
173         cr.close()
174         return result
175
176     def uri2local(self, uri):
177         uparts=urlparse.urlparse(uri)
178         reluri=uparts[2]
179         if reluri and reluri[-1]=="/":
180             reluri=reluri[:-1]
181         return reluri
182
183     #
184     # pos: -1 to get the parent of the uri
185     #
186     def get_cr(self, uri):        
187         pdb = self.parent.auth_proxy.last_auth
188         reluri = self.uri2local(uri)        
189         try:
190             dbname = reluri.split('/')[2]
191         except:
192             dbname = False
193         if not dbname:
194             return None, None, None, False, None
195         if not pdb and dbname:
196             # if dbname was in our uri, we should have authenticated
197             # against that.
198             raise Exception("Programming error")        
199         assert pdb == dbname, " %s != %s" %(pdb, dbname)
200         user, passwd, dbn2, uid = self.parent.auth_proxy.auth_creds[pdb]
201         db,pool = pooler.get_db_and_pool(dbname)
202         cr = db.cursor()
203         uri2 = reluri.split('/')[3:]
204         return cr, uid, pool, dbname, uri2
205
206     def uri2object(self, cr,uid, pool,uri):
207         if not uid:
208             return None
209         return pool.get('document.directory').get_object(cr, uid, uri)
210
211     def get_data(self,uri):
212         self.parent.log_message('GET: %s' % uri)
213         if uri[-1]=='/':uri=uri[:-1]
214         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
215         try:
216             if not dbname:
217                 raise DAV_Error, 409
218             node = self.uri2object(cr,uid,pool, uri2)
219             if not node:
220                 raise DAV_NotFound(uri2)
221             try:
222                 datas = node.get_data(cr)
223             except TypeError,e:
224                 import traceback
225                 self.parent.log_error("GET typeError: %s", str(e))
226                 self.parent.log_message("Exc: %s",traceback.format_exc())
227                 raise DAV_Forbidden
228             except IndexError,e :
229                 self.parent.log_error("GET IndexError: %s", str(e))
230                 raise DAV_NotFound(uri2)
231             except Exception,e:
232                 import traceback
233                 self.parent.log_error("GET exception: %s",str(e))
234                 self.parent.log_message("Exc: %s", traceback.format_exc())
235                 raise DAV_Error, 409
236             return datas
237         finally:
238             cr.close()
239
240     @memoize(CACHE_SIZE)
241     def _get_dav_resourcetype(self,uri):
242         """ return type of object """
243         self.parent.log_message('get RT: %s' % uri)
244         if uri[-1]=='/':uri=uri[:-1]
245         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
246         try:
247             if not dbname:
248                 return COLLECTION
249             node = self.uri2object(cr,uid,pool, uri2)
250             if not node:
251                 raise DAV_NotFound(uri2)
252             if node.type in ('collection','database'):
253                 return COLLECTION
254             return OBJECT
255         finally:
256             cr.close()
257
258     def _get_dav_displayname(self,uri):
259         self.parent.log_message('get DN: %s' % uri)
260         if uri[-1]=='/':uri=uri[:-1]
261         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
262         if not dbname:
263             cr.close()
264             return COLLECTION
265         node = self.uri2object(cr,uid,pool, uri2)
266         if not node:
267             cr.close()
268             raise DAV_NotFound(uri2)
269         cr.close()
270         return node.displayname
271
272     @memoize(CACHE_SIZE)
273     def _get_dav_getcontentlength(self,uri):
274         """ return the content length of an object """
275         self.parent.log_message('get length: %s' % uri)
276         if uri[-1]=='/':uri=uri[:-1]
277         result = 0
278         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
279         if not dbname:
280             cr.close()
281             return '0'
282         node = self.uri2object(cr, uid, pool, uri2)
283         if not node:
284             cr.close()
285             raise DAV_NotFound(uri2)
286         result = node.content_length or 0
287         cr.close()
288         return str(result)
289
290     @memoize(CACHE_SIZE)
291     def _get_dav_getetag(self,uri):
292         """ return the ETag of an object """
293         self.parent.log_message('get etag: %s' % uri)
294         if uri[-1]=='/':uri=uri[:-1]
295         result = 0
296         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
297         if not dbname:
298             cr.close()
299             return '0'
300         node = self.uri2object(cr, uid, pool, uri2)
301         if not node:
302             cr.close()
303             raise DAV_NotFound(uri2)
304         result = node.get_etag(cr)
305         cr.close()
306         return str(result)
307
308     @memoize(CACHE_SIZE)
309     def get_lastmodified(self,uri):
310         """ return the last modified date of the object """
311         if uri[-1]=='/':uri=uri[:-1]
312         today = time.time()
313         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
314         try:
315             if not dbname:
316                 return today
317             node = self.uri2object(cr,uid,pool, uri2)
318             if not node:
319                 raise DAV_NotFound(uri2)
320             if node.write_date:
321                 return time.mktime(time.strptime(node.write_date,'%Y-%m-%d %H:%M:%S'))
322             else:
323                 return today
324         finally:
325             cr.close()
326
327     @memoize(CACHE_SIZE)
328     def get_creationdate(self,uri):
329         """ return the last modified date of the object """
330
331         if uri[-1]=='/':uri=uri[:-1]
332         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
333         try:
334             if not dbname:
335                 raise DAV_Error, 409
336             node = self.uri2object(cr,uid,pool, uri2)
337             if not node:
338                 raise DAV_NotFound(uri2)
339             if node.create_date:
340                 result = time.strptime(node.create_date,'%Y-%m-%d %H:%M:%S')
341             else:
342                 result = time.gmtime()
343             return result
344         finally:
345             cr.close()
346
347     @memoize(CACHE_SIZE)
348     def _get_dav_getcontenttype(self,uri):
349         self.parent.log_message('get contenttype: %s' % uri)
350         if uri[-1]=='/':uri=uri[:-1]
351         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
352         try:
353             if not dbname:
354                 return 'httpd/unix-directory'
355             node = self.uri2object(cr,uid,pool, uri2)
356             if not node:
357                 raise DAV_NotFound(uri2)
358             result = node.mimetype
359             return result
360             #raise DAV_NotFound, 'Could not find %s' % path
361         finally:
362             cr.close()
363
364     def mkcol(self,uri):
365         """ create a new collection """
366         self.parent.log_message('MKCOL: %s' % uri)
367         if uri[-1]=='/':uri=uri[:-1]
368         parent='/'.join(uri.split('/')[:-1])
369         if not parent.startswith(self.baseuri):
370             parent=self.baseuri + ''.join(parent[1:])
371         if not uri.startswith(self.baseuri):
372             uri=self.baseuri + ''.join(uri[1:])
373
374
375         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
376         if not dbname:
377             raise DAV_Error, 409
378         node = self.uri2object(cr,uid,pool, uri2[:-1])
379         object2 = False            
380         if isinstance(node, node_res_obj):
381             object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False            
382         
383         obj = node.context._dirobj.browse(cr, uid, node.dir_id)            
384         if obj and (obj.type == 'ressource') and not object2:
385             raise OSError(1, 'Operation not permited.')
386
387         objname = uri2[-1]
388         val = {
389                 'name': objname,
390                 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
391                 'ressource_id': object2 and object2.id or False,
392                 'parent_id' : False
393         }
394         if (obj and (obj.type in ('directory'))) or not object2:                
395             val['parent_id'] =  obj and obj.id or False            
396         
397         pool.get('document.directory').create(cr, uid, val)
398         cr.commit() 
399         cr.close()
400         return True
401
402     def put(self,uri,data,content_type=None):
403         """ put the object into the filesystem """
404         self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), len(data), content_type))
405         parent='/'.join(uri.split('/')[:-1])
406         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
407         if not dbname:
408             raise DAV_Forbidden
409         try:
410             node = self.uri2object(cr,uid,pool, uri2[:])
411         except:
412             node = False
413         objname = uri2[-1]
414         ext = objname.find('.') >0 and objname.split('.')[1] or False
415
416         if not node:
417             dir_node = self.uri2object(cr,uid,pool, uri2[:-1])
418             if not dir_node:
419                 raise DAV_NotFound('Parent folder not found')
420             try:
421                 dir_node.create_child(cr,objname,data)
422             except Exception,e:
423                 import traceback
424                 self.parent.log_error("Cannot create %s: %s", objname, str(e))
425                 self.parent.log_message("Exc: %s",traceback.format_exc())
426                 raise DAV_Forbidden
427         else:
428             try:
429                 node.set_data(cr,data)
430             except Exception,e:
431                 import traceback
432                 self.parent.log_error("Cannot save %s: %s", objname, str(e))
433                 self.parent.log_message("Exc: %s",traceback.format_exc())
434                 raise DAV_Forbidden
435             
436         cr.commit()
437         cr.close()
438         return 201
439
440     def rmcol(self,uri):
441         """ delete a collection """
442         if uri[-1]=='/':uri=uri[:-1]
443
444         cr, uid, pool, dbname, uri2 = self.get_cr(uri)        
445         if not dbname: # *-*
446             raise DAV_Error, 409
447         node = self.uri2object(cr, uid, pool, uri2)             
448         object = node.context._dirobj.browse(cr, uid, node.dir_id)
449         
450         if not object:
451             raise OSError(2, 'Not such file or directory.')        
452         if object._table_name=='document.directory':
453             if node.children(cr):
454                 raise OSError(39, 'Directory not empty.')
455             res = pool.get('document.directory').unlink(cr, uid, [object.id])
456         else:
457             raise OSError(1, 'Operation not permited.')
458
459         cr.commit()
460         cr.close()
461         return 204
462
463     def rm(self,uri):
464         if uri[-1]=='/':uri=uri[:-1]
465
466         object=False
467         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
468         if not dbname:        
469             cr.close()
470             raise DAV_Error, 409
471         node = self.uri2object(cr,uid,pool, uri2)
472         object = pool.get('ir.attachment').browse(cr, uid, node.file_id)
473         self.parent.log_message(' rm %s "%s"'%(object._table_name,uri))
474         if not object:
475             raise OSError(2, 'Not such file or directory.')
476         if object._table_name == 'ir.attachment':
477             res = pool.get('ir.attachment').unlink(cr, uid, [object.id])
478         else:
479             raise OSError(1, 'Operation not permited.')       
480         
481         cr.commit()
482         cr.close()
483         return 204
484
485     ### DELETE handlers (examples)
486     ### (we use the predefined methods in davcmd instead of doing
487     ### a rm directly
488     ###
489
490     def delone(self,uri):
491         """ delete a single resource
492
493         You have to return a result dict of the form
494         uri:error_code
495         or None if everything's ok
496
497         """
498         if uri[-1]=='/':uri=uri[:-1]
499         res=delone(self,uri)
500         parent='/'.join(uri.split('/')[:-1])
501         return res
502
503     def deltree(self,uri):
504         """ delete a collection
505
506         You have to return a result dict of the form
507         uri:error_code
508         or None if everything's ok
509         """
510         if uri[-1]=='/':uri=uri[:-1]
511         res=deltree(self,uri)
512         parent='/'.join(uri.split('/')[:-1])
513         return res
514
515
516     ###
517     ### MOVE handlers (examples)
518     ###
519
520     def moveone(self,src,dst,overwrite):
521         """ move one resource with Depth=0
522
523         an alternative implementation would be
524
525         result_code=201
526         if overwrite:
527             result_code=204
528             r=os.system("rm -f '%s'" %dst)
529             if r: return 412
530         r=os.system("mv '%s' '%s'" %(src,dst))
531         if r: return 412
532         return result_code
533
534         (untested!). This would not use the davcmd functions
535         and thus can only detect errors directly on the root node.
536         """
537         res=moveone(self,src,dst,overwrite)
538         return res
539
540     def movetree(self,src,dst,overwrite):
541         """ move a collection with Depth=infinity
542
543         an alternative implementation would be
544
545         result_code=201
546         if overwrite:
547             result_code=204
548             r=os.system("rm -rf '%s'" %dst)
549             if r: return 412
550         r=os.system("mv '%s' '%s'" %(src,dst))
551         if r: return 412
552         return result_code
553
554         (untested!). This would not use the davcmd functions
555         and thus can only detect errors directly on the root node"""
556
557         res=movetree(self,src,dst,overwrite)
558         return res
559
560     ###
561     ### COPY handlers
562     ###
563
564     def copyone(self,src,dst,overwrite):
565         """ copy one resource with Depth=0
566
567         an alternative implementation would be
568
569         result_code=201
570         if overwrite:
571             result_code=204
572             r=os.system("rm -f '%s'" %dst)
573             if r: return 412
574         r=os.system("cp '%s' '%s'" %(src,dst))
575         if r: return 412
576         return result_code
577
578         (untested!). This would not use the davcmd functions
579         and thus can only detect errors directly on the root node.
580         """
581         res=copyone(self,src,dst,overwrite)
582         return res
583
584     def copytree(self,src,dst,overwrite):
585         """ copy a collection with Depth=infinity
586
587         an alternative implementation would be
588
589         result_code=201
590         if overwrite:
591             result_code=204
592             r=os.system("rm -rf '%s'" %dst)
593             if r: return 412
594         r=os.system("cp -r '%s' '%s'" %(src,dst))
595         if r: return 412
596         return result_code
597
598         (untested!). This would not use the davcmd functions
599         and thus can only detect errors directly on the root node"""
600         res=copytree(self,src,dst,overwrite)
601         return res
602
603     ###
604     ### copy methods.
605     ### This methods actually copy something. low-level
606     ### They are called by the davcmd utility functions
607     ### copytree and copyone (not the above!)
608     ### Look in davcmd.py for further details.
609     ###
610
611     def copy(self,src,dst):
612         src=urllib.unquote(src)
613         dst=urllib.unquote(dst)
614         ct = self._get_dav_getcontenttype(src)
615         data = self.get_data(src)
616         self.put(dst,data,ct)
617         return 201
618
619     def copycol(self,src,dst):
620         """ copy a collection.
621
622         As this is not recursive (the davserver recurses itself)
623         we will only create a new directory here. For some more
624         advanced systems we might also have to copy properties from
625         the source to the destination.
626         """
627         print " copy a collection."
628         return self.mkcol(dst)
629
630
631     def exists(self,uri):
632         """ test if a resource exists """
633         result = False
634         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
635         if not dbname:
636             cr.close()
637             return True
638         try:
639             node = self.uri2object(cr,uid,pool, uri2)
640             if node:
641                 result = True
642         except:
643             pass
644         cr.close()
645         return result
646
647     @memoize(CACHE_SIZE)
648     def is_collection(self,uri):
649         """ test if the given uri is a collection """
650         return self._get_dav_resourcetype(uri)==COLLECTION