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
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 EnvironmentError:
400 self._log.exception('Cannot rename "%s" to "%s" at "%s"', src, datacr[2], datacr[1])
401 raise OSError(1,'Operation not permited.')
403 def stat(self, node):
404 raise NotImplementedError()
406 # --- Wrapper methods around os.path.*
409 def isfile(self, node):
410 if node and (node.type in ('file','content')):
415 def islink(self, path):
416 """Return True if path is a symbolic link."""
419 def isdir(self, node):
420 """Return True if path is a directory."""
423 if node and (node.type in ('collection','database')):
427 def getsize(self, datacr):
428 """Return the size of the specified file in bytes."""
429 if not (datacr and datacr[1]):
430 raise IOError(errno.ENOENT, "No such file or directory")
431 if datacr[1].type in ('file', 'content'):
432 return datacr[1].get_data_len(datacr[0]) or 0L
436 def getmtime(self, datacr):
437 """Return the last modified time as a number of seconds since
441 if node.write_date or node.create_date:
442 dt = (node.write_date or node.create_date)[:19]
443 result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
445 result = time.mktime(time.localtime())
449 def realpath(self, path):
450 """Return the canonical version of path eliminating any
451 symbolic links encountered in the path (if they are
452 supported by the operating system).
457 def lexists(self, path):
458 """Return True if path refers to an existing path, including
459 a broken or circular symbolic link.
461 raise DeprecationWarning()
462 return path and True or False
466 # Ok, can be improved
467 def glob1(self, dirname, pattern):
468 """Return a list of files matching a dirname pattern
471 Unlike glob.glob1 raises exception if os.listdir() fails.
473 names = self.listdir(dirname)
474 if pattern[0] != '.':
475 names = filter(lambda x: x.path[0] != '.', names)
476 return fnmatch.filter(names, pattern)
478 # --- Listing utilities
480 # note: the following operations are no more blocking
482 def get_list_dir(self, datacr):
483 """"Return an iterator object that yields a directory listing
484 in a form suitable for LIST command.
488 elif self.isdir(datacr[1]):
489 listing = self.listdir(datacr)
490 return self.format_list(datacr[0], datacr[1], listing)
491 # if path is a file or a symlink we return information about it
492 elif self.isfile(datacr[1]):
493 par = datacr[1].parent
494 return self.format_list(datacr[0], par, [datacr[1]])
496 def get_stat_dir(self, rawline, datacr):
497 """Return an iterator object that yields a list of files
498 matching a dirname pattern non-recursively in a form
499 suitable for STAT command.
501 - (str) rawline: the raw string passed by client as command
504 ftppath = self.ftpnorm(rawline)
505 if not glob.has_magic(ftppath):
506 return self.get_list_dir(self.ftp2fs(rawline, datacr))
508 basedir, basename = os.path.split(ftppath)
509 if glob.has_magic(basedir):
510 return iter(['Directory recursion not supported.\r\n'])
512 basedir = self.ftp2fs(basedir, datacr)
513 listing = self.glob1(basedir, basename)
516 return self.format_list(basedir, listing)
518 def format_list(self, cr, parent_node, listing, ignore_err=True):
519 """Return an iterator object that yields the entries of given
520 directory emulating the "/bin/ls -lA" UNIX command output.
522 - (str) basedir: the parent directory node. Can be None
523 - (list) listing: a list of nodes
524 - (bool) ignore_err: when False raise exception if os.lstat()
527 On platforms which do not support the pwd and grp modules (such
528 as Windows), ownership is printed as "owner" and "group" as a
529 default, and number of hard links is always "1". On UNIX
530 systems, the actual owner, group, and number of links are
533 This is how output appears to client:
535 -rw-rw-rw- 1 owner group 7045120 Sep 02 3:47 music.mp3
536 drwxrwxrwx 1 owner group 0 Aug 31 18:50 e-books
537 -rw-rw-rw- 1 owner group 380 Sep 02 3:40 module.py
540 perms = filemode(node.unixperms) # permissions
542 size = node.content_length or 0L
543 uname = _to_decode(node.uuser)
544 gname = _to_decode(node.ugroup)
545 # stat.st_mtime could fail (-1) if last mtime is too old
546 # in which case we return the local time as last mtime
548 st_mtime = node.write_date or 0.0
549 if isinstance(st_mtime, basestring):
550 st_mtime = time.strptime(st_mtime, '%Y-%m-%d %H:%M:%S')
551 elif isinstance(st_mtime, float):
552 st_mtime = time.localtime(st_mtime)
553 mname=_get_month_name(time.strftime("%m", st_mtime ))
554 mtime = mname+' '+time.strftime("%d %H:%M", st_mtime)
556 mname=_get_month_name(time.strftime("%m"))
557 mtime = mname+' '+time.strftime("%d %H:%M")
559 if isinstance(fpath, (list, tuple)):
561 # formatting is matched with proftpd ls output
562 path=_to_decode(fpath)
563 yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
567 def format_mlsx(self, cr, basedir, listing, perms, facts, ignore_err=True):
568 """Return an iterator object that yields the entries of a given
569 directory or of a single file in a form suitable with MLSD and
572 Every entry includes a list of "facts" referring the listed
573 element. See RFC-3659, chapter 7, to see what every single
576 - (str) basedir: the absolute dirname.
577 - (list) listing: the names of the entries in basedir
578 - (str) perms: the string referencing the user permissions.
579 - (str) facts: the list of "facts" to be returned.
580 - (bool) ignore_err: when False raise exception if os.stat()
583 Note that "facts" returned may change depending on the platform
584 and on what user specified by using the OPTS command.
586 This is how output could appear to the client issuing
589 type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
590 type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
591 type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
593 permdir = ''.join([x for x in perms if x not in 'arw'])
594 permfile = ''.join([x for x in perms if x not in 'celmp'])
595 if ('w' in perms) or ('a' in perms) or ('f' in perms):
599 type = size = perm = modify = create = unique = mode = uid = gid = ""
606 perm = 'perm=%s;' %permdir
611 perm = 'perm=%s;' %permfile
613 size = 'size=%s;' % (node.content_length or 0L)
614 # last modification time
615 if 'modify' in facts:
617 st_mtime = node.write_date or 0.0
618 if isinstance(st_mtime, basestring):
619 st_mtime = time.strptime(st_mtime, '%Y-%m-%d %H:%M:%S')
620 elif isinstance(st_mtime, float):
621 st_mtime = time.localtime(st_mtime)
622 modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S", st_mtime)
624 # stat.st_mtime could fail (-1) if last mtime is too old
626 if 'create' in facts:
627 # on Windows we can provide also the creation time
629 st_ctime = node.create_date or 0.0
630 if isinstance(st_ctime, basestring):
631 st_ctime = time.strptime(st_ctime, '%Y-%m-%d %H:%M:%S')
632 elif isinstance(st_mtime, float):
633 st_ctime = time.localtime(st_ctime)
634 create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",st_ctime)
638 if 'unix.mode' in facts:
639 mode = 'unix.mode=%s;' %oct(node.unixperms & 0777)
640 if 'unix.uid' in facts:
641 uid = 'unix.uid=%s;' % _to_decode(node.uuser)
642 if 'unix.gid' in facts:
643 gid = 'unix.gid=%s;' % _to_decode(node.ugroup)
644 # We provide unique fact (see RFC-3659, chapter 7.5.2) on
645 # posix platforms only; we get it by mixing st_dev and
646 # st_ino values which should be enough for granting an
647 # uniqueness for the file listed.
648 # The same approach is used by pure-ftpd.
649 # Implementors who want to provide unique fact on other
650 # platforms should use some platform-specific method (e.g.
651 # on Windows NTFS filesystems MTF records could be used).
652 # if 'unique' in facts: todo
653 # unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
655 if isinstance (path, (list, tuple)):
657 path=_to_decode(path)
658 yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
659 mode, uid, gid, unique, path)
661 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: