diff --git a/attachment_s3/README.rst b/attachment_s3/README.rst index 409fa30..b0bccd6 100644 --- a/attachment_s3/README.rst +++ b/attachment_s3/README.rst @@ -12,7 +12,7 @@ With system parameters: * Create or set the system parameter with the key ``ir_attachment.location`` and the value in the form ``s3://:@`` * 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 With environment variables: @@ -24,6 +24,21 @@ With environment variables: * ``AWS_SECRET_ACCESS_KEY`` * ``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 ----------- diff --git a/attachment_s3/models/ir_attachment.py b/attachment_s3/models/ir_attachment.py index 80dbe63..2315933 100644 --- a/attachment_s3/models/ir_attachment.py +++ b/attachment_s3/models/ir_attachment.py @@ -9,10 +9,12 @@ import os import xml.dom.minidom from functools import partial +from distutils.util import strtobool + import boto from boto.exception import S3ResponseError -from openerp import _, api, exceptions, models +from openerp import _, api, exceptions, fields, models _logger = logging.getLogger(__name__) @@ -20,6 +22,61 @@ _logger = logging.getLogger(__name__) class IrAttachment(models.Model): _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 def _get_s3_bucket(self, bucket_url): """Connect to S3 and return the bucket @@ -29,13 +86,13 @@ class IrAttachment(models.Model): ``s3://:@`` 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 - ``ir_attachment.location.s3host`` can be configured in the System + ``ir_attachment.s3.host`` can be configured in the System Parameters with the hostname. - The following environment variable can be set: + The following environment variables can be set: * ``AWS_HOST`` * ``AWS_ACCESS_KEY_ID`` * ``AWS_SECRET_ACCESS_KEY`` @@ -45,8 +102,8 @@ class IrAttachment(models.Model): assert bucket_url.startswith('s3://') host = os.environ.get('AWS_HOST') if not host: - host = self.env['ir.config_parameter'].get_param( - 'ir_attachment.location.s3host', default=None + host = self.env['ir.config_parameter'].sudo().get_param( + 'ir_attachment.s3.host', default=None ) if host: connect_s3 = partial(boto.connect_s3, host=host) @@ -63,6 +120,7 @@ class IrAttachment(models.Model): '* AWS_ACCESS_KEY_ID\n' '* AWS_SECRET_ACCESS_KEY\n' '* AWS_BUCKETNAME\n' + '* AWS_HOST (optional)\n' ) ) else: @@ -103,27 +161,37 @@ class IrAttachment(models.Model): msg = '%s: %s' % (msg, msg_node[0].childNodes[0].data) return msg + @api.model + def _file_read_s3(self, bucket, fname, bin_size=False): + filekey = bucket.get_key(fname) + if filekey: + if bin_size: + read = filekey.size + else: + read = base64.b64encode(filekey.get_contents_as_string()) + else: + # If the attachment has been created before the installation + # of the addon, it might be stored on the filesystem. + # Fallback on the filesystem read. + # Consider running ``force_storage()`` to move all the + # attachments on the Object Storage + try: + _super = super(IrAttachment, self) + read = _super._file_read(fname, bin_size=bin_size) + except (IOError, OSError): + # File is missing + read = '' + return read + @api.model def _file_read(self, fname, bin_size=False): - _super = super(IrAttachment, self) storage = self._storage() if storage.startswith('s3://'): + storage = self._storage() bucket = self._get_s3_bucket(storage) - filekey = bucket.get_key(fname) - if filekey: - read = base64.b64encode(filekey.get_contents_as_string()) - else: - # If the attachment has been created before the installation - # of the addon, it might be stored on the filesystem. - # Fallback on the filesystem read. - # Consider running ``force_storage()`` to move all the - # attachments on the Object Storage - try: - read = _super._file_read(fname, bin_size=bin_size) - except (IOError, OSError): - # File is missing - return '' + read = self._file_read_s3(bucket, fname, bin_size=bin_size) else: + _super = super(IrAttachment, self) read = _super._file_read(fname, bin_size=bin_size) return read