1 # -*- encoding: utf-8 -*-
5 from tarfile import filemode
16 from service import security
18 from document.nodes import get_node_context
20 def _get_month_name(month):
22 if month==1:return 'Jan'
23 elif month==2:return 'Feb'
24 elif month==3:return 'Mar'
25 elif month==4:return 'Apr'
26 elif month==5:return 'May'
27 elif month==6:return 'Jun'
28 elif month==7:return 'Jul'
29 elif month==8:return 'Aug'
30 elif month==9:return 'Sep'
31 elif month==10:return 'Oct'
32 elif month==11:return 'Nov'
33 elif month==12:return 'Dec'
35 from ftpserver import _to_decode, _to_unicode
38 class abstracted_fs(object):
39 """A class used to interact with the file system, providing a high
40 level, cross-platform interface compatible with both Windows and
41 UNIX style filesystems.
43 It provides some utility methods and some wraps around operations
44 involved in file creation and file system operations like moving
45 files or removing directories.
48 - (str) root: the user home directory.
49 - (str) cwd: the current working directory.
50 - (str) rnfr: source file to be renamed.
59 self._log = logging.getLogger('FTP.fs')
63 """Get the list of available databases, with FTPd support
65 s = netsvc.ExportService.getService('db')
66 result = s.exp_list(document=True)
67 self.db_name_list = []
68 for db_name in result:
72 db = sql_db.db_connect(db_name)
74 cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
78 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document_ftp' AND state IN ('installed', 'to install', 'to upgrade') ")
81 self.db_name_list.append(db_name)
84 self._log.warning('Cannot use db "%s"', db_name)
88 return self.db_name_list
90 def ftpnorm(self, ftppath):
91 """Normalize a "virtual" ftp pathname (tipically the raw string
94 Pathname returned is relative!.
96 p = os.path.normpath(ftppath)
97 # normalize string in a standard web-path notation having '/'
98 # as separator. xrg: is that really in the spec?
99 p = p.replace("\\", "/")
100 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
101 # don't need them. In case we get an UNC path we collapse
102 # redundant separators appearing at the beginning of the string
110 """ return the cwd, decoded in utf"""
111 return _to_decode(self.cwd)
113 def ftp2fs(self, path_orig, data):
114 raise DeprecationWarning()
116 def fs2ftp(self, node):
117 """ Return the string path of a node, in ftp form
121 paths = node.full_path()
122 res = '/' + node.context.dbname + '/' + \
123 _to_decode(os.path.join(*paths))
127 def validpath(self, path):
128 """Check whether the path belongs to user's home directory.
129 Expected argument is a datacr tuple
131 # TODO: are we called for "/" ?
132 return isinstance(path, tuple) and path[1] and True or False
134 # --- Wrapper methods around open() and tempfile.mkstemp
136 def create(self, datacr, objname, mode):
137 """ Create a children file-node under node, open it
138 @return open node_descriptor of the created node
140 objname = _to_unicode(objname)
141 cr , node, rem = datacr
143 child = node.child(cr, objname)
145 if child.type not in ('file','content'):
146 raise OSError(1, 'Operation not permited.')
148 ret = child.open_data(cr, mode)
150 assert ret, "Cannot create descriptor for %r: %r" % (child, ret)
152 except EnvironmentError:
155 self._log.exception('Cannot locate item %s at node %s', objname, repr(node))
159 child = node.create_child(cr, objname, data=None)
160 ret = child.open_data(cr, mode)
161 assert ret, "cannot create descriptor for %r" % child
164 except EnvironmentError:
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 # TODO
191 text = not 'b' in mode
192 # for unique file , maintain version if duplicate file
196 pool = pooler.get_pool(node.context.dbname)
197 object=dir and dir.object or False
198 object2=dir and dir.object2 or False
199 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)])
201 pre = prefix.split('.')
202 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
203 return self.create(dir,suffix+prefix,text)
208 def chdir(self, datacr):
209 if (not datacr) or datacr == (None, None, None):
214 raise OSError(1, 'Operation not permitted')
215 if datacr[1].type not in ('collection','database'):
216 raise OSError(2, 'Path is not a directory')
217 self.cwd = '/'+datacr[1].context.dbname + '/'
218 self.cwd += '/'.join(datacr[1].full_path())
219 self.cwd_node = datacr[1]
222 def mkdir(self, datacr, basename):
223 """Create the specified directory."""
224 cr, node, rem = datacr or (None, None, None)
226 raise OSError(1, 'Operation not permited.')
229 basename =_to_unicode(basename)
230 cdir = node.create_child_collection(cr, basename)
231 self._log.debug("Created child dir: %r", cdir)
234 self._log.exception('Cannot create dir "%s" at node %s', basename, repr(node))
235 raise OSError(1, 'Operation not permited.')
237 def close_cr(self, data):
242 def get_cr(self, pathname):
243 raise DeprecationWarning()
245 def get_crdata(self, line, mode='file'):
246 """ Get database cursor, node and remainder data, for commands
248 This is the helper function that will prepare the arguments for
249 any of the subsequent commands.
250 It returns a tuple in the form of:
251 @code ( cr, node, rem_path=None )
253 @param line An absolute or relative ftp path, as passed to the cmd.
254 @param mode A word describing the mode of operation, so that this
255 function behaves properly in the different commands.
257 path = self.ftpnorm(line)
258 if self.cwd_node is None:
259 if not os.path.isabs(path):
260 path = os.path.join(self.root, path)
262 if path == '/' and mode in ('list', 'cwd'):
263 return (None, None, None )
265 path = _to_unicode(os.path.normpath(path)) # again, for '/db/../ss'
266 if path == '.': path = ''
268 if os.path.isabs(path) and self.cwd_node is not None \
269 and path.startswith(self.cwd):
270 # make relative, so that cwd_node is used again
271 path = path[len(self.cwd):]
272 if path.startswith('/'):
275 p_parts = path.split('/') # hard-code the unix sep here, by spec.
277 assert '..' not in p_parts
280 if mode in ('create',):
281 rem_path = p_parts[-1]
282 p_parts = p_parts[:-1]
284 if os.path.isabs(path):
285 # we have to start from root, again
286 while p_parts and p_parts[0] == '':
287 p_parts = p_parts[1:]
288 # self._log.debug("Path parts: %r ", p_parts)
290 raise IOError(errno.EPERM, 'Cannot perform operation at root dir')
292 if dbname not in self.db_list():
293 raise IOError(errno.ENOENT,'Invalid database path: %s' % dbname)
295 db = pooler.get_db(dbname)
297 raise OSError(1, 'Database cannot be used.')
300 uid = security.login(dbname, self.username, self.password)
306 raise OSError(2, 'Authentification Required.')
307 n = get_node_context(cr, uid, {})
308 node = n.get_uri(cr, p_parts[1:])
309 return (cr, node, rem_path)
311 # we never reach here if cwd_node is not set
312 if p_parts and p_parts[-1] == '':
313 p_parts = p_parts[:-1]
314 cr, uid = self.get_node_cr_uid(self.cwd_node)
316 node = self.cwd_node.get_uri(cr, p_parts)
319 if node is False and mode not in ('???'):
321 raise IOError(errno.ENOENT, 'Path does not exist')
322 return (cr, node, rem_path)
324 def get_node_cr_uid(self, node):
325 """ Get cr, uid, pool from a node
328 db = pooler.get_db(node.context.dbname)
329 return db.cursor(), node.context.uid
331 def get_node_cr(self, node):
332 """ Get the cursor for the database of a node
334 The cursor is the only thing that a node will not store
335 persistenly, so we have to obtain a new one for each call.
337 return self.get_node_cr_uid(node)[0]
339 def listdir(self, datacr):
340 """List the content of a directory."""
341 class false_node(object):
350 def __init__(self, db):
353 if datacr[1] is None:
355 for db in self.db_list():
357 result.append(false_node(db))
358 except osv.except_osv:
361 cr, node, rem = datacr
362 res = node.children(cr)
365 def rmdir(self, datacr):
366 """Remove the specified directory."""
367 cr, node, rem = datacr
372 def remove(self, datacr):
374 if datacr[1].type == 'collection':
375 return self.rmdir(datacr)
376 elif datacr[1].type == 'file':
377 return self.rmfile(datacr)
378 raise OSError(1, 'Operation not permited.')
380 def rmfile(self, datacr):
381 """Remove the specified file."""
387 def rename(self, src, datacr):
388 """ Renaming operation, the effect depends on the src:
389 * A file: read, create and remove
390 * A directory: change the parent and reassign children to ressource
394 nname = _to_unicode(datacr[2])
395 ret = src.move_to(cr, datacr[1], new_name=nname)
396 # API shouldn't wait for us to write the object
397 assert (ret is True) or (ret is False)
399 except EnvironmentError:
402 self._log.exception('Cannot rename "%s" to "%s" at "%s"', src, datacr[2], datacr[1])
403 raise OSError(1,'Operation not permited.')
405 def stat(self, node):
406 raise NotImplementedError()
408 # --- Wrapper methods around os.path.*
411 def isfile(self, node):
412 if node and (node.type in ('file','content')):
417 def islink(self, path):
418 """Return True if path is a symbolic link."""
421 def isdir(self, node):
422 """Return True if path is a directory."""
425 if node and (node.type in ('collection','database')):
429 def getsize(self, datacr):
430 """Return the size of the specified file in bytes."""
431 if not (datacr and datacr[1]):
432 raise IOError(errno.ENOENT, "No such file or directory")
433 if datacr[1].type in ('file', 'content'):
434 return datacr[1].get_data_len(datacr[0]) or 0L
438 def getmtime(self, datacr):
439 """Return the last modified time as a number of seconds since
443 if node.write_date or node.create_date:
444 dt = (node.write_date or node.create_date)[:19]
445 result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
447 result = time.mktime(time.localtime())
451 def realpath(self, path):
452 """Return the canonical version of path eliminating any
453 symbolic links encountered in the path (if they are
454 supported by the operating system).
459 def lexists(self, path):
460 """Return True if path refers to an existing path, including
461 a broken or circular symbolic link.
463 raise DeprecationWarning()
464 return path and True or False
468 # Ok, can be improved
469 def glob1(self, dirname, pattern):
470 """Return a list of files matching a dirname pattern
473 Unlike glob.glob1 raises exception if os.listdir() fails.
475 names = self.listdir(dirname)
476 if pattern[0] != '.':
477 names = filter(lambda x: x.path[0] != '.', names)
478 return fnmatch.filter(names, pattern)
480 # --- Listing utilities
482 # note: the following operations are no more blocking
484 def get_list_dir(self, datacr):
485 """"Return an iterator object that yields a directory listing
486 in a form suitable for LIST command.
490 elif self.isdir(datacr[1]):
491 listing = self.listdir(datacr)
492 return self.format_list(datacr[0], datacr[1], listing)
493 # if path is a file or a symlink we return information about it
494 elif self.isfile(datacr[1]):
495 par = datacr[1].parent
496 return self.format_list(datacr[0], par, [datacr[1]])
498 def get_stat_dir(self, rawline, datacr):
499 """Return an iterator object that yields a list of files
500 matching a dirname pattern non-recursively in a form
501 suitable for STAT command.
503 - (str) rawline: the raw string passed by client as command
506 ftppath = self.ftpnorm(rawline)
507 if not glob.has_magic(ftppath):
508 return self.get_list_dir(self.ftp2fs(rawline, datacr))
510 basedir, basename = os.path.split(ftppath)
511 if glob.has_magic(basedir):
512 return iter(['Directory recursion not supported.\r\n'])
514 basedir = self.ftp2fs(basedir, datacr)
515 listing = self.glob1(basedir, basename)
518 return self.format_list(basedir, listing)
520 def format_list(self, cr, parent_node, listing, ignore_err=True):
521 """Return an iterator object that yields the entries of given
522 directory emulating the "/bin/ls -lA" UNIX command output.
524 - (str) basedir: the parent directory node. Can be None
525 - (list) listing: a list of nodes
526 - (bool) ignore_err: when False raise exception if os.lstat()
529 On platforms which do not support the pwd and grp modules (such
530 as Windows), ownership is printed as "owner" and "group" as a
531 default, and number of hard links is always "1". On UNIX
532 systems, the actual owner, group, and number of links are
535 This is how output appears to client:
537 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
538 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
539 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
542 perms = filemode(node.unixperms) # permissions
544 size = node.content_length or 0L
545 uname = _to_decode(node.uuser)
546 gname = _to_decode(node.ugroup)
547 # stat.st_mtime could fail (-1) if last mtime is too old
548 # in which case we return the local time as last mtime
550 st_mtime = node.write_date or 0.0
551 if isinstance(st_mtime, basestring):
552 st_mtime = time.strptime(st_mtime, '%Y-%m-%d %H:%M:%S')
553 elif isinstance(st_mtime, float):
554 st_mtime = time.localtime(st_mtime)
555 mname=_get_month_name(time.strftime("%m", st_mtime ))
556 mtime = mname+' '+time.strftime("%d %H:%M", st_mtime)
558 mname=_get_month_name(time.strftime("%m"))
559 mtime = mname+' '+time.strftime("%d %H:%M")
561 if isinstance(fpath, (list, tuple)):
563 # formatting is matched with proftpd ls output
564 path=_to_decode(fpath)
565 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
569 def format_mlsx(self, cr, basedir, listing, perms, facts, ignore_err=True):
570 """Return an iterator object that yields the entries of a given
571 directory or of a single file in a form suitable with MLSD and
574 Every entry includes a list of "facts" referring the listed
575 element. See RFC-3659, chapter 7, to see what every single
578 - (str) basedir: the absolute dirname.
579 - (list) listing: the names of the entries in basedir
580 - (str) perms: the string referencing the user permissions.
581 - (str) facts: the list of "facts" to be returned.
582 - (bool) ignore_err: when False raise exception if os.stat()
585 Note that "facts" returned may change depending on the platform
586 and on what user specified by using the OPTS command.
588 This is how output could appear to the client issuing
591 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
592 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
593 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
595 permdir = ''.join([x for x in perms if x not in 'arw'])
596 permfile = ''.join([x for x in perms if x not in 'celmp'])
597 if ('w' in perms) or ('a' in perms) or ('f' in perms):
601 type = size = perm = modify = create = unique = mode = uid = gid = ""
608 perm = 'perm=%s;' %permdir
613 perm = 'perm=%s;' %permfile
615 size = 'size=%s;' % (node.content_length or 0L)
616 # last modification time
617 if 'modify' in facts:
619 st_mtime = node.write_date or 0.0
620 if isinstance(st_mtime, basestring):
621 st_mtime = time.strptime(st_mtime, '%Y-%m-%d %H:%M:%S')
622 elif isinstance(st_mtime, float):
623 st_mtime = time.localtime(st_mtime)
624 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S", st_mtime)
626 # stat.st_mtime could fail (-1) if last mtime is too old
628 if 'create' in facts:
629 # on Windows we can provide also the creation time
631 st_ctime = node.create_date or 0.0
632 if isinstance(st_ctime, basestring):
633 st_ctime = time.strptime(st_ctime, '%Y-%m-%d %H:%M:%S')
634 elif isinstance(st_mtime, float):
635 st_ctime = time.localtime(st_ctime)
636 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",st_ctime)
640 if 'unix.mode' in facts:
641 mode = 'unix.mode=%s;' %oct(node.unixperms & 0777)
642 if 'unix.uid' in facts:
643 uid = 'unix.uid=%s;' % _to_decode(node.uuser)
644 if 'unix.gid' in facts:
645 gid = 'unix.gid=%s;' % _to_decode(node.ugroup)
646 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
647 # posix platforms only; we get it by mixing st_dev and
648 # st_ino values which should be enough for granting an
649 # uniqueness for the file listed.
650 # The same approach is used by pure-ftpd.
651 # Implementors who want to provide unique fact on other
652 # platforms should use some platform-specific method (e.g.
653 # on Windows NTFS filesystems MTF records could be used).
654 # if 'unique' in facts: todo
655 # unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
657 if isinstance (path, (list, tuple)):
659 path=_to_decode(path)
660 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
661 mode, uid, gid, unique, path)
663 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: