Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
547888c
[13.0][ADD] storage_backend_ftp
acsonefho Jul 10, 2021
4c2cb0e
[FIX] storage_backend_ftp: fix list and get
LoisRForgeFlow Aug 5, 2021
35ec67a
[IMP] storage_backend_ftp: fix tests and do not support unsecure prot…
LoisRForgeFlow Oct 26, 2021
2d0d349
[UPD] Update storage_backend_ftp.pot
oca-travis Oct 27, 2021
ae575e7
[UPD] README.rst
OCA-git-bot Oct 27, 2021
74d1db3
[ADD] icon.png
OCA-git-bot Oct 27, 2021
3b0ddb4
storage_backend_ftp 13.0.1.0.1
OCA-git-bot Oct 27, 2021
01d7371
Migrate storage_backend_ftp to 14
florian-dacosta Dec 8, 2021
e81972d
[UPD] Update storage_backend_ftp.pot
oca-travis Dec 11, 2021
4acf817
[UPD] README.rst
OCA-git-bot Dec 11, 2021
ee99ddd
storage_backend_ftp 14.0.1.0.1
OCA-git-bot Dec 11, 2021
f3bb66b
[IMP] storage_backend_ftp: Fix connection issue for implicit FTP over…
JasminSForgeFlow Dec 14, 2021
8e36522
[MIG] storage_backend_ftp: Migration to 15.0
JasminSForgeFlow Jan 31, 2022
93516e8
[UPD] Update storage_backend_ftp.pot
Mar 22, 2022
401b3d1
[UPD] README.rst
OCA-git-bot Mar 22, 2022
1bf3a07
[FIX] storage_backend_ftp: use full path in ``move_files()``
SilvioC2C Apr 6, 2022
d068945
storage_backend_ftp 15.0.1.0.1
OCA-git-bot Apr 6, 2022
80deaf6
[IMP] storage_backend_ftp: Implement Explicit FTP over TLS
JasminSForgeFlow Apr 8, 2022
ccadfe7
[UPD] Update storage_backend_ftp.pot
Mar 27, 2023
9e8f049
storage_backend_ftp 15.0.1.1.0
OCA-git-bot Mar 27, 2023
0bb9d64
[UPD] README.rst
OCA-git-bot Sep 3, 2023
56ca782
[MIG] storage_backend_ftp: Migration to 16.0
IJOL Feb 3, 2026
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,
)
77 changes: 77 additions & 0 deletions storage_backend_ftp/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
===================
Storage Backend FTP
===================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:caf4f1efb3f07f4b77cec7afe2c53bec2e64cb061f93ecad19fb1bae279121e2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
:target: https://github.com/OCA/storage/tree/16.0/storage_backend_ftp
:alt: OCA/storage
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/storage-16-0/storage-16-0-storage_backend_ftp
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

Add FTP as storage backend

**Table of contents**

.. contents::
:local:

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20storage_backend_ftp%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* Acsone SA/NV

Contributors
~~~~~~~~~~~~

* François Honoré <francois.honore@acsone.eu>
* Lois Rilo <lois.rilo@forgeflow.com>

Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/16.0/storage_backend_ftp>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
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 LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.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
171 changes: 171 additions & 0 deletions storage_backend_ftp/components/ftp_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# Copyright 2021 ACSONE SA/NV (<http://acsone.eu>)
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.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


FTP_TIMEOUT = 30


@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=FTP_TIMEOUT)
elif backend.ftp_encryption == "tls":
_ftp = ImplicitFTPTLS(timeout=FTP_TIMEOUT)
# 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(timeout=FTP_TIMEOUT)
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 e
except OSError as e:
raise ValueError(repr(e)) from e

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 e
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)
fp = self._fullpath
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 using absolute filepaths
client.rename(fp(ftp_file), fp(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()
Loading