From 64ffbe97fcb3a8066f39eb7d9b525e844a7af6bb Mon Sep 17 00:00:00 2001 From: Holger Brunn Date: Mon, 26 Jan 2015 12:53:28 +0100 Subject: [PATCH] [ADD] attachments_to_filesystem --- attachments_to_filesystem/README.rst | 50 +++++++ attachments_to_filesystem/__init__.py | 21 +++ attachments_to_filesystem/__openerp__.py | 43 ++++++ attachments_to_filesystem/data/init.xml | 6 + attachments_to_filesystem/data/ir_cron.xml | 17 +++ attachments_to_filesystem/models/__init__.py | 21 +++ .../models/ir_attachment.py | 136 ++++++++++++++++++ .../static/description/icon.png | Bin 0 -> 11654 bytes 8 files changed, 294 insertions(+) create mode 100644 attachments_to_filesystem/README.rst create mode 100644 attachments_to_filesystem/__init__.py create mode 100644 attachments_to_filesystem/__openerp__.py create mode 100644 attachments_to_filesystem/data/init.xml create mode 100644 attachments_to_filesystem/data/ir_cron.xml create mode 100644 attachments_to_filesystem/models/__init__.py create mode 100644 attachments_to_filesystem/models/ir_attachment.py create mode 100644 attachments_to_filesystem/static/description/icon.png diff --git a/attachments_to_filesystem/README.rst b/attachments_to_filesystem/README.rst new file mode 100644 index 00000000..eb56c6f3 --- /dev/null +++ b/attachments_to_filesystem/README.rst @@ -0,0 +1,50 @@ +Introduction +============ +This addon allows to automatically move existing attachments to the file +system. + +Configuration +============= +If it doesn't exist, the module creates a parameter `ir_attachment.location` +with value `file`. This will make new attachments end up in your +data path (the odoo configuration value `data_dir`) in a subdirectory called +`filestore`. + +Then it will create a cron job that does the actual transfer and schedule it +for 01:42 at night in the installing user's time zone. The cronjob will do a +maximum of 10000 conversions, after which it creates a new cronjob to run for +the next batch 24 hours later. The limit is configurable with the parameter +`attachments_to_filesystem.limit`. + +If you need to run the migration synchronously during install, set the +parameter `attachments_to_filesystem.move_during_init` *before* installing this +addon. + +Credits +======= + +Contributors +------------ + +* Holger Brunn + +Icon +---- + +http://commons.wikimedia.org/wiki/File:Crystal_Clear_app_harddrive.png + +Maintainer +---------- + +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit http://odoo-community.org. + diff --git a/attachments_to_filesystem/__init__.py b/attachments_to_filesystem/__init__.py new file mode 100644 index 00000000..0f8f0e70 --- /dev/null +++ b/attachments_to_filesystem/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2015 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import models diff --git a/attachments_to_filesystem/__openerp__.py b/attachments_to_filesystem/__openerp__.py new file mode 100644 index 00000000..e1e5d90e --- /dev/null +++ b/attachments_to_filesystem/__openerp__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2015 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + "name": "Move existing attachments to filesystem", + "version": "1.0", + "author": "Therp BV", + "license": "AGPL-3", + "complexity": "normal", + "category": "Knowledge Management", + "depends": [ + 'base', + ], + "data": [ + "data/ir_cron.xml", + "data/init.xml", + ], + "test": [ + ], + "auto_install": False, + "installable": True, + "application": False, + "external_dependencies": { + 'python': ['dateutil', 'pytz'], + }, +} diff --git a/attachments_to_filesystem/data/init.xml b/attachments_to_filesystem/data/init.xml new file mode 100644 index 00000000..60c1a671 --- /dev/null +++ b/attachments_to_filesystem/data/init.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/attachments_to_filesystem/data/ir_cron.xml b/attachments_to_filesystem/data/ir_cron.xml new file mode 100644 index 00000000..e56ee90e --- /dev/null +++ b/attachments_to_filesystem/data/ir_cron.xml @@ -0,0 +1,17 @@ + + + + + Move attachments to filestore + + + + + 2000-01-01 + + ir.attachment + _attachments_to_filesystem_cron + () + + + diff --git a/attachments_to_filesystem/models/__init__.py b/attachments_to_filesystem/models/__init__.py new file mode 100644 index 00000000..f7f41eaf --- /dev/null +++ b/attachments_to_filesystem/models/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2015 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +from . import ir_attachment diff --git a/attachments_to_filesystem/models/ir_attachment.py b/attachments_to_filesystem/models/ir_attachment.py new file mode 100644 index 00000000..c4bbd47e --- /dev/null +++ b/attachments_to_filesystem/models/ir_attachment.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2015 Therp BV (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +import pytz +import logging +from datetime import datetime +from dateutil.relativedelta import relativedelta +from openerp.osv.orm import Model +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT + + +class IrAttachment(Model): + _inherit = 'ir.attachment' + + def _attachments_to_filesystem_init(self, cr, uid, context=None): + """Set up config parameter and cron job""" + module_name = __name__.split('.')[-3] + ir_model_data = self.pool['ir.model.data'] + ir_cron = self.pool['ir.cron'] + location = self.pool['ir.config_parameter'].get_param( + cr, uid, 'ir_attachment.location') + if location: + # we assume the user knows what she's doing. Might be file:, but + # also whatever other scheme shouldn't matter. We want to bring + # data from the database to there + pass + else: + ir_model_data._update( + cr, uid, 'ir.config_parameter', module_name, + { + 'key': 'ir_attachment.location', + 'value': 'file', + }, + xml_id='config_parameter_ir_attachment_location', + context=context) + + # synchronous behavior + if self.pool['ir.config_parameter'].get_param( + cr, uid, 'attachments_to_filesystem.move_during_init'): + self._attachments_to_filesystem_cron(cr, uid, context, limit=None) + return + + # otherwise, configure our cronjob to run next night + user = self.pool['res.users'].browse(cr, uid, uid, context=context) + next_night = datetime.now() + relativedelta( + hour=01, minute=42, second=0) + next_night = pytz.timezone(user.tz).localize(next_night).astimezone( + pytz.utc).replace(tzinfo=None) + if next_night < datetime.now(): + next_night += relativedelta(days=1) + ir_cron.write( + cr, uid, + [ + ir_model_data.get_object_reference( + cr, uid, module_name, 'cron_move_attachments')[1], + ], + { + 'nextcall': + next_night.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'doall': True, + }, + context=context) + + def _attachments_to_filesystem_cron(self, cr, uid, context=None, + limit=10000): + """Do the actual moving""" + limit = int( + self.pool['ir.config_parameter'].get_param( + cr, uid, 'attachments_to_filesystem.limit', '0')) or limit + ir_attachment = self.pool['ir.attachment'] + attachment_ids = ir_attachment.search( + cr, uid, [('db_datas', '!=', False)], limit=limit, context=context) + logging.info('moving %d attachments to filestore', len(attachment_ids)) + counter = 1 + for attachment_data in ir_attachment.read( + cr, uid, attachment_ids, ['datas'], context=context): + ir_attachment.write( + cr, uid, [attachment_data['id']], + { + 'datas': attachment_data['datas'], + 'db_datas': False, + }, + context=context) + if not counter % (len(attachment_ids) / 100 or limit): + logging.info('moving attachments: %d done', counter) + counter += 1 + # see if we need to create a new cronjob for the next batch + if ir_attachment.search( + cr, uid, [('db_datas', '!=', False)], limit=1, + context=context): + module_name = __name__.split('.')[-3] + ir_cron = self.pool['ir.cron'] + last_job_id = ir_cron.search( + cr, uid, + [ + ('model', '=', 'ir.attachment'), + ('function', '=', '_attachments_to_filesystem_cron'), + ], + order='nextcall desc', + limit=1, + context=context) + if not last_job_id: + return + new_job_data = ir_cron.copy_data( + cr, uid, last_job_id[0], context=context) + new_job_data.update( + nextcall=( + datetime.strptime( + new_job_data['nextcall'], + DEFAULT_SERVER_DATETIME_FORMAT) + + relativedelta(days=1) + ).strftime(DEFAULT_SERVER_DATETIME_FORMAT), + ) + self.pool['ir.model.data']._update( + cr, uid, 'ir.cron', module_name, + new_job_data, + xml_id='config_parameter_ir_attachment_location' + str( + last_job_id[0]), + context=context) diff --git a/attachments_to_filesystem/static/description/icon.png b/attachments_to_filesystem/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..56fb52643476fbcdbbf0d47bac38cdf31e1ca3f4 GIT binary patch literal 11654 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