651 lines
23 KiB
Diff
651 lines
23 KiB
Diff
diff -r ab268bcaf539 trytond/doc/ref/models/fields.rst
|
|
--- a/trytond/doc/ref/models/fields.rst Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/doc/ref/models/fields.rst Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -165,7 +165,8 @@
|
|
.. method:: Field.sql_type()
|
|
|
|
Return the namedtuple('SQLType', 'base type') which defines the SQL type to
|
|
- use for creation and casting.
|
|
+ use for creation and casting. Or `None` if the field is not stored in the
|
|
+ database.
|
|
|
|
.. method:: Field.sql_column(table)
|
|
|
|
@@ -378,7 +379,7 @@
|
|
separated by a dot and its value is the string `size` then the read value
|
|
is the size instead of the content.
|
|
|
|
-:class:`Binary` has one extra optional argument:
|
|
+:class:`Binary` has three extra optional arguments:
|
|
|
|
.. attribute:: Binary.filename
|
|
|
|
@@ -387,6 +388,21 @@
|
|
filename is hidden, and the "Open" button is hidden when the widget is set
|
|
to "image").
|
|
|
|
+.. attribute:: Binary.file_id
|
|
+
|
|
+ Name of the field that holds the `FileStore` identifier. Default value is
|
|
+ `None` which means the data is stored in the database. The field must be on
|
|
+ the same table and accept `char` values.
|
|
+
|
|
+.. warning::
|
|
+ Switching from database to file-store is supported transparently. But
|
|
+ switching from file-store to database is not supported without manually
|
|
+ upload to the database all the files.
|
|
+
|
|
+.. attribute:: Binary.store_prefix
|
|
+
|
|
+ The prefix to use with the `FileStore`. Default value is `None` which means
|
|
+ the database name is used.
|
|
|
|
Selection
|
|
---------
|
|
diff -r ab268bcaf539 trytond/doc/topics/configuration.rst
|
|
--- a/trytond/doc/topics/configuration.rst Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/doc/topics/configuration.rst Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -227,6 +227,24 @@
|
|
|
|
Default: `pipe,name=trytond;urp;StarOffice.ComponentContext`
|
|
|
|
+attachment
|
|
+----------
|
|
+
|
|
+Defines how to store the attachments
|
|
+
|
|
+filestore
|
|
+~~~~~~~~~
|
|
+
|
|
+A boolean value to store attachment in the :ref:`FileStore <ref-filestore>`.
|
|
+
|
|
+Default: `True`
|
|
+
|
|
+store_prefix
|
|
+~~~~~~~~~~~~
|
|
+
|
|
+The prefix to use with the `FileStore`.
|
|
+
|
|
+Default: `None`
|
|
|
|
.. _JSON-RPC: http://en.wikipedia.org/wiki/JSON-RPC
|
|
.. _XML-RPC: http://en.wikipedia.org/wiki/XML-RPC
|
|
diff -r ab268bcaf539 trytond/trytond/ir/attachment.py
|
|
--- a/trytond/trytond/ir/attachment.py Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/trytond/ir/attachment.py Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -1,15 +1,15 @@
|
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
-import os
|
|
-import hashlib
|
|
+from sql import Null
|
|
from sql.operators import Concat
|
|
|
|
from ..model import ModelView, ModelSQL, fields, Unique
|
|
-from ..config import config
|
|
from .. import backend
|
|
from ..transaction import Transaction
|
|
from ..pyson import Eval
|
|
from .resource import ResourceMixin
|
|
+from ..filestore import filestore
|
|
+from ..config import config
|
|
|
|
__all__ = [
|
|
'Attachment',
|
|
@@ -22,6 +22,13 @@
|
|
except StopIteration:
|
|
return ''
|
|
|
|
+if config.getboolean('attachment', 'filestore', default=True):
|
|
+ file_id = 'file_id'
|
|
+ store_prefix = config.get('attachment', 'store_prefix', default=None)
|
|
+else:
|
|
+ file_id = None
|
|
+ store_prefix = None
|
|
+
|
|
|
|
class Attachment(ResourceMixin, ModelSQL, ModelView):
|
|
"Attachment"
|
|
@@ -39,8 +46,7 @@
|
|
link = fields.Char('Link', states={
|
|
'invisible': Eval('type') != 'link',
|
|
}, depends=['type'])
|
|
- digest = fields.Char('Digest', size=32)
|
|
- collision = fields.Integer('Collision')
|
|
+ file_id = fields.Char('File ID', readonly=True)
|
|
data_size = fields.Function(fields.Integer('Data size', states={
|
|
'invisible': Eval('type') != 'data',
|
|
}, depends=['type']), 'get_data')
|
|
@@ -77,14 +83,26 @@
|
|
table.drop_column('res_model')
|
|
table.drop_column('res_id')
|
|
|
|
+ # Migration from 4.0: merge digest and collision into file_id
|
|
+ if table.column_exist('digest') and table.column_exist('collision'):
|
|
+ cursor.execute(*attachment.update(
|
|
+ [attachment.file_id],
|
|
+ [attachment.digest],
|
|
+ where=(attachment.collision == 0)
|
|
+ | (attachment.collision == Null)))
|
|
+ cursor.execute(*attachment.update(
|
|
+ [attachment.file_id],
|
|
+ [Concat(Concat(attachment.digest, '-'),
|
|
+ attachment.collision)],
|
|
+ where=(attachment.collision != 0)
|
|
+ & (attachment.collision != Null)))
|
|
+ table.drop_column('digest')
|
|
+ table.drop_column('collision')
|
|
+
|
|
@staticmethod
|
|
def default_type():
|
|
return 'data'
|
|
|
|
- @staticmethod
|
|
- def default_collision():
|
|
- return 0
|
|
-
|
|
def get_data(self, name):
|
|
db_name = Transaction().database.name
|
|
format_ = Transaction().context.get(
|
|
@@ -92,22 +110,16 @@
|
|
value = None
|
|
if name == 'data_size' or format_ == 'size':
|
|
value = 0
|
|
- if self.digest:
|
|
- filename = self.digest
|
|
- if self.collision:
|
|
- filename = filename + '-' + str(self.collision)
|
|
- filename = os.path.join(config.get('database', 'path'), db_name,
|
|
- filename[0:2], filename[2:4], filename)
|
|
+ if self.file_id:
|
|
if name == 'data_size' or format_ == 'size':
|
|
try:
|
|
- statinfo = os.stat(filename)
|
|
- value = statinfo.st_size
|
|
+ value = filestore.size(self.file_id, prefix=db_name)
|
|
except OSError:
|
|
pass
|
|
else:
|
|
try:
|
|
- with open(filename, 'rb') as file_p:
|
|
- value = fields.Binary.cast(file_p.read())
|
|
+ value = fields.Binary.cast(
|
|
+ filestore.get(self.file_id, prefix=db_name))
|
|
except IOError:
|
|
pass
|
|
return value
|
|
@@ -117,51 +129,11 @@
|
|
if value is None:
|
|
return
|
|
transaction = Transaction()
|
|
- cursor = transaction.connection.cursor()
|
|
- table = cls.__table__()
|
|
db_name = transaction.database.name
|
|
- directory = os.path.join(config.get('database', 'path'), db_name)
|
|
- if not os.path.isdir(directory):
|
|
- os.makedirs(directory, 0770)
|
|
- digest = hashlib.md5(value).hexdigest()
|
|
- directory = os.path.join(directory, digest[0:2], digest[2:4])
|
|
- if not os.path.isdir(directory):
|
|
- os.makedirs(directory, 0770)
|
|
- filename = os.path.join(directory, digest)
|
|
- collision = 0
|
|
- if os.path.isfile(filename):
|
|
- with open(filename, 'rb') as file_p:
|
|
- data = file_p.read()
|
|
- if value != data:
|
|
- cursor.execute(*table.select(table.collision,
|
|
- where=(table.digest == digest)
|
|
- & (table.collision != 0),
|
|
- group_by=table.collision,
|
|
- order_by=table.collision))
|
|
- collision2 = 0
|
|
- for row in cursor.fetchall():
|
|
- collision2 = row[0]
|
|
- filename = os.path.join(directory,
|
|
- digest + '-' + str(collision2))
|
|
- if os.path.isfile(filename):
|
|
- with open(filename, 'rb') as file_p:
|
|
- data = file_p.read()
|
|
- if value == data:
|
|
- collision = collision2
|
|
- break
|
|
- if collision == 0:
|
|
- collision = collision2 + 1
|
|
- filename = os.path.join(directory,
|
|
- digest + '-' + str(collision))
|
|
- with open(filename, 'wb') as file_p:
|
|
- file_p.write(value)
|
|
- else:
|
|
- with open(filename, 'wb') as file_p:
|
|
- file_p.write(value)
|
|
+ file_id = filestore.set(value, prefix=db_name)
|
|
cls.write(attachments, {
|
|
- 'digest': digest,
|
|
- 'collision': collision,
|
|
- })
|
|
+ 'file_id': file_id,
|
|
+ })
|
|
|
|
@fields.depends('description')
|
|
def on_change_with_summary(self, name=None):
|
|
diff -r ab268bcaf539 trytond/trytond/model/fields/binary.py
|
|
--- a/trytond/trytond/model/fields/binary.py Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/trytond/model/fields/binary.py Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -1,10 +1,12 @@
|
|
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
-from sql import Query, Expression
|
|
+from sql import Query, Expression, Column, Null
|
|
|
|
from .field import Field, SQLType
|
|
from ...transaction import Transaction
|
|
from ... import backend
|
|
+from ...tools import grouped_slice, reduce_ids
|
|
+from ...filestore import filestore
|
|
|
|
|
|
class Binary(Field):
|
|
@@ -16,21 +18,22 @@
|
|
|
|
def __init__(self, string='', help='', required=False, readonly=False,
|
|
domain=None, states=None, select=False, on_change=None,
|
|
- on_change_with=None, depends=None, filename=None, context=None,
|
|
- loading='lazy'):
|
|
+ on_change_with=None, depends=None, context=None, loading='lazy',
|
|
+ filename=None, file_id=None, store_prefix=None):
|
|
if filename is not None:
|
|
self.filename = filename
|
|
if depends is None:
|
|
depends = [filename]
|
|
else:
|
|
depends.append(filename)
|
|
+ self.file_id = file_id
|
|
+ self.store_prefix = store_prefix
|
|
super(Binary, self).__init__(string=string, help=help,
|
|
required=required, readonly=readonly, domain=domain, states=states,
|
|
select=select, on_change=on_change, on_change_with=on_change_with,
|
|
depends=depends, context=context, loading=loading)
|
|
|
|
- @classmethod
|
|
- def get(cls, ids, model, name, values=None):
|
|
+ def get(self, ids, model, name, values=None):
|
|
'''
|
|
Convert the binary value into ``bytes``
|
|
|
|
@@ -42,15 +45,45 @@
|
|
'''
|
|
if values is None:
|
|
values = {}
|
|
+ transaction = Transaction()
|
|
res = {}
|
|
- converter = cls.cast
|
|
+ converter = self.cast
|
|
default = None
|
|
format_ = Transaction().context.get(
|
|
'%s.%s' % (model.__name__, name), '')
|
|
if format_ == 'size':
|
|
converter = len
|
|
default = 0
|
|
+
|
|
+ if self.file_id:
|
|
+ table = model.__table__()
|
|
+ cursor = transaction.connection.cursor()
|
|
+
|
|
+ prefix = self.store_prefix
|
|
+ if prefix is None:
|
|
+ prefix = transaction.database.name
|
|
+
|
|
+ if format_ == 'size':
|
|
+ store_func = filestore.size
|
|
+ else:
|
|
+ def store_func(id, prefix):
|
|
+ return self.cast(filestore.get(id, prefix=prefix))
|
|
+
|
|
+ for sub_ids in grouped_slice(ids):
|
|
+ cursor.execute(*table.select(
|
|
+ table.id, Column(table, self.file_id),
|
|
+ where=reduce_ids(table.id, sub_ids)
|
|
+ & (Column(table, self.file_id) != Null)
|
|
+ & (Column(table, self.file_id) != '')))
|
|
+ for record_id, file_id in cursor:
|
|
+ try:
|
|
+ res[record_id] = store_func(file_id, prefix)
|
|
+ except (IOError, OSError):
|
|
+ pass
|
|
+
|
|
for i in values:
|
|
+ if i['id'] in res:
|
|
+ continue
|
|
value = i[name]
|
|
if value:
|
|
if isinstance(value, unicode):
|
|
@@ -63,6 +96,27 @@
|
|
res.setdefault(i, default)
|
|
return res
|
|
|
|
+ def set(self, Model, name, ids, value, *args):
|
|
+ transaction = Transaction()
|
|
+ table = Model.__table__()
|
|
+ cursor = transaction.connection.cursor()
|
|
+
|
|
+ prefix = self.store_prefix
|
|
+ if prefix is None:
|
|
+ prefix = transaction.database.name
|
|
+
|
|
+ args = iter((ids, value) + args)
|
|
+ for ids, value in zip(args, args):
|
|
+ if self.file_id:
|
|
+ columns = [Column(table, self.file_id), Column(table, name)]
|
|
+ values = [
|
|
+ filestore.set(value, prefix) if value else None, None]
|
|
+ else:
|
|
+ columns = [Column(table, name)]
|
|
+ values = [self.sql_format(value)]
|
|
+ cursor.execute(*table.update(columns, values,
|
|
+ where=reduce_ids(table.id, ids)))
|
|
+
|
|
@staticmethod
|
|
def sql_format(value):
|
|
if isinstance(value, (Query, Expression)):
|
|
diff -r ab268bcaf539 trytond/trytond/model/fields/field.py
|
|
--- a/trytond/trytond/model/fields/field.py Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/trytond/model/fields/field.py Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -255,7 +255,7 @@
|
|
return value
|
|
|
|
def sql_type(self):
|
|
- raise NotImplementedError
|
|
+ return None
|
|
|
|
def sql_column(self, table):
|
|
return Column(table, self.name)
|
|
diff -r ab268bcaf539 trytond/trytond/model/fields/function.py
|
|
--- a/trytond/trytond/model/fields/function.py Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/trytond/model/fields/function.py Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -58,9 +58,8 @@
|
|
return
|
|
setattr(self._field, name, value)
|
|
|
|
- @property
|
|
def sql_type(self):
|
|
- raise AttributeError
|
|
+ return None
|
|
|
|
def convert_domain(self, domain, tables, Model):
|
|
name, operator, value = domain[:3]
|
|
diff -r ab268bcaf539 trytond/trytond/model/modelsql.py
|
|
--- a/trytond/trytond/model/modelsql.py Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/trytond/model/modelsql.py Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -145,10 +145,10 @@
|
|
for field_name, field in cls._fields.iteritems():
|
|
if field_name == 'id':
|
|
continue
|
|
+ sql_type = field.sql_type()
|
|
+ if not sql_type:
|
|
+ continue
|
|
default_fun = None
|
|
- if hasattr(field, 'set'):
|
|
- continue
|
|
- sql_type = field.sql_type()
|
|
if field_name in cls._defaults:
|
|
default_fun = cls._defaults[field_name]
|
|
|
|
@@ -199,6 +199,10 @@
|
|
field_name, action=field.select and 'add' or 'remove')
|
|
|
|
required = field.required
|
|
+ # Do not set 'NOT NULL' for Binary field stored in the filestore
|
|
+ # as the database column will be left empty.
|
|
+ if isinstance(field, fields.Binary) and field.file_id:
|
|
+ required = False
|
|
table.not_null_action(
|
|
field_name, action=required and 'add' or 'remove')
|
|
|
|
@@ -228,7 +232,7 @@
|
|
cursor.execute(*history_table.select(history_table.id))
|
|
if not cursor.fetchone():
|
|
columns = [n for n, f in cls._fields.iteritems()
|
|
- if not hasattr(f, 'set')]
|
|
+ if f.sql_type()]
|
|
cursor.execute(*history_table.insert(
|
|
[Column(history_table, c) for c in columns],
|
|
sql_table.select(*(Column(sql_table, c)
|
|
@@ -274,7 +278,7 @@
|
|
field = cls._fields[field_name]
|
|
# Check required fields
|
|
if (field.required
|
|
- and not hasattr(field, 'set')
|
|
+ and field.sql_type()
|
|
and field_name not in ('create_uid', 'create_date')):
|
|
if values.get(field_name) is None:
|
|
cls.raise_user_error('required_field',
|
|
@@ -354,7 +358,7 @@
|
|
'write_date': cls.write_date,
|
|
}
|
|
for fname, field in sorted(fields.iteritems()):
|
|
- if hasattr(field, 'set'):
|
|
+ if not field.sql_type():
|
|
continue
|
|
columns.append(Column(table, fname))
|
|
hcolumns.append(Column(history, fname))
|
|
@@ -384,7 +388,7 @@
|
|
columns = []
|
|
hcolumns = []
|
|
fnames = sorted(n for n, f in cls._fields.iteritems()
|
|
- if not hasattr(f, 'set'))
|
|
+ if f.sql_type())
|
|
for fname in fnames:
|
|
columns.append(Column(table, fname))
|
|
if fname == 'write_uid':
|
|
@@ -663,7 +667,7 @@
|
|
columns = []
|
|
for f in fields_names + fields_related.keys() + datetime_fields:
|
|
field = cls._fields.get(f)
|
|
- if field and not hasattr(field, 'set'):
|
|
+ if field and field.sql_type():
|
|
columns.append(field.sql_column(table).as_(f))
|
|
elif f == '_timestamp' and not table_query:
|
|
sql_type = fields.Char('timestamp').sql_type().base
|
|
@@ -715,7 +719,7 @@
|
|
if field == '_timestamp':
|
|
continue
|
|
if (getattr(cls._fields[field], 'translate', False)
|
|
- and not hasattr(field, 'set')):
|
|
+ and not hasattr(field, 'get')):
|
|
translations = Translation.get_ids(cls.__name__ + ',' + field,
|
|
'model', Transaction().language, ids)
|
|
for row in result:
|
|
diff -r ab268bcaf539 trytond/trytond/tests/test.py
|
|
--- a/trytond/trytond/tests/test.py Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/trytond/tests/test.py Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -35,7 +35,7 @@
|
|
'Property',
|
|
'Selection', 'SelectionRequired',
|
|
'DictSchema', 'Dict', 'DictDefault', 'DictRequired',
|
|
- 'Binary', 'BinaryDefault', 'BinaryRequired',
|
|
+ 'Binary', 'BinaryDefault', 'BinaryRequired', 'BinaryFileStorage',
|
|
'Many2OneDomainValidation', 'Many2OneTarget', 'Many2OneOrderBy',
|
|
'Many2OneSearch', 'Many2OneTree', 'Many2OneMPTT',
|
|
]
|
|
@@ -727,6 +727,13 @@
|
|
binary = fields.Binary('Binary Required', required=True)
|
|
|
|
|
|
+class BinaryFileStorage(ModelSQL):
|
|
+ "Binary in FileStorage"
|
|
+ __name__ = 'test.binary_filestorage'
|
|
+ binary = fields.Binary('Binary', file_id='binary_id')
|
|
+ binary_id = fields.Char('Binary ID')
|
|
+
|
|
+
|
|
class Many2OneTarget(ModelSQL):
|
|
"Many2One Domain Validation Target"
|
|
__name__ = 'test.many2one_target'
|
|
diff -r ab268bcaf539 trytond/trytond/tests/test_fields.py
|
|
--- a/trytond/trytond/tests/test_fields.py Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/trytond/tests/test_fields.py Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -11,12 +11,16 @@
|
|
sys.modules['cdecimal'] = decimal
|
|
import unittest
|
|
import datetime
|
|
+import shutil
|
|
+import tempfile
|
|
+
|
|
from decimal import Decimal
|
|
from trytond.tests.test_tryton import install_module, with_transaction
|
|
from trytond.transaction import Transaction
|
|
from trytond.exceptions import UserError
|
|
from trytond.model import fields
|
|
from trytond.pool import Pool
|
|
+from trytond.config import config
|
|
|
|
|
|
class FieldsTestCase(unittest.TestCase):
|
|
@@ -26,6 +30,13 @@
|
|
def setUpClass(cls):
|
|
install_module('tests')
|
|
|
|
+ def setUp(self):
|
|
+ path = config.get('database', 'path')
|
|
+ dtemp = tempfile.mkdtemp()
|
|
+ config.set('database', 'path', dtemp)
|
|
+ self.addCleanup(config.set, 'database', 'path', path)
|
|
+ self.addCleanup(shutil.rmtree, dtemp)
|
|
+
|
|
@with_transaction()
|
|
def test_boolean(self):
|
|
'Test Boolean'
|
|
@@ -3287,6 +3298,31 @@
|
|
[{'binary': fields.Binary.cast(b'')}])
|
|
|
|
@with_transaction()
|
|
+ def test_binary_filestorage(self):
|
|
+ "Test Binary FileStorage"
|
|
+ pool = Pool()
|
|
+ Binary = pool.get('test.binary_filestorage')
|
|
+ transaction = Transaction()
|
|
+
|
|
+ bin1, = Binary.create([{
|
|
+ 'binary': fields.Binary.cast(b'foo'),
|
|
+ }])
|
|
+ self.assertEqual(bin1.binary, fields.Binary.cast(b'foo'))
|
|
+ self.assertTrue(bin1.binary_id)
|
|
+
|
|
+ Binary.write([bin1], {'binary': fields.Binary.cast(b'bar')})
|
|
+ self.assertEqual(bin1.binary, fields.Binary.cast(b'bar'))
|
|
+
|
|
+ with transaction.set_context(
|
|
+ {'test.binary_filestorage.binary': 'size'}):
|
|
+ bin1_size = Binary(bin1.id)
|
|
+ self.assertEqual(bin1_size.binary, len(b'bar'))
|
|
+
|
|
+ Binary.write([bin1], {'binary': None})
|
|
+ self.assertEqual(bin1.binary, None)
|
|
+ self.assertEqual(bin1.binary_id, None)
|
|
+
|
|
+ @with_transaction()
|
|
def test_many2one(self):
|
|
'Test Many2One'
|
|
pool = Pool()
|
|
diff -r ab268bcaf539 trytond/trytond/tools/misc.py
|
|
--- a/trytond/trytond/tools/misc.py Tue Mar 13 10:44:15 2018 +0100
|
|
+++ b/trytond/trytond/tools/misc.py Thu Mar 15 11:47:28 2018 +0100
|
|
@@ -12,6 +12,7 @@
|
|
import types
|
|
import io
|
|
import warnings
|
|
+import importlib
|
|
|
|
from sql import Literal
|
|
from sql.operators import Or
|
|
@@ -284,3 +285,17 @@
|
|
type_ = klass.__dict__.get(method)
|
|
if type_ is not None:
|
|
return isinstance(type_, types.FunctionType)
|
|
+
|
|
+
|
|
+def resolve(name):
|
|
+ "Resolve a dotted name to a global object."
|
|
+ name = name.split('.')
|
|
+ used = name.pop(0)
|
|
+ found = importlib.import_module(used)
|
|
+ for n in name:
|
|
+ used = used + '.' + n
|
|
+ try:
|
|
+ found = getattr(found, n)
|
|
+ except AttributeError:
|
|
+ found = importlib.import_module(used)
|
|
+ return found
|
|
diff -r ab268bcaf539 trytond/trytond/filestore.py
|
|
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
|
|
+++ b/trytond/trytond/filestore.py Thu Mar 15 12:00:05 2018 +0100
|
|
@@ -0,0 +1,67 @@
|
|
+# This file is part of Tryton. The COPYRIGHT file at the top level of
|
|
+# this repository contains the full copyright notices and license terms.
|
|
+import os
|
|
+import hashlib
|
|
+
|
|
+from trytond.config import config
|
|
+from trytond.tools import resolve
|
|
+
|
|
+__all__ = ['filestore']
|
|
+
|
|
+
|
|
+class FileStore(object):
|
|
+
|
|
+ def get(self, id, prefix=''):
|
|
+ filename = self._filename(id, prefix)
|
|
+ with open(filename, 'rb') as fp:
|
|
+ return fp.read()
|
|
+
|
|
+ def getmany(self, ids, prefix=''):
|
|
+ return [self.get(id, prefix) for id in ids]
|
|
+
|
|
+ def size(self, id, prefix=''):
|
|
+ filename = self._filename(id, prefix)
|
|
+ statinfo = os.stat(filename)
|
|
+ return statinfo.st_size
|
|
+
|
|
+ def sizemany(self, ids, prefix=''):
|
|
+ return [self.size(id, prefix) for id in ids]
|
|
+
|
|
+ def set(self, data, prefix=''):
|
|
+ id = self._id(data)
|
|
+ filename = self._filename(id, prefix)
|
|
+ dirname = os.path.dirname(filename)
|
|
+ if not os.path.exists(dirname):
|
|
+ os.makedirs(dirname, 0770)
|
|
+
|
|
+ collision = 0
|
|
+ while True:
|
|
+ basename = os.path.basename(filename)
|
|
+ if os.path.exists(filename):
|
|
+ if data != self.get(basename, prefix):
|
|
+ collision += 1
|
|
+ filename = self._filename(
|
|
+ '%s-%s' % (id, collision), prefix)
|
|
+ continue
|
|
+ else:
|
|
+ with open(filename, 'wb')as fp:
|
|
+ fp.write(data)
|
|
+ return basename
|
|
+
|
|
+ def setmany(self, data, prefix=''):
|
|
+ return [self.set(d, prefix) for d in data]
|
|
+
|
|
+ def _filename(self, id, prefix):
|
|
+ path = os.path.normpath(config.get('database', 'path'))
|
|
+ filename = os.path.join(path, prefix, id[0:2], id[2:4], id)
|
|
+ filename = os.path.normpath(filename)
|
|
+ if not filename.startswith(path):
|
|
+ raise ValueError('Bad prefix')
|
|
+ return filename
|
|
+
|
|
+ def _id(self, data):
|
|
+ return hashlib.md5(data).hexdigest()
|
|
+
|
|
+if config.get('database', 'class'):
|
|
+ FileStore = resolve(config.get('database', 'class'))
|
|
+filestore = FileStore()
|