1 # -*- encoding: utf-8 -*-
4 from tarfile import filemode
14 from service import security
16 from document.nodes import node_res_dir, node_res_obj
20 logger = netsvc.Logger()
21 logger.notifyChannel('DMS', netsvc.LOG_ERROR, message)
24 def _get_month_name(month):
26 if month==1:return 'Jan'
27 elif month==2:return 'Feb'
28 elif month==3:return 'Mar'
29 elif month==4:return 'Apr'
30 elif month==5:return 'May'
31 elif month==6:return 'Jun'
32 elif month==7:return 'Jul'
33 elif month==8:return 'Aug'
34 elif month==9:return 'Sep'
35 elif month==10:return 'Oct'
36 elif month==11:return 'Nov'
37 elif month==12:return 'Dec'
41 return s.decode('utf-8')
44 return s.decode('latin')
47 return s.encode('ascii')
53 return s.encode('utf-8')
56 return s.encode('latin')
59 return s.decode('ascii')
64 class file_wrapper(StringIO.StringIO):
65 def __init__(self, sstr='', ressource_id=False, dbname=None, uid=1, name=''):
66 StringIO.StringIO.__init__(self, sstr)
67 self.ressource_id = ressource_id
71 def close(self, *args, **kwargs):
72 db,pool = pooler.get_db_and_pool(self.dbname)
78 'datas': base64.encodestring(val),
79 'file_size': len(val),
81 pool.get('ir.attachment').write(cr, self.uid, [self.ressource_id], val2)
85 StringIO.StringIO.close(self, *args, **kwargs)
87 class content_wrapper(StringIO.StringIO):
88 def __init__(self, dbname, uid, pool, node, name=''):
89 StringIO.StringIO.__init__(self, '')
95 def close(self, *args, **kwargs):
96 db,pool = pooler.get_db_and_pool(self.dbname)
100 getattr(self.pool.get('document.directory.content'), 'process_write')(cr, self.uid, self.node, self.getvalue())
104 StringIO.StringIO.close(self, *args, **kwargs)
108 """A class used to interact with the file system, providing a high
109 level, cross-platform interface compatible with both Windows and
110 UNIX style filesystems.
112 It provides some utility methods and some wraps around operations
113 involved in file creation and file system operations like moving
114 files or removing directories.
117 - (str) root: the user home directory.
118 - (str) cwd: the current working directory.
119 - (str) rnfr: source file to be renamed.
124 #return pooler.pool_dic.keys()
125 s = netsvc.ExportService.getService('db')
126 result = s.exp_list()
127 self.db_name_list = []
128 for db_name in result:
132 db = pooler.get_db_only(db_name)
134 cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
135 if not cr.fetchone():
138 cr.execute("select id from ir_module_module where name like 'document%' and state='installed' ")
141 self.db_name_list.append(db_name)
149 # pooler.close_db(db_name)
150 return self.db_name_list
158 # --- Pathname / conversion utilities
161 def ftpnorm(self, ftppath):
162 """Normalize a "virtual" ftp pathname (tipically the raw string
163 coming from client) depending on the current working directory.
165 Example (having "/foo" as current working directory):
168 Note: directory separators are system independent ("/").
169 Pathname returned is always absolutized.
171 if os.path.isabs(ftppath):
172 p = os.path.normpath(ftppath)
174 p = os.path.normpath(os.path.join(self.cwd, ftppath))
175 # normalize string in a standard web-path notation having '/'
177 p = p.replace("\\", "/")
178 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
179 # don't need them. In case we get an UNC path we collapse
180 # redundant separators appearing at the beginning of the string
183 # Anti path traversal: don't trust user input, in the event
184 # that self.cwd is not absolute, return "/" as a safety measure.
185 # This is for extra protection, maybe not really necessary.
186 if not os.path.isabs(p):
191 def ftp2fs(self, path_orig, data):
192 path = self.ftpnorm(path_orig)
193 if not data or (path and path=='/'):
195 path2 = filter(None,path.split('/'))[1:]
196 (cr, uid, pool) = data
198 path2[-1]=_to_unicode(path2[-1])
199 res = pool.get('document.directory').get_object(cr, uid, path2[:])
201 raise OSError(2, 'Not such file or directory.')
205 def fs2ftp(self, node):
208 paths = node.full_path()
209 paths = map(lambda x: '/' +x, paths)
210 res = os.path.normpath(''.join(paths))
211 res = res.replace("\\", "/")
212 while res[:2] == '//':
214 res = '/' + node.context.dbname + '/' + _to_decode(res)
216 #res = node and ('/' + node.cr.dbname + '/' + _to_decode(self.ftpnorm(node.path))) or '/'
220 def validpath(self, path):
221 """Check whether the path belongs to user's home directory.
222 Expected argument is a "real" filesystem pathname.
224 If path is a symbolic link it is resolved to check its real
227 Pathnames escaping from user's root directory are considered
230 return path and True or False
232 # --- Wrapper methods around open() and tempfile.mkstemp
235 def create(self, node, objname, mode):
236 objname = _to_unicode(objname)
239 uid = node.context.uid
240 pool = pooler.get_pool(node.context.dbname)
241 cr = pooler.get_db(node.context.dbname).cursor()
242 child = node.child(cr, objname)
244 if child.type in ('collection','database'):
245 raise OSError(1, 'Operation not permited.')
246 if child.type == 'content':
247 s = content_wrapper(node.context.dbname, uid, pool, child)
249 fobj = pool.get('ir.attachment')
250 ext = objname.find('.') >0 and objname.split('.')[1] or False
252 # TODO: test if already exist and modify in this case if node.type=file
253 ### checked already exits
255 if isinstance(node, node_res_obj):
256 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False
259 object = node.context._dirobj.browse(cr, uid, node.dir_id)
260 where = [('name','=',objname)]
261 if object and (object.type in ('directory')) or object2:
262 where.append(('parent_id','=',object.id))
264 where.append(('parent_id','=',False))
267 where += [('res_id','=',object2.id),('res_model','=',object2._name)]
268 cids = fobj.search(cr, uid,where)
275 'datas_fname': objname,
276 'parent_id' : node.dir_id,
280 'store_method' : (object.storage_id.type == 'filestore' and 'fs')\
281 or (object.storage_id.type == 'db' and 'db')
283 if object and (object.type in ('directory')) or not object2:
284 val['parent_id']= object and object.id or False
287 if 'partner_id' in object2 and object2.partner_id.id:
288 partner = object2.partner_id.id
289 if object2._name == 'res.partner':
292 'res_model': object2._name,
293 'partner_id': partner,
296 cid = fobj.create(cr, uid, val, context={})
299 s = file_wrapper('', cid, node.context.dbname, uid, )
303 raise OSError(1, 'Operation not permited.')
309 def open(self, node, mode):
311 raise OSError(1, 'Operation not permited.')
313 cr = pooler.get_db(node.context.dbname).cursor()
316 if node.type not in ('collection','database'):
317 res = node.open(cr, mode)
322 raise OSError(1, 'Operation not permited.')
325 # ok, but need test more
327 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
328 """A wrap around tempfile.mkstemp creating a file with a unique
329 name. Unlike mkstemp it returns an object with a file-like
332 raise 'Not Yet Implemented'
334 # def __init__(self, fd, name):
337 # def __getattr__(self, attr):
338 # return getattr(self.file, attr)
340 # text = not 'b' in mode
341 # # max number of tries to find out a unique file name
342 # tempfile.TMP_MAX = 50
343 # fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
344 # file = os.fdopen(fd, mode)
345 # return FileWrapper(file, name)
347 text = not 'b' in mode
348 # for unique file , maintain version if duplicate file
352 pool = pooler.get_pool(node.context.dbname)
353 object=dir and dir.object or False
354 object2=dir and dir.object2 or False
355 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)])
357 pre = prefix.split('.')
358 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
359 #prefix = prefix + '.'
360 return self.create(dir,suffix+prefix,text)
365 def chdir(self, path):
369 if path.type in ('collection','database'):
370 self.cwd = self.fs2ftp(path)
371 elif path.type in ('file'):
372 parent_path = path.full_path()[:-1]
373 self.cwd = os.path.normpath(''.join(parent_path))
375 raise OSError(1, 'Operation not permited.')
378 def mkdir(self, node, basename):
379 """Create the specified directory."""
382 raise OSError(1, 'Operation not permited.')
384 basename =_to_unicode(basename)
385 cr = pooler.get_db(node.context.dbname).cursor()
386 uid = node.context.uid
387 pool = pooler.get_pool(node.context.dbname)
389 if isinstance(node, node_res_obj):
390 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False
391 obj = node.context._dirobj.browse(cr, uid, node.dir_id)
392 if obj and (obj.type == 'ressource') and not object2:
393 raise OSError(1, 'Operation not permited.')
396 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
397 'ressource_id': object2 and object2.id or False,
400 if (obj and (obj.type in ('directory'))) or not object2:
401 val['parent_id'] = obj and obj.id or False
402 # Check if it alreayd exists !
403 pool.get('document.directory').create(cr, uid, val)
407 raise OSError(1, 'Operation not permited.')
412 def close_cr(self, data):
417 def get_cr(self, path):
418 path = self.ftpnorm(path)
421 dbname = path.split('/')[1]
422 if dbname not in self.db_list():
425 db,pool = pooler.get_db_and_pool(dbname)
427 raise OSError(1, 'Operation not permited.')
429 uid = security.login(dbname, self.username, self.password)
431 raise OSError(2, 'Authentification Required.')
435 def listdir(self, path):
436 """List the content of a directory."""
437 class false_node(object):
441 def __init__(self, db):
446 for db in self.db_list():
448 uid = security.login(db, self.username, self.password)
450 result.append(false_node(db))
451 except osv.except_osv:
454 cr = pooler.get_db(path.context.dbname).cursor()
455 res = path.children(cr)
460 def rmdir(self, node):
461 """Remove the specified directory."""
463 cr = pooler.get_db(node.context.dbname).cursor()
464 uid = node.context.uid
465 pool = pooler.get_pool(node.context.dbname)
466 object = node.context._dirobj.browse(cr, uid, node.dir_id)
468 raise OSError(2, 'Not such file or directory.')
469 if object._table_name == 'document.directory':
470 if node.children(cr):
471 raise OSError(39, 'Directory not empty.')
472 res = pool.get('document.directory').unlink(cr, uid, [object.id])
474 raise OSError(1, 'Operation not permited.')
480 def remove(self, node):
482 if node.type == 'collection':
483 return self.rmdir(node)
484 elif node.type == 'file':
485 return self.rmfile(node)
486 raise OSError(1, 'Operation not permited.')
488 def rmfile(self, node):
489 """Remove the specified file."""
491 if node.type == 'collection':
492 return self.rmdir(node)
493 uid = node.context.uid
494 pool = pooler.get_pool(node.context.dbname)
495 cr = pooler.get_db(node.context.dbname).cursor()
496 object = pool.get('ir.attachment').browse(cr, uid, node.file_id)
498 raise OSError(2, 'Not such file or directory.')
499 if object._table_name == 'ir.attachment':
500 res = pool.get('ir.attachment').unlink(cr, uid, [object.id])
502 raise OSError(1, 'Operation not permited.')
507 def rename(self, src, dst_basedir, dst_basename):
509 Renaming operation, the effect depends on the src:
510 * A file: read, create and remove
511 * A directory: change the parent and reassign childs to ressource
515 dst_basename = _to_unicode(dst_basename)
516 cr = pooler.get_db(src.context.dbname).cursor()
517 uid = src.context.uid
518 if src.type == 'collection':
521 pool = pooler.get_pool(src.context.dbname)
522 if isinstance(src, node_res_obj):
523 obj2 = src and pool.get(src.context.context['res_model']).browse(cr, uid, src.context.context['res_id']) or False
524 obj = src.context._dirobj.browse(cr, uid, src.dir_id)
525 if isinstance(dst_basedir, node_res_obj):
526 dst_obj2 = dst_basedir and pool.get(dst_basedir.context.context['res_model']).browse(cr, uid, dst_basedir.context.context['res_id']) or False
527 dst_obj = dst_basedir.context._dirobj.browse(cr, uid, dst_basedir.dir_id)
528 if obj._table_name <> 'document.directory':
529 raise OSError(1, 'Operation not permited.')
534 # Compute all childs to set the new ressource ID
536 while len(child_ids):
537 node = child_ids.pop(0)
538 child_ids += node.children(cr)
539 if node.type == 'collection':
541 if isinstance(node, node_res_obj):
542 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False
543 object = node.context._dirobj.browse(cr, uid, node.dir_id)
544 result['directory'].append(object.id)
545 if (not object.ressource_id) and object2:
546 raise OSError(1, 'Operation not permited.')
547 elif node.type == 'file':
548 result['attachment'].append(object.id)
550 if obj2 and not obj.ressource_id:
551 raise OSError(1, 'Operation not permited.')
553 if (dst_obj and (dst_obj.type in ('directory'))) or not dst_obj2:
554 parent_id = dst_obj and dst_obj.id or False
560 ressource_type_id = pool.get('ir.model').search(cr, uid, [('model','=',dst_obj2._name)])[0]
561 ressource_id = dst_obj2.id
562 title = dst_obj2.name
563 ressource_model = dst_obj2._name
564 if dst_obj2._name == 'res.partner':
565 partner_id = dst_obj2.id
567 partner_id = pool.get(dst_obj2._name).fields_get(cr, uid, ['partner_id']) and dst_obj2.partner_id.id or False
569 ressource_type_id = False
571 ressource_model = False
574 pool.get('document.directory').write(cr, uid, result['directory'], {
575 'name' : dst_basename,
576 'ressource_id': ressource_id,
577 'ressource_parent_type_id': ressource_type_id,
578 'parent_id' : parent_id
581 'res_id': ressource_id,
582 'res_model': ressource_model,
584 'partner_id': partner_id
586 pool.get('ir.attachment').write(cr, uid, result['attachment'], val)
587 if (not val['res_id']) and result['attachment']:
588 cr.execute('update ir_attachment set res_id=NULL where id in ('+','.join(map(str,result['attachment']))+')')
592 elif src.type == 'file':
593 pool = pooler.get_pool(src.context.dbname)
594 obj = pool.get('ir.attachment').browse(cr, uid, src.file_id)
596 if isinstance(dst_basedir, node_res_obj):
597 dst_obj2 = dst_basedir and pool.get(dst_basedir.context.context['res_model']).browse(cr, uid, dst_basedir.context.context['res_id']) or False
598 dst_obj = dst_basedir.context._dirobj.browse(cr, uid, dst_basedir.dir_id)
604 'name': dst_basename,
605 'datas_fname': dst_basename,
606 'title': dst_basename,
609 if (dst_obj and (dst_obj.type in ('directory','ressource'))) or not dst_obj2:
610 val['parent_id'] = dst_obj and dst_obj.id or False
612 val['parent_id'] = False
615 val['res_model'] = dst_obj2._name
616 val['res_id'] = dst_obj2.id
617 val['title'] = dst_obj2.name
618 if dst_obj2._name == 'res.partner':
619 val['partner_id'] = dst_obj2.id
621 val['partner_id'] = pool.get(dst_obj2._name).fields_get(cr, uid, ['partner_id']) and dst_obj2.partner_id.id or False
623 # I had to do that because writing False to an integer writes 0 instead of NULL
624 # change if one day we decide to improve osv/fields.py
625 cr.execute('update ir_attachment set res_id=NULL where id=%s', (obj.id,))
627 pool.get('ir.attachment').write(cr, uid, [obj.id], val)
629 elif src.type=='content':
630 src_file = self.open(src,'r')
631 dst_file = self.create(dst_basedir, dst_basename, 'w')
632 dst_file.write(src_file.getvalue())
637 raise OSError(1, 'Operation not permited.')
638 except Exception,err:
640 raise OSError(1,'Operation not permited.')
647 def stat(self, node):
648 r = list(os.stat('/'))
649 if self.isfile(node):
651 r[6] = self.getsize(node)
652 r[7] = self.getmtime(node)
653 r[8] = self.getmtime(node)
654 r[9] = self.getmtime(node)
655 return os.stat_result(r)
658 # --- Wrapper methods around os.path.*
661 def isfile(self, node):
662 if node and (node.type not in ('collection','database')):
667 def islink(self, path):
668 """Return True if path is a symbolic link."""
672 def isdir(self, node):
673 """Return True if path is a directory."""
676 if node and (node.type in ('collection','database')):
681 def getsize(self, node):
682 """Return the size of the specified file in bytes."""
684 if node.type=='file':
685 result = node.content_length or 0L
689 def getmtime(self, node):
690 """Return the last modified time as a number of seconds since
693 if node.write_date or node.create_date:
694 dt = (node.write_date or node.create_date)[:19]
695 result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
697 result = time.mktime(time.localtime())
701 def realpath(self, path):
702 """Return the canonical version of path eliminating any
703 symbolic links encountered in the path (if they are
704 supported by the operating system).
709 def lexists(self, path):
710 """Return True if path refers to an existing path, including
711 a broken or circular symbolic link.
713 return path and True or False
716 # Ok, can be improved
717 def glob1(self, dirname, pattern):
718 """Return a list of files matching a dirname pattern
721 Unlike glob.glob1 raises exception if os.listdir() fails.
723 names = self.listdir(dirname)
724 if pattern[0] != '.':
725 names = filter(lambda x: x.path[0] != '.', names)
726 return fnmatch.filter(names, pattern)
728 # --- Listing utilities
730 # note: the following operations are no more blocking
733 def get_list_dir(self, path):
734 """"Return an iterator object that yields a directory listing
735 in a form suitable for LIST command.
738 listing = self.listdir(path)
740 return self.format_list(path and path.path or '/', listing)
741 # if path is a file or a symlink we return information about it
742 elif self.isfile(path):
743 basedir, filename = os.path.split(path.path)
744 self.lstat(path) # raise exc in case of problems
745 return self.format_list(basedir, [path])
749 def get_stat_dir(self, rawline, datacr):
750 """Return an iterator object that yields a list of files
751 matching a dirname pattern non-recursively in a form
752 suitable for STAT command.
754 - (str) rawline: the raw string passed by client as command
757 ftppath = self.ftpnorm(rawline)
758 if not glob.has_magic(ftppath):
759 return self.get_list_dir(self.ftp2fs(rawline, datacr))
761 basedir, basename = os.path.split(ftppath)
762 if glob.has_magic(basedir):
763 return iter(['Directory recursion not supported.\r\n'])
765 basedir = self.ftp2fs(basedir, datacr)
766 listing = self.glob1(basedir, basename)
769 return self.format_list(basedir, listing)
772 def format_list(self, basedir, listing, ignore_err=True):
773 """Return an iterator object that yields the entries of given
774 directory emulating the "/bin/ls -lA" UNIX command output.
776 - (str) basedir: the absolute dirname.
777 - (list) listing: the names of the entries in basedir
778 - (bool) ignore_err: when False raise exception if os.lstat()
781 On platforms which do not support the pwd and grp modules (such
782 as Windows), ownership is printed as "owner" and "group" as a
783 default, and number of hard links is always "1". On UNIX
784 systems, the actual owner, group, and number of links are
787 This is how output appears to client:
789 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
790 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
791 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
795 st = self.lstat(file)
800 perms = filemode(st.st_mode) # permissions
801 nlinks = st.st_nlink # number of links to inode
802 if not nlinks: # non-posix system, let's use a bogus value
804 size = st.st_size # file size
807 # stat.st_mtime could fail (-1) if last mtime is too old
808 # in which case we return the local time as last mtime
810 mname=_get_month_name(time.strftime("%m", time.localtime(st.st_mtime)))
811 mtime = mname+' '+time.strftime("%d %H:%M", time.localtime(st.st_mtime))
813 mname=_get_month_name(time.strftime("%m"))
814 mtime = mname+' '+time.strftime("%d %H:%M")
815 # formatting is matched with proftpd ls output
816 path=_to_decode(file.path) #file.path.encode('ascii','replace').replace('?','_')
817 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
818 size, mtime, path.split('/')[-1])
821 def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
822 """Return an iterator object that yields the entries of a given
823 directory or of a single file in a form suitable with MLSD and
826 Every entry includes a list of "facts" referring the listed
827 element. See RFC-3659, chapter 7, to see what every single
830 - (str) basedir: the absolute dirname.
831 - (list) listing: the names of the entries in basedir
832 - (str) perms: the string referencing the user permissions.
833 - (str) facts: the list of "facts" to be returned.
834 - (bool) ignore_err: when False raise exception if os.stat()
837 Note that "facts" returned may change depending on the platform
838 and on what user specified by using the OPTS command.
840 This is how output could appear to the client issuing
843 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
844 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
845 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
847 permdir = ''.join([x for x in perms if x not in 'arw'])
848 permfile = ''.join([x for x in perms if x not in 'celmp'])
849 if ('w' in perms) or ('a' in perms) or ('f' in perms):
853 type = size = perm = modify = create = unique = mode = uid = gid = ""
862 if stat.S_ISDIR(st.st_mode):
866 perm = 'perm=%s;' %permdir
871 perm = 'perm=%s;' %permfile
873 size = 'size=%s;' %st.st_size # file size
874 # last modification time
875 if 'modify' in facts:
877 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
878 time.localtime(st.st_mtime))
880 # stat.st_mtime could fail (-1) if last mtime is too old
882 if 'create' in facts:
883 # on Windows we can provide also the creation time
885 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
886 time.localtime(st.st_ctime))
890 if 'unix.mode' in facts:
891 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
892 if 'unix.uid' in facts:
893 uid = 'unix.uid=%s;' %st.st_uid
894 if 'unix.gid' in facts:
895 gid = 'unix.gid=%s;' %st.st_gid
896 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
897 # posix platforms only; we get it by mixing st_dev and
898 # st_ino values which should be enough for granting an
899 # uniqueness for the file listed.
900 # The same approach is used by pure-ftpd.
901 # Implementors who want to provide unique fact on other
902 # platforms should use some platform-specific method (e.g.
903 # on Windows NTFS filesystems MTF records could be used).
904 if 'unique' in facts:
905 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
906 path=_to_decode(file.path)
907 path = path and path.split('/')[-1] or None
908 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
909 mode, uid, gid, unique, path)
911 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: