dav_provider.py 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  1. # coding: utf8
  2. import re
  3. from os.path import basename, dirname
  4. from sqlalchemy.orm.exc import NoResultFound
  5. from tracim_backend import CFG
  6. from tracim_backend.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_backend.lib.webdav.lock_storage import LockStorage
  11. from tracim_backend.lib.core.content import ContentApi
  12. from tracim_backend.lib.core.content import ContentRevisionRO
  13. from tracim_backend.lib.core.workspace import WorkspaceApi
  14. from tracim_backend.lib.webdav import resources
  15. from tracim_backend.lib.webdav.utils import normpath
  16. from tracim_backend.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(
  58. path=path,
  59. environ=environ,
  60. user=user,
  61. session=session
  62. )
  63. workspace_api = WorkspaceApi(
  64. current_user=user,
  65. session=session,
  66. config=self.app_config,
  67. )
  68. workspace = self.get_workspace_from_path(path, workspace_api)
  69. # If the request path is in the form root/name, then we return a WorkspaceResource resource
  70. parent_path = dirname(path)
  71. if parent_path == root_path:
  72. if not workspace:
  73. return None
  74. return resources.WorkspaceResource(
  75. path=path,
  76. environ=environ,
  77. workspace=workspace,
  78. user=user,
  79. session=session,
  80. )
  81. # And now we'll work on the path to establish which type or resource is requested
  82. content_api = ContentApi(
  83. current_user=user,
  84. session=session,
  85. config=self.app_config,
  86. show_archived=False, # self._show_archive,
  87. show_deleted=False, # self._show_delete
  88. )
  89. content = self.get_content_from_path(
  90. path=path,
  91. content_api=content_api,
  92. workspace=workspace
  93. )
  94. # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
  95. if path.endswith(SpecialFolderExtension.Archived) and self._show_archive:
  96. return resources.ArchivedFolderResource(
  97. path=path,
  98. environ=environ,
  99. workspace=workspace,
  100. user=user,
  101. content=content,
  102. session=session,
  103. )
  104. if path.endswith(SpecialFolderExtension.Deleted) and self._show_delete:
  105. return resources.DeletedFolderResource(
  106. path=path,
  107. environ=environ,
  108. workspace=workspace,
  109. user=user,
  110. content=content,
  111. session=session,
  112. )
  113. if path.endswith(SpecialFolderExtension.History) and self._show_history:
  114. is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
  115. is_archived_folder = re.search(r'/\.archived/\.history$', path) is not None
  116. type = HistoryType.Deleted if is_deleted_folder \
  117. else HistoryType.Archived if is_archived_folder \
  118. else HistoryType.Standard
  119. return resources.HistoryFolderResource(
  120. path=path,
  121. environ=environ,
  122. workspace=workspace,
  123. user=user,
  124. content=content,
  125. session=session,
  126. type=type
  127. )
  128. # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
  129. is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
  130. if is_history_file_folder and self._show_history:
  131. return resources.HistoryFileFolderResource(
  132. path=path,
  133. environ=environ,
  134. user=user,
  135. content=content,
  136. session=session,
  137. )
  138. # And here next step :
  139. is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
  140. if self._show_history and is_history_file:
  141. revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path).group(1)
  142. content_revision = content_api.get_one_revision(revision_id)
  143. content = self.get_content_from_revision(content_revision, content_api)
  144. if content.type == ContentType.File:
  145. return resources.HistoryFileResource(
  146. path=path,
  147. environ=environ,
  148. user=user,
  149. content=content,
  150. content_revision=content_revision,
  151. session=session,
  152. )
  153. else:
  154. return resources.HistoryOtherFile(
  155. path=path,
  156. environ=environ,
  157. user=user,
  158. content=content,
  159. content_revision=content_revision,
  160. session=session,
  161. )
  162. # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
  163. # and return the corresponding resource
  164. if content is None:
  165. return None
  166. if content.type == ContentType.Folder:
  167. return resources.FolderResource(
  168. path=path,
  169. environ=environ,
  170. workspace=content.workspace,
  171. content=content,
  172. session=session,
  173. user=user,
  174. )
  175. elif content.type == ContentType.File:
  176. return resources.FileResource(
  177. path=path,
  178. environ=environ,
  179. content=content,
  180. session=session,
  181. user=user
  182. )
  183. else:
  184. return resources.OtherFileResource(
  185. path=path,
  186. environ=environ,
  187. content=content,
  188. session=session,
  189. user=user,
  190. )
  191. def exists(self, path, environ) -> bool:
  192. """
  193. Called by wsgidav to check if a certain path is linked to a _DAVResource
  194. """
  195. path = normpath(path)
  196. working_path = self.reduce_path(path)
  197. root_path = environ['http_authenticator.realm']
  198. parent_path = dirname(working_path)
  199. user = environ['tracim_user']
  200. session = environ['tracim_dbsession']
  201. if path == root_path:
  202. return True
  203. workspace = self.get_workspace_from_path(
  204. path,
  205. WorkspaceApi(
  206. current_user=user,
  207. session=session,
  208. config=self.app_config,
  209. )
  210. )
  211. if parent_path == root_path or workspace is None:
  212. return workspace is not None
  213. # TODO bastien: Arnaud avait mis a True, verif le comportement
  214. # lorsque l'on explore les dossiers archive et deleted
  215. content_api = ContentApi(
  216. current_user=user,
  217. session=session,
  218. config=self.app_config,
  219. show_archived=False,
  220. show_deleted=False
  221. )
  222. revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path)
  223. is_archived = self.is_path_archive(path)
  224. is_deleted = self.is_path_delete(path)
  225. if revision_id:
  226. revision_id = revision_id.group(1)
  227. content = content_api.get_one_revision(revision_id)
  228. else:
  229. content = self.get_content_from_path(working_path, content_api, workspace)
  230. return content is not None \
  231. and content.is_deleted == is_deleted \
  232. and content.is_archived == is_archived
  233. def is_path_archive(self, path):
  234. """
  235. This function will check if a given path is linked to a file that's archived or not. We're checking if the
  236. given path end with one of these string :
  237. ex:
  238. - /a/b/.archived/my_file
  239. - /a/b/.archived/.history/my_file
  240. - /a/b/.archived/.history/my_file/(3615 - edition) my_file
  241. """
  242. return re.search(
  243. r'/\.archived/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$',
  244. path
  245. ) is not None
  246. def is_path_delete(self, path):
  247. """
  248. This function will check if a given path is linked to a file that's deleted or not. We're checking if the
  249. given path end with one of these string :
  250. ex:
  251. - /a/b/.deleted/my_file
  252. - /a/b/.deleted/.history/my_file
  253. - /a/b/.deleted/.history/my_file/(3615 - edition) my_file
  254. """
  255. return re.search(
  256. r'/\.deleted/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$',
  257. path
  258. ) is not None
  259. def reduce_path(self, path: str) -> str:
  260. """
  261. As we use the given path to request the database
  262. ex: if the path is /a/b/.deleted/c/.archived, we're trying to get the archived content of the 'c' resource,
  263. we need to keep the path /a/b/c
  264. 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
  265. the path /a/b/my_file
  266. ex: if the path is /a/b/.history/my_file/(1985 - edition) my_old_name, we're looking for,
  267. thus we remove all useless information
  268. """
  269. path = re.sub(r'/\.archived', r'', path)
  270. path = re.sub(r'/\.deleted', r'', path)
  271. path = re.sub(r'/\.history/[^/]+/(\d+)-.+', r'/\1', path)
  272. path = re.sub(r'/\.history/([^/]+)', r'/\1', path)
  273. path = re.sub(r'/\.history', r'', path)
  274. return path
  275. def get_content_from_path(self, path, content_api: ContentApi, workspace: Workspace) -> Content:
  276. """
  277. Called whenever we want to get the Content item from the database for a given path
  278. """
  279. path = self.reduce_path(path)
  280. parent_path = dirname(path)
  281. relative_parents_path = parent_path[len(workspace.label)+1:]
  282. parents = relative_parents_path.split('/')
  283. try:
  284. parents.remove('')
  285. except ValueError:
  286. pass
  287. parents = [transform_to_bdd(x) for x in parents]
  288. try:
  289. return content_api.get_one_by_label_and_parent_labels(
  290. content_label=transform_to_bdd(basename(path)),
  291. content_parent_labels=parents,
  292. workspace=workspace,
  293. )
  294. except NoResultFound:
  295. return None
  296. def get_content_from_revision(self, revision: ContentRevisionRO, api: ContentApi) -> Content:
  297. try:
  298. return api.get_one(revision.content_id, ContentType.Any)
  299. except NoResultFound:
  300. return None
  301. def get_parent_from_path(self, path, api: ContentApi, workspace) -> Content:
  302. return self.get_content_from_path(dirname(path), api, workspace)
  303. def get_workspace_from_path(self, path: str, api: WorkspaceApi) -> Workspace:
  304. try:
  305. return api.get_one_by_label(transform_to_bdd(path.split('/')[1]))
  306. except NoResultFound:
  307. return None