doc webdav: switch on the DAV v2 features
[odoo/odoo.git] / addons / document_webdav / dav_fs.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21 import pooler
22
23 import base64
24 import sys
25 import os
26 import time
27 from string import joinfields, split, lower
28
29 import netsvc
30 import urlparse
31
32 from DAV.constants import COLLECTION, OBJECT
33 from DAV.errors import *
34 from DAV.iface import *
35 import urllib
36
37 from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree
38 from cache import memoize
39 from tools import misc
40 from tools.dict_tools import dict_merge2
41
42 CACHE_SIZE=20000
43
44 #hack for urlparse: add webdav in the net protocols
45 urlparse.uses_netloc.append('webdav')
46 urlparse.uses_netloc.append('webdavs')
47
48 day_names = { 0: 'Mon', 1: 'Tue' , 2: 'Wed', 3: 'Thu', 4: 'Fri', 5: 'Sat', 6: 'Sun' }
49 month_names = { 1: 'Jan', 2: 'Feb', 3: 'Mar', 4: 'Apr', 5: 'May', 6: 'Jun',
50         7: 'Jul', 8: 'Aug', 9: 'Sep', 10: 'Oct', 11: 'Nov', 12: 'Dec' }
51
52 class DAV_NotFound2(DAV_NotFound):
53     """404 exception, that accepts our list uris
54     """
55     def __init__(self, *args):
56         if len(args) and isinstance(args[0], (tuple, list)):
57             path = ''.join([ '/' + x for x in args[0]])
58             args = (path, )
59         DAV_NotFound.__init__(self, *args)
60
61
62 def _str2time(cre):
63     """ Convert a string with time representation (from db) into time (float)
64     """
65     if not cre:
66         return time.time()
67     frac = 0.0
68     if isinstance(cre, basestring) and '.' in cre:
69         fdot = cre.find('.')
70         frac = float(cre[fdot:])
71         cre = cre[:fdot]
72     return time.mktime(time.strptime(cre,'%Y-%m-%d %H:%M:%S')) + frac
73
74 class openerp_dav_handler(dav_interface):
75     """
76     This class models a OpenERP interface for the DAV server
77     """
78     PROPS={'DAV:': dav_interface.PROPS['DAV:'],}
79
80     M_NS={ "DAV:" : dav_interface.M_NS['DAV:'],}
81
82     def __init__(self,  parent, verbose=False):
83         self.db_name_list=[]
84         self.parent = parent
85         self.baseuri = parent.baseuri
86         self.verbose = verbose
87
88     def get_propnames(self, uri):
89         props = self.PROPS
90         self.parent.log_message('get propnames: %s' % uri)
91         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
92         if not dbname:
93             if cr: cr.close()
94             # TODO: maybe limit props for databases..?
95             return props
96         node = self.uri2object(cr, uid, pool, uri2)
97         if node:
98             props = dict_merge2(props, node.get_dav_props(cr))
99         cr.close()
100         return props
101
102     def _try_function(self, funct, args, opname='run function', cr=None,
103             default_exc=DAV_Forbidden):
104         """ Try to run a function, and properly convert exceptions to DAV ones.
105
106             @objname the name of the operation being performed
107             @param cr if given, the cursor to close at exceptions
108         """
109
110         try:
111             return funct(*args)
112         except DAV_Error:
113             if cr: cr.close()
114             raise
115         except NotImplementedError, e:
116             if cr: cr.close()
117             import traceback
118             self.parent.log_error("Cannot %s: %s", opname, str(e))
119             self.parent.log_message("Exc: %s",traceback.format_exc())
120             # see par 9.3.1 of rfc
121             raise DAV_Error(403, str(e) or 'Not supported at this path')
122         except EnvironmentError, err:
123             if cr: cr.close()
124             import traceback
125             self.parent.log_error("Cannot %s: %s", opname, err.strerror)
126             self.parent.log_message("Exc: %s",traceback.format_exc())
127             raise default_exc(err.strerror)
128         except Exception,e:
129             import traceback
130             if cr: cr.close()
131             self.parent.log_error("Cannot %s: %s", opname, str(e))
132             self.parent.log_message("Exc: %s",traceback.format_exc())
133             raise default_exc("Operation failed")
134
135     #def _get_dav_lockdiscovery(self, uri):
136     #    raise DAV_NotFound
137
138     #def A_get_dav_supportedlock(self, uri):
139     #    raise DAV_NotFound
140
141     def match_prop(self, uri, match, ns, propname):
142         if self.M_NS.has_key(ns):
143             return match == dav_interface.get_prop(self, uri, ns, propname)
144         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
145         if not dbname:
146             if cr: cr.close()
147             raise DAV_NotFound
148         node = self.uri2object(cr, uid, pool, uri2)
149         if not node:
150             cr.close()
151             raise DAV_NotFound
152         res = node.match_dav_eprop(cr, match, ns, propname)
153         cr.close()
154         return res
155
156     def prep_http_options(self, uri, opts):
157         """see HttpOptions._prep_OPTIONS """
158         self.parent.log_message('get options: %s' % uri)
159         cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
160
161         if not dbname:
162             if cr: cr.close()
163             return opts
164         node = self.uri2object(cr, uid, pool, uri2[:])
165
166         if not node:
167             if cr: cr.close()
168             return opts
169         else:
170             if hasattr(node, 'http_options'):
171                 ret = opts.copy()
172                 for key, val in node.http_options.items():
173                     if isinstance(val, basestring):
174                         val = [val, ]
175                     if key in ret:
176                         ret[key] = ret[key][:]  # copy the orig. array
177                     else:
178                         ret[key] = []
179                     ret[key].extend(val)
180
181                 self.parent.log_message('options: %s' % ret)
182             else:
183                 ret = opts
184             cr.close()
185             return ret
186
187     def get_prop(self, uri, ns, propname):
188         """ return the value of a given property
189
190             uri        -- uri of the object to get the property of
191             ns        -- namespace of the property
192             pname        -- name of the property
193          """
194         if self.M_NS.has_key(ns):
195             try:
196                 # if it's not in the interface class, a "DAV:" property
197                 # may be at the node class. So shouldn't give up early.
198                 return dav_interface.get_prop(self, uri, ns, propname)
199             except DAV_NotFound:
200                 pass
201         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
202         if not dbname:
203             if cr: cr.close()
204             raise DAV_NotFound
205         node = self.uri2object(cr, uid, pool, uri2)
206         if not node:
207             cr.close()
208             raise DAV_NotFound
209         res = node.get_dav_eprop(cr, ns, propname)
210         cr.close()
211         return res
212
213     def get_db(self, uri, rest_ret=False, allow_last=False):
214         """Parse the uri and get the dbname and the rest.
215            Db name should be the first component in the unix-like
216            path supplied in uri.
217
218            @param rest_ret Instead of the db_name, return (db_name, rest),
219                 where rest is the remaining path
220            @param allow_last If the dbname is the last component in the
221                 path, allow it to be resolved. The default False value means
222                 we will not attempt to use the db, unless there is more
223                 path.
224
225            @return db_name or (dbname, rest) depending on rest_ret,
226                 will return dbname=False when component is not found.
227         """
228
229         uri2 = self.uri2local(uri)
230         if uri2.startswith('/'):
231             uri2 = uri2[1:]
232         names=uri2.split('/',1)
233         db_name=False
234         rest = None
235         if allow_last:
236             ll = 0
237         else:
238             ll = 1
239         if len(names) > ll and names[0]:
240             db_name = names[0]
241             names = names[1:]
242
243         if rest_ret:
244             if len(names):
245                 rest = names[0]
246             return db_name, rest
247         return db_name
248
249
250     def urijoin(self,*ajoin):
251         """ Return the base URI of this request, or even join it with the
252             ajoin path elements
253         """
254         return self.baseuri+ '/'.join(ajoin)
255
256     @memoize(4)
257     def db_list(self):
258         s = netsvc.ExportService.getService('db')
259         result = s.exp_list()
260         self.db_name_list=[]
261         for db_name in result:
262             cr = None
263             try:
264                 db = pooler.get_db_only(db_name)
265                 cr = db.cursor()
266                 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document' AND state='installed' ")
267                 res=cr.fetchone()
268                 if res and len(res):
269                     self.db_name_list.append(db_name)
270             except Exception, e:
271                 self.parent.log_error("Exception in db list: %s" % e)
272             finally:
273                 if cr:
274                     cr.close()
275         return self.db_name_list
276
277     def get_childs(self, uri, filters=None):
278         """ return the child objects as self.baseuris for the given URI """
279         self.parent.log_message('get childs: %s' % uri)
280         cr, uid, pool, dbname, uri2 = self.get_cr(uri, allow_last=True)
281
282         if not dbname:
283             if cr: cr.close()
284             res = map(lambda x: self.urijoin(x), self.db_list())
285             return res
286         result = []
287         node = self.uri2object(cr, uid, pool, uri2[:])
288
289         try:
290             if not node:
291                 raise DAV_NotFound2(uri2)
292             else:
293                 fp = node.full_path()
294                 if fp and len(fp):
295                     fp = '/'.join(fp)
296                     self.parent.log_message('childs for: %s' % fp)
297                 else:
298                     fp = None
299                 domain = None
300                 if filters:
301                     domain = node.get_domain(cr, filters)
302                 for d in node.children(cr, domain):
303                     self.parent.log_message('child: %s' % d.path)
304                     if fp:
305                         result.append( self.urijoin(dbname,fp,d.path) )
306                     else:
307                         result.append( self.urijoin(dbname,d.path) )
308         finally:
309             if cr: cr.close()
310         return result
311
312     def uri2local(self, uri):
313         uparts=urlparse.urlparse(uri)
314         reluri=uparts[2]
315         if reluri and reluri[-1]=="/":
316             reluri=reluri[:-1]
317         return reluri
318
319     #
320     # pos: -1 to get the parent of the uri
321     #
322     def get_cr(self, uri, allow_last=False):
323         """ Split the uri, grab a cursor for that db
324         """
325         pdb = self.parent.auth_proxy.last_auth
326         dbname, uri2 = self.get_db(uri, rest_ret=True, allow_last=allow_last)
327         uri2 = (uri2 and uri2.split('/')) or []
328         if not dbname:
329             return None, None, None, False, uri2
330         # if dbname was in our uri, we should have authenticated
331         # against that.
332         assert pdb == dbname, " %s != %s" %(pdb, dbname)
333         res = self.parent.auth_proxy.auth_creds.get(dbname, False)
334         if not res:
335             self.parent.auth_proxy.checkRequest(self.parent, uri, dbname)
336             res = self.parent.auth_proxy.auth_creds[dbname]
337         user, passwd, dbn2, uid = res
338         db,pool = pooler.get_db_and_pool(dbname)
339         cr = db.cursor()
340         return cr, uid, pool, dbname, uri2
341
342     def uri2object(self, cr, uid, pool, uri):
343         if not uid:
344             return None
345         return pool.get('document.directory').get_object(cr, uid, uri)
346
347     def get_data(self,uri, rrange=None):
348         self.parent.log_message('GET: %s' % uri)
349         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
350         try:
351             if not dbname:
352                 raise DAV_Error, 409
353             node = self.uri2object(cr, uid, pool, uri2)
354             if not node:
355                 raise DAV_NotFound2(uri2)
356             try:
357                 if rrange:
358                     self.parent.log_error("Doc get_data cannot use range")
359                     raise DAV_Error(409)
360                 datas = node.get_data(cr)
361             except TypeError,e:
362                 # for the collections that return this error, the DAV standard
363                 # says we'd better just return 200 OK with empty data
364                 return ''
365             except IndexError,e :
366                 self.parent.log_error("GET IndexError: %s", str(e))
367                 raise DAV_NotFound2(uri2)
368             except Exception,e:
369                 import traceback
370                 self.parent.log_error("GET exception: %s",str(e))
371                 self.parent.log_message("Exc: %s", traceback.format_exc())
372                 raise DAV_Error, 409
373             return str(datas) # FIXME!
374         finally:
375             if cr: cr.close()
376
377     @memoize(CACHE_SIZE)
378     def _get_dav_resourcetype(self, uri):
379         """ return type of object """
380         self.parent.log_message('get RT: %s' % uri)
381         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
382         try:
383             if not dbname:
384                 return COLLECTION
385             node = self.uri2object(cr, uid, pool, uri2)
386             if not node:
387                 raise DAV_NotFound2(uri2)
388             try:
389                 return node.get_dav_resourcetype(cr)
390             except NotImplementedError:
391                 if node.type in ('collection','database'):
392                     return ('collection', 'DAV:')
393                 return ''
394         finally:
395             if cr: cr.close()
396
397     def _get_dav_displayname(self,uri):
398         self.parent.log_message('get DN: %s' % uri)
399         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
400         if not dbname:
401             if cr: cr.close()
402             # at root, dbname, just return the last component
403             # of the path.
404             if uri2 and len(uri2) < 2:
405                 return uri2[-1]
406             return ''
407         node = self.uri2object(cr, uid, pool, uri2)
408         if not node:
409             if cr: cr.close()
410             raise DAV_NotFound2(uri2)
411         cr.close()
412         return node.displayname
413
414     @memoize(CACHE_SIZE)
415     def _get_dav_getcontentlength(self, uri):
416         """ return the content length of an object """        
417         self.parent.log_message('get length: %s' % uri)
418         result = 0
419         cr, uid, pool, dbname, uri2 = self.get_cr(uri)        
420         if not dbname:
421             if cr: cr.close()
422             return str(result)
423         node = self.uri2object(cr, uid, pool, uri2)
424         if not node:
425             if cr: cr.close()
426             raise DAV_NotFound2(uri2)
427         result = node.content_length or 0
428         cr.close()
429         return str(result)
430
431     @memoize(CACHE_SIZE)
432     def _get_dav_getetag(self,uri):
433         """ return the ETag of an object """
434         self.parent.log_message('get etag: %s' % uri)
435         result = 0
436         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
437         if not dbname:
438             if cr: cr.close()
439             return '0'
440         node = self.uri2object(cr, uid, pool, uri2)
441         if not node:
442             cr.close()
443             raise DAV_NotFound2(uri2)
444         result = self._try_function(node.get_etag ,(cr,), "etag %s" %uri, cr=cr)
445         cr.close()
446         return str(result)
447
448     @memoize(CACHE_SIZE)
449     def get_lastmodified(self, uri):
450         """ return the last modified date of the object """
451         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
452         if not dbname:
453             return time.time()
454         try:            
455             node = self.uri2object(cr, uid, pool, uri2)
456             if not node:
457                 raise DAV_NotFound2(uri2)
458             return _str2time(node.write_date)
459         finally:
460             if cr: cr.close()
461
462     def _get_dav_getlastmodified(self,uri):
463         """ return the last modified date of a resource
464         """
465         d=self.get_lastmodified(uri)
466         # format it. Note that we explicitly set the day, month names from
467         # an array, so that strftime() doesn't use its own locale-aware
468         # strings.
469         gmt = time.gmtime(d)
470         return time.strftime("%%s, %d %%s %Y %H:%M:%S GMT", gmt ) % \
471                     (day_names[gmt.tm_wday], month_names[gmt.tm_mon])
472
473     @memoize(CACHE_SIZE)
474     def get_creationdate(self, uri):
475         """ return the last modified date of the object """        
476         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
477         if not dbname:
478             raise DAV_Error, 409
479         try:            
480             node = self.uri2object(cr, uid, pool, uri2)
481             if not node:
482                 raise DAV_NotFound2(uri2)
483
484             return _str2time(node.create_date)
485         finally:
486             if cr: cr.close()
487
488     @memoize(CACHE_SIZE)
489     def _get_dav_getcontenttype(self,uri):
490         self.parent.log_message('get contenttype: %s' % uri)
491         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
492         if not dbname:
493             if cr: cr.close()
494             return 'httpd/unix-directory'
495         try:            
496             node = self.uri2object(cr, uid, pool, uri2)
497             if not node:
498                 raise DAV_NotFound2(uri2)
499             result = str(node.mimetype)
500             return result
501             #raise DAV_NotFound, 'Could not find %s' % path
502         finally:
503             if cr: cr.close()    
504     
505     def mkcol(self,uri):
506         """ create a new collection
507             see par. 9.3 of rfc4918
508         """
509         self.parent.log_message('MKCOL: %s' % uri)
510         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
511         if not uri2[-1]:
512             if cr: cr.close()
513             raise DAV_Error(409, "Cannot create nameless collection")
514         if not dbname:
515             if cr: cr.close()
516             raise DAV_Error, 409
517         node = self.uri2object(cr,uid,pool, uri2[:-1])
518         if not node:
519             cr.close()
520             raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
521         nc = node.child(cr, uri2[-1])
522         if nc:
523             cr.close()
524             raise DAV_Error(405, "Path already exists")
525         self._try_function(node.create_child_collection, (cr, uri2[-1]),
526                     "create col %s" % uri2[-1], cr=cr)
527         cr.commit()
528         cr.close()
529         return True
530
531     def put(self, uri, data, content_type=None):
532         """ put the object into the filesystem """
533         self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), data and len(data) or 0, content_type))
534         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
535         if not dbname:
536             if cr: cr.close()
537             raise DAV_Forbidden
538         try:
539             node = self.uri2object(cr, uid, pool, uri2[:])
540         except Exception:
541             node = False
542         
543         objname = uri2[-1]
544         ext = objname.find('.') >0 and objname.split('.')[1] or False
545
546         ret = None
547         if not node:
548             dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
549             if not dir_node:
550                 cr.close()
551                 raise DAV_NotFound('Parent folder not found')
552
553             newchild = self._try_function(dir_node.create_child, (cr, objname, data),
554                     "create %s" % objname, cr=cr)
555             if not newchild:
556                 cr.commit()
557                 cr.close()
558                 raise DAV_Error(400, "Failed to create resource")
559             
560             uparts=urlparse.urlparse(uri)
561             fileloc = '/'.join(newchild.full_path())
562             if isinstance(fileloc, unicode):
563                 fileloc = fileloc.encode('utf-8')
564             # the uri we get is a mangled one, where the davpath has been removed
565             davpath = self.parent.get_davpath()
566             
567             surl = '%s://%s' % (uparts[0], uparts[1])
568             uloc = urllib.quote(fileloc)
569             hurl = False
570             if uri != ('/'+uloc) and uri != (surl + '/' + uloc):
571                 hurl = '%s%s/%s/%s' %(surl, davpath, dbname, uloc)
572             etag = False
573             try:
574                 etag = str(newchild.get_etag(cr))
575             except Exception, e:
576                 self.parent.log_error("Cannot get etag for node: %s" % e)
577             ret = (hurl, etag)
578         else:
579             self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
580             
581         cr.commit()
582         cr.close()
583         return ret
584
585     def rmcol(self,uri):
586         """ delete a collection """
587         cr, uid, pool, dbname, uri2 = self.get_cr(uri)        
588         if not dbname:
589             if cr: cr.close()
590             raise DAV_Error, 409
591
592         node = self.uri2object(cr, uid, pool, uri2)             
593         self._try_function(node.rmcol, (cr,), "rmcol %s" % uri, cr=cr)
594
595         cr.commit()
596         cr.close()
597         return 204
598
599     def rm(self,uri):
600         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
601         if not dbname:        
602             if cr: cr.close()
603             raise DAV_Error, 409
604         node = self.uri2object(cr, uid, pool, uri2)
605         res = self._try_function(node.rm, (cr,), "rm %s"  % uri, cr=cr)
606         if not res:
607             if cr: cr.close()
608             raise OSError(1, 'Operation not permited.')        
609         cr.commit()
610         cr.close()
611         return 204
612
613     ### DELETE handlers (examples)
614     ### (we use the predefined methods in davcmd instead of doing
615     ### a rm directly
616     ###
617
618     def delone(self, uri):
619         """ delete a single resource
620
621         You have to return a result dict of the form
622         uri:error_code
623         or None if everything's ok
624
625         """
626         if uri[-1]=='/':uri=uri[:-1]
627         res=delone(self,uri)
628         parent='/'.join(uri.split('/')[:-1])
629         return res
630
631     def deltree(self, uri):
632         """ delete a collection
633
634         You have to return a result dict of the form
635         uri:error_code
636         or None if everything's ok
637         """
638         if uri[-1]=='/':uri=uri[:-1]
639         res=deltree(self, uri)
640         parent='/'.join(uri.split('/')[:-1])
641         return res
642
643
644     ###
645     ### MOVE handlers (examples)
646     ###
647
648     def moveone(self, src, dst, overwrite):
649         """ move one resource with Depth=0
650
651         an alternative implementation would be
652
653         result_code=201
654         if overwrite:
655             result_code=204
656             r=os.system("rm -f '%s'" %dst)
657             if r: return 412
658         r=os.system("mv '%s' '%s'" %(src,dst))
659         if r: return 412
660         return result_code
661
662         (untested!). This would not use the davcmd functions
663         and thus can only detect errors directly on the root node.
664         """
665         res=moveone(self, src, dst, overwrite)
666         return res
667
668     def movetree(self, src, dst, overwrite):
669         """ move a collection with Depth=infinity
670
671         an alternative implementation would be
672
673         result_code=201
674         if overwrite:
675             result_code=204
676             r=os.system("rm -rf '%s'" %dst)
677             if r: return 412
678         r=os.system("mv '%s' '%s'" %(src,dst))
679         if r: return 412
680         return result_code
681
682         (untested!). This would not use the davcmd functions
683         and thus can only detect errors directly on the root node"""
684
685         res=movetree(self, src, dst, overwrite)
686         return res
687
688     ###
689     ### COPY handlers
690     ###
691
692     def copyone(self, src, dst, overwrite):
693         """ copy one resource with Depth=0
694
695         an alternative implementation would be
696
697         result_code=201
698         if overwrite:
699             result_code=204
700             r=os.system("rm -f '%s'" %dst)
701             if r: return 412
702         r=os.system("cp '%s' '%s'" %(src,dst))
703         if r: return 412
704         return result_code
705
706         (untested!). This would not use the davcmd functions
707         and thus can only detect errors directly on the root node.
708         """
709         res=copyone(self, src, dst, overwrite)
710         return res
711
712     def copytree(self, src, dst, overwrite):
713         """ copy a collection with Depth=infinity
714
715         an alternative implementation would be
716
717         result_code=201
718         if overwrite:
719             result_code=204
720             r=os.system("rm -rf '%s'" %dst)
721             if r: return 412
722         r=os.system("cp -r '%s' '%s'" %(src,dst))
723         if r: return 412
724         return result_code
725
726         (untested!). This would not use the davcmd functions
727         and thus can only detect errors directly on the root node"""
728         res=copytree(self, src, dst, overwrite)
729         return res
730
731     ###
732     ### copy methods.
733     ### This methods actually copy something. low-level
734     ### They are called by the davcmd utility functions
735     ### copytree and copyone (not the above!)
736     ### Look in davcmd.py for further details.
737     ###
738
739     def copy(self, src, dst):
740         src=urllib.unquote(src)
741         dst=urllib.unquote(dst)
742         ct = self._get_dav_getcontenttype(src)
743         data = self.get_data(src)
744         self.put(dst, data, ct)
745         return 201
746
747     def copycol(self, src, dst):
748         """ copy a collection.
749
750         As this is not recursive (the davserver recurses itself)
751         we will only create a new directory here. For some more
752         advanced systems we might also have to copy properties from
753         the source to the destination.
754         """
755         return self.mkcol(dst)
756
757
758     def exists(self, uri):
759         """ test if a resource exists """
760         result = False
761         cr, uid, pool,dbname, uri2 = self.get_cr(uri)
762         if not dbname:
763             if cr: cr.close()
764             return True
765         try:
766             node = self.uri2object(cr, uid, pool, uri2)
767             if node:
768                 result = True
769         except Exception:
770             pass
771         cr.close()
772         return result
773
774     @memoize(CACHE_SIZE)
775     def is_collection(self, uri):
776         """ test if the given uri is a collection """
777         cr, uid, pool, dbname, uri2 = self.get_cr(uri)
778         try:
779             if not dbname:
780                 return True
781             node = self.uri2object(cr,uid,pool, uri2)
782             if not node:
783                 raise DAV_NotFound2(uri2)
784             if node.type in ('collection','database'):
785                 return True
786             return False
787         finally:
788             if cr: cr.close()
789
790 #eof