[TYPO] replace tinyerp per openerp
[odoo/odoo.git] / addons / document_ftp / ftpserver / abstracted_fs.py
1 # -*- encoding: utf-8 -*-
2
3 import os
4 import time
5 from tarfile import filemode
6 import logging
7 import errno
8
9 import glob
10 import fnmatch
11
12 import pooler
13 import netsvc
14 import sql_db
15
16 from service import security
17 from osv import osv
18 from document.nodes import get_node_context
19
20 def _get_month_name(month):
21     month=int(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'
34
35 from ftpserver import _to_decode, _to_unicode
36
37
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.
42
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.
46
47     Instance attributes:
48      - (str) root: the user home directory.
49      - (str) cwd: the current working directory.
50      - (str) rnfr: source file to be renamed.
51
52     """
53
54     def __init__(self):
55         self.root = None
56         self.cwd = '/'
57         self.cwd_node = None
58         self.rnfr = None
59         self._log = logging.getLogger(__name__)
60
61     # Ok
62     def db_list(self):
63         """Get the list of available databases, with FTPd support
64         """
65         s = netsvc.ExportService.getService('db')
66         result = s.exp_list(document=True)
67         self.db_name_list = []
68         for db_name in result:
69             db, cr = None, None
70             try:
71                 try:
72                     db = sql_db.db_connect(db_name)
73                     cr = db.cursor()
74                     cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
75                     if not cr.fetchone():
76                         continue
77
78                     cr.execute("SELECT id FROM ir_module_module WHERE name = 'document_ftp' AND state IN ('installed', 'to install', 'to upgrade') ")
79                     res = cr.fetchone()
80                     if res and len(res):
81                         self.db_name_list.append(db_name)
82                     cr.commit()
83                 except Exception:
84                     self._log.warning('Cannot use db "%s"', db_name)
85             finally:
86                 if cr is not None:
87                     cr.close()
88         return self.db_name_list
89
90     def ftpnorm(self, ftppath):
91         """Normalize a "virtual" ftp pathname (tipically the raw string
92         coming from client).
93
94         Pathname returned is relative!.
95         """
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
103         while p[:2] == '//':
104             p = p[1:]
105         if p == '.':
106             return ''
107         return p
108
109     def get_cwd(self):
110         """ return the cwd, decoded in utf"""
111         return _to_decode(self.cwd)
112
113     def ftp2fs(self, path_orig, data):
114         raise DeprecationWarning()
115
116     def fs2ftp(self, node):
117         """ Return the string path of a node, in ftp form
118         """
119         res='/'
120         if node:
121             paths = node.full_path()
122             res = '/' + node.context.dbname + '/' +  \
123                 _to_decode(os.path.join(*paths))
124
125         return res
126
127     def validpath(self, path):
128         """Check whether the path belongs to user's home directory.
129         Expected argument is a datacr tuple
130         """
131         # TODO: are we called for "/" ?
132         return isinstance(path, tuple) and path[1] and True or False
133
134     # --- Wrapper methods around open() and tempfile.mkstemp
135
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
139         """
140         objname = _to_unicode(objname)
141         cr , node, rem = datacr
142         try:
143             child = node.child(cr, objname)
144             if child:
145                 if child.type not in ('file','content'):
146                     raise OSError(1, 'Operation not permited.')
147
148                 ret = child.open_data(cr, mode)
149                 cr.commit()
150                 assert ret, "Cannot create descriptor for %r: %r" % (child, ret)
151                 return ret
152         except EnvironmentError:
153             raise
154         except Exception:
155             self._log.exception('Cannot locate item %s at node %s', objname, repr(node))
156             pass
157
158         try:
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
162             cr.commit()
163             return ret
164         except EnvironmentError:
165             raise
166         except Exception:
167             self._log.exception('Cannot create item %s at node %s', objname, repr(node))
168             raise OSError(1, 'Operation not permited.')
169
170     def open(self, datacr, mode):
171         if not (datacr and datacr[1]):
172             raise OSError(1, 'Operation not permited.')
173         # Reading operation
174         cr, node, rem = datacr
175         try:
176             res = node.open_data(cr, mode)
177             cr.commit()
178         except TypeError:
179             raise IOError(errno.EINVAL, "No data")
180         return res
181
182     # ok, but need test more
183
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
187         interface.
188         """
189         raise NotImplementedError # TODO
190
191         text = not 'b' in mode
192         # for unique file , maintain version if duplicate file
193         if dir:
194             cr = dir.cr
195             uid = dir.uid
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)])
200             if len(res):
201                 pre = prefix.split('.')
202                 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
203         return self.create(dir,suffix+prefix,text)
204
205
206
207     # Ok
208     def chdir(self, datacr):
209         if (not datacr) or datacr == (None, None, None):
210             self.cwd = '/'
211             self.cwd_node = None
212             return None
213         if not datacr[1]:
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]
220
221     # Ok
222     def mkdir(self, datacr, basename):
223         """Create the specified directory."""
224         cr, node, rem = datacr or (None, None, None)
225         if not node:
226             raise OSError(1, 'Operation not permited.')
227
228         try:
229             basename =_to_unicode(basename)
230             cdir = node.create_child_collection(cr, basename)
231             self._log.debug("Created child dir: %r", cdir)
232             cr.commit()
233         except Exception:
234             self._log.exception('Cannot create dir "%s" at node %s', basename, repr(node))
235             raise OSError(1, 'Operation not permited.')
236
237     def close_cr(self, data):
238         if data and data[0]:
239             data[0].close()
240         return True
241
242     def get_cr(self, pathname):
243         raise DeprecationWarning()
244
245     def get_crdata(self, line, mode='file'):
246         """ Get database cursor, node and remainder data, for commands
247
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 )
252
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.
256         """
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)
261
262         if path == '/' and mode in ('list', 'cwd'):
263             return (None, None, None )
264
265         path = _to_unicode(os.path.normpath(path)) # again, for '/db/../ss'
266         if path == '.': path = ''
267
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('/'):
273                 path = path[1:]
274
275         p_parts = path.split('/') # hard-code the unix sep here, by spec.
276
277         assert '..' not in p_parts
278
279         rem_path = None
280         if mode in ('create',):
281             rem_path = p_parts[-1]
282             p_parts = p_parts[:-1]
283
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)
289             if not p_parts:
290                 raise IOError(errno.EPERM, 'Cannot perform operation at root dir')
291             dbname = p_parts[0]
292             if dbname not in self.db_list():
293                 raise IOError(errno.ENOENT,'Invalid database path: %s' % dbname)
294             try:
295                 db = pooler.get_db(dbname)
296             except Exception:
297                 raise OSError(1, 'Database cannot be used.')
298             cr = db.cursor()
299             try:
300                 uid = security.login(dbname, self.username, self.password)
301             except Exception:
302                 cr.close()
303                 raise
304             if not uid:
305                 cr.close()
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)
310         else:
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)
315             if p_parts:
316                 node = self.cwd_node.get_uri(cr, p_parts)
317             else:
318                 node = self.cwd_node
319             if node is False and mode not in ('???'):
320                 cr.close()
321                 raise IOError(errno.ENOENT, 'Path does not exist')
322             return (cr, node, rem_path)
323
324     def get_node_cr_uid(self, node):
325         """ Get cr, uid, pool from a node
326         """
327         assert node
328         db = pooler.get_db(node.context.dbname)
329         return db.cursor(), node.context.uid
330
331     def get_node_cr(self, node):
332         """ Get the cursor for the database of a node
333
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.
336         """
337         return self.get_node_cr_uid(node)[0]
338
339     def listdir(self, datacr):
340         """List the content of a directory."""
341         class false_node(object):
342             write_date = 0.0
343             create_date = 0.0
344             unixperms = 040550
345             content_length = 0L
346             uuser = 'root'
347             ugroup = 'root'
348             type = 'database'
349
350             def __init__(self, db):
351                 self.path = db
352
353         if datacr[1] is None:
354             result = []
355             for db in self.db_list():
356                 try:
357                     result.append(false_node(db))
358                 except osv.except_osv:
359                     pass
360             return result
361         cr, node, rem = datacr
362         res = node.children(cr)
363         return res
364
365     def rmdir(self, datacr):
366         """Remove the specified directory."""
367         cr, node, rem = datacr
368         assert node
369         node.rmcol(cr)
370         cr.commit()
371
372     def remove(self, datacr):
373         assert datacr[1]
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.')
379
380     def rmfile(self, datacr):
381         """Remove the specified file."""
382         assert datacr[1]
383         cr = datacr[0]
384         datacr[1].rm(cr)
385         cr.commit()
386
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
391         """
392         cr = datacr[0]
393         try:
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)
398             cr.commit()
399         except EnvironmentError:
400             raise
401         except Exception:
402             self._log.exception('Cannot rename "%s" to "%s" at "%s"', src, datacr[2], datacr[1])
403             raise OSError(1,'Operation not permited.')
404
405     def stat(self, node):
406         raise NotImplementedError()
407
408     # --- Wrapper methods around os.path.*
409
410     # Ok
411     def isfile(self, node):
412         if node and (node.type in ('file','content')):
413             return True
414         return False
415
416     # Ok
417     def islink(self, path):
418         """Return True if path is a symbolic link."""
419         return False
420
421     def isdir(self, node):
422         """Return True if path is a directory."""
423         if node is None:
424             return True
425         if node and (node.type in ('collection','database')):
426             return True
427         return False
428
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
435         return 0L
436
437     # Ok
438     def getmtime(self, datacr):
439         """Return the last modified time as a number of seconds since
440         the epoch."""
441
442         node = datacr[1]
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'))
446         else:
447             result = time.mktime(time.localtime())
448         return result
449
450     # Ok
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).
455         """
456         return path
457
458     # Ok
459     def lexists(self, path):
460         """Return True if path refers to an existing path, including
461         a broken or circular symbolic link.
462         """
463         raise DeprecationWarning()
464         return path and True or False
465
466     exists = lexists
467
468     # Ok, can be improved
469     def glob1(self, dirname, pattern):
470         """Return a list of files matching a dirname pattern
471         non-recursively.
472
473         Unlike glob.glob1 raises exception if os.listdir() fails.
474         """
475         names = self.listdir(dirname)
476         if pattern[0] != '.':
477             names = filter(lambda x: x.path[0] != '.', names)
478         return fnmatch.filter(names, pattern)
479
480     # --- Listing utilities
481
482     # note: the following operations are no more blocking
483
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.
487         """
488         if not datacr:
489             return None
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]])
497
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.
502
503          - (str) rawline: the raw string passed by client as command
504          argument.
505         """
506         ftppath = self.ftpnorm(rawline)
507         if not glob.has_magic(ftppath):
508             return self.get_list_dir(self.ftp2fs(rawline, datacr))
509         else:
510             basedir, basename = os.path.split(ftppath)
511             if glob.has_magic(basedir):
512                 return iter(['Directory recursion not supported.\r\n'])
513             else:
514                 basedir = self.ftp2fs(basedir, datacr)
515                 listing = self.glob1(basedir, basename)
516                 if listing:
517                     listing.sort()
518                 return self.format_list(basedir, listing)
519
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.
523
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()
527          call fails.
528
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
533         printed.
534
535         This is how output appears to client:
536
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
540         """
541         for node in listing:
542             perms = filemode(node.unixperms)  # permissions
543             nlinks = 1
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
549             try:
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)
557             except ValueError:
558                 mname=_get_month_name(time.strftime("%m"))
559                 mtime = mname+' '+time.strftime("%d %H:%M")
560             fpath = node.path
561             if isinstance(fpath, (list, tuple)):
562                 fpath = fpath[-1]
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,
566                                                      size, mtime, path)
567
568     # Ok
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
572         MLST commands.
573
574         Every entry includes a list of "facts" referring the listed
575         element.  See RFC-3659, chapter 7, to see what every single
576         fact stands for.
577
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()
583          call fails.
584
585         Note that "facts" returned may change depending on the platform
586         and on what user specified by using the OPTS command.
587
588         This is how output could appear to the client issuing
589         a MLSD request:
590
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
594         """
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):
598             permdir += 'c'
599         if 'd' in perms:
600             permdir += 'p'
601         type = size = perm = modify = create = unique = mode = uid = gid = ""
602         for node in listing:
603             # type + perm
604             if self.isdir(node):
605                 if 'type' in facts:
606                     type = 'type=dir;'
607                 if 'perm' in facts:
608                     perm = 'perm=%s;' %permdir
609             else:
610                 if 'type' in facts:
611                     type = 'type=file;'
612                 if 'perm' in facts:
613                     perm = 'perm=%s;' %permfile
614             if 'size' in facts:
615                 size = 'size=%s;' % (node.content_length or 0L)
616             # last modification time
617             if 'modify' in facts:
618                 try:
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)
625                 except ValueError:
626                     # stat.st_mtime could fail (-1) if last mtime is too old
627                     modify = ""
628             if 'create' in facts:
629                 # on Windows we can provide also the creation time
630                 try:
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)
637                 except ValueError:
638                     create = ""
639             # UNIX only
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)
656             path = node.path
657             if isinstance (path, (list, tuple)):
658                 path = path[-1]
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)
662
663 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
664