0073b26b9f255f2ced32f5f04a3a8bf1346eda97
[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 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_'+self.node.content.extension[1:])(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             res = os.path.normpath(''.join(node.full_path()))
209             res = res.replace("\\", "/")        
210             while res[:2] == '//':
211                 res = res[1:]
212             res = '/' + node.context.dbname + '/' + _to_decode(res)            
213             
214         #res = node and ('/' + node.cr.dbname + '/' + _to_decode(self.ftpnorm(node.path))) or '/'
215         return res
216
217     # Ok
218     def validpath(self, path):
219         """Check whether the path belongs to user's home directory.
220         Expected argument is a "real" filesystem pathname.
221
222         If path is a symbolic link it is resolved to check its real
223         destination.
224
225         Pathnames escaping from user's root directory are considered
226         not valid.
227         """
228         return path and True or False
229
230     # --- Wrapper methods around open() and tempfile.mkstemp
231
232     # Ok
233     def create(self, node, objname, mode):
234         objname = _to_unicode(objname) 
235         cr = None       
236         try:
237             uid = node.context.uid
238             pool = pooler.get_pool(node.context.dbname)
239             cr = pooler.get_db(node.context.dbname).cursor()
240             child = node.child(cr, objname)
241             if child:
242                 if child.type in ('collection','database'):
243                     raise OSError(1, 'Operation not permited.')
244                 if child.type == 'content':
245                     s = content_wrapper(node.context.dbname, uid, pool, child)
246                     return s
247             fobj = pool.get('ir.attachment')
248             ext = objname.find('.') >0 and objname.split('.')[1] or False
249
250             # TODO: test if already exist and modify in this case if node.type=file
251             ### checked already exits
252             object2 = False
253             if isinstance(node, node_res_obj):
254                 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False            
255             
256             cid = False
257             object = node.context._dirobj.browse(cr, uid, node.dir_id)
258             where = [('name','=',objname)]
259             if object and (object.type in ('directory')) or object2:
260                 where.append(('parent_id','=',object.id))
261             else:
262                 where.append(('parent_id','=',False))
263
264             if object2:
265                 where += [('res_id','=',object2.id),('res_model','=',object2._name)]
266             cids = fobj.search(cr, uid,where)
267             if len(cids):
268                 cid = cids[0]
269
270             if not cid:
271                 val = {
272                     'name': objname,
273                     'datas_fname': objname,
274                     'parent_id' : node.dir_id, 
275                     'datas': '',
276                     'file_size': 0L,
277                     'file_type': ext,
278                     'store_method' :  (object.storage_id.type == 'filestore' and 'fs')\
279                                  or  (object.storage_id.type == 'db' and 'db')
280                 }
281                 if object and (object.type in ('directory')) or not object2:
282                     val['parent_id']= object and object.id or False
283                 partner = False
284                 if object2:
285                     if 'partner_id' in object2 and object2.partner_id.id:
286                         partner = object2.partner_id.id
287                     if object2._name == 'res.partner':
288                         partner = object2.id
289                     val.update( {
290                         'res_model': object2._name,
291                         'partner_id': partner,
292                         'res_id': object2.id
293                     })
294                 cid = fobj.create(cr, uid, val, context={})
295             cr.commit()
296
297             s = file_wrapper('', cid, node.context.dbname, uid, )
298             return s
299         except Exception,e:
300             log(e)
301             raise OSError(1, 'Operation not permited.')
302         finally:
303             if cr:
304                 cr.close()
305
306     # Ok
307     def open(self, node, mode):
308         if not node:
309             raise OSError(1, 'Operation not permited.')
310         # Reading operation
311         if node.type=='file':
312             cr = pooler.get_db(node.context.dbname).cursor()
313             uid = node.context.uid
314             if not self.isfile(node):
315                 raise OSError(1, 'Operation not permited.')
316             fobj = node.context._dirobj.pool.get('ir.attachment').browse(cr, uid, node.file_id, context=node.context.context)
317             if fobj.store_method and fobj.store_method== 'fs' :
318                 s = StringIO.StringIO(node.get_data(cr, fobj))
319             else:
320                 s = StringIO.StringIO(base64.decodestring(fobj.db_datas or ''))
321             s.name = node
322             cr.close()
323             return s
324         elif node.type=='content':
325             uid = node.context.uid
326             cr = pooler.get_db(node.context.dbname).cursor()
327             pool = pooler.get_pool(node.context.dbname)
328             res = getattr(pool.get('document.directory.content'), 'process_read')(cr, uid, node)
329             cr.close()
330             return res
331         else:
332             raise OSError(1, 'Operation not permited.')
333
334     # ok, but need test more
335
336     def mkstemp(self, suffix='', prefix='', dir=None, mode='wb'):
337         """A wrap around tempfile.mkstemp creating a file with a unique
338         name.  Unlike mkstemp it returns an object with a file-like
339         interface.
340         """
341         raise 'Not Yet Implemented'
342 #        class FileWrapper:
343 #            def __init__(self, fd, name):
344 #                self.file = fd
345 #                self.name = name
346 #            def __getattr__(self, attr):
347 #                return getattr(self.file, attr)
348 #
349 #        text = not 'b' in mode
350 #        # max number of tries to find out a unique file name
351 #        tempfile.TMP_MAX = 50
352 #        fd, name = tempfile.mkstemp(suffix, prefix, dir, text=text)
353 #        file = os.fdopen(fd, mode)
354 #        return FileWrapper(file, name)
355
356         text = not 'b' in mode
357         # for unique file , maintain version if duplicate file
358         if dir:
359             cr = dir.cr
360             uid = dir.uid
361             pool = pooler.get_pool(node.context.dbname)
362             object=dir and dir.object or False
363             object2=dir and dir.object2 or False
364             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)])
365             if len(res):
366                 pre = prefix.split('.')
367                 prefix=pre[0] + '.v'+str(len(res))+'.'+pre[1]
368             #prefix = prefix + '.'
369         return self.create(dir,suffix+prefix,text)
370
371
372
373     # Ok
374     def chdir(self, path):        
375         if not path:
376             self.cwd = '/'
377             return None        
378         if path.type in ('collection','database'):
379             self.cwd = self.fs2ftp(path)
380         elif path.type in ('file'):            
381             parent_path = path.full_path()[:-1]            
382             self.cwd = os.path.normpath(''.join(parent_path))
383         else:
384             raise OSError(1, 'Operation not permited.')
385
386     # Ok
387     def mkdir(self, node, basename):
388         """Create the specified directory."""
389         cr = False
390         if not node:
391             raise OSError(1, 'Operation not permited.')
392         try:
393             basename =_to_unicode(basename)
394             cr = pooler.get_db(node.context.dbname).cursor()
395             uid = node.context.uid
396             pool = pooler.get_pool(node.context.dbname)
397             object2 = False            
398             if isinstance(node, node_res_obj):
399                 object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False            
400             obj = node.context._dirobj.browse(cr, uid, node.dir_id)            
401             if obj and (obj.type == 'ressource') and not object2:
402                 raise OSError(1, 'Operation not permited.')
403             val = {
404                 'name': basename,
405                 'ressource_parent_type_id': obj and obj.ressource_type_id.id or False,
406                 'ressource_id': object2 and object2.id or False,
407                 'parent_id' : False
408             }
409             if (obj and (obj.type in ('directory'))) or not object2:                
410                 val['parent_id'] =  obj and obj.id or False            
411             # Check if it alreayd exists !
412             pool.get('document.directory').create(cr, uid, val)
413             cr.commit()            
414         except Exception,e:
415             log(e)
416             raise OSError(1, 'Operation not permited.')
417         finally:
418             if cr: cr.close()
419
420     # Ok
421     def close_cr(self, data):
422         if data:
423             data[0].close()
424         return True
425
426     def get_cr(self, path):
427         path = self.ftpnorm(path)
428         if path=='/':
429             return None
430         dbname = path.split('/')[1]
431         if dbname not in self.db_list():
432             return None
433         try:
434             db,pool = pooler.get_db_and_pool(dbname)
435         except:
436             raise OSError(1, 'Operation not permited.')
437         cr = db.cursor()
438         uid = security.login(dbname, self.username, self.password)
439         if not uid:
440             raise OSError(2, 'Authentification Required.')
441         return cr, uid, pool
442
443     # Ok
444     def listdir(self, path):
445         """List the content of a directory."""
446         class false_node(object):
447             write_date = None
448             create_date = None
449             type = 'database'
450             def __init__(self, db):
451                 self.path = '/'+db
452
453         if path is None:
454             result = []
455             for db in self.db_list():
456                 try:
457                     uid = security.login(db, self.username, self.password)
458                     if uid:
459                         result.append(false_node(db))                    
460                 except osv.except_osv:          
461                     pass
462             return result
463         cr = pooler.get_db(path.context.dbname).cursor()        
464         res = path.children(cr)
465         cr.close()
466         return res
467
468     # Ok
469     def rmdir(self, node):
470         """Remove the specified directory."""
471         assert node
472         cr = pooler.get_db(node.context.dbname).cursor()
473         uid = node.context.uid
474         pool = pooler.get_pool(node.context.dbname)        
475         object = node.context._dirobj.browse(cr, uid, node.dir_id)
476         if not object:
477             raise OSError(2, 'Not such file or directory.')
478         if object._table_name == 'document.directory':            
479             if node.children(cr):
480                 raise OSError(39, 'Directory not empty.')
481             res = pool.get('document.directory').unlink(cr, uid, [object.id])
482         else:
483             raise OSError(1, 'Operation not permited.')
484
485         cr.commit()
486         cr.close()
487
488     # Ok
489     def remove(self, node):
490         assert node
491         if node.type == 'collection':
492             return self.rmdir(node)
493         elif node.type == 'file':
494             return self.rmfile(node)
495         raise OSError(1, 'Operation not permited.')
496
497     def rmfile(self, node):
498         """Remove the specified file."""
499         assert node
500         if node.type == 'collection':
501             return self.rmdir(node)
502         uid = node.context.uid
503         pool = pooler.get_pool(node.context.dbname)
504         cr = pooler.get_db(node.context.dbname).cursor()
505         object = pool.get('ir.attachment').browse(cr, uid, node.file_id)
506         if not object:
507             raise OSError(2, 'Not such file or directory.')
508         if object._table_name == 'ir.attachment':
509             res = pool.get('ir.attachment').unlink(cr, uid, [object.id])
510         else:
511             raise OSError(1, 'Operation not permited.')
512         cr.commit()
513         cr.close()
514
515     # Ok
516     def rename(self, src, dst_basedir, dst_basename):
517         """
518             Renaming operation, the effect depends on the src:
519             * A file: read, create and remove
520             * A directory: change the parent and reassign childs to ressource
521         """
522         cr = False
523         try:
524             dst_basename = _to_unicode(dst_basename)
525             cr = pooler.get_db(src.context.dbname).cursor()
526             uid = src.context.uid                       
527             if src.type == 'collection':
528                 obj2 = False
529                 dst_obj2 = False
530                 pool = pooler.get_pool(src.context.dbname)
531                 if isinstance(src, node_res_obj):
532                     obj2 = src and pool.get(src.context.context['res_model']).browse(cr, uid, src.context.context['res_id']) or False            
533                 obj = src.context._dirobj.browse(cr, uid, src.dir_id)                 
534                 if isinstance(dst_basedir, node_res_obj):
535                     dst_obj2 = dst_basedir and pool.get(dst_basedir.context.context['res_model']).browse(cr, uid, dst_basedir.context.context['res_id']) or False
536                 dst_obj = dst_basedir.context._dirobj.browse(cr, uid, dst_basedir.dir_id)                 
537                 if obj._table_name <> 'document.directory':
538                     raise OSError(1, 'Operation not permited.')
539                 result = {
540                     'directory': [],
541                     'attachment': []
542                 }
543                 # Compute all childs to set the new ressource ID                
544                 child_ids = [src]
545                 while len(child_ids):
546                     node = child_ids.pop(0)                    
547                     child_ids += node.children(cr)                        
548                     if node.type == 'collection':
549                         object2 = False                                            
550                         if isinstance(node, node_res_obj):                  
551                             object2 = node and pool.get(node.context.context['res_model']).browse(cr, uid, node.context.context['res_id']) or False                           
552                         object = node.context._dirobj.browse(cr, uid, node.dir_id)                         
553                         result['directory'].append(object.id)
554                         if (not object.ressource_id) and object2:
555                             raise OSError(1, 'Operation not permited.')
556                     elif node.type == 'file':
557                         result['attachment'].append(object.id)
558                 
559                 if obj2 and not obj.ressource_id:
560                     raise OSError(1, 'Operation not permited.')
561                 
562                 if (dst_obj and (dst_obj.type in ('directory'))) or not dst_obj2:
563                     parent_id = dst_obj and dst_obj.id or False
564                 else:
565                     parent_id = False              
566                 
567                 
568                 if dst_obj2:                    
569                     ressource_type_id = pool.get('ir.model').search(cr, uid, [('model','=',dst_obj2._name)])[0]
570                     ressource_id = dst_obj2.id
571                     title = dst_obj2.name
572                     ressource_model = dst_obj2._name                    
573                     if dst_obj2._name == 'res.partner':
574                         partner_id = dst_obj2.id
575                     else:                                                
576                         partner_id = pool.get(dst_obj2._name).fields_get(cr, uid, ['partner_id']) and dst_obj2.partner_id.id or False
577                 else:
578                     ressource_type_id = False
579                     ressource_id = False
580                     ressource_model = False
581                     partner_id = False
582                     title = False                
583                 pool.get('document.directory').write(cr, uid, result['directory'], {
584                     'name' : dst_basename,
585                     'ressource_id': ressource_id,
586                     'ressource_parent_type_id': ressource_type_id,
587                     'parent_id' : parent_id
588                 })
589                 val = {
590                     'res_id': ressource_id,
591                     'res_model': ressource_model,
592                     'title': title,
593                     'partner_id': partner_id
594                 }
595                 pool.get('ir.attachment').write(cr, uid, result['attachment'], val)
596                 if (not val['res_id']) and result['attachment']:
597                     cr.execute('update ir_attachment set res_id=NULL where id in ('+','.join(map(str,result['attachment']))+')')
598
599                 cr.commit()
600
601             elif src.type == 'file':    
602                 pool = pooler.get_pool(src.context.dbname)                
603                 obj = pool.get('ir.attachment').browse(cr, uid, src.file_id)                
604                 dst_obj2 = False                     
605                 if isinstance(dst_basedir, node_res_obj):
606                     dst_obj2 = dst_basedir and pool.get(dst_basedir.context.context['res_model']).browse(cr, uid, dst_basedir.context.context['res_id']) or False            
607                 dst_obj = dst_basedir.context._dirobj.browse(cr, uid, dst_basedir.dir_id)  
608              
609                 val = {
610                     'partner_id':False,
611                     #'res_id': False,
612                     'res_model': False,
613                     'name': dst_basename,
614                     'datas_fname': dst_basename,
615                     'title': dst_basename,
616                 }
617
618                 if (dst_obj and (dst_obj.type in ('directory','ressource'))) or not dst_obj2:
619                     val['parent_id'] = dst_obj and dst_obj.id or False
620                 else:
621                     val['parent_id'] = False
622
623                 if dst_obj2:
624                     val['res_model'] = dst_obj2._name
625                     val['res_id'] = dst_obj2.id
626                     val['title'] = dst_obj2.name
627                     if dst_obj2._name == 'res.partner':
628                         val['partner_id'] = dst_obj2.id
629                     else:                        
630                         val['partner_id'] = pool.get(dst_obj2._name).fields_get(cr, uid, ['partner_id']) and dst_obj2.partner_id.id or False
631                 elif obj.res_id:
632                     # I had to do that because writing False to an integer writes 0 instead of NULL
633                     # change if one day we decide to improve osv/fields.py
634                     cr.execute('update ir_attachment set res_id=NULL where id=%s', (obj.id,))
635
636                 pool.get('ir.attachment').write(cr, uid, [obj.id], val)
637                 cr.commit()
638             elif src.type=='content':
639                 src_file = self.open(src,'r')
640                 dst_file = self.create(dst_basedir, dst_basename, 'w')
641                 dst_file.write(src_file.getvalue())
642                 dst_file.close()
643                 src_file.close()
644                 cr.commit()
645             else:
646                 raise OSError(1, 'Operation not permited.')
647         except Exception,err:
648             log(err)
649             raise OSError(1,'Operation not permited.')
650         finally:
651             if cr: cr.close()
652
653
654
655     # Nearly Ok
656     def stat(self, node):
657         r = list(os.stat('/'))
658         if self.isfile(node):
659             r[0] = 33188
660         r[6] = self.getsize(node)
661         r[7] = self.getmtime(node)
662         r[8] =  self.getmtime(node)
663         r[9] =  self.getmtime(node)
664         return os.stat_result(r)
665     lstat = stat
666
667     # --- Wrapper methods around os.path.*
668
669     # Ok
670     def isfile(self, node):
671         if node and (node.type not in ('collection','database')):
672             return True
673         return False
674
675     # Ok
676     def islink(self, path):
677         """Return True if path is a symbolic link."""
678         return False
679
680     # Ok
681     def isdir(self, node):
682         """Return True if path is a directory."""
683         if node is None:
684             return True
685         if node and (node.type in ('collection','database')):
686             return True
687         return False
688
689     # Ok
690     def getsize(self, node):
691         """Return the size of the specified file in bytes."""
692         result = 0L
693         if node.type=='file':
694             result = node.content_length or 0L
695         return result
696
697     # Ok
698     def getmtime(self, node):
699         """Return the last modified time as a number of seconds since
700         the epoch."""
701         
702         if node.write_date or node.create_date:
703             dt = (node.write_date or node.create_date)[:19]
704             result = time.mktime(time.strptime(dt, '%Y-%m-%d %H:%M:%S'))
705         else:
706             result = time.mktime(time.localtime())
707         return result
708
709     # Ok
710     def realpath(self, path):
711         """Return the canonical version of path eliminating any
712         symbolic links encountered in the path (if they are
713         supported by the operating system).
714         """
715         return path
716
717     # Ok
718     def lexists(self, path):
719         """Return True if path refers to an existing path, including
720         a broken or circular symbolic link.
721         """
722         return path and True or False
723     exists = lexists
724
725     # Ok, can be improved
726     def glob1(self, dirname, pattern):
727         """Return a list of files matching a dirname pattern
728         non-recursively.
729
730         Unlike glob.glob1 raises exception if os.listdir() fails.
731         """
732         names = self.listdir(dirname)
733         if pattern[0] != '.':
734             names = filter(lambda x: x.path[0] != '.', names)
735         return fnmatch.filter(names, pattern)
736
737     # --- Listing utilities
738
739     # note: the following operations are no more blocking
740
741     # Ok
742     def get_list_dir(self, path):
743         """"Return an iterator object that yields a directory listing
744         in a form suitable for LIST command.
745         """        
746         if self.isdir(path):
747             listing = self.listdir(path)
748             #listing.sort()
749             return self.format_list(path and path.path or '/', listing)
750         # if path is a file or a symlink we return information about it
751         elif self.isfile(path):
752             basedir, filename = os.path.split(path.path)
753             self.lstat(path)  # raise exc in case of problems
754             return self.format_list(basedir, [path])
755
756
757     # Ok
758     def get_stat_dir(self, rawline, datacr):
759         """Return an iterator object that yields a list of files
760         matching a dirname pattern non-recursively in a form
761         suitable for STAT command.
762
763          - (str) rawline: the raw string passed by client as command
764          argument.
765         """
766         ftppath = self.ftpnorm(rawline)
767         if not glob.has_magic(ftppath):
768             return self.get_list_dir(self.ftp2fs(rawline, datacr))
769         else:
770             basedir, basename = os.path.split(ftppath)
771             if glob.has_magic(basedir):
772                 return iter(['Directory recursion not supported.\r\n'])
773             else:
774                 basedir = self.ftp2fs(basedir, datacr)
775                 listing = self.glob1(basedir, basename)
776                 if listing:
777                     listing.sort()
778                 return self.format_list(basedir, listing)
779
780     # Ok    
781     def format_list(self, basedir, listing, ignore_err=True):
782         """Return an iterator object that yields the entries of given
783         directory emulating the "/bin/ls -lA" UNIX command output.
784
785          - (str) basedir: the absolute dirname.
786          - (list) listing: the names of the entries in basedir
787          - (bool) ignore_err: when False raise exception if os.lstat()
788          call fails.
789
790         On platforms which do not support the pwd and grp modules (such
791         as Windows), ownership is printed as "owner" and "group" as a
792         default, and number of hard links is always "1". On UNIX
793         systems, the actual owner, group, and number of links are
794         printed.
795
796         This is how output appears to client:
797
798         -rw-rw-rw-   1 owner   group    7045120 Sep 02  3:47 music.mp3
799         drwxrwxrwx   1 owner   group          0 Aug 31 18:50 e-books
800         -rw-rw-rw-   1 owner   group        380 Sep 02  3:40 module.py
801         """
802         for file in listing:
803             try:
804                 st = self.lstat(file)
805             except os.error:
806                 if ignore_err:
807                     continue
808                 raise
809             perms = filemode(st.st_mode)  # permissions
810             nlinks = st.st_nlink  # number of links to inode
811             if not nlinks:  # non-posix system, let's use a bogus value
812                 nlinks = 1
813             size = st.st_size  # file size
814             uname = "owner"
815             gname = "group"
816             # stat.st_mtime could fail (-1) if last mtime is too old
817             # in which case we return the local time as last mtime
818             try:
819                 mname=_get_month_name(time.strftime("%m", time.localtime(st.st_mtime)))               
820                 mtime = mname+' '+time.strftime("%d %H:%M", time.localtime(st.st_mtime))
821             except ValueError:
822                 mname=_get_month_name(time.strftime("%m"))
823                 mtime = mname+' '+time.strftime("%d %H:%M")            
824             # formatting is matched with proftpd ls output            
825             path=_to_decode(file.path) #file.path.encode('ascii','replace').replace('?','_')                    
826             yield "%s %3s %-8s %-8s %8s %s %s\r\n" %(perms, nlinks, uname, gname,
827                                                      size, mtime, path.split('/')[-1])
828
829     # Ok
830     def format_mlsx(self, basedir, listing, perms, facts, ignore_err=True):
831         """Return an iterator object that yields the entries of a given
832         directory or of a single file in a form suitable with MLSD and
833         MLST commands.
834
835         Every entry includes a list of "facts" referring the listed
836         element.  See RFC-3659, chapter 7, to see what every single
837         fact stands for.
838
839          - (str) basedir: the absolute dirname.
840          - (list) listing: the names of the entries in basedir
841          - (str) perms: the string referencing the user permissions.
842          - (str) facts: the list of "facts" to be returned.
843          - (bool) ignore_err: when False raise exception if os.stat()
844          call fails.
845
846         Note that "facts" returned may change depending on the platform
847         and on what user specified by using the OPTS command.
848
849         This is how output could appear to the client issuing
850         a MLSD request:
851
852         type=file;size=156;perm=r;modify=20071029155301;unique=801cd2; music.mp3
853         type=dir;size=0;perm=el;modify=20071127230206;unique=801e33; ebooks
854         type=file;size=211;perm=r;modify=20071103093626;unique=801e32; module.py
855         """
856         permdir = ''.join([x for x in perms if x not in 'arw'])
857         permfile = ''.join([x for x in perms if x not in 'celmp'])
858         if ('w' in perms) or ('a' in perms) or ('f' in perms):
859             permdir += 'c'
860         if 'd' in perms:
861             permdir += 'p'
862         type = size = perm = modify = create = unique = mode = uid = gid = ""
863         for file in listing:                        
864             try:
865                 st = self.stat(file)
866             except OSError:
867                 if ignore_err:
868                     continue
869                 raise
870             # type + perm
871             if stat.S_ISDIR(st.st_mode):
872                 if 'type' in facts:
873                     type = 'type=dir;'                    
874                 if 'perm' in facts:
875                     perm = 'perm=%s;' %permdir
876             else:
877                 if 'type' in facts:
878                     type = 'type=file;'
879                 if 'perm' in facts:
880                     perm = 'perm=%s;' %permfile
881             if 'size' in facts:
882                 size = 'size=%s;' %st.st_size  # file size
883             # last modification time
884             if 'modify' in facts:
885                 try:
886                     modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
887                                            time.localtime(st.st_mtime))
888                 except ValueError:
889                     # stat.st_mtime could fail (-1) if last mtime is too old
890                     modify = ""
891             if 'create' in facts:
892                 # on Windows we can provide also the creation time
893                 try:
894                     create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
895                                            time.localtime(st.st_ctime))
896                 except ValueError:
897                     create = ""
898             # UNIX only
899             if 'unix.mode' in facts:
900                 mode = 'unix.mode=%s;' %oct(st.st_mode & 0777)
901             if 'unix.uid' in facts:
902                 uid = 'unix.uid=%s;' %st.st_uid
903             if 'unix.gid' in facts:
904                 gid = 'unix.gid=%s;' %st.st_gid
905             # We provide unique fact (see RFC-3659, chapter 7.5.2) on
906             # posix platforms only; we get it by mixing st_dev and
907             # st_ino values which should be enough for granting an
908             # uniqueness for the file listed.
909             # The same approach is used by pure-ftpd.
910             # Implementors who want to provide unique fact on other
911             # platforms should use some platform-specific method (e.g.
912             # on Windows NTFS filesystems MTF records could be used).
913             if 'unique' in facts:
914                 unique = "unique=%x%x;" %(st.st_dev, st.st_ino)
915             path=_to_decode(file.path)
916             yield "%s%s%s%s%s%s%s%s%s %s\r\n" %(type, size, perm, modify, create,
917                                                 mode, uid, gid, unique, path)
918
919 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
920