├── __init__.py ├── models ├── __init__.py ├── models.py └── s3_helper.py ├── static └── description │ └── icon.png ├── LICENSE.md ├── __manifest__.py └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import models 4 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import models -------------------------------------------------------------------------------- /static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolstycjw/odoo-s3-storage/HEAD/static/description/icon.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 bolstycjw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | 'name': "S3 Storages", 4 | 5 | 'summary': """ 6 | Allows you to use a DigitalOcean Spaces bucket for file storage""", 7 | 8 | 'description': """ 9 | Binary files such as attachments and pictures are stored by default 10 | in the file system of the host running Odoo. In some cases you may 11 | want to decrease the overall response time by delegating static file 12 | storage to a specialized instance such as an DigitalOcean Spaces bucket. 13 | This module allows you to configure Odoo so that an DigitalOcean Spaces bucket is 14 | used instead of the file system for binary files storage. 15 | """, 16 | 17 | 'author': "brolycjw, hp-bkeys", 18 | 'website': "http://primetechnologies.com.sg/, https://homeprotech.com/", 19 | 20 | # Categories can be used to filter modules in modules listing 21 | # Check https://github.com/odoo/odoo/blob/master/odoo/addons/base/module/module_data.xml 22 | # for the full list 23 | 'category': 'Technical Settings', 24 | 'version': '0.1', 25 | 26 | # any module necessary for this one to work correctly 27 | 'depends': ['base'], 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Odoo S3 Storage 2 | 3 | ## Dependencies 4 | `Odoo-S3-Storage` uses [`boto3`](https://github.com/boto/boto3) to talk to DigitalOcean. You will need to install it on the host running Odoo. 5 | 6 | ## Installation 7 | Make sure you set the `ODOO_ADDONS_PATH` variable to the directory where you install your custom Odoo modules. 8 | 9 | ``` 10 | pip install boto3 11 | cd $ODOO_ADDONS_PATH 12 | git clone https://github.com/HP-bkeys/odoo-s3-storage.git 13 | ``` 14 | 15 | ## Compatibility 16 | This module is compatible with **Odoo 11** and **Python 3**. For older versions, you can refer to the original source code (see credits below). 17 | 18 | ## Configuration 19 | In order to use `Odoo-S3-Storage` you will need to switch to "Developer mode" and define a new system parameter as follows: 20 | 21 | * without encryption: 22 | ``` 23 | ir_attachment.location ---> s3://:@& 24 | 25 | ``` 26 | * with server-side encryption (only AES256, since [aws:kms is not supported in boto3](https://github.com/boto/botocore/issues/471)): 27 | ``` 28 | ir_attachment.location ---> s3://:@&+SSE 29 | 30 | ## Additional Information 31 | This module is based on `Odoo-S3`(https://github.com/tvanesse/odoo-s3) and `Odoo-S3-Storage`(https://github.com/bolstycjw/odoo-s3-storage). The code was rewritten to work with **Odoo v10.0**, uses boto3 instead of boto, and works with DigitalOcean Spaces. 32 | 33 | 34 | ``` -------------------------------------------------------------------------------- /models/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | s3-storage.models 4 | ~~~~~~~~~~~~~~~~~ 5 | 6 | Use s3 as file storage mechanism 7 | 8 | :copyright: (c) 2017 by Marc Lijour, brolycjw. 9 | :license: MIT License, see LICENSE for more details. 10 | """ 11 | 12 | import hashlib 13 | import base64 14 | 15 | from odoo import models 16 | 17 | from . import s3_helper 18 | 19 | 20 | class S3Attachment(models.Model): 21 | """Extends ir.attachment to implement the S3 storage engine 22 | """ 23 | _inherit = "ir.attachment" 24 | 25 | def _connect_to_S3_bucket(self, s3, bucket_name): 26 | s3_bucket = s3.Bucket(bucket_name) 27 | exists = s3_helper.bucket_exists(s3, bucket_name) 28 | 29 | if not exists: 30 | s3_bucket = s3.create_bucket(Bucket=bucket_name) 31 | return s3_bucket 32 | 33 | def _file_read(self, fname, bin_size=False): 34 | storage = self._storage() 35 | if storage[:5] == 's3://': 36 | access_key_id, secret_key, bucket_name, do_space_url = s3_helper.parse_bucket_url( 37 | storage) 38 | s3 = s3_helper.get_resource( 39 | access_key_id, secret_key, do_space_url) 40 | s3_bucket = self._connect_to_S3_bucket(s3, bucket_name) 41 | file_exists = s3_helper.object_exists(s3, s3_bucket.name, fname) 42 | if not file_exists: 43 | # Some old files (prior to the installation of odoo-s3) may 44 | # still be stored in the file system even though 45 | # ir_attachment.location is configured to use S3 46 | try: 47 | read = super(S3Attachment, self)._file_read( 48 | fname, bin_size=False) 49 | except Exception: 50 | # Could not find the file in the file system either. 51 | return False 52 | else: 53 | s3_key = s3.Object(s3_bucket.name, fname) 54 | read = base64.b64encode(s3_key.get()['Body'].read()) 55 | else: 56 | read = super(S3Attachment, self)._file_read(fname, bin_size=False) 57 | return read 58 | 59 | def _file_write(self, value, checksum): 60 | storage = self._storage() 61 | if storage[:5] == 's3://': 62 | access_key_id, secret_key, bucket_name, do_space_url = s3_helper.parse_bucket_url( 63 | storage) 64 | s3 = s3_helper.get_resource( 65 | access_key_id, secret_key, do_space_url) 66 | s3_bucket = self._connect_to_S3_bucket(s3, bucket_name) 67 | bin_value = base64.b64decode(value) 68 | fname = hashlib.sha1(bin_value).hexdigest() 69 | if encryption_enabled: 70 | s3.Object(s3_bucket.name, fname).put(Body=bin_value, ServerSideEncryption='AES256') 71 | else: 72 | s3.Object(s3_bucket.name, fname).put(Body=bin_value) 73 | 74 | else: # falling back on Odoo's local filestore 75 | fname = super(S3Attachment, self)._file_write(value, checksum) 76 | 77 | return fname 78 | 79 | -------------------------------------------------------------------------------- /models/s3_helper.py: -------------------------------------------------------------------------------- 1 | """ 2 | s3_helper.py 3 | ~~~~~~~~~~~~~~~~~ 4 | 5 | Helper functions for models.py 6 | 7 | :copyright: (c) 2017 by Marc Lijour, brolycjw. 8 | :license: MIT License, see LICENSE for more details. 9 | """ 10 | 11 | import boto3 12 | # uncomment for debug mode: 13 | # boto3.set_stream_logger('') 14 | import botocore 15 | from boto3.session import Session 16 | from boto3.s3.transfer import S3Transfer 17 | 18 | def parse_bucket_url(bucket_url): 19 | scheme = bucket_url[:5] 20 | assert scheme == 's3://', \ 21 | "Expecting an s3:// scheme, got {} instead.".format(scheme) 22 | 23 | # scheme: 24 | # s3://:@&+SSE 25 | # where +SSE is optional (meaning server-side encryption enabled) 26 | 27 | try: 28 | encryption_enabled = False 29 | remain = bucket_url.lstrip(scheme) 30 | access_key_id = remain.split(':')[0] 31 | remain = remain.lstrip(access_key_id).lstrip(':') 32 | secret_key = remain.split('@')[0] 33 | remain = remain.lstrip(secret_key).lstrip('@') 34 | bucket_name = remain.split('&')[0] 35 | remain = remain.lstrip(bucket_name).lstrip('&').split('+') 36 | do_space_url = remain[0] 37 | encryption_enabled = len(remain) > 1 38 | 39 | if not access_key_id or not secret_key: 40 | raise Exception( 41 | "No AWS access and secret keys were provided." 42 | " Unable to establish a connexion to S3." 43 | ) 44 | except Exception: 45 | raise Exception("Unable to parse the S3 bucket url.") 46 | 47 | return (access_key_id, secret_key, bucket_name, do_space_url, encryption_enabled) 48 | 49 | 50 | def bucket_exists(s3, bucket_name): 51 | exists = True 52 | try: 53 | s3.meta.client.head_bucket(Bucket=bucket_name) 54 | except botocore.exceptions.ClientError as e: 55 | error_code = int(e.response['Error']['Code']) 56 | if error_code == 404: 57 | exists = False 58 | return exists 59 | 60 | 61 | def object_exists(s3, bucket_name, key): 62 | exists = True 63 | try: 64 | s3.meta.client.head_object(Bucket=bucket_name, Key=key) 65 | except botocore.exceptions.ClientError as e: 66 | error_code = int(e.response['Error']['Code']) 67 | if error_code == 404: 68 | exists = False 69 | return exists 70 | 71 | 72 | def get_resource(access_key_id, secret_key, endpoint_url): 73 | session = boto3.Session(access_key_id, secret_key) 74 | s3 = session.resource('s3', endpoint_url='https://' + endpoint_url) 75 | return s3 76 | 77 | # extra: works for files stored in the file system 78 | # (not called by models.py which only deal with in-memory) 79 | def upload(value, storage): 80 | access_key_id, secret_key, bucket_name, do_space_url, encryption_enabled = parse_bucket_url(storage) 81 | s3 = get_resource(access_key_id, secret_key) 82 | ### S3Transfer allows multi-part, call backs etc 83 | # http://boto3.readthedocs.io/en/latest/_modules/boto3/s3/transfer.html 84 | transfer = S3Transfer(s3.meta.client) 85 | if encryption_enabled: 86 | transfer.upload_file(value, bucket_name, do_space_url, value, extra_args={'ServerSideEncryption': 'AES256'}) 87 | else: 88 | transfer.upload_file(value, bucket_name, do_space_url, value) 89 | --------------------------------------------------------------------------------