From d5a68a7b692a6fa568c5fcebc580109afb5bb6d4 Mon Sep 17 00:00:00 2001
From: p_christeas <>
Date: Wed, 2 Dec 2009 11:06:57 +0530
Subject: [PATCH] [IMP]document : get improvement of document from xrg-addons
branch who made nice improvement in document
bzr revid: hmo@tinyerp.com-20091202053657-4oflezyhbolu3xvo
---
addons/document/__init__.py | 7 +
addons/document/__terp__.py | 8 +-
addons/document/content_index.py | 228 +++-
addons/document/directory_content.py | 113 ++
addons/document/directory_report.py | 64 +
addons/document/doc_conf_wizard.py | 163 +++
addons/document/document.py | 776 +----------
addons/document/document_data.xml | 104 +-
addons/document/document_demo.xml | 84 --
addons/document/document_directory.py | 312 +++++
addons/document/document_storage.py | 281 ++++
addons/document/document_view.xml | 178 +--
addons/document/ftpserver/__init__.py | 5 +-
addons/document/ftpserver/abstracted_fs.py | 37 +-
addons/document/ftpserver/authorizer.py | 26 +-
addons/document/ftpserver/ftpserver.py | 1375 +++++++++-----------
addons/document/nodes.py | 776 +++++++++++
addons/document/odt2txt.py | 1 +
addons/document/std_index.py | 111 ++
addons/document/test_cindex.py | 46 +
20 files changed, 2909 insertions(+), 1786 deletions(-)
create mode 100644 addons/document/directory_content.py
create mode 100644 addons/document/directory_report.py
create mode 100644 addons/document/doc_conf_wizard.py
create mode 100644 addons/document/document_directory.py
create mode 100644 addons/document/document_storage.py
create mode 100644 addons/document/nodes.py
create mode 100644 addons/document/std_index.py
create mode 100755 addons/document/test_cindex.py
diff --git a/addons/document/__init__.py b/addons/document/__init__.py
index 5c97428f05d..a9810a8b7b8 100644
--- a/addons/document/__init__.py
+++ b/addons/document/__init__.py
@@ -19,6 +19,13 @@
#
##############################################################################
+import content_index
+import std_index
+import document_storage
+import document_directory
+import directory_content
+import directory_report
+import doc_conf_wizard
import document
import ftpserver
diff --git a/addons/document/__terp__.py b/addons/document/__terp__.py
index d2246d270bf..be9eb6a9d3f 100644
--- a/addons/document/__terp__.py
+++ b/addons/document/__terp__.py
@@ -22,7 +22,7 @@
{
'name': 'Integrated Document Management System',
- 'version': '1.1',
+ 'version': '1.99',
'category': 'Generic Modules/Others',
'description': """This is a complete document management system:
* FTP Interface
@@ -32,17 +32,17 @@
'author': 'Tiny',
'website': 'http://www.openerp.com',
'depends': ['base', 'process'],
- 'init_xml': ['document_data.xml', 'document_demo.xml'],
+ 'init_xml': [],
'update_xml': [
'document_view.xml',
'document_data.xml',
'security/document_security.xml',
'security/ir.model.access.csv'
],
- 'demo_xml': [],
+ 'demo_xml': [ 'document_demo.xml',],
'installable': True,
'active': False,
- 'certificate': '0070515416461',
+ 'certificate': None,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/document/content_index.py b/addons/document/content_index.py
index 6c0f083b8ac..a063251c698 100644
--- a/addons/document/content_index.py
+++ b/addons/document/content_index.py
@@ -18,53 +18,195 @@
# along with this program. If not, see .
#
##############################################################################
-
-import time
import os
-import StringIO
-import odt2txt
import tempfile
-#
-# This should be the indexer
-#
-def _to_unicode(s):
- try:
- return s.decode('utf-8')
- except UnicodeError:
- try:
- return s.decode('latin')
- except UnicodeError:
- try:
- return s.encode('ascii')
- except UnicodeError:
- return s
+# A quick hack: if netsvc is not there, emulate it. Thus, work offline, too
+try:
+ import netsvc
+ def log(lvl,msg):
+ netsvc.Logger().notifyChannel("index",lvl,msg)
+except:
+ class netsvc:
+ LOG_NOTSET = 'notset'
+ LOG_DEBUG_RPC = 'debug_rpc'
+ LOG_DEBUG = 'debug'
+ LOG_DEBUG2 = 'debug2'
+ LOG_INFO = 'info'
+ LOG_WARNING = 'warn'
+ LOG_ERROR = 'error'
+ LOG_CRITICAL = 'critical'
+
+ def log(lvl,msg):
+ print msg
-def content_index(content, filename=None, content_type=None):
- fname,ext = os.path.splitext(filename)
- result = ''
- if ext in ('.doc'): #or content_type ?
- (stdin,stdout) = os.popen2('antiword -', 'b')
- stdin.write(content)
- stdin.close()
- result = _to_unicode(stdout.read())
- elif ext == '.pdf':
- file_descriptor, file_name = tempfile.mkstemp(suffix=ext)
- os.write(file_descriptor, content)
- os.close(file_descriptor)
- fp = os.popen('pdftotext -enc UTF-8 -nopgbrk '+file_name+' -', 'r')
- result = fp.read()
- fp.close()
- elif ext in ('.xls','.ods','.odt','.odp'):
- s = StringIO.StringIO(content)
- o = odt2txt.OpenDocumentTextFile(s)
- result = _to_unicode(o.toString())
- s.close()
- elif ext in ('.txt','.py','.patch','.html','.csv','.xml'):
- result = content
- #else:
- # result = content
- return result
+class NhException(Exception):
+ pass
+
+from subprocess import Popen, PIPE
+
+class indexer():
+ """ An indexer knows how to parse the content of some file.
+
+ Typically, one indexer should be instantiated per file
+ type.
+ Override this class to add more functionality. Note that
+ you should only override the Content or the File methods
+ that give an optimal result. """
+
+ def _getMimeTypes(self):
+ """ Return supported mimetypes """
+ return []
+
+ def _getExtensions(self):
+ return []
+
+ def _getDefMime(self,ext):
+ """ Return a mimetype for this document type, ideally the
+ closest to the extension ext. """
+ mts = self._getMimeTypes();
+ if len (mts):
+ return mts[0]
+ return None
+
+ def indexContent(self,content,filename=None, realfile = None):
+ """ Use either content or the real file, to index.
+ Some parsers will work better with the actual
+ content, others parse a file easier. Try the
+ optimal.
+ """
+ res = ''
+ try:
+ if content != None:
+ return self._doIndexContent(content)
+ except NhException:
+ pass
+
+ if realfile != None:
+ try:
+ return self._doIndexFile(realfile)
+ except NhException:
+ pass
+
+ fp = open(realfile,'rb')
+ content2 = fp.read()
+ fp.close()
+
+ # The not-handled exception may be raised here
+ return self._doIndexContent(content2)
+
+
+ # last try, with a tmp file
+ if content:
+ try:
+ fname,ext = filename and os.path.splitext(filename) or ('','')
+ fd, rfname = tempfile.mkstemp(suffix=ext)
+ os.write(fd, content)
+ os.close(fd)
+ res = self._doIndexFile(rfname)
+ os.unlink(rfname)
+ return res
+ except NhException:
+ pass
+
+ raise NhException('No appropriate method to index file')
+
+ def _doIndexContent(self,content):
+ raise NhException("Content not handled here")
+
+ def _doIndexFile(self,fpath):
+ raise NhException("Content not handled here")
+
+
+
+def mime_match(mime, mdict):
+ if mdict.has_key(mime):
+ return (mime, mdict[mime])
+ if '/' in mime:
+ mpat = mime.split('/')[0]+'/*'
+ if mdict.has_key(mpat):
+ return (mime, mdict[mpat])
+
+ return (None, None)
+
+class contentIndex() :
+ def __init__(self):
+ self.mimes = {}
+ self.exts = {}
+
+ def register(self, obj):
+ f = False
+ for mime in obj._getMimeTypes():
+ self.mimes[mime] = obj
+ f = True
+
+ for ext in obj._getExtensions():
+ self.exts[ext] = obj
+ f = True
+
+ if f:
+ log(netsvc.LOG_DEBUG, "Register content indexer: %r" % obj)
+ if not f:
+ raise Exception("Your indexer should at least suport a mimetype or extension")
+
+ def doIndex(self,content, filename=None, content_type=None, realfname = None, debug=False):
+ fobj = None
+ fname = None
+ mime = None
+ if content_type and self.mimes.has_key(content_type):
+ mime = content_type
+ fobj = self.mimes[content_type]
+ elif filename:
+ bname,ext = os.path.splitext(filename)
+ if self.exts.has_key(ext):
+ fobj = self.exts[ext]
+ mime = fobj._getDefMime(ext)
+
+ if content_type and not fobj:
+ mime,fobj = mime_match(content_type, self.mimes)
+
+ if not fobj:
+ try:
+ if realfname :
+ fname = realfname
+ else:
+ bname,ext = os.path.splitext(filename)
+ fd, fname = tempfile.mkstemp(suffix=ext)
+ os.write(fd, content)
+ os.close(fd)
+
+ fp = Popen(['file','-b','--mime-type',fname], shell=False, stdout=PIPE).stdout
+ result = fp.read()
+ fp.close()
+ mime2 = result.strip()
+ log(netsvc.LOG_DEBUG,"File gave us: %s" % mime2)
+ # Note that the temporary file still exists now.
+ mime,fobj = mime_match(mime2, self.mimes)
+ if not mime:
+ mime = mime2
+ except Exception, e:
+ log(netsvc.LOG_WARNING,"Cannot determine mime type: %s" % str(e))
+
+ try:
+ if fobj:
+ res = (mime, fobj.indexContent(content,filename,fname or realfname) )
+ else:
+ log(netsvc.LOG_DEBUG,"Have no object, return (%s, None)" % mime)
+ res = (mime, None )
+ except Exception, e:
+ log(netsvc.LOG_WARNING,"Could not index file, %s" % e)
+ res = None
+
+ # If we created a tmp file, unlink it now
+ if not realfname and fname:
+ try:
+ os.unlink(fname)
+ except Exception, e:
+ log(netsvc.LOG_WARNING,"Could not unlink %s, %s" %(fname, e))
+
+ return res
+
+cntIndex = contentIndex()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/document/directory_content.py b/addons/document/directory_content.py
new file mode 100644
index 00000000000..4ccad653711
--- /dev/null
+++ b/addons/document/directory_content.py
@@ -0,0 +1,113 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2009 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+import base64
+
+from osv import osv, fields
+from osv.orm import except_orm
+import urlparse
+
+import os
+import nodes
+
+class document_directory_content_type(osv.osv):
+ _name = 'document.directory.content.type'
+ _description = 'Directory Content Type'
+ _columns = {
+ 'name': fields.char('Content Type', size=64, required=True),
+ 'code': fields.char('Extension', size=4),
+ 'active': fields.boolean('Active'),
+ 'mimetype': fields.char('Mime Type',size=32)
+ }
+ _defaults = {
+ 'active': lambda *args: 1
+ }
+document_directory_content_type()
+
+class document_directory_content(osv.osv):
+ _name = 'document.directory.content'
+ _description = 'Directory Content'
+ _order = "sequence"
+ def _extension_get(self, cr, uid, context={}):
+ cr.execute('select code,name from document_directory_content_type where active')
+ res = cr.fetchall()
+ return res
+ _columns = {
+ 'name': fields.char('Content Name', size=64, required=True),
+ 'sequence': fields.integer('Sequence', size=16),
+ 'prefix': fields.char('Prefix', size=16),
+ 'suffix': fields.char('Suffix', size=16),
+ 'report_id': fields.many2one('ir.actions.report.xml', 'Report'),
+ 'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4),
+ 'include_name': fields.boolean('Include Record Name', help="Check this field if you want that the name of the file start by the record name."),
+ 'directory_id': fields.many2one('document.directory', 'Directory'),
+ }
+ _defaults = {
+ 'extension': lambda *args: '.pdf',
+ 'sequence': lambda *args: 1,
+ 'include_name': lambda *args: 1,
+ }
+
+ def _file_get(self,cr,node, nodename, content, context=None):
+ """ return the nodes of a parent having a content
+ The return value MUST be false or a list of node_class objects.
+ """
+
+ # TODO: respect the context!
+ if content.include_name and not node.object2:
+ return False
+
+ res2 = []
+ tname = ''
+ if content.include_name:
+ content_name = node.object2.name
+ obj = pool.get(node.object.ressource_type_id.model)
+ name_for = obj._name.split('.')[-1]
+ if content_name and content_name.find(name_for) == 0 :
+ content_name = content_name.replace(name_for,'')
+ tname = (content.prefix or '') + content_name + (content.suffix or '') + (content.extension or '')
+ else:
+ tname = (content.prefix or '') + (content.suffix or '') + (content.extension or '')
+ if tname.find('/'):
+ tname=tname.replace('/', '_')
+ if not nodename:
+ n = nodes.node_content(tname, node, node.context,content)
+ res2.append( n)
+ else:
+ if nodename == tname:
+ n = nodes.node_content(tname, node, node.context,content)
+ n.fill_fields(cr)
+ res2.append(n)
+ return res2
+
+ def process_write(self, cr, uid, node, data):
+ if node.extension != '.pdf':
+ raise Exception("Invalid content: %s" % node.extension)
+ return True
+ def process_read(self, cr, uid, node):
+ if node.extension != '.pdf':
+ raise Exception("Invalid content: %s" % node.extension)
+ report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.content.report_id.id)
+ srv = netsvc.LocalService('report.'+report.report_name)
+ pdf,pdftype = srv.create(cr, uid, [node.object.id], {}, {})
+ s = StringIO.StringIO(pdf)
+ s.name = node
+ return s
+document_directory_content()
diff --git a/addons/document/directory_report.py b/addons/document/directory_report.py
new file mode 100644
index 00000000000..b5a80adc7ec
--- /dev/null
+++ b/addons/document/directory_report.py
@@ -0,0 +1,64 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2009 Tiny SPRL (). All Rights Reserved
+# $Id$
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import base64
+
+from osv import osv, fields
+from osv.orm import except_orm
+import urlparse
+
+import os
+
+class ir_action_report_xml(osv.osv):
+ _name="ir.actions.report.xml"
+ _inherit ="ir.actions.report.xml"
+
+ def _model_get(self, cr, uid, ids, name, arg, context):
+ res = {}
+ model_pool = self.pool.get('ir.model')
+ for data in self.read(cr,uid,ids,['model']):
+ model = data.get('model',False)
+ if model:
+ model_id =model_pool.search(cr,uid,[('model','=',model)])
+ if model_id:
+ res[data.get('id')] = model_id[0]
+ else:
+ res[data.get('id')] = False
+ return res
+
+ def _model_search(self, cr, uid, obj, name, args):
+ if not len(args):
+ return []
+ model_id= args[0][2]
+ if not model_id:
+ return []
+ model = self.pool.get('ir.model').read(cr,uid,[model_id])[0]['model']
+ report_id = self.search(cr,uid,[('model','=',model)])
+ if not report_id:
+ return [('id','=','0')]
+ return [('id','in',report_id)]
+
+ _columns={
+ 'model_id' : fields.function(_model_get,fnct_search=_model_search,method=True,string='Model Id'),
+ }
+
+ir_action_report_xml()
diff --git a/addons/document/doc_conf_wizard.py b/addons/document/doc_conf_wizard.py
new file mode 100644
index 00000000000..66354c8876b
--- /dev/null
+++ b/addons/document/doc_conf_wizard.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2009 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import base64
+
+from osv import osv, fields
+from osv.orm import except_orm
+import urlparse
+
+import os
+
+class document_configuration_wizard(osv.osv_memory):
+ _name='document.configuration.wizard'
+ _rec_name = 'Auto Directory configuration'
+ _columns = {
+ 'host': fields.char('Server Address', size=64, help="Put here the server address or IP. " \
+ "Keep localhost if you don't know what to write.", required=True)
+ }
+
+ def detect_ip_addr(self, cr, uid, context=None):
+ def _detect_ip_addr(self, cr, uid, context=None):
+ from array import array
+ import socket
+ from struct import pack, unpack
+
+ try:
+ import fcntl
+ except ImportError:
+ fcntl = None
+
+ if not fcntl: # not UNIX:
+ host = socket.gethostname()
+ ip_addr = socket.gethostbyname(host)
+ else: # UNIX:
+ # get all interfaces:
+ nbytes = 128 * 32
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ names = array('B', '\0' * nbytes)
+ outbytes = unpack('iL', fcntl.ioctl( s.fileno(), 0x8912, pack('iL', nbytes, names.buffer_info()[0])))[0]
+ namestr = names.tostring()
+ ifaces = [namestr[i:i+32].split('\0', 1)[0] for i in range(0, outbytes, 32)]
+
+ for ifname in [iface for iface in ifaces if iface != 'lo']:
+ ip_addr = socket.inet_ntoa(fcntl.ioctl(s.fileno(), 0x8915, pack('256s', ifname[:15]))[20:24])
+ break
+ return ip_addr
+
+ try:
+ ip_addr = _detect_ip_addr(self, cr, uid, context)
+ except:
+ ip_addr = 'localhost'
+ return ip_addr
+
+ _defaults = {
+ 'host': detect_ip_addr,
+ }
+
+ def action_cancel(self,cr,uid,ids,conect=None):
+ return {
+ 'view_type': 'form',
+ "view_mode": 'form',
+ 'res_model': 'ir.actions.configuration.wizard',
+ 'type': 'ir.actions.act_window',
+ 'target':'new',
+ }
+
+ def action_config(self, cr, uid, ids, context=None):
+ conf = self.browse(cr, uid, ids[0], context)
+ obj=self.pool.get('document.directory')
+ objid=self.pool.get('ir.model.data')
+
+ if self.pool.get('sale.order'):
+ id = objid._get_id(cr, uid, 'document', 'dir_sale_order_all')
+ id = objid.browse(cr, uid, id, context=context).res_id
+ mid = self.pool.get('ir.model').search(cr, uid, [('model','=','sale.order')])
+ obj.write(cr, uid, [id], {
+ 'type':'ressource',
+ 'ressource_type_id': mid[0],
+ 'domain': '[]',
+ })
+ aid = objid._get_id(cr, uid, 'sale', 'report_sale_order')
+ aid = objid.browse(cr, uid, aid, context=context).res_id
+
+ self.pool.get('document.directory.content').create(cr, uid, {
+ 'name': "Print Order",
+ 'suffix': "_print",
+ 'report_id': aid,
+ 'extension': '.pdf',
+ 'include_name': 1,
+ 'directory_id': id,
+ })
+ id = objid._get_id(cr, uid, 'document', 'dir_sale_order_quote')
+ id = objid.browse(cr, uid, id, context=context).res_id
+ obj.write(cr, uid, [id], {
+ 'type':'ressource',
+ 'ressource_type_id': mid[0],
+ 'domain': "[('state','=','draft')]",
+ })
+
+ if self.pool.get('product.product'):
+ id = objid._get_id(cr, uid, 'document', 'dir_product')
+ id = objid.browse(cr, uid, id, context=context).res_id
+ mid = self.pool.get('ir.model').search(cr, uid, [('model','=','product.product')])
+ obj.write(cr, uid, [id], {
+ 'type':'ressource',
+ 'ressource_type_id': mid[0],
+ })
+
+ if self.pool.get('stock.location'):
+ aid = objid._get_id(cr, uid, 'stock', 'report_product_history')
+ aid = objid.browse(cr, uid, aid, context=context).res_id
+
+ self.pool.get('document.directory.content').create(cr, uid, {
+ 'name': "Product Stock",
+ 'suffix': "_stock_forecast",
+ 'report_id': aid,
+ 'extension': '.pdf',
+ 'include_name': 1,
+ 'directory_id': id,
+ })
+
+ if self.pool.get('account.analytic.account'):
+ id = objid._get_id(cr, uid, 'document', 'dir_project')
+ id = objid.browse(cr, uid, id, context=context).res_id
+ mid = self.pool.get('ir.model').search(cr, uid, [('model','=','account.analytic.account')])
+ obj.write(cr, uid, [id], {
+ 'type':'ressource',
+ 'ressource_type_id': mid[0],
+ 'domain': '[]',
+ 'ressource_tree': 1
+ })
+
+ # Update the action for FTP browse.
+ aid = objid._get_id(cr, uid, 'document', 'action_document_browse')
+ aid = objid.browse(cr, uid, aid, context=context).res_id
+ self.pool.get('ir.actions.url').write(cr, uid, [aid], {'url': 'ftp://'+(conf.host or 'localhost')+':8021/'})
+
+ return {
+ 'view_type': 'form',
+ "view_mode": 'form',
+ 'res_model': 'ir.actions.configuration.wizard',
+ 'type': 'ir.actions.act_window',
+ 'target': 'new',
+ }
+document_configuration_wizard()
diff --git a/addons/document/document.py b/addons/document/document.py
index cd3d667f8bf..1dffba0453d 100644
--- a/addons/document/document.py
+++ b/addons/document/document.py
@@ -22,498 +22,19 @@
import base64
from osv import osv, fields
-from osv.orm import except_orm
import urlparse
import os
import pooler
-from content_index import content_index
import netsvc
-import StringIO
+#import StringIO
-import random
-import string
from psycopg2 import Binary
-from tools import config
+#from tools import config
import tools
from tools.translate import _
-
-def random_name():
- random.seed()
- d = [random.choice(string.ascii_letters) for x in xrange(10) ]
- name = "".join(d)
- return name
-
-
-# Unsupported WebDAV Commands:
-# label
-# search
-# checkin
-# checkout
-# propget
-# propset
-
-#
-# An object that represent an uri
-# path: the uri of the object
-# content: the Content it belongs to (_print.pdf)
-# type: content or collection
-# content: objct = res.partner
-# collection: object = directory, object2 = res.partner
-# file: objct = ir.attachement
-# root: if we are at the first directory of a ressource
-#
-INVALID_CHARS={'*':str(hash('*')), '|':str(hash('|')) , "\\":str(hash("\\")), '/':'__', ':':str(hash(':')), '"':str(hash('"')), '<':str(hash('<')) , '>':str(hash('>')) , '?':str(hash('?'))}
-
-
-class node_class(object):
- def __init__(self, cr, uid, path, object, object2=False, context={}, content=False, type='collection', root=False):
- self.cr = cr
- self.uid = uid
- self.path = path
- self.object = object
- self.object2 = object2
- self.context = context
- self.content = content
- self.type=type
- self.root=root
-
- def _file_get(self, nodename=False):
- if not self.object:
- return []
- pool = pooler.get_pool(self.cr.dbname)
- fobj = pool.get('ir.attachment')
- res2 = []
- where = []
- if self.object2:
- where.append( ('res_model','=',self.object2._name) )
- where.append( ('res_id','=',self.object2.id) )
- else:
- where.append( ('parent_id','=',self.object.id) )
- where.append( ('res_id','=',False) )
- if nodename:
- where.append( (fobj._rec_name,'=',nodename) )
- for content in self.object.content_ids:
- if self.object2 or not content.include_name:
- if content.include_name:
- content_name = self.object2.name
- obj = pool.get(self.object.ressource_type_id.model)
- name_for = obj._name.split('.')[-1]
- if content_name and content_name.find(name_for) == 0 :
- content_name = content_name.replace(name_for,'')
- test_nodename = content_name + (content.suffix or '') + (content.extension or '')
- else:
- test_nodename = (content.suffix or '') + (content.extension or '')
- if test_nodename.find('/'):
- test_nodename=test_nodename.replace('/', '_')
- path = self.path+'/'+test_nodename
- if not nodename:
- n = node_class(self.cr, self.uid,path, self.object2, False, context=self.context, content=content, type='content', root=False)
- res2.append( n)
- else:
- if nodename == test_nodename:
- n = node_class(self.cr, self.uid, path, self.object2, False, context=self.context, content=content, type='content', root=False)
- res2.append(n)
-
- ids = fobj.search(self.cr, self.uid, where+[ ('parent_id','=',self.object and self.object.id or False) ])
- if self.object and self.root and (self.object.type=='ressource'):
- ids += fobj.search(self.cr, self.uid, where+[ ('parent_id','=',False) ])
- res = fobj.browse(self.cr, self.uid, ids, context=self.context)
- return map(lambda x: node_class(self.cr, self.uid, self.path+'/'+eval('x.'+fobj._rec_name), x, False, context=self.context, type='file', root=False), res) + res2
-
- def get_translation(self,value,lang):
- result = value
- #TODO : to get translation term
- return result
-
- def directory_list_for_child(self,nodename,parent=False):
- pool = pooler.get_pool(self.cr.dbname)
- where = []
- if nodename:
- nodename = self.get_translation(nodename, self.context['lang'])
- where.append(('name','=',nodename))
- if (self.object and self.object.type=='directory') or not self.object2:
- where.append(('parent_id','=',self.object and self.object.id or False))
- else:
- where.append(('parent_id','=',False))
- if self.object:
- where.append(('ressource_parent_type_id','=',self.object.ressource_type_id.id))
- else:
- where.append(('ressource_parent_type_id','=',False))
-
- ids = pool.get('document.directory').search(self.cr, self.uid, where+[('ressource_id','=',0)])
- if self.object2:
- ids += pool.get('document.directory').search(self.cr, self.uid, where+[('ressource_id','=',self.object2.id)])
- res = pool.get('document.directory').browse(self.cr, self.uid, ids, self.context)
- return res
-
- def _child_get(self, nodename=False):
- if self.type not in ('collection','database'):
- return []
- res = self.directory_list_for_child(nodename)
- result= map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name, x, x.type=='directory' and self.object2 or False, context=self.context, root=self.root), res)
- if self.type=='database':
- pool = pooler.get_pool(self.cr.dbname)
- fobj = pool.get('ir.attachment')
- vargs = [('parent_id','=',False),('res_id','=',False)]
- if nodename:
- vargs.append((fobj._rec_name,'=',nodename))
- file_ids=fobj.search(self.cr,self.uid,vargs)
-
- res = fobj.browse(self.cr, self.uid, file_ids, context=self.context)
- result +=map(lambda x: node_class(self.cr, self.uid, self.path+'/'+eval('x.'+fobj._rec_name), x, False, context=self.context, type='file', root=self.root), res)
- if self.type=='collection' and self.object.type=="ressource":
- where = self.object.domain and eval(self.object.domain, {'active_id':self.root, 'uid':self.uid}) or []
- pool = pooler.get_pool(self.cr.dbname)
- obj = pool.get(self.object.ressource_type_id.model)
- _dirname_field = obj._rec_name
- if len(obj.fields_get(self.cr, self.uid, ['dirname'])):
- _dirname_field = 'dirname'
-
- name_for = obj._name.split('.')[-1]
- if nodename and nodename.find(name_for) == 0 :
- id = int(nodename.replace(name_for,''))
- where.append(('id','=',id))
- elif nodename:
- if nodename.find('__') :
- nodename=nodename.replace('__','/')
- for invalid in INVALID_CHARS:
- if nodename.find(INVALID_CHARS[invalid]) :
- nodename=nodename.replace(INVALID_CHARS[invalid],invalid)
- nodename = self.get_translation(nodename, self.context['lang'])
- where.append((_dirname_field,'=',nodename))
-
- if self.object.ressource_tree:
- if obj._parent_name in obj.fields_get(self.cr,self.uid):
- where.append((obj._parent_name,'=',self.object2 and self.object2.id or False))
- ids = obj.search(self.cr, self.uid, where)
- res = obj.browse(self.cr, self.uid, ids,self.context)
- result+= map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name.replace('/','__'), self.object, x, context=self.context, root=x.id), res)
- return result
- else :
- if self.object2:
- return result
- else:
- if self.object2:
- return result
-
-
- ids = obj.search(self.cr, self.uid, where)
- res = obj.browse(self.cr, self.uid, ids,self.context)
- for r in res:
- if len(obj.fields_get(self.cr, self.uid, [_dirname_field])):
- r.name = eval('r.'+_dirname_field)
- else:
- r.name = False
- if not r.name:
- r.name = name_for + '%d'%r.id
- for invalid in INVALID_CHARS:
- if r.name.find(invalid) :
- r.name = r.name.replace(invalid,INVALID_CHARS[invalid])
- result2 = map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name.replace('/','__'), self.object, x, context=self.context, root=x.id), res)
- if result2:
- if self.object.ressource_tree:
- result += result2
- else:
- result = result2
- return result
-
- def children(self):
- return self._child_get() + self._file_get()
-
- def child(self, name):
- res = self._child_get(name)
- if res:
- return res[0]
- res = self._file_get(name)
- if res:
- return res[0]
- return None
-
- def path_get(self):
- path = self.path
- if self.path[0]=='/':
- path = self.path[1:]
- return path
-
-class document_directory(osv.osv):
- _name = 'document.directory'
- _description = 'Document directory'
- _columns = {
- 'name': fields.char('Name', size=64, required=True, select=1),
- 'write_date': fields.datetime('Date Modified', readonly=True),
- 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
- 'create_date': fields.datetime('Date Created', readonly=True),
- 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
- 'file_type': fields.char('Content Type', size=32),
- 'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
- 'user_id': fields.many2one('res.users', 'Owner'),
- 'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
- 'parent_id': fields.many2one('document.directory', 'Parent Item'),
- 'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
- 'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
- 'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
- 'type': fields.selection([('directory','Static Directory'),('ressource','Other Resources')], 'Type', required=True),
- 'ressource_type_id': fields.many2one('ir.model', 'Directories Mapped to Objects',
- help="Select an object here and Open ERP will create a mapping for each of these " \
- "objects, using the given domain, when browsing through FTP."),
- 'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model',
- help="If you put an object here, this directory template will appear bellow all of these objects. " \
- "Don't put a parent directory if you select a parent model."),
- 'ressource_id': fields.integer('Resource ID'),
- 'ressource_tree': fields.boolean('Tree Structure',
- help="Check this if you want to use the same tree structure as the object selected in the system."),
- }
- _defaults = {
- 'user_id': lambda self,cr,uid,ctx: uid,
- 'domain': lambda self,cr,uid,ctx: '[]',
- 'type': lambda *args: 'directory',
- 'ressource_id': lambda *a: 0
- }
- _sql_constraints = [
- ('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !')
- ]
-
- def get_resource_path(self,cr,uid,dir_id,res_model,res_id):
- # this method will be used in process module
- # to be need test and Improvement if resource dir has parent resource (link resource)
- path=[]
- def _parent(dir_id,path):
- parent=self.browse(cr,uid,dir_id)
- if parent.parent_id and not parent.ressource_parent_type_id:
- _parent(parent.parent_id.id,path)
- path.append(parent.name)
- else:
- path.append(parent.name)
- return path
-
- directory=self.browse(cr,uid,dir_id)
- model_ids=self.pool.get('ir.model').search(cr,uid,[('model','=',res_model)])
- if directory:
- _parent(dir_id,path)
- if res_id:
- path.append(self.pool.get(directory.ressource_type_id.model).browse(cr,uid,res_id).name)
- user=self.pool.get('res.users').browse(cr,uid,uid)
- return "ftp://%s:%s@localhost:%s/%s/%s"%(user.login,user.password,config.get('ftp_server_port',8021),cr.dbname,'/'.join(path))
- return False
-
- def _check_recursion(self, cr, uid, ids):
- level = 100
- while len(ids):
- cr.execute('select distinct parent_id from document_directory where id in ('+','.join(map(str, ids))+')')
- ids = filter(None, map(lambda x:x[0], cr.fetchall()))
- if not level:
- return False
- level -= 1
- return True
-
- _constraints = [
- (_check_recursion, 'Error! You can not create recursive Directories.', ['parent_id'])
- ]
- def __init__(self, *args, **kwargs):
- res = super(document_directory, self).__init__(*args, **kwargs)
- self._cache = {}
-
- def onchange_content_id(self, cr, uid, ids, ressource_type_id):
- return {}
-
- def _get_childs(self, cr, uid, node, nodename=False, context={}):
- where = []
- if nodename:
- nodename = self.get_translation(nodename, self.context['lang'])
- where.append(('name','=',nodename))
- if object:
- where.append(('parent_id','=',object.id))
- ids = self.search(cr, uid, where, context)
- return self.browse(cr, uid, ids, context), False
-
- """
- PRE:
- uri: of the form "Sales Order/SO001"
- PORT:
- uri
- object: the object.directory or object.directory.content
- object2: the other object linked (if object.directory.content)
- """
- def get_object(self, cr, uid, uri, context={}):
- #TODO : set user's context_lang in context
- context.update({'lang':False})
- if not uri:
- return node_class(cr, uid, '', False, context=context, type='database')
- turi = tuple(uri)
- if False and (turi in self._cache):
- (path, oo, oo2, context, content,type,root) = self._cache[turi]
- if oo:
- object = self.pool.get(oo[0]).browse(cr, uid, oo[1], context)
- else:
- object = False
- if oo2:
- object2 = self.pool.get(oo2[0]).browse(cr, uid, oo2[1], context)
- else:
- object2 = False
- node = node_class(cr, uid, '/', False, context=context, type='database')
- return node
-
- node = node_class(cr, uid, '/', False, context=context, type='database')
- for path in uri[:]:
- if path:
- node = node.child(path)
- if not node:
- return False
- oo = node.object and (node.object._name, node.object.id) or False
- oo2 = node.object2 and (node.object2._name, node.object2.id) or False
- self._cache[turi] = (node.path, oo, oo2, node.context, node.content,node.type,node.root)
- return node
-
- def get_childs(self, cr, uid, uri, context={}):
- node = self.get_object(cr, uid, uri, context)
- if uri:
- children = node.children()
- else:
- children= [node]
- result = map(lambda node: node.path_get(), children)
- #childs,object2 = self._get_childs(cr, uid, object, False, context)
- #result = map(lambda x: urlparse.urljoin(path+'/',x.name), childs)
- return result
-
- def copy(self, cr, uid, id, default=None, context=None):
- if not default:
- default ={}
- name = self.read(cr, uid, [id])[0]['name']
- default.update({'name': name+ " (copy)"})
- return super(document_directory,self).copy(cr,uid,id,default,context)
-
- def _check_duplication(self, cr, uid,vals,ids=[],op='create'):
- name=vals.get('name',False)
- parent_id=vals.get('parent_id',False)
- ressource_parent_type_id=vals.get('ressource_parent_type_id',False)
- ressource_id=vals.get('ressource_id',0)
- if op=='write':
- for directory in self.browse(cr,uid,ids):
- if not name:
- name=directory.name
- if not parent_id:
- parent_id=directory.parent_id and directory.parent_id.id or False
- if not ressource_parent_type_id:
- ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False
- if not ressource_id:
- ressource_id=directory.ressource_id and directory.ressource_id or 0
- res=self.search(cr,uid,[('id','<>',directory.id),('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
- if len(res):
- return False
- if op=='create':
- res=self.search(cr,uid,[('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
- if len(res):
- return False
- return True
- def write(self, cr, uid, ids, vals, context=None):
- if not self._check_duplication(cr,uid,vals,ids,op='write'):
- raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
- return super(document_directory,self).write(cr,uid,ids,vals,context=context)
-
- def create(self, cr, uid, vals, context=None):
- if not self._check_duplication(cr,uid,vals):
- raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
- if vals.get('name',False) and (vals.get('name').find('/')+1 or vals.get('name').find('@')+1 or vals.get('name').find('$')+1 or vals.get('name').find('#')+1) :
- raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
- return super(document_directory,self).create(cr, uid, vals, context)
-
-document_directory()
-
-class document_directory_node(osv.osv):
- _inherit = 'process.node'
- _columns = {
- 'directory_id': fields.many2one('document.directory', 'Document directory', ondelete="set null"),
- }
-document_directory_node()
-
-class document_directory_content_type(osv.osv):
- _name = 'document.directory.content.type'
- _description = 'Directory Content Type'
- _columns = {
- 'name': fields.char('Content Type', size=64, required=True),
- 'code': fields.char('Extension', size=4),
- 'active': fields.boolean('Active'),
- }
- _defaults = {
- 'active': lambda *args: 1
- }
-document_directory_content_type()
-
-class document_directory_content(osv.osv):
- _name = 'document.directory.content'
- _description = 'Directory Content'
- _order = "sequence"
- def _extension_get(self, cr, uid, context={}):
- cr.execute('select code,name from document_directory_content_type where active')
- res = cr.fetchall()
- return res
- _columns = {
- 'name': fields.char('Content Name', size=64, required=True),
- 'sequence': fields.integer('Sequence', size=16),
- 'suffix': fields.char('Suffix', size=16),
- 'report_id': fields.many2one('ir.actions.report.xml', 'Report'),
- 'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4),
- 'include_name': fields.boolean('Include Record Name', help="Check this field if you want that the name of the file start by the record name."),
- 'directory_id': fields.many2one('document.directory', 'Directory'),
- }
- _defaults = {
- 'extension': lambda *args: '.pdf',
- 'sequence': lambda *args: 1,
- 'include_name': lambda *args: 1,
- }
- def process_write_pdf(self, cr, uid, node, context={}):
- return True
- def process_read_pdf(self, cr, uid, node, context={}):
- report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.content.report_id.id)
- srv = netsvc.LocalService('report.'+report.report_name)
- pdf,pdftype = srv.create(cr, uid, [node.object.id], {}, {})
- s = StringIO.StringIO(pdf)
- s.name = node
- return s
-document_directory_content()
-
-class ir_action_report_xml(osv.osv):
- _name="ir.actions.report.xml"
- _inherit ="ir.actions.report.xml"
-
- def _model_get(self, cr, uid, ids, name, arg, context):
- res = {}
- model_pool = self.pool.get('ir.model')
- for data in self.read(cr,uid,ids,['model']):
- model = data.get('model',False)
- if model:
- model_id =model_pool.search(cr,uid,[('model','=',model)])
- if model_id:
- res[data.get('id')] = model_id[0]
- else:
- res[data.get('id')] = False
- return res
-
- def _model_search(self, cr, uid, obj, name, args):
- if not len(args):
- return []
- model_id= args[0][2]
- if not model_id:
- return []
- model = self.pool.get('ir.model').read(cr,uid,[model_id])[0]['model']
- report_id = self.search(cr,uid,[('model','=',model)])
- if not report_id:
- return [('id','=','0')]
- return [('id','in',report_id)]
-
- _columns={
- 'model_id' : fields.function(_model_get,fnct_search=_model_search,method=True,string='Model Id'),
- }
-
-ir_action_report_xml()
-
-def create_directory(path):
- dir_name = random_name()
- path = os.path.join(path,dir_name)
- os.makedirs(path)
- return dir_name
+import nodes
class document_file(osv.osv):
_inherit = 'ir.attachment'
@@ -522,70 +43,49 @@ class document_file(osv.osv):
return os.path.join(tools.config['root_path'], 'filestore', cr.dbname)
def _data_get(self, cr, uid, ids, name, arg, context):
+ fbrl = self.browse(cr,uid,ids,context=context)
+ nctx = nodes.get_node_context(cr,uid,context)
result = {}
- cr.execute('select id,store_fname,link from ir_attachment where id in ('+','.join(map(str, ids))+')')
- for id,r,l in cr.fetchall():
- try:
- value = file(os.path.join(self._get_filestore(cr), r), 'rb').read()
- result[id] = base64.encodestring(value)
- except:
- result[id]=''
-# if context.get('bin_size', False):
-# result[id] = tools.human_size(result[id])
+ bin_size = context.get('bin_size', False)
+ for fbro in fbrl:
+ fnode = nodes.node_file(None,None,nctx,fbro)
+ if not bin_size:
+ data = fnode.get_data(cr,fbro)
+ result[fbro.id] = base64.encodestring(data or '')
+ else:
+ result[fbro.id] = fnode.get_data_len(cr,fbro)
+
return result
#
# This code can be improved
#
- def _data_set(self, cr, uid, id, name, value, args=None, context={}):
+ def _data_set(self, cr, uid, id, name, value, arg, context):
if not value:
- filename = self.browse(cr, uid, id, context).store_fname
- try:
- os.unlink(os.path.join(self._get_filestore(cr), filename))
- except:
- pass
- cr.execute('update ir_attachment set store_fname=NULL WHERE id=%s', (id,) )
return True
- #if (not context) or context.get('store_method','fs')=='fs':
- try:
- path = self._get_filestore(cr)
- if not os.path.isdir(path):
- try:
- os.makedirs(path)
- except:
- raise except_orm(_('Permission Denied !'), _('You do not permissions to write on the server side.'))
-
- flag = None
- # This can be improved
- for dirs in os.listdir(path):
- if os.path.isdir(os.path.join(path,dirs)) and len(os.listdir(os.path.join(path,dirs)))<4000:
- flag = dirs
- break
- flag = flag or create_directory(path)
- filename = random_name()
- fname = os.path.join(path, flag, filename)
- fp = file(fname,'wb')
- v = base64.decodestring(value)
- fp.write(v)
- filesize = os.stat(fname).st_size
- cr.execute('update ir_attachment set store_fname=%s,store_method=%s,file_size=%s where id=%s', (os.path.join(flag,filename),'fs',len(v),id))
- return True
- except Exception,e :
- raise except_orm(_('Error!'), str(e))
+ fbro = self.browse(cr,uid,id,context=context)
+ nctx = nodes.get_node_context(cr,uid,context)
+ fnode = nodes.node_file(None,None,nctx,fbro)
+ res = fnode.set_data(cr,base64.decodestring(value),fbro)
+ return res
_columns = {
'user_id': fields.many2one('res.users', 'Owner', select=1),
'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
- 'parent_id': fields.many2one('document.directory', 'Directory', select=1),
+ # the directory id now is mandatory. It can still be computed automatically.
+ 'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True),
'file_size': fields.integer('File Size', required=True),
'file_type': fields.char('Content Type', size=32),
+ # If ir.attachment contained any data before document is installed, preserve
+ # the data, don't drop the column!
+ 'db_datas': fields.binary('Data',oldname='datas'),
'index_content': fields.text('Indexed Content'),
'write_date': fields.datetime('Date Modified', readonly=True),
'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
'create_date': fields.datetime('Date Created', readonly=True),
'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
'store_method': fields.selection([('db','Database'),('fs','Filesystem'),('link','Link')], "Storing Method"),
- 'datas': fields.function(_data_get,method=True,fnct_inv=_data_set,string='File Content',type="binary"),
+ 'datas': fields.function(_data_get,method=True,fnct_inv=_data_set,string='File Content',type="binary", nodrop=True),
'store_fname': fields.char('Stored Filename', size=200),
'res_model': fields.char('Attached Model', size=64), #res_model
'res_id': fields.integer('Attached ID'), #res_id
@@ -593,10 +93,15 @@ class document_file(osv.osv):
'title': fields.char('Resource Title',size=64),
}
+ def __get_def_directory(self,cr,uid, context = None):
+ dirobj = self.pool.get('document.directory')
+ return dirobj._get_root_directory(cr,uid,context)
+
_defaults = {
'user_id': lambda self,cr,uid,ctx:uid,
'file_size': lambda self,cr,uid,ctx:0,
- 'store_method': lambda *args: 'db'
+ 'store_method': lambda *args: 'db',
+ 'parent_id': __get_def_directory
}
_sql_constraints = [
('filename_uniq', 'unique (name,parent_id,res_id,res_model)', 'The file name must be unique !')
@@ -638,20 +143,11 @@ class document_file(osv.osv):
raise except_orm(_('ValidateError'), _('File name must be unique!'))
result = super(document_file,self).write(cr,uid,ids,vals,context=context)
cr.commit()
- try:
- for f in self.browse(cr, uid, ids, context=context):
- #if 'datas' not in vals:
- # vals['datas']=f.datas
- res = content_index(base64.decodestring(vals['datas']), f.datas_fname, f.file_type or None)
- super(document_file,self).write(cr, uid, ids, {
- 'index_content': res
- })
- cr.commit()
- except:
- pass
return result
- def create(self, cr, uid, vals, context={}):
+ def create(self, cr, uid, vals, context=None):
+ if not context:
+ context = {}
vals['title']=vals['name']
vals['parent_id'] = context.get('parent_id',False) or vals.get('parent_id',False)
if not vals.get('res_id', False) and context.get('default_res_id',False):
@@ -686,200 +182,30 @@ class document_file(osv.osv):
import urllib
datas=base64.encodestring(urllib.urlopen(vals['link']).read())
else:
- datas = vals.get('datas',False)
-
+ datas=vals.get('datas',False)
vals['file_size']= datas and len(datas) or 0
if not self._check_duplication(cr,uid,vals):
raise except_orm(_('ValidateError'), _('File name must be unique!'))
result = super(document_file,self).create(cr, uid, vals, context)
cr.commit()
- try:
- res = content_index(base64.decodestring(datas), vals['datas_fname'], vals.get('content_type', None))
- super(document_file,self).write(cr, uid, [result], {
- 'index_content': res,
- })
- cr.commit()
- except:
- pass
return result
def unlink(self,cr, uid, ids, context={}):
+ stor = self.pool.get('document.storage')
+ unres= []
+ # We have to do the unlink in 2 stages: prepare a list of actual
+ # files to be unlinked, update the db (safer to do first, can be
+ # rolled back) and then unlink the files. The list wouldn't exist
+ # after we discard the objects
+
for f in self.browse(cr, uid, ids, context):
- #if f.store_method=='fs':
- try:
- os.unlink(os.path.join(self._get_filestore(cr), f.store_fname))
- except:
- pass
- return super(document_file, self).unlink(cr, uid, ids, context)
+ # TODO: update the node cache
+ r = stor.prepare_unlink(cr,uid,f.parent_id.storage_id, f)
+ if r:
+ unres.append(r)
+ res = super(document_file, self).unlink(cr, uid, ids, context)
+ stor.do_unlink(cr,uid,unres)
+ return res
+
document_file()
-class document_configuration_wizard(osv.osv_memory):
- _name='document.configuration.wizard'
- _rec_name = 'Auto Directory configuration'
- _columns = {
- 'host': fields.char('Server Address', size=64, help="Put here the server address or IP. " \
- "Keep localhost if you don't know what to write.", required=True),
- 'suggested_host': fields.char('Suggested Server Address', size=64, readonly=True, help="This is the guessed server address"),
- 'port': fields.char('Server Port', size=5, help="Put here the server port. " \
- "Keep 8021 if you don't know what to write.", required=True)
- }
-
- def get_ftp_server_address(self, cr, uid, context=None):
- default_address = config.get('ftp_server_address', None)
- if default_address is None:
- default_address = tools.misc.detect_ip_addr()
-
- return default_address
-
- def get_ftp_server_port(self, cr, uid, context=None):
- return config.get('ftp_server_port', 8021)
-
- def get_suggested_ftp_server_address(self, cr, uid, context=None):
- return tools.misc.detect_ip_addr()
-
- _defaults = {
- 'host': get_ftp_server_address,
- 'suggested_host': get_suggested_ftp_server_address,
- 'port': get_ftp_server_port,
- }
-
- def action_cancel(self,cr,uid,ids,conect=None):
- return {
- 'view_type': 'form',
- "view_mode": 'form',
- 'res_model': 'ir.actions.configuration.wizard',
- 'type': 'ir.actions.act_window',
- 'target':'new',
- }
-
- def action_config(self, cr, uid, ids, context=None):
- conf = self.browse(cr, uid, ids[0], context)
- obj=self.pool.get('document.directory')
- objid=self.pool.get('ir.model.data')
-
- if self.pool.get('sale.order'):
- id = objid._get_id(cr, uid, 'document', 'dir_sale_order_all')
- id = objid.browse(cr, uid, id, context=context).res_id
- mid = self.pool.get('ir.model').search(cr, uid, [('model','=','sale.order')])
- obj.write(cr, uid, [id], {
- 'type':'ressource',
- 'ressource_type_id': mid[0],
- 'domain': '[]',
- })
- aid = objid._get_id(cr, uid, 'sale', 'report_sale_order')
- aid = objid.browse(cr, uid, aid, context=context).res_id
-
- self.pool.get('document.directory.content').create(cr, uid, {
- 'name': "Print Order",
- 'suffix': "_print",
- 'report_id': aid,
- 'extension': '.pdf',
- 'include_name': 1,
- 'directory_id': id,
- })
- id = objid._get_id(cr, uid, 'document', 'dir_sale_order_quote')
- id = objid.browse(cr, uid, id, context=context).res_id
- obj.write(cr, uid, [id], {
- 'type':'ressource',
- 'ressource_type_id': mid[0],
- 'domain': "[('state','=','draft')]",
- })
-
- if self.pool.get('product.product'):
- id = objid._get_id(cr, uid, 'document', 'dir_product')
- id = objid.browse(cr, uid, id, context=context).res_id
- mid = self.pool.get('ir.model').search(cr, uid, [('model','=','product.product')])
- obj.write(cr, uid, [id], {
- 'type':'ressource',
- 'ressource_type_id': mid[0],
- })
-
- if self.pool.get('stock.location'):
- aid = objid._get_id(cr, uid, 'stock', 'report_product_history')
- aid = objid.browse(cr, uid, aid, context=context).res_id
-
- self.pool.get('document.directory.content').create(cr, uid, {
- 'name': "Product Stock",
- 'suffix': "_stock_forecast",
- 'report_id': aid,
- 'extension': '.pdf',
- 'include_name': 1,
- 'directory_id': id,
- })
-
- if self.pool.get('account.analytic.account'):
- id = objid._get_id(cr, uid, 'document', 'dir_project')
- id = objid.browse(cr, uid, id, context=context).res_id
- mid = self.pool.get('ir.model').search(cr, uid, [('model','=','account.analytic.account')])
- obj.write(cr, uid, [id], {
- 'type':'ressource',
- 'ressource_type_id': mid[0],
- 'domain': '[]',
- 'ressource_tree': 1
- })
-
- aid = objid._get_id(cr, uid, 'document', 'action_document_browse')
- aid = objid.browse(cr, uid, aid, context=context).res_id
- self.pool.get('ir.actions.url').write(cr, uid, [aid], {'url': 'ftp://'+(conf.host or 'localhost')+':'+conf.port+'/'})
-
- config['ftp_server_address'] = conf.host
- config['ftp_server_port'] = conf.port
- config.save()
-
- return {
- 'view_type': 'form',
- "view_mode": 'form',
- 'res_model': 'ir.actions.configuration.wizard',
- 'type': 'ir.actions.act_window',
- 'target': 'new',
- }
-
-document_configuration_wizard()
-
-
-class document_configuration_ftpserver_wizard(osv.osv_memory):
- _name='document.configuration.ftp_server.wizard'
- _rec_name = 'Configure FTP server address'
- _columns = {
- 'host': fields.char('Server Address', size=64, help="Put here the server address or IP. " \
- "Keep localhost if you don't know what to write.", required=True),
- 'suggested_host': fields.char('Suggested Server Address', size=64, readonly=True, help="This is the guessed server address"),
- 'port': fields.char('Server Port', size=5, help="Put here the server port. " \
- "Keep 8021 if you don't know what to write.", required=True)
- }
-
- def get_ftp_server_address(self, cr, uid, context=None):
- default_address = tools.misc.detect_ip_addr()
- return config.get('ftp_server_address', default_address)
-
- def get_ftp_server_port(self, cr, uid, context=None):
- return config.get('ftp_server_port', '8021')
-
- def get_suggested_ftp_server_address(self, cr, uid, context=None):
- return tools.misc.detect_ip_addr()
-
- _defaults = {
- 'host': get_ftp_server_address,
- 'suggested_host': get_suggested_ftp_server_address,
- 'port': get_ftp_server_port,
- }
-
- def action_cancel(self,cr,uid,ids,conect=None):
- return {}
-
- def action_config(self, cr, uid, ids, context=None):
- conf = self.browse(cr, uid, ids[0], context)
- obj = self.pool.get('ir.model.data')
-
- aid = obj._get_id(cr, uid, 'document', 'action_document_browse')
- aid = obj.browse(cr, uid, aid, context=context).res_id
- self.pool.get('ir.actions.url').write(cr, uid, [aid], {'url': 'ftp://'+(conf.host or 'localhost')+':'+conf.port+'/'})
-
- config['ftp_server_address'] = conf.host
- config['ftp_server_port'] = conf.port
- config.save()
-
- return {}
-
-document_configuration_ftpserver_wizard()
-
diff --git a/addons/document/document_data.xml b/addons/document/document_data.xml
index 511de005b6f..77c80b078d1 100644
--- a/addons/document/document_data.xml
+++ b/addons/document/document_data.xml
@@ -1,10 +1,112 @@
-
+
.pdfPDF Report
+ application/pdf
+
+
+
+ Default File storage
+
+
+
+
+ DB storage
+
+
+
+
+
+
+ Documents
+
+
+ 0
+
+
+
+ My Folder
+
+
+
+ 0
+
+
+
+
+ Partners by Category
+
+ ressource
+ 1
+
+ 0
+
+
+
+
+
+
+ Partners
+ ressource
+ [('category_id','in',[active_id])]
+
+
+
+ 0
+
+
+
+
+ Personnal Folders
+
+ ressource
+
+
+ 0
+
+
+
+
+ Products
+
+
+ 0
+
+
+
+
+ Sales Order
+
+
+ 0
+
+
+
+
+ All Sales Order
+
+
+ 0
+
+
+
+
+ Quotations
+
+
+ 0
+
+
+
+
+ Projects
+
+
+ 0
+
diff --git a/addons/document/document_demo.xml b/addons/document/document_demo.xml
index 95910e6031b..1c757a73909 100644
--- a/addons/document/document_demo.xml
+++ b/addons/document/document_demo.xml
@@ -2,90 +2,6 @@
-
- Documents
-
- 0
-
-
-
- My Folder
-
-
- 0
-
-
-
-
- Partners by Category
-
- ressource
- 1
- 0
-
-
-
-
-
-
- Partners
- ressource
- [('category_id','in',[active_id])]
-
-
-
- 0
-
-
-
-
- Personnal Folders
-
- ressource
-
- 0
-
-
-
-
- Products
-
-
- 0
-
-
-
-
- Sales Order
-
-
- 0
-
-
-
-
- All Sales Order
-
-
- 0
-
-
-
-
- Quotations
-
-
- 0
-
-
-
-
- Projects
-
-
- 0
-
-
diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py
new file mode 100644
index 00000000000..133927f9664
--- /dev/null
+++ b/addons/document/document_directory.py
@@ -0,0 +1,312 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2009 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import base64
+
+from osv import osv, fields
+from osv.orm import except_orm
+import urlparse
+
+import os
+import nodes
+
+class document_directory(osv.osv):
+ _name = 'document.directory'
+ _description = 'Document directory'
+ _columns = {
+ 'name': fields.char('Name', size=64, required=True, select=1),
+ 'write_date': fields.datetime('Date Modified', readonly=True),
+ 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
+ 'create_date': fields.datetime('Date Created', readonly=True),
+ 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
+ 'file_type': fields.char('Content Type', size=32),
+ 'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
+ 'user_id': fields.many2one('res.users', 'Owner'),
+ 'storage_id': fields.many2one('document.storage', 'Storage'),
+ 'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
+ 'parent_id': fields.many2one('document.directory', 'Parent Item'),
+ 'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
+ 'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
+ 'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
+ 'type': fields.selection([('directory','Static Directory'),('ressource','Other Resources')], 'Type', required=True),
+ 'ressource_type_id': fields.many2one('ir.model', 'Directories Mapped to Objects',
+ help="Select an object here and Open ERP will create a mapping for each of these " \
+ "objects, using the given domain, when browsing through FTP."),
+ 'resource_field': fields.char('Name field',size=32,help='Field to be used as name on resource directories. If empty, the "name" will be used.'),
+ 'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model',
+ help="If you put an object here, this directory template will appear bellow all of these objects. " \
+ "Don't put a parent directory if you select a parent model."),
+ 'ressource_id': fields.integer('Resource ID'),
+ 'ressource_tree': fields.boolean('Tree Structure',
+ help="Check this if you want to use the same tree structure as the object selected in the system."),
+ 'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
+ }
+ def _get_root_directory(self, cr,uid, context=None):
+ objid=self.pool.get('ir.model.data')
+ try:
+ mid = objid._get_id(cr, uid, 'document', 'dir_root')
+ if not mid:
+ return None
+ except Exception, e:
+ import netsvc
+ logger = netsvc.Logger()
+ logger.notifyChannel("document", netsvc.LOG_WARNING, 'Cannot set directory root:'+ str(e))
+ return None
+ return objid.browse(cr, uid, mid, context=context).res_id
+
+ def _get_def_storage(self,cr,uid,context=None):
+ if context and context.has_key('default_parent_id'):
+ # Use the same storage as the parent..
+ diro = self.browse(cr,uid,context['default_parent_id'])
+ if diro.storage_id:
+ return diro.storage_id.id
+ objid=self.pool.get('ir.model.data')
+ try:
+ mid = objid._get_id(cr, uid, 'document', 'storage_default')
+ return objid.browse(cr, uid, mid, context=context).res_id
+ except Exception:
+ return None
+
+ _defaults = {
+ 'user_id': lambda self,cr,uid,ctx: uid,
+ 'domain': lambda self,cr,uid,ctx: '[]',
+ 'type': lambda *args: 'directory',
+ 'ressource_id': lambda *a: 0,
+ 'parent_id': _get_root_directory,
+ 'storage_id': _get_def_storage,
+ }
+ _sql_constraints = [
+ ('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'),
+ ('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!')
+ ]
+
+ def ol_get_resource_path(self,cr,uid,dir_id,res_model,res_id):
+ # this method will be used in process module
+ # to be need test and Improvement if resource dir has parent resource (link resource)
+ path=[]
+ def _parent(dir_id,path):
+ parent=self.browse(cr,uid,dir_id)
+ if parent.parent_id and not parent.ressource_parent_type_id:
+ _parent(parent.parent_id.id,path)
+ path.append(parent.name)
+ else:
+ path.append(parent.name)
+ return path
+
+ directory=self.browse(cr,uid,dir_id)
+ model_ids=self.pool.get('ir.model').search(cr,uid,[('model','=',res_model)])
+ if directory:
+ _parent(dir_id,path)
+ path.append(self.pool.get(directory.ressource_type_id.model).browse(cr,uid,res_id).name)
+ #user=self.pool.get('res.users').browse(cr,uid,uid)
+ #return "ftp://%s:%s@localhost:%s/%s/%s"%(user.login,user.password,config.get('ftp_server_port',8021),cr.dbname,'/'.join(path))
+ # No way we will return the password!
+ return "ftp://user:pass@host:port/test/this"
+ return False
+
+ def _check_recursion(self, cr, uid, ids):
+ level = 100
+ while len(ids):
+ cr.execute('select distinct parent_id from document_directory where id in ('+','.join(map(str,ids))+')')
+ ids = filter(None, map(lambda x:x[0], cr.fetchall()))
+ if not level:
+ return False
+ level -= 1
+ return True
+
+ _constraints = [
+ (_check_recursion, 'Error! You can not create recursive Directories.', ['parent_id'])
+ ]
+ def __init__(self, *args, **kwargs):
+ res = super(document_directory, self).__init__(*args, **kwargs)
+ #self._cache = {}
+
+ def onchange_content_id(self, cr, uid, ids, ressource_type_id):
+ return {}
+
+ """
+ PRE:
+ uri: of the form "Sales Order/SO001"
+ PORT:
+ uri
+ object: the object.directory or object.directory.content
+ object2: the other object linked (if object.directory.content)
+ """
+ def get_object(self, cr, uid, uri, context=None):
+ """ Return a node object for the given uri.
+ This fn merely passes the call to node_context
+ """
+ if not context:
+ context = {}
+ lang = context.get('lang',False)
+ if not lang:
+ user = self.pool.get('res.users').browse(cr, uid, uid)
+ lang = user.context_lang
+ context['lang'] = lang
+
+ try: #just instrumentation
+ return nodes.get_node_context(cr, uid, context).get_uri(cr,uri)
+ except Exception,e:
+ print "exception: ",e
+ raise
+
+
+ def _locate_child(self, cr,uid, root_id, uri,nparent, ncontext):
+ """ try to locate the node in uri,
+ Return a tuple (node_dir, remaining_path)
+ """
+ did = root_id
+ duri = uri
+ path = []
+ context = ncontext.context
+ while len(duri):
+ nid = self.search(cr,uid,[('parent_id','=',did),('name','=',duri[0]),('type','=','directory')], context=context)
+ if not nid:
+ break
+ if len(nid)>1:
+ print "Duplicate dir? p= %d, n=\"%s\"" %(did,duri[0])
+ path.append(duri[0])
+ duri = duri[1:]
+ did = nid[0]
+
+ return (nodes.node_dir(path, nparent,ncontext,self.browse(cr,uid,did, context)), duri)
+
+
+ nid = self.search(cr,uid,[('parent_id','=',did),('name','=',duri[0]),('type','=','ressource')], context=context)
+ if nid:
+ if len(nid)>1:
+ print "Duplicate dir? p= %d, n=\"%s\"" %(did,duri[0])
+ path.append(duri[0])
+ duri = duri[1:]
+ did = nid[0]
+ return nodes.node_res_dir(path, nparent,ncontext,self.browse(cr,uid,did, context))
+
+ # Here, we must find the appropriate non-dir child..
+ # Chech for files:
+ fil_obj = self.pool.get('ir.attachment')
+ nid = fil_obj.search(cr,uid,[('parent_id','=',did),('name','=',duri[0])],context=context)
+ if nid:
+ if len(duri)>1:
+ # cannot treat child as a dir
+ return None
+ if len(nid)>1:
+ print "Duplicate file?",did,duri[0]
+ path.append(duri[0])
+ return nodes.node_file(path,nparent,ncontext,fil_obj.browse(cr,uid,nid[0],context))
+
+ print "nothing found:",did, duri
+ #still, nothing found
+ return None
+
+ def old_code():
+ if not uri:
+ return node_database(cr, uid, context=context)
+ turi = tuple(uri)
+ node = node_class(cr, uid, '/', False, context=context, type='database')
+ for path in uri[:]:
+ if path:
+ node = node.child(path)
+ if not node:
+ return False
+ oo = node.object and (node.object._name, node.object.id) or False
+ oo2 = node.object2 and (node.object2._name, node.object2.id) or False
+ return node
+
+ def ol_get_childs(self, cr, uid, uri, context={}):
+ node = self.get_object(cr, uid, uri, context)
+ if uri:
+ children = node.children()
+ else:
+ children= [node]
+ result = map(lambda node: node.path_get(), children)
+ return result
+
+ def copy(self, cr, uid, id, default=None, context=None):
+ if not default:
+ default ={}
+ name = self.read(cr, uid, [id])[0]['name']
+ default.update({'name': name+ " (copy)"})
+ return super(document_directory,self).copy(cr,uid,id,default,context)
+
+ def _check_duplication(self, cr, uid,vals,ids=[],op='create'):
+ name=vals.get('name',False)
+ parent_id=vals.get('parent_id',False)
+ ressource_parent_type_id=vals.get('ressource_parent_type_id',False)
+ ressource_id=vals.get('ressource_id',0)
+ if op=='write':
+ for directory in self.browse(cr,uid,ids):
+ if not name:
+ name=directory.name
+ if not parent_id:
+ parent_id=directory.parent_id and directory.parent_id.id or False
+ if not ressource_parent_type_id:
+ ressource_parent_type_id=directory.ressource_parent_type_id and directory.ressource_parent_type_id.id or False
+ if not ressource_id:
+ ressource_id=directory.ressource_id and directory.ressource_id or 0
+ res=self.search(cr,uid,[('id','<>',directory.id),('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
+ if len(res):
+ return False
+ if op=='create':
+ res=self.search(cr,uid,[('name','=',name),('parent_id','=',parent_id),('ressource_parent_type_id','=',ressource_parent_type_id),('ressource_id','=',ressource_id)])
+ if len(res):
+ return False
+ return True
+ def write(self, cr, uid, ids, vals, context=None):
+ if not self._check_duplication(cr,uid,vals,ids,op='write'):
+ raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
+ return super(document_directory,self).write(cr,uid,ids,vals,context=context)
+
+ def create(self, cr, uid, vals, context=None):
+ if not self._check_duplication(cr,uid,vals):
+ raise osv.except_osv(_('ValidateError'), _('Directory name must be unique!'))
+ if vals.get('name',False) and (vals.get('name').find('/')+1 or vals.get('name').find('@')+1 or vals.get('name').find('$')+1 or vals.get('name').find('#')+1) :
+ raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
+ return super(document_directory,self).create(cr, uid, vals, context)
+
+document_directory()
+
+class document_directory_dctx(osv.osv):
+ """ In order to evaluate dynamic folders, child items could have a limiting
+ domain expression. For that, their parents will export a context where useful
+ information will be passed on.
+ If you define sth like "s_id" = "this.id" at a folder iterating over sales, its
+ children could have a domain like [('sale_id', = ,dctx_s_id )]
+ This system should be used recursively, that is, parent dynamic context will be
+ appended to all children down the tree.
+ """
+ _name = 'document.directory.dctx'
+ _description = 'Directory dynamic context'
+ _columns = {
+ 'dir_id': fields.many2one('document.directory', 'Directory', required=True),
+ 'field': fields.char('Field', size=20, required=True, select=1, help="The name of the field. Note that the prefix \"dctx_\" will be prepended to what is typed here."),
+ 'expr': fields.char('Expression', size=64, required=True, help="A python expression used to evaluate the field.\n" + \
+ "You can use 'dir_id' for current dir, 'res_id', 'res_model' as a reference to the current record, in dynamic folders"),
+ }
+
+document_directory_dctx()
+
+
+class document_directory_node(osv.osv):
+ _inherit = 'process.node'
+ _columns = {
+ 'directory_id': fields.many2one('document.directory', 'Document directory', ondelete="set null"),
+ }
+document_directory_node()
diff --git a/addons/document/document_storage.py b/addons/document/document_storage.py
new file mode 100644
index 00000000000..d4096138800
--- /dev/null
+++ b/addons/document/document_storage.py
@@ -0,0 +1,281 @@
+# -*- encoding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+#
+# Copyright (C) P. Christeas, 2009, all rights reserved
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+from osv import osv, fields
+import os
+import tools
+import base64
+from tools.misc import ustr
+
+from osv.orm import except_orm
+
+import random
+import string
+import netsvc
+from content_index import cntIndex
+
+
+""" The algorithm of data storage
+
+We have to consider 3 cases of data /retrieval/:
+ Given (context,path) we need to access the file (aka. node).
+ given (directory, context), we need one of its children (for listings, views)
+ given (ir.attachment, context), we needs its data and metadata (node).
+
+For data /storage/ we have the cases:
+ Have (ir.attachment, context), we modify the file (save, update, rename etc).
+ Have (directory, context), we create a file.
+ Have (path, context), we create or modify a file.
+
+Note that in all above cases, we don't explicitly choose the storage media,
+but always require a context to be present.
+
+Note that a node will not always have a corresponding ir.attachment. Dynamic
+nodes, for once, won't. Their metadata will be computed by the parent storage
+media + directory.
+
+The algorithm says that in any of the above cases, our first goal is to locate
+the node for any combination of search criteria. It would be wise NOT to
+represent each node in the path (like node[/] + node[/dir1] + node[/dir1/dir2])
+but directly jump to the end node (like node[/dir1/dir2]) whenever possible.
+
+We also contain all the parenting loop code in one function. This is intentional,
+because one day this will be optimized in the db (Pg 8.4).
+
+
+"""
+
+def random_name():
+ random.seed()
+ d = [random.choice(string.ascii_letters) for x in xrange(10) ]
+ name = "".join(d)
+ return name
+
+INVALID_CHARS={'*':str(hash('*')), '|':str(hash('|')) , "\\":str(hash("\\")), '/':'__', ':':str(hash(':')), '"':str(hash('"')), '<':str(hash('<')) , '>':str(hash('>')) , '?':str(hash('?'))}
+
+
+def create_directory(path):
+ dir_name = random_name()
+ path = os.path.join(path,dir_name)
+ os.makedirs(path)
+ return dir_name
+
+
+class document_storage(osv.osv):
+ """ The primary object for data storage.
+ Each instance of this object is a storage media, in which our application
+ can store contents. The object here controls the behaviour of the storage
+ media.
+ The referring document.directory-ies will control the placement of data
+ into the storage.
+
+ It is a bad idea to have multiple document.storage objects pointing to
+ the same tree of filesystem storage.
+ """
+ _name = 'document.storage'
+ _description = 'Document storage media'
+ _columns = {
+ 'name': fields.char('Name', size=64, required=True, select=1),
+ 'write_date': fields.datetime('Date Modified', readonly=True),
+ 'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
+ 'create_date': fields.datetime('Date Created', readonly=True),
+ 'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
+ 'user_id': fields.many2one('res.users', 'Owner'),
+ 'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
+ 'dir_ids': fields.one2many('document.directory', 'parent_id', 'Directories'),
+ 'type': fields.selection([('db','Database'),('filestore','Internal File storage'),
+ ('realstore','External file storage'), ('virtual','Virtual storage')], 'Type', required=True),
+ 'path': fields.char('Path',size=250,select=1,help="For file storage, the root path of the storage"),
+ 'online': fields.boolean('Online',help="If not checked, media is currently offline and its contents not available", required=True),
+ 'readonly': fields.boolean('Read Only', help="If set, media is for reading only"),
+ }
+
+ def _get_rootpath(self,cr,uid,context=None):
+ from tools import config
+ return os.path.join(tools.config['root_path'], 'filestore', cr.dbname)
+
+ _defaults = {
+ 'user_id': lambda self,cr,uid,ctx: uid,
+ 'online': lambda *args: True,
+ 'readonly': lambda *args: False,
+ # Note: the defaults below should only be used ONCE for the default
+ # storage media. All other times, we should create different paths at least.
+ 'type': lambda *args: 'filestore',
+ 'path': _get_rootpath,
+ }
+ _sql_constraints = [
+ # SQL note: a path = NULL doesn't have to be unique.
+ ('path_uniq', 'UNIQUE(type,path)', "The storage path must be unique!")
+ ]
+
+ def get_data(self, cr,uid, id, file_node,context = None, fil_obj = None):
+ """ retrieve the contents of some file_node having storage_id = id
+ optionally, fil_obj could point to the browse object of the file
+ (ir.attachment)
+ """
+ if not context:
+ context = {}
+ boo = self.browse(cr,uid,id,context)
+ if fil_obj:
+ ira = fil_obj
+ else:
+ ira = self.pool.get('ir.attachment').browse(cr,uid, file_node.file_id, context=context)
+ return self.__get_data_3(cr,uid,boo, ira, context)
+
+ def __get_data_3(self,cr,uid, boo, ira, context):
+ if not boo.online:
+ raise RuntimeError('media offline')
+ if boo.type == 'filestore':
+ if not ira.store_fname:
+ # On a migrated db, some files may have the wrong storage type
+ # try to fix their directory.
+ if ira.file_size:
+ netsvc.Logger().notifyChannel('document',netsvc.LOG_WARNING,"ir.attachment #%d does not have a filename, but is at filestore, fix it!" %ira.id)
+ return None
+ fpath = os.path.join(boo.path,ira.store_fname)
+ print "Trying to read \"%s\".."% fpath
+ return file(fpath,'rb').read()
+ elif boo.type == 'db':
+ # TODO: we need a better api for large files
+ if ira.db_datas:
+ out=base64.decodestring(ira.db_datas)
+ else:
+ out=''
+ return out
+ elif boo.type == 'realstore':
+ # fpath = os.path.join(boo.path,
+ return None
+ else:
+ raise TypeError("No %s storage" % boo.type)
+
+ def set_data(self, cr,uid, id, file_node, data, context = None, fil_obj = None):
+ """ store the data.
+ This function MUST be used from an ir.attachment. It wouldn't make sense
+ to store things persistently for other types (dynamic).
+ """
+ if not context:
+ context = {}
+ boo = self.browse(cr,uid,id,context)
+ logger = netsvc.Logger()
+ if fil_obj:
+ ira = fil_obj
+ else:
+ ira = self.pool.get('ir.attachment').browse(cr,uid, file_node.file_id, context=context)
+
+ if not boo.online:
+ raise RuntimeError('media offline')
+ logger.notifyChannel('document',netsvc.LOG_DEBUG,"Store data for ir.attachment #%d" %ira.id)
+ store_fname = None
+ fname = None
+ if boo.type == 'filestore':
+ path = boo.path
+ try:
+ flag = None
+ # This can be improved
+ for dirs in os.listdir(path):
+ if os.path.isdir(os.path.join(path,dirs)) and len(os.listdir(os.path.join(path,dirs)))<4000:
+ flag = dirs
+ break
+ flag = flag or create_directory(path)
+ filename = random_name()
+ fname = os.path.join(path, flag, filename)
+ fp = file(fname,'wb')
+ fp.write(data)
+ fp.close()
+ logger.notifyChannel('document',netsvc.LOG_DEBUG,"Saved data to %s" % fname)
+ filesize = len(data) # os.stat(fname).st_size
+ store_fname = os.path.join(flag,filename)
+
+ # TODO Here, an old file would be left hanging.
+
+ except Exception,e :
+ netsvc.Logger().notifyChannel('document',netsvc.LOG_WARNING,"Couldn't save data: %s" %str(e))
+ raise except_orm(_('Error!'), str(e))
+ elif boo.type == 'db':
+ filesize = len(data)
+ # will that work for huge data? TODO
+ out=base64.encodestring(data)
+ cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
+ ( out, file_node.file_id ))
+ else:
+ raise TypeError("No %s storage" % boo.type)
+
+ # 2nd phase: store the metadata
+ try:
+ icont = ''
+ mime = ira.file_type
+ try:
+ mime,icont = cntIndex.doIndex(data, ira.datas_fname,
+ ira.file_type or None,fname)
+ except Exception,e:
+ logger.notifyChannel('document', netsvc.LOG_DEBUG, 'Cannot index file: %s' % str(e))
+ pass
+
+ # a hack: /assume/ that the calling write operation will not try
+ # to write the fname and size, and update them in the db concurrently.
+ # We cannot use a write() here, because we are already in one.
+ cr.execute('UPDATE ir_attachment SET store_fname = %s, file_size = %s, index_content = %s, file_type = %s WHERE id = %s',
+ (store_fname, filesize, ustr(icont), mime, file_node.file_id ))
+ file_node.content_length = filesize
+ file_node.content_type = mime
+ return True
+ except Exception,e :
+ netsvc.Logger().notifyChannel('document',netsvc.LOG_WARNING,"Couldn't save data: %s" %str(e))
+ # should we really rollback once we have written the actual data?
+ # at the db case (only), that rollback would be safe
+ raise except_orm(_('Error at doc write!'), str(e))
+
+ def prepare_unlink(self,cr,uid,storage_bo, fil_bo):
+ """ Before we unlink a file (fil_boo), prepare the list of real
+ files that have to be removed, too. """
+
+ if not storage_bo.online:
+ raise RuntimeError('media offline')
+
+ if storage_bo.type == 'filestore':
+ fname = fil_bo.store_fname
+ if not fname:
+ return None
+ path = storage_bo.path
+ return ( storage_bo.id, 'file', os.path.join(path,fname))
+ elif storage_bo.type == 'db':
+ return None
+ else:
+ raise TypeError("No %s storage" % boo.type)
+
+ def do_unlink(self, cr,uid,unres):
+ for id, ktype, fname in unres:
+ if ktype == 'file':
+ try:
+ os.unlink(fname)
+ except Exception,e:
+ netsvc.Logger().notifyChannel('document',netsvc.LOG_WARNING,"Could not remove file %s, please remove manually." % fname)
+ else:
+ netsvc.Logger().notifyChannel('document',netsvc.LOG_WARNING,"Unknown unlink key %s" % ktype)
+
+ return True
+
+
+document_storage()
+
+
+#eof
diff --git a/addons/document/document_view.xml b/addons/document/document_view.xml
index 6e570bb406a..babc5af33b3 100644
--- a/addons/document/document_view.xml
+++ b/addons/document/document_view.xml
@@ -3,6 +3,48 @@
+
+ document.storage
+ document.storage
+ form
+
+
+
+
+
+
+ document.storage
+ document.storage
+ tree
+
+
+
+
+
+
+
+
+
+
+
+ ir.actions.act_window
+ document.storage
+ form
+ tree,form
+
+
+
document.directorydocument.directory
@@ -12,6 +54,7 @@
+
@@ -20,14 +63,17 @@
+
-
-
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
@@ -151,7 +210,7 @@
-
+
@@ -243,86 +302,45 @@
-
-
- Auto Configure Directory
- document.configuration.wizard
- form
-
-
-
-
+
+ Auto Configure Directory
+ document.configuration.wizard
+ form
+
+
+
+
-
- Auto Configure Directory
- ir.actions.act_window
- document.configuration.wizard
- form
- form
- new
-
+
+ Auto Configure Directory
+ ir.actions.act_window
+ document.configuration.wizard
+ form
+ form
+ new
+
-
- Auto Configure Directory
- This wizard will configure the URL of the server of the document management system. These ftp server address and port will be stored in the configuration file.
-
-
+
+ Auto Configure Directory
+ This wizard will configure the URL of the server of the document management system.
+
+
-
-
-
- Configure FTP server address
- document.configuration.ftp_server.wizard
- form
-
-
-
-
-
-
- Configure FTP server address
- ir.actions.act_window
- document.configuration.ftp_server.wizard
- form
- form
- new
-
-
-
+
diff --git a/addons/document/ftpserver/__init__.py b/addons/document/ftpserver/__init__.py
index 47615a442a6..3dc1f1b1202 100644
--- a/addons/document/ftpserver/__init__.py
+++ b/addons/document/ftpserver/__init__.py
@@ -28,7 +28,7 @@ import netsvc
from tools import config
from tools.misc import detect_ip_addr
HOST = ''
-PORT = int(config.get('ftp_server_port', 8021))
+PORT = 8021
PASSIVE_PORTS = None
pps = config.get('ftp_server_passive_ports', '').split(':')
if len(pps) == 2:
@@ -53,8 +53,7 @@ class ftp_server(threading.Thread):
ftpserver.logerror = lambda msg: self.log(netsvc.LOG_ERROR, msg)
HOST = config.get('ftp_server_address', detect_ip_addr())
- PORT = int(config.get('ftp_server_port', '8021'))
-
+ PORT = int(config.get('ftp_server_port', '8021'))
address = (HOST, PORT)
ftpd = ftpserver.FTPServer(address, ftpserver.FTPHandler)
ftpd.serve_forever()
diff --git a/addons/document/ftpserver/abstracted_fs.py b/addons/document/ftpserver/abstracted_fs.py
index 21b38d7dc4f..d795110d4ea 100644
--- a/addons/document/ftpserver/abstracted_fs.py
+++ b/addons/document/ftpserver/abstracted_fs.py
@@ -1,24 +1,4 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
+# -*- encoding: utf-8 -*-
import os
import time
from tarfile import filemode
@@ -141,8 +121,8 @@ class abstracted_fs:
# Ok
def db_list(self):
#return pooler.pool_dic.keys()
- s = netsvc.LocalService('db')
- result = s.list()
+ s = netsvc.ExportService.getService('db')
+ result = s.exp_list()
self.db_name_list = []
for db_name in result:
db, cr = None, None
@@ -222,13 +202,18 @@ class abstracted_fs:
# Ok
def fs2ftp(self, node):
- res='/'
+ res = '/'
if node:
- res=os.path.normpath(node.path)
+ path = node.path
+ print '**********', path
+ if isinstance(path,list):
+ path = '/'.join(path)
+ res = os.path.normpath(path)
res = res.replace("\\", "/")
while res[:2] == '//':
res = res[1:]
- res='/' + node.cr.dbname + '/' + _to_decode(res)
+ if len(node.path):
+ res = '/' + node.path[0] + '/' + _to_decode(res)
#res = node and ('/' + node.cr.dbname + '/' + _to_decode(self.ftpnorm(node.path))) or '/'
return res
diff --git a/addons/document/ftpserver/authorizer.py b/addons/document/ftpserver/authorizer.py
index afa36680009..5a64ca8f7a0 100644
--- a/addons/document/ftpserver/authorizer.py
+++ b/addons/document/ftpserver/authorizer.py
@@ -1,26 +1,4 @@
-# -*- coding: utf-8 -*-
-##############################################################################
-#
-# OpenERP, Open Source Management Solution
-# Copyright (C) 2004-2009 Tiny SPRL ().
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see .
-#
-##############################################################################
-
-import pooler
-from service import security
+# -*- encoding: utf-8 -*-
class authorizer:
read_perms = "elr"
@@ -69,7 +47,7 @@ class authorizer:
paths = path.split('/')
if not len(paths)>2:
return True
- db_name = paths[1]
+ db_name = paths[1]
db,pool = pooler.get_db_and_pool(db_name)
res = security.login(db_name, username, self.password)
return bool(res)
diff --git a/addons/document/ftpserver/ftpserver.py b/addons/document/ftpserver/ftpserver.py
index 3414fa78bbd..13c44287303 100644
--- a/addons/document/ftpserver/ftpserver.py
+++ b/addons/document/ftpserver/ftpserver.py
@@ -1,10 +1,12 @@
-# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
# ftpserver.py
#
# pyftpdlib is released under the MIT license, reproduced below:
# ======================================================================
# Copyright (C) 2007 Giampaolo Rodola'
# Hacked by Fabien Pinckaers (C) 2008
+#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
@@ -56,9 +58,6 @@ the backend functionality for the FTPd:
providing a high level, cross-platform interface compatible
with both Windows and UNIX style filesystems.
- [CallLater] - calls a function at a later time whithin the polling
- loop asynchronously.
-
[AuthorizerError] - base class for authorizers exceptions.
@@ -117,11 +116,11 @@ import traceback
import errno
import time
import glob
+import fnmatch
import tempfile
import warnings
import random
import stat
-import heapq
from tarfile import filemode
try:
@@ -131,80 +130,67 @@ except ImportError:
pwd = grp = None
+LOG_ACTIVE = True
+
__all__ = ['proto_cmds', 'Error', 'log', 'logline', 'logerror', 'DummyAuthorizer',
- 'AuthorizerError', 'FTPHandler', 'FTPServer', 'PassiveDTP',
- 'ActiveDTP', 'DTPHandler', 'FileProducer', 'BufferedIteratorProducer',
- 'AbstractedFS', 'CallLater']
+ 'FTPHandler', 'FTPServer', 'PassiveDTP', 'ActiveDTP', 'DTPHandler',
+ 'FileProducer', 'IteratorProducer', 'BufferedIteratorProducer',
+ 'AbstractedFS',]
__pname__ = 'Python FTP server library (pyftpdlib)'
-__ver__ = '0.5.1'
-__date__ = '2009-01-21'
+__ver__ = '0.4.0'
+__date__ = '2008-05-16'
__author__ = "Giampaolo Rodola' "
__web__ = 'http://code.google.com/p/pyftpdlib/'
proto_cmds = {
- # cmd : (perm, auth, arg, path, help)
- 'ABOR': (None, True, False, False, 'Syntax: ABOR (abort transfer).'),
- 'ALLO': (None, True, True, False, 'Syntax: ALLO bytes (noop; allocate storage).'),
- 'APPE': ('a', True, True, True, 'Syntax: APPE file-name (append data to an existent file).'),
- 'CDUP': ('e', True, False, True, 'Syntax: CDUP (go to parent directory).'),
- 'CWD' : ('e', True, None, True, 'Syntax: CWD [ dir-name] (change current working directory).'),
- 'DELE': ('d', True, True, True, 'Syntax: DELE file-name (delete file).'),
- 'EPRT': (None, True, True, False, 'Syntax: EPRT |proto|ip|port| (set server in extended active mode).'),
- 'EPSV': (None, True, None, False, 'Syntax: EPSV [ proto/"ALL"] (set server in extended passive mode).'),
- 'FEAT': (None, False, False, False, 'Syntax: FEAT (list all new features supported).'),
- 'HELP': (None, False, None, False, 'Syntax: HELP [ cmd] (show help).'),
- 'LIST': ('l', True, None, True, 'Syntax: LIST [ path-name] (list files).'),
- 'MDTM': (None, True, True, True, 'Syntax: MDTM [ file-name] (get last modification time).'),
- 'MLSD': ('l', True, None, True, 'Syntax: MLSD [ dir-name] (list files in a machine-processable form)'),
- 'MLST': (None, True, None, True, 'Syntax: MLST [ path-name] (show a path in a machine-processable form)'),
- 'MODE': (None, True, True, False, 'Syntax: MODE mode (noop; set data transfer mode).'),
- 'MKD' : ('m', True, True, True, 'Syntax: MDK dir-name (create directory).'),
- 'NLST': ('l', True, None, True, 'Syntax: NLST [ path-name] (list files in a compact form).'),
- 'NOOP': (None, False, False, False, 'Syntax: NOOP (just do nothing).'),
- 'OPTS': (None, True, True, False, 'Syntax: OPTS ftp-command [ option] (specify options for FTP commands)'),
- 'PASS': (None, False, True, False, 'Syntax: PASS user-name (set user password).'),
- 'PASV': (None, True, False, False, 'Syntax: PASV (set server in passive mode).'),
- 'PORT': (None, True, True, False, 'Syntax: PORT h1,h2,h3,h4,p1,p2 (set server in active mode).'),
- 'PWD' : (None, True, False, False, 'Syntax: PWD (get current working directory).'),
- 'QUIT': (None, False, False, False, 'Syntax: QUIT (quit current session).'),
- 'REIN': (None, True, False, False, 'Syntax: REIN (reinitialize / flush account).'),
- 'REST': (None, True, True, False, 'Syntax: REST marker (restart file position).'),
- 'RETR': ('r', True, True, True, 'Syntax: RETR file-name (retrieve a file).'),
- 'RMD' : ('d', True, True, True, 'Syntax: RMD dir-name (remove directory).'),
- 'RNFR': ('f', True, True, True, 'Syntax: RNFR file-name (file renaming (source name)).'),
- 'RNTO': (None, True, True, True, 'Syntax: RNTO file-name (file renaming (destination name)).'),
- 'SITE': (None, False, True, False, 'Syntax: SITE site-command (execute the specified SITE command).'),
- 'SITE HELP' : (None, False, None, False, 'Syntax: SITE HELP [ site-command] (show SITE command help).'),
- 'SIZE': (None, True, True, True, 'Syntax: HELP file-name (get file size).'),
- 'STAT': ('l', False, None, True, 'Syntax: STAT [ path name] (status information [list files]).'),
- 'STOR': ('w', True, True, True, 'Syntax: STOR file-name (store a file).'),
- 'STOU': ('w', True, None, True, 'Syntax: STOU [ file-name] (store a file with a unique name).'),
- 'STRU': (None, True, True, False, 'Syntax: STRU type (noop; set file structure).'),
- 'SYST': (None, False, False, False, 'Syntax: SYST (get operating system type).'),
- 'TYPE': (None, True, True, False, 'Syntax: TYPE [A | I] (set transfer type).'),
- 'USER': (None, False, True, False, 'Syntax: USER user-name (set username).'),
- 'XCUP': ('e', True, False, True, 'Syntax: XCUP (obsolete; go to parent directory).'),
- 'XCWD': ('e', True, None, True, 'Syntax: XCWD [ dir-name] (obsolete; change current directory).'),
- 'XMKD': ('m', True, True, True, 'Syntax: XMDK dir-name (obsolete; create directory).'),
- 'XPWD': (None, True, False, False, 'Syntax: XPWD (obsolete; get current dir).'),
- 'XRMD': ('d', True, True, True, 'Syntax: XRMD dir-name (obsolete; remove directory).'),
+ 'ABOR': 'Syntax: ABOR (abort transfer).',
+ 'ALLO': 'Syntax: ALLO bytes (obsolete; allocate storage).',
+ 'APPE': 'Syntax: APPE file-name (append data to an existent file).',
+ 'CDUP': 'Syntax: CDUP (go to parent directory).',
+ 'CWD' : 'Syntax: CWD dir-name (change current working directory).',
+ 'DELE': 'Syntax: DELE file-name (delete file).',
+ 'EPRT': 'Syntax: EPRT |proto|ip|port| (set server in extended active mode).',
+ 'EPSV': 'Syntax: EPSV [ proto/"ALL"] (set server in extended passive mode).',
+ 'FEAT': 'Syntax: FEAT (list all new features supported).',
+ 'HELP': 'Syntax: HELP [ cmd] (show help).',
+ 'LIST': 'Syntax: LIST [ path-name] (list files).',
+ 'MDTM': 'Syntax: MDTM file-name (get last modification time).',
+ 'MLSD': 'Syntax: MLSD [ dir-name] (list files in a machine-processable form)',
+ 'MLST': 'Syntax: MLST [ path-name] (show a path in a machine-processable form)',
+ 'MODE': 'Syntax: MODE mode (obsolete; set data transfer mode).',
+ 'MKD' : 'Syntax: MDK dir-name (create directory).',
+ 'NLST': 'Syntax: NLST [ path-name] (list files in a compact form).',
+ 'NOOP': 'Syntax: NOOP (just do nothing).',
+ 'OPTS': 'Syntax: OPTS ftp-command [ option] (specify options for FTP commands)',
+ 'PASS': 'Syntax: PASS user-name (set user password).',
+ 'PASV': 'Syntax: PASV (set server in passive mode).',
+ 'PORT': 'Syntax: PORT h1,h2,h3,h4,p1,p2 (set server in active mode).',
+ 'PWD' : 'Syntax: PWD (get current working directory).',
+ 'QUIT': 'Syntax: QUIT (quit current session).',
+ 'REIN': 'Syntax: REIN (reinitialize / flush account).',
+ 'REST': 'Syntax: REST marker (restart file position).',
+ 'RETR': 'Syntax: RETR file-name (retrieve a file).',
+ 'RMD' : 'Syntax: RMD dir-name (remove directory).',
+ 'RNFR': 'Syntax: RNFR file-name (file renaming (source name)).',
+ 'RNTO': 'Syntax: RNTO file-name (file renaming (destination name)).',
+ 'SIZE': 'Syntax: HELP file-name (get file size).',
+ 'STAT': 'Syntax: STAT [ path name] (status information [list files]).',
+ 'STOR': 'Syntax: STOR file-name (store a file).',
+ 'STOU': 'Syntax: STOU [ file-name] (store a file with a unique name).',
+ 'STRU': 'Syntax: STRU type (obsolete; set file structure).',
+ 'SYST': 'Syntax: SYST (get operating system type).',
+ 'TYPE': 'Syntax: TYPE [A | I] (set transfer type).',
+ 'USER': 'Syntax: USER user-name (set username).',
+ 'XCUP': 'Syntax: XCUP (obsolete; go to parent directory).',
+ 'XCWD': 'Syntax: XCWD dir-name (obsolete; change current directory).',
+ 'XMKD': 'Syntax: XMDK dir-name (obsolete; create directory).',
+ 'XPWD': 'Syntax: XPWD (obsolete; get current dir).',
+ 'XRMD': 'Syntax: XRMD dir-name (obsolete; remove directory).',
}
-class _CommandProperty:
- def __init__(self, perm, auth_needed, arg_needed, check_path, help):
- self.perm = perm
- self.auth_needed = auth_needed
- self.arg_needed = arg_needed
- self.check_path = check_path
- self.help = help
-
-for cmd, properties in proto_cmds.iteritems():
- proto_cmds[cmd] = _CommandProperty(*properties)
-del cmd, properties
-
# hack around format_exc function of traceback module to grant
# backward compatibility with python < 2.4
@@ -235,97 +221,6 @@ def _strerror(err):
else:
return err.strerror
-# the heap used for the scheduled tasks
-_tasks = []
-
-def _scheduler():
- """Run the scheduled functions due to expire soonest (if any)."""
- now = time.time()
- while _tasks and now >= _tasks[0].timeout:
- call = heapq.heappop(_tasks)
- if call.repush:
- heapq.heappush(_tasks, call)
- call.repush = False
- continue
- try:
- call.call()
- finally:
- if not call.cancelled:
- call.cancel()
-
-
-class CallLater:
- """Calls a function at a later time.
-
- It can be used to asynchronously schedule a call within the polling
- loop without blocking it. The instance returned is an object that
- can be used to cancel or reschedule the call.
- """
-
- def __init__(self, seconds, target, *args, **kwargs):
- """
- - (int) seconds: the number of seconds to wait
- - (obj) target: the callable object to call later
- - args: the arguments to call it with
- - kwargs: the keyword arguments to call it with
- """
- assert callable(target), "%s is not callable" %target
- assert sys.maxint >= seconds >= 0, "%s is not greater than or equal " \
- "to 0 seconds" % (seconds)
- self.__delay = seconds
- self.__target = target
- self.__args = args
- self.__kwargs = kwargs
- # seconds from the epoch at which to call the function
- self.timeout = time.time() + self.__delay
- self.repush = False
- self.cancelled = False
- heapq.heappush(_tasks, self)
-
- def __le__(self, other):
- return self.timeout <= other.timeout
-
- def call(self):
- """Call this scheduled function."""
- assert not self.cancelled, "Already cancelled"
- self.__target(*self.__args, **self.__kwargs)
-
- def reset(self):
- """Reschedule this call resetting the current countdown."""
- assert not self.cancelled, "Already cancelled"
- self.timeout = time.time() + self.__delay
- self.repush = True
-
- def delay(self, seconds):
- """Reschedule this call for a later time."""
- assert not self.cancelled, "Already cancelled."
- assert sys.maxint >= seconds >= 0, "%s is not greater than or equal " \
- "to 0 seconds" %(seconds)
- self.__delay = seconds
- newtime = time.time() + self.__delay
- if newtime > self.timeout:
- self.timeout = newtime
- self.repush = True
- else:
- # XXX - slow, can be improved
- self.timeout = newtime
- heapq.heapify(_tasks)
-
- def cancel(self):
- """Unschedule this call."""
- assert not self.cancelled, "Already cancelled"
- self.cancelled = True
- del self.__target, self.__args, self.__kwargs
- if self in _tasks:
- pos = _tasks.index(self)
- if pos == 0:
- heapq.heappop(_tasks)
- elif pos == len(_tasks) - 1:
- _tasks.pop(pos)
- else:
- _tasks[pos] = _tasks.pop()
- heapq._siftup(_tasks, pos)
-
# --- library defined exceptions
@@ -340,11 +235,13 @@ class AuthorizerError(Error):
def log(msg):
"""Log messages intended for the end user."""
- print msg
+ if LOG_ACTIVE:
+ print msg
def logline(msg):
"""Log commands and responses passing through the command channel."""
- print msg
+ if LOG_ACTIVE:
+ print msg
def logerror(msg):
"""Log traceback outputs occurring in case of errors."""
@@ -403,14 +300,20 @@ class DummyAuthorizer:
"""
if self.has_user(username):
raise AuthorizerError('User "%s" already exists' %username)
+ homedir = os.path.realpath(homedir)
if not os.path.isdir(homedir):
raise AuthorizerError('No such directory: "%s"' %homedir)
- homedir = os.path.realpath(homedir)
- self._check_permissions(username, perm)
+ for p in perm:
+ if p not in 'elradfmw':
+ raise AuthorizerError('No such permission "%s"' %p)
+ for p in perm:
+ if (p in self.write_perms) and (username == 'anonymous'):
+ warnings.warn("write permissions assigned to anonymous user.",
+ RuntimeWarning)
+ break
dic = {'pwd': str(password),
'home': homedir,
'perm': perm,
- 'operms': {},
'msg_login': str(msg_login),
'msg_quit': str(msg_quit)
}
@@ -438,26 +341,9 @@ class DummyAuthorizer:
"""Remove a user from the virtual users table."""
del self.user_table[username]
- def override_perm(self, username, directory, perm, recursive=False):
- """Override permissions for a given directory."""
- self._check_permissions(username, perm)
- if not os.path.isdir(directory):
- raise AuthorizerError('No such directory: "%s"' %directory)
- directory = os.path.normcase(os.path.realpath(directory))
- home = os.path.normcase(self.get_home_dir(username))
- if directory == home:
- raise AuthorizerError("Can't override home directory permissions")
- if not self._issubpath(directory, home):
- raise AuthorizerError("Path escapes user home directory")
- self.user_table[username]['operms'][directory] = perm, recursive
-
def validate_authentication(self, username, password):
"""Return True if the supplied username and password match the
stored credentials."""
- if not self.has_user(username):
- return False
- if username == 'anonymous':
- return True
return self.user_table[username]['pwd'] == password
def impersonate_user(self, username, password):
@@ -489,19 +375,6 @@ class DummyAuthorizer:
Expected perm argument is one of the following letters:
"elradfmw".
"""
- if path is None:
- return perm in self.user_table[username]['perm']
-
- path = os.path.normcase(path)
- for dir in self.user_table[username]['operms'].keys():
- operm, recursive = self.user_table[username]['operms'][dir]
- if self._issubpath(path, dir):
- if recursive:
- return perm in operm
- if (path == dir) or (os.path.dirname(path) == dir \
- and not os.path.isdir(path)):
- return perm in operm
-
return perm in self.user_table[username]['perm']
def get_perms(self, username):
@@ -520,23 +393,6 @@ class DummyAuthorizer:
"""Return the user's quitting message."""
return self.user_table[username]['msg_quit']
- def _check_permissions(self, username, perm):
- warned = 0
- for p in perm:
- if p not in self.read_perms + self.write_perms:
- raise AuthorizerError('No such permission "%s"' %p)
- if (username == 'anonymous') and (p in self.write_perms) and not warned:
- warnings.warn("Write permissions assigned to anonymous user.",
- RuntimeWarning)
- warned = 1
-
- def _issubpath(self, a, b):
- """Return True if a is a sub-path of b or if the paths are equal."""
- p1 = a.rstrip(os.sep).split(os.sep)
- p2 = b.rstrip(os.sep).split(os.sep)
- return p1[:len(p2)] == p2
-
-
# --- DTP classes
@@ -544,11 +400,7 @@ class PassiveDTP(asyncore.dispatcher):
"""This class is an asyncore.disptacher subclass. It creates a
socket listening on a local port, dispatching the resultant
connection to DTPHandler.
-
- - (int) timeout: the timeout for a remote client to establish
- connection with the listening socket. Defaults to 30 seconds.
"""
- timeout = 30
def __init__(self, cmd_channel, extmode=False):
"""Initialize the passive data server.
@@ -558,17 +410,13 @@ class PassiveDTP(asyncore.dispatcher):
"""
asyncore.dispatcher.__init__(self)
self.cmd_channel = cmd_channel
- if self.timeout:
- self.idler = CallLater(self.timeout, self.handle_timeout)
- else:
- self.idler = None
ip = self.cmd_channel.getsockname()[0]
self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
- if self.cmd_channel.passive_ports is None:
- # By using 0 as port number value we let kernel choose a
- # free unprivileged random port.
+ if not self.cmd_channel.passive_ports:
+ # By using 0 as port number value we let kernel choose a free
+ # unprivileged random port.
self.bind((ip, 0))
else:
ports = list(self.cmd_channel.passive_ports)
@@ -613,12 +461,8 @@ class PassiveDTP(asyncore.dispatcher):
def handle_accept(self):
"""Called when remote client initiates a connection."""
- try:
- sock, addr = self.accept()
- except TypeError:
- # for some reason sometimes accept() returns None instead
- # of a socket
- return
+ sock, addr = self.accept()
+
# Check the origin of data connection. If not expressively
# configured we drop the incoming data connection if remote
# IP address does not match the client's IP address.
@@ -648,10 +492,6 @@ class PassiveDTP(asyncore.dispatcher):
self.cmd_channel.data_channel = handler
self.cmd_channel.on_dtp_connection()
- def handle_timeout(self):
- self.cmd_channel.respond("421 Passive data channel timed out.")
- self.close()
-
def writable(self):
return 0
@@ -664,21 +504,16 @@ class PassiveDTP(asyncore.dispatcher):
logerror(traceback.format_exc())
self.close()
- def close(self):
- if self.idler is not None and not self.idler.cancelled:
- self.idler.cancel()
- asyncore.dispatcher.close(self)
+ def handle_close(self):
+ """Called on closing the data connection."""
+ self.close()
class ActiveDTP(asyncore.dispatcher):
"""This class is an asyncore.disptacher subclass. It creates a
socket resulting from the connection to a remote user-port,
dispatching it to DTPHandler.
-
- - (int) timeout: the timeout for us to establish connection with
- the client's listening data socket.
"""
- timeout = 30
def __init__(self, ip, port, cmd_channel):
"""Initialize the active data channel attemping to connect
@@ -690,10 +525,6 @@ class ActiveDTP(asyncore.dispatcher):
"""
asyncore.dispatcher.__init__(self)
self.cmd_channel = cmd_channel
- if self.timeout:
- self.idler = CallLater(self.timeout, self.handle_timeout)
- else:
- self.idler = None
self.create_socket(self.cmd_channel.af, socket.SOCK_STREAM)
try:
self.connect((ip, port))
@@ -703,19 +534,11 @@ class ActiveDTP(asyncore.dispatcher):
# --- connection / overridden
- # NOOP, overridden to prevent unhandled read/write event
- # messages to be printed on Python < 2.6
-
def handle_write(self):
- pass
-
- def handle_read(self):
- pass
+ """NOOP, must be overridden to prevent unhandled write event."""
def handle_connect(self):
"""Called when connection is established."""
- if self.idler is not None and not self.idler.cancelled:
- self.idler.cancel()
self.cmd_channel.respond('200 Active data connection established.')
# delegate such connection to DTP handler
handler = self.cmd_channel.dtp_handler(self.socket, self.cmd_channel)
@@ -723,10 +546,6 @@ class ActiveDTP(asyncore.dispatcher):
self.cmd_channel.on_dtp_connection()
#self.close() # <-- (done automatically)
- def handle_timeout(self):
- self.cmd_channel.respond("421 Active data channel timed out.")
- self.close()
-
def handle_expt(self):
self.cmd_channel.respond("425 Can't connect to specified address.")
self.close()
@@ -744,11 +563,6 @@ class ActiveDTP(asyncore.dispatcher):
self.cmd_channel.respond("425 Can't connect to specified address.")
self.close()
- def close(self):
- if self.idler is not None and not self.idler.cancelled:
- self.idler.cancel()
- asyncore.dispatcher.close(self)
-
try:
from collections import deque
@@ -764,16 +578,15 @@ class DTPHandler(asyncore.dispatcher):
RFC-959) managing data-transfer operations involving sending
and receiving data.
- Class attributes:
+ Instance attributes defined in this class, initialized when
+ channel is opened:
- - (int) timeout: the timeout which roughly is the maximum time we
- permit data transfers to stall for with no progress. If the
- timeout triggers, the remote client will be kicked off
- (defaults 300).
-
- - (int) ac_in_buffer_size: incoming data buffer size (defaults 65536)
-
- - (int) ac_out_buffer_size: outgoing data buffer size (defaults 65536)
+ - (instance) cmd_channel: the command channel class instance.
+ - (file) file_obj: the file transferred (if any).
+ - (bool) receive: True if channel is used for receiving data.
+ - (bool) transfer_finished: True if transfer completed successfully.
+ - (int) tot_bytes_sent: the total bytes sent.
+ - (int) tot_bytes_received: the total bytes received.
DTPHandler implementation note:
@@ -795,9 +608,8 @@ class DTPHandler(asyncore.dispatcher):
available we use a list instead.
"""
- timeout = 300
- ac_in_buffer_size = 65536
- ac_out_buffer_size = 65536
+ ac_in_buffer_size = 8192
+ ac_out_buffer_size = 8192
def __init__(self, sock_obj, cmd_channel):
"""Initialize the command channel.
@@ -819,12 +631,6 @@ class DTPHandler(asyncore.dispatcher):
self.tot_bytes_sent = 0
self.tot_bytes_received = 0
self.data_wrapper = lambda x: x
- self._lastdata = 0
- self._closed = False
- if self.timeout:
- self.idler = CallLater(self.timeout, self.handle_timeout)
- else:
- self.idler = None
# --- utility methods
@@ -836,10 +642,7 @@ class DTPHandler(asyncore.dispatcher):
- (str) type: current transfer type, 'a' (ASCII) or 'i' (binary).
"""
if type == 'a':
- if os.linesep == '\r\n':
- self.data_wrapper = lambda x: x
- else:
- self.data_wrapper = lambda x: x.replace('\r\n', os.linesep)
+ self.data_wrapper = lambda x: x.replace('\r\n', os.linesep)
elif type == 'i':
self.data_wrapper = lambda x: x
else:
@@ -945,20 +748,6 @@ class DTPHandler(asyncore.dispatcher):
# we tried to send some actual data
return
- def handle_timeout(self):
- """Called cyclically to check if data trasfer is stalling with
- no progress in which case the client is kicked off.
- """
- if self.get_transmitted_bytes() > self._lastdata:
- self._lastdata = self.get_transmitted_bytes()
- self.idler = CallLater(self.timeout, self.handle_timeout)
- else:
- msg = "Data connection timed out."
- self.cmd_channel.log(msg)
- self.cmd_channel.respond("421 " + msg)
- self.cmd_channel.close_when_done()
- self.close()
-
def handle_expt(self):
"""Called on "exceptional" data events."""
self.cmd_channel.respond("426 Connection error; transfer aborted.")
@@ -1016,19 +805,10 @@ class DTPHandler(asyncore.dispatcher):
def close(self):
"""Close the data channel, first attempting to close any remaining
file handles."""
- if not self._closed:
- self._closed = True
- if self.file_obj is not None and not self.file_obj.closed:
- self.file_obj.close()
- if self.idler is not None and not self.idler.cancelled:
- self.idler.cancel()
- asyncore.dispatcher.close(self)
- if self.file_obj is not None and self.transfer_finished:
- if self.receive:
- self.cmd_channel.on_file_received(self.file_obj.name)
- else:
- self.cmd_channel.on_file_sent(self.file_obj.name)
- self.cmd_channel.on_dtp_close()
+ if self.file_obj and not self.file_obj.closed:
+ self.file_obj.close()
+ asyncore.dispatcher.close(self)
+ self.cmd_channel.on_dtp_close()
# --- producers
@@ -1047,10 +827,7 @@ class FileProducer:
self.done = False
self.file = file
if type == 'a':
- if os.linesep == '\r\n':
- self.data_wrapper = lambda x: x
- else:
- self.data_wrapper = lambda x: x.replace(os.linesep, '\r\n')
+ self.data_wrapper = lambda x: x.replace(os.linesep, '\r\n')
elif type == 'i':
self.data_wrapper = lambda x: x
else:
@@ -1068,6 +845,22 @@ class FileProducer:
return data
+class IteratorProducer:
+ """Producer for iterator objects."""
+
+ def __init__(self, iterator):
+ self.iterator = iterator
+
+ def more(self):
+ """Attempt a chunk of data from iterator by calling its next()
+ method.
+ """
+ try:
+ return self.iterator.next()
+ except StopIteration:
+ return ''
+
+
class BufferedIteratorProducer:
"""Producer for iterator objects with buffer capabilities."""
# how many times iterator.next() will be called before
@@ -1250,9 +1043,9 @@ class AbstractedFS:
os.chdir(basedir)
self.cwd = self.fs2ftp(path)
- def mkdir(self, path):
+ def mkdir(self, path, basename):
"""Create the specified directory."""
- os.mkdir(path)
+ os.mkdir(os.path.join(path, basename))
def listdir(self, path):
"""List the content of a directory."""
@@ -1330,6 +1123,17 @@ class AbstractedFS:
exists = lexists # alias for backward compatibility with 0.2.0
+ def glob1(self, dirname, pattern):
+ """Return a list of files matching a dirname pattern
+ non-recursively.
+
+ Unlike glob.glob1 raises exception if os.listdir() fails.
+ """
+ names = self.listdir(dirname)
+ if pattern[0] != '.':
+ names = filter(lambda x: x[0] != '.', names)
+ return fnmatch.filter(names, pattern)
+
# --- Listing utilities
# note: the following operations are no more blocking
@@ -1348,6 +1152,28 @@ class AbstractedFS:
self.lstat(path) # raise exc in case of problems
return self.format_list(basedir, [filename])
+ def get_stat_dir(self, rawline):
+ """Return an iterator object that yields a list of files
+ matching a dirname pattern non-recursively in a form
+ suitable for STAT command.
+
+ - (str) rawline: the raw string passed by client as command
+ argument.
+ """
+ ftppath = self.ftpnorm(rawline)
+ if not glob.has_magic(ftppath):
+ return self.get_list_dir(self.ftp2fs(rawline))
+ else:
+ basedir, basename = os.path.split(ftppath)
+ if glob.has_magic(basedir):
+ return iter(['Directory recursion not supported.\r\n'])
+ else:
+ basedir = self.ftp2fs(basedir)
+ listing = self.glob1(basedir, basename)
+ if listing:
+ listing.sort()
+ return self.format_list(basedir, listing)
+
def format_list(self, basedir, listing, ignore_err=True):
"""Return an iterator object that yields the entries of given
directory emulating the "/bin/ls -lA" UNIX command output.
@@ -1397,15 +1223,14 @@ class AbstractedFS:
# bogus values for owner and group
uname = "owner"
gname = "group"
+ # stat.st_mtime could fail (-1) if last mtime is too old
+ # in which case we return the local time as last mtime
try:
mtime = time.strftime("%b %d %H:%M", time.localtime(st.st_mtime))
except ValueError:
- # It could be raised if last mtime happens to be too
- # old (prior to year 1900) in which case we return
- # the current time as last mtime.
mtime = time.strftime("%b %d %H:%M")
# if the file is a symlink, resolve it, e.g. "symlink -> realfile"
- if stat.S_ISLNK(st.st_mode) and hasattr(os, 'readlink'):
+ if stat.S_ISLNK(st.st_mode):
basename = basename + " -> " + os.readlink(file)
# formatting is matched with proftpd ls output
@@ -1476,9 +1301,8 @@ class AbstractedFS:
try:
modify = 'modify=%s;' %time.strftime("%Y%m%d%H%M%S",
time.localtime(st.st_mtime))
- # it could be raised if last mtime happens to be too old
- # (prior to year 1900)
except ValueError:
+ # stat.st_mtime could fail (-1) if last mtime is too old
modify = ""
if 'create' in facts:
# on Windows we can provide also the creation time
@@ -1519,11 +1343,6 @@ class FTPHandler(asynchat.async_chat):
reproduced below and can be modified before instantiating this
class.
- - (int) timeout:
- The timeout which is the maximum time a remote client may spend
- between FTP commands. If the timeout triggers, the remote client
- will be kicked off. Defaults to 300 seconds.
-
- (str) banner: the string sent when client connects.
- (int) max_login_attempts:
@@ -1579,7 +1398,6 @@ class FTPHandler(asynchat.async_chat):
abstracted_fs = AbstractedFS
# session attributes (explained in the docstring)
- timeout = 300
banner = "pyftpdlib %s ready." %__ver__
max_login_attempts = 3
permit_foreign_addresses = False
@@ -1594,17 +1412,17 @@ class FTPHandler(asynchat.async_chat):
established connection.
- (instance) server: the ftp server class instance.
"""
- asynchat.async_chat.__init__(self, conn)
- self.set_terminator("\r\n")
- # try to handle urgent data inline
try:
- self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_OOBINLINE, 1)
- except socket.error:
- pass
-
- # public session attributes
+ asynchat.async_chat.__init__(self, conn=conn) # python2.5
+ except TypeError, e:
+ asynchat.async_chat.__init__(self, sock=conn) # python2.6
self.server = server
self.remote_ip, self.remote_port = self.socket.getpeername()[:2]
+ self.in_buffer = []
+ self.in_buffer_len = 0
+ self.set_terminator("\r\n")
+
+ # session attributes
self.fs = self.abstracted_fs()
self.authenticated = False
self.username = ""
@@ -1613,13 +1431,24 @@ class FTPHandler(asynchat.async_chat):
self.current_type = 'a'
self.restart_position = 0
self.quit_pending = False
- self.sleeping = False
+ self._epsvall = False
+ self.__in_dtp_queue = None
+ self.__out_dtp_queue = None
+
+ # mlsx facts attributes
+ self.current_facts = ['type', 'perm', 'size', 'modify']
+ if os.name == 'posix':
+ self.current_facts.append('unique')
+ self.available_facts = self.current_facts[:]
+ if pwd and grp:
+ self.available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
+ if os.name == 'nt':
+ self.available_facts.append('create')
+
+ # dtp attributes
self.data_server = None
self.data_channel = None
- if self.timeout:
- self.idler = CallLater(self.timeout, self.handle_timeout)
- else:
- self.idler = None
+
if hasattr(self.socket, 'family'):
self.af = self.socket.family
else: # python < 2.5
@@ -1627,23 +1456,6 @@ class FTPHandler(asynchat.async_chat):
self.af = socket.getaddrinfo(ip, port, socket.AF_UNSPEC,
socket.SOCK_STREAM)[0][0]
- # private session attributes
- self._in_buffer = []
- self._in_buffer_len = 0
- self._epsvall = False
- self._in_dtp_queue = None
- self._out_dtp_queue = None
- self._closed = False
- self._extra_feats = []
- self._current_facts = ['type', 'perm', 'size', 'modify']
- if os.name == 'posix':
- self._current_facts.append('unique')
- self._available_facts = self._current_facts[:]
- if pwd and grp:
- self._available_facts += ['unix.mode', 'unix.uid', 'unix.gid']
- if os.name == 'nt':
- self._available_facts.append('create')
-
def handle(self):
"""Return a 220 'Ready' response to the client over the command
channel.
@@ -1675,33 +1487,37 @@ class FTPHandler(asynchat.async_chat):
self.log(msg)
self.close_when_done()
- def handle_timeout(self):
- """Called when client does not send any command within the time
- specified in attribute."""
- msg = "Control connection timed out."
- self.log(msg)
- self.respond("421 " + msg)
- self.close_when_done()
-
# --- asyncore / asynchat overridden methods
def readable(self):
# if there's a quit pending we stop reading data from socket
- return not self.sleeping
+ return not self.quit_pending
def collect_incoming_data(self, data):
"""Read incoming data and append to the input buffer."""
- self._in_buffer.append(data)
- self._in_buffer_len += len(data)
+ self.in_buffer.append(data)
+ self.in_buffer_len += len(data)
# Flush buffer if it gets too long (possible DoS attacks).
# RFC-959 specifies that a 500 response could be given in
# such cases
buflimit = 2048
- if self._in_buffer_len > buflimit:
+ if self.in_buffer_len > buflimit:
self.respond('500 Command too long.')
self.log('Command received exceeded buffer limit of %s.' %(buflimit))
- self._in_buffer = []
- self._in_buffer_len = 0
+ self.in_buffer = []
+ self.in_buffer_len = 0
+
+ # commands accepted before authentication
+ unauth_cmds = ('FEAT','HELP','NOOP','PASS','QUIT','STAT','SYST','USER')
+
+ # commands needing an argument
+ arg_cmds = ('ALLO','APPE','DELE','EPRT','MDTM','MODE','MKD','OPTS','PORT',
+ 'REST','RETR','RMD','RNFR','RNTO','SIZE', 'STOR','STRU',
+ 'TYPE','USER','XMKD','XRMD')
+
+ # commands needing no argument
+ unarg_cmds = ('ABOR','CDUP','FEAT','NOOP','PASV','PWD','QUIT','REIN',
+ 'SYST','XCUP','XPWD')
def found_terminator(self):
r"""Called when the incoming data stream matches the \r\n
@@ -1711,109 +1527,130 @@ class FTPHandler(asynchat.async_chat):
corresponding method (e.g. for received command "MKD pathname",
ftp_MKD() method is called with "pathname" as the argument).
"""
- if self.idler is not None and not self.idler.cancelled:
- self.idler.reset()
-
- line = ''.join(self._in_buffer)
- self._in_buffer = []
- self._in_buffer_len = 0
+ line = ''.join(self.in_buffer)
+ self.in_buffer = []
+ self.in_buffer_len = 0
cmd = line.split(' ')[0].upper()
- arg = line[len(cmd)+1:]
- if cmd == "SITE" and arg:
- cmd = "SITE %s" %arg.split(' ')[0].upper()
- arg = line[len(cmd)+1:]
+ space = line.find(' ')
+ if space != -1:
+ arg = line[space + 1:]
+ else:
+ arg = ""
if cmd != 'PASS':
self.logline("<== %s" %line)
else:
self.logline("<== %s %s" %(line.split(' ')[0], '*' * 6))
- # Recognize those commands having a "special semantic". They
- # should be sent by following the RFC-959 procedure of sending
- # Telnet IP/Synch sequence (chr 242 and 255) as OOB data but
- # since many ftp clients don't do it correctly we check the
- # last 4 characters only.
- if not cmd in proto_cmds:
- if cmd[-4:] in ('ABOR', 'STAT', 'QUIT'):
- cmd = cmd[-4:]
- else:
- self.respond('500 Command "%s" not understood.' %cmd)
- return
-
- if not arg and proto_cmds[cmd].arg_needed is True:
+ # let's check if user provided an argument for those commands
+ # needing one
+ if not arg and cmd in self.arg_cmds:
self.respond("501 Syntax error: command needs an argument.")
return
- if arg and proto_cmds[cmd].arg_needed is False:
+
+ # let's do the same for those commands requiring no argument.
+ elif arg and cmd in self.unarg_cmds:
self.respond("501 Syntax error: command does not accept arguments.")
return
- if not self.authenticated:
- if proto_cmds[cmd].auth_needed or (cmd == 'STAT' and arg):
+ # provide a limited set of commands if user isn't
+ # authenticated yet
+ if (not self.authenticated):
+ if cmd in self.unauth_cmds:
+ # we permit STAT during this phase but we don't want
+ # STAT to return a directory LISTing if the user is
+ # not authenticated yet (this could happen if STAT
+ # is used with an argument)
+ if (cmd == 'STAT') and arg:
+ self.respond("530 Log in with USER and PASS first.")
+ else:
+ method = getattr(self, 'ftp_' + cmd)
+ method(arg) # call the proper ftp_* method
+ elif cmd in proto_cmds:
self.respond("530 Log in with USER and PASS first.")
else:
- method = getattr(self, 'ftp_' + cmd.replace(' ', '_'))
- method(arg) # call the proper ftp_* method
- else:
- if cmd == 'STAT' and not arg:
- self.ftp_STAT('')
- return
+ self.respond('500 Command "%s" not understood.' %line)
- # for file-system related commands check whether real path
- # destination is valid
- if proto_cmds[cmd].check_path and cmd != 'STOU':
- if cmd in ('CWD', 'XCWD'):
- arg = arg or '/'
- elif cmd in ('CDUP', 'XCUP'):
- arg = '..'
- elif cmd == 'LIST':
- if arg.lower() in ('-a', '-l', '-al', '-la'):
- arg = self.fs.cwd
- else:
- arg = arg or self.fs.cwd
- elif cmd == 'STAT':
- if glob.has_magic(arg):
- self.respond('550 Globbing not supported.')
- return
- arg = arg or self.fs.cwd
- else: # LIST, NLST, MLSD, MLST
- arg = arg or self.fs.cwd
- if not self.fs.validpath(arg or '/'):
- line = self.fs.ftpnorm(arg)
+ # provide full command set
+ elif (self.authenticated) and (cmd in proto_cmds):
+ if not (self.__check_path(arg, arg)): # and self.__check_perm(cmd, arg)):
+ return
+ method = getattr(self, 'ftp_' + cmd)
+ method(arg) # call the proper ftp_* method
+
+ else:
+ # recognize those commands having "special semantics"
+ if 'ABOR' in cmd:
+ self.ftp_ABOR("")
+ elif 'STAT' in cmd:
+ self.ftp_STAT("")
+ # unknown command
+ else:
+ self.respond('500 Command "%s" not understood.' %line)
+
+ def __check_path(self, cmd, line):
+ """Check whether a path is valid."""
+ # For the following commands we have to make sure that the real
+ # path destination belongs to the user's root directory.
+ # If provided path is a symlink we follow its final destination
+ # to do so.
+ if cmd in ('APPE','CWD','DELE','MDTM','NLST','MLSD','MLST','RETR',
+ 'RMD','SIZE','STOR','XCWD','XRMD'):
+ datacr = None
+ datacr = self.fs.get_cr(line)
+ try:
+ if not self.fs.validpath(self.fs.ftp2fs(line, datacr)):
+ line = self.fs.ftpnorm(line)
err = '"%s" points to a path which is outside ' \
"the user's root directory" %line
self.respond("550 %s." %err)
- self.log('FAIL %s "%s". %s.' %(cmd, line, err))
- return
- # check permission
- perm = proto_cmds[cmd].perm
- if perm is not None and cmd != 'STOU':
- if not self.authorizer.has_perm(self.username, perm, arg or '/'):
- self.log('FAIL %s "%s". Not enough privileges.' \
- %(cmd, self.fs.fs2ftp(line)))
- self.respond("550 Can't %s. Not enough privileges." %cmd)
- return
- # call the proper ftp_* method
- method = getattr(self, 'ftp_' + cmd.replace(' ', '_'))
- method(arg)
+ self.log('FAIL %s "%s". %s.' %(cmd, line, err))
+ self.fs.close_cr(datacr)
+ return False
+ except:
+ pass
+ self.fs.close_cr(datacr)
+ return True
+
+ def __check_perm(self, cmd, line, datacr):
+ """Check permissions depending on issued command."""
+ map = {'CWD':'e', 'XCWD':'e', 'CDUP':'e', 'XCUP':'e',
+ 'LIST':'l', 'NLST':'l', 'MLSD':'l', 'STAT':'l',
+ 'RETR':'r',
+ 'APPE':'a',
+ 'DELE':'d', 'RMD':'d', 'XRMD':'d',
+ 'RNFR':'f',
+ 'MKD':'m', 'XMKD':'m',
+ 'STOR':'w'}
+ if cmd in map:
+ if cmd == 'STAT' and not line:
+ return True
+ perm = map[cmd]
+ if not line and (cmd in ('LIST','NLST','MLSD')):
+ path = self.fs.ftp2fs(self.fs.cwd, datacr)
+ else:
+ path = self.fs.ftp2fs(line, datacr)
+ if not self.authorizer.has_perm(self.username, perm, path):
+ self.log('FAIL %s "%s". Not enough privileges.' \
+ %(cmd, self.fs.ftpnorm(line)))
+ self.respond("550 Can't %s. Not enough privileges." %cmd)
+ return False
+ return True
def handle_expt(self):
- """Called when there is out of band (OOB) data to be read.
- This could happen in case of such clients strictly following
- the RFC-959 directives of sending Telnet IP and Synch as OOB
- data before issuing ABOR, STAT and QUIT commands.
- It should never be called since the SO_OOBINLINE option is
- enabled except on some systems like FreeBSD where it doesn't
- seem to have effect.
+ """Called when there is out of band (OOB) data for the socket
+ connection. This could happen in case of such commands needing
+ "special action" (typically STAT and ABOR) in which case we
+ append OOB data to incoming buffer.
"""
if hasattr(socket, 'MSG_OOB'):
try:
data = self.socket.recv(1024, socket.MSG_OOB)
- except socket.error, why:
- if why[0] == errno.EINVAL:
- return
+ except socket.error:
+ pass
else:
- self._in_buffer.append(data)
+ self.in_buffer.append(data)
return
self.log("Can't handle OOB data.")
self.close()
@@ -1838,23 +1675,21 @@ class FTPHandler(asynchat.async_chat):
def handle_close(self):
self.close()
+ _closed = False
def close(self):
"""Close the current channel disconnecting the client."""
if not self._closed:
self._closed = True
- if self.data_server is not None:
+ if self.data_server:
self.data_server.close()
del self.data_server
- if self.data_channel is not None:
+ if self.data_channel:
self.data_channel.close()
del self.data_channel
- del self._out_dtp_queue
- del self._in_dtp_queue
-
- if self.idler is not None and not self.idler.cancelled:
- self.idler.cancel()
+ del self.__out_dtp_queue
+ del self.__in_dtp_queue
# remove client IP address from ip map
self.server.ip_map.remove(self.remote_ip)
@@ -1863,16 +1698,6 @@ class FTPHandler(asynchat.async_chat):
# --- callbacks
- def on_file_sent(self, file):
- """Called every time a file has been succesfully sent.
- 'file' is the complete filename of the file being sent.
- """
-
- def on_file_received(self, file):
- """Called every time a file has been succesfully received.
- 'file' is the complete filename of the file being received.
- """
-
def on_dtp_connection(self):
"""Called every time data channel connects (either active or
passive).
@@ -1882,45 +1707,34 @@ class FTPHandler(asynchat.async_chat):
If awaiting inbound data, the data channel is enabled for
receiving.
"""
- if self.data_server is not None:
+ if self.data_server:
self.data_server.close()
self.data_server = None
- # stop the idle timer as long as the data transfer is not finished
- if self.idler is not None and not self.idler.cancelled:
- self.idler.cancel()
-
# check for data to send
- if self._out_dtp_queue is not None:
- data, isproducer, file = self._out_dtp_queue
- self._out_dtp_queue = None
+ if self.__out_dtp_queue:
+ data, isproducer, file = self.__out_dtp_queue
if file:
self.data_channel.file_obj = file
- try:
- if not isproducer:
- self.data_channel.push(data)
- else:
- self.data_channel.push_with_producer(data)
- if self.data_channel is not None:
- self.data_channel.close_when_done()
- except:
- # dealing with this exception is up to DTP (see bug #84)
- self.data_channel.handle_error()
+ if not isproducer:
+ self.data_channel.push(data)
+ else:
+ self.data_channel.push_with_producer(data)
+ if self.data_channel:
+ self.data_channel.close_when_done()
+ self.__out_dtp_queue = None
# check for data to receive
- elif self._in_dtp_queue is not None:
- self.data_channel.file_obj = self._in_dtp_queue
+ elif self.__in_dtp_queue:
+ self.data_channel.file_obj = self.__in_dtp_queue
self.data_channel.enable_receiving(self.current_type)
- self._in_dtp_queue = None
+ self.__in_dtp_queue = None
def on_dtp_close(self):
"""Called every time the data channel is closed."""
self.data_channel = None
if self.quit_pending:
self.close_when_done()
- elif self.timeout:
- # data transfer finished, restart the idle timer
- self.idler = CallLater(self.timeout, self.handle_timeout)
# --- utility
@@ -1943,26 +1757,22 @@ class FTPHandler(asynchat.async_chat):
- (bool) isproducer: whether treat data as a producer.
- (file) file: the file[-like] object to send (if any).
"""
- if self.data_channel is not None:
+ if self.data_channel:
self.respond("125 Data connection already open. Transfer starting.")
if file:
self.data_channel.file_obj = file
- try:
- if not isproducer:
- self.data_channel.push(data)
- else:
- self.data_channel.push_with_producer(data)
- if self.data_channel is not None:
- self.data_channel.close_when_done()
- except:
- # dealing with this exception is up to DTP (see bug #84)
- self.data_channel.handle_error()
+ if not isproducer:
+ self.data_channel.push(data)
+ else:
+ self.data_channel.push_with_producer(data)
+ if self.data_channel:
+ self.data_channel.close_when_done()
else:
self.respond("150 File status okay. About to open data connection.")
- self._out_dtp_queue = (data, isproducer, file)
+ self.__out_dtp_queue = (data, isproducer, file)
def log(self, msg):
- """Log a message, including additional identifying session data."""
+ """Log a message, including additional identifying session data."""
log("[%s]@%s:%s %s" %(self.username, self.remote_ip,
self.remote_port, msg))
@@ -1974,11 +1784,11 @@ class FTPHandler(asynchat.async_chat):
"""Flush account information by clearing attributes that need
to be reset on a REIN or new USER command.
"""
- if self.data_channel is not None:
+ if self.data_channel:
if not self.data_channel.transfer_in_progress():
self.data_channel.close()
self.data_channel = None
- if self.data_server is not None:
+ if self.data_server:
self.data_server.close()
self.data_server = None
@@ -1990,9 +1800,8 @@ class FTPHandler(asynchat.async_chat):
self.current_type = 'a'
self.restart_position = 0
self.quit_pending = False
- self.sleeping = False
- self._in_dtp_queue = None
- self._out_dtp_queue = None
+ self.__in_dtp_queue = None
+ self.__out_dtp_queue = None
def run_as_current_user(self, function, *args, **kwargs):
"""Execute a function impersonating the current logged-in user."""
@@ -2011,33 +1820,36 @@ class FTPHandler(asynchat.async_chat):
# FTP bounce attacks protection: according to RFC-2577 it's
# recommended to reject PORT if IP address specified in it
# does not match client IP address.
- if not self.permit_foreign_addresses and ip != self.remote_ip:
- self.log("Rejected data connection to foreign address %s:%s."
- %(ip, port))
- self.respond("501 Can't connect to a foreign address.")
- return
+ if not self.permit_foreign_addresses:
+ if ip != self.remote_ip:
+ self.log("Rejected data connection to foreign address %s:%s."
+ %(ip, port))
+ self.respond("501 Can't connect to a foreign address.")
+ return
# ...another RFC-2577 recommendation is rejecting connections
# to privileged ports (< 1024) for security reasons.
- if not self.permit_privileged_ports and port < 1024:
- self.log('PORT against the privileged port "%s" refused.' %port)
- self.respond("501 Can't connect over a privileged port.")
- return
+ if not self.permit_privileged_ports:
+ if port < 1024:
+ self.log('PORT against the privileged port "%s" refused.' %port)
+ self.respond("501 Can't connect over a privileged port.")
+ return
# close existent DTP-server instance, if any.
- if self.data_server is not None:
+ if self.data_server:
self.data_server.close()
self.data_server = None
- if self.data_channel is not None:
+ if self.data_channel:
self.data_channel.close()
self.data_channel = None
# make sure we are not hitting the max connections limit
- if self.server.max_cons and len(self._map) >= self.server.max_cons:
- msg = "Too many connections. Can't open data channel."
- self.respond("425 %s" %msg)
- self.log(msg)
- return
+ if self.server.max_cons:
+ if len(self._map) >= self.server.max_cons:
+ msg = "Too many connections. Can't open data channel."
+ self.respond("425 %s" %msg)
+ self.log(msg)
+ return
# open data channel
self.active_dtp(ip, port, self)
@@ -2049,20 +1861,21 @@ class FTPHandler(asynchat.async_chat):
which case extended passive mode will be used (see RFC-2428).
"""
# close existing DTP-server instance, if any
- if self.data_server is not None:
+ if self.data_server:
self.data_server.close()
self.data_server = None
- if self.data_channel is not None:
+ if self.data_channel:
self.data_channel.close()
self.data_channel = None
# make sure we are not hitting the max connections limit
- if self.server.max_cons and len(self._map) >= self.server.max_cons:
- msg = "Too many connections. Can't open data channel."
- self.respond("425 %s" %msg)
- self.log(msg)
- return
+ if self.server.max_cons:
+ if len(self._map) >= self.server.max_cons:
+ msg = "Too many connections. Can't open data channel."
+ self.respond("425 %s" %msg)
+ self.log(msg)
+ return
# open data channel
self.data_server = self.passive_dtp(self, extmode)
@@ -2123,7 +1936,7 @@ class FTPHandler(asynchat.async_chat):
assert len(octs) == 4
for x in octs:
assert 0 <= x <= 255
- except (AssertionError, ValueError, OverflowError):
+ except (AssertionError, ValueError, OverflowError), err:
self.respond("501 Invalid EPRT format.")
else:
self._make_eport(ip, port)
@@ -2186,7 +1999,12 @@ class FTPHandler(asynchat.async_chat):
self.respond('501 Unknown network protocol (use 2).')
def ftp_QUIT(self, line):
- """Quit the current session disconnecting the client."""
+ """Quit the current session."""
+ # From RFC-959:
+ # This command terminates a USER and if file transfer is not
+ # in progress, the server closes the control connection.
+ # If file transfer is in progress, the connection will remain
+ # open for result response and the server will then close it.
if self.authenticated:
msg_quit = self.authorizer.get_msg_quit(self.username)
else:
@@ -2197,15 +2015,12 @@ class FTPHandler(asynchat.async_chat):
self.push("221-%s\r\n" %msg_quit)
self.respond("221 ")
- # From RFC-959:
- # If file transfer is in progress, the connection will remain
- # open for result response and the server will then close it.
- # We also stop responding to any further command.
- if self.data_channel:
- self.quit_pending = True
- self.sleeping = True
- else:
+ if not self.data_channel:
self.close_when_done()
+ else:
+ # tell the cmd channel to stop responding to commands.
+ self.quit_pending = True
+
# --- data transferring
@@ -2216,30 +2031,35 @@ class FTPHandler(asynchat.async_chat):
# - If no argument, fall back on cwd as default.
# - Some older FTP clients erroneously issue /bin/ls-like LIST
# formats in which case we fall back on cwd as default.
- datacr = None
- try:
- datacr = self.fs.get_cr(line)
- path = self.fs.ftp2fs(line, datacr)
+ if not line or line.lower() in ('-a', '-l', '-al', '-la'):
+ line = self.fs.cwd
+ try:
+ data = None
+ data = self.fs.get_cr(line)
+ path = self.fs.ftp2fs(line, data)
line = self.fs.ftpnorm(line)
- iterator = self.run_as_current_user(self.fs.get_list_dir, path)
+ iterator = self.run_as_current_user(self.fs.get_list_dir, path)
except OSError, err:
+ self.fs.close_cr(data)
why = _strerror(err)
self.log('FAIL LIST "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
else:
+ self.fs.close_cr(data)
self.log('OK LIST "%s". Transfer starting.' %line)
producer = BufferedIteratorProducer(iterator)
self.push_dtp_data(producer, isproducer=True)
- self.fs.close_cr(datacr)
def ftp_NLST(self, line):
"""Return a list of files in the specified directory in a
compact form to the client.
"""
- datacr = None
- try:
- datacr = self.fs.get_cr(line)
- path = self.fs.ftp2fs(line, datacr)
+ if not line:
+ line = self.fs.cwd
+ try:
+ data = None
+ data = self.fs.get_cr(line)
+ path = self.fs.ftp2fs(line, data)
line = self.fs.ftpnorm(line)
if self.fs.isdir(path):
listing = self.run_as_current_user(self.fs.listdir, path)
@@ -2248,19 +2068,21 @@ class FTPHandler(asynchat.async_chat):
# if path is a file we just list its name
self.fs.lstat(path) # raise exc in case of problems
basedir, filename = os.path.split(line)
- listing = [filename]
+ listing = [filename]
except OSError, err:
+ self.fs.close_cr(data)
why = _strerror(err)
self.log('FAIL NLST "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
else:
+ self.fs.close_cr(data)
data = ''
if listing:
listing.sort()
data = '\r\n'.join(listing) + '\r\n'
self.log('OK NLST "%s". Transfer starting.' %line)
self.push_dtp_data(data)
- self.fs.close_cr(datacr)
+
# --- MLST and MLSD commands
# The MLST and MLSD commands are intended to standardize the file and
@@ -2271,9 +2093,12 @@ class FTPHandler(asynchat.async_chat):
def ftp_MLST(self, line):
"""Return information about a pathname in a machine-processable
form as defined in RFC-3659.
- """
- datacr = None
- try:
+ """
+ # if no argument, fall back on cwd as default
+ if not line:
+ line = self.fs.cwd
+ try:
+ datacr = None
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
@@ -2281,7 +2106,7 @@ class FTPHandler(asynchat.async_chat):
perms = self.authorizer.get_perms(self.username)
iterator = self.run_as_current_user(self.fs.format_mlsx, basedir,
[basename], perms, self.current_facts, ignore_err=False)
- data = ''.join(iterator)
+ data = ''.join(iterator)
except OSError, err:
self.fs.close_cr(datacr)
why = _strerror(err)
@@ -2302,8 +2127,11 @@ class FTPHandler(asynchat.async_chat):
"""Return contents of a directory in a machine-processable form
as defined in RFC-3659.
"""
- datacr = None
- try:
+ # if no argument, fall back on cwd as default
+ if not line:
+ line = self.fs.cwd
+ try:
+ datacr = None
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
@@ -2313,7 +2141,7 @@ class FTPHandler(asynchat.async_chat):
self.log('FAIL MLSD "%s". %s.' %(line, err))
self.respond("501 %s." %err)
return
- listing = self.run_as_current_user(self.fs.listdir, path)
+ listing = self.run_as_current_user(self.fs.listdir, path)
except OSError, err:
self.fs.close_cr(datacr)
why = _strerror(err)
@@ -2323,7 +2151,7 @@ class FTPHandler(asynchat.async_chat):
self.fs.close_cr(datacr)
perms = self.authorizer.get_perms(self.username)
iterator = self.fs.format_mlsx(path, listing, perms,
- self._current_facts)
+ self.current_facts)
producer = BufferedIteratorProducer(iterator)
self.log('OK MLSD "%s". Transfer starting.' %line)
self.push_dtp_data(producer, isproducer=True)
@@ -2332,14 +2160,18 @@ class FTPHandler(asynchat.async_chat):
"""Retrieve the specified file (transfer from the server to the
client)
"""
- datacr = None
- try:
+ try:
+ datacr = None
datacr = self.fs.get_cr(line)
file = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
- rest_pos = self.restart_position
- self.restart_position = 0
- fd = self.run_as_current_user(self.fs.open, file, 'rb')
+ fd = self.run_as_current_user(self.fs.open, file, 'rb')
+ except OSError, err:
+ self.fs.close_cr(datacr)
+ why = _strerror(err)
+ self.log('FAIL RETR "%s". %s.' %(line, why))
+ self.respond('550 %s.' %why)
+ return
except IOError, err:
self.fs.close_cr(datacr)
why = _strerror(err)
@@ -2347,7 +2179,7 @@ class FTPHandler(asynchat.async_chat):
self.respond('550 %s.' %why)
return
- if rest_pos:
+ if self.restart_position:
# Make sure that the requested offset is valid (within the
# size of the file being resumed).
# According to RFC-1123 a 554 reply may result in case that
@@ -2355,18 +2187,19 @@ class FTPHandler(asynchat.async_chat):
# the REST.
ok = 0
try:
- assert not rest_pos > self.fs.getsize(file)
- fd.seek(rest_pos)
+ assert not self.restart_position > self.fs.getsize(file)
+ fd.seek(self.restart_position)
ok = 1
except AssertionError:
why = "Invalid REST parameter"
except IOError, err:
why = _strerror(err)
- if not ok:
+ self.restart_position = 0
+ if not ok:
self.respond('554 %s' %why)
self.log('FAIL RETR "%s". %s.' %(line, why))
self.fs.close_cr(datacr)
- return
+ return
self.log('OK RETR "%s". Download starting.' %line)
producer = FileProducer(fd, self.current_type)
self.push_dtp_data(producer, isproducer=True, file=fd)
@@ -2383,7 +2216,7 @@ class FTPHandler(asynchat.async_chat):
cmd = 'APPE'
else:
cmd = 'STOR'
-
+
line = self.fs.ftpnorm(line)
basedir,basename = os.path.split(line)
@@ -2394,14 +2227,12 @@ class FTPHandler(asynchat.async_chat):
except OSError, err:
self.fs.close_cr(datacr)
- why = ftpserver._strerror(err)
+ why = _strerror(err)
self.log('FAIL %s "%s". %s.' %(cmd, line, why))
self.respond('550 %s.' %why)
return
-
- rest_pos = self.restart_position
- self.restart_position = 0
- if rest_pos:
+
+ if self.restart_position:
mode = 'r+'
try:
fd = self.run_as_current_user(self.fs.create, file, basename, mode + 'b')
@@ -2412,7 +2243,7 @@ class FTPHandler(asynchat.async_chat):
self.respond('550 %s.' %why)
return
- if rest_pos:
+ if self.restart_position:
# Make sure that the requested offset is valid (within the
# size of the file being resumed).
# According to RFC-1123 a 554 reply may result in case
@@ -2420,29 +2251,31 @@ class FTPHandler(asynchat.async_chat):
# specified in the REST.
ok = 0
try:
- assert not rest_pos > self.fs.getsize(file)
- fd.seek(rest_pos)
+ assert not self.restart_position > self.fs.getsize(self.fs.ftp2fs(line, datacr))
+ fd.seek(self.restart_position)
ok = 1
except AssertionError:
why = "Invalid REST parameter"
except IOError, err:
why = _strerror(err)
+ self.restart_position = 0
if not ok:
+ self.fs.close_cr(datacr)
self.respond('554 %s' %why)
self.log('FAIL %s "%s". %s.' %(cmd, line, why))
- self.fs.close_cr(datacr)
return
self.log('OK %s "%s". Upload starting.' %(cmd, line))
- if self.data_channel is not None:
+ if self.data_channel:
self.respond("125 Data connection already open. Transfer starting.")
self.data_channel.file_obj = fd
self.data_channel.enable_receiving(self.current_type)
else:
self.respond("150 File status okay. About to open data connection.")
- self._in_dtp_queue = fd
+ self.__in_dtp_queue = fd
self.fs.close_cr(datacr)
+
def ftp_STOU(self, line):
"""Store a file on the server with a unique name."""
# Note 1: RFC-959 prohibited STOU parameters, but this
@@ -2459,7 +2292,7 @@ class FTPHandler(asynchat.async_chat):
if self.restart_position:
self.respond("450 Can't STOU while REST request is pending.")
return
-
+
datacr = None
datacr = self.fs.get_cr(line)
@@ -2469,7 +2302,7 @@ class FTPHandler(asynchat.async_chat):
basedir = self.fs.ftp2fs(basedir, datacr)
#prefix = prefix + '.'
else:
- basedir = self.fs.ftp2fs(self.fs.cwd)
+ basedir = self.fs.ftp2fs(self.fs.cwd, datacr)
prefix = 'ftpd.'
try:
fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix,
@@ -2487,12 +2320,8 @@ class FTPHandler(asynchat.async_chat):
self.fs.close_cr(datacr)
return
- if not self.authorizer.has_perm(self.username, 'w', fd.name):
- try:
- fd.close()
- self.run_as_current_user(self.fs.remove, fd.name)
- except os.error:
- pass
+ filename = line
+ if not self.authorizer.has_perm(self.username, 'w', filename):
self.log('FAIL STOU "%s". Not enough privileges'
%self.fs.ftpnorm(line))
self.respond("550 Can't STOU: not enough privileges.")
@@ -2500,30 +2329,27 @@ class FTPHandler(asynchat.async_chat):
return
# now just acts like STOR except that restarting isn't allowed
- filename = os.path.basename(fd.name)
self.log('OK STOU "%s". Upload starting.' %filename)
- if self.data_channel is not None:
+ if self.data_channel:
self.respond("125 FILE: %s" %filename)
self.data_channel.file_obj = fd
self.data_channel.enable_receiving(self.current_type)
else:
self.respond("150 FILE: %s" %filename)
- self._in_dtp_queue = fd
+ self.__in_dtp_queue = fd
self.fs.close_cr(datacr)
- def ftp_APPE(self, file):
+
+ def ftp_APPE(self, line):
"""Append data to an existing file on the server."""
# watch for APPE preceded by REST, which makes no sense.
if self.restart_position:
- self.respond("450 Can't APPE while REST request is pending.")
+ self.respond("550 Can't APPE while REST request is pending.")
else:
- self.ftp_STOR(file, mode='a')
+ self.ftp_STOR(line, mode='a')
def ftp_REST(self, line):
"""Restart a file transfer from a previous mark."""
- if self.current_type == 'a':
- self.respond('501 Resuming transfers not allowed in ASCII mode.')
- return
try:
marker = int(line)
if marker < 0:
@@ -2531,7 +2357,9 @@ class FTPHandler(asynchat.async_chat):
except (ValueError, OverflowError):
self.respond("501 Invalid parameter.")
else:
- self.respond("350 Restarting at position %s." %marker)
+ self.respond("350 Restarting at position %s. " \
+ "Now use RETR/STOR for resuming." %marker)
+ self.log("OK REST %s." %marker)
self.restart_position = marker
def ftp_ABOR(self, line):
@@ -2542,7 +2370,7 @@ class FTPHandler(asynchat.async_chat):
resp = "225 No transfer to abort."
else:
# a PASV was received but connection wasn't made yet
- if self.data_server is not None:
+ if self.data_server:
self.data_server.close()
self.data_server = None
resp = "225 ABOR command successful; data channel closed."
@@ -2554,7 +2382,7 @@ class FTPHandler(asynchat.async_chat):
# was successfully processed.
# If no data has been transmitted we just respond with 225
# indicating that no transfer was in progress.
- if self.data_channel is not None:
+ if self.data_channel:
if self.data_channel.transfer_in_progress():
self.data_channel.close()
self.data_channel = None
@@ -2573,6 +2401,10 @@ class FTPHandler(asynchat.async_chat):
def ftp_USER(self, line):
"""Set the username for the current session."""
+ # we always treat anonymous user as lower-case string.
+ if line.lower() == "anonymous":
+ line = "anonymous"
+
# RFC-959 specifies a 530 response to the USER command if the
# username is not valid. If the username is valid is required
# ftpd returns a 331 response instead. In order to prevent a
@@ -2593,10 +2425,9 @@ class FTPHandler(asynchat.async_chat):
self.respond('331 %s, send password.' %msg)
self.username = line
- _auth_failed_timeout = 5
-
def ftp_PASS(self, line):
"""Check username's password against the authorizer."""
+
if self.authenticated:
self.respond("503 User already authenticated.")
return
@@ -2604,42 +2435,50 @@ class FTPHandler(asynchat.async_chat):
self.respond("503 Login with USER first.")
return
- def auth_failed(msg="Authentication failed."):
- if not self._closed:
+ # username ok
+ if self.authorizer.has_user(self.username):
+ if self.username == 'anonymous' \
+ or self.authorizer.validate_authentication(self.username, line):
+ msg_login = self.authorizer.get_msg_login(self.username)
+ if len(msg_login) <= 75:
+ self.respond('230 %s' %msg_login)
+ else:
+ self.push("230-%s\r\n" %msg_login)
+ self.respond("230 ")
+
+ self.authenticated = True
+ self.password = line
+ self.attempted_logins = 0
+ self.fs.root = self.authorizer.get_home_dir(self.username)
+ self.fs.username=self.username
+ self.fs.password=line
+ self.log("User %s logged in." %self.username)
+ else:
self.attempted_logins += 1
if self.attempted_logins >= self.max_login_attempts:
- msg = "530 " + msg + " Disconnecting."
- self.respond(msg)
- self.log(msg)
- self.close_when_done()
+ self.respond("530 Maximum login attempts. Disconnecting.")
+ self.close()
else:
- self.respond("530 " + msg)
- self.log(msg)
- self.sleeping = False
+ self.respond("530 Authentication failed.")
+ self.log('Authentication failed (user: "%s").' %self.username)
+ self.username = ""
- if self.authorizer.validate_authentication(self.username, line):
- msg_login = self.authorizer.get_msg_login(self.username)
- if len(msg_login) <= 75:
- self.respond('230 %s' %msg_login)
- else:
- self.push("230-%s\r\n" %msg_login)
- self.respond("230 ")
- self.authenticated = True
- self.password = line
- self.fs.username = self.username
- self.fs.password = line
- self.attempted_logins = 0
- self.fs.root = self.authorizer.get_home_dir(self.username)
- self.log("User %s logged in." %self.username)
+ # wrong username
else:
- self.username = ""
- self.fs.username = ""
- self.sleeping = True
- if self.username == 'anonymous':
- CallLater(self._auth_failed_timeout, auth_failed,
- "Anonymous access not allowed.")
+ self.attempted_logins += 1
+ if self.attempted_logins >= self.max_login_attempts:
+ self.log('Authentication failed: unknown username "%s".'
+ %self.username)
+ self.respond("530 Maximum login attempts. Disconnecting.")
+ self.close()
+ elif self.username.lower() == 'anonymous':
+ self.respond("530 Anonymous access not allowed.")
+ self.log('Authentication failed: anonymous access not allowed.')
else:
- CallLater(self._auth_failed_timeout, auth_failed)
+ self.respond("530 Authentication failed.")
+ self.log('Authentication failed: unknown username "%s".'
+ %self.username)
+ self.username = ""
def ftp_REIN(self, line):
"""Reinitialize user's current session."""
@@ -2665,6 +2504,9 @@ class FTPHandler(asynchat.async_chat):
def ftp_CWD(self, line):
"""Change the current working directory."""
+ # TODO: a lot of FTP servers go back to root directory if no
+ # arg is provided but this is not specified in RFC-959.
+ # Search for official references about this behaviour.
if not line:
line = '/'
datacr = None
@@ -2694,38 +2536,33 @@ class FTPHandler(asynchat.async_chat):
def ftp_SIZE(self, line):
"""Return size of file in a format suitable for using with
- RESTart as defined in RFC-3659."""
+ RESTart as defined in RFC-3659.
- # Implementation note: properly handling the SIZE command when
- # TYPE ASCII is used would require to scan the entire file to
- # perform the ASCII translation logic
- # (file.read().replace(os.linesep, '\r\n')) and then calculating
- # the len of such data which may be different than the actual
- # size of the file on the server. Considering that calculating
- # such result could be very resource-intensive and also dangerous
- # (DoS) we reject SIZE when the current TYPE is ASCII.
- # However, clients in general should not be resuming downloads
- # in ASCII mode. Resuming downloads in binary mode is the
- # recommended way as specified in RFC-3659.
-
- if self.current_type == 'a':
- why = "SIZE not allowed in ASCII mode"
- self.log('FAIL SIZE "%s". %s.' %(line, why))
- self.respond("550 %s." %why)
- return
- datacr = False
+ Implementation note:
+ properly handling the SIZE command when TYPE ASCII is used would
+ require to scan the entire file to perform the ASCII translation
+ logic (file.read().replace(os.linesep, '\r\n')) and then
+ calculating the len of such data which may be different than
+ the actual size of the file on the server. Considering that
+ calculating such result could be very resource-intensive it
+ could be easy for a malicious client to try a DoS attack, thus
+ we do not perform the ASCII translation.
+
+ However, clients in general should not be resuming downloads in
+ ASCII mode. Resuming downloads in binary mode is the recommended
+ way as specified in RFC-3659.
+ """
+ datacr = None
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
- if not self.fs.isfile(self.fs.realpath(path)):
+ if self.fs.isdir(path):
why = "%s is not retrievable" %line
self.log('FAIL SIZE "%s". %s.' %(line, why))
self.respond("550 %s." %why)
- self.fs.close_cr(datacr)
return
size = self.run_as_current_user(self.fs.getsize, path)
- size = self.run_as_current_user(self.fs.getsize, path)
except OSError, err:
why = _strerror(err)
self.log('FAIL SIZE "%s". %s.' %(line, why))
@@ -2739,7 +2576,7 @@ class FTPHandler(asynchat.async_chat):
"""Return last modification time of file to the client as an ISO
3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
"""
- datacr = None
+ datacr = None
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
@@ -2750,23 +2587,17 @@ class FTPHandler(asynchat.async_chat):
self.respond("550 %s." %why)
self.fs.close_cr(datacr)
return
- secs = self.run_as_current_user(self.fs.getmtime, path)
- lmt = self.run_as_current_user(self.fs.getmtime, path)
-
- except (OSError, ValueError), err:
- if isinstance(err, OSError):
- why = _strerror(err)
- else:
- # It could happen if file's last modification time
- # happens to be too old (prior to year 1900)
- why = "Can't determine file's last modification time"
+ lmt = self.run_as_current_user(self.fs.getmtime, path)
+ except OSError, err:
+ why = _strerror(err)
self.log('FAIL MDTM "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
else:
+ lmt = time.strftime("%Y%m%d%H%M%S", time.localtime(lmt))
self.respond("213 %s" %lmt)
self.log('OK MDTM "%s".' %line)
self.fs.close_cr(datacr)
-
+
def ftp_MKD(self, line):
"""Create the specified directory."""
datacr = None
@@ -2782,15 +2613,12 @@ class FTPHandler(asynchat.async_chat):
self.respond('550 %s.' %why)
else:
self.log('OK MKD "%s".' %line)
- # The 257 response is supposed to include the directory
- # name and in case it contains embedded double-quotes
- # they must be doubled (see RFC-959, chapter 7, appendix 2).
- self.respond('257 "%s" directory created.' %line.replace('"', '""'))
+ self.respond("257 Directory created.")
self.fs.close_cr(datacr)
def ftp_RMD(self, line):
"""Remove the specified directory."""
- datacr = False
+ datacr = None
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
@@ -2801,7 +2629,7 @@ class FTPHandler(asynchat.async_chat):
self.log('FAIL MKD "/". %s' %msg)
self.fs.close_cr(datacr)
return
- self.run_as_current_user(self.fs.rmdir, path)
+ self.run_as_current_user(self.fs.rmdir, path)
except OSError, err:
why = _strerror(err)
self.log('FAIL RMD "%s". %s.' %(line, why))
@@ -2810,7 +2638,7 @@ class FTPHandler(asynchat.async_chat):
self.log('OK RMD "%s".' %line)
self.respond("250 Directory removed.")
self.fs.close_cr(datacr)
-
+
def ftp_DELE(self, line):
"""Delete the specified file."""
datacr = None
@@ -2827,11 +2655,10 @@ class FTPHandler(asynchat.async_chat):
self.log('OK DELE "%s".' %line)
self.respond("250 File removed.")
self.fs.close_cr(datacr)
-
+
def ftp_RNFR(self, line):
"""Rename the specified (only the source name is specified
here, see RNTO command)"""
-
datacr = None
try:
datacr = self.fs.get_cr(line)
@@ -2855,7 +2682,7 @@ class FTPHandler(asynchat.async_chat):
if not self.fs.rnfr:
self.respond("503 Bad sequence of commands: use RNFR first.")
return
- datacr = None
+ datacr = None
try:
try:
datacr = self.fs.get_cr(line)
@@ -2878,50 +2705,35 @@ class FTPHandler(asynchat.async_chat):
self.fs.close_cr(datacr)
-
# --- others
def ftp_TYPE(self, line):
"""Set current type data type to binary/ascii"""
- type = line.upper().replace(' ', '')
- if type in ("A", "L7"):
+ line = line.upper()
+ if line in ("A", "AN", "A N"):
self.respond("200 Type set to: ASCII.")
self.current_type = 'a'
- elif type in ("I", "L8"):
+ elif line in ("I", "L8", "L 8"):
self.respond("200 Type set to: Binary.")
self.current_type = 'i'
else:
self.respond('504 Unsupported type "%s".' %line)
def ftp_STRU(self, line):
- """Set file structure ("F" is the only one supported (noop))."""
- stru = line.upper()
- if stru == 'F':
+ """Set file structure (obsolete)."""
+ # obsolete (backward compatibility with older ftp clients)
+ if line in ('f','F'):
self.respond('200 File transfer structure set to: F.')
- elif stru in ('P', 'R'):
- # R is required in minimum implementations by RFC-959, 5.1.
- # RFC-1123, 4.1.2.13, amends this to only apply to servers
- # whose file systems support record structures, but also
- # suggests that such a server "may still accept files with
- # STRU R, recording the byte stream literally".
- # Should we accept R but with no operational difference from
- # F? proftpd and wu-ftpd don't accept STRU R. We just do
- # the same.
- #
- # RFC-1123 recommends against implementing P.
- self.respond('504 Unimplemented STRU type.')
else:
- self.respond('501 Unrecognized STRU type.')
+ self.respond('504 Unimplemented STRU type.')
def ftp_MODE(self, line):
- """Set data transfer mode ("S" is the only one supported (noop))."""
- mode = line.upper()
- if mode == 'S':
+ """Set data transfer mode (obsolete)"""
+ # obsolete (backward compatibility with older ftp clients)
+ if line in ('s', 'S'):
self.respond('200 Transfer mode set to: S')
- elif mode in ('B', 'C'):
- self.respond('504 Unimplemented MODE type.')
else:
- self.respond('501 Unrecognized MODE type.')
+ self.respond('504 Unimplemented MODE type.')
def ftp_STAT(self, line):
"""Return statistics about current ftp session. If an argument
@@ -2929,15 +2741,17 @@ class FTPHandler(asynchat.async_chat):
Implementation note:
- RFC-959 does not explicitly mention globbing but many FTP
- servers do support it as a measure of convenience for FTP
- clients and users.
+ RFC-959 do not explicitly mention globbing; this means that FTP
+ servers are not required to support globbing in order to be
+ compliant. However, many FTP servers do support globbing as a
+ measure of convenience for FTP clients and users.
In order to search for and match the given globbing expression,
the code has to search (possibly) many directories, examine
each contained filename, and build a list of matching files in
memory. Since this operation can be quite intensive, both CPU-
- and memory-wise, we do not support globbing.
+ and memory-wise, we limit the search to only one directory
+ non-recursively, as LIST does.
"""
# return STATus information about ftpd
if not line:
@@ -2955,9 +2769,9 @@ class FTPHandler(asynchat.async_chat):
else:
type = 'Binary'
s.append("TYPE: %s; STRUcture: File; MODE: Stream" %type)
- if self.data_server is not None:
+ if self.data_server:
s.append('Passive data channel waiting for connection.')
- elif self.data_channel is not None:
+ elif self.data_channel:
bytes_sent = self.data_channel.tot_bytes_sent
bytes_recv = self.data_channel.tot_bytes_received
s.append('Data connection open:')
@@ -2976,22 +2790,19 @@ class FTPHandler(asynchat.async_chat):
datacr = self.fs.get_cr(line)
iterator = self.run_as_current_user(self.fs.get_stat_dir, line, datacr)
except OSError, err:
- why = _strerror(err)
- self.log('FAIL STAT "%s". %s.' %(line, why))
- self.respond('550 %s.' %why)
+ self.respond('550 %s.' %_strerror(err))
else:
- self.push('213-Status of "%s":\r\n' %line)
+ self.push('213-Status of "%s":\r\n' %self.fs.ftpnorm(line))
self.push_with_producer(BufferedIteratorProducer(iterator))
self.respond('213 End of status.')
self.fs.close_cr(datacr)
-
+
def ftp_FEAT(self, line):
"""List all new features supported as defined in RFC-2398."""
features = ['EPRT','EPSV','MDTM','MLSD','REST STREAM','SIZE','TVFS']
- features.extend(self._extra_feats)
s = ''
- for fact in self._available_facts:
- if fact in self._current_facts:
+ for fact in self.available_facts:
+ if fact in self.current_facts:
s += fact + '*;'
else:
s += fact + ';'
@@ -3016,8 +2827,8 @@ class FTPHandler(asynchat.async_chat):
self.respond('501 %s.' %err)
else:
facts = [x.lower() for x in arg.split(';')]
- self._current_facts = [x for x in facts if x in self._available_facts]
- f = ''.join([x + ';' for x in self._current_facts])
+ self.current_facts = [x for x in facts if x in self.available_facts]
+ f = ''.join([x + ';' for x in self.current_facts])
self.respond('200 MLST OPTS ' + f)
def ftp_NOOP(self, line):
@@ -3034,23 +2845,22 @@ class FTPHandler(asynchat.async_chat):
self.respond("215 UNIX Type: L8")
def ftp_ALLO(self, line):
- """Allocate bytes for storage (noop)."""
- # not necessary (always respond with 202)
+ """Allocate bytes for storage (obsolete)."""
+ # obsolete (always respond with 202)
self.respond("202 No storage allocation necessary.")
def ftp_HELP(self, line):
"""Return help text to the client."""
if line:
- line = line.upper()
- if line in proto_cmds:
- self.respond("214 %s" %proto_cmds[line].help)
+ if line.upper() in proto_cmds:
+ self.respond("214 %s" %proto_cmds[line.upper()])
else:
self.respond("501 Unrecognized command.")
else:
# provide a compact list of recognized commands
def formatted_help():
cmds = []
- keys = [x for x in proto_cmds.keys() if not x.startswith('SITE ')]
+ keys = proto_cmds.keys()
keys.sort()
while keys:
elems = tuple((keys[0:8]))
@@ -3062,30 +2872,6 @@ class FTPHandler(asynchat.async_chat):
self.push(formatted_help())
self.respond("214 Help command successful.")
- # --- site commands
-
- # No SITE commands aside from SITE HELP are implemented by default.
- # The user willing to add support for a specific SITE command has
- # to define a new ftp_SITE_%CMD% method in the subclass.
-
- def ftp_SITE_HELP(self, line):
- """Return help text to the client for a given SITE command."""
- if line:
- line = line.upper()
- if line in proto_cmds:
- self.respond("214 %s" %proto_cmds[line].help)
- else:
- self.respond("501 Unrecognized SITE command.")
- else:
- self.push("214-The following SITE commands are recognized:\r\n")
- site_cmds = []
- keys = proto_cmds.keys()
- keys.sort()
- for cmd in keys:
- if cmd.startswith('SITE '):
- site_cmds.append(' %s\r\n' %cmd[5:])
- self.push(''.join(site_cmds))
- self.respond("214 Help SITE command successful.")
# --- support for deprecated cmds
@@ -3186,50 +2972,43 @@ class FTPServer(asyncore.dispatcher):
return
asyncore.dispatcher.set_reuse_addr(self)
- def serve_forever(self, timeout=1, use_poll=False, map=None, count=None):
+ def serve_forever(self, **kwargs):
"""A wrap around asyncore.loop(); starts the asyncore polling
- loop including running the scheduler.
- The arguments are the same expected by original asyncore.loop()
- function.
+ loop.
+
+ The keyword arguments in kwargs are the same expected by
+ asyncore.loop() function: timeout, use_poll, map and count.
"""
- if map is None:
- map = asyncore.socket_map
- # backward compatibility for python versions < 2.4
+ if not 'count' in kwargs:
+ log("Serving FTP on %s:%s" %self.socket.getsockname()[:2])
+
+ # backward compatibility for python < 2.4
if not hasattr(self, '_map'):
+ if not 'map' in kwargs:
+ map = asyncore.socket_map
+ else:
+ map = kwargs['map']
self._map = self.handler._map = map
- if use_poll and hasattr(asyncore.select, 'poll'):
- poll_fun = asyncore.poll2
- else:
- poll_fun = asyncore.poll
-
- if count is None:
- log("Serving FTP on %s:%s" %self.socket.getsockname()[:2])
- try:
- while map or _tasks:
- if map:
- poll_fun(timeout, map)
- if _tasks:
- _scheduler()
- except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
- log("Shutting down FTP server.")
- self.close_all()
- else:
- while (map or _tasks) and count > 0:
- if map:
- poll_fun(timeout, map)
- if _tasks:
- _scheduler()
- count = count - 1
+ try:
+ # FIX #16, #26
+ # use_poll specifies whether to use select module's poll()
+ # with asyncore or whether to use asyncore's own poll()
+ # method Python versions < 2.4 need use_poll set to False
+ # This breaks on OS X systems if use_poll is set to True.
+ # All systems seem to work fine with it set to False
+ # (tested on Linux, Windows, and OS X platforms)
+ if kwargs:
+ asyncore.loop(**kwargs)
+ else:
+ asyncore.loop(timeout=1.0, use_poll=False)
+ except (KeyboardInterrupt, SystemExit, asyncore.ExitNow):
+ log("Shutting down FTPd.")
+ self.close_all()
def handle_accept(self):
"""Called when remote client initiates a connection."""
- try:
- sock_obj, addr = self.accept()
- except TypeError:
- # for some reason sometimes accept() returns None instead
- # of a socket
- return
+ sock_obj, addr = self.accept()
log("[]%s:%s Connected." %addr[:2])
handler = self.handler(sock_obj, self)
@@ -3268,7 +3047,7 @@ class FTPServer(asyncore.dispatcher):
self.close()
def close_all(self, map=None, ignore_all=False):
- """Stop serving and also disconnects all currently connected
+ """Stop serving; close all existent connections disconnecting
clients.
- (dict) map:
@@ -3281,11 +3060,13 @@ class FTPServer(asyncore.dispatcher):
Implementation note:
- This is how asyncore.close_all() is implemented starting from
+ Instead of using the current asyncore.close_all() function
+ which only close sockets, we iterate over all existent channels
+ calling close() method for each one of them, avoiding memory
+ leaks.
+
+ This is how asyncore.close_all() function should work in
Python 2.6.
- The previous versions of close_all() instead of iteratating over
- all opened channels and calling close() method for each one
- of them only closed sockets generating memory leaks.
"""
if map is None:
map = self._map
@@ -3307,13 +3088,15 @@ class FTPServer(asyncore.dispatcher):
def test():
# cmd line usage (provide a read-only anonymous ftp server):
- # python -m pyftpdlib.ftpserver
+ # python -m pyftpdlib.FTPServer
authorizer = DummyAuthorizer()
- authorizer.add_anonymous(os.getcwd())
+ authorizer.add_anonymous(os.getcwd(), perm='elradfmw')
FTPHandler.authorizer = authorizer
- address = ('', 21)
+ address = ('', 8021)
ftpd = FTPServer(address, FTPHandler)
ftpd.serve_forever()
if __name__ == '__main__':
test()
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
diff --git a/addons/document/nodes.py b/addons/document/nodes.py
new file mode 100644
index 00000000000..19efe97c7c1
--- /dev/null
+++ b/addons/document/nodes.py
@@ -0,0 +1,776 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2009 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import base64
+
+from osv import osv, fields
+from osv.orm import except_orm
+import urlparse
+import pooler
+from tools.safe_eval import safe_eval
+
+import os
+import time
+
+#
+# An object that represent an uri
+# path: the uri of the object
+# content: the Content it belongs to (_print.pdf)
+# type: content or collection
+# content: objct = res.partner
+# collection: object = directory, object2 = res.partner
+# file: objct = ir.attachement
+# root: if we are at the first directory of a ressource
+#
+
+def get_node_context(cr, uid, context):
+ return node_context(cr,uid,context)
+
+class node_context(object):
+ """ This is the root node, representing access to some particular
+ context """
+ cached_roots = {}
+
+ def __init__(self, cr, uid, context=None):
+ # we don't cache the cr!
+ #self.cr = cr
+ self.uid = uid
+ self.context = context
+ self._dirobj = pooler.get_pool(cr.dbname).get('document.directory')
+ assert self._dirobj
+ self.rootdir = self._dirobj._get_root_directory(cr,uid,context)
+
+ def get_uri(self, cr, uri):
+ """ Although this fn passes back to doc.dir, it is needed since
+ it is a potential caching point """
+
+ (ndir, duri) = self._dirobj._locate_child(cr,self.uid, self.rootdir,uri, None, self)
+
+ while duri:
+ ndir = ndir.child(cr, duri[0])
+ if not ndir:
+ return False
+ duri = duri[1:]
+ return ndir
+
+
+class node_database():
+ """ A node representing the database directory
+ Useless?
+ """
+ def __init__(self,ncontext):
+ self.nctx = ncontext
+
+
+
+
+class node_class(object):
+ """ this is a superclass for our inodes
+ It is an API for all code that wants to access the document files.
+ Nodes have attributes which contain usual file properties
+ """
+ our_type = 'baseclass'
+ def __init__(self, path, parent, context):
+ assert isinstance(context,node_context)
+ assert (not parent ) or isinstance(parent,node_class)
+ self.path = path
+ self.context = context
+ self.type=self.our_type
+ self.parent = parent
+ self.mimetype = 'application/octet-stream'
+ self.create_date = None
+ self.write_date = None
+ self.content_length = 0
+ # dynamic context:
+ self.dctx = {}
+ if parent:
+ self.dctx = parent.dctx.copy()
+ self.displayname = 'Object'
+
+ def full_path(self):
+ if self.parent:
+ s = self.parent.full_path()
+ else:
+ s = []
+ if isinstance(self.path,list):
+ s+=self.path
+ else:
+ s.append(self.path)
+ return s
+
+ def children(self, cr):
+ print "node_class.children()"
+ return [] #stub
+
+ def child(self,cr, name):
+ print "node_class.child()"
+ return None
+
+ def path_get(self):
+ print "node_class.path_get()"
+ return False
+
+ def get_data(self,cr):
+ raise TypeError('no data for %s'% self.type)
+
+ def _get_storage(self,cr):
+ raise RuntimeError("no storage for base class")
+
+ def get_etag(self,cr):
+ """ Get a tag, unique per object + modification.
+
+ see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """
+ return self._get_ttag(cr) + ':' + self._get_wtag(cr)
+
+ def _get_wtag(self,cr):
+ """ Return the modification time as a unique, compact string """
+ if self.write_date:
+ wtime = time.mktime(time.strptime(self.write_date,'%Y-%m-%d %H:%M:%S'))
+ else: wtime = time.time()
+ return str(wtime)
+
+ def _get_ttag(self,cr):
+ """ Get a unique tag for this type/id of object.
+ Must be overriden, so that each node is uniquely identified.
+ """
+ print "node_class.get_ttag()",self
+ raise RuntimeError("get_etag stub()")
+
+ def get_dav_props(self, cr):
+ """ If this class has special behaviour for GroupDAV etc, export
+ its capabilities """
+ return {}
+
+ def get_dav_eprop(self,cr,ns,prop):
+ return None
+
+class node_dir(node_class):
+ our_type = 'collection'
+ def __init__(self,path, parent, context, dirr, dctx=None):
+ super(node_dir,self).__init__(path, parent,context)
+ self.dir_id = dirr.id
+ #todo: more info from dirr
+ self.mimetype = 'application/x-directory'
+ # 'httpd/unix-directory'
+ self.create_date = dirr.create_date
+ # TODO: the write date should be MAX(file.write)..
+ self.write_date = dirr.write_date or dirr.create_date
+ self.content_length = 0
+ if dctx:
+ self.dctx.update(dctx)
+ dc2 = self.context.context
+ dc2.update(self.dctx)
+ dc2['dir_id'] = self.dir_id
+ self.displayname = dirr.name
+ for dfld in dirr.dctx_ids:
+ try:
+ self.dctx['dctx_' + dfld.field] = safe_eval(dfld.expr,dc2)
+ except Exception,e:
+ print "Cannot eval %s" % dfld.expr
+ print e
+ pass
+
+ def children(self,cr):
+ return self._child_get(cr) + self._file_get(cr)
+
+ def child(self,cr, name):
+ res = self._child_get(cr,name)
+ if res:
+ return res[0]
+ res = self._file_get(cr,name)
+ if res:
+ return res[0]
+ return None
+
+ def _file_get(self,cr, nodename=False):
+ res = []
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('directory_id','=',self.dir_id) ]
+ ids = cntobj.search(cr,uid,where,context=ctx)
+ for content in cntobj.browse(cr,uid,ids,context=ctx):
+ res3 = cntobj._file_get(cr,self,nodename,content)
+ if res3:
+ res.extend(res3)
+
+ return res
+
+ def get_dav_props(self, cr):
+ res = {}
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('directory_id','=',self.dir_id) ]
+ ids = cntobj.search(cr,uid,where,context=ctx)
+ for content in cntobj.browse(cr,uid,ids,context=ctx):
+ if content.extension == '.ics': # FIXME: call the content class!
+ res['http://groupdav.org/'] = ('resourcetype',)
+ break
+ return res
+
+ def get_dav_eprop(self,cr,ns,prop):
+ if ns != 'http://groupdav.org/' or prop != 'resourcetype':
+ print "Who asked for %s:%s?" % (ns,prop)
+ return None
+ res = {}
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('directory_id','=',self.dir_id) ]
+ ids = cntobj.search(cr,uid,where,context=ctx)
+ for content in cntobj.browse(cr,uid,ids,context=ctx):
+ if content.extension == '.ics': # FIXME: call the content class!
+ return ('vevent-collection','http://groupdav.org/')
+ return None
+
+ def _child_get(self,cr,name = None):
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('parent_id','=',self.dir_id) ]
+ if name:
+ where.append(('name','=',name))
+ ids = dirobj.search(cr, uid, where,context=ctx)
+ res = []
+ if ids:
+ for dirr in dirobj.browse(cr,uid,ids,context=ctx):
+ if dirr.type == 'directory':
+ res.append(node_dir(dirr.name,self,self.context,dirr))
+ elif dirr.type == 'ressource':
+ res.append(node_res_dir(dirr.name,self,self.context,dirr))
+
+ fil_obj=dirobj.pool.get('ir.attachment')
+ #where2 = where # + [('res_model', '=', None)]
+ ids = fil_obj.search(cr,uid,where,context=ctx)
+ if ids:
+ for fil in fil_obj.browse(cr,uid,ids,context=ctx):
+ res.append(node_file(fil.name,self,self.context,fil))
+
+ return res
+
+ def create_child(self,cr,path,data):
+ """ API function to create a child file object and node
+ Return the node_* created
+ """
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ fil_obj=dirobj.pool.get('ir.attachment')
+ val = {
+ 'name': path,
+ 'datas_fname': path,
+ 'parent_id': self.dir_id,
+ # Datas are not set here
+ }
+
+ fil_id = fil_obj.create(cr,uid, val, context=ctx)
+ fil = fil_obj.browse(cr,uid,fil_id,context=ctx)
+ fnode = node_file(path,self,self.context,fil)
+ fnode.set_data(cr,data,fil)
+ return fnode
+
+ def _get_ttag(self,cr):
+ return 'dir-%d' % self.dir_id
+
+class node_res_dir(node_class):
+ """ A special sibling to node_dir, which does only contain dynamically
+ created folders foreach resource in the foreign model.
+ All folders should be of type node_res_obj and merely behave like
+ node_dirs (with limited domain).
+ """
+ our_type = 'collection'
+ def __init__(self,path, parent, context, dirr, dctx=None ):
+ super(node_res_dir,self).__init__(path, parent,context)
+ self.dir_id = dirr.id
+ #todo: more info from dirr
+ self.mimetype = 'application/x-directory'
+ # 'httpd/unix-directory'
+ self.create_date = dirr.create_date
+ # TODO: the write date should be MAX(file.write)..
+ self.write_date = dirr.write_date or dirr.create_date
+ self.content_length = 0
+ self.res_model = dirr.ressource_type_id.model
+ self.resm_id = dirr.ressource_id
+ self.namefield = dirr.resource_field or 'name'
+ self.displayname = dirr.name
+ # Important: the domain is evaluated using the *parent* dctx!
+ self.domain = safe_eval(dirr.domain,self.dctx)
+ # and then, we add our own vars in the dctx:
+ if dctx:
+ self.dctx.update(dctx)
+
+ # and then, we prepare a dctx dict, for deferred evaluation:
+ self.dctx_dict = {}
+ for dfld in dirr.dctx_ids:
+ self.dctx_dict['dctx_' + dfld.field] = dfld.expr
+
+ def children(self,cr):
+ return self._child_get(cr)
+
+ def child(self,cr, name):
+ res = self._child_get(cr,name)
+ if res:
+ return res[0]
+ return None
+
+ def _child_get(self,cr,name = None):
+ """ return virtual children of resource, based on the
+ foreign object.
+
+ Note that many objects use NULL for a name, so we should
+ better call the name_search(),name_get() set of methods
+ """
+ obj = self.context._dirobj.pool.get(self.res_model)
+ if not obj:
+ print "couldn't find model", self.res_model
+ return []
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = []
+ if self.domain:
+ where.append(self.domain)
+ if self.resm_id:
+ where.append(('id','=',self.resm_id))
+
+ if name:
+ where.append((self.namefield,'=',name))
+ # print "Where clause for %s" % self.res_model, where
+
+ resids = obj.search(cr,uid, where, context=ctx)
+ res = []
+ for bo in obj.browse(cr,uid,resids,context=ctx):
+ if not bo:
+ continue
+ name = getattr(bo,self.namefield)
+ if not name:
+ continue
+ # Yes! we can't do better but skip nameless records.
+ res.append(node_res_obj(name,self,self.context,self.res_model, bo))
+ return res
+
+ def _get_ttag(self,cr):
+ return 'rdir-%d' % self.dir_id
+
+class node_res_obj(node_class):
+ """ A special sibling to node_dir, which does only contain dynamically
+ created folders foreach resource in the foreign model.
+ All folders should be of type node_res_obj and merely behave like
+ node_dirs (with limited domain).
+ """
+ our_type = 'collection'
+ def __init__(self,path, parent, context, res_model, res_bo, res_id = None):
+ super(node_res_obj,self).__init__(path, parent,context)
+ assert parent
+ #todo: more info from dirr
+ self.dir_id = parent.dir_id
+ self.mimetype = 'application/x-directory'
+ # 'httpd/unix-directory'
+ self.create_date = parent.create_date
+ # TODO: the write date should be MAX(file.write)..
+ self.write_date = parent.write_date
+ self.content_length = 0
+ self.res_model = res_model
+ self.domain = parent.domain
+ self.displayname = path
+
+ if res_bo:
+ self.res_id = res_bo.id
+ dc2 = self.context.context
+ dc2.update(self.dctx)
+ dc2['res_model'] = res_model
+ dc2['res_id'] = res_bo.id
+ dc2['this'] = res_bo
+ for fld,expr in parent.dctx_dict.items():
+ try:
+ self.dctx[fld] = safe_eval(expr,dc2)
+ except Exception,e:
+ print "Cannot eval %s for %s" % (expr, fld)
+ print e
+ pass
+ else:
+ self.res_id = res_id
+
+ def children(self,cr):
+ return self._child_get(cr) + self._file_get(cr)
+
+ def child(self,cr, name):
+ res = self._child_get(cr,name)
+ if res:
+ return res[0]
+ res = self._file_get(cr,name)
+ if res:
+ return res[0]
+ return None
+
+ def _file_get(self,cr, nodename=False):
+ res = []
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('directory_id','=',self.dir_id) ]
+ #if self.domain:
+ # where.extend(self.domain)
+ # print "res_obj file_get clause", where
+ ids = cntobj.search(cr,uid,where,context=ctx)
+ for content in cntobj.browse(cr,uid,ids,context=ctx):
+ res3 = cntobj._file_get(cr,self,nodename,content, context=ctx)
+ if res3:
+ res.extend(res3)
+
+ return res
+
+ def get_dav_props(self, cr):
+ res = {}
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('directory_id','=',self.dir_id) ]
+ ids = cntobj.search(cr,uid,where,context=ctx)
+ for content in cntobj.browse(cr,uid,ids,context=ctx):
+ if content.extension == '.ics': # FIXME: call the content class!
+ res['http://groupdav.org/'] = ('resourcetype',)
+ return res
+
+ def get_dav_eprop(self,cr,ns,prop):
+ if ns != 'http://groupdav.org/' or prop != 'resourcetype':
+ print "Who asked for %s:%s?" % (ns,prop)
+ return None
+ res = {}
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('directory_id','=',self.dir_id) ]
+ ids = cntobj.search(cr,uid,where,context=ctx)
+ for content in cntobj.browse(cr,uid,ids,context=ctx):
+ if content.extension == '.ics': # FIXME: call the content class!
+ return ('vevent-collection','http://groupdav.org/')
+ return None
+
+ def _child_get(self,cr,name = None):
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ where = [('parent_id','=',self.dir_id) ]
+ if name:
+ where.append(('name','=',name))
+ ids = dirobj.search(cr, uid, where,context=ctx)
+ res = []
+ if ids:
+ for dirr in dirobj.browse(cr,uid,ids,context=ctx):
+ if dirr.type == 'directory':
+ res.append(node_res_obj(dirr.name,self,self.context,self.res_model,res_bo = None, res_id = self.res_id))
+ elif dirr.type == 'ressource':
+ # child resources can be controlled by properly set dctx
+ res.append(node_res_dir(dirr.name,self,self.context,dirr))
+
+ fil_obj=dirobj.pool.get('ir.attachment')
+ where2 = where + [('res_model', '=', self.res_model), ('res_id','=',self.res_id)]
+ # print "where clause for dir_obj", where2
+ ids = fil_obj.search(cr,uid,where2,context=ctx)
+ if ids:
+ for fil in fil_obj.browse(cr,uid,ids,context=ctx):
+ res.append(node_file(fil.name,self,self.context,fil))
+
+ return res
+
+ def create_child(self,cr,path,data):
+ """ API function to create a child file object and node
+ Return the node_* created
+ """
+ dirobj = self.context._dirobj
+ uid = self.context.uid
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ fil_obj=dirobj.pool.get('ir.attachment')
+ val = {
+ 'name': path,
+ 'datas_fname': path,
+ 'parent_id': self.dir_id,
+ 'res_model': self.res_model,
+ 'res_id': self.res_id,
+ # Datas are not set here
+ }
+
+ fil_id = fil_obj.create(cr,uid, val, context=ctx)
+ fil = fil_obj.browse(cr,uid,fil_id,context=ctx)
+ fnode = node_file(path,self,self.context,fil)
+ fnode.set_data(cr,data,fil)
+ return fnode
+
+ def _get_ttag(self,cr):
+ return 'rodir-%d-%d' % (self.dir_id,self.res_id)
+
+class node_file(node_class):
+ our_type = 'file'
+ def __init__(self,path, parent, context, fil):
+ super(node_file,self).__init__(path, parent,context)
+ self.file_id = fil.id
+ #todo: more info from ir_attachment
+ if fil.file_type and '/' in fil.file_type:
+ self.mimetype = fil.file_type
+ self.create_date = fil.create_date
+ self.write_date = fil.write_date or fil.create_date
+ self.content_length = fil.file_size
+ self.displayname = fil.name
+
+ # This only propagates the problem to get_data. Better
+ # fix those files to point to the root dir.
+ if fil.parent_id:
+ self.storage_id = fil.parent_id.storage_id.id
+ else:
+ self.storage_id = None
+
+ def get_data(self, cr, fil_obj = None):
+ """ Retrieve the data for some file.
+ fil_obj may optionally be specified, and should be a browse object
+ for the file. This is useful when the caller has already initiated
+ the browse object. """
+ # this is where storage kicks in..
+ stor = self.storage_id
+ assert stor
+ stobj = self.context._dirobj.pool.get('document.storage')
+ return stobj.get_data(cr,self.context.uid,stor, self,self.context.context, fil_obj)
+
+ def get_data_len(self, cr, fil_obj = None):
+ # TODO: verify with the storage object!
+ return self.content_length
+
+ def set_data(self, cr, data, fil_obj = None):
+ """ Store data at some file.
+ fil_obj may optionally be specified, and should be a browse object
+ for the file. This is useful when the caller has already initiated
+ the browse object. """
+ # this is where storage kicks in..
+ stor = self.storage_id
+ assert stor
+ stobj = self.context._dirobj.pool.get('document.storage')
+ return stobj.set_data(cr,self.context.uid,stor, self, data, self.context.context, fil_obj)
+
+ def _get_ttag(self,cr):
+ return 'file-%d' % self.file_id
+
+class node_content(node_class):
+ our_type = 'content'
+ def __init__(self,path, parent, context, cnt, dctx = None, act_id=None):
+ super(node_content,self).__init__(path, parent,context)
+ self.cnt_id = cnt.id
+ self.create_date = False
+ self.write_date = False
+ self.content_length = False
+ self.extension = cnt.extension
+ self.report_id = cnt.report_id and cnt.report_id.id
+ #self.mimetype = cnt.extension.
+ self.displayname = path
+ if dctx:
+ self.dctx.update(dctx)
+ self.act_id = act_id
+
+ def fill_fields(self,cr,dctx = None):
+ """ Try to read the object and fill missing fields, like mimetype,
+ dates etc.
+ This function must be different from the constructor, because
+ it uses the db cursor.
+ """
+
+ cr.execute('SELECT DISTINCT mimetype FROM document_directory_content_type WHERE active AND code = %s;',
+ (self.extension,))
+ res = cr.fetchall()
+ if res and res[0][0]:
+ self.mimetype = res[0][0]
+
+
+ def get_data(self, cr, fil_obj = None):
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ data = cntobj.process_read(cr,self.context.uid,self,ctx)
+ if data:
+ self.content_length = len(data)
+ return data
+
+ def get_data_len(self, cr, fil_obj = None):
+ if not self.content_length:
+ self.get_data(cr,fil_obj)
+ return self.content_length
+
+ def set_data(self, cr, data, fil_obj = None):
+ cntobj = self.context._dirobj.pool.get('document.directory.content')
+ ctx = self.context.context.copy()
+ ctx.update(self.dctx)
+ return cntobj.process_write(cr,self.context.uid,self, data,ctx)
+
+ def _get_ttag(self,cr):
+ return 'cnt-%d%s' % (self.cnt_id,(self.act_id and ('-' + str(self.act_id))) or '')
+
+class old_class():
+ # the old code, remove..
+ def __init__(self, cr, uid, path, object, object2=False, context={}, content=False, type='collection', root=False):
+ self.cr = cr
+ def _file_get(self, nodename=False):
+ if not self.object:
+ return []
+ pool = pooler.get_pool(self.cr.dbname)
+ fobj = pool.get('ir.attachment')
+ res2 = []
+ where = []
+ if self.object2:
+ where.append( ('res_model','=',self.object2._name) )
+ where.append( ('res_id','=',self.object2.id) )
+ else:
+ where.append( ('parent_id','=',self.object.id) )
+ where.append( ('res_id','=',False) )
+ if nodename:
+ where.append( (fobj._rec_name,'=',nodename) )
+ for content in self.object.content_ids:
+ res3 = content._table._file_get(self,nodename,content)
+ if res3:
+ res2.extend(res3)
+
+ ids = fobj.search(self.cr, self.uid, where+[ ('parent_id','=',self.object and self.object.id or False) ])
+ if self.object and self.root and (self.object.type=='ressource'):
+ ids += fobj.search(self.cr, self.uid, where+[ ('parent_id','=',False) ])
+ res = fobj.browse(self.cr, self.uid, ids, context=self.context)
+ return map(lambda x: node_class(self.cr, self.uid, self.path+'/'+eval('x.'+fobj._rec_name), x, False, context=self.context, type='file', root=False), res) + res2
+
+ def get_translation(self,value,lang):
+ # Must go, it works on arbitrary models and could be ambiguous.
+ result = value
+ pool = pooler.get_pool(self.cr.dbname)
+ translation_ids = pool.get('ir.translation').search(self.cr, self.uid, [('value','=',value),('lang','=',lang),('type','=','model')])
+ if len(translation_ids):
+ tran_id = translation_ids[0]
+ translation = pool.get('ir.translation').read(self.cr, self.uid, tran_id, ['res_id','name'])
+ res_model,field_name = tuple(translation['name'].split(','))
+ res_id = translation['res_id']
+ res = pool.get(res_model).read(self.cr, self.uid, res_id, [field_name])
+ if res:
+ result = res[field_name]
+ return result
+
+ def directory_list_for_child(self,nodename,parent=False):
+ pool = pooler.get_pool(self.cr.dbname)
+ where = []
+ if nodename:
+ nodename = self.get_translation(nodename, self.context['lang'])
+ where.append(('name','=',nodename))
+ if (self.object and self.object.type=='directory') or not self.object2:
+ where.append(('parent_id','=',self.object and self.object.id or False))
+ else:
+ where.append(('parent_id','=',False))
+ if self.object:
+ where.append(('ressource_parent_type_id','=',self.object.ressource_type_id.id))
+ else:
+ where.append(('ressource_parent_type_id','=',False))
+
+ ids = pool.get('document.directory').search(self.cr, self.uid, where+[('ressource_id','=',0)])
+ if self.object2:
+ ids += pool.get('document.directory').search(self.cr, self.uid, where+[('ressource_id','=',self.object2.id)])
+ res = pool.get('document.directory').browse(self.cr, self.uid, ids, self.context)
+ return res
+
+ def _child_get(self, nodename=False):
+ if self.type not in ('collection','database'):
+ return []
+ res = self.directory_list_for_child(nodename)
+ result= map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name, x, x.type=='directory' and self.object2 or False, context=self.context, root=self.root), res)
+ if self.type=='database':
+ pool = pooler.get_pool(self.cr.dbname)
+ fobj = pool.get('ir.attachment')
+ vargs = [('parent_id','=',False),('res_id','=',False)]
+ if nodename:
+ vargs.append((fobj._rec_name,'=',nodename))
+ file_ids=fobj.search(self.cr,self.uid,vargs)
+
+ res = fobj.browse(self.cr, self.uid, file_ids, context=self.context)
+ result +=map(lambda x: node_class(self.cr, self.uid, self.path+'/'+eval('x.'+fobj._rec_name), x, False, context=self.context, type='file', root=self.root), res)
+ if self.type=='collection' and self.object.type=="ressource":
+ where = self.object.domain and eval(self.object.domain, {'active_id':self.root, 'uid':self.uid}) or []
+ pool = pooler.get_pool(self.cr.dbname)
+ obj = pool.get(self.object.ressource_type_id.model)
+ _dirname_field = obj._rec_name
+ if len(obj.fields_get(self.cr, self.uid, ['dirname'])):
+ _dirname_field = 'dirname'
+
+ name_for = obj._name.split('.')[-1]
+ if nodename and nodename.find(name_for) == 0 :
+ id = int(nodename.replace(name_for,''))
+ where.append(('id','=',id))
+ elif nodename:
+ if nodename.find('__') :
+ nodename=nodename.replace('__','/')
+ for invalid in INVALID_CHARS:
+ if nodename.find(INVALID_CHARS[invalid]) :
+ nodename=nodename.replace(INVALID_CHARS[invalid],invalid)
+ nodename = self.get_translation(nodename, self.context['lang'])
+ where.append((_dirname_field,'=',nodename))
+
+ if self.object.ressource_tree:
+ if obj._parent_name in obj.fields_get(self.cr,self.uid):
+ where.append((obj._parent_name,'=',self.object2 and self.object2.id or False))
+ ids = obj.search(self.cr, self.uid, where)
+ res = obj.browse(self.cr, self.uid, ids,self.context)
+ result+= map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name.replace('/','__'), self.object, x, context=self.context, root=x.id), res)
+ return result
+ else :
+ if self.object2:
+ return result
+ else:
+ if self.object2:
+ return result
+
+
+ ids = obj.search(self.cr, self.uid, where)
+ res = obj.browse(self.cr, self.uid, ids,self.context)
+ for r in res:
+ if len(obj.fields_get(self.cr, self.uid, [_dirname_field])):
+ r.name = eval('r.'+_dirname_field)
+ else:
+ r.name = False
+ if not r.name:
+ r.name = name_for + '%d'%r.id
+ for invalid in INVALID_CHARS:
+ if r.name.find(invalid) :
+ r.name = r.name.replace(invalid,INVALID_CHARS[invalid])
+ result2 = map(lambda x: node_class(self.cr, self.uid, self.path+'/'+x.name.replace('/','__'), self.object, x, context=self.context, root=x.id), res)
+ if result2:
+ if self.object.ressource_tree:
+ result += result2
+ else:
+ result = result2
+ return result
+
+
+ def path_get(self):
+ path = self.path
+ if self.path[0]=='/':
+ path = self.path[1:]
+ return path
diff --git a/addons/document/odt2txt.py b/addons/document/odt2txt.py
index 9ee9d088b0a..96acc6f7bd1 100644
--- a/addons/document/odt2txt.py
+++ b/addons/document/odt2txt.py
@@ -1,3 +1,4 @@
+#!/usr/bin/python
# -*- coding: utf-8 -*-
##############################################################################
#
diff --git a/addons/document/std_index.py b/addons/document/std_index.py
new file mode 100644
index 00000000000..730e5082a1b
--- /dev/null
+++ b/addons/document/std_index.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2009 Tiny SPRL ().
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+#
+##############################################################################
+
+import StringIO
+import odt2txt
+
+from content_index import indexer, cntIndex
+
+
+def _to_unicode(s):
+ try:
+ return s.decode('utf-8')
+ except UnicodeError:
+ try:
+ return s.decode('latin')
+ except UnicodeError:
+ try:
+ return s.encode('ascii')
+ except UnicodeError:
+ return s
+
+class TxtIndex(indexer):
+ def _getMimeTypes(self):
+ return ['text/plain','text/html','text/diff','text/xml', 'text/*',
+ 'application/xml']
+
+ def _getExtensions(self):
+ return ['.txt', '.py']
+
+ def _doIndexContent(self,content):
+ return content
+
+cntIndex.register(TxtIndex())
+
+class DocIndex(indexer):
+ def _getMimeTypes(self):
+ return [ 'application/ms-word']
+
+ def _getExtensions(self):
+ return ['.doc']
+
+ def _doIndexFile(self,fname):
+ fp = Popen(['antiword',fname], shell=False, stdout=PIPE).stdout
+ return _to_unicode( fp.read())
+
+cntIndex.register(DocIndex())
+
+class PdfIndex(indexer):
+ def _getMimeTypes(self):
+ return [ 'application/pdf']
+
+ def _getExtensions(self):
+ return ['.pdf']
+
+ def _doIndexFile(self,fname):
+ fp = Popen(['pdftotext', '-enc', 'UTF-8', '-nopgbrk', fname, '-'], shell=False, stdout=PIPE).stdout
+ return _to_unicode( fp.read())
+
+cntIndex.register(PdfIndex())
+
+class ImageNoIndex(indexer):
+ def _getMimeTypes(self):
+ return [ 'image/*']
+
+ def _getExtensions(self):
+ #better return no extension, and let 'file' do its magic
+ return []
+ #return ['.png','.jpg','.gif','.jpeg','.bmp','.tiff']
+
+ def _doIndexContent(self,content):
+ return 'image'
+
+
+cntIndex.register(ImageNoIndex())
+
+#class Doc(indexer):
+ #def _getDefMime(self,ext):
+
+#def content_index(content, filename=None, content_type=None):
+ #fname,ext = os.path.splitext(filename)
+ #result = ''
+ #elif ext in ('.xls','.ods','.odt','.odp'):
+ #s = StringIO.StringIO(content)
+ #o = odt2txt.OpenDocumentTextFile(s)
+ #result = _to_unicode(o.toString())
+ #s.close()
+ #elif ext in ('.txt','.py','.patch','.html','.csv','.xml'):
+ #result = content
+ #else:
+ #result = content
+ #return result
+
+#eof
diff --git a/addons/document/test_cindex.py b/addons/document/test_cindex.py
new file mode 100755
index 00000000000..9993ffad425
--- /dev/null
+++ b/addons/document/test_cindex.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+
+import sys
+import os
+import glob
+
+from optparse import OptionParser
+
+parser = OptionParser()
+parser.add_option("-q", "--quiet",
+ action="store_false", dest="verbose", default=True,
+ help="don't print status messages to stdout")
+
+parser.add_option("-C", "--content",
+ action="store_true", dest="docontent", default=False,
+ help="Disect content, rather than the file.")
+
+(options, args) = parser.parse_args()
+
+import content_index, std_index
+
+from content_index import cntIndex
+
+for fname in args:
+ try:
+ if options.docontent:
+ fp = open(fname,'rb')
+ content = fp.read()
+ fp.close()
+ res = cntIndex.doIndex(content,fname,None,None,True)
+ else:
+ res = cntIndex.doIndex(None, fname, None, fname,True)
+
+ if res:
+ print "Result: ", res[0]
+ print res[1]
+ else:
+ print "No result"
+ except Exception,e:
+ import traceback,sys
+ tb_s = reduce(lambda x, y: x+y, traceback.format_exception( sys.exc_type, sys.exc_value, sys.exc_traceback))
+ print "Traceback:",tb_s
+ print "Exception: ",e
+
+
+#eof