mirror of
https://github.com/camptocamp/odoo-cloud-platform.git
synced 2026-06-24 02:08:36 +00:00
Add read-only mode that fallbacks on the database
This commit is contained in:
@@ -12,7 +12,7 @@ With system parameters:
|
|||||||
* Create or set the system parameter with the key ``ir_attachment.location``
|
* Create or set the system parameter with the key ``ir_attachment.location``
|
||||||
and the value in the form ``s3://<access-key>:<secret-key>@<bucket-name>``
|
and the value in the form ``s3://<access-key>:<secret-key>@<bucket-name>``
|
||||||
* If the host is not AWS services, you can set the key
|
* If the host is not AWS services, you can set the key
|
||||||
``ir_attachment.location.s3host`` to the hostname of the Object Storage
|
``ir_attachment.s3.host`` to the hostname of the Object Storage
|
||||||
service
|
service
|
||||||
|
|
||||||
With environment variables:
|
With environment variables:
|
||||||
@@ -24,6 +24,21 @@ With environment variables:
|
|||||||
* ``AWS_SECRET_ACCESS_KEY``
|
* ``AWS_SECRET_ACCESS_KEY``
|
||||||
* ``AWS_BUCKETNAME``
|
* ``AWS_BUCKETNAME``
|
||||||
|
|
||||||
|
Read-only mode:
|
||||||
|
|
||||||
|
You can configure the storage to be only for reads on the Object Storage.
|
||||||
|
This is convenient for replications/tests instances, that will be able to
|
||||||
|
access to the same content than the production database without any risk to
|
||||||
|
alter it. The files created or modified the read-only mode is active are
|
||||||
|
created in the database.
|
||||||
|
|
||||||
|
To activate the read-only mode, 2 possibilities:
|
||||||
|
|
||||||
|
* create the system parameter ``ir_attachment.s3.readonly`` and set a positive
|
||||||
|
value (1, true)
|
||||||
|
* set the environment variable ``AWS_ATTACHMENT_READONLY`` to a positive
|
||||||
|
value (1, true)
|
||||||
|
|
||||||
Limitations
|
Limitations
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import os
|
|||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
from distutils.util import strtobool
|
||||||
|
|
||||||
import boto
|
import boto
|
||||||
from boto.exception import S3ResponseError
|
from boto.exception import S3ResponseError
|
||||||
|
|
||||||
from openerp import _, api, exceptions, models
|
from openerp import _, api, exceptions, fields, models
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -20,6 +22,61 @@ _logger = logging.getLogger(__name__)
|
|||||||
class IrAttachment(models.Model):
|
class IrAttachment(models.Model):
|
||||||
_inherit = "ir.attachment"
|
_inherit = "ir.attachment"
|
||||||
|
|
||||||
|
datas = fields.Binary(
|
||||||
|
compute='_compute_datas',
|
||||||
|
inverse='_inverse_datas',
|
||||||
|
string='File Content',
|
||||||
|
nodrop=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _s3_readonly(self):
|
||||||
|
|
||||||
|
def is_true(strval):
|
||||||
|
return bool(strtobool(strval or '0'.lower()))
|
||||||
|
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
storage = params.get_param('ir_attachment.location', default='')
|
||||||
|
env_ro = is_true(os.environ.get('AWS_ATTACHMENT_READONLY'))
|
||||||
|
param_ro = is_true(params.get_param('ir_attachment.s3.readonly'))
|
||||||
|
return storage.startswith('s3://') and (env_ro or param_ro)
|
||||||
|
|
||||||
|
@api.depends('store_fname', 'db_datas')
|
||||||
|
def _compute_datas(self):
|
||||||
|
bin_size = self._context.get('bin_size')
|
||||||
|
if self._s3_readonly():
|
||||||
|
for attach in self:
|
||||||
|
# look first in db_datas in case a file has been modified
|
||||||
|
# locally
|
||||||
|
data = attach.db_datas
|
||||||
|
if data:
|
||||||
|
attach.datas = data
|
||||||
|
else:
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
bucket_url = params.get_param('ir_attachment.location')
|
||||||
|
bucket = self._get_s3_bucket(bucket_url)
|
||||||
|
attach.datas = self._file_read_s3(bucket,
|
||||||
|
attach.store_fname,
|
||||||
|
bin_size)
|
||||||
|
else:
|
||||||
|
values = self._data_get('datas', None)
|
||||||
|
for attach in self:
|
||||||
|
attach.datas = values.get(attach.id)
|
||||||
|
|
||||||
|
def _inverse_datas(self):
|
||||||
|
for attach in self:
|
||||||
|
self._data_set('datas', attach.datas, None)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _storage(self):
|
||||||
|
if self._s3_readonly():
|
||||||
|
# When the S3 readonly mode is active, we force the storage
|
||||||
|
# to be in the database. We'll override the read method
|
||||||
|
# to look in S3 if we have a value though.
|
||||||
|
return 'db'
|
||||||
|
else:
|
||||||
|
return super(IrAttachment, self)._storage()
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _get_s3_bucket(self, bucket_url):
|
def _get_s3_bucket(self, bucket_url):
|
||||||
"""Connect to S3 and return the bucket
|
"""Connect to S3 and return the bucket
|
||||||
@@ -29,13 +86,13 @@ class IrAttachment(models.Model):
|
|||||||
``s3://<access-key>:<secret-key>@<bucket-name>``
|
``s3://<access-key>:<secret-key>@<bucket-name>``
|
||||||
|
|
||||||
Alternatively, we can also use environment variables, in that case,
|
Alternatively, we can also use environment variables, in that case,
|
||||||
you must set the url to ``s3://``.
|
you must set the parameter to ``s3://``.
|
||||||
|
|
||||||
If the S3 provider is not AWS, the key
|
If the S3 provider is not AWS, the key
|
||||||
``ir_attachment.location.s3host`` can be configured in the System
|
``ir_attachment.s3.host`` can be configured in the System
|
||||||
Parameters with the hostname.
|
Parameters with the hostname.
|
||||||
|
|
||||||
The following environment variable can be set:
|
The following environment variables can be set:
|
||||||
* ``AWS_HOST``
|
* ``AWS_HOST``
|
||||||
* ``AWS_ACCESS_KEY_ID``
|
* ``AWS_ACCESS_KEY_ID``
|
||||||
* ``AWS_SECRET_ACCESS_KEY``
|
* ``AWS_SECRET_ACCESS_KEY``
|
||||||
@@ -45,8 +102,8 @@ class IrAttachment(models.Model):
|
|||||||
assert bucket_url.startswith('s3://')
|
assert bucket_url.startswith('s3://')
|
||||||
host = os.environ.get('AWS_HOST')
|
host = os.environ.get('AWS_HOST')
|
||||||
if not host:
|
if not host:
|
||||||
host = self.env['ir.config_parameter'].get_param(
|
host = self.env['ir.config_parameter'].sudo().get_param(
|
||||||
'ir_attachment.location.s3host', default=None
|
'ir_attachment.s3.host', default=None
|
||||||
)
|
)
|
||||||
if host:
|
if host:
|
||||||
connect_s3 = partial(boto.connect_s3, host=host)
|
connect_s3 = partial(boto.connect_s3, host=host)
|
||||||
@@ -63,6 +120,7 @@ class IrAttachment(models.Model):
|
|||||||
'* AWS_ACCESS_KEY_ID\n'
|
'* AWS_ACCESS_KEY_ID\n'
|
||||||
'* AWS_SECRET_ACCESS_KEY\n'
|
'* AWS_SECRET_ACCESS_KEY\n'
|
||||||
'* AWS_BUCKETNAME\n'
|
'* AWS_BUCKETNAME\n'
|
||||||
|
'* AWS_HOST (optional)\n'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -104,13 +162,12 @@ class IrAttachment(models.Model):
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _file_read(self, fname, bin_size=False):
|
def _file_read_s3(self, bucket, fname, bin_size=False):
|
||||||
_super = super(IrAttachment, self)
|
|
||||||
storage = self._storage()
|
|
||||||
if storage.startswith('s3://'):
|
|
||||||
bucket = self._get_s3_bucket(storage)
|
|
||||||
filekey = bucket.get_key(fname)
|
filekey = bucket.get_key(fname)
|
||||||
if filekey:
|
if filekey:
|
||||||
|
if bin_size:
|
||||||
|
read = filekey.size
|
||||||
|
else:
|
||||||
read = base64.b64encode(filekey.get_contents_as_string())
|
read = base64.b64encode(filekey.get_contents_as_string())
|
||||||
else:
|
else:
|
||||||
# If the attachment has been created before the installation
|
# If the attachment has been created before the installation
|
||||||
@@ -119,11 +176,22 @@ class IrAttachment(models.Model):
|
|||||||
# Consider running ``force_storage()`` to move all the
|
# Consider running ``force_storage()`` to move all the
|
||||||
# attachments on the Object Storage
|
# attachments on the Object Storage
|
||||||
try:
|
try:
|
||||||
|
_super = super(IrAttachment, self)
|
||||||
read = _super._file_read(fname, bin_size=bin_size)
|
read = _super._file_read(fname, bin_size=bin_size)
|
||||||
except (IOError, OSError):
|
except (IOError, OSError):
|
||||||
# File is missing
|
# File is missing
|
||||||
return ''
|
read = ''
|
||||||
|
return read
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _file_read(self, fname, bin_size=False):
|
||||||
|
storage = self._storage()
|
||||||
|
if storage.startswith('s3://'):
|
||||||
|
storage = self._storage()
|
||||||
|
bucket = self._get_s3_bucket(storage)
|
||||||
|
read = self._file_read_s3(bucket, fname, bin_size=bin_size)
|
||||||
else:
|
else:
|
||||||
|
_super = super(IrAttachment, self)
|
||||||
read = _super._file_read(fname, bin_size=bin_size)
|
read = _super._file_read(fname, bin_size=bin_size)
|
||||||
return read
|
return read
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user