Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ fsspec>=2024.5.0
fsspec>=2025.3.0
fsspec[s3]
paramiko
pyftpdlib
python_slugify
1 change: 1 addition & 0 deletions setup/storage_backend_ftp/odoo/addons/storage_backend_ftp
6 changes: 6 additions & 0 deletions setup/storage_backend_ftp/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)
3 changes: 3 additions & 0 deletions storage_backend_ftp/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# storage_backend_ftp

This is the README for storage_backend_ftp.
2 changes: 2 additions & 0 deletions storage_backend_ftp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from . import components
14 changes: 14 additions & 0 deletions storage_backend_ftp/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2021 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Storage Backend FTP",
"summary": "Implement FTP Storage",
"version": "16.0.1.0.0",
"category": "Storage",
"website": "https://github.com/OCA/storage",
"author": " Acsone SA/NV,Odoo Community Association (OCA)",
"license": "LGPL-3",
"external_dependencies": {"python": ["pyftpdlib"]},
"depends": ["storage_backend"],
"data": ["views/backend_storage_view.xml"],
}
1 change: 1 addition & 0 deletions storage_backend_ftp/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import ftp_adapter
167 changes: 167 additions & 0 deletions storage_backend_ftp/components/ftp_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Copyright 2021 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import errno
import io
import logging
import os
import ssl
from contextlib import contextmanager
from io import BytesIO

from odoo.exceptions import UserError

from odoo.addons.component.core import Component

_logger = logging.getLogger(__name__)

try:
import ftplib
except ImportError as err: # pragma: no cover
_logger.debug(err)

FTP_SECURITY_TO_PROTOCOL = {
"tls": ssl.PROTOCOL_TLS,
"tlsv1": ssl.PROTOCOL_TLSv1,
"tlsv1_1": ssl.PROTOCOL_TLSv1_1,
"tlsv1_2": ssl.PROTOCOL_TLSv1_2,
"sslv2": "sslv2 has been deprecated due to security issues",
"sslv23": ssl.PROTOCOL_SSLv23,
"sslv3": "sslv3 has been deprecated due to security issues",
}


def ftp_mkdirs(client, path):
try:
client.mkd(path)
except IOError as e:
if e.errno == errno.ENOENT and path:
ftp_mkdirs(client, os.path.dirname(path))
client.mkd(path)
else:
raise # pragma: no cover


class ImplicitFTPTLS(ftplib.FTP_TLS):
"""FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._sock = None

@property
def sock(self):
"""Return the socket."""
return self._sock

@sock.setter
def sock(self, value):
"""When modifying the socket, ensure that it is ssl wrapped."""
if value is not None and not isinstance(value, ssl.SSLSocket):
value = self.context.wrap_socket(value)
self._sock = value


@contextmanager
def ftp(backend):
security = None
prot_p = False
if backend.ftp_encryption in ["ftp", "tls", "tls_explicit"]:
if backend.ftp_encryption == "ftp":
ftp = ftplib.FTP(timeout=False)
elif backend.ftp_encryption == "tls":
ftp = ImplicitFTPTLS()
# Due to a bug into between ftplib and ssl, this part (about ssl) might not work!
# https://bugs.python.org/issue31727
security = FTP_SECURITY_TO_PROTOCOL.get(backend.ftp_security, None)
prot_p = True
if isinstance(security, str):
raise UserError(security)
elif backend.ftp_encryption == "tls_explicit":
ftp = ftplib.FTP_TLS()
prot_p = True
with ftp as client:
if security:
client.ssl_version = security
client.connect(host=backend.ftp_server, port=backend.ftp_port)
client.login(backend.ftp_login, backend.ftp_password)
if prot_p:
client.prot_p()
if backend.ftp_passive:
client.set_pasv(True)
yield client


class FTPStorageBackendAdapter(Component):
_name = "ftp.adapter"
_inherit = "base.storage.adapter"
_usage = "ftp"

def add(self, relative_path, data, **kwargs):
with ftp(self.collection) as client:
full_path = self._fullpath(relative_path)
dirname = os.path.dirname(full_path)
if dirname:
try:
client.cwd(dirname)
except IOError as e:
if e.errno == errno.ENOENT:
ftp_mkdirs(client, dirname)
else:
raise # pragma: no cover
with io.BytesIO(data) as tmp_file:
try:
client.storbinary("STOR " + full_path, tmp_file)
except ftplib.Error as e:
raise ValueError(repr(e)) from None
except OSError as e:
raise ValueError(repr(e)) from None

def get(self, relative_path, **kwargs):
full_path = self._fullpath(relative_path)
with ftp(self.collection) as client, BytesIO() as buff:
try:
client.retrbinary("RETR " + full_path, buff.write)
data = buff.getvalue()
except ftplib.Error as e:
raise FileNotFoundError(repr(e)) from None
return data

def list(self, relative_path):
full_path = self._fullpath(relative_path)
with ftp(self.collection) as client:
try:
return client.nlst(full_path)
except IOError as e:
if e.errno == errno.ENOENT:
# The path do not exist return an empty list
return []
else:
raise # pragma: no cover

def move_files(self, files, destination_path):
_logger.debug("mv %s %s", files, destination_path)
with ftp(self.collection) as client:
for ftp_file in files:
dest_file_path = os.path.join(
destination_path, os.path.basename(ftp_file)
)
# Remove existing file at the destination path (an error is raised
# otherwise)
result = []
try:
result = client.nlst(dest_file_path)
except ftplib.Error:
_logger.debug("destination %s is free", dest_file_path)
if result:
client.delete(dest_file_path)
# Move the file
client.rename(ftp_file, dest_file_path)

def delete(self, relative_path):
full_path = self._fullpath(relative_path)
with ftp(self.collection) as client:
return client.delete(full_path)

def validate_config(self):
with ftp(self.collection) as client:
client.getwelcome()
64 changes: 64 additions & 0 deletions storage_backend_ftp/i18n/de.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
msgid ""
msgstr ""
"Project-Id-Version: ametras-xsolutions\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: German\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Crowdin-Project: ametras-xsolutions\n"
"X-Crowdin-Project-ID: 530964\n"
"X-Crowdin-Language: de\n"
"X-Crowdin-File: /[AmetrasIntelligence.oca-migrated] 16.0/storage_backend_ftp/i18n/storage_backend_ftp.pot\n"
"X-Crowdin-File-ID: 21438\n"
"Language: de_DE\n"
"PO-Revision-Date: 2025-02-18 16:30\n"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__ftp
#: model_terms:ir.ui.view,arch_db:storage_backend_ftp.storage_backend_view_form
msgid "FTP"
msgstr "FTP"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__tls
msgid "Implicit FTP over TLS"
msgstr "Implizites FTP über TLS"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__none
msgid "None"
msgstr "Ohne"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv2
msgid "SSLv2"
msgstr "SSLv2"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv23
msgid "SSLv23"
msgstr "SSLv23"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv3
msgid "SSLv3"
msgstr "SSLv3"

#. module: storage_backend_ftp
#: model:ir.model,name:storage_backend_ftp.model_storage_backend
msgid "Storage Backend"
msgstr "Ablage-Backend"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1
msgid "TLS"
msgstr "TLS"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1_1
msgid "TLSv1_1"
msgstr "TLSv1_1"

64 changes: 64 additions & 0 deletions storage_backend_ftp/i18n/fr.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
msgid ""
msgstr ""
"Project-Id-Version: ametras-xsolutions\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: French\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Crowdin-Project: ametras-xsolutions\n"
"X-Crowdin-Project-ID: 530964\n"
"X-Crowdin-Language: fr\n"
"X-Crowdin-File: /[AmetrasIntelligence.oca-migrated] 16.0/storage_backend_ftp/i18n/storage_backend_ftp.pot\n"
"X-Crowdin-File-ID: 21438\n"
"Language: fr_FR\n"
"PO-Revision-Date: 2024-09-25 12:08\n"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__ftp
#: model_terms:ir.ui.view,arch_db:storage_backend_ftp.storage_backend_view_form
msgid "FTP"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__tls
msgid "Implicit FTP over TLS"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__none
msgid "None"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv2
msgid "SSLv2"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv23
msgid "SSLv23"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv3
msgid "SSLv3"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model,name:storage_backend_ftp.model_storage_backend
msgid "Storage Backend"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1
msgid "TLS"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1_1
msgid "TLSv1_1"
msgstr ""

60 changes: 60 additions & 0 deletions storage_backend_ftp/i18n/storage_backend_ftp.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * storage_backend_ftp
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__ftp
#: model_terms:ir.ui.view,arch_db:storage_backend_ftp.storage_backend_view_form
msgid "FTP"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__tls
msgid "Implicit FTP over TLS"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__none
msgid "None"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv2
msgid "SSLv2"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv23
msgid "SSLv23"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv3
msgid "SSLv3"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model,name:storage_backend_ftp.model_storage_backend
msgid "Storage Backend"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1
msgid "TLS"
msgstr ""

#. module: storage_backend_ftp
#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1_1
msgid "TLSv1_1"
msgstr ""
Loading
Loading