3
from tarfile import filemode
13
from service import security
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.
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.
25
- (str) root: the user home directory.
26
- (str) cwd: the current working directory.
27
- (str) rnfr: source file to be renamed.
32
s = netsvc.LocalService('db')
35
for db_name in result:
36
db = pooler.get_db_only(db_name)
38
cr.execute("select id from ir_module_module where name like 'document%' and state='installed' ")
41
self.db_name_list.append(db_name)
43
return self.db_name_list
51
# --- Pathname / conversion utilities
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.
58
Example (having "/foo" as current working directory):
61
Note: directory separators are system independent ("/").
62
Pathname returned is always absolutized.
64
if os.path.isabs(ftppath):
65
p = os.path.normpath(ftppath)
67
p = os.path.normpath(os.path.join(self.cwd, ftppath))
68
# normalize string in a standard web-path notation having '/'
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
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):
84
def ftp2fs(self, path_orig, data):
85
path = self.ftpnorm(path_orig)
86
if path and path=='/':
88
path2 = filter(None,path.split('/'))[1:]
89
(cr, uid, pool) = data
90
res = pool.get('document.directory').get_object(cr, uid, path2[:])
92
raise OSError(2, 'Not such file or directory.')
96
def fs2ftp(self, node):
97
res = node and ('/' + node.cr.dbname + '/' + node.path) or '/'
101
def validpath(self, path):
102
"""Check whether the path belongs to user's home directory.
103
Expected argument is a "real" filesystem pathname.
105
If path is a symbolic link it is resolved to check its real
108
Pathnames escaping from user's root directory are considered
111
return path and True or False
113
# --- Wrapper methods around open() and tempfile.mkstemp
116
def create(self, node, objname, mode):
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
125
def close(self, *args, **kwargs):
126
db,pool = pooler.get_db_and_pool(self.dbname)
129
val = self.getvalue()
131
'datas': base64.encodestring(val),
132
'file_size': len(val),
134
pool.get('ir.attachment').write(cr, uid, [self.ressource_id], val2)
136
StringIO.StringIO.close(self, *args, **kwargs)
140
pool = pooler.get_pool(cr.dbname)
142
fobj = pool.get('ir.attachment')
143
ext = objname.find('.') >0 and objname.split('.')[1] or False
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
151
where=[('name','=',objname)]
152
if object and (object.type in ('directory','ressource')) or object2:
153
where.append(('parent_id','=',object.id))
155
where.append(('parent_id','=',False))
158
where +=[('res_id','=',object2.id),('res_model','=',object2._name)]
159
cids = fobj.search(cr, uid,where)
166
'datas_fname': objname,
171
if object and (object.type in ('directory','ressource')) or not object2:
172
val['parent_id']= object and object.id or False
175
if 'partner_id' in object2 and object2.partner_id.id:
176
partner = object2.partner_id.id
177
if object2._name == 'res.partner':
180
'res_model': object2._name,
181
'partner_id': partner,
184
cid = fobj.create(cr, uid, val, context={})
187
s = file_wrapper('', cid, cr.dbname, uid, )
191
raise OSError(1, 'Operation not permited.')
194
def open(self, node, mode):
196
raise OSError(1, 'Operation not permited.')
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 ''))
204
elif node.type=='content':
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)
215
raise OSError(1, 'Operation not permited.')
217
# ok, but need test more
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
224
raise 'Not Yet Implemented'
226
# def __init__(self, fd, name):
229
# def __getattr__(self, attr):
230
# return getattr(self.file, attr)
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)
239
text = not 'b' in mode
240
# for unique file , maintain version if duplicate file
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)])
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)
257
def chdir(self, path):
261
if path.type in ('collection','database'):
262
self.cwd = self.fs2ftp(path)
264
raise OSError(1, 'Operation not permited.')
267
def mkdir(self, node, basename):
268
"""Create the specified directory."""
270
raise OSError(1, 'Operation not permited.')
272
object2=node and node.object2 or False
273
object=node and node.object or False
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.')
281
'ressource_parent_type_id': object and object.ressource_type_id.id or False,
282
'ressource_id': object2 and object2.id or False
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)
291
raise OSError(1, 'Operation not permited.')
295
def close_cr(self, data):
300
def get_cr(self, path):
301
path = self.ftpnorm(path)
304
dbname = path.split('/')[1]
306
db,pool = pooler.get_db_and_pool(dbname)
308
raise OSError(1, 'Operation not permited.')
310
uid = security.login(dbname, self.username, self.password)
312
raise OSError(1, 'Operation not permited.')
316
def listdir(self, path):
317
"""List the content of a directory."""
321
def __init__(self, db):
326
for db in self.db_list():
327
result.append(false_node(db))
329
return path.children()
332
def rmdir(self, node):
333
"""Remove the specified directory."""
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':
341
raise OSError(39, 'Directory not empty.')
342
res = pool.get('document.directory').unlink(cr, uid, [object.id])
344
raise OSError(39, 'Directory not empty.')
349
def remove(self, node):
350
"""Remove the specified file."""
353
pool = pooler.get_pool(cr.dbname)
354
object2=node and node.object2 or False
355
object=node and node.object or False
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])
361
raise OSError(1, 'Operation not permited.')
365
def rename(self, src, dst_basedir,dst_basename):
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
372
if src.type=='collection':
373
if src.object._table_name <> 'document.directory':
374
raise OSError(1, 'Operation not permited.')
379
# Compute all childs to set the new ressource ID
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)
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.')
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
404
val['parent_id'] = False
405
res = pool.get('document.directory').write(cr, uid, [object.id],val)
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
415
partner_id= dst_basedir.object2.partner_id and dst_basedir.object2.partner_id.id or False
417
ressource_type_id = False
419
ressource_model = False
423
pool.get('document.directory').write(cr, uid, result['directory'], {
424
'ressource_id': ressource_id,
425
'ressource_parent_type_id': ressource_type_id
428
'res_id': ressource_id,
429
'res_model': ressource_model,
431
'partner_id': partner_id
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']))+')')
438
elif src.type=='file':
439
pool = pooler.get_pool(src.cr.dbname)
444
'name': dst_basename,
445
'datas_fname': dst_basename,
446
'title': dst_basename,
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
452
val['parent_id'] = False
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
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,))
467
pool.get('ir.attachment').write(src.cr, src.uid, [src.object.id], val)
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())
477
raise OSError(1, 'Operation not permited.')
478
except Exception,err:
480
raise OSError(1,'Operation not permited.')
486
def stat(self, node):
487
r = list(os.stat('/'))
488
if self.isfile(node):
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)
497
# --- Wrapper methods around os.path.*
500
def isfile(self, node):
501
if node and (node.type not in ('collection','database')):
506
def islink(self, path):
507
"""Return True if path is a symbolic link."""
511
def isdir(self, node):
512
"""Return True if path is a directory."""
515
if node and (node.type in ('collection','database')):
520
def getsize(self, node):
521
"""Return the size of the specified file in bytes."""
523
if node.type=='file':
524
result = node.object.file_size or 0L
528
def getmtime(self, node):
529
"""Return the last modified time as a number of seconds since
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'))
535
result = time.mktime(time.localtime())
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).
547
def lexists(self, path):
548
"""Return True if path refers to an existing path, including
549
a broken or circular symbolic link.
551
return path and True or False
554
# Ok, can be improved
555
def glob1(self, dirname, pattern):
556
"""Return a list of files matching a dirname pattern
559
Unlike glob.glob1 raises exception if os.listdir() fails.
561
names = self.listdir(dirname)
562
if pattern[0] != '.':
563
names = filter(lambda x: x.path[0] != '.', names)
564
return fnmatch.filter(names, pattern)
566
# --- Listing utilities
568
# note: the following operations are no more blocking
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.
576
listing = self.listdir(path)
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])
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.
592
- (str) rawline: the raw string passed by client as command
595
ftppath = self.ftpnorm(rawline)
596
if not glob.has_magic(ftppath):
597
return self.get_list_dir(self.ftp2fs(rawline, datacr))
599
basedir, basename = os.path.split(ftppath)
600
if glob.has_magic(basedir):
601
return iter(['Directory recursion not supported.\r\n'])
603
basedir = self.ftp2fs(basedir, datacr)
604
listing = self.glob1(basedir, basename)
607
return self.format_list(basedir, listing)
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.
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()
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
625
This is how output appears to client:
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
633
st = self.lstat(file)
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
642
size = st.st_size # file size
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
648
mtime = time.strftime("%b %d %H:%M", time.localtime(st.st_mtime))
650
mtime = time.strftime("%b %d %H:%M")
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])
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
662
Every entry includes a list of "facts" referring the listed
663
element. See RFC-3659, chapter 7, to see what every single
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()
673
Note that "facts" returned may change depending on the platform
674
and on what user specified by using the OPTS command.
676
This is how output could appear to the client issuing
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
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):
689
type = size = perm = modify = create = unique = mode = uid = gid = ""
690
for basename in listing:
691
file = os.path.join(basedir, basename)
699
if stat.S_ISDIR(st.st_mode):
703
elif basename == '..':
708
perm = 'perm=%s;' %permdir
713
perm = 'perm=%s;' %permfile
715
size = 'size=%s;' %st.st_size # file size
716
# last modification time
717
if 'modify' in facts:
719
modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
720
time.localtime(st.st_mtime))
722
# stat.st_mtime could fail (-1) if last mtime is too old
724
if 'create' in facts:
725
# on Windows we can provide also the creation time
727
create = 'create=%s;' %time.strftime("%Y%m%d%H%M%S",
728
time.localtime(st.st_ctime))
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)
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)