Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3013128
[13.0][ADD] storage_backend_ftp
acsonefho Jul 10, 2021
834519c
[FIX] storage_backend_ftp: fix list and get
LoisRForgeFlow Aug 5, 2021
f327323
[IMP] storage_backend_ftp: fix tests and do not support unsecure prot…
LoisRForgeFlow Oct 26, 2021
7f90370
[UPD] Update storage_backend_ftp.pot
oca-travis Oct 27, 2021
bcb3957
[UPD] README.rst
OCA-git-bot Oct 27, 2021
24a0301
[ADD] icon.png
OCA-git-bot Oct 27, 2021
0f5b83f
storage_backend_ftp 13.0.1.0.1
OCA-git-bot Oct 27, 2021
abfdfdb
Migrate storage_backend_ftp to 14
florian-dacosta Dec 8, 2021
91eaf63
[UPD] Update storage_backend_ftp.pot
oca-travis Dec 11, 2021
e71d412
[UPD] README.rst
OCA-git-bot Dec 11, 2021
ac4d717
storage_backend_ftp 14.0.1.0.1
OCA-git-bot Dec 11, 2021
dd2d6b6
[IMP] storage_backend_ftp: Fix connection issue for implicit FTP over…
JasminSForgeFlow Dec 14, 2021
c264b3f
storage_backend_ftp 14.0.1.0.2
OCA-git-bot Jan 28, 2022
d66a97e
[FIX] storage_backend_ftp: use full path in ``move_files()``
SilvioC2C Apr 6, 2022
9fdbe33
storage_backend_ftp 14.0.1.0.3
OCA-git-bot Apr 7, 2022
b8d2743
[IMP] storage_backend_ftp: Implement Explicit FTP over TLS
JasminSForgeFlow Apr 8, 2022
f2b13be
[UPD] Update storage_backend_ftp.pot
Mar 27, 2023
85067b8
storage_backend_ftp 14.0.1.1.0
OCA-git-bot Mar 27, 2023
389fee0
[UPD] README.rst
OCA-git-bot Sep 4, 2023
26abb19
Added translation using Weblate (Italian)
mymage Nov 18, 2024
d306031
Translated using Weblate (Italian)
mymage Mar 14, 2025
0ad5dda
[IMP] storage_backend_ftp: pre-commit auto fixes
thienvh332 Apr 3, 2025
38d4702
[MIG] storage_backend_ftp: Migration to 18.0
thienvh332 Apr 3, 2025
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 @@ -2,4 +2,5 @@
boto3
fsspec>=2024.5.0
paramiko
pyftpdlib
python_slugify
84 changes: 84 additions & 0 deletions storage_backend_ftp/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
===================
Storage Backend FTP
===================

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

.. |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/18.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-18-0/storage-18-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=18.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:%2018.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>
- thienvh <thienvh@trobz.com>

Other credits
-------------

The migration of this module from 14.0 to 18.0 was financially supported
by Camptocamp.

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/18.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 AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
{
"name": "Storage Backend FTP",
"summary": "Implement FTP Storage",
"version": "18.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 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 OSError 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=30)
elif backend.ftp_encryption == "tls":
_ftp = ImplicitFTPTLS()
# Due to a bug between ftplib and ssl, this part (about ssl)
# might not work! See: 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=30)
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 OSError 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 OSError 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