You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
7.7 KiB

  1. ###################################################################################
  2. #
  3. # Copyright (c) 2017-2019 MuK IT GmbH.
  4. #
  5. # This file is part of MuK Filestore Field
  6. # (see https://mukit.at).
  7. #
  8. # This program is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published by
  10. # the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # This program is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License
  19. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. #
  21. ###################################################################################
  22. import os
  23. import re
  24. import shutil
  25. import base64
  26. import hashlib
  27. import logging
  28. import binascii
  29. import tempfile
  30. from collections import defaultdict
  31. from odoo import fields, tools
  32. from odoo.tools import human_size, config
  33. from odoo.addons.muk_utils.tools.file import ensure_path_directories
  34. _logger = logging.getLogger(__name__)
  35. def get_store_path(dbname):
  36. return os.path.join(config.get('data_dir'), 'files', dbname)
  37. def clean_store(dbname, env):
  38. tables = defaultdict(set)
  39. for model_name in env.registry.models:
  40. model = env[model_name]
  41. if not model._abstract:
  42. for name, field in model._fields.items():
  43. if field.type == 'file':
  44. tables[model._table].add(name)
  45. checklist = set()
  46. filestore = get_store_path(dbname)
  47. path = os.path.join(filestore, 'checklist')
  48. for root, dirs, files in os.walk(path):
  49. for file in files:
  50. checkpath = os.path.join(root, file)
  51. relpath = os.path.relpath(checkpath, path)
  52. checklist.add(os.path.join(filestore, relpath))
  53. env.cr.commit()
  54. whitelist = set()
  55. for table, fields in tables.items():
  56. select_fields = list(fields)
  57. env.cr.execute("LOCK %s IN SHARE MODE" % table)
  58. select_query = "SELECT {0}".format(', '.join(select_fields))
  59. where_query = "WHERE {0} IN %(paths)s".format(select_fields[0])
  60. if len(select_fields) > 1:
  61. for field in select_fields[:1]:
  62. where_query += "OR {0} IN %s".format(field)
  63. sql_query = "{0} FROM {1} {2};".format(select_query, table, where_query)
  64. for paths in env.cr.split_for_in_conditions(checklist):
  65. env.cr.execute(sql_query, {'paths': paths})
  66. for row in env.cr.fetchall():
  67. for column in row:
  68. whitelist.add(column)
  69. remove = checklist - whitelist
  70. for file in remove:
  71. try:
  72. os.unlink(file)
  73. except (OSError, IOError):
  74. _logger.warn("Deleting file from %s failed!", file, exc_info=True)
  75. with tools.ignore(OSError):
  76. shutil.rmtree(path)
  77. env.cr.commit()
  78. _logger.info("Cleaned files [ %d checked | %d removed ]", len(checklist), len(remove))
  79. class File(fields.Field):
  80. type = 'file'
  81. column_type = ('varchar', 'varchar')
  82. _slots = {
  83. 'prefetch': False,
  84. 'context_dependent': True,
  85. }
  86. def _get_file_path(self, checksume, dbname):
  87. name = os.path.join(checksume[:2], checksume)
  88. name = re.sub('[.]', '', name).strip('/\\')
  89. filestore = get_store_path(dbname)
  90. path = os.path.join(filestore, name)
  91. ensure_path_directories(path)
  92. return path
  93. def _add_to_checklist(self, path, dbname):
  94. filestore = get_store_path(dbname)
  95. relpath = os.path.relpath(path, filestore)
  96. checklist = os.path.join(filestore, 'checklist', relpath)
  97. if not os.path.exists(checklist):
  98. ensure_path_directories(checklist)
  99. open(checklist, 'ab').close()
  100. def _get_checksum(self, value):
  101. if isinstance(value, bytes):
  102. return hashlib.sha1(value).hexdigest()
  103. else:
  104. checksum = hashlib.sha1()
  105. while True:
  106. chunk = value.read(4096)
  107. if not chunk:
  108. return checksum.hexdigest()
  109. checksum.update(chunk)
  110. def convert_to_column(self, value, record, values=None, validate=True):
  111. path = None
  112. try:
  113. current_path = record.with_context({'path': True})[self.name]
  114. if current_path:
  115. self._add_to_checklist(current_path, record.env.cr.dbname)
  116. if not value:
  117. return None
  118. binary = None
  119. if isinstance(value, bytes):
  120. binary = value
  121. elif isinstance(value, str):
  122. binary = base64.b64decode(value)
  123. if binary:
  124. checksume = self._get_checksum(binary)
  125. path = self._get_file_path(checksume, record.env.cr.dbname)
  126. with open(path, 'wb') as file:
  127. file.write(binary)
  128. self._add_to_checklist(path, record.env.cr.dbname)
  129. else:
  130. checksume = self._get_checksum(value)
  131. path = self._get_file_path(checksume, record.env.cr.dbname)
  132. value.seek(0, 0)
  133. with open(path, 'wb') as file:
  134. while True:
  135. chunk = value.read(4096)
  136. if not chunk:
  137. break
  138. file.write(chunk)
  139. self._add_to_checklist(path)
  140. except (IOError, OSError):
  141. _logger.warn("Writing file to %s failed!", path, exc_info=True)
  142. return path
  143. def convert_to_record(self, value, record):
  144. if value and isinstance(value, str) and os.path.exists(value):
  145. try:
  146. with open(value, 'rb') as file:
  147. if record._context.get('human_size'):
  148. return human_size(file.seek(0, 2))
  149. elif record._context.get('bin_size'):
  150. return file.seek(0, 2)
  151. elif record._context.get('path'):
  152. return value
  153. elif record._context.get('base64'):
  154. return base64.b64encode(file.read())
  155. elif record._context.get('stream'):
  156. temp = tempfile.TemporaryFile()
  157. while True:
  158. chunk = file.read(4096)
  159. if not chunk:
  160. temp.seek(0)
  161. return temp
  162. temp.write(chunk)
  163. elif record._context.get('checksum'):
  164. checksum = hashlib.sha1()
  165. while True:
  166. chunk = file.read(4096)
  167. if not chunk:
  168. return checksum.hexdigest()
  169. checksum.update(chunk)
  170. else:
  171. return file.read()
  172. except (IOError, OSError):
  173. _logger.warn("Reading file from %s failed!", value, exc_info=True)
  174. return value
  175. def convert_to_export(self, value, record):
  176. if value:
  177. try:
  178. with open(value, 'rb') as file:
  179. if record._context.get('export_raw_data'):
  180. return file.read()
  181. return base64.b64encode(file.read())
  182. except (IOError, OSError):
  183. _logger.warn("Reading file from %s failed!", value, exc_info=True)
  184. return ''