~xmo-deactivatedaccount/openobject-addons/5.0-sql-fixes

« back to all changes in this revision

Viewing changes to document/ftpserver/abstracted_fs.py

Document Management System

Show diffs side-by-side

added added

removed removed

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