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