dav_provider.py 13KB

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