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)
154 except EnvironmentError:
157 self._log.exception('Cannot locate item %s at node %s', objname, repr(node))
161 child = node.create_child(cr, objname, data=None)
162 return child.open_data(cr, mode)
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
172 res = node.open_data(cr, mode)
175 # ok, but need test more
177 def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
178 """A wrap around tempfile.mkstemp creating a file with a unique
179 name. Unlike mkstemp it returns an object with a file-like
182 raise NotImplementedError
184 text = not 'b' in mode
185 # for unique file , maintain version if duplicate file
190 pool = pooler.get_pool(node.context.dbname)
191 object=dir and dir.object or False
192 object2=dir and dir.object2 or False
193 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)])
195 pre = prefix.split('.')
196 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
197 #prefix = prefix + '.'
198 return self.create(dir,suffix+prefix,text)
203 def chdir(self, datacr):
204 if (not datacr) or datacr == (None, None, None):
209 raise OSError(1, 'Operation not permitted')
210 if datacr[1].type not in ('collection','database'):
211 raise OSError(2, 'Path is not a directory')
212 self.cwd = '/'+datacr[1].context.dbname + '/'
213 self.cwd += '/'.join(datacr[1].full_path())
214 self.cwd_node = datacr[1]
217 def mkdir(self, datacr, basename):
218 """Create the specified directory."""
219 cr, node, rem = datacr or (None, None, None)
221 raise OSError(1, 'Operation not permited.')
224 basename =_to_unicode(basename)
225 cdir = node.create_child_collection(cr, basename)
226 self._log.debug("Created child dir: %r", cdir)
229 self._log.exception('Cannot create dir "%s" at node %s', basename, repr(node))
230 raise OSError(1, 'Operation not permited.')
232 def close_cr(self, data):
237 def get_cr(self, pathname):
238 raise DeprecationWarning()
240 def get_crdata(self, line, mode='file'):
241 """ Get database cursor, node and remainder data, for commands
243 This is the helper function that will prepare the arguments for
244 any of the subsequent commands.
245 It returns a tuple in the form of:
246 @code ( cr, node, rem_path=None )
248 @param line An absolute or relative ftp path, as passed to the cmd.
249 @param mode A word describing the mode of operation, so that this
250 function behaves properly in the different commands.
252 path = self.ftpnorm(line)
253 if self.cwd_node is None:
254 if not os.path.isabs(path):
255 path = os.path.join(self.root, path)
257 if path == '/' and mode in ('list', 'cwd'):
258 return (None, None, None )
260 path = _to_unicode(os.path.normpath(path)) # again, for '/db/../ss'
261 if path == '.': path = ''
263 if os.path.isabs(path) and self.cwd_node is not None \
264 and path.startswith(self.cwd):
265 # make relative, so that cwd_node is used again
266 path = path[len(self.cwd):]
267 if path.startswith('/'):
270 p_parts = path.split('/') # hard-code the unix sep here, by spec.
272 assert '..' not in p_parts
275 if mode in ('create',):
276 rem_path = p_parts[-1]
277 p_parts = p_parts[:-1]
279 if os.path.isabs(path):
280 # we have to start from root, again
281 while p_parts[0] == '':
282 p_parts = p_parts[1:]
283 # self._log.debug("Path parts: %r ", p_parts)
285 raise IOError(errno.EPERM, 'Cannot perform operation at root dir')
287 if dbname not in self.db_list():
288 raise IOError(errno.ENOENT,'Invalid database path')
290 db = pooler.get_db(dbname)
292 raise OSError(1, 'Database cannot be used.')
295 uid = security.login(dbname, self.username, self.password)
301 raise OSError(2, 'Authentification Required.')
302 n = get_node_context(cr, uid, {})
303 node = n.get_uri(cr, p_parts[1:])
304 # self._log.debug("get_crdata(abs): %r" % ( (cr, node, rem_path),))
305 return (cr, node, rem_path)
307 # we never reach here if cwd_node is not set
308 if p_parts and p_parts[-1] == '':
309 p_parts = p_parts[:-1]
310 cr, uid = self.get_node_cr_uid(self.cwd_node)
312 node = self.cwd_node.get_uri(cr, p_parts)
315 if node is False and mode not in ('???'):
317 raise IOError(errno.ENOENT, 'Path does not exist')
318 # self._log.debug("get_crdata(rel): %r" % ( (cr, node, rem_path),))
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
366 cr = self.get_node_cr(node)
370 def remove(self, datacr):
372 if datacr[1].type == 'collection':
373 return self.rmdir(datacr)
374 elif datacr[1].type == 'file':
375 return self.rmfile(datacr)
376 raise OSError(1, 'Operation not permited.')
378 def rmfile(self, datacr):
379 """Remove the specified file."""
385 def rename(self, src, datacr):
386 """ Renaming operation, the effect depends on the src:
387 * A file: read, create and remove
388 * A directory: change the parent and reassign childs to ressource
392 nname = _to_unicode(datacr[2])
393 ret = src.move_to(cr, datacr[1], new_name=nname)
394 # API shouldn't wait for us to write the object
395 assert (ret is True) or (ret is False)
397 except Exception,err:
398 self._log.exception('Cannot rename "%s" to "%s" at "%s"', src, datacr[2], datacr[1])
399 raise OSError(1,'Operation not permited.')
401 def stat(self, node):
402 raise NotImplementedError()
404 # --- Wrapper methods around os.path.*
407 def isfile(self, node):
408 if node and (node.type in ('file','content')):
413 def islink(self, path):
414 """Return True if path is a symbolic link."""
417 def isdir(self, node):
418 """Return True if path is a directory."""
421 if node and (node.type in ('collection','database')):
425 def getsize(self, datacr):
426 """Return the size of the specified file in bytes."""
427 if not (datacr and datacr[1]):
429 if datacr[1].type in ('file', 'content'):
430 return datacr[1].content_length or 0L
434 def getmtime(self, datacr):
435 """Return the last modified time as a number of seconds since
439 if node.write_date or node.create_date:
440 dt = (node.write_date or node.create_date)[:19]
441 result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
443 result = time.mktime(time.localtime())
447 def realpath(self, path):
448 """Return the canonical version of path eliminating any
449 symbolic links encountered in the path (if they are
450 supported by the operating system).
455 def lexists(self, path):
456 """Return True if path refers to an existing path, including
457 a broken or circular symbolic link.
459 raise DeprecationWarning()
460 return path and True or False
464 # Ok, can be improved
465 def glob1(self, dirname, pattern):
466 """Return a list of files matching a dirname pattern
469 Unlike glob.glob1 raises exception if os.listdir() fails.
471 names = self.listdir(dirname)
472 if pattern[0] != '.':
473 names = filter(lambda x: x.path[0] != '.', names)
474 return fnmatch.filter(names, pattern)
476 # --- Listing utilities
478 # note: the following operations are no more blocking
480 def get_list_dir(self, datacr):
481 """"Return an iterator object that yields a directory listing
482 in a form suitable for LIST command.
486 elif self.isdir(datacr[1]):
487 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: