Merge remote-tracking branch 'ir_attachment_exchange/master' into 8.0-external_file_sync

This commit is contained in:
Valentin Chemiere 2015-03-11 16:47:10 +01:00
commit 3fc73f7fd1
21 changed files with 1104 additions and 0 deletions

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2014 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
from . import attachment
from . import location
from . import task
from . import tasks

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2015 Akretion (http://www.akretion.com).
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
{
'name': 'external_file_location',
'version': '0.0.1',
'author': 'Akretion',
'website': 'www.akretion.com',
'license': 'AGPL-3',
'category': 'Generic Modules',
'description': """
File exchange system with multiple protocol (SFTP, FTP, Filestore)
""",
'depends': [
'base',
'ir_attachment_metadata',
],
'data': [
'attachment_view.xml',
'menu.xml',
'location_view.xml',
'task_view.xml',
'cron.xml',
],
'installable': True,
'application': True,
}

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from base64 import b64encode
class AbstractTask(object):
def create_file(self, filename, data):
ir_attachment_id = self.env['ir.attachment'].create(
{
'name': filename,
'datas': b64encode(data),
'datas_fname': filename,
'task_id': self.task.id,
'location_id': self.task.location_id.id,
'external_hash': self.ext_hash
}
)
return ir_attachment_id

View File

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2014 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
from openerp import models, fields
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
sync_date = fields.Datetime()
state = fields.Selection([
('pending', 'Pending'),
('failed', 'Failed'),
('done', 'Done'),
], readonly=False, required=True, default='pending')
state_message = fields.Text()
task_id = fields.Many2one('external.file.task', string='Task')
location_id = fields.Many2one('external.file.location', string='Location',
related='task_id.location_id', store=True
)

View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_attachment_improved_form" model="ir.ui.view">
<field name="model">ir.attachment</field>
<field name="inherit_id" ref="base.view_attachment_form" />
<field name="arch" type="xml">
<field name="url" position="after">
<field name="sync_date"/>
<field name="state"/>
<field name="state_message"/>
<field name="task_id"/>
<field name="location_id"/>
</field>
</field>
</record>
<record id="view_external_attachment_tree" model="ir.ui.view">
<field name="model">ir.attachment</field>
<field name="arch" type="xml">
<tree string="Attachments" >
<field name="name"/>
<field name="datas_fname"/>
<field name="task_id"/>
<field name="location_id"/>
<field name="type"/>
<field name="create_date"/>
<field name="state"/>
</tree>
</field>
</record>
<record id="view_external_attachment_search" model="ir.ui.view">
<field name="model">ir.attachment</field>
<field name="arch" type="xml">
<search string="Attachments">
<field name="name" filter_domain="['|', ('name','ilike',self), ('datas_fname','ilike',self)]" string="Attachment"/>
<field name="create_date"/>
<filter icon="terp-stage"
string="URL"
domain="[('type','=','url')]"/>
<filter icon="terp-stock_align_left_24"
string="Binary"
domain="[('type','=','binary')]"/>
<separator/>
<filter name="my_documents_filter"
string="My Document(s)"
icon="terp-personal"
domain="[('create_uid','=',uid)]"
help="Filter on my documents"/>
<field name="create_uid"/>
<field name="type"/>
<filter string="Pending" domain="[('state', '=', 'pending')]"/>
<filter string="Failed" domain="[('state', '=', 'failed')]"/>
<filter string="Done" domain="[('state', '=', 'done')]"/>
<group expand="0" string="Group By">
<filter string="Owner" icon="terp-personal" domain="[]" context="{'group_by':'create_uid'}"/>
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'type'}" groups="base.group_no_one"/>
<filter string="Company" icon="terp-gtk-home" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
<filter string="Creation Month" icon="terp-go-month" domain="[]" context="{'group_by':'create_date'}"/>
<filter string="State" domain="[]" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
<record id="action_attachment" model="ir.actions.act_window">
<field name="name">Attachments</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">ir.attachment</field>
<field name="view_type">form</field>
<field name="view_mode">kanban,tree,form</field>
<field name="view_id" eval="False"/>
<field name="domain">[('task_id', '!=', False)]</field>
<field name="search_view_id" ref="view_external_attachment_search"/>
</record>
<record id="ir_attachment_view1" model="ir.actions.act_window.view">
<field eval="20" name="sequence"/>
<field name="view_mode">kanban</field>
<field name="view_id" ref="mail.view_document_file_kanban"/>
<field name="act_window_id" ref="action_attachment"/>
</record>
<record id="ir_attachment_view2" model="ir.actions.act_window.view">
<field eval="10" name="sequence"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_external_attachment_tree"/>
<field name="act_window_id" ref="action_attachment"/>
</record>
<menuitem id="menu_ir_attachment"
parent="menu_file_exchange"
sequence="20"
action="action_attachment"/>
</data>
</openerp>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record model="ir.cron" id="cronjob_run_exchange_tasks">
<field name='name'>Run file exchange tasks</field>
<field name='interval_number'>30</field>
<field name='interval_type'>minutes</field>
<field name="numbercall">-1</field>
<field name="active">True</field>
<field name="doall" eval="False" />
<field name="model">external.file.task</field>
<field name="function">_run</field>
<field name="args">([])</field>
</record>
</data>
</openerp>

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Joel Grand-Guillaume
# Copyright 2011-2012 Camptocamp SA
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
def itersubclasses(cls, _seen=None):
"""
itersubclasses(cls)
Generator over all subclasses of a given class, in depth first order.
>>> list(itersubclasses(int)) == [bool]
True
>>> class A(object): pass
>>> class B(A): pass
>>> class C(A): pass
>>> class D(B,C): pass
>>> class E(D): pass
>>>
>>> for cls in itersubclasses(A):
... print(cls.__name__)
B
D
E
C
>>> # get ALL (new-style) classes currently defined
>>> [cls.__name__ for cls in itersubclasses(object)] #doctest: +ELLIPSIS
['type', ...'tuple', ...]
"""
#import pdb; pdb.set_trace()
if not isinstance(cls, type):
raise TypeError('itersubclasses must be called with '
'new-style classes, not %.100r' % cls
)
if _seen is None:
_seen = set()
try:
subs = cls.__subclasses__()
except TypeError: # fails only when cls is type
subs = cls.__subclasses__(cls)
for sub in subs:
if sub not in _seen:
_seen.add(sub)
yield sub
for sub in itersubclasses(sub, _seen):
yield sub

View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2015 Akretion (http://www.akretion.com).
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
from openerp import models, fields
from abstract_task import AbstractTask
from helper import itersubclasses
class Location(models.Model):
_name = 'external.file.location'
_description = 'Description'
name = fields.Char(string='Name')
protocol = fields.Selection(selection='_get_protocol')
address = fields.Char(string='Address')
port = fields.Integer()
login = fields.Char()
password = fields.Char()
task_ids = fields.One2many('external.file.task', 'location_id')
def _get_protocol(self):
res = []
for cls in itersubclasses(AbstractTask):
if not cls._synchronize_type:
cls_info = (cls._key, cls._name)
res.append(cls_info)
return res

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_location_form" model="ir.ui.view">
<field name="model">external.file.location</field>
<field name="arch" type="xml">
<form string="Location" version="7.0">
<sheet>
<group col="4">
<div class="oe_title" style="width: 390px;" colspan="4">
<label class="oe_edit_only" for="name" string="Location Name"/>
<h1><field name="name" class="oe_inline"/></h1>
</div>
<newline/>
<field name="protocol" colspan="2"/>
<newline/>
<field name="address" colspan="2"/>
<field name="port" colspan="2"/>
<field name="login" colspan="2"/>
<field name="password" password="1" colspan="2"/>
<separator string="Tasks" colspan="4"/>
<field name="task_ids" colspan="4" nolabel="1">
<tree>
<field name="name"/>
<field name="method"/>
<field name="filename"/>
<field name="filepath"/>
<button name="run" type="object" string="Run" icon="gtk-execute"/>
</tree>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_location_tree" model="ir.ui.view">
<field name="model">external.file.location</field>
<field name="arch" type="xml">
<tree string="Location">
<field name="name" select="1"/>
<field name="protocol"/>
<field name="address"/>
<field name="login"/>
</tree>
</field>
</record>
<record id="action_location" model="ir.actions.act_window">
<field name="name">Locations</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">external.file.location</field>
<field name="view_type">form</field>
<field name="view_id" eval="False"/>
</record>
<menuitem id="menu_ir_location"
parent="menu_file_exchange"
sequence="20"
action="action_location"/>
</data>
</openerp>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<menuitem id="menu_file_exchange"
parent="base.menu_administration"
sequence="20"
name="File exchange"
/>
</data>
</openerp>

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2015 Akretion (http://www.akretion.com).
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
from openerp import models, fields, api
from helper import itersubclasses
from abstract_task import AbstractTask
class Task(models.Model):
_name = 'external.file.task'
_description = 'Description'
name = fields.Char()
method = fields.Selection(selection='_get_method')
method_type = fields.Char()
filename = fields.Char()
filepath = fields.Char()
location_id = fields.Many2one('external.file.location', string='Location')
attachment_ids = fields.One2many('ir.attachment', 'task_id',
string='Attachment')
delete_file = fields.Boolean(string='Delete file')
move_file = fields.Boolean(string='Move file')
move_path = fields.Char(string='Move path')
def _get_method(self):
res = []
for cls in itersubclasses(AbstractTask):
if cls._synchronize_type:
cls_info = (cls._key + '_' + cls._synchronize_type,
cls._name + ' ' + cls._synchronize_type)
res.append(cls_info)
return res
@api.onchange('method')
def onchage_method(self):
if 'import' in self.method:
self.method_type = 'import'
elif 'export' in self.method:
self.method_type = 'export'
@api.model
def _run(self, domain=None):
if not domain:
domain = []
tasks = self.env['external.file.task'].search(domain)
tasks.run()
@api.one
def run(self):
for cls in itersubclasses(AbstractTask):
if cls._synchronize_type and \
cls._key + '_' + cls._synchronize_type == self.method:
method_class = cls
config = {
'host': self.location_id.address,
'user': self.location_id.login,
'pwd': self.location_id.password,
'port': self.location_id.port,
'allow_dir_creation': False,
'file_name': self.filename,
'path': self.filepath,
'attachment_ids': self.attachment_ids,
'task': self,
'move_path': self.move_path,
'delete_file': self.delete_file,
'move_file': self.move_file,
}
conn = method_class(self.env, config)
conn.run()

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_task_form" model="ir.ui.view">
<field name="model">external.file.task</field>
<field name="arch" type="xml">
<form string="Tasks" version="7.0">
<sheet>
<field name="method_type" invisible="1"/>
<group col="4">
<div class="oe_title" style="width: 390px;" colspan="4">
<label class="oe_edit_only" for="name" string="Product Name"/>
<h1><field name="name" class="oe_inline"/></h1>
</div>
<field name="method" colspan="2"/>
<field name="location_id" colspan="2"/>
<field name="filename" colspan="4" attrs="{'invisible':[('method_type','!=','import')]}"/>
<field name="filepath" colspan="4" />
<field name="move_file" colspan="2" attrs="{'invisible':[('method_type','!=','import')]}"/>
<field name="move_path" colspan="2" attrs="{'invisible':['|', ('move_file','=',False), ('method_type','!=','import')]}"/>
<field name="delete_file" colspan="2" attrs="{'invisible':[('method_type','!=','import')]}"/>
<group attrs="{'invisible':[('method_type','!=','export')]}" colspan="4">
<separator string="Attachments" colspan="4" />
<field name="attachment_ids" colspan="4" nolabel="1"/>
</group>
<button name="run" type="object" string="Run" icon="gtk-execute"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_task_tree" model="ir.ui.view">
<field name="model">external.file.task</field>
<field name="arch" type="xml">
<tree string="Tasks" >
<field name="name" select="1"/>
<field name="method"/>
<field name="filename"/>
<field name="filepath"/>
<button name="run" type="object" string="Run" icon="gtk-execute"/>
</tree>
</field>
</record>
<record id="action_task" model="ir.actions.act_window">
<field name="name">Tasks</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">external.file.task</field>
<field name="view_type">form</field>
<field name="view_id" eval="False"/>
</record>
<menuitem id="menu_external_file_task"
parent="menu_file_exchange"
sequence="20"
action="action_task"/>
</data>
</openerp>

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2015 Akretion (http://www.akretion.com).
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
from . import ftp

View File

@ -0,0 +1,73 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from ..backend import AbstractTask
import sys
import os
from tempfile import TemporaryFile
class FileStore(AbstractTask):
_key = "filestore"
_name = "Filestore"
_synchronize_type = None
def __init__(self, config):
# super(FilestoreConnection, self).__init__(host, user, pwd, port, allow_dir_creation)
self.host = config.get('host', '')
self.user = config.get('user', '')
self.pwd = config.get('pwd', '')
self.port = config.get('port', '')
self.allow_dir_creation = config.get('allow_dir_creation', '')
self.filename = config.get('filename', '')
self.path = config.get('path', '')
def connect(self):
return NotImplemented
def close(self):
return NotImplemented
def get(self):
if self.path:
filepath = "{}/{}".format(self.path, self.filename)
else:
filepath = self.filename
return open(filepath, 'r+b')
def put(self):
if self.path:
filepath = "{}/{}".format(self.path, self.filename)
else:
filepath = self.filename
output = open(filepath, 'w+b')
return True
def search(self):
if self.path:
filepath = "{}/{}".format(self.path, self.filename)
else:
filepath = self.filename
connection_list_result = os.listdir(filepath)
return [x for x in connection_list_result if filename in x]
class ImportFileStore(FileStore):
_synchronize_type = "import"
def run():
self.connect()
file = self.get(self.filename)
self.close()
return file
class ExportFileStore(FileStore):
_synchronize_type = "export"
def run():
self.connect()
self.put(self.filename)
self.close()

View File

@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (C) 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
##############################################################################
from ..abstract_task import AbstractTask
from base64 import b64decode
import ftputil
import ftputil.session
from ftputil.error import FTPIOError
import logging
from base64 import b64encode
import hashlib
_logger = logging.getLogger(__name__)
class FtpTask(AbstractTask):
_key = 'ftp'
_name = 'FTP'
_synchronize_type = None
def __init__(self, env, config):
self.env = env
self.host = config.get('host', '')
self.user = config.get('user', '')
self.pwd = config.get('pwd', '')
self.port = config.get('port', '')
self.allow_dir_creation = config.get('allow_dir_creation', '')
self.file_name = config.get('file_name', '')
self.path = config.get('path', '.')
self.move_path = config.get('move_path', '')
self.move_file = config.get('move_file', False)
self.delete_file = config.get('delete_file', False)
self.attachment_ids = config.get('attachment_ids', False)
self.task = config.get('task', False)
self.ext_hash = False
class FtpImportTask(FtpTask):
"""FTP Configuration options:
- host, user, password, port
- download_directory: directory on the FTP server where files are
downloaded from
- move_directory: If present, files will be moved to this directory
on the FTP server after download.
- delete_files: If true, files will be deleted on the FTP server
after download.
"""
_synchronize_type = 'import'
def _handle_new_source(self, ftp_conn, download_directory, file_name,
move_directory):
"""open and read given file into create_file method,
move file if move_directory is given"""
with ftp_conn.open(self._source_name(download_directory, file_name),
"rb") as fileobj:
data = fileobj.read()
return self.create_file(file_name, data)
def _source_name(self, download_directory, file_name):
"""helper to get the full name"""
return download_directory + '/' + file_name
def _move_file(self, ftp_conn, source, target):
"""Moves a file on the FTP server"""
_logger.info('Moving file %s %s' % (source, target))
ftp_conn.rename(source, target)
def _delete_file(self, ftp_conn, source):
"""Deletes a file from the FTP server"""
_logger.info('Deleting file %s' % source)
ftp_conn.remove(source)
@staticmethod
def _get_hash(file_name, ftp_conn):
with ftp_conn.open(file_name, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
def run(self):
port_session_factory = ftputil.session.session_factory(
port=self.port)
with ftputil.FTPHost(self.host, self.user,
self.pwd,
session_factory=port_session_factory) as ftp_conn:
path = self.path or '.'
file_list = ftp_conn.listdir(path)
downloaded_files = []
for ftpfile in file_list:
source_name = self._source_name(path, self.file_name)
if ftp_conn.path.isfile(source_name) and \
ftpfile == self.file_name:
self.ext_hash = self._get_hash(source_name, ftp_conn)
self._handle_new_source(
ftp_conn,
path,
self.file_name,
self.move_path)
downloaded_files.append(self.file_name)
# Move/delete files only after all files have been processed.
if self.delete_file:
for ftpfile in downloaded_files:
self._delete_file(ftp_conn,
self._source_name(path,
ftpfile))
elif self.move_path:
if not ftp_conn.path.exists(self.move_path):
ftp_conn.mkdir(self.move_path)
for ftpfile in downloaded_files:
self._move_file(
ftp_conn,
self._source_name(path, ftpfile),
self._source_name(self.move_path, ftpfile))
class FtpExportTask(FtpTask):
"""FTP Configuration options:
- host, user, password, port
- upload_directory: directory on the FTP server where files are
uploaded to
"""
_synchronize_type = 'export'
def _handle_existing_target(self, ftp_conn, target_name, filedata):
raise Exception("%s already exists" % target_name)
def _handle_new_target(self, ftp_conn, target_name, filedata):
try:
with ftp_conn.open(target_name, mode='wb') as fileobj:
fileobj.write(filedata)
_logger.info('wrote %s, size %d', target_name, len(filedata))
self.attachment_id.state = 'done'
self.attachment_id.state_message = ''
except FTPIOError:
self.attachment_id.state = 'failed'
self.attachment_id.state_message = (
'The directory doesn\'t exist or had insufficient rights')
def _target_name(self, ftp_conn, upload_directory, filename):
return upload_directory + '/' + filename
def _upload_file(self, host, port, user, pwd, path, filename, filedata):
upload_directory = path or '.'
port_session_factory = ftputil.session.session_factory(port=port)
with ftputil.FTPHost(host, user, pwd,
session_factory=port_session_factory) as ftp_conn:
target_name = self._target_name(ftp_conn,
upload_directory,
filename)
if ftp_conn.path.isfile(target_name):
self._handle_existing_target(ftp_conn, target_name, filedata)
else:
self._handle_new_target(ftp_conn, target_name, filedata)
def run(self, async=True):
for attachment in self.attachment_ids:
if attachment.state in ('pending', 'failed'):
self.attachment_id = attachment
self._upload_file(self.host, self.port, self.user, self.pwd,
self.path,
attachment.datas_fname,
b64decode(attachment.datas))

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
from tempfile import TemporaryFile
from ftplib import FTP
class FTPConnection(object):
def __init__(self, host, user, pwd, port=None, allow_dir_creation=False):
super(FTPConnection, self).__init__(host, user, pwd, port, allow_dir_creation)
if not port:
self.port = 21
self.protocol = "FTP"
def connect(self):
self.connection = FTP(self.location, self.port)
self.connection.login(self.user, self.pwd)
def close(self):
self.connection.close()
def get(self, filename, path=None):
if path:
filepath = "{}/{}".format(path, filename)
else:
filepath = filename
outfile = TemporaryFile('w+b')
self.connection.retrbinary('RETR ' + filepath, outfile.write)
return outfile
def put(self, fileobject, filename, path=None):
if path:
filepath = "{}/{}".format(path, filename)
else:
filepath = filename
self.connection.storbinary('STOR ' + filepath, fileobject)
return True
def search(self, filename, path=None):
if path:
filepath = "{}/{}".format(path, filename)
else:
filepath = filename
connection_list_result = self.connection.nlst()
return [x for x in connection_list_result if filename in x]
def move(self, filename, oldpath, newpath):
self.connection.rename(
os.path.join(oldpath, filename),
os.path.join(newpath, filename)
)
def rename(self, oldfilename, newfilename, path=None):
return NotImplemented

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import paramiko
import os
from tempfile import TemporaryFile
class SFTPConnection(AbstractConnection):
def __init__(self, host, user, pwd, port=None, allow_dir_creation=False):
super(SFTPConnection, self).__init__(host, user, pwd, port, allow_dir_creation)
if not port:
self.port = 22
self.protocol = "STFP"
def connect(self):
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(self.host, self.port, self.user, self.pwd, compress=True)
self.connection = self.ssh.open_sftp()
def close(self):
self.connection.close()
def get(self, filename, path=None):
if path:
remotefile = "{}/{}".format(path, filename)
else:
remotefile = filename
localfile = filename
newfile = open(filename, 'w')
self.connection.getfo(remotefile, newfile)
return newfile
def put(self, fileobject, filename, path=None):
if path:
remotefile = "{}/{}".format(path, filename)
else:
remotefile = filename
if self.allow_dir_creation:
self.connection.mkdirs(path)
oldfile = open(fileobj, 'r')
self.connection.putfo(oldfile, remotefile)
def search(self, filename, path=None):
if path:
self.connection.chdir(path)
file_list = self.connection.listdir()
return [x for x in file_list if filename in x]
def move(self, filename, oldpath, newpath):
self.connection.rename(os.path.join(oldpath, filename), os.path.join(newpath, filename))
def rename(self, oldfilename, newfilename, path=None):
if not path:
path = ''
self.connection.rename(os.path.join(path, oldfilename), os.path.join(path, newfilename))

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2015 Akretion (http://www.akretion.com).
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
from . import attachment

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2015 Akretion (http://www.akretion.com).
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
{'name': 'ir_attachment_metadata',
'version': '0.0.1',
'author': 'Akretion',
'website': 'www.akretion.com',
'license': 'AGPL-3',
'category': 'Generic Modules',
'description': """
Add some useful field to ir.attachment object like:
internal and external hash for coherence verification
""",
'depends': [
'base',
],
'data': [
'attachment_view.xml',
],
'installable': True,
'application': False,
}

View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
###############################################################################
#
# Module for OpenERP
# Copyright (C) 2015 Akretion (http://www.akretion.com).
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
#
# 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 <http://www.gnu.org/licenses/>.
#
###############################################################################
from openerp import models, fields, api, _
from openerp.exceptions import Warning
import hashlib
from base64 import b64decode
class IrAttachment(models.Model):
_inherit = 'ir.attachment'
internal_hash = fields.Char(store=True, compute='_compute_hash')
external_hash = fields.Char()
@api.depends('datas', 'external_hash')
def _compute_hash(self):
if self.datas:
self.internal_hash = hashlib.md5(b64decode(self.datas)).hexdigest()
if self.external_hash and self.internal_hash != self.external_hash:
raise Warning(_('File corrupted'),
_("Something was wrong with the retreived file, "
"please relaunch the task."))

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_attachment_improved_form" model="ir.ui.view">
<field name="model">ir.attachment</field>
<field name="inherit_id" ref="base.view_attachment_form" />
<field name="arch" type="xml">
<field name="url" position="after">
<field name="internal_hash"/>
<field name="external_hash"/>
</field>
</field>
</record>
</data>
</openerp>