utils.py 6.0KB

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