dav_provider.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. # coding: utf8
  2. import re
  3. from os.path import basename, dirname
  4. from sqlalchemy.orm.exc import NoResultFound
  5. from tracim import CFG
  6. from tracim.lib.webdav.utils import transform_to_bdd, HistoryType, \
  7. SpecialFolderExtension
  8. from wsgidav.dav_provider import DAVProvider
  9. from wsgidav.lock_manager import LockManager
  10. from tracim.lib.webdav.lock_storage import LockStorage
  11. from tracim.lib.core.content import ContentApi
  12. from tracim.lib.core.content import ContentRevisionRO
  13. from tracim.lib.core.workspace import WorkspaceApi
  14. from tracim.lib.webdav import resources
  15. from tracim.lib.webdav.utils import normpath
  16. from tracim.models.data import ContentType, Content, Workspace
  17. class Provider(DAVProvider):
  18. """
  19. This class' role is to provide to wsgidav _DAVResource. Wsgidav will then use them to execute action and send
  20. informations to the client
  21. """
  22. def __init__(
  23. self,
  24. app_config: CFG,
  25. show_history=True,
  26. show_deleted=True,
  27. show_archived=True,
  28. manage_locks=True,
  29. ):
  30. super(Provider, self).__init__()
  31. if manage_locks:
  32. self.lockManager = LockManager(LockStorage())
  33. self.app_config = app_config
  34. self._show_archive = show_archived
  35. self._show_delete = show_deleted
  36. self._show_history = show_history
  37. def show_history(self):
  38. return self._show_history
  39. def show_delete(self):
  40. return self._show_delete
  41. def show_archive(self):
  42. return self._show_archive
  43. #########################################################
  44. # Everything override from DAVProvider
  45. def getResourceInst(self, path: str, environ: dict):
  46. """
  47. Called by wsgidav whenever a request is called to get the _DAVResource corresponding to the path
  48. """
  49. user = environ['tracim_user']
  50. session = environ['tracim_dbsession']
  51. if not self.exists(path, environ):
  52. return None
  53. path = normpath(path)
  54. root_path = environ['http_authenticator.realm']
  55. # If the requested path is the root, then we return a RootResource resource
  56. if path == root_path:
  57. return resources.RootResource(path, environ, user=user, session=session)
  58. workspace_api = WorkspaceApi(current_user=user, session=session)
  59. workspace = self.get_workspace_from_path(path, workspace_api)
  60. # If the request path is in the form root/name, then we return a WorkspaceResource resource
  61. parent_path = dirname(path)
  62. if parent_path == root_path:
  63. if not workspace:
  64. return None
  65. return resources.WorkspaceResource(
  66. path=path,
  67. environ=environ,
  68. workspace=workspace,
  69. user=user,
  70. session=session,
  71. )
  72. # And now we'll work on the path to establish which type or resource is requested
  73. content_api = ContentApi(
  74. current_user=user,
  75. session=session,
  76. config=self.app_config,
  77. show_archived=False, # self._show_archive,
  78. show_deleted=False, # self._show_delete
  79. )
  80. content = self.get_content_from_path(
  81. path=path,
  82. content_api=content_api,
  83. workspace=workspace
  84. )
  85. # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
  86. if path.endswith(SpecialFolderExtension.Archived) and self._show_archive:
  87. return resources.ArchivedFolderResource(path, environ, workspace, content)
  88. if path.endswith(SpecialFolderExtension.Deleted) and self._show_delete:
  89. return resources.DeletedFolderResource(path, environ, workspace, content)
  90. if path.endswith(SpecialFolderExtension.History) and self._show_history:
  91. is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
  92. is_archived_folder = re.search(r'/\.archived/\.history$', path) is not None
  93. type = HistoryType.Deleted if is_deleted_folder \
  94. else HistoryType.Archived if is_archived_folder \
  95. else HistoryType.Standard
  96. return resources.HistoryFolderResource(path, environ, workspace, content, type)
  97. # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
  98. is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
  99. if is_history_file_folder and self._show_history:
  100. return resources.HistoryFileFolderResource(
  101. path=path,
  102. environ=environ,
  103. content=content
  104. )
  105. # And here next step :
  106. is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
  107. if self._show_history and is_history_file:
  108. revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path).group(1)
  109. content_revision = content_api.get_one_revision(revision_id)
  110. content = self.get_content_from_revision(content_revision, content_api)
  111. if content.type == ContentType.File:
  112. return resources.HistoryFileResource(path, environ, content, content_revision)
  113. else:
  114. return resources.HistoryOtherFile(path, environ, content, content_revision)
  115. # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
  116. # and return the corresponding resource
  117. if content is None:
  118. return None
  119. if content.type == ContentType.Folder:
  120. return resources.FolderResource(
  121. path=path,
  122. environ=environ,
  123. workspace=content.workspace,
  124. content=content,
  125. session=session,
  126. user=user,
  127. )
  128. elif content.type == ContentType.File:
  129. return resources.FileResource(
  130. path=path,
  131. environ=environ,
  132. content=content,
  133. session=session,
  134. user=user
  135. )
  136. else:
  137. return resources.OtherFileResource(
  138. path=path,
  139. environ=environ,
  140. content=content,
  141. session=session,
  142. user=user,
  143. )
  144. def exists(self, path, environ) -> bool:
  145. """
  146. Called by wsgidav to check if a certain path is linked to a _DAVResource
  147. """
  148. path = normpath(path)
  149. working_path = self.reduce_path(path)
  150. root_path = environ['http_authenticator.realm']
  151. parent_path = dirname(working_path)
  152. user = environ['tracim_user']
  153. session = environ['tracim_dbsession']
  154. if path == root_path:
  155. return True
  156. workspace = self.get_workspace_from_path(
  157. path,
  158. WorkspaceApi(current_user=user, session=session)
  159. )
  160. if parent_path == root_path or workspace is None:
  161. return workspace is not None
  162. # TODO bastien: Arnaud avait mis a True, verif le comportement
  163. # lorsque l'on explore les dossiers archive et deleted
  164. content_api = ContentApi(
  165. current_user=user,
  166. session=session,
  167. config=self.app_config,
  168. show_archived=False,
  169. show_deleted=False
  170. )
  171. revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path)
  172. is_archived = self.is_path_archive(path)
  173. is_deleted = self.is_path_delete(path)
  174. if revision_id:
  175. revision_id = revision_id.group(1)
  176. content = content_api.get_one_revision(revision_id)
  177. else:
  178. content = self.get_content_from_path(working_path, content_api, workspace)
  179. return content is not None \
  180. and content.is_deleted == is_deleted \
  181. and content.is_archived == is_archived
  182. def is_path_archive(self, path):
  183. """
  184. This function will check if a given path is linked to a file that's archived or not. We're checking if the
  185. given path end with one of these string :
  186. ex:
  187. - /a/b/.archived/my_file
  188. - /a/b/.archived/.history/my_file
  189. - /a/b/.archived/.history/my_file/(3615 - edition) my_file
  190. """
  191. return re.search(
  192. r'/\.archived/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$',
  193. path
  194. ) is not None
  195. def is_path_delete(self, path):
  196. """
  197. This function will check if a given path is linked to a file that's deleted or not. We're checking if the
  198. given path end with one of these string :
  199. ex:
  200. - /a/b/.deleted/my_file
  201. - /a/b/.deleted/.history/my_file
  202. - /a/b/.deleted/.history/my_file/(3615 - edition) my_file
  203. """
  204. return re.search(
  205. r'/\.deleted/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$',
  206. path
  207. ) is not None
  208. def reduce_path(self, path: str) -> str:
  209. """
  210. As we use the given path to request the database
  211. ex: if the path is /a/b/.deleted/c/.archived, we're trying to get the archived content of the 'c' resource,
  212. we need to keep the path /a/b/c
  213. ex: if the path is /a/b/.history/my_file, we're trying to get the history of the file my_file, thus we need
  214. the path /a/b/my_file
  215. ex: if the path is /a/b/.history/my_file/(1985 - edition) my_old_name, we're looking for,
  216. thus we remove all useless information
  217. """
  218. path = re.sub(r'/\.archived', r'', path)
  219. path = re.sub(r'/\.deleted', r'', path)
  220. path = re.sub(r'/\.history/[^/]+/(\d+)-.+', r'/\1', path)
  221. path = re.sub(r'/\.history/([^/]+)', r'/\1', path)
  222. path = re.sub(r'/\.history', r'', path)
  223. return path
  224. def get_content_from_path(self, path, content_api: ContentApi, workspace: Workspace) -> Content:
  225. """
  226. Called whenever we want to get the Content item from the database for a given path
  227. """
  228. path = self.reduce_path(path)
  229. parent_path = dirname(path)
  230. relative_parents_path = parent_path[len(workspace.label)+1:]
  231. parents = relative_parents_path.split('/')
  232. try:
  233. parents.remove('')
  234. except ValueError:
  235. pass
  236. parents = [transform_to_bdd(x) for x in parents]
  237. try:
  238. return content_api.get_one_by_label_and_parent_labels(
  239. content_label=transform_to_bdd(basename(path)),
  240. content_parent_labels=parents,
  241. workspace=workspace,
  242. )
  243. except NoResultFound:
  244. return None
  245. def get_content_from_revision(self, revision: ContentRevisionRO, api: ContentApi) -> Content:
  246. try:
  247. return api.get_one(revision.content_id, ContentType.Any)
  248. except NoResultFound:
  249. return None
  250. def get_parent_from_path(self, path, api: ContentApi, workspace) -> Content:
  251. return self.get_content_from_path(dirname(path), api, workspace)
  252. def get_workspace_from_path(self, path: str, api: WorkspaceApi) -> Workspace:
  253. try:
  254. return api.get_one_by_label(transform_to_bdd(path.split('/')[1]))
  255. except NoResultFound:
  256. return None