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