1 # -*- encoding: utf-8 -*-
5 from tarfile import filemode
15 from service import security
17 from document.nodes import get_node_context
19 def _get_month_name(month):
21 if month==1:return 'Jan'
22 elif month==2:return 'Feb'
23 elif month==3:return 'Mar'
24 elif month==4:return 'Apr'
25 elif month==5:return 'May'
26 elif month==6:return 'Jun'
27 elif month==7:return 'Jul'
28 elif month==8:return 'Aug'
29 elif month==9:return 'Sep'
30 elif month==10:return 'Oct'
31 elif month==11:return 'Nov'
32 elif month==12:return 'Dec'
34 from ftpserver import _to_decode, _to_unicode
37 class abstracted_fs(object):
38 """A class used to interact with the file system, providing a high
39 level, cross-platform interface compatible with both Windows and
40 UNIX style filesystems.
42 It provides some utility methods and some wraps around operations
43 involved in file creation and file system operations like moving
44 files or removing directories.
47 - (str) root: the user home directory.
48 - (str) cwd: the current working directory.
49 - (str) rnfr: source file to be renamed.
58 self._log = logging.getLogger('FTP.fs')
62 """Get the list of available databases, with FTPd support
64 s = netsvc.ExportService.getService('db')
65 result = s.exp_list(document=True)
66 self.db_name_list = []
67 for db_name in result:
71 db = pooler.get_db_only(db_name)
73 cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
77 cr.execute("SELECT id FROM ir_module_module WHERE name = 'document_ftp' AND state IN ('installed', 'to install', 'to upgrade') ")
80 self.db_name_list.append(db_name)
83 self._log.warning('Cannot use db "%s"', db_name)
87 return self.db_name_list
89 def ftpnorm(self, ftppath):
90 """Normalize a "virtual" ftp pathname (tipically the raw string
93 Pathname returned is relative!.
95 p = os.path.normpath(ftppath)
96 # normalize string in a standard web-path notation having '/'
97 # as separator. xrg: is that really in the spec?
98 p = p.replace("\\", "/")
99 # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
100 # don't need them. In case we get an UNC path we collapse
101 # redundant separators appearing at the beginning of the string
109 """ return the cwd, decoded in utf"""
110 return _to_decode(self.cwd)
112 def ftp2fs(self, path_orig, data):
113 raise DeprecationWarning()
115 def fs2ftp(self, node):
116 """ Return the string path of a node, in ftp form
120 paths = node.full_path()
121 res = '/' + node.context.dbname + '/' + \
122 _to_decode(os.path.join(*paths))
126 def validpath(self, path):
127 """Check whether the path belongs to user's home directory.
128 Expected argument is a datacr tuple
130 # TODO: are we called for "/" ?
131 return isinstance(path, tuple) and path[1] and True or False
133 # --- Wrapper methods around open() and tempfile.mkstemp
135 def create(self, datacr, objname, mode):
136 """ Create a children file-node under node, open it
137 @return open node_descriptor of the created node
139 objname = _to_unicode(objname)
140 cr , node, rem = datacr
142 child = node.child(cr, objname)
144 if child.type not in ('file','content'):
145 raise OSError(1, 'Operation not permited.')
147 ret = child.open_data(cr, mode)
150 except EnvironmentError:
153 self._log.exception('Cannot locate item %s at node %s', objname, repr(node))
157 child = node.create_child(cr, objname, data=None)
158 ret = child.open_data(cr, mode)
161 except EnvironmentError:
164 self._log.exception('Cannot create item %s at node %s', objname, repr(node))
165 raise OSError(1, 'Operation not permited.')
167 def open(self, datacr, mode):
168 if not (datacr and datacr[1]):
169 raise OSError(1, 'Operation not permited.')
171 cr, node, rem = datacr
173 res = node.open_data(cr, mode)
176 raise IOError(errno.EINVAL, "No data")
179 # ok, but need test more
181 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
182 """A wrap around tempfile.mkstemp creating a file with a unique
183 name. Unlike mkstemp it returns an object with a file-like
186 raise NotImplementedError # TODO
188 text = not 'b' in mode
189 # for unique file , maintain version if duplicate file
193 pool = pooler.get_pool(node.context.dbname)
194 object=dir and dir.object or False
195 object2=dir and dir.object2 or False
196 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)])
198 pre = prefix.split('.')
199 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
200 return self.create(dir,suffix+prefix,text)
205 def chdir(self, datacr):
206 if (not datacr) or datacr == (None, None, None):
211 raise OSError(1, 'Operation not permitted')
212 if datacr[1].type not in ('collection','database'):
213 raise OSError(2, 'Path is not a directory')
214 self.cwd = '/'+datacr[1].context.dbname + '/'
215 self.cwd += '/'.join(datacr[1].full_path())
216 self.cwd_node = datacr[1]
219 def mkdir(self, datacr, basename):
220 """Create the specified directory."""
221 cr, node, rem = datacr or (None, None, None)
223 raise OSError(1, 'Operation not permited.')
226 basename =_to_unicode(basename)
227 cdir = node.create_child_collection(cr, basename)
228 self._log.debug("Created child dir: %r", cdir)
231 self._log.exception('Cannot create dir "%s" at node %s', basename, repr(node))
232 raise OSError(1, 'Operation not permited.')
234 def close_cr(self, data):
239 def get_cr(self, pathname):
240 raise DeprecationWarning()
242 def get_crdata(self, line, mode='file'):
243 """ Get database cursor, node and remainder data, for commands
245 This is the helper function that will prepare the arguments for
246 any of the subsequent commands.
247 It returns a tuple in the form of:
248 @code ( cr, node, rem_path=None )
250 @param line An absolute or relative ftp path, as passed to the cmd.
251 @param mode A word describing the mode of operation, so that this
252 function behaves properly in the different commands.
254 path = self.ftpnorm(line)
255 if self.cwd_node is None:
256 if not os.path.isabs(path):
257 path = os.path.join(self.root, path)
259 if path == '/' and mode in ('list', 'cwd'):
260 return (None, None, None )
262 path = _to_unicode(os.path.normpath(path)) # again, for '/db/../ss'
263 if path == '.': path = ''
265 if os.path.isabs(path) and self.cwd_node is not None \
266 and path.startswith(self.cwd):
267 # make relative, so that cwd_node is used again
268 path = path[len(self.cwd):]
269 if path.startswith('/'):
272 p_parts = path.split('/') # hard-code the unix sep here, by spec.
274 assert '..' not in p_parts
277 if mode in ('create',):
278 rem_path = p_parts[-1]
279 p_parts = p_parts[:-1]
281 if os.path.isabs(path):
282 # we have to start from root, again
283 while p_parts and p_parts[0] == '':
284 p_parts = p_parts[1:]
285 # self._log.debug("Path parts: %r ", p_parts)
287 raise IOError(errno.EPERM, 'Cannot perform operation at root dir')
289 if dbname not in self.db_list():
290 raise IOError(errno.ENOENT,'Invalid database path: %s' % dbname)
292 db = pooler.get_db(dbname)
294 raise OSError(1, 'Database cannot be used.')
297 uid = security.login(dbname, self.username, self.password)
303 raise OSError(2, 'Authentification Required.')
304 n = get_node_context(cr, uid, {})
305 node = n.get_uri(cr, p_parts[1:])
306 return (cr, node, rem_path)
308 # we never reach here if cwd_node is not set
309 if p_parts and p_parts[-1] == '':
310 p_parts = p_parts[:-1]
311 cr, uid = self.get_node_cr_uid(self.cwd_node)
313 node = self.cwd_node.get_uri(cr, p_parts)
316 if node is False and mode not in ('???'):
318 raise IOError(errno.ENOENT, 'Path does not exist')
319 return (cr, node, rem_path)
321 def get_node_cr_uid(self, node):
322 """ Get cr, uid, pool from a node
325 db = pooler.get_db(node.context.dbname)
326 return db.cursor(), node.context.uid
328 def get_node_cr(self, node):
329 """ Get the cursor for the database of a node
331 The cursor is the only thing that a node will not store
332 persistenly, so we have to obtain a new one for each call.
334 return self.get_node_cr_uid(node)[0]
336 def listdir(self, datacr):
337 """List the content of a directory."""
338 class false_node(object):
347 def __init__(self, db):
350 if datacr[1] is None:
352 for db in self.db_list():
354 result.append(false_node(db))
355 except osv.except_osv:
358 cr, node, rem = datacr
359 res = node.children(cr)
362 def rmdir(self, datacr):
363 """Remove the specified directory."""
364 cr, node, rem = datacr
369 def remove(self, datacr):
371 if datacr[1].type == 'collection':
372 return self.rmdir(datacr)
373 elif datacr[1].type == 'file':
374 return self.rmfile(datacr)
375 raise OSError(1, 'Operation not permited.')
377 def rmfile(self, datacr):
378 """Remove the specified file."""
384 def rename(self, src, datacr):
385 """ Renaming operation, the effect depends on the src:
386 * A file: read, create and remove
387 * A directory: change the parent and reassign childs to ressource
391 nname = _to_unicode(datacr[2])
392 ret = src.move_to(cr, datacr[1], new_name=nname)
393 # API shouldn't wait for us to write the object
394 assert (ret is True) or (ret is False)
396 except EnvironmentError:
399 self._log.exception('Cannot rename "%s" to "%s" at "%s"', src, datacr[2], datacr[1])
400 raise OSError(1,'Operation not permited.')
402 def stat(self, node):
403 raise NotImplementedError()
405 # --- Wrapper methods around os.path.*
408 def isfile(self, node):
409 if node and (node.type in ('file','content')):
414 def islink(self, path):
415 """Return True if path is a symbolic link."""
418 def isdir(self, node):
419 """Return True if path is a directory."""
422 if node and (node.type in ('collection','database')):
426 def getsize(self, datacr):
427 """Return the size of the specified file in bytes."""
428 if not (datacr and datacr[1]):
429 raise IOError(errno.ENOENT, "No such file or directory")
430 if datacr[1].type in ('file', 'content'):
431 return datacr[1].get_data_len(datacr[0]) or 0L
435 def getmtime(self, datacr):
436 """Return the last modified time as a number of seconds since
440 if node.write_date or node.create_date:
441 dt = (node.write_date or node.create_date)[:19]
442 result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
444 result = time.mktime(time.localtime())
448 def realpath(self, path):
449 """Return the canonical version of path eliminating any
450 symbolic links encountered in the path (if they are
451 supported by the operating system).
456 def lexists(self, path):
457 """Return True if path refers to an existing path, including
458 a broken or circular symbolic link.
460 raise DeprecationWarning()
461 return path and True or False
465 # Ok, can be improved
466 def glob1(self, dirname, pattern):
467 """Return a list of files matching a dirname pattern
470 Unlike glob.glob1 raises exception if os.listdir() fails.
472 names = self.listdir(dirname)
473 if pattern[0] != '.':
474 names = filter(lambda x: x.path[0] != '.', names)
475 return fnmatch.filter(names, pattern)
477 # --- Listing utilities
479 # note: the following operations are no more blocking
481 def get_list_dir(self, datacr):
482 """"Return an iterator object that yields a directory listing
483 in a form suitable for LIST command.
487 elif self.isdir(datacr[1]):
488 listing = self.listdir(datacr)
489 return self.format_list(datacr[0], datacr[1], listing)
490 # if path is a file or a symlink we return information about it
491 elif self.isfile(datacr[1]):
492 par = datacr[1].parent
493 return self.format_list(datacr[0], par, [datacr[1]])
495 def get_stat_dir(self, rawline, datacr):
496 """Return an iterator object that yields a list of files
497 matching a dirname pattern non-recursively in a form
498 suitable for STAT command.
500 - (str) rawline: the raw string passed by client as command
503 ftppath = self.ftpnorm(rawline)
504 if not glob.has_magic(ftppath):
505 return self.get_list_dir(self.ftp2fs(rawline, datacr))
507 basedir, basename = os.path.split(ftppath)
508 if glob.has_magic(basedir):
509 return iter(['Directory recursion not supported.\r\n'])
511 basedir = self.ftp2fs(basedir, datacr)
512 listing = self.glob1(basedir, basename)
515 return self.format_list(basedir, listing)
517 def format_list(self, cr, parent_node, listing, ignore_err=True):
518 """Return an iterator object that yields the entries of given
519 directory emulating the "/bin/ls -lA" UNIX command output.
521 - (str) basedir: the parent directory node. Can be None
522 - (list) listing: a list of nodes
523 - (bool) ignore_err: when False raise exception if os.lstat()
526 On platforms which do not support the pwd and grp modules (such
527 as Windows), ownership is printed as "owner" and "group" as a
528 default, and number of hard links is always "1". On UNIX
529 systems, the actual owner, group, and number of links are
532 This is how output appears to client:
534 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
535 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
536 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
539 perms = filemode(node.unixperms) # permissions
541 size = node.content_length or 0L
542 uname = _to_decode(node.uuser)
543 gname = _to_decode(node.ugroup)
544 # stat.st_mtime could fail (-1) if last mtime is too old
545 # in which case we return the local time as last mtime
547 st_mtime = node.write_date or 0.0
548 if isinstance(st_mtime, basestring):
549 st_mtime = time.strptime(st_mtime, '%Y-%m-%d %H:%M:%S')
550 elif isinstance(st_mtime, float):
551 st_mtime = time.localtime(st_mtime)
552 mname=_get_month_name(time.strftime("%m", st_mtime ))
553 mtime = mname+' '+time.strftime("%d %H:%M", st_mtime)
555 mname=_get_month_name(time.strftime("%m"))
556 mtime = mname+' '+time.strftime("%d %H:%M")
558 if isinstance(fpath, (list, tuple)):
560 # formatting is matched with proftpd ls output
561 path=_to_decode(fpath)
562 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
566 def format_mlsx(self, cr, basedir, listing, perms, facts, ignore_err=True):
567 """Return an iterator object that yields the entries of a given
568 directory or of a single file in a form suitable with MLSD and
571 Every entry includes a list of "facts" referring the listed
572 element. See RFC-3659, chapter 7, to see what every single
575 - (str) basedir: the absolute dirname.
576 - (list) listing: the names of the entries in basedir
577 - (str) perms: the string referencing the user permissions.
578 - (str) facts: the list of "facts" to be returned.
579 - (bool) ignore_err: when False raise exception if os.stat()
582 Note that "facts" returned may change depending on the platform
583 and on what user specified by using the OPTS command.
585 This is how output could appear to the client issuing
588 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
589 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
590 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
592 permdir = ''.join([x for x in perms if x not in 'arw'])
593 permfile = ''.join([x for x in perms if x not in 'celmp'])
594 if ('w' in perms) or ('a' in perms) or ('f' in perms):
598 type = size = perm = modify = create = unique = mode = uid = gid = ""
605 perm = 'perm=%s;' %permdir
610 perm = 'perm=%s;' %permfile
612 size = 'size=%s;' % (node.content_length or 0L)
613 # last modification time
614 if 'modify' in facts:
616 st_mtime = node.write_date or 0.0
617 if isinstance(st_mtime, basestring):
618 st_mtime = time.strptime(st_mtime, '%Y-%m-%d %H:%M:%S')
619 elif isinstance(st_mtime, float):
620 st_mtime = time.localtime(st_mtime)
621 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S", st_mtime)
623 # stat.st_mtime could fail (-1) if last mtime is too old
625 if 'create' in facts:
626 # on Windows we can provide also the creation time
628 st_ctime = node.create_date or 0.0
629 if isinstance(st_ctime, basestring):
630 st_ctime = time.strptime(st_ctime, '%Y-%m-%d %H:%M:%S')
631 elif isinstance(st_mtime, float):
632 st_ctime = time.localtime(st_ctime)
633 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",st_ctime)
637 if 'unix.mode' in facts:
638 mode = 'unix.mode=%s;' %oct(node.unixperms & 0777)
639 if 'unix.uid' in facts:
640 uid = 'unix.uid=%s;' % _to_decode(node.uuser)
641 if 'unix.gid' in facts:
642 gid = 'unix.gid=%s;' % _to_decode(node.ugroup)
643 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
644 # posix platforms only; we get it by mixing st_dev and
645 # st_ino values which should be enough for granting an
646 # uniqueness for the file listed.
647 # The same approach is used by pure-ftpd.
648 # Implementors who want to provide unique fact on other
649 # platforms should use some platform-specific method (e.g.
650 # on Windows NTFS filesystems MTF records could be used).
651 # if 'unique' in facts: todo
652 # unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
654 if isinstance (path, (list, tuple)):
656 path=_to_decode(path)
657 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
658 mode, uid, gid, unique, path)
660 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: