utils.py 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. # -*- coding: utf-8 -*-
  2. import transaction
  3. from os.path import normpath as base_normpath
  4. from sqlalchemy.orm import Session
  5. from tracim_backend.models.contents import CONTENT_TYPES
  6. from wsgidav import util
  7. from wsgidav import compat
  8. from tracim_backend.lib.core.content import ContentApi
  9. from tracim_backend.models.data import Workspace
  10. from tracim_backend.models.data import Content
  11. from tracim_backend.models.data import ActionDescription
  12. from tracim_backend.models.revision_protection import new_revision
  13. def transform_to_display(string: str) -> str:
  14. """
  15. As characters that Windows does not support may have been inserted
  16. through Tracim in names, before displaying information we update path
  17. so that all these forbidden characters are replaced with similar
  18. shape character that are allowed so that the user isn't trouble and
  19. isn't limited in his naming choice
  20. """
  21. _TO_DISPLAY = {
  22. '/': '⧸',
  23. '\\': '⧹',
  24. ':': '∶',
  25. '*': '∗',
  26. '?': 'ʔ',
  27. '"': 'ʺ',
  28. '<': '❮',
  29. '>': '❯',
  30. '|': '∣'
  31. }
  32. for key, value in _TO_DISPLAY.items():
  33. string = string.replace(key, value)
  34. return string
  35. def transform_to_bdd(string: str) -> str:
  36. """
  37. Called before sending request to the database to recover the right names
  38. """
  39. _TO_BDD = {
  40. '⧸': '/',
  41. '⧹': '\\',
  42. '∶': ':',
  43. '∗': '*',
  44. 'ʔ': '?',
  45. 'ʺ': '"',
  46. '❮': '<',
  47. '❯': '>',
  48. '∣': '|'
  49. }
  50. for key, value in _TO_BDD.items():
  51. string = string.replace(key, value)
  52. return string
  53. def normpath(path):
  54. if path == b'':
  55. path = b'/'
  56. elif path == '':
  57. path = '/'
  58. return base_normpath(path)
  59. class HistoryType(object):
  60. Deleted = 'deleted'
  61. Archived = 'archived'
  62. Standard = 'standard'
  63. All = 'all'
  64. class SpecialFolderExtension(object):
  65. Deleted = '/.deleted'
  66. Archived = '/.archived'
  67. History = '/.history'
  68. class FakeFileStream(object):
  69. """
  70. Fake a FileStream that we're giving to wsgidav to receive data and create files / new revisions
  71. There's two scenarios :
  72. - when a new file is created, wsgidav will call the method createEmptyResource and except to get a _DAVResource
  73. which should have both 'beginWrite' and 'endWrite' method implemented
  74. - when a file which already exists is updated, he's going to call the 'beginWrite' function of the _DAVResource
  75. to get a filestream and write content in it
  76. In the first case scenario, the transfer takes two part : it first create the resource (createEmptyResource)
  77. then add its content (beginWrite, write, close..). If we went without this class, we would create two revision
  78. of the file upon creating a new file, which is not what we want.
  79. """
  80. def __init__(
  81. self,
  82. session: Session,
  83. content_api: ContentApi,
  84. workspace: Workspace,
  85. path: str,
  86. file_name: str='',
  87. content: Content=None,
  88. parent: Content=None
  89. ):
  90. """
  91. :param content_api:
  92. :param workspace:
  93. :param path:
  94. :param file_name:
  95. :param content:
  96. :param parent:
  97. """
  98. self._file_stream = compat.BytesIO()
  99. self._session = session
  100. self._file_name = file_name if file_name != '' else self._content.file_name
  101. self._content = content
  102. self._api = content_api
  103. self._workspace = workspace
  104. self._parent = parent
  105. self._path = path
  106. def getRefUrl(self) -> str:
  107. """
  108. As wsgidav expect to receive a _DAVResource upon creating a new resource, this method's result is used
  109. by Windows client to establish both file's path and file's name
  110. """
  111. return self._path
  112. def beginWrite(self, contentType) -> 'FakeFileStream':
  113. """
  114. Called by wsgidav, it expect a filestream which possess both 'write' and 'close' operation to write
  115. the file content.
  116. """
  117. return self
  118. def endWrite(self, withErrors: bool):
  119. """
  120. Called by request_server when finished writing everything.
  121. As we call operation to create new content or revision in the close operation, called before endWrite, there
  122. is nothing to do here.
  123. """
  124. pass
  125. def write(self, s: str):
  126. """
  127. Called by request_server when writing content to files, we put it inside a filestream
  128. """
  129. self._file_stream.write(s)
  130. def close(self):
  131. """
  132. Called by request_server when the file content has been written. We either add a new content or create
  133. a new revision
  134. """
  135. self._file_stream.seek(0)
  136. if self._content is None:
  137. self.create_file()
  138. else:
  139. self.update_file()
  140. transaction.commit()
  141. def create_file(self):
  142. """
  143. Called when this is a new file; will create a new Content initialized with the correct content
  144. """
  145. is_temporary = self._file_name.startswith('.~') or self._file_name.startswith('~')
  146. file = self._api.create(
  147. filename=self._file_name,
  148. content_type_slug=CONTENT_TYPES.File.slug,
  149. workspace=self._workspace,
  150. parent=self._parent,
  151. is_temporary=is_temporary
  152. )
  153. self._api.update_file_data(
  154. file,
  155. self._file_name,
  156. util.guessMimeType(self._file_name),
  157. self._file_stream.read()
  158. )
  159. self._api.save(file, ActionDescription.CREATION)
  160. def update_file(self):
  161. """
  162. Called when we're updating an existing content; we create a new revision and update the file content
  163. """
  164. with new_revision(
  165. session=self._session,
  166. content=self._content,
  167. tm=transaction.manager,
  168. ):
  169. self._api.update_file_data(
  170. self._content,
  171. self._file_name,
  172. util.guessMimeType(self._content.file_name),
  173. self._file_stream.read()
  174. )
  175. self._api.save(self._content, ActionDescription.REVISION)