1 # -*- encoding: utf-8 -*-
4 from tarfile import filemode
14 from service import security
17 logger = netsvc.Logger()
18 logger.notifyChannel('DMS', netsvc.LOG_ERROR, message)
21 def _get_month_name(month):
23 if month==1:return 'Jan'
24 elif month==2:return 'Feb'
25 elif month==3:return 'Mar'
26 elif month==4:return 'Apr'
27 elif month==5:return 'May'
28 elif month==6:return 'Jun'
29 elif month==7:return 'Jul'
30 elif month==8:return 'Aug'
31 elif month==9:return 'Sep'
32 elif month==10:return 'Oct'
33 elif month==11:return 'Nov'
34 elif month==12:return 'Dec'
38 return s.decode('utf-8')
41 return s.decode('latin')
44 return s.encode('ascii')
50 return s.encode('utf-8')
53 return s.encode('latin')
56 return s.decode('ascii')
61 class file_wrapper(StringIO.StringIO):
62 def __init__(self, sstr='', ressource_id=False, dbname=None, uid=1, name=''):
63 StringIO.StringIO.__init__(self, sstr)
64 self.ressource_id = ressource_id
68 def close(self, *args, **kwargs):
69 db,pool = pooler.get_db_and_pool(self.dbname)
75 'datas': base64.encodestring(val),
76 'file_size': len(val),
78 pool.get('ir.attachment').write(cr, self.uid, [self.ressource_id], val2)
82 StringIO.StringIO.close(self, *args, **kwargs)
84 class content_wrapper(StringIO.StringIO):
85 def __init__(self, dbname, uid, pool, node, name=''):
86 StringIO.StringIO.__init__(self, '')
92 def close(self, *args, **kwargs):
93 db,pool = pooler.get_db_and_pool(self.dbname)
97 getattr(self.pool.get('document.directory.content'), 'process_write_'+self.node.content.extension[1:])(cr, self.uid, self.node, self.getvalue())
101 StringIO.StringIO.close(self, *args, **kwargs)
105 """A class used to interact with the file system, providing a high
106 level, cross-platform interface compatible with both Windows and
107 UNIX style filesystems.
109 It provides some utility methods and some wraps around operations
110 involved in file creation and file system operations like moving
111 files or removing directories.
114 - (str) root: the user home directory.
115 - (str) cwd: the current working directory.
116 - (str) rnfr: source file to be renamed.
121 #return pooler.pool_dic.keys()
122 s = netsvc.LocalService('db')
124 self.db_name_list = []
125 for db_name in result:
128 db = pooler.get_db_only(db_name)
130 cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
131 if not cr.fetchone():
134 cr.execute("select id from ir_module_module where name like 'document%' and state='installed' ")
137 self.db_name_list.append(db_name)
147 # pooler.close_db(db_name)
148 return self.db_name_list
156 # --- Pathname / conversion utilities
159 def ftpnorm(self, ftppath):
160 """Normalize a "virtual" ftp pathname (tipically the raw string
161 coming from client) depending on the current working directory.
163 Example (having "/foo" as current working directory):
166 Note: directory separators are system independent ("/").
167 Pathname returned is always absolutized.
169 if os.path.isabs(ftppath):
170 p = os.path.normpath(ftppath)
172 p = os.path.normpath(os.path.join(self.cwd, ftppath))
173 # normalize string in a standard web-path notation having '/'
175 p = p.replace("\\", "/")
176 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
177 # don't need them. In case we get an UNC path we collapse
178 # redundant separators appearing at the beginning of the string
181 # Anti path traversal: don't trust user input, in the event
182 # that self.cwd is not absolute, return "/" as a safety measure.
183 # This is for extra protection, maybe not really necessary.
184 if not os.path.isabs(p):
189 def ftp2fs(self, path_orig, data):
190 path = self.ftpnorm(path_orig)
191 if path and path=='/':
193 path2 = filter(None,path.split('/'))[1:]
194 (cr, uid, pool) = data
196 path2[-1]=_to_unicode(path2[-1])
197 res = pool.get('document.directory').get_object(cr, uid, path2[:])
199 raise OSError(2, 'Not such file or directory.')
203 def fs2ftp(self, node):
206 res=os.path.normpath(node.path)
207 res = res.replace("\\", "/")
208 while res[:2] == '//':
210 res='/' + node.cr.dbname + '/' + _to_decode(res)
212 #res = node and ('/' + node.cr.dbname + '/' + _to_decode(self.ftpnorm(node.path))) or '/'
216 def validpath(self, path):
217 """Check whether the path belongs to user's home directory.
218 Expected argument is a "real" filesystem pathname.
220 If path is a symbolic link it is resolved to check its real
223 Pathnames escaping from user's root directory are considered
226 return path and True or False
228 # --- Wrapper methods around open() and tempfile.mkstemp
231 def create(self, node, objname, mode):
232 objname=_to_unicode(objname)
235 pool = pooler.get_pool(cr.dbname)
236 child = node.child(objname)
238 if child.type in ('collection','database'):
239 raise OSError(1, 'Operation not permited.')
240 if child.type=='content':
241 s = content_wrapper(cr.dbname, uid, pool, child)
244 fobj = pool.get('ir.attachment')
245 ext = objname.find('.') >0 and objname.split('.')[1] or False
247 # TODO: test if already exist and modify in this case if node.type=file
248 ### checked already exits
249 object2=node and node.object2 or False
250 object=node and node.object or False
253 where=[('name','=',objname)]
254 if object and (object.type in ('directory')) or object2:
255 where.append(('parent_id','=',object.id))
257 where.append(('parent_id','=',False))
260 where +=[('res_id','=',object2.id),('res_model','=',object2._name)]
261 cids = fobj.search(cr, uid,where)
268 'datas_fname': objname,
273 if object and (object.type in ('directory')) or not object2:
274 val['parent_id']= object and object.id or False
277 if 'partner_id' in object2 and object2.partner_id.id:
278 partner = object2.partner_id.id
279 if object2._name == 'res.partner':
282 'res_model': object2._name,
283 'partner_id': partner,
286 cid = fobj.create(cr, uid, val, context={})
289 s = file_wrapper('', cid, cr.dbname, uid, )
293 raise OSError(1, 'Operation not permited.')
296 def open(self, node, mode):
298 raise OSError(1, 'Operation not permited.')
300 if node.type=='file':
301 if not self.isfile(node):
302 raise OSError(1, 'Operation not permited.')
303 s = StringIO.StringIO(base64.decodestring(node.object.datas or ''))
306 elif node.type=='content':
309 pool = pooler.get_pool(cr.dbname)
310 return getattr(pool.get('document.directory.content'), 'process_read_'+node.content.extension[1:])(cr, uid, node)
312 raise OSError(1, 'Operation not permited.')
314 # ok, but need test more
316 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
317 """A wrap around tempfile.mkstemp creating a file with a unique
318 name. Unlike mkstemp it returns an object with a file-like
321 raise 'Not Yet Implemented'
323 # def __init__(self, fd, name):
326 # def __getattr__(self, attr):
327 # return getattr(self.file, attr)
329 # text = not 'b' in mode
330 # # max number of tries to find out a unique file name
331 # tempfile.TMP_MAX = 50
332 # fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
333 # file = os.fdopen(fd, mode)
334 # return FileWrapper(file, name)
336 text = not 'b' in mode
337 # for unique file , maintain version if duplicate file
341 pool = pooler.get_pool(cr.dbname)
342 object=dir and dir.object or False
343 object2=dir and dir.object2 or False
344 res=pool.get('ir.attachment').search(cr,uid,[('name','like',prefix),('parent_id','=',object and object.type in ('directory','ressource') and object.id or False),('res_id','=',object2 and object2.id or False),('res_model','=',object2 and object2._name or False)])
346 pre = prefix.split('.')
347 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
348 #prefix = prefix + '.'
349 return self.create(dir,suffix+prefix,text)
354 def chdir(self, path):
358 if path.type in ('collection','database'):
359 self.cwd = self.fs2ftp(path)
361 raise OSError(1, 'Operation not permited.')
364 def mkdir(self, node, basename):
365 """Create the specified directory."""
367 raise OSError(1, 'Operation not permited.')
369 basename=_to_unicode(basename)
370 object2=node and node.object2 or False
371 object=node and node.object or False
374 pool = pooler.get_pool(cr.dbname)
375 if node.object and (node.object.type=='ressource') and not node.object2:
376 raise OSError(1, 'Operation not permited.')
379 'ressource_parent_type_id': object and object.ressource_type_id.id or False,
380 'ressource_id': object2 and object2.id or False
382 if (object and (object.type in ('directory'))) or not object2:
383 val['parent_id'] = object and object.id or False
384 # Check if it alreayd exists !
385 pool.get('document.directory').create(cr, uid, val)
389 raise OSError(1, 'Operation not permited.')
393 def close_cr(self, data):
398 def get_cr(self, path):
399 path = self.ftpnorm(path)
402 dbname = path.split('/')[1]
404 db,pool = pooler.get_db_and_pool(dbname)
406 raise OSError(1, 'Operation not permited.')
408 uid = security.login(dbname, self.username, self.password)
410 raise OSError(2, 'Authentification Required.')
414 def listdir(self, path):
415 """List the content of a directory."""
419 def __init__(self, db):
424 for db in self.db_list():
425 result.append(false_node(db))
427 return path.children()
430 def rmdir(self, node):
431 """Remove the specified directory."""
434 pool = pooler.get_pool(cr.dbname)
435 object2=node and node.object2 or False
436 object=node and node.object or False
437 if object._table_name=='document.directory':
439 raise OSError(39, 'Directory not empty.')
440 res = pool.get('document.directory').unlink(cr, uid, [object.id])
442 raise OSError(39, 'Directory not empty.')
447 def remove(self, node):
448 """Remove the specified file."""
451 pool = pooler.get_pool(cr.dbname)
452 object2=node and node.object2 or False
453 object=node and node.object or False
455 raise OSError(2, 'Not such file or directory.')
456 if object._table_name=='ir.attachment':
457 res = pool.get('ir.attachment').unlink(cr, uid, [object.id])
459 raise OSError(1, 'Operation not permited.')
463 def rename(self, src, dst_basedir,dst_basename):
465 Renaming operation, the effect depends on the src:
466 * A file: read, create and remove
467 * A directory: change the parent and reassign childs to ressource
470 dst_basename=_to_unicode(dst_basename)
471 if src.type=='collection':
472 if src.object._table_name <> 'document.directory':
473 raise OSError(1, 'Operation not permited.')
478 # Compute all childs to set the new ressource ID
480 while len(child_ids):
481 node = child_ids.pop(0)
482 child_ids += node.children()
483 if node.type =='collection':
484 result['directory'].append(node.object.id)
485 if (not node.object.ressource_id) and node.object2:
486 raise OSError(1, 'Operation not permited.')
487 elif node.type =='file':
488 result['attachment'].append(node.object.id)
492 pool = pooler.get_pool(cr.dbname)
493 object2=src and src.object2 or False
494 object=src and src.object or False
495 if object2 and not object.ressource_id:
496 raise OSError(1, 'Operation not permited.')
500 if (dst_basedir.object and (dst_basedir.object.type in ('directory'))) or not dst_basedir.object2:
501 val['parent_id'] = dst_basedir.object and dst_basedir.object.id or False
503 val['parent_id'] = False
504 res = pool.get('document.directory').write(cr, uid, [object.id],val)
506 if dst_basedir.object2:
507 ressource_type_id = pool.get('ir.model').search(cr,uid,[('model','=',dst_basedir.object2._name)])[0]
508 ressource_id = dst_basedir.object2.id
509 title = dst_basedir.object2.name
510 ressource_model = dst_basedir.object2._name
511 if dst_basedir.object2._name=='res.partner':
512 partner_id=dst_basedir.object2.id
514 obj2=pool.get(dst_basedir.object2._name)
515 partner_id= obj2.fields_get(cr,uid,['partner_id']) and dst_basedir.object2.partner_id.id or False
517 ressource_type_id = False
519 ressource_model = False
523 pool.get('document.directory').write(cr, uid, result['directory'], {
524 'ressource_id': ressource_id,
525 'ressource_type_id': ressource_type_id
528 'res_id': ressource_id,
529 'res_model': ressource_model,
531 'partner_id': partner_id
533 pool.get('ir.attachment').write(cr, uid, result['attachment'], val)
534 if (not val['res_id']) and result['attachment']:
535 dst_basedir.cr.execute('update ir_attachment set res_id=NULL where id in ('+','.join(map(str,result['attachment']))+')')
538 elif src.type=='file':
539 pool = pooler.get_pool(src.cr.dbname)
544 'name': dst_basename,
545 'datas_fname': dst_basename,
546 'title': dst_basename,
549 if (dst_basedir.object and (dst_basedir.object.type in ('directory','ressource'))) or not dst_basedir.object2:
550 val['parent_id'] = dst_basedir.object and dst_basedir.object.id or False
552 val['parent_id'] = False
554 if dst_basedir.object2:
555 val['res_model'] = dst_basedir.object2._name
556 val['res_id'] = dst_basedir.object2.id
557 val['title'] = dst_basedir.object2.name
558 if dst_basedir.object2._name=='res.partner':
559 val['partner_id']=dst_basedir.object2.id
561 obj2=pool.get(dst_basedir.object2._name)
562 val['partner_id']= obj2.fields_get(cr,uid,['partner_id']) and dst_basedir.object2.partner_id.id or False
563 elif src.object.res_id:
564 # I had to do that because writing False to an integer writes 0 instead of NULL
565 # change if one day we decide to improve osv/fields.py
566 dst_basedir.cr.execute('update ir_attachment set res_id=NULL where id=%s', (src.object.id,))
568 pool.get('ir.attachment').write(src.cr, src.uid, [src.object.id], val)
570 elif src.type=='content':
571 src_file=self.open(src,'r')
572 dst_file=self.create(dst_basedir,dst_basename,'w')
573 dst_file.write(src_file.getvalue())
578 raise OSError(1, 'Operation not permited.')
579 except Exception,err:
581 raise OSError(1,'Operation not permited.')
587 def stat(self, node):
588 r = list(os.stat('/'))
589 if self.isfile(node):
591 r[6] = self.getsize(node)
592 r[7] = self.getmtime(node)
593 r[8] = self.getmtime(node)
594 r[9] = self.getmtime(node)
595 return os.stat_result(r)
598 # --- Wrapper methods around os.path.*
601 def isfile(self, node):
602 if node and (node.type not in ('collection','database')):
607 def islink(self, path):
608 """Return True if path is a symbolic link."""
612 def isdir(self, node):
613 """Return True if path is a directory."""
616 if node and (node.type in ('collection','database')):
621 def getsize(self, node):
622 """Return the size of the specified file in bytes."""
624 if node.type=='file':
625 result = node.object.file_size or 0L
629 def getmtime(self, node):
630 """Return the last modified time as a number of seconds since
632 if node.object and node.type<>'content':
633 dt = (node.object.write_date or node.object.create_date)[:19]
634 result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
636 result = time.mktime(time.localtime())
640 def realpath(self, path):
641 """Return the canonical version of path eliminating any
642 symbolic links encountered in the path (if they are
643 supported by the operating system).
648 def lexists(self, path):
649 """Return True if path refers to an existing path, including
650 a broken or circular symbolic link.
652 return path and True or False
655 # Ok, can be improved
656 def glob1(self, dirname, pattern):
657 """Return a list of files matching a dirname pattern
660 Unlike glob.glob1 raises exception if os.listdir() fails.
662 names = self.listdir(dirname)
663 if pattern[0] != '.':
664 names = filter(lambda x: x.path[0] != '.', names)
665 return fnmatch.filter(names, pattern)
667 # --- Listing utilities
669 # note: the following operations are no more blocking
672 def get_list_dir(self, path):
673 """"Return an iterator object that yields a directory listing
674 in a form suitable for LIST command.
677 listing = self.listdir(path)
679 return self.format_list(path and path.path or '/', listing)
680 # if path is a file or a symlink we return information about it
681 elif self.isfile(path):
682 basedir, filename = os.path.split(path.path)
683 self.lstat(path) # raise exc in case of problems
684 return self.format_list(basedir, [path])
688 def get_stat_dir(self, rawline, datacr):
689 """Return an iterator object that yields a list of files
690 matching a dirname pattern non-recursively in a form
691 suitable for STAT command.
693 - (str) rawline: the raw string passed by client as command
696 ftppath = self.ftpnorm(rawline)
697 if not glob.has_magic(ftppath):
698 return self.get_list_dir(self.ftp2fs(rawline, datacr))
700 basedir, basename = os.path.split(ftppath)
701 if glob.has_magic(basedir):
702 return iter(['Directory recursion not supported.\r\n'])
704 basedir = self.ftp2fs(basedir, datacr)
705 listing = self.glob1(basedir, basename)
708 return self.format_list(basedir, listing)
711 def format_list(self, basedir, listing, ignore_err=True):
712 """Return an iterator object that yields the entries of given
713 directory emulating the "/bin/ls -lA" UNIX command output.
715 - (str) basedir: the absolute dirname.
716 - (list) listing: the names of the entries in basedir
717 - (bool) ignore_err: when False raise exception if os.lstat()
720 On platforms which do not support the pwd and grp modules (such
721 as Windows), ownership is printed as "owner" and "group" as a
722 default, and number of hard links is always "1". On UNIX
723 systems, the actual owner, group, and number of links are
726 This is how output appears to client:
728 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
729 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
730 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
734 st = self.lstat(file)
739 perms = filemode(st.st_mode) # permissions
740 nlinks = st.st_nlink # number of links to inode
741 if not nlinks: # non-posix system, let's use a bogus value
743 size = st.st_size # file size
746 # stat.st_mtime could fail (-1) if last mtime is too old
747 # in which case we return the local time as last mtime
749 mname=_get_month_name(time.strftime("%m", time.localtime(st.st_mtime)))
750 mtime = mname+' '+time.strftime("%d %H:%M", time.localtime(st.st_mtime))
752 mname=_get_month_name(time.strftime("%m"))
753 mtime = mname+' '+time.strftime("%d %H:%M")
754 # formatting is matched with proftpd ls output
755 path=_to_decode(file.path) #file.path.encode('ascii','replace').replace('?','_')
756 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
757 size, mtime, path.split('/')[-1])
760 def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
761 """Return an iterator object that yields the entries of a given
762 directory or of a single file in a form suitable with MLSD and
765 Every entry includes a list of "facts" referring the listed
766 element. See RFC-3659, chapter 7, to see what every single
769 - (str) basedir: the absolute dirname.
770 - (list) listing: the names of the entries in basedir
771 - (str) perms: the string referencing the user permissions.
772 - (str) facts: the list of "facts" to be returned.
773 - (bool) ignore_err: when False raise exception if os.stat()
776 Note that "facts" returned may change depending on the platform
777 and on what user specified by using the OPTS command.
779 This is how output could appear to the client issuing
782 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
783 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
784 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
786 permdir = ''.join([x for x in perms if x not in 'arw'])
787 permfile = ''.join([x for x in perms if x not in 'celmp'])
788 if ('w' in perms) or ('a' in perms) or ('f' in perms):
792 type = size = perm = modify = create = unique = mode = uid = gid = ""
793 for basename in listing:
794 file = os.path.join(basedir, basename)
802 if stat.S_ISDIR(st.st_mode):
806 elif basename == '..':
811 perm = 'perm=%s;' %permdir
816 perm = 'perm=%s;' %permfile
818 size = 'size=%s;' %st.st_size # file size
819 # last modification time
820 if 'modify' in facts:
822 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
823 time.localtime(st.st_mtime))
825 # stat.st_mtime could fail (-1) if last mtime is too old
827 if 'create' in facts:
828 # on Windows we can provide also the creation time
830 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
831 time.localtime(st.st_ctime))
835 if 'unix.mode' in facts:
836 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
837 if 'unix.uid' in facts:
838 uid = 'unix.uid=%s;' %st.st_uid
839 if 'unix.gid' in facts:
840 gid = 'unix.gid=%s;' %st.st_gid
841 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
842 # posix platforms only; we get it by mixing st_dev and
843 # st_ino values which should be enough for granting an
844 # uniqueness for the file listed.
845 # The same approach is used by pure-ftpd.
846 # Implementors who want to provide unique fact on other
847 # platforms should use some platform-specific method (e.g.
848 # on Windows NTFS filesystems MTF records could be used).
849 if 'unique' in facts:
850 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
851 basename=_to_decode(basename)
852 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
853 mode, uid, gid, unique, basename)
855 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: