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.

195 lines
7.8 KiB

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