Document_ftp: *only* serve databases that have the module installed.
[odoo/odoo.git] / addons / document_ftp / ftpserver / abstracted_fs.py
1 # -*- encoding: utf-8 -*-
2 import os
3 import time
4 from tarfile import filemode
5 import StringIO
6 import base64
7 import logging
8
9 import glob
10 import fnmatch
11
12 import pooler
13 import netsvc
14 import os
15 from service import security
16 from osv import osv
17 #from document.nodes import node_res_dir, node_res_obj
18 from document.nodes import get_node_context
19 import stat
20
21 def _get_month_name(month):
22     month=int(month)
23     if month==1:return 'Jan'
24     elif month==2:return 'Feb'
25     elif month==3:return 'Mar'
26     elif month==4:return 'Apr'
27     elif month==5:return 'May'
28     elif month==6:return 'Jun'
29     elif month==7:return 'Jul'
30     elif month==8:return 'Aug'
31     elif month==9:return 'Sep'
32     elif month==10:return 'Oct'
33     elif month==11:return 'Nov'
34     elif month==12:return 'Dec'
35
36 def _to_unicode(s):
37     try:
38         return s.decode('utf-8')
39     except UnicodeError:
40         try:
41             return s.decode('latin')
42         except UnicodeError:
43             try:
44                 return s.encode('ascii')
45             except UnicodeError:
46                 return s
47
48 def _to_decode(s):
49     try:
50         return s.encode('utf-8')
51     except UnicodeError:
52         try:
53             return s.encode('latin')
54         except UnicodeError:
55             try:
56                 return s.decode('ascii')
57             except UnicodeError:
58                 return s  
59
60 class abstracted_fs(object):
61     """A class used to interact with the file system, providing a high
62     level, cross-platform interface compatible with both Windows and
63     UNIX style filesystems.
64
65     It provides some utility methods and some wraps around operations
66     involved in file creation and file system operations like moving
67     files or removing directories.
68
69     Instance attributes:
70      - (str) root: the user home directory.
71      - (str) cwd: the current working directory.
72      - (str) rnfr: source file to be renamed.
73     """
74
75     # Ok
76     def db_list(self):
77         #return pooler.pool_dic.keys()
78         s = netsvc.ExportService.getService('db')
79         result = s.exp_list(document=True)
80         self.db_name_list = []
81         for db_name in result:
82             db, cr = None, None
83             try:
84                 try:
85                     db = pooler.get_db_only(db_name)
86                     cr = db.cursor()
87                     cr.execute("SELECT 1 FROM pg_class WHERE relkind = 'r' AND relname = 'ir_module_module'")
88                     if not cr.fetchone():
89                         continue
90     
91                     cr.execute("SELECT id FROM ir_module_module WHERE name = 'document_ftp' AND state='installed' ")
92                     res = cr.fetchone()
93                     if res and len(res):
94                         self.db_name_list.append(db_name)
95                     cr.commit()
96                 except Exception:
97                     self._log.warning('Cannot use db "%s"', db_name)
98             finally:
99                 if cr is not None:
100                     cr.close()
101                 #if db is not None:
102                 #    pooler.close_db(db_name)        
103         return self.db_name_list
104
105     # Ok
106     def __init__(self):
107         self.root = None
108         self.cwd = '/'
109         self.rnfr = None
110         self._log = logging.getLogger('FTP.fs')
111
112     # --- Pathname / conversion utilities
113
114     # Ok
115     def ftpnorm(self, ftppath):
116         """Normalize a "virtual" ftp pathname (tipically the raw string
117         coming from client) depending on the current working directory.
118
119         Example (having "/foo" as current working directory):
120         'x' -> '/foo/x'
121
122         Note: directory separators are system independent ("/").
123         Pathname returned is always absolutized.
124         """
125         if os.path.isabs(ftppath):
126             p = os.path.normpath(ftppath)
127         else:
128             p = os.path.normpath(os.path.join(self.cwd, ftppath))
129         # normalize string in a standard web-path notation having '/'
130         # as separator.
131         p = p.replace("\\", "/")
132         # os.path.normpath supports UNC paths (e.g. "//a/b/c") but we
133         # don't need them.  In case we get an UNC path we collapse
134         # redundant separators appearing at the beginning of the string
135         while p[:2] == '//':
136             p = p[1:]
137         # Anti path traversal: don't trust user input, in the event
138         # that self.cwd is not absolute, return "/" as a safety measure.
139         # This is for extra protection, maybe not really necessary.
140         if not os.path.isabs(p):
141             p = "/"
142         return p
143
144     # Ok
145     def ftp2fs(self, path_orig, data):
146         path = self.ftpnorm(path_orig)
147         if not data or (path and path=='/'):
148             return None               
149         path2 = filter(None,path.split('/'))[1:]
150         (cr, uid) = data
151         if len(path2):     
152             path2[-1]=_to_unicode(path2[-1])
153        
154         ctx = get_node_context(cr, uid, {})
155         res = ctx.get_uri(cr, path2[:])
156         if not res:
157             raise OSError(2, 'Not such file or directory.')
158         return res
159
160     # Ok
161     def fs2ftp(self, node):        
162         res='/'
163         if node:
164             paths = node.full_path()
165             paths = map(lambda x: '/' +x, paths)
166             res = os.path.normpath(''.join(paths))
167             res = res.replace("\\", "/")        
168             while res[:2] == '//':
169                 res = res[1:]
170             res = '/' + node.context.dbname + '/' + _to_decode(res)            
171             
172         #res = node and ('/' + node.cr.dbname + '/' + _to_decode(self.ftpnorm(node.path))) or '/'
173         return res
174
175     # Ok
176     def validpath(self, path):
177         """Check whether the path belongs to user's home directory.
178         Expected argument is a "real" filesystem pathname.
179
180         If path is a symbolic link it is resolved to check its real
181         destination.
182
183         Pathnames escaping from user's root directory are considered
184         not valid.
185         """
186         return path and True or False
187
188     # --- Wrapper methods around open() and tempfile.mkstemp
189
190     # Ok
191     def create(self, node, objname, mode):
192         objname = _to_unicode(objname) 
193         cr = None       
194         try:
195             uid = node.context.uid
196             pool = pooler.get_pool(node.context.dbname)
197             cr = pooler.get_db(node.context.dbname).cursor()
198             child = node.child(cr, objname)
199             if child:
200                 if child.type in ('collection','database'):
201                     raise OSError(1, 'Operation not permited.')
202                 if child.type == 'content':
203                     s = content_wrapper(node.context.dbname, uid, pool, child)
204                     return s
205             fobj = pool.get('ir.attachment')
206             ext = objname.find('.') >0 and objname.split('.')[1] or False
207
208             # TODO: test if already exist and modify in this case if node.type=file
209             ### checked already exits
210             object2 = False
211             if isinstance(node, node_res_obj):
212                 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False            
213             
214             cid = False
215             object = node.context._dirobj.browse(cr, uid, node.dir_id)
216             where = [('name','=',objname)]
217             if object and (object.type in ('directory')) or object2:
218                 where.append(('parent_id','=',object.id))
219             else:
220                 where.append(('parent_id','=',False))
221
222             if object2:
223                 where += [('res_id','=',object2.id),('res_model','=',object2._name)]
224             cids = fobj.search(cr, uid, where)
225             if len(cids):
226                 cid = cids[0]
227
228             if not cid:
229                 val = {
230                     'name': objname,
231                     'datas_fname': objname,
232                     'parent_id' : node.dir_id, 
233                     'datas': '',
234                     'file_size': 0L,
235                     'file_type': ext,
236                     'store_method' :  (object.storage_id.type == 'filestore' and 'fs')\
237                                  or  (object.storage_id.type == 'db' and 'db')
238                 }
239                 if object and (object.type in ('directory')) or not object2:
240                     val['parent_id']= object and object.id or False
241                 partner = False
242                 if object2:
243                     if 'partner_id' in object2 and object2.partner_id.id:
244                         partner = object2.partner_id.id
245                     if object2._name == 'res.partner':
246                         partner = object2.id
247                     val.update( {
248                         'res_model': object2._name,
249                         'partner_id': partner,
250                         'res_id': object2.id
251                     })
252                 cid = fobj.create(cr, uid, val, context={})
253             cr.commit()
254
255             s = file_wrapper('', cid, node.context.dbname, uid, )
256             return s
257         except Exception,e:
258             self._log.exception('Cannot create item %s at node %s', objname, repr(node))
259             raise OSError(1, 'Operation not permited.')
260         finally:
261             if cr:
262                 cr.close()
263
264     def open(self, node, mode):
265         if not node:
266             raise OSError(1, 'Operation not permited.')
267         # Reading operation
268         cr = pooler.get_db(node.context.dbname).cursor()
269         try:
270             res = node.open_data(cr, mode)
271         finally:
272             cr.close()
273         return res
274
275     # ok, but need test more
276
277     def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
278         """A wrap around tempfile.mkstemp creating a file with a unique
279         name.  Unlike mkstemp it returns an object with a file-like
280         interface.
281         """
282         raise 'Not Yet Implemented'
283 #        class FileWrapper:
284 #            def __init__(self, fd, name):
285 #                self.file = fd
286 #                self.name = name
287 #            def __getattr__(self, attr):
288 #                return getattr(self.file, attr)
289 #
290 #        text = not 'b' in mode
291 #        # max number of tries to find out a unique file name
292 #        tempfile.TMP_MAX = 50
293 #        fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
294 #        file = os.fdopen(fd, mode)
295 #        return FileWrapper(file, name)
296
297         text = not 'b' in mode
298         # for unique file , maintain version if duplicate file
299         if dir:
300             cr = dir.cr
301             uid = dir.uid
302             pool = pooler.get_pool(node.context.dbname)
303             object=dir and dir.object or False
304             object2=dir and dir.object2 or False
305             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)])
306             if len(res):
307                 pre = prefix.split('.')
308                 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
309             #prefix = prefix + '.'
310         return self.create(dir,suffix+prefix,text)
311
312
313
314     # Ok
315     def chdir(self, path):        
316         if not path:
317             self.cwd = '/'
318             return None        
319         if path.type in ('collection','database'):
320             self.cwd = self.fs2ftp(path)
321         elif path.type in ('file'):            
322             parent_path = path.full_path()[:-1]            
323             self.cwd = os.path.normpath(''.join(parent_path))
324         else:
325             raise OSError(1, 'Operation not permited.')
326
327     # Ok
328     def mkdir(self, node, basename):
329         """Create the specified directory."""
330         cr = False
331         if not node:
332             raise OSError(1, 'Operation not permited.')
333         try:
334             basename =_to_unicode(basename)
335             cr = pooler.get_db(node.context.dbname).cursor()
336             uid = node.context.uid
337             pool = pooler.get_pool(node.context.dbname)
338             object2 = False            
339             if isinstance(node, node_res_obj):
340                 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False            
341             obj = node.context._dirobj.browse(cr, uid, node.dir_id)            
342             if obj and (obj.type == 'ressource') and not object2:
343                 raise OSError(1, 'Operation not permited.')
344             val = {
345                 'name': basename,
346                 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
347                 'ressource_id': object2 and object2.id or False,
348                 'parent_id' : False
349             }
350             if (obj and (obj.type in ('directory'))) or not object2:                
351                 val['parent_id'] =  obj and obj.id or False            
352             # Check if it alreayd exists !
353             pool.get('document.directory').create(cr, uid, val)
354             cr.commit()            
355         except Exception,e:
356             self._log.exception('Cannot create dir "%s" at node %s', basename, repr(node))
357             raise OSError(1, 'Operation not permited.')
358         finally:
359             if cr: cr.close()
360
361     def close_cr(self, data):
362         if data:
363             data[0].close()
364         return True
365
366     def get_cr(self, path):
367         path = self.ftpnorm(path)
368         if path=='/':
369             return None
370         dbname = path.split('/')[1]
371         if dbname not in self.db_list():
372             return None
373         try:
374             db = pooler.get_db(dbname)
375         except Exception:
376             raise OSError(1, 'Operation not permited.')
377         cr = db.cursor()
378         try:
379             uid = security.login(dbname, self.username, self.password)
380         except Exception:
381             cr.close()
382             raise
383         if not uid:
384             cr.close()
385             raise OSError(2, 'Authentification Required.')
386         return cr, uid
387
388     def get_node_cr_uid(self, node):
389         """ Get cr, uid, pool from a node
390         """
391         db = pooler.get_db(node.context.dbname)
392         return db.cursor(), node.context.uid
393         
394     def get_node_cr(self, node):
395         """ Get the cursor for the database of a node
396         
397         The cursor is the only thing that a node will not store 
398         persistenly, so we have to obtain a new one for each call.
399         """
400         return self.get_node_cr_uid(node)[0]
401         
402     def listdir(self, path):
403         """List the content of a directory."""
404         class false_node(object):
405             write_date = None
406             create_date = None
407             type = 'database'
408             def __init__(self, db):
409                 self.path = '/'+db
410
411         if path is None:
412             result = []
413             for db in self.db_list():
414                 try:
415                     result.append(false_node(db))
416                 except osv.except_osv:
417                     pass
418             return result
419         cr = self.get_node_cr(path)
420         res = path.children(cr)
421         cr.close()
422         return res
423
424     def rmdir(self, node):
425         """Remove the specified directory."""
426         assert node
427         cr = self.get_node_cr(node)
428         try:
429             node.rmcol(cr)
430             cr.commit()
431         finally:
432             cr.close()
433
434     def remove(self, node):
435         assert node
436         if node.type == 'collection':
437             return self.rmdir(node)
438         elif node.type == 'file':
439             return self.rmfile(node)
440         raise OSError(1, 'Operation not permited.')
441
442     def rmfile(self, node):
443         """Remove the specified file."""
444         assert node
445         cr = self.get_node_cr(node)
446         try:
447             node.rm(cr)
448             cr.commit()
449         finally:
450             cr.close()
451
452     # Ok
453     def rename(self, src, dst_basedir, dst_basename):
454         """
455             Renaming operation, the effect depends on the src:
456             * A file: read, create and remove
457             * A directory: change the parent and reassign childs to ressource
458         """
459         cr = False
460         try:
461             # FIXME! wrong code here, doesn't use the node API
462             dst_basename = _to_unicode(dst_basename)
463             cr = pooler.get_db(src.context.dbname).cursor()
464             uid = src.context.uid                       
465             if src.type == 'collection':
466                 obj2 = False
467                 dst_obj2 = False
468                 pool = pooler.get_pool(src.context.dbname)
469                 if isinstance(src, node_res_obj):
470                     obj2 = src and pool.get(src.context.context['res_model']).browse(cr, uid, src.context.context['res_id']) or False            
471                 obj = src.context._dirobj.browse(cr, uid, src.dir_id)                 
472                 if isinstance(dst_basedir, node_res_obj):
473                     dst_obj2 = dst_basedir and pool.get(dst_basedir.context.context['res_model']).browse(cr, uid, dst_basedir.context.context['res_id']) or False
474                 dst_obj = dst_basedir.context._dirobj.browse(cr, uid, dst_basedir.dir_id)                 
475                 if obj._table_name <> 'document.directory':
476                     raise OSError(1, 'Operation not permited.')
477                 result = {
478                     'directory': [],
479                     'attachment': []
480                 }
481                 # Compute all childs to set the new ressource ID                
482                 child_ids = [src]
483                 while len(child_ids):
484                     node = child_ids.pop(0)                    
485                     child_ids += node.children(cr)                        
486                     if node.type == 'collection':
487                         object2 = False                                            
488                         if isinstance(node, node_res_obj):                  
489                             object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False                           
490                         object = node.context._dirobj.browse(cr, uid, node.dir_id)                         
491                         result['directory'].append(object.id)
492                         if (not object.ressource_id) and object2:
493                             raise OSError(1, 'Operation not permited.')
494                     elif node.type == 'file':
495                         result['attachment'].append(object.id)
496                 
497                 if obj2 and not obj.ressource_id:
498                     raise OSError(1, 'Operation not permited.')
499                 
500                 if (dst_obj and (dst_obj.type in ('directory'))) or not dst_obj2:
501                     parent_id = dst_obj and dst_obj.id or False
502                 else:
503                     parent_id = False              
504                 
505                 
506                 if dst_obj2:                    
507                     ressource_type_id = pool.get('ir.model').search(cr, uid, [('model','=',dst_obj2._name)])[0]
508                     ressource_id = dst_obj2.id                    
509                     ressource_model = dst_obj2._name                    
510                     if dst_obj2._name == 'res.partner':
511                         partner_id = dst_obj2.id
512                     else:                                                
513                         partner_id = pool.get(dst_obj2._name).fields_get(cr, uid, ['partner_id']) and dst_obj2.partner_id.id or False
514                 else:
515                     ressource_type_id = False
516                     ressource_id = False
517                     ressource_model = False
518                     partner_id = False                               
519                 pool.get('document.directory').write(cr, uid, result['directory'], {
520                     'name' : dst_basename,
521                     'ressource_id': ressource_id,
522                     'ressource_parent_type_id': ressource_type_id,
523                     'parent_id' : parent_id
524                 })
525                 val = {
526                     'res_id': ressource_id,
527                     'res_model': ressource_model,                    
528                     'partner_id': partner_id
529                 }
530                 pool.get('ir.attachment').write(cr, uid, result['attachment'], val)
531                 if (not val['res_id']) and result['attachment']:
532                     cr.execute('update ir_attachment set res_id=NULL where id in ('+','.join(map(str,result['attachment']))+')')
533
534                 cr.commit()
535
536             elif src.type == 'file':    
537                 pool = pooler.get_pool(src.context.dbname)                
538                 obj = pool.get('ir.attachment').browse(cr, uid, src.file_id)                
539                 dst_obj2 = False                     
540                 if isinstance(dst_basedir, node_res_obj):
541                     dst_obj2 = dst_basedir and pool.get(dst_basedir.context.context['res_model']).browse(cr, uid, dst_basedir.context.context['res_id']) or False            
542                 dst_obj = dst_basedir.context._dirobj.browse(cr, uid, dst_basedir.dir_id)  
543              
544                 val = {
545                     'partner_id':False,
546                     #'res_id': False,
547                     'res_model': False,
548                     'name': dst_basename,
549                     'datas_fname': dst_basename,
550                 }
551
552                 if (dst_obj and (dst_obj.type in ('directory','ressource'))) or not dst_obj2:
553                     val['parent_id'] = dst_obj and dst_obj.id or False
554                 else:
555                     val['parent_id'] = False
556
557                 if dst_obj2:
558                     val['res_model'] = dst_obj2._name
559                     val['res_id'] = dst_obj2.id                    
560                     if dst_obj2._name == 'res.partner':
561                         val['partner_id'] = dst_obj2.id
562                     else:                        
563                         val['partner_id'] = pool.get(dst_obj2._name).fields_get(cr, uid, ['partner_id']) and dst_obj2.partner_id.id or False
564                 elif obj.res_id:
565                     # I had to do that because writing False to an integer writes 0 instead of NULL
566                     # change if one day we decide to improve osv/fields.py
567                     cr.execute('update ir_attachment set res_id=NULL where id=%s', (obj.id,))
568
569                 pool.get('ir.attachment').write(cr, uid, [obj.id], val)
570                 cr.commit()
571             elif src.type=='content':
572                 src_file = self.open(src,'r')
573                 dst_file = self.create(dst_basedir, dst_basename, 'w')
574                 dst_file.write(src_file.getvalue())
575                 dst_file.close()
576                 src_file.close()
577                 cr.commit()
578             else:
579                 raise OSError(1, 'Operation not permited.')
580         except Exception,err:
581             self._log.exception('Cannot rename "%s" to "%s" at "%s"', src, dst_basename, dst_basedir)
582             raise OSError(1,'Operation not permited.')
583         finally:
584             if cr: cr.close()
585
586
587
588     # Nearly Ok
589     def stat(self, node):
590         r = list(os.stat('/'))
591         if self.isfile(node):
592             r[0] = 33188
593         r[6] = self.getsize(node)
594         r[7] = self.getmtime(node)
595         r[8] =  self.getmtime(node)
596         r[9] =  self.getmtime(node)
597         return os.stat_result(r)
598     lstat = stat
599
600     # --- Wrapper methods around os.path.*
601
602     # Ok
603     def isfile(self, node):
604         if node and (node.type not in ('collection','database')):
605             return True
606         return False
607
608     # Ok
609     def islink(self, path):
610         """Return True if path is a symbolic link."""
611         return False
612
613     # Ok
614     def isdir(self, node):
615         """Return True if path is a directory."""
616         if node is None:
617             return True
618         if node and (node.type in ('collection','database')):
619             return True
620         return False
621
622     # Ok
623     def getsize(self, node):
624         """Return the size of the specified file in bytes."""
625         result = 0L
626         if node.type=='file':
627             result = node.content_length or 0L
628         return result
629
630     # Ok
631     def getmtime(self, node):
632         """Return the last modified time as a number of seconds since
633         the epoch."""
634         
635         if node.write_date or node.create_date:
636             dt = (node.write_date or node.create_date)[:19]
637             result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
638         else:
639             result = time.mktime(time.localtime())
640         return result
641
642     # Ok
643     def realpath(self, path):
644         """Return the canonical version of path eliminating any
645         symbolic links encountered in the path (if they are
646         supported by the operating system).
647         """
648         return path
649
650     # Ok
651     def lexists(self, path):
652         """Return True if path refers to an existing path, including
653         a broken or circular symbolic link.
654         """
655         return path and True or False
656     exists = lexists
657
658     # Ok, can be improved
659     def glob1(self, dirname, pattern):
660         """Return a list of files matching a dirname pattern
661         non-recursively.
662
663         Unlike glob.glob1 raises exception if os.listdir() fails.
664         """
665         names = self.listdir(dirname)
666         if pattern[0] != '.':
667             names = filter(lambda x: x.path[0] != '.', names)
668         return fnmatch.filter(names, pattern)
669
670     # --- Listing utilities
671
672     # note: the following operations are no more blocking
673
674     # Ok
675     def get_list_dir(self, path):
676         """"Return an iterator object that yields a directory listing
677         in a form suitable for LIST command.
678         """        
679         if self.isdir(path):
680             listing = self.listdir(path)
681             #listing.sort()
682             return self.format_list(path and path.path or '/', listing)
683         # if path is a file or a symlink we return information about it
684         elif self.isfile(path):
685             basedir, filename = os.path.split(path.path)
686             self.lstat(path)  # raise exc in case of problems
687             return self.format_list(basedir, [path])
688
689
690     # Ok
691     def get_stat_dir(self, rawline, datacr):
692         """Return an iterator object that yields a list of files
693         matching a dirname pattern non-recursively in a form
694         suitable for STAT command.
695
696          - (str) rawline: the raw string passed by client as command
697          argument.
698         """
699         ftppath = self.ftpnorm(rawline)
700         if not glob.has_magic(ftppath):
701             return self.get_list_dir(self.ftp2fs(rawline, datacr))
702         else:
703             basedir, basename = os.path.split(ftppath)
704             if glob.has_magic(basedir):
705                 return iter(['Directory recursion not supported.\r\n'])
706             else:
707                 basedir = self.ftp2fs(basedir, datacr)
708                 listing = self.glob1(basedir, basename)
709                 if listing:
710                     listing.sort()
711                 return self.format_list(basedir, listing)
712
713     # Ok    
714     def format_list(self, basedir, listing, ignore_err=True):
715         """Return an iterator object that yields the entries of given
716         directory emulating the "/bin/ls -lA" UNIX command output.
717
718          - (str) basedir: the absolute dirname.
719          - (list) listing: the names of the entries in basedir
720          - (bool) ignore_err: when False raise exception if os.lstat()
721          call fails.
722
723         On platforms which do not support the pwd and grp modules (such
724         as Windows), ownership is printed as "owner" and "group" as a
725         default, and number of hard links is always "1". On UNIX
726         systems, the actual owner, group, and number of links are
727         printed.
728
729         This is how output appears to client:
730
731         -rw-rw-rw-   1 owner   group    7045120 Sep 02  3:47 music.mp3
732         drwxrwxrwx   1 owner   group          0 Aug 31 18:50 e-books
733         -rw-rw-rw-   1 owner   group        380 Sep 02  3:40 module.py
734         """
735         for file in listing:
736             try:
737                 st = self.lstat(file)
738             except os.error:
739                 if ignore_err:
740                     continue
741                 raise
742             perms = filemode(st.st_mode)  # permissions
743             nlinks = st.st_nlink  # number of links to inode
744             if not nlinks:  # non-posix system, let's use a bogus value
745                 nlinks = 1
746             size = st.st_size  # file size
747             uname = "owner"
748             gname = "group"
749             # stat.st_mtime could fail (-1) if last mtime is too old
750             # in which case we return the local time as last mtime
751             try:
752                 mname=_get_month_name(time.strftime("%m", time.localtime(st.st_mtime)))               
753                 mtime = mname+' '+time.strftime("%d %H:%M", time.localtime(st.st_mtime))
754             except ValueError:
755                 mname=_get_month_name(time.strftime("%m"))
756                 mtime = mname+' '+time.strftime("%d %H:%M")            
757             # formatting is matched with proftpd ls output            
758             path=_to_decode(file.path) #file.path.encode('ascii','replace').replace('?','_')                    
759             yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
760                                                      size, mtime, path.split('/')[-1])
761
762     # Ok
763     def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
764         """Return an iterator object that yields the entries of a given
765         directory or of a single file in a form suitable with MLSD and
766         MLST commands.
767
768         Every entry includes a list of "facts" referring the listed
769         element.  See RFC-3659, chapter 7, to see what every single
770         fact stands for.
771
772          - (str) basedir: the absolute dirname.
773          - (list) listing: the names of the entries in basedir
774          - (str) perms: the string referencing the user permissions.
775          - (str) facts: the list of "facts" to be returned.
776          - (bool) ignore_err: when False raise exception if os.stat()
777          call fails.
778
779         Note that "facts" returned may change depending on the platform
780         and on what user specified by using the OPTS command.
781
782         This is how output could appear to the client issuing
783         a MLSD request:
784
785         type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
786         type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
787         type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
788         """
789         permdir = ''.join([x for x in perms if x not in 'arw'])
790         permfile = ''.join([x for x in perms if x not in 'celmp'])
791         if ('w' in perms) or ('a' in perms) or ('f' in perms):
792             permdir += 'c'
793         if 'd' in perms:
794             permdir += 'p'
795         type = size = perm = modify = create = unique = mode = uid = gid = ""
796         for file in listing:                        
797             try:
798                 st = self.stat(file)
799             except OSError:
800                 if ignore_err:
801                     continue
802                 raise
803             # type + perm
804             if stat.S_ISDIR(st.st_mode):
805                 if 'type' in facts:
806                     type = 'type=dir;'                    
807                 if 'perm' in facts:
808                     perm = 'perm=%s;' %permdir
809             else:
810                 if 'type' in facts:
811                     type = 'type=file;'
812                 if 'perm' in facts:
813                     perm = 'perm=%s;' %permfile
814             if 'size' in facts:
815                 size = 'size=%s;' %st.st_size  # file size
816             # last modification time
817             if 'modify' in facts:
818                 try:
819                     modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
820                                            time.localtime(st.st_mtime))
821                 except ValueError:
822                     # stat.st_mtime could fail (-1) if last mtime is too old
823                     modify = ""
824             if 'create' in facts:
825                 # on Windows we can provide also the creation time
826                 try:
827                     create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
828                                            time.localtime(st.st_ctime))
829                 except ValueError:
830                     create = ""
831             # UNIX only
832             if 'unix.mode' in facts:
833                 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
834             if 'unix.uid' in facts:
835                 uid = 'unix.uid=%s;' %st.st_uid
836             if 'unix.gid' in facts:
837                 gid = 'unix.gid=%s;' %st.st_gid
838             # We provide unique fact (see RFC-3659, chapter 7.5.2) on
839             # posix platforms only; we get it by mixing st_dev and
840             # st_ino values which should be enough for granting an
841             # uniqueness for the file listed.
842             # The same approach is used by pure-ftpd.
843             # Implementors who want to provide unique fact on other
844             # platforms should use some platform-specific method (e.g.
845             # on Windows NTFS filesystems MTF records could be used).
846             if 'unique' in facts:
847                 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
848             path=_to_decode(file.path)
849             path = path and path.split('/')[-1] or None
850             yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
851                                                 mode, uid, gid, unique, path)
852
853 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
854