dav_provider.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371
  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(
  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