diff --git a/external_file_location/README.rst b/external_file_location/README.rst
index 3b425a69..94313350 100644
--- a/external_file_location/README.rst
+++ b/external_file_location/README.rst
@@ -12,6 +12,7 @@ Installation
To install this module, you need to:
* FTPUtil python module
+* Paramiko python module
Usage
=====
@@ -29,7 +30,6 @@ For further information, please visit:
Known issues / Roadmap
======================
-* add SFTP protocol
* add FILESTORE protocol
Credits
diff --git a/external_file_location/abstract_task.py b/external_file_location/abstract_task.py
index ce98bd76..b1016fac 100644
--- a/external_file_location/abstract_task.py
+++ b/external_file_location/abstract_task.py
@@ -7,7 +7,7 @@ from base64 import b64encode
class AbstractTask(object):
def create_file(self, filename, data):
- ir_attachment_id = self.env['ir.attachment'].create({
+ ir_attachment_id = self.env['ir.attachment.metadata'].create({
'name': filename,
'datas': b64encode(data),
'datas_fname': filename,
diff --git a/external_file_location/location_view.xml b/external_file_location/location_view.xml
index db5815d8..827baa7b 100644
--- a/external_file_location/location_view.xml
+++ b/external_file_location/location_view.xml
@@ -20,7 +20,7 @@
-
+
diff --git a/external_file_location/task.py b/external_file_location/task.py
index befbb093..ffadedb5 100644
--- a/external_file_location/task.py
+++ b/external_file_location/task.py
@@ -52,7 +52,8 @@ class Task(models.Model):
def _get_method(self):
res = []
for cls in itersubclasses(AbstractTask):
- if cls._synchronize_type:
+ if cls._synchronize_type \
+ and cls._key == self._context.get('protocol'):
cls_info = (cls._key + '_' + cls._synchronize_type,
cls._name + ' ' + cls._synchronize_type)
res.append(cls_info)
diff --git a/external_file_location/task_view.xml b/external_file_location/task_view.xml
index 780b87d0..648bfdbd 100644
--- a/external_file_location/task_view.xml
+++ b/external_file_location/task_view.xml
@@ -20,7 +20,7 @@
-
+
diff --git a/external_file_location/tasks/__init__.py b/external_file_location/tasks/__init__.py
index d3e1509a..97071ed0 100644
--- a/external_file_location/tasks/__init__.py
+++ b/external_file_location/tasks/__init__.py
@@ -21,3 +21,4 @@
###############################################################################
from . import ftp
+from . import sftp
diff --git a/external_file_location/tasks/sftp.py b/external_file_location/tasks/sftp.py
new file mode 100644
index 00000000..710ac18a
--- /dev/null
+++ b/external_file_location/tasks/sftp.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Copyright (C) 2014 initOS GmbH & Co. KG ().
+# @author Valentin CHEMIERE
+#
+# 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 .
+#
+##############################################################################
+
+from ..abstract_task import AbstractTask
+from base64 import b64decode
+import paramiko
+import logging
+import os
+_logger = logging.getLogger(__name__)
+
+
+class SftpTask(AbstractTask):
+
+ _key = 'sftp'
+ _name = 'SFTP'
+ _synchronize_type = None
+ _default_port = 22
+
+ 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') or '.'
+ self.move_path = config.get('move_path', '')
+ self.after_import = config.get('after_import', False)
+ self.attachment_ids = config.get('attachment_ids', False)
+ self.task = config.get('task', False)
+ self.ext_hash = False
+ self.md5_check = config.get('md5_check', False)
+
+
+class SftpImportTask(SftpTask):
+ """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 os.path.join(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)
+ if self.md5_check:
+ ftp_conn.rename(source + '.md5', target + '.md5')
+
+ def _delete_file(self, ftp_conn, source):
+ """Deletes a file from the FTP server"""
+ _logger.info('Deleting file %s' % source)
+ ftp_conn.remove(source)
+ if self.md5_check:
+ ftp_conn.remove(source + '.md5')
+
+ def _get_hash(self, file_name, ftp_conn):
+ hash_file_name = file_name + '.md5'
+ with ftp_conn.open(hash_file_name, 'rb') as f:
+ return f.read().rstrip('\r\n')
+
+ def _get_files(self, conn, path):
+ process_files = []
+ files_list = conn.listdir(path)
+ for file in files_list:
+ if file == self.file_name:
+ source_name = self._source_name(self.path, self.file_name)
+ process_files.append((file, source_name))
+ return process_files
+
+ def _process_file(self, conn, file_to_process):
+ if self.md5_check:
+ self.ext_hash = self._get_hash(file_to_process[1], conn)
+ self._handle_new_source(
+ conn,
+ self.path,
+ self.file_name,
+ self.move_path)
+
+ # Move/delete files only after all files have been processed.
+ if self.after_import == 'delete':
+ self._delete_file(conn, file_to_process[1])
+ elif self.after_import == 'move':
+ if not conn.path.exists(self.move_path):
+ conn.mkdir(self.move_path)
+ self._move_file(
+ conn,
+ file_to_process[1],
+ self._source_name(self.move_path, file_to_process[0]))
+
+ def run(self):
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh_conn = ssh.connect(self.host, self.port,
+ username=self.user, password=self.pwd)
+ ftp_conn = ssh.open_sftp()
+ path = self.path or '.'
+ files_to_process = self._get_files(ftp_conn, path)
+ for file_to_process in files_to_process:
+ self._process_file(ftp_conn, file_to_process)
+
+
+class SftpExportTask(SftpTask):
+ """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 IOError:
+ 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 os.path.join(upload_directory, filename)
+
+ def _upload_file(self, host, port, user, pwd, path, filename, filedata):
+ upload_directory = path or '.'
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+ ssh.connect(self.host, self.port,
+ username=self.user, password=self.pwd)
+ ftp_conn = ssh.open_sftp()
+ target_name = self._target_name(ftp_conn,
+ upload_directory,
+ filename)
+ if filename in ftp_conn.listdir(upload_directory):
+ 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))