1 # -*- encoding: utf-8 -*-
4 from tarfile import filemode
16 from service import security
18 #from document.nodes import node_res_dir, node_res_obj
19 from document.nodes import get_node_context
22 def _get_month_name(month):
24 if month==1:return 'Jan'
25 elif month==2:return 'Feb'
26 elif month==3:return 'Mar'
27 elif month==4:return 'Apr'
28 elif month==5:return 'May'
29 elif month==6:return 'Jun'
30 elif month==7:return 'Jul'
31 elif month==8:return 'Aug'
32 elif month==9:return 'Sep'
33 elif month==10:return 'Oct'
34 elif month==11:return 'Nov'
35 elif month==12:return 'Dec'
37 from ftpserver import _to_decode, _to_unicode
40 class abstracted_fs(object):
41 """A class used to interact with the file system, providing a high
42 level, cross-platform interface compatible with both Windows and
43 UNIX style filesystems.
45 It provides some utility methods and some wraps around operations
46 involved in file creation and file system operations like moving
47 files or removing directories.
50 - (str) root: the user home directory.
51 - (str) cwd: the current working directory.
52 - (str) rnfr: source file to be renamed.
61 self._log = logging.getLogger('FTP.fs')
65 """Get the list of available databases, with FTPd support
67 s = netsvc.ExportService.getService('db')
68 result = s.exp_list(document=True)
69 self.db_name_list = []
70 for db_name in result:
74 db = pooler.get_db_only(db_name)
76 cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
80 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document_ftp' AND state IN ('installed', 'to upgrade') ")
83 self.db_name_list.append(db_name)
86 self._log.warning('Cannot use db "%s"', db_name)
91 # pooler.close_db(db_name)
92 return self.db_name_list
94 def ftpnorm(self, ftppath):
95 """Normalize a "virtual" ftp pathname (tipically the raw string
98 Pathname returned is relative!.
100 p = os.path.normpath(ftppath)
101 # normalize string in a standard web-path notation having '/'
102 # as separator. xrg: is that really in the spec?
103 p = p.replace("\\", "/")
104 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
105 # don't need them. In case we get an UNC path we collapse
106 # redundant separators appearing at the beginning of the string
114 """ return the cwd, decoded in utf"""
115 return _to_decode(self.cwd)
117 def ftp2fs(self, path_orig, data):
118 raise DeprecationWarning()
120 def fs2ftp(self, node):
121 """ Return the string path of a node, in ftp form
125 paths = node.full_path()
126 res = '/' + node.context.dbname + '/' + \
127 _to_decode(os.path.join(*paths))
131 def validpath(self, path):
132 """Check whether the path belongs to user's home directory.
133 Expected argument is a datacr tuple
135 # TODO: are we called for "/" ?
136 return isinstance(path, tuple) and path[1] and True or False
138 # --- Wrapper methods around open() and tempfile.mkstemp
140 def create(self, datacr, objname, mode):
141 """ Create a children file-node under node, open it
142 @return open node_descriptor of the created node
144 objname = _to_unicode(objname)
145 cr , node, rem = datacr
147 child = node.child(cr, objname)
149 if child.type not in ('file','content'):
150 raise OSError(1, 'Operation not permited.')
152 ret = child.open_data(cr, mode)
155 except EnvironmentError:
158 self._log.exception('Cannot locate item %s at node %s', objname, repr(node))
162 child = node.create_child(cr, objname, data=None)
163 ret = child.open_data(cr, mode)
167 self._log.exception('Cannot create item %s at node %s', objname, repr(node))
168 raise OSError(1, 'Operation not permited.')
170 def open(self, datacr, mode):
171 if not (datacr and datacr[1]):
172 raise OSError(1, 'Operation not permited.')
174 cr, node, rem = datacr
176 res = node.open_data(cr, mode)
179 raise IOError(errno.EINVAL, "No data")
182 # ok, but need test more
184 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
185 """A wrap around tempfile.mkstemp creating a file with a unique
186 name. Unlike mkstemp it returns an object with a file-like
189 raise NotImplementedError
191 text = not 'b' in mode
192 # for unique file , maintain version if duplicate file
197 pool = pooler.get_pool(node.context.dbname)
198 object=dir and dir.object or False
199 object2=dir and dir.object2 or False
200 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)])
202 pre = prefix.split('.')
203 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
204 #prefix = prefix + '.'
205 return self.create(dir,suffix+prefix,text)
210 def chdir(self, datacr):
211 if (not datacr) or datacr == (None, None, None):
216 raise OSError(1, 'Operation not permitted')
217 if datacr[1].type not in ('collection','database'):
218 raise OSError(2, 'Path is not a directory')
219 self.cwd = '/'+datacr[1].context.dbname + '/'
220 self.cwd += '/'.join(datacr[1].full_path())
221 self.cwd_node = datacr[1]
224 def mkdir(self, datacr, basename):
225 """Create the specified directory."""
226 cr, node, rem = datacr or (None, None, None)
228 raise OSError(1, 'Operation not permited.')
231 basename =_to_unicode(basename)
232 cdir = node.create_child_collection(cr, basename)
233 self._log.debug("Created child dir: %r", cdir)
236 self._log.exception('Cannot create dir "%s" at node %s', basename, repr(node))
237 raise OSError(1, 'Operation not permited.')
239 def close_cr(self, data):
244 def get_cr(self, pathname):
245 raise DeprecationWarning()
247 def get_crdata(self, line, mode='file'):
248 """ Get database cursor, node and remainder data, for commands
250 This is the helper function that will prepare the arguments for
251 any of the subsequent commands.
252 It returns a tuple in the form of:
253 @code ( cr, node, rem_path=None )
255 @param line An absolute or relative ftp path, as passed to the cmd.
256 @param mode A word describing the mode of operation, so that this
257 function behaves properly in the different commands.
259 path = self.ftpnorm(line)
260 if self.cwd_node is None:
261 if not os.path.isabs(path):
262 path = os.path.join(self.root, path)
264 if path == '/' and mode in ('list', 'cwd'):
265 return (None, None, None )
267 path = _to_unicode(os.path.normpath(path)) # again, for '/db/../ss'
268 if path == '.': path = ''
270 if os.path.isabs(path) and self.cwd_node is not None \
271 and path.startswith(self.cwd):
272 # make relative, so that cwd_node is used again
273 path = path[len(self.cwd):]
274 if path.startswith('/'):
277 p_parts = path.split('/') # hard-code the unix sep here, by spec.
279 assert '..' not in p_parts
282 if mode in ('create',):
283 rem_path = p_parts[-1]
284 p_parts = p_parts[:-1]
286 if os.path.isabs(path):
287 # we have to start from root, again
288 while p_parts[0] == '':
289 p_parts = p_parts[1:]
290 # self._log.debug("Path parts: %r ", p_parts)
292 raise IOError(errno.EPERM, 'Cannot perform operation at root dir')
294 if dbname not in self.db_list():
295 raise IOError(errno.ENOENT,'Invalid database path')
297 db = pooler.get_db(dbname)
299 raise OSError(1, 'Database cannot be used.')
302 uid = security.login(dbname, self.username, self.password)
308 raise OSError(2, 'Authentification Required.')
309 n = get_node_context(cr, uid, {})
310 node = n.get_uri(cr, p_parts[1:])
311 # self._log.debug("get_crdata(abs): %r" % ( (cr, node, rem_path),))
312 return (cr, node, rem_path)
314 # we never reach here if cwd_node is not set
315 if p_parts and p_parts[-1] == '':
316 p_parts = p_parts[:-1]
317 cr, uid = self.get_node_cr_uid(self.cwd_node)
319 node = self.cwd_node.get_uri(cr, p_parts)
322 if node is False and mode not in ('???'):
324 raise IOError(errno.ENOENT, 'Path does not exist')
325 # self._log.debug("get_crdata(rel): %r" % ( (cr, node, rem_path),))
326 return (cr, node, rem_path)
328 def get_node_cr_uid(self, node):
329 """ Get cr, uid, pool from a node
332 db = pooler.get_db(node.context.dbname)
333 return db.cursor(), node.context.uid
335 def get_node_cr(self, node):
336 """ Get the cursor for the database of a node
338 The cursor is the only thing that a node will not store
339 persistenly, so we have to obtain a new one for each call.
341 return self.get_node_cr_uid(node)[0]
343 def listdir(self, datacr):
344 """List the content of a directory."""
345 class false_node(object):
354 def __init__(self, db):
357 if datacr[1] is None:
359 for db in self.db_list():
361 result.append(false_node(db))
362 except osv.except_osv:
365 cr, node, rem = datacr
366 res = node.children(cr)
369 def rmdir(self, datacr):
370 """Remove the specified directory."""
371 cr, node, rem = datacr
373 cr = self.get_node_cr(node)
377 def remove(self, datacr):
379 if datacr[1].type == 'collection':
380 return self.rmdir(datacr)
381 elif datacr[1].type == 'file':
382 return self.rmfile(datacr)
383 raise OSError(1, 'Operation not permited.')
385 def rmfile(self, datacr):
386 """Remove the specified file."""
392 def rename(self, src, datacr):
393 """ Renaming operation, the effect depends on the src:
394 * A file: read, create and remove
395 * A directory: change the parent and reassign childs to ressource
399 nname = _to_unicode(datacr[2])
400 ret = src.move_to(cr, datacr[1], new_name=nname)
401 # API shouldn't wait for us to write the object
402 assert (ret is True) or (ret is False)
404 except Exception,err:
405 self._log.exception('Cannot rename "%s" to "%s" at "%s"', src, datacr[2], datacr[1])
406 raise OSError(1,'Operation not permited.')
408 def stat(self, node):
409 raise NotImplementedError()
411 # --- Wrapper methods around os.path.*
414 def isfile(self, node):
415 if node and (node.type in ('file','content')):
420 def islink(self, path):
421 """Return True if path is a symbolic link."""
424 def isdir(self, node):
425 """Return True if path is a directory."""
428 if node and (node.type in ('collection','database')):
432 def getsize(self, datacr):
433 """Return the size of the specified file in bytes."""
434 if not (datacr and datacr[1]):
436 if datacr[1].type in ('file', 'content'):
437 return datacr[1].get_data_len(datacr[0]) or 0L
441 def getmtime(self, datacr):
442 """Return the last modified time as a number of seconds since
446 if node.write_date or node.create_date:
447 dt = (node.write_date or node.create_date)[:19]
448 result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
450 result = time.mktime(time.localtime())
454 def realpath(self, path):
455 """Return the canonical version of path eliminating any
456 symbolic links encountered in the path (if they are
457 supported by the operating system).
462 def lexists(self, path):
463 """Return True if path refers to an existing path, including
464 a broken or circular symbolic link.
466 raise DeprecationWarning()
467 return path and True or False
471 # Ok, can be improved
472 def glob1(self, dirname, pattern):
473 """Return a list of files matching a dirname pattern
476 Unlike glob.glob1 raises exception if os.listdir() fails.
478 names = self.listdir(dirname)
479 if pattern[0] != '.':
480 names = filter(lambda x: x.path[0] != '.', names)
481 return fnmatch.filter(names, pattern)
483 # --- Listing utilities
485 # note: the following operations are no more blocking
487 def get_list_dir(self, datacr):
488 """"Return an iterator object that yields a directory listing
489 in a form suitable for LIST command.
493 elif self.isdir(datacr[1]):
494 listing = self.listdir(datacr)
496 return self.format_list(datacr[0], datacr[1], listing)
497 # if path is a file or a symlink we return information about it
498 elif self.isfile(datacr[1]):
499 par = datacr[1].parent
500 return self.format_list(datacr[0], par, [datacr[1]])
502 def get_stat_dir(self, rawline, datacr):
503 """Return an iterator object that yields a list of files
504 matching a dirname pattern non-recursively in a form
505 suitable for STAT command.
507 - (str) rawline: the raw string passed by client as command
510 ftppath = self.ftpnorm(rawline)
511 if not glob.has_magic(ftppath):
512 return self.get_list_dir(self.ftp2fs(rawline, datacr))
514 basedir, basename = os.path.split(ftppath)
515 if glob.has_magic(basedir):
516 return iter(['Directory recursion not supported.\r\n'])
518 basedir = self.ftp2fs(basedir, datacr)
519 listing = self.glob1(basedir, basename)
522 return self.format_list(basedir, listing)
524 def format_list(self, cr, parent_node, listing, ignore_err=True):
525 """Return an iterator object that yields the entries of given
526 directory emulating the "/bin/ls -lA" UNIX command output.
528 - (str) basedir: the parent directory node. Can be None
529 - (list) listing: a list of nodes
530 - (bool) ignore_err: when False raise exception if os.lstat()
533 On platforms which do not support the pwd and grp modules (such
534 as Windows), ownership is printed as "owner" and "group" as a
535 default, and number of hard links is always "1". On UNIX
536 systems, the actual owner, group, and number of links are
539 This is how output appears to client:
541 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
542 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
543 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
546 perms = filemode(node.unixperms) # permissions
548 size = node.content_length or 0L
549 uname = _to_decode(node.uuser)
550 gname = _to_decode(node.ugroup)
551 # stat.st_mtime could fail (-1) if last mtime is too old
552 # in which case we return the local time as last mtime
554 st_mtime = node.write_date or 0.0
555 if isinstance(st_mtime, basestring):
556 st_mtime = time.strptime(st_mtime, '%Y-%m-%d %H:%M:%S')
557 elif isinstance(st_mtime, float):
558 st_mtime = time.localtime(st_mtime)
559 mname=_get_month_name(time.strftime("%m", st_mtime ))
560 mtime = mname+' '+time.strftime("%d %H:%M", st_mtime)
562 mname=_get_month_name(time.strftime("%m"))
563 mtime = mname+' '+time.strftime("%d %H:%M")
565 if isinstance(fpath, (list, tuple)):
567 # formatting is matched with proftpd ls output
568 path=_to_decode(fpath)
569 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
573 def format_mlsx(self, cr, basedir, listing, perms, facts, ignore_err=True):
574 """Return an iterator object that yields the entries of a given
575 directory or of a single file in a form suitable with MLSD and
578 Every entry includes a list of "facts" referring the listed
579 element. See RFC-3659, chapter 7, to see what every single
582 - (str) basedir: the absolute dirname.
583 - (list) listing: the names of the entries in basedir
584 - (str) perms: the string referencing the user permissions.
585 - (str) facts: the list of "facts" to be returned.
586 - (bool) ignore_err: when False raise exception if os.stat()
589 Note that "facts" returned may change depending on the platform
590 and on what user specified by using the OPTS command.
592 This is how output could appear to the client issuing
595 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
596 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
597 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
599 permdir = ''.join([x for x in perms if x not in 'arw'])
600 permfile = ''.join([x for x in perms if x not in 'celmp'])
601 if ('w' in perms) or ('a' in perms) or ('f' in perms):
605 type = size = perm = modify = create = unique = mode = uid = gid = ""
612 perm = 'perm=%s;' %permdir
617 perm = 'perm=%s;' %permfile
619 size = 'size=%s;' % (node.content_length or 0L)
620 # last modification time
621 if 'modify' in facts:
623 st_mtime = node.write_date or 0.0
624 if isinstance(st_mtime, basestring):
625 st_mtime = time.strptime(st_mtime, '%Y-%m-%d %H:%M:%S')
626 elif isinstance(st_mtime, float):
627 st_mtime = time.localtime(st_mtime)
628 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S", st_mtime)
630 # stat.st_mtime could fail (-1) if last mtime is too old
632 if 'create' in facts:
633 # on Windows we can provide also the creation time
635 st_ctime = node.create_date or 0.0
636 if isinstance(st_ctime, basestring):
637 st_ctime = time.strptime(st_ctime, '%Y-%m-%d %H:%M:%S')
638 elif isinstance(st_mtime, float):
639 st_ctime = time.localtime(st_ctime)
640 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",st_ctime)
644 if 'unix.mode' in facts:
645 mode = 'unix.mode=%s;' %oct(node.unixperms & 0777)
646 if 'unix.uid' in facts:
647 uid = 'unix.uid=%s;' % _to_decode(node.uuser)
648 if 'unix.gid' in facts:
649 gid = 'unix.gid=%s;' % _to_decode(node.ugroup)
650 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
651 # posix platforms only; we get it by mixing st_dev and
652 # st_ino values which should be enough for granting an
653 # uniqueness for the file listed.
654 # The same approach is used by pure-ftpd.
655 # Implementors who want to provide unique fact on other
656 # platforms should use some platform-specific method (e.g.
657 # on Windows NTFS filesystems MTF records could be used).
658 # if 'unique' in facts: todo
659 # unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
661 if isinstance (path, (list, tuple)):
663 path=_to_decode(path)
664 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
665 mode, uid, gid, unique, path)
667 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: