mirror of
https://github.com/camptocamp/odoo-cloud-platform.git
synced 2026-06-24 02:08:36 +00:00
Get OVH modules from 10.0
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
Attachments on Swift storage
|
||||
============================
|
||||
|
||||
This addon enable storing attachments (documents and assets) on OpenStack Object Storage (Swift)
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
Activate Swift storage:
|
||||
|
||||
* Create or set the system parameter with the key ``ir_attachment.location`` with the following value ``swift``.
|
||||
|
||||
Configure accesses with environment variables:
|
||||
|
||||
* ``SWIFT_AUTH_URL`` : URL of the Swift server
|
||||
* ``SWIFT_TENANT_NAME``
|
||||
* ``SWIFT_ACCOUNT``
|
||||
* ``SWIFT_PASSWORD``
|
||||
* ``SWIFT_WRITE_CONTAINER`` : Name of the container to use in the store (created if not existing)
|
||||
|
||||
Read-only mode:
|
||||
|
||||
The container name and the key are stored in the attachment. So if you change the
|
||||
``SWIFT_WRITE_CONTAINER`` or the ``ir_attachment.location``, the existing attachments
|
||||
will still be read on their former container. But as soon as they are written over
|
||||
or new attachments are created, they will be created on the new container or on
|
||||
the other location (db or filesystem). This is a convenient way to be able to
|
||||
read the production attachments on a replication (since you have the
|
||||
credentials) without any risk to alter the production data.
|
||||
|
||||
This addon must be added in the server wide addons with (``--load`` option):
|
||||
|
||||
``--load=web,web_kanban,attachment_swift``
|
||||
|
||||
Python Dependencies
|
||||
-------------------
|
||||
|
||||
This module needs the python-swiftclient and the python-keystoneclient (For auth v2.0) to work.
|
||||
The python-keystoneclient needs the linux package build-essential and python-dev to install properly.
|
||||
|
||||
The python-swiftclient can be used from the command line, useful to test:
|
||||
|
||||
export AUTH_VERSION=2.0
|
||||
export OS_USERNAME={SWIFT_ACCOUNT}
|
||||
export OS_PASSWORD={SWIFT_PASSWORD}
|
||||
export OS_TENANT_NAME={SWIFT_TENANT_NAME}
|
||||
export OS_AUTH_URL=https://auth.cloud.ovh.net/v2.0
|
||||
swift stat
|
||||
|
||||
More information at
|
||||
https://docs.openstack.org/python-swiftclient/latest/cli/index.html#swift-usage
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
|
||||
{'name': 'Attachments on Swift storage',
|
||||
'summary': 'Store assets and attachments on a Swift compatible object store',
|
||||
'version': '10.0.1.1.0',
|
||||
'author': 'Camptocamp,Odoo Community Association (OCA)',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Knowledge Management',
|
||||
'depends': ['base_attachment_object_storage'],
|
||||
'external_dependencies': {
|
||||
'python': ['swiftclient',
|
||||
'keystoneclient',
|
||||
],
|
||||
},
|
||||
'website': 'https://www.camptocamp.com',
|
||||
'data': [],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import ir_attachment
|
||||
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
from ..swift_uri import SwiftUri
|
||||
|
||||
from odoo import api, exceptions, models, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import swiftclient
|
||||
from swiftclient.exceptions import ClientException
|
||||
except ImportError:
|
||||
swiftclient = None
|
||||
ClientException = None
|
||||
_logger.debug("Cannot 'import swiftclient'.")
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
def _get_stores(self):
|
||||
l = ['swift']
|
||||
l += super(IrAttachment, self)._get_stores()
|
||||
return l
|
||||
|
||||
@api.model
|
||||
def _get_swift_connection(self):
|
||||
""" Returns a connection object for the Swift object store """
|
||||
host = os.environ.get('SWIFT_AUTH_URL')
|
||||
account = os.environ.get('SWIFT_ACCOUNT')
|
||||
password = os.environ.get('SWIFT_PASSWORD')
|
||||
tenant_name = os.environ.get('SWIFT_TENANT_NAME')
|
||||
region = os.environ.get('SWIFT_REGION_NAME')
|
||||
os_options = {}
|
||||
if region:
|
||||
os_options['region_name'] = region
|
||||
if not (host and account and password and tenant_name):
|
||||
raise exceptions.UserError(_(
|
||||
"Problem connecting to Swift store, are the env variables "
|
||||
"(SWIFT_AUTH_URL, SWIFT_ACCOUNT, SWIFT_PASSWORD, "
|
||||
"SWIFT_TENANT_NAME) properly set?"
|
||||
))
|
||||
try:
|
||||
conn = swiftclient.client.Connection(authurl=host,
|
||||
user=account,
|
||||
key=password,
|
||||
tenant_name=tenant_name,
|
||||
auth_version='2.0',
|
||||
os_options=os_options,
|
||||
)
|
||||
except ClientException:
|
||||
_logger.exception('Error connecting to Swift object store')
|
||||
raise exceptions.UserError(_('Error on Swift connection'))
|
||||
return conn
|
||||
|
||||
@api.model
|
||||
def _store_file_read(self, fname, bin_size=False):
|
||||
if fname.startswith('swift://'):
|
||||
swifturi = SwiftUri(fname)
|
||||
try:
|
||||
conn = self._get_swift_connection()
|
||||
except exceptions.UserError:
|
||||
_logger.exception(
|
||||
"error reading attachment '%s' from object storage", fname
|
||||
)
|
||||
return ''
|
||||
try:
|
||||
resp, obj_content = conn.get_object(swifturi.container(),
|
||||
swifturi.item())
|
||||
read = base64.b64encode(obj_content)
|
||||
except ClientException:
|
||||
read = ''
|
||||
_logger.exception(
|
||||
'Error reading object from Swift object store')
|
||||
return read
|
||||
else:
|
||||
return super(IrAttachment, self)._store_file_read(fname, bin_size)
|
||||
|
||||
def _store_file_write(self, key, bin_data):
|
||||
if self._storage() == 'swift':
|
||||
container = os.environ.get('SWIFT_WRITE_CONTAINER')
|
||||
conn = self._get_swift_connection()
|
||||
conn.put_container(container)
|
||||
filename = 'swift://{}/{}'.format(container, key)
|
||||
try:
|
||||
conn.put_object(container, key, bin_data)
|
||||
except ClientException:
|
||||
_logger.exception('Error writing to Swift object store')
|
||||
raise exceptions.UserError(_('Error writing to Swift'))
|
||||
else:
|
||||
_super = super(IrAttachment, self)
|
||||
filename = _super._store_file_write(key, bin_data)
|
||||
return filename
|
||||
|
||||
@api.model
|
||||
def _store_file_delete(self, fname):
|
||||
if fname.startswith('swift://'):
|
||||
swifturi = SwiftUri(fname)
|
||||
container = swifturi.container()
|
||||
# delete the file only if it is on the current configured bucket
|
||||
# otherwise, we might delete files used on a different environment
|
||||
if container == os.environ.get('SWIFT_WRITE_CONTAINER'):
|
||||
conn = self._get_swift_connection()
|
||||
try:
|
||||
conn.delete_object(container, swifturi.item())
|
||||
except ClientException:
|
||||
_logger.exception(
|
||||
_('Error deleting an object on the Swift store'))
|
||||
# we ignore the error, file will stay on the object
|
||||
# storage but won't disrupt the process
|
||||
else:
|
||||
super(IrAttachment, self)._file_delete_from_store(fname)
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
import re
|
||||
|
||||
|
||||
class SwiftUri(object):
|
||||
|
||||
_url_re = re.compile("^swift:///*([^/]*)/?(.*)",
|
||||
re.IGNORECASE | re.UNICODE)
|
||||
|
||||
def __init__(self, uri):
|
||||
match = self._url_re.match(uri)
|
||||
if not match:
|
||||
raise ValueError("%s: is not a valid Swift URI" % (uri,))
|
||||
self._container, self._item = match.groups()
|
||||
|
||||
def container(self):
|
||||
return self._container
|
||||
|
||||
def item(self):
|
||||
return self._item
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_mock_swift_api
|
||||
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
import os
|
||||
|
||||
from mock import patch
|
||||
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
|
||||
from ..swift_uri import SwiftUri
|
||||
|
||||
|
||||
class TestAttachmentSwift(TestIrAttachment):
|
||||
|
||||
def setup(self):
|
||||
super(TestAttachmentSwift, self).setUp()
|
||||
self.env['ir.config_parameter'].set_param('ir_attachment.location',
|
||||
'swift')
|
||||
|
||||
@patch('swiftclient.client')
|
||||
def test_connection(self, mock_swift_client):
|
||||
""" Test the connection to the store"""
|
||||
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
|
||||
os.environ['SWIFT_ACCOUNT'] = 'account'
|
||||
os.environ['SWIFT_PASSWORD'] = 'password'
|
||||
os.environ['SWIFT_TENANT_NAME'] = 'tenant_name'
|
||||
os.environ['SWIFT_REGION_NAME'] = 'NOWHERE'
|
||||
attachment = self.Attachment
|
||||
attachment._get_swift_connection()
|
||||
mock_swift_client.Connection.assert_called_once_with(
|
||||
authurl=os.environ.get('SWIFT_AUTH_URL'),
|
||||
user=os.environ.get('SWIFT_ACCOUNT'),
|
||||
key=os.environ.get('SWIFT_PASSWORD'),
|
||||
tenant_name=os.environ.get('SWIFT_TENANT_NAME'),
|
||||
auth_version='2.0',
|
||||
os_options={'region_name': os.environ.get('SWIFT_REGION_NAME')},
|
||||
)
|
||||
|
||||
def test_store_file_on_swift(self):
|
||||
"""
|
||||
Test writing a file
|
||||
"""
|
||||
(self.env['ir.config_parameter'].
|
||||
set_param('ir_attachment.location', 'swift'))
|
||||
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
|
||||
os.environ['SWIFT_ACCOUNT'] = 'account'
|
||||
os.environ['SWIFT_PASSWORD'] = 'password'
|
||||
os.environ['SWIFT_TENANT_NAME'] = 'tenant_name'
|
||||
os.environ['SWIFT_WRITE_CONTAINER'] = 'my_container'
|
||||
container = os.environ.get('SWIFT_WRITE_CONTAINER')
|
||||
attachment = self.Attachment
|
||||
bin_data = self.blob1_b64.decode('base64')
|
||||
with patch('swiftclient.client.Connection') as MockConnection:
|
||||
conn = MockConnection.return_value
|
||||
attachment.create({'name': 'a5', 'datas': self.blob1_b64})
|
||||
conn.put_object.assert_called_with(
|
||||
container,
|
||||
attachment._compute_checksum(bin_data),
|
||||
bin_data)
|
||||
|
||||
def test_delete_file_on_swift(self):
|
||||
"""
|
||||
Test deleting a file
|
||||
"""
|
||||
(self.env['ir.config_parameter'].
|
||||
set_param('ir_attachment.location', 'swift'))
|
||||
os.environ['SWIFT_AUTH_URL'] = 'auth_url'
|
||||
os.environ['SWIFT_ACCOUNT'] = 'account'
|
||||
os.environ['SWIFT_PASSWORD'] = 'password'
|
||||
os.environ['SWIFT_TENANT_NAME'] = 'tenant_name'
|
||||
os.environ['SWIFT_WRITE_CONTAINER'] = 'my_container'
|
||||
|
||||
attachment = self.Attachment
|
||||
container = os.environ.get('SWIFT_WRITE_CONTAINER')
|
||||
with patch('swiftclient.client.Connection') as MockConnection:
|
||||
conn = MockConnection.return_value
|
||||
a5 = attachment.create({'name': 'a5', 'datas': self.blob1_b64})
|
||||
uri = SwiftUri(a5.store_fname)
|
||||
a5.unlink()
|
||||
conn.delete_object.assert_called_with(container, uri.item())
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo.addons.base.tests.test_ir_attachment import TestIrAttachment
|
||||
from ..swift_uri import SwiftUri
|
||||
from swiftclient.exceptions import ClientException
|
||||
|
||||
|
||||
class TestAttachmentSwift(TestIrAttachment):
|
||||
"""
|
||||
Those tests are made to be run against a real Swift store (local or remote)
|
||||
"""
|
||||
|
||||
def setup(self):
|
||||
super(TestAttachmentSwift, self).setUp()
|
||||
self.env['ir.config_parameter'].set_param('ir_attachment.location',
|
||||
'swift')
|
||||
|
||||
def test_connection(self):
|
||||
""" Test the connection to the Swift object store """
|
||||
conn = self.Attachment._get_swift_connection()
|
||||
self.assertNotEquals(conn, False)
|
||||
|
||||
def test_store_file_on_swift(self):
|
||||
""" Test writing a file and then reading it """
|
||||
(self.env['ir.config_parameter'].
|
||||
set_param('ir_attachment.location', 'swift'))
|
||||
a5 = self.Attachment.create({'name': 'a5', 'datas': self.blob1_b64})
|
||||
a5bis = self.Attachment.browse(a5.id)[0]
|
||||
self.assertEquals(a5.datas, a5bis.datas)
|
||||
|
||||
def test_delete_file_on_swift(self):
|
||||
""" Create a file and then test the deletion """
|
||||
(self.env['ir.config_parameter'].
|
||||
set_param('ir_attachment.location', 'swift'))
|
||||
a5 = self.Attachment.create({'name': 'a5', 'datas': self.blob1_b64})
|
||||
uri = SwiftUri(a5.store_fname)
|
||||
con = self.Attachment._get_swift_connection()
|
||||
con.get_object(uri.container(), uri.item())
|
||||
a5.unlink()
|
||||
with self.assertRaises(ClientException):
|
||||
con.get_object(uri.container(), uri.item())
|
||||
@@ -0,0 +1,7 @@
|
||||
Base class for attachments on external object store
|
||||
===================================================
|
||||
|
||||
This is a base addon that regroup common code used by addons targeting specific object store
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -0,0 +1,17 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
|
||||
{'name': 'Base Attachment Object Store',
|
||||
'summary': 'Base module for the implementation of external object store.',
|
||||
'version': '10.0.1.2.0',
|
||||
'author': 'Camptocamp,Odoo Community Association (OCA)',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Knowledge Management',
|
||||
'depends': ['base'],
|
||||
'website': 'http://www.camptocamp.com',
|
||||
'data': [],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import ir_attachment
|
||||
@@ -0,0 +1,299 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import psycopg2
|
||||
import odoo
|
||||
|
||||
from contextlib import closing, contextmanager
|
||||
from odoo import api, exceptions, models, _
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def clean_fs(files):
|
||||
_logger.info('cleaning old files from filestore')
|
||||
for full_path in files:
|
||||
if os.path.exists(full_path):
|
||||
try:
|
||||
os.unlink(full_path)
|
||||
except OSError:
|
||||
_logger.info(
|
||||
"_file_delete could not unlink %s",
|
||||
full_path, exc_info=True
|
||||
)
|
||||
except IOError:
|
||||
# Harmless and needed for race conditions
|
||||
_logger.info(
|
||||
"_file_delete could not unlink %s",
|
||||
full_path, exc_info=True
|
||||
)
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
_local_fields = ('image_small', 'image_medium', 'web_icon_data')
|
||||
|
||||
@api.cr
|
||||
def _register_hook(self):
|
||||
super(IrAttachment, self)._register_hook()
|
||||
# ignore if we are not using an object storage
|
||||
if self._storage() not in self._get_stores():
|
||||
return
|
||||
curframe = inspect.currentframe()
|
||||
calframe = inspect.getouterframes(curframe, 2)
|
||||
# the caller of _register_hook is 'load_modules' in
|
||||
# odoo/modules/loading.py
|
||||
load_modules_frame = calframe[1][0]
|
||||
# 'update_module' is an argument that 'load_modules' receives with a
|
||||
# True-ish value meaning that an install or upgrade of addon has been
|
||||
# done during the initialization. We need to move the attachments that
|
||||
# could have been created or updated in other addons before this addon
|
||||
# was loaded
|
||||
update_module = load_modules_frame.f_locals.get('update_module')
|
||||
|
||||
# We need to call the migration on the loading of the model because
|
||||
# when we are upgrading addons, some of them might add attachments.
|
||||
# To be sure they are migrated to the storage we need to call the
|
||||
# migration here.
|
||||
# Typical example is images of ir.ui.menu which are updated in
|
||||
# ir.attachment at every upgrade of the addons
|
||||
if update_module:
|
||||
self.env['ir.attachment'].sudo()._force_storage_to_object_storage()
|
||||
|
||||
@api.multi
|
||||
def _save_in_db_anyway(self):
|
||||
""" Return whether an attachment must be stored in db
|
||||
|
||||
When we are using an Object Store. This is sometimes required
|
||||
because the object storage is slower than the database/filesystem.
|
||||
|
||||
We store image_small and image_medium from 'Binary' fields
|
||||
because they should be fast to read as they are often displayed
|
||||
in kanbans / lists. The same for web_icon_data.
|
||||
|
||||
We store the assets locally as well. Not only for performance,
|
||||
but also because it improves the portability of the database:
|
||||
when assets are invalidated, they are deleted so we don't have
|
||||
an old database with attachments pointing to deleted assets.
|
||||
|
||||
"""
|
||||
self.ensure_one()
|
||||
# assets
|
||||
if self.res_model == 'ir.ui.view':
|
||||
# assets are stored in 'ir.ui.view'
|
||||
return True
|
||||
|
||||
# Binary fields
|
||||
if self.res_field:
|
||||
# Binary fields are stored with the name of the field in
|
||||
# 'res_field'
|
||||
# 'image' fields can be rather large and should usually
|
||||
# not be requests in bulk in lists
|
||||
if self.res_field and self.res_field in self._local_fields:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _inverse_datas(self):
|
||||
# override in order to store files that need fast access,
|
||||
# we keep them in the database instead of the object storage
|
||||
location = self._storage()
|
||||
for attach in self:
|
||||
if location in self._get_stores() and attach._save_in_db_anyway():
|
||||
# compute the fields that depend on datas
|
||||
value = attach.datas
|
||||
bin_data = value and value.decode('base64') or ''
|
||||
vals = {
|
||||
'file_size': len(bin_data),
|
||||
'checksum': self._compute_checksum(bin_data),
|
||||
'db_datas': value,
|
||||
# we seriously don't need index content on those fields
|
||||
'index_content': False,
|
||||
'store_fname': False,
|
||||
}
|
||||
fname = attach.store_fname
|
||||
# write as superuser, as user probably does not
|
||||
# have write access
|
||||
super(IrAttachment, attach.sudo()).write(vals)
|
||||
if fname:
|
||||
self._file_delete(fname)
|
||||
continue
|
||||
super(IrAttachment, attach)._inverse_datas()
|
||||
|
||||
@api.model
|
||||
def _file_read(self, fname, bin_size=False):
|
||||
if self._is_file_from_a_store(fname):
|
||||
return self._store_file_read(fname, bin_size=bin_size)
|
||||
else:
|
||||
_super = super(IrAttachment, self)
|
||||
return _super._file_read(fname, bin_size=bin_size)
|
||||
|
||||
def _store_file_read(self, fname, bin_size=False):
|
||||
storage = fname.partition('://')[0]
|
||||
raise NotImplementedError(
|
||||
'No implementation for %s' % (storage,)
|
||||
)
|
||||
|
||||
def _store_file_write(self, key, bin_data):
|
||||
raise NotImplementedError(
|
||||
'No implementation for %s' % (self.storage(),)
|
||||
)
|
||||
|
||||
def _store_file_delete(self, fname):
|
||||
storage = fname.partition('://')[0]
|
||||
raise NotImplementedError(
|
||||
'No implementation for %s' % (storage,)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _file_write(self, value, checksum):
|
||||
if self._storage() in self._get_stores():
|
||||
bin_data = value.decode('base64')
|
||||
key = self._compute_checksum(bin_data)
|
||||
filename = self._store_file_write(key, bin_data)
|
||||
else:
|
||||
filename = super(IrAttachment, self)._file_write(value, checksum)
|
||||
return filename
|
||||
|
||||
@api.model
|
||||
def _file_delete(self, fname):
|
||||
if self._is_file_from_a_store(fname):
|
||||
cr = self.env.cr
|
||||
# using SQL to include files hidden through unlink or due to record
|
||||
# rules
|
||||
cr.execute("SELECT COUNT(*) FROM ir_attachment "
|
||||
"WHERE store_fname = %s", (fname,))
|
||||
count = cr.fetchone()[0]
|
||||
if not count:
|
||||
self._store_file_delete(fname)
|
||||
else:
|
||||
super(IrAttachment, self)._file_delete(fname)
|
||||
|
||||
@api.model
|
||||
def _is_file_from_a_store(self, fname):
|
||||
for store_name in self._get_stores():
|
||||
uri = '{}://'.format(store_name)
|
||||
if fname.startswith(uri):
|
||||
return True
|
||||
return False
|
||||
|
||||
@contextmanager
|
||||
def do_in_new_env(self, new_cr=False):
|
||||
""" Context manager that yields a new environment
|
||||
|
||||
Using a new Odoo Environment thus a new PG transaction.
|
||||
"""
|
||||
with api.Environment.manage():
|
||||
if new_cr:
|
||||
registry = odoo.modules.registry.RegistryManager.get(
|
||||
self.env.cr.dbname
|
||||
)
|
||||
with closing(registry.cursor()) as cr:
|
||||
try:
|
||||
yield self.env(cr=cr)
|
||||
except:
|
||||
cr.rollback()
|
||||
raise
|
||||
else:
|
||||
# disable pylint error because this is a valid commit,
|
||||
# we are in a new env
|
||||
cr.commit() # pylint: disable=invalid-commit
|
||||
else:
|
||||
# make a copy
|
||||
yield self.env()
|
||||
|
||||
@api.multi
|
||||
def _move_attachment_to_store(self):
|
||||
self.ensure_one()
|
||||
_logger.info('inspecting attachment %s (%d)', self.name, self.id)
|
||||
fname = self.store_fname
|
||||
if fname:
|
||||
# migrating from filesystem filestore
|
||||
# or from the old 'store_fname' without the bucket name
|
||||
_logger.info('moving %s on the object storage', fname)
|
||||
self.write({'datas': self.datas,
|
||||
# this is required otherwise the
|
||||
# mimetype gets overriden with
|
||||
# 'application/octet-stream'
|
||||
# on assets
|
||||
'mimetype': self.mimetype})
|
||||
_logger.info('moved %s on the object storage', fname)
|
||||
return self._full_path(fname)
|
||||
elif self.db_datas:
|
||||
_logger.info('moving on the object storage from database')
|
||||
self.write({'datas': self.datas})
|
||||
|
||||
@api.model
|
||||
def force_storage(self):
|
||||
if not self.env['res.users'].browse(self.env.uid)._is_admin():
|
||||
raise exceptions.AccessError(
|
||||
_('Only administrators can execute this action.'))
|
||||
storage = self._storage()
|
||||
if storage not in self._get_stores():
|
||||
return super(IrAttachment, self).force_storage()
|
||||
self._force_storage_to_object_storage()
|
||||
|
||||
@api.model
|
||||
def _force_storage_to_object_storage(self, new_cr=False):
|
||||
_logger.info('migrating files to the object storage')
|
||||
storage = self._storage()
|
||||
# The weird "res_field = False OR res_field != False" domain
|
||||
# is required! It's because of an override of _search in ir.attachment
|
||||
# which adds ('res_field', '=', False) when the domain does not
|
||||
# contain 'res_field'.
|
||||
# https://github.com/odoo/odoo/blob/9032617120138848c63b3cfa5d1913c5e5ad76db/odoo/addons/base/ir/ir_attachment.py#L344-L347
|
||||
domain = ['!', ('store_fname', '=like', '{}://%'.format(storage)),
|
||||
'|',
|
||||
('res_field', '=', False),
|
||||
('res_field', '!=', False)]
|
||||
# We do a copy of the environment so we can workaround the cache issue
|
||||
# below. We do not create a new cursor by default because it causes
|
||||
# serialization issues due to concurrent updates on attachments during
|
||||
# the installation
|
||||
with self.do_in_new_env(new_cr=new_cr) as new_env:
|
||||
model_env = new_env['ir.attachment']
|
||||
ids = model_env.search(domain).ids
|
||||
files_to_clean = []
|
||||
for attachment_id in ids:
|
||||
try:
|
||||
with new_env.cr.savepoint():
|
||||
# check that no other transaction has
|
||||
# locked the row, don't send a file to storage
|
||||
# in that case
|
||||
self.env.cr.execute("SELECT id "
|
||||
"FROM ir_attachment "
|
||||
"WHERE id = %s "
|
||||
"FOR UPDATE NOWAIT",
|
||||
(attachment_id,),
|
||||
log_exceptions=False)
|
||||
|
||||
# This is a trick to avoid having the 'datas'
|
||||
# function fields computed for every attachment on
|
||||
# each iteration of the loop. The former issue
|
||||
# being that it reads the content of the file of
|
||||
# ALL the attachments on each loop.
|
||||
new_env.clear()
|
||||
attachment = model_env.browse(attachment_id)
|
||||
path = attachment._move_attachment_to_store()
|
||||
if path:
|
||||
files_to_clean.append(path)
|
||||
except psycopg2.OperationalError:
|
||||
_logger.error('Could not migrate attachment %s to S3',
|
||||
attachment_id)
|
||||
|
||||
def clean():
|
||||
clean_fs(files_to_clean)
|
||||
|
||||
# delete the files from the filesystem once we know the changes
|
||||
# have been committed in ir.attachment
|
||||
if files_to_clean:
|
||||
new_env.cr.after('commit', clean)
|
||||
|
||||
def _get_stores(self):
|
||||
""" To get the list of stores activated in the system """
|
||||
return []
|
||||
@@ -0,0 +1,7 @@
|
||||
Cloud Platform OVH
|
||||
==================
|
||||
|
||||
Install addons specific to the OVH setup.
|
||||
|
||||
* The object storage is Swift
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
|
||||
{'name': 'Cloud Platform OVH',
|
||||
'summary': 'Addons required for the Camptocamp Cloud Platform on OVH',
|
||||
'version': '10.0.1.1.0',
|
||||
'author': 'Camptocamp,Odoo Community Association (OCA)',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Extra Tools',
|
||||
'depends': [
|
||||
'cloud_platform',
|
||||
'attachment_swift',
|
||||
],
|
||||
'website': 'https://www.camptocamp.com',
|
||||
'data': [],
|
||||
'installable': True,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import cloud_platform
|
||||
@@ -0,0 +1,41 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2017 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from odoo.addons.cloud_platform.models.cloud_platform import FilestoreKind
|
||||
from odoo.addons.cloud_platform.models.cloud_platform import PlatformConfig
|
||||
except ImportError:
|
||||
FilestoreKind = None
|
||||
PlatformConfig = None
|
||||
_logger.debug("Cannot 'import from cloud_platform'")
|
||||
|
||||
|
||||
class CloudPlatform(models.AbstractModel):
|
||||
_inherit = 'cloud.platform'
|
||||
|
||||
@api.model
|
||||
def _platform_kinds(self):
|
||||
kinds = super(CloudPlatform, self)._platform_kinds()
|
||||
kinds.append('ovh')
|
||||
return kinds
|
||||
|
||||
@api.model
|
||||
def _config_by_server_env_for_ovh(self):
|
||||
configs = {
|
||||
'prod': PlatformConfig(filestore=FilestoreKind.swift),
|
||||
'integration': PlatformConfig(filestore=FilestoreKind.swift),
|
||||
'test': PlatformConfig(filestore=FilestoreKind.db),
|
||||
'dev': PlatformConfig(filestore=FilestoreKind.db),
|
||||
}
|
||||
return configs
|
||||
|
||||
@api.model
|
||||
def install_ovh(self):
|
||||
self.install('ovh')
|
||||
Reference in New Issue
Block a user