1 # -*- encoding: utf-8 -*-
4 from tarfile import filemode
15 from service import security
17 #from document.nodes import node_res_dir, node_res_obj
18 from document.nodes import get_node_context
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')
60 class abstracted_fs(object):
61 """A class used to interact with the file system, providing a high
62 level, cross-platform interface compatible with both Windows and
63 UNIX style filesystems.
65 It provides some utility methods and some wraps around operations
66 involved in file creation and file system operations like moving
67 files or removing directories.
70 - (str) root: the user home directory.
71 - (str) cwd: the current working directory.
72 - (str) rnfr: source file to be renamed.
77 #return pooler.pool_dic.keys()
78 s = netsvc.ExportService.getService('db')
79 result = s.exp_list(document=True)
80 self.db_name_list = []
81 for db_name in result:
85 db = pooler.get_db_only(db_name)
87 cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
91 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document_ftp' AND state='installed' ")
94 self.db_name_list.append(db_name)
97 self._log.warning('Cannot use db "%s"', db_name)
102 # pooler.close_db(db_name)
103 return self.db_name_list
110 self._log = logging.getLogger('FTP.fs')
112 # --- Pathname / conversion utilities
115 def ftpnorm(self, ftppath):
116 """Normalize a "virtual" ftp pathname (tipically the raw string
117 coming from client) depending on the current working directory.
119 Example (having "/foo" as current working directory):
122 Note: directory separators are system independent ("/").
123 Pathname returned is always absolutized.
125 if os.path.isabs(ftppath):
126 p = os.path.normpath(ftppath)
128 p = os.path.normpath(os.path.join(self.cwd, ftppath))
129 # normalize string in a standard web-path notation having '/'
131 p = p.replace("\\", "/")
132 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
133 # don't need them. In case we get an UNC path we collapse
134 # redundant separators appearing at the beginning of the string
137 # Anti path traversal: don't trust user input, in the event
138 # that self.cwd is not absolute, return "/" as a safety measure.
139 # This is for extra protection, maybe not really necessary.
140 if not os.path.isabs(p):
145 def ftp2fs(self, path_orig, data):
146 path = self.ftpnorm(path_orig)
147 if not data or (path and path=='/'):
149 path2 = filter(None,path.split('/'))[1:]
152 path2[-1]=_to_unicode(path2[-1])
154 ctx = get_node_context(cr, uid, {})
155 res = ctx.get_uri(cr, path2[:])
157 raise OSError(2, 'Not such file or directory.')
161 def fs2ftp(self, node):
164 paths = node.full_path()
165 paths = map(lambda x: '/' +x, paths)
166 res = os.path.normpath(''.join(paths))
167 res = res.replace("\\", "/")
168 while res[:2] == '//':
170 res = '/' + node.context.dbname + '/' + _to_decode(res)
172 #res = node and ('/' + node.cr.dbname + '/' + _to_decode(self.ftpnorm(node.path))) or '/'
176 def validpath(self, path):
177 """Check whether the path belongs to user's home directory.
178 Expected argument is a "real" filesystem pathname.
180 If path is a symbolic link it is resolved to check its real
183 Pathnames escaping from user's root directory are considered
186 return path and True or False
188 # --- Wrapper methods around open() and tempfile.mkstemp
191 def create(self, node, objname, mode):
192 objname = _to_unicode(objname)
195 uid = node.context.uid
196 pool = pooler.get_pool(node.context.dbname)
197 cr = pooler.get_db(node.context.dbname).cursor()
198 child = node.child(cr, objname)
200 if child.type in ('collection','database'):
201 raise OSError(1, 'Operation not permited.')
202 if child.type == 'content':
203 s = content_wrapper(node.context.dbname, uid, pool, child)
205 fobj = pool.get('ir.attachment')
206 ext = objname.find('.') >0 and objname.split('.')[1] or False
208 # TODO: test if already exist and modify in this case if node.type=file
209 ### checked already exits
211 if isinstance(node, node_res_obj):
212 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False
215 object = node.context._dirobj.browse(cr, uid, node.dir_id)
216 where = [('name','=',objname)]
217 if object and (object.type in ('directory')) or object2:
218 where.append(('parent_id','=',object.id))
220 where.append(('parent_id','=',False))
223 where += [('res_id','=',object2.id),('res_model','=',object2._name)]
224 cids = fobj.search(cr, uid, where)
231 'datas_fname': objname,
232 'parent_id' : node.dir_id,
236 'store_method' : (object.storage_id.type == 'filestore' and 'fs')\
237 or (object.storage_id.type == 'db' and 'db')
239 if object and (object.type in ('directory')) or not object2:
240 val['parent_id']= object and object.id or False
243 if 'partner_id' in object2 and object2.partner_id.id:
244 partner = object2.partner_id.id
245 if object2._name == 'res.partner':
248 'res_model': object2._name,
249 'partner_id': partner,
252 cid = fobj.create(cr, uid, val, context={})
255 s = file_wrapper('', cid, node.context.dbname, uid, )
258 self._log.exception('Cannot create item %s at node %s', objname, repr(node))
259 raise OSError(1, 'Operation not permited.')
264 def open(self, node, mode):
266 raise OSError(1, 'Operation not permited.')
268 cr = pooler.get_db(node.context.dbname).cursor()
270 res = node.open_data(cr, mode)
275 # ok, but need test more
277 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
278 """A wrap around tempfile.mkstemp creating a file with a unique
279 name. Unlike mkstemp it returns an object with a file-like
282 raise 'Not Yet Implemented'
284 # def __init__(self, fd, name):
287 # def __getattr__(self, attr):
288 # return getattr(self.file, attr)
290 # text = not 'b' in mode
291 # # max number of tries to find out a unique file name
292 # tempfile.TMP_MAX = 50
293 # fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
294 # file = os.fdopen(fd, mode)
295 # return FileWrapper(file, name)
297 text = not 'b' in mode
298 # for unique file , maintain version if duplicate file
302 pool = pooler.get_pool(node.context.dbname)
303 object=dir and dir.object or False
304 object2=dir and dir.object2 or False
305 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)])
307 pre = prefix.split('.')
308 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
309 #prefix = prefix + '.'
310 return self.create(dir,suffix+prefix,text)
315 def chdir(self, path):
319 if path.type in ('collection','database'):
320 self.cwd = self.fs2ftp(path)
321 elif path.type in ('file'):
322 parent_path = path.full_path()[:-1]
323 self.cwd = os.path.normpath(''.join(parent_path))
325 raise OSError(1, 'Operation not permited.')
328 def mkdir(self, node, basename):
329 """Create the specified directory."""
332 raise OSError(1, 'Operation not permited.')
334 basename =_to_unicode(basename)
335 cr = pooler.get_db(node.context.dbname).cursor()
336 uid = node.context.uid
337 pool = pooler.get_pool(node.context.dbname)
339 if isinstance(node, node_res_obj):
340 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False
341 obj = node.context._dirobj.browse(cr, uid, node.dir_id)
342 if obj and (obj.type == 'ressource') and not object2:
343 raise OSError(1, 'Operation not permited.')
346 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
347 'ressource_id': object2 and object2.id or False,
350 if (obj and (obj.type in ('directory'))) or not object2:
351 val['parent_id'] = obj and obj.id or False
352 # Check if it alreayd exists !
353 pool.get('document.directory').create(cr, uid, val)
356 self._log.exception('Cannot create dir "%s" at node %s', basename, repr(node))
357 raise OSError(1, 'Operation not permited.')
361 def close_cr(self, data):
366 def get_cr(self, path):
367 path = self.ftpnorm(path)
370 dbname = path.split('/')[1]
371 if dbname not in self.db_list():
374 db = pooler.get_db(dbname)
376 raise OSError(1, 'Operation not permited.')
379 uid = security.login(dbname, self.username, self.password)
385 raise OSError(2, 'Authentification Required.')
388 def get_node_cr_uid(self, node):
389 """ Get cr, uid, pool from a node
391 db = pooler.get_db(node.context.dbname)
392 return db.cursor(), node.context.uid
394 def get_node_cr(self, node):
395 """ Get the cursor for the database of a node
397 The cursor is the only thing that a node will not store
398 persistenly, so we have to obtain a new one for each call.
400 return self.get_node_cr_uid(node)[0]
402 def listdir(self, path):
403 """List the content of a directory."""
404 class false_node(object):
408 def __init__(self, db):
413 for db in self.db_list():
415 result.append(false_node(db))
416 except osv.except_osv:
419 cr = self.get_node_cr(path)
420 res = path.children(cr)
424 def rmdir(self, node):
425 """Remove the specified directory."""
427 cr = self.get_node_cr(node)
434 def remove(self, node):
436 if node.type == 'collection':
437 return self.rmdir(node)
438 elif node.type == 'file':
439 return self.rmfile(node)
440 raise OSError(1, 'Operation not permited.')
442 def rmfile(self, node):
443 """Remove the specified file."""
445 cr = self.get_node_cr(node)
453 def rename(self, src, dst_basedir, dst_basename):
455 Renaming operation, the effect depends on the src:
456 * A file: read, create and remove
457 * A directory: change the parent and reassign childs to ressource
461 # FIXME! wrong code here, doesn't use the node API
462 dst_basename = _to_unicode(dst_basename)
463 cr = pooler.get_db(src.context.dbname).cursor()
464 uid = src.context.uid
465 if src.type == 'collection':
468 pool = pooler.get_pool(src.context.dbname)
469 if isinstance(src, node_res_obj):
470 obj2 = src and pool.get(src.context.context['res_model']).browse(cr, uid, src.context.context['res_id']) or False
471 obj = src.context._dirobj.browse(cr, uid, src.dir_id)
472 if isinstance(dst_basedir, node_res_obj):
473 dst_obj2 = dst_basedir and pool.get(dst_basedir.context.context['res_model']).browse(cr, uid, dst_basedir.context.context['res_id']) or False
474 dst_obj = dst_basedir.context._dirobj.browse(cr, uid, dst_basedir.dir_id)
475 if obj._table_name <> 'document.directory':
476 raise OSError(1, 'Operation not permited.')
481 # Compute all childs to set the new ressource ID
483 while len(child_ids):
484 node = child_ids.pop(0)
485 child_ids += node.children(cr)
486 if node.type == 'collection':
488 if isinstance(node, node_res_obj):
489 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False
490 object = node.context._dirobj.browse(cr, uid, node.dir_id)
491 result['directory'].append(object.id)
492 if (not object.ressource_id) and object2:
493 raise OSError(1, 'Operation not permited.')
494 elif node.type == 'file':
495 result['attachment'].append(object.id)
497 if obj2 and not obj.ressource_id:
498 raise OSError(1, 'Operation not permited.')
500 if (dst_obj and (dst_obj.type in ('directory'))) or not dst_obj2:
501 parent_id = dst_obj and dst_obj.id or False
507 ressource_type_id = pool.get('ir.model').search(cr, uid, [('model','=',dst_obj2._name)])[0]
508 ressource_id = dst_obj2.id
509 ressource_model = dst_obj2._name
510 if dst_obj2._name == 'res.partner':
511 partner_id = dst_obj2.id
513 partner_id = pool.get(dst_obj2._name).fields_get(cr, uid, ['partner_id']) and dst_obj2.partner_id.id or False
515 ressource_type_id = False
517 ressource_model = False
519 pool.get('document.directory').write(cr, uid, result['directory'], {
520 'name' : dst_basename,
521 'ressource_id': ressource_id,
522 'ressource_parent_type_id': ressource_type_id,
523 'parent_id' : parent_id
526 'res_id': ressource_id,
527 'res_model': ressource_model,
528 'partner_id': partner_id
530 pool.get('ir.attachment').write(cr, uid, result['attachment'], val)
531 if (not val['res_id']) and result['attachment']:
532 cr.execute('update ir_attachment set res_id=NULL where id in ('+','.join(map(str,result['attachment']))+')')
536 elif src.type == 'file':
537 pool = pooler.get_pool(src.context.dbname)
538 obj = pool.get('ir.attachment').browse(cr, uid, src.file_id)
540 if isinstance(dst_basedir, node_res_obj):
541 dst_obj2 = dst_basedir and pool.get(dst_basedir.context.context['res_model']).browse(cr, uid, dst_basedir.context.context['res_id']) or False
542 dst_obj = dst_basedir.context._dirobj.browse(cr, uid, dst_basedir.dir_id)
548 'name': dst_basename,
549 'datas_fname': dst_basename,
552 if (dst_obj and (dst_obj.type in ('directory','ressource'))) or not dst_obj2:
553 val['parent_id'] = dst_obj and dst_obj.id or False
555 val['parent_id'] = False
558 val['res_model'] = dst_obj2._name
559 val['res_id'] = dst_obj2.id
560 if dst_obj2._name == 'res.partner':
561 val['partner_id'] = dst_obj2.id
563 val['partner_id'] = pool.get(dst_obj2._name).fields_get(cr, uid, ['partner_id']) and dst_obj2.partner_id.id or False
565 # I had to do that because writing False to an integer writes 0 instead of NULL
566 # change if one day we decide to improve osv/fields.py
567 cr.execute('update ir_attachment set res_id=NULL where id=%s', (obj.id,))
569 pool.get('ir.attachment').write(cr, uid, [obj.id], val)
571 elif src.type=='content':
572 src_file = self.open(src,'r')
573 dst_file = self.create(dst_basedir, dst_basename, 'w')
574 dst_file.write(src_file.getvalue())
579 raise OSError(1, 'Operation not permited.')
580 except Exception,err:
581 self._log.exception('Cannot rename "%s" to "%s" at "%s"', src, dst_basename, dst_basedir)
582 raise OSError(1,'Operation not permited.')
589 def stat(self, node):
590 r = list(os.stat('/'))
591 if self.isfile(node):
593 r[6] = self.getsize(node)
594 r[7] = self.getmtime(node)
595 r[8] = self.getmtime(node)
596 r[9] = self.getmtime(node)
597 return os.stat_result(r)
600 # --- Wrapper methods around os.path.*
603 def isfile(self, node):
604 if node and (node.type not in ('collection','database')):
609 def islink(self, path):
610 """Return True if path is a symbolic link."""
614 def isdir(self, node):
615 """Return True if path is a directory."""
618 if node and (node.type in ('collection','database')):
623 def getsize(self, node):
624 """Return the size of the specified file in bytes."""
626 if node.type=='file':
627 result = node.content_length or 0L
631 def getmtime(self, node):
632 """Return the last modified time as a number of seconds since
635 if node.write_date or node.create_date:
636 dt = (node.write_date or node.create_date)[:19]
637 result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
639 result = time.mktime(time.localtime())
643 def realpath(self, path):
644 """Return the canonical version of path eliminating any
645 symbolic links encountered in the path (if they are
646 supported by the operating system).
651 def lexists(self, path):
652 """Return True if path refers to an existing path, including
653 a broken or circular symbolic link.
655 return path and True or False
658 # Ok, can be improved
659 def glob1(self, dirname, pattern):
660 """Return a list of files matching a dirname pattern
663 Unlike glob.glob1 raises exception if os.listdir() fails.
665 names = self.listdir(dirname)
666 if pattern[0] != '.':
667 names = filter(lambda x: x.path[0] != '.', names)
668 return fnmatch.filter(names, pattern)
670 # --- Listing utilities
672 # note: the following operations are no more blocking
675 def get_list_dir(self, path):
676 """"Return an iterator object that yields a directory listing
677 in a form suitable for LIST command.
680 listing = self.listdir(path)
682 return self.format_list(path and path.path or '/', listing)
683 # if path is a file or a symlink we return information about it
684 elif self.isfile(path):
685 basedir, filename = os.path.split(path.path)
686 self.lstat(path) # raise exc in case of problems
687 return self.format_list(basedir, [path])
691 def get_stat_dir(self, rawline, datacr):
692 """Return an iterator object that yields a list of files
693 matching a dirname pattern non-recursively in a form
694 suitable for STAT command.
696 - (str) rawline: the raw string passed by client as command
699 ftppath = self.ftpnorm(rawline)
700 if not glob.has_magic(ftppath):
701 return self.get_list_dir(self.ftp2fs(rawline, datacr))
703 basedir, basename = os.path.split(ftppath)
704 if glob.has_magic(basedir):
705 return iter(['Directory recursion not supported.\r\n'])
707 basedir = self.ftp2fs(basedir, datacr)
708 listing = self.glob1(basedir, basename)
711 return self.format_list(basedir, listing)
714 def format_list(self, basedir, listing, ignore_err=True):
715 """Return an iterator object that yields the entries of given
716 directory emulating the "/bin/ls -lA" UNIX command output.
718 - (str) basedir: the absolute dirname.
719 - (list) listing: the names of the entries in basedir
720 - (bool) ignore_err: when False raise exception if os.lstat()
723 On platforms which do not support the pwd and grp modules (such
724 as Windows), ownership is printed as "owner" and "group" as a
725 default, and number of hard links is always "1". On UNIX
726 systems, the actual owner, group, and number of links are
729 This is how output appears to client:
731 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
732 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
733 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
737 st = self.lstat(file)
742 perms = filemode(st.st_mode) # permissions
743 nlinks = st.st_nlink # number of links to inode
744 if not nlinks: # non-posix system, let's use a bogus value
746 size = st.st_size # file size
749 # stat.st_mtime could fail (-1) if last mtime is too old
750 # in which case we return the local time as last mtime
752 mname=_get_month_name(time.strftime("%m", time.localtime(st.st_mtime)))
753 mtime = mname+' '+time.strftime("%d %H:%M", time.localtime(st.st_mtime))
755 mname=_get_month_name(time.strftime("%m"))
756 mtime = mname+' '+time.strftime("%d %H:%M")
757 # formatting is matched with proftpd ls output
758 path=_to_decode(file.path) #file.path.encode('ascii','replace').replace('?','_')
759 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
760 size, mtime, path.split('/')[-1])
763 def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
764 """Return an iterator object that yields the entries of a given
765 directory or of a single file in a form suitable with MLSD and
768 Every entry includes a list of "facts" referring the listed
769 element. See RFC-3659, chapter 7, to see what every single
772 - (str) basedir: the absolute dirname.
773 - (list) listing: the names of the entries in basedir
774 - (str) perms: the string referencing the user permissions.
775 - (str) facts: the list of "facts" to be returned.
776 - (bool) ignore_err: when False raise exception if os.stat()
779 Note that "facts" returned may change depending on the platform
780 and on what user specified by using the OPTS command.
782 This is how output could appear to the client issuing
785 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
786 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
787 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
789 permdir = ''.join([x for x in perms if x not in 'arw'])
790 permfile = ''.join([x for x in perms if x not in 'celmp'])
791 if ('w' in perms) or ('a' in perms) or ('f' in perms):
795 type = size = perm = modify = create = unique = mode = uid = gid = ""
804 if stat.S_ISDIR(st.st_mode):
808 perm = 'perm=%s;' %permdir
813 perm = 'perm=%s;' %permfile
815 size = 'size=%s;' %st.st_size # file size
816 # last modification time
817 if 'modify' in facts:
819 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
820 time.localtime(st.st_mtime))
822 # stat.st_mtime could fail (-1) if last mtime is too old
824 if 'create' in facts:
825 # on Windows we can provide also the creation time
827 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
828 time.localtime(st.st_ctime))
832 if 'unix.mode' in facts:
833 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
834 if 'unix.uid' in facts:
835 uid = 'unix.uid=%s;' %st.st_uid
836 if 'unix.gid' in facts:
837 gid = 'unix.gid=%s;' %st.st_gid
838 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
839 # posix platforms only; we get it by mixing st_dev and
840 # st_ino values which should be enough for granting an
841 # uniqueness for the file listed.
842 # The same approach is used by pure-ftpd.
843 # Implementors who want to provide unique fact on other
844 # platforms should use some platform-specific method (e.g.
845 # on Windows NTFS filesystems MTF records could be used).
846 if 'unique' in facts:
847 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
848 path=_to_decode(file.path)
849 path = path and path.split('/')[-1] or None
850 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
851 mode, uid, gid, unique, path)
853 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: