resources.py 50KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482
  1. # coding: utf8
  2. import logging
  3. import os
  4. import transaction
  5. import typing
  6. import re
  7. from datetime import datetime
  8. from time import mktime
  9. from os.path import dirname, basename
  10. from sqlalchemy.orm import Session
  11. from tracim_backend.config import CFG
  12. from tracim_backend.lib.core.content import ContentApi
  13. from tracim_backend.lib.core.user import UserApi
  14. from tracim_backend.lib.webdav.utils import transform_to_display, HistoryType, \
  15. FakeFileStream
  16. from tracim_backend.lib.webdav.utils import transform_to_bdd
  17. from tracim_backend.lib.core.workspace import WorkspaceApi
  18. from tracim_backend.app_models.contents import CONTENT_TYPES
  19. from tracim_backend.models.data import User, ContentRevisionRO
  20. from tracim_backend.models.data import Workspace
  21. from tracim_backend.models.data import Content
  22. from tracim_backend.models.data import ActionDescription
  23. from tracim_backend.lib.webdav.design import designThread, designPage
  24. from wsgidav import compat
  25. from wsgidav.dav_error import DAVError, HTTP_FORBIDDEN
  26. from wsgidav.dav_provider import DAVCollection, DAVNonCollection
  27. from wsgidav.dav_provider import _DAVResource
  28. from tracim_backend.lib.webdav.utils import normpath
  29. from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
  30. from tracim_backend.models.revision_protection import new_revision
  31. logger = logging.getLogger()
  32. class ManageActions(object):
  33. """
  34. This object is used to encapsulate all Deletion/Archiving related
  35. method as to not duplicate too much code
  36. """
  37. def __init__(self,
  38. session: Session,
  39. action_type: str,
  40. api: ContentApi,
  41. content: Content
  42. ):
  43. self.session = session
  44. self.content_api = api
  45. self.content = content
  46. self._actions = {
  47. ActionDescription.ARCHIVING: self.content_api.archive,
  48. ActionDescription.DELETION: self.content_api.delete,
  49. ActionDescription.UNARCHIVING: self.content_api.unarchive,
  50. ActionDescription.UNDELETION: self.content_api.undelete
  51. }
  52. self._type = action_type
  53. def action(self):
  54. with new_revision(
  55. session=self.session,
  56. tm=transaction.manager,
  57. content=self.content,
  58. ):
  59. self._actions[self._type](self.content)
  60. self.content_api.save(self.content, self._type)
  61. transaction.commit()
  62. class RootResource(DAVCollection):
  63. """
  64. RootResource ressource that represents tracim's home, which contains all workspaces
  65. """
  66. def __init__(self, path: str, environ: dict, user: User, session: Session):
  67. super(RootResource, self).__init__(path, environ)
  68. self.user = user
  69. self.session = session
  70. # TODO BS 20170221: Web interface should list all workspace to. We
  71. # disable it here for moment. When web interface will be updated to
  72. # list all workspace, change this here to.
  73. self.workspace_api = WorkspaceApi(
  74. current_user=self.user,
  75. session=session,
  76. force_role=True,
  77. config=self.provider.app_config
  78. )
  79. def __repr__(self) -> str:
  80. return '<DAVCollection: RootResource>'
  81. def getMemberNames(self) -> [str]:
  82. """
  83. This method returns the names (here workspace's labels) of all its children
  84. Though for perfomance issue, we're not using this function anymore
  85. """
  86. return [workspace.label for workspace in self.workspace_api.get_all()]
  87. def getMember(self, label: str) -> DAVCollection:
  88. """
  89. This method returns the child Workspace that corresponds to a given name
  90. Though for perfomance issue, we're not using this function anymore
  91. """
  92. try:
  93. workspace = self.workspace_api.get_one_by_label(label)
  94. workspace_path = '%s%s%s' % (self.path, '' if self.path == '/' else '/', transform_to_display(workspace.label))
  95. return WorkspaceResource(
  96. workspace_path,
  97. self.environ,
  98. workspace,
  99. session=self.session,
  100. user=self.user,
  101. )
  102. except AttributeError:
  103. return None
  104. def createEmptyResource(self, name: str):
  105. """
  106. This method is called whenever the user wants to create a DAVNonCollection resource (files in our case).
  107. There we don't allow to create files at the root;
  108. only workspaces (thus collection) can be created.
  109. """
  110. raise DAVError(HTTP_FORBIDDEN)
  111. def createCollection(self, name: str):
  112. """
  113. This method is called whenever the user wants to create a DAVCollection resource as a child (in our case,
  114. we create workspaces as this is the root).
  115. [For now] we don't allow to create new workspaces through
  116. webdav client. Though if we come to allow it, deleting the error's raise will
  117. make it possible.
  118. """
  119. # TODO : remove comment here
  120. # raise DAVError(HTTP_FORBIDDEN)
  121. new_workspace = self.workspace_api.create_workspace(name)
  122. self.workspace_api.save(new_workspace)
  123. workspace_path = '%s%s%s' % (
  124. self.path, '' if self.path == '/' else '/', transform_to_display(new_workspace.label))
  125. transaction.commit()
  126. return WorkspaceResource(
  127. workspace_path,
  128. self.environ,
  129. new_workspace,
  130. user=self.user,
  131. session=self.session,
  132. )
  133. def getMemberList(self):
  134. """
  135. This method is called by wsgidav when requesting with a depth > 0, it will return a list of _DAVResource
  136. of all its direct children
  137. """
  138. members = []
  139. for workspace in self.workspace_api.get_all():
  140. workspace_path = '%s%s%s' % (self.path, '' if self.path == '/' else '/', workspace.label)
  141. members.append(
  142. WorkspaceResource(
  143. path=workspace_path,
  144. environ=self.environ,
  145. workspace=workspace,
  146. user=self.user,
  147. session=self.session,
  148. )
  149. )
  150. return members
  151. class WorkspaceResource(DAVCollection):
  152. """
  153. Workspace resource corresponding to tracim's workspaces.
  154. Direct children can only be folders, though files might come later on and are supported
  155. """
  156. def __init__(self,
  157. path: str,
  158. environ: dict,
  159. workspace: Workspace,
  160. user: User,
  161. session: Session
  162. ) -> None:
  163. super(WorkspaceResource, self).__init__(path, environ)
  164. self.workspace = workspace
  165. self.content = None
  166. self.user = user
  167. self.session = session
  168. self.content_api = ContentApi(
  169. current_user=self.user,
  170. session=session,
  171. config=self.provider.app_config,
  172. show_temporary=True
  173. )
  174. self._file_count = 0
  175. def __repr__(self) -> str:
  176. return "<DAVCollection: Workspace (%d)>" % self.workspace.workspace_id
  177. def getPreferredPath(self):
  178. return self.path
  179. def getCreationDate(self) -> float:
  180. return mktime(self.workspace.created.timetuple())
  181. def getDisplayName(self) -> str:
  182. return self.workspace.label
  183. def getLastModified(self) -> float:
  184. return mktime(self.workspace.updated.timetuple())
  185. def getMemberNames(self) -> [str]:
  186. retlist = []
  187. children = self.content_api.get_all(
  188. parent_id=self.content.id if self.content is not None else None,
  189. workspace=self.workspace
  190. )
  191. for content in children:
  192. # the purpose is to display .history only if there's at least one content's type that has a history
  193. if content.type != CONTENT_TYPES.Folder.slug:
  194. self._file_count += 1
  195. retlist.append(content.get_label_as_file())
  196. return retlist
  197. def getMember(self, content_label: str) -> _DAVResource:
  198. return self.provider.getResourceInst(
  199. '%s/%s' % (self.path, transform_to_display(content_label)),
  200. self.environ
  201. )
  202. def createEmptyResource(self, file_name: str):
  203. """
  204. [For now] we don't allow to create files right under workspaces.
  205. Though if we come to allow it, deleting the error's raise will make it possible.
  206. """
  207. # TODO : remove commentary here raise DAVError(HTTP_FORBIDDEN)
  208. if '/.deleted/' in self.path or '/.archived/' in self.path:
  209. raise DAVError(HTTP_FORBIDDEN)
  210. content = None
  211. # Note: To prevent bugs, check here again if resource already exist
  212. path = os.path.join(self.path, file_name)
  213. resource = self.provider.getResourceInst(path, self.environ)
  214. if resource:
  215. content = resource.content
  216. return FakeFileStream(
  217. session=self.session,
  218. file_name=file_name,
  219. content_api=self.content_api,
  220. workspace=self.workspace,
  221. content=content,
  222. parent=self.content,
  223. path=self.path + '/' + file_name
  224. )
  225. def createCollection(self, label: str) -> 'FolderResource':
  226. """
  227. Create a new folder for the current workspace. As it's not possible for the user to choose
  228. which types of content are allowed in this folder, we allow allow all of them.
  229. This method return the DAVCollection created.
  230. """
  231. if '/.deleted/' in self.path or '/.archived/' in self.path:
  232. raise DAVError(HTTP_FORBIDDEN)
  233. folder = self.content_api.create(
  234. content_type_slug=CONTENT_TYPES.Folder.slug,
  235. workspace=self.workspace,
  236. label=label,
  237. parent=self.content
  238. )
  239. subcontent = dict(
  240. folder=True,
  241. thread=True,
  242. file=True,
  243. page=True
  244. )
  245. self.content_api.set_allowed_content(folder, subcontent)
  246. self.content_api.save(folder)
  247. transaction.commit()
  248. return FolderResource('%s/%s' % (self.path, transform_to_display(label)),
  249. self.environ,
  250. content=folder,
  251. session=self.session,
  252. user=self.user,
  253. workspace=self.workspace,
  254. )
  255. def delete(self):
  256. """For now, it is not possible to delete a workspace through the webdav client."""
  257. raise DAVError(HTTP_FORBIDDEN)
  258. def supportRecursiveMove(self, destpath):
  259. return True
  260. def moveRecursive(self, destpath):
  261. if dirname(normpath(destpath)) == self.environ['http_authenticator.realm']:
  262. self.workspace.label = basename(normpath(destpath))
  263. transaction.commit()
  264. else:
  265. raise DAVError(HTTP_FORBIDDEN)
  266. def getMemberList(self) -> [_DAVResource]:
  267. members = []
  268. children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
  269. for content in children:
  270. content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
  271. if content.type == CONTENT_TYPES.Folder.slug:
  272. members.append(
  273. FolderResource(
  274. path=content_path,
  275. environ=self.environ,
  276. workspace=self.workspace,
  277. user=self.user,
  278. content=content,
  279. session=self.session,
  280. )
  281. )
  282. elif content.type == CONTENT_TYPES.File.slug:
  283. self._file_count += 1
  284. members.append(
  285. FileResource(
  286. path=content_path,
  287. environ=self.environ,
  288. content=content,
  289. user=self.user,
  290. session=self.session,
  291. )
  292. )
  293. else:
  294. self._file_count += 1
  295. members.append(
  296. OtherFileResource(
  297. content_path,
  298. self.environ,
  299. content,
  300. session=self.session,
  301. user=self.user,
  302. ))
  303. if self._file_count > 0 and self.provider.show_history():
  304. members.append(
  305. HistoryFolderResource(
  306. path=self.path + '/' + ".history",
  307. environ=self.environ,
  308. content=self.content,
  309. workspace=self.workspace,
  310. type=HistoryType.Standard,
  311. session=self.session,
  312. user=self.user,
  313. )
  314. )
  315. if self.provider.show_delete():
  316. members.append(
  317. DeletedFolderResource(
  318. path=self.path + '/' + ".deleted",
  319. environ=self.environ,
  320. content=self.content,
  321. workspace=self.workspace,
  322. session=self.session,
  323. user=self.user,
  324. )
  325. )
  326. if self.provider.show_archive():
  327. members.append(
  328. ArchivedFolderResource(
  329. path=self.path + '/' + ".archived",
  330. environ=self.environ,
  331. content=self.content,
  332. workspace=self.workspace,
  333. user=self.user,
  334. session=self.session,
  335. )
  336. )
  337. return members
  338. class FolderResource(WorkspaceResource):
  339. """
  340. FolderResource resource corresponding to tracim's folders.
  341. Direct children can only be either folder, files, pages or threads
  342. By default when creating new folders, we allow them to contain all types of content
  343. """
  344. def __init__(
  345. self,
  346. path: str,
  347. environ: dict,
  348. workspace: Workspace,
  349. content: Content,
  350. user: User,
  351. session: Session
  352. ):
  353. super(FolderResource, self).__init__(
  354. path=path,
  355. environ=environ,
  356. workspace=workspace,
  357. user=user,
  358. session=session,
  359. )
  360. self.content = content
  361. def __repr__(self) -> str:
  362. return "<DAVCollection: Folder (%s)>" % self.content.label
  363. def getCreationDate(self) -> float:
  364. return mktime(self.content.created.timetuple())
  365. def getDisplayName(self) -> str:
  366. return transform_to_display(self.content.get_label_as_file())
  367. def getLastModified(self) -> float:
  368. return mktime(self.content.updated.timetuple())
  369. def delete(self):
  370. ManageActions(
  371. action_type=ActionDescription.DELETION,
  372. api=self.content_api,
  373. content=self.content,
  374. session=self.session,
  375. ).action()
  376. def supportRecursiveMove(self, destpath: str):
  377. return True
  378. def moveRecursive(self, destpath: str):
  379. """
  380. As we support recursive move, copymovesingle won't be called, though with copy it'll be called
  381. but i have to check if the client ever call that function...
  382. """
  383. destpath = normpath(destpath)
  384. invalid_path = False
  385. # if content is either deleted or archived, we'll check that we try moving it to the parent
  386. # if yes, then we'll unarchive / undelete them, else the action's not allowed
  387. if self.content.is_deleted or self.content.is_archived:
  388. # we remove all archived and deleted from the path and we check to the destpath
  389. # has to be equal or else path not valid
  390. # ex: /a/b/.deleted/resource, to be valid destpath has to be = /a/b/resource (no other solution)
  391. current_path = re.sub(r'/\.(deleted|archived)', '', self.path)
  392. if current_path == destpath:
  393. ManageActions(
  394. action_type=ActionDescription.UNDELETION if self.content.is_deleted else ActionDescription.UNARCHIVING,
  395. api=self.content_api,
  396. content=self.content,
  397. session=self.session,
  398. ).action()
  399. else:
  400. invalid_path = True
  401. # if the content is not deleted / archived, check if we're trying to delete / archive it by
  402. # moving it to a .deleted / .archived folder
  403. elif basename(dirname(destpath)) in ['.deleted', '.archived']:
  404. # same test as above ^
  405. dest_path = re.sub(r'/\.(deleted|archived)', '', destpath)
  406. if dest_path == self.path:
  407. ManageActions(
  408. action_type=ActionDescription.DELETION if '.deleted' in destpath else ActionDescription.ARCHIVING,
  409. api=self.content_api,
  410. content=self.content,
  411. session=self.session,
  412. ).action()
  413. else:
  414. invalid_path = True
  415. # else we check if the path is good (not at the root path / not in a deleted/archived path)
  416. # and we move the content
  417. else:
  418. invalid_path = any(x in destpath for x in ['.deleted', '.archived'])
  419. invalid_path = invalid_path or any(x in self.path for x in ['.deleted', '.archived'])
  420. invalid_path = invalid_path or dirname(destpath) == self.environ['http_authenticator.realm']
  421. if not invalid_path:
  422. self.move_folder(destpath)
  423. if invalid_path:
  424. raise DAVError(HTTP_FORBIDDEN)
  425. def move_folder(self, destpath):
  426. workspace_api = WorkspaceApi(
  427. current_user=self.user,
  428. session=self.session,
  429. config=self.provider.app_config,
  430. )
  431. workspace = self.provider.get_workspace_from_path(
  432. normpath(destpath), workspace_api
  433. )
  434. parent = self.provider.get_parent_from_path(
  435. normpath(destpath),
  436. self.content_api,
  437. workspace
  438. )
  439. with new_revision(
  440. content=self.content,
  441. tm=transaction.manager,
  442. session=self.session,
  443. ):
  444. if basename(destpath) != self.getDisplayName():
  445. self.content_api.update_content(self.content, transform_to_bdd(basename(destpath)))
  446. self.content_api.save(self.content)
  447. else:
  448. if workspace.workspace_id == self.content.workspace.workspace_id:
  449. self.content_api.move(self.content, parent)
  450. else:
  451. self.content_api.move_recursively(self.content, parent, workspace)
  452. transaction.commit()
  453. def getMemberList(self) -> [_DAVResource]:
  454. members = []
  455. content_api = ContentApi(
  456. current_user=self.user,
  457. config=self.provider.app_config,
  458. session=self.session,
  459. )
  460. visible_children = content_api.get_all(
  461. self.content.content_id,
  462. CONTENT_TYPES.Any_SLUG,
  463. self.workspace,
  464. )
  465. for content in visible_children:
  466. content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
  467. try:
  468. if content.type == CONTENT_TYPES.Folder.slug:
  469. members.append(
  470. FolderResource(
  471. path=content_path,
  472. environ=self.environ,
  473. workspace=self.workspace,
  474. content=content,
  475. user=self.user,
  476. session=self.session,
  477. )
  478. )
  479. elif content.type == CONTENT_TYPES.File.slug:
  480. self._file_count += 1
  481. members.append(
  482. FileResource(
  483. path=content_path,
  484. environ=self.environ,
  485. content=content,
  486. user=self.user,
  487. session=self.session,
  488. ))
  489. else:
  490. self._file_count += 1
  491. members.append(
  492. OtherFileResource(
  493. path=content_path,
  494. environ=self.environ,
  495. content=content,
  496. user=self.user,
  497. session=self.session,
  498. ))
  499. except NotImplementedError as exc:
  500. pass
  501. # except Exception as exc:
  502. # logger.exception(
  503. # 'Unable to construct member {}'.format(
  504. # content_path,
  505. # ),
  506. # exc_info=True,
  507. # )
  508. if self._file_count > 0 and self.provider.show_history():
  509. members.append(
  510. HistoryFolderResource(
  511. path=self.path + '/' + ".history",
  512. environ=self.environ,
  513. content=self.content,
  514. workspace=self.workspace,
  515. type=HistoryType.Standard,
  516. user=self.user,
  517. session=self.session,
  518. )
  519. )
  520. if self.provider.show_delete():
  521. members.append(
  522. DeletedFolderResource(
  523. path=self.path + '/' + ".deleted",
  524. environ=self.environ,
  525. content=self.content,
  526. workspace=self.workspace,
  527. user=self.user,
  528. session=self.session,
  529. )
  530. )
  531. if self.provider.show_archive():
  532. members.append(
  533. ArchivedFolderResource(
  534. path=self.path + '/' + ".archived",
  535. environ=self.environ,
  536. content=self.content,
  537. workspace=self.workspace,
  538. user=self.user,
  539. session=self.session,
  540. )
  541. )
  542. return members
  543. # TODO - G.M - 02-05-2018 - Check these object (History/Deleted/Archived Folder)
  544. # Those object are now not in used by tracim and also not tested,
  545. class HistoryFolderResource(FolderResource):
  546. """
  547. A virtual resource which contains a sub-folder for every files (DAVNonCollection) contained in the parent
  548. folder
  549. """
  550. def __init__(self,
  551. path,
  552. environ,
  553. workspace: Workspace,
  554. user: User,
  555. session: Session,
  556. content: Content=None,
  557. type: str=HistoryType.Standard
  558. ) -> None:
  559. super(HistoryFolderResource, self).__init__(
  560. path=path,
  561. environ=environ,
  562. workspace=workspace,
  563. content=content,
  564. user=user,
  565. session=session,
  566. )
  567. self._is_archived = type == HistoryType.Archived
  568. self._is_deleted = type == HistoryType.Deleted
  569. self.content_api = ContentApi(
  570. current_user=self.user,
  571. show_archived=self._is_archived,
  572. show_deleted=self._is_deleted,
  573. session=self.session,
  574. config=self.provider.app_config,
  575. )
  576. def __repr__(self) -> str:
  577. return "<DAVCollection: HistoryFolderResource (%s)>" % self.content.file_name
  578. def getCreationDate(self) -> float:
  579. return mktime(datetime.now().timetuple())
  580. def getDisplayName(self) -> str:
  581. return '.history'
  582. def getLastModified(self) -> float:
  583. return mktime(datetime.now().timetuple())
  584. def getMember(self, content_label: str) -> _DAVResource:
  585. content = self.content_api.get_one_by_label_and_parent(
  586. content_label=content_label,
  587. content_parent=self.content
  588. )
  589. return HistoryFileFolderResource(
  590. path='%s/%s' % (self.path, content.get_label_as_file()),
  591. environ=self.environ,
  592. content=content,
  593. session=self.session,
  594. user=self.user,
  595. )
  596. def getMemberNames(self) -> [str]:
  597. ret = []
  598. content_id = None if self.content is None else self.content.id
  599. for content in self.content_api.get_all(content_id, CONTENT_TYPES.Any_SLUG, self.workspace):
  600. if (self._is_archived and content.is_archived or
  601. self._is_deleted and content.is_deleted or
  602. not (content.is_archived or self._is_archived or content.is_deleted or self._is_deleted))\
  603. and content.type != CONTENT_TYPES.Folder.slug:
  604. ret.append(content.get_label_as_file())
  605. return ret
  606. def createEmptyResource(self, name: str):
  607. raise DAVError(HTTP_FORBIDDEN)
  608. def createCollection(self, name: str):
  609. raise DAVError(HTTP_FORBIDDEN)
  610. def delete(self):
  611. raise DAVError(HTTP_FORBIDDEN)
  612. def handleDelete(self):
  613. return True
  614. def handleCopy(self, destPath: str, depthInfinity):
  615. return True
  616. def handleMove(self, destPath: str):
  617. return True
  618. def getMemberList(self) -> [_DAVResource]:
  619. members = []
  620. if self.content:
  621. children = self.content.children
  622. else:
  623. children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
  624. for content in children:
  625. if content.is_archived == self._is_archived and content.is_deleted == self._is_deleted:
  626. members.append(HistoryFileFolderResource(
  627. path='%s/%s' % (self.path, content.get_label_as_file()),
  628. environ=self.environ,
  629. content=content,
  630. user=self.user,
  631. session=self.session,
  632. ))
  633. return members
  634. class DeletedFolderResource(HistoryFolderResource):
  635. """
  636. A virtual resources which exists for every folder or workspaces which contains their deleted children
  637. """
  638. def __init__(
  639. self,
  640. path: str,
  641. environ: dict,
  642. workspace: Workspace,
  643. user: User,
  644. session: Session,
  645. content: Content=None
  646. ):
  647. super(DeletedFolderResource, self).__init__(
  648. path=path,
  649. environ=environ,
  650. workspace=workspace,
  651. user=user,
  652. content=content,
  653. session=session,
  654. type=HistoryType.Deleted
  655. )
  656. self._file_count = 0
  657. def __repr__(self):
  658. return "<DAVCollection: DeletedFolderResource (%s)" % self.content.file_name
  659. def getCreationDate(self) -> float:
  660. return mktime(datetime.now().timetuple())
  661. def getDisplayName(self) -> str:
  662. return '.deleted'
  663. def getLastModified(self) -> float:
  664. return mktime(datetime.now().timetuple())
  665. def getMember(self, content_label) -> _DAVResource:
  666. content = self.content_api.get_one_by_label_and_parent(
  667. content_label=content_label,
  668. content_parent=self.content
  669. )
  670. return self.provider.getResourceInst(
  671. path='%s/%s' % (self.path, transform_to_display(content.get_label_as_file())),
  672. environ=self.environ
  673. )
  674. def getMemberNames(self) -> [str]:
  675. retlist = []
  676. if self.content:
  677. children = self.content.children
  678. else:
  679. children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
  680. for content in children:
  681. if content.is_deleted:
  682. retlist.append(content.get_label_as_file())
  683. if content.type != CONTENT_TYPES.Folder.slug:
  684. self._file_count += 1
  685. return retlist
  686. def getMemberList(self) -> [_DAVResource]:
  687. members = []
  688. if self.content:
  689. children = self.content.children
  690. else:
  691. children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
  692. for content in children:
  693. if content.is_deleted:
  694. content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
  695. if content.type == CONTENT_TYPES.Folder.slug:
  696. members.append(
  697. FolderResource(
  698. content_path,
  699. self.environ,
  700. self.workspace,
  701. content,
  702. user=self.user,
  703. session=self.session,
  704. ))
  705. elif content.type == CONTENT_TYPES.File.slug:
  706. self._file_count += 1
  707. members.append(
  708. FileResource(
  709. content_path,
  710. self.environ,
  711. content,
  712. user=self.user,
  713. session=self.session,
  714. )
  715. )
  716. else:
  717. self._file_count += 1
  718. members.append(
  719. OtherFileResource(
  720. content_path,
  721. self.environ,
  722. content,
  723. user=self.user,
  724. session=self.session,
  725. ))
  726. if self._file_count > 0 and self.provider.show_history():
  727. members.append(
  728. HistoryFolderResource(
  729. path=self.path + '/' + ".history",
  730. environ=self.environ,
  731. content=self.content,
  732. workspace=self.workspace,
  733. user=self.user,
  734. type=HistoryType.Standard,
  735. session=self.session,
  736. )
  737. )
  738. return members
  739. class ArchivedFolderResource(HistoryFolderResource):
  740. """
  741. A virtual resources which exists for every folder or workspaces which contains their archived children
  742. """
  743. def __init__(
  744. self,
  745. path: str,
  746. environ: dict,
  747. workspace: Workspace,
  748. user: User,
  749. session: Session,
  750. content: Content=None
  751. ):
  752. super(ArchivedFolderResource, self).__init__(
  753. path=path,
  754. environ=environ,
  755. workspace=workspace,
  756. user=user,
  757. content=content,
  758. session=session,
  759. type=HistoryType.Archived
  760. )
  761. self._file_count = 0
  762. def __repr__(self) -> str:
  763. return "<DAVCollection: ArchivedFolderResource (%s)" % self.content.file_name
  764. def getCreationDate(self) -> float:
  765. return mktime(datetime.now().timetuple())
  766. def getDisplayName(self) -> str:
  767. return '.archived'
  768. def getLastModified(self) -> float:
  769. return mktime(datetime.now().timetuple())
  770. def getMember(self, content_label) -> _DAVResource:
  771. content = self.content_api.get_one_by_label_and_parent(
  772. content_label=content_label,
  773. content_parent=self.content
  774. )
  775. return self.provider.getResourceInst(
  776. path=self.path + '/' + transform_to_display(content.get_label_as_file()),
  777. environ=self.environ
  778. )
  779. def getMemberNames(self) -> [str]:
  780. retlist = []
  781. for content in self.content_api.get_all_with_filter(
  782. self.content if self.content is None else self.content.id, CONTENT_TYPES.Any_SLUG):
  783. retlist.append(content.get_label_as_file())
  784. if content.type != CONTENT_TYPES.Folder.slug:
  785. self._file_count += 1
  786. return retlist
  787. def getMemberList(self) -> [_DAVResource]:
  788. members = []
  789. if self.content:
  790. children = self.content.children
  791. else:
  792. children = self.content_api.get_all(False, CONTENT_TYPES.Any_SLUG, self.workspace)
  793. for content in children:
  794. if content.is_archived:
  795. content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
  796. if content.type == CONTENT_TYPES.Folder.slug:
  797. members.append(
  798. FolderResource(
  799. content_path,
  800. self.environ,
  801. self.workspace,
  802. content,
  803. user=self.user,
  804. session=self.session,
  805. ))
  806. elif content.type == CONTENT_TYPES.File.slug:
  807. self._file_count += 1
  808. members.append(
  809. FileResource(
  810. content_path,
  811. self.environ,
  812. content,
  813. user=self.user,
  814. session=self.session,
  815. ))
  816. else:
  817. self._file_count += 1
  818. members.append(
  819. OtherFileResource(
  820. content_path,
  821. self.environ,
  822. content,
  823. user=self.user,
  824. session=self.session,
  825. ))
  826. if self._file_count > 0 and self.provider.show_history():
  827. members.append(
  828. HistoryFolderResource(
  829. path=self.path + '/' + ".history",
  830. environ=self.environ,
  831. content=self.content,
  832. workspace=self.workspace,
  833. user=self.user,
  834. type=HistoryType.Standard,
  835. session=self.session,
  836. )
  837. )
  838. return members
  839. class HistoryFileFolderResource(HistoryFolderResource):
  840. """
  841. A virtual resource that contains for a given content (file/page/thread) all its revisions
  842. """
  843. def __init__(
  844. self,
  845. path: str,
  846. environ: dict,
  847. content: Content,
  848. user: User,
  849. session: Session
  850. ) -> None:
  851. super(HistoryFileFolderResource, self).__init__(
  852. path=path,
  853. environ=environ,
  854. workspace=content.workspace,
  855. content=content,
  856. user=user,
  857. session=session,
  858. type=HistoryType.All,
  859. )
  860. def __repr__(self) -> str:
  861. return "<DAVCollection: HistoryFileFolderResource (%s)" % self.content.file_name
  862. def getDisplayName(self) -> str:
  863. return self.content.get_label_as_file()
  864. def createCollection(self, name):
  865. raise DAVError(HTTP_FORBIDDEN)
  866. def getMemberNames(self) -> [int]:
  867. """
  868. Usually we would return a string, but here as we're working with different
  869. revisions of the same content, we'll work with revision_id
  870. """
  871. ret = []
  872. for content in self.content.revisions:
  873. ret.append(content.revision_id)
  874. return ret
  875. def getMember(self, item_id) -> DAVNonCollection:
  876. revision = self.content_api.get_one_revision(item_id)
  877. left_side = '%s/(%d - %s) ' % (self.path, revision.revision_id, revision.revision_type)
  878. if self.content.type == CONTENT_TYPES.File.slug:
  879. return HistoryFileResource(
  880. path='%s%s' % (left_side, transform_to_display(revision.file_name)),
  881. environ=self.environ,
  882. content=self.content,
  883. content_revision=revision,
  884. session=self.session,
  885. user=self.user,
  886. )
  887. else:
  888. return HistoryOtherFile(
  889. path='%s%s' % (left_side, transform_to_display(revision.get_label_as_file())),
  890. environ=self.environ,
  891. content=self.content,
  892. content_revision=revision,
  893. session=self.session,
  894. user=self.user,
  895. )
  896. def getMemberList(self) -> [_DAVResource]:
  897. members = []
  898. for content in self.content.revisions:
  899. left_side = '%s/(%d - %s) ' % (self.path, content.revision_id, content.revision_type)
  900. if self.content.type == CONTENT_TYPES.File.slug:
  901. members.append(HistoryFileResource(
  902. path='%s%s' % (left_side, transform_to_display(content.file_name)),
  903. environ=self.environ,
  904. content=self.content,
  905. content_revision=content,
  906. user=self.user,
  907. session=self.session,
  908. )
  909. )
  910. else:
  911. members.append(HistoryOtherFile(
  912. path='%s%s' % (left_side, transform_to_display(content.file_name)),
  913. environ=self.environ,
  914. content=self.content,
  915. content_revision=content,
  916. user=self.user,
  917. session=self.session,
  918. )
  919. )
  920. return members
  921. class FileResource(DAVNonCollection):
  922. """
  923. FileResource resource corresponding to tracim's files
  924. """
  925. def __init__(
  926. self,
  927. path: str,
  928. environ: dict,
  929. content: Content,
  930. user: User,
  931. session: Session,
  932. ) -> None:
  933. super(FileResource, self).__init__(path, environ)
  934. self.content = content
  935. self.user = user
  936. self.session = session
  937. self.content_api = ContentApi(
  938. current_user=self.user,
  939. config=self.provider.app_config,
  940. session=self.session,
  941. )
  942. # this is the property that windows client except to check if the file is read-write or read-only,
  943. # but i wasn't able to set this property so you'll have to look into it >.>
  944. # self.setPropertyValue('Win32FileAttributes', '00000021')
  945. def __repr__(self) -> str:
  946. return "<DAVNonCollection: FileResource (%d)>" % self.content.revision_id
  947. def getContentLength(self) -> int:
  948. return self.content.depot_file.file.content_length
  949. def getContentType(self) -> str:
  950. return self.content.file_mimetype
  951. def getCreationDate(self) -> float:
  952. return mktime(self.content.created.timetuple())
  953. def getDisplayName(self) -> str:
  954. return self.content.file_name
  955. def getLastModified(self) -> float:
  956. return mktime(self.content.updated.timetuple())
  957. def getContent(self) -> typing.BinaryIO:
  958. filestream = compat.BytesIO()
  959. filestream.write(self.content.depot_file.file.read())
  960. filestream.seek(0)
  961. return filestream
  962. def beginWrite(self, contentType: str=None) -> FakeFileStream:
  963. return FakeFileStream(
  964. content=self.content,
  965. content_api=self.content_api,
  966. file_name=self.content.get_label_as_file(),
  967. workspace=self.content.workspace,
  968. path=self.path,
  969. session=self.session,
  970. )
  971. def moveRecursive(self, destpath):
  972. """As we support recursive move, copymovesingle won't be called, though with copy it'll be called
  973. but i have to check if the client ever call that function..."""
  974. destpath = normpath(destpath)
  975. invalid_path = False
  976. # if content is either deleted or archived, we'll check that we try moving it to the parent
  977. # if yes, then we'll unarchive / undelete them, else the action's not allowed
  978. if self.content.is_deleted or self.content.is_archived:
  979. # we remove all archived and deleted from the path and we check to the destpath
  980. # has to be equal or else path not valid
  981. # ex: /a/b/.deleted/resource, to be valid destpath has to be = /a/b/resource (no other solution)
  982. current_path = re.sub(r'/\.(deleted|archived)', '', self.path)
  983. if current_path == destpath:
  984. ManageActions(
  985. action_type=ActionDescription.UNDELETION if self.content.is_deleted else ActionDescription.UNARCHIVING,
  986. api=self.content_api,
  987. content=self.content,
  988. session=self.session,
  989. ).action()
  990. else:
  991. invalid_path = True
  992. # if the content is not deleted / archived, check if we're trying to delete / archive it by
  993. # moving it to a .deleted / .archived folder
  994. elif basename(dirname(destpath)) in ['.deleted', '.archived']:
  995. # same test as above ^
  996. dest_path = re.sub(r'/\.(deleted|archived)', '', destpath)
  997. if dest_path == self.path:
  998. ManageActions(
  999. action_type=ActionDescription.DELETION if '.deleted' in destpath else ActionDescription.ARCHIVING,
  1000. api=self.content_api,
  1001. content=self.content,
  1002. session=self.session,
  1003. ).action()
  1004. else:
  1005. invalid_path = True
  1006. # else we check if the path is good (not at the root path / not in a deleted/archived path)
  1007. # and we move the content
  1008. else:
  1009. invalid_path = any(x in destpath for x in ['.deleted', '.archived'])
  1010. invalid_path = invalid_path or any(x in self.path for x in ['.deleted', '.archived'])
  1011. invalid_path = invalid_path or dirname(destpath) == self.environ['http_authenticator.realm']
  1012. if not invalid_path:
  1013. self.move_file(destpath)
  1014. if invalid_path:
  1015. raise DAVError(HTTP_FORBIDDEN)
  1016. def move_file(self, destpath: str) -> None:
  1017. """
  1018. Move file mean changing the path to access to a file. This can mean
  1019. simple renaming(1), moving file from a directory to one another(2)
  1020. but also renaming + moving file from a directory to one another at
  1021. the same time (3).
  1022. (1): move /dir1/file1 -> /dir1/file2
  1023. (2): move /dir1/file1 -> /dir2/file1
  1024. (3): move /dir1/file1 -> /dir2/file2
  1025. :param destpath: destination path of webdav move
  1026. :return: nothing
  1027. """
  1028. workspace = self.content.workspace
  1029. parent = self.content.parent
  1030. with new_revision(
  1031. content=self.content,
  1032. tm=transaction.manager,
  1033. session=self.session,
  1034. ):
  1035. # INFO - G.M - 2018-03-09 - First, renaming file if needed
  1036. if basename(destpath) != self.getDisplayName():
  1037. new_given_file_name = transform_to_bdd(basename(destpath))
  1038. new_file_name, new_file_extension = \
  1039. os.path.splitext(new_given_file_name)
  1040. self.content_api.update_content(
  1041. self.content,
  1042. new_file_name,
  1043. )
  1044. self.content.file_extension = new_file_extension
  1045. self.content_api.save(self.content)
  1046. # INFO - G.M - 2018-03-09 - Moving file if needed
  1047. workspace_api = WorkspaceApi(
  1048. current_user=self.user,
  1049. session=self.session,
  1050. config=self.provider.app_config,
  1051. )
  1052. content_api = ContentApi(
  1053. current_user=self.user,
  1054. session=self.session,
  1055. config=self.provider.app_config
  1056. )
  1057. destination_workspace = self.provider.get_workspace_from_path(
  1058. destpath,
  1059. workspace_api,
  1060. )
  1061. destination_parent = self.provider.get_parent_from_path(
  1062. destpath,
  1063. content_api,
  1064. destination_workspace,
  1065. )
  1066. if destination_parent != parent or destination_workspace != workspace: # nopep8
  1067. # INFO - G.M - 12-03-2018 - Avoid moving the file "at the same place" # nopep8
  1068. # if the request does not result in a real move.
  1069. self.content_api.move(
  1070. item=self.content,
  1071. new_parent=destination_parent,
  1072. must_stay_in_same_workspace=False,
  1073. new_workspace=destination_workspace
  1074. )
  1075. transaction.commit()
  1076. def copyMoveSingle(self, destpath, isMove):
  1077. if isMove:
  1078. # INFO - G.M - 12-03-2018 - This case should not happen
  1079. # As far as moveRecursive method exist, all move should not go
  1080. # through this method. If such case appear, try replace this to :
  1081. ####
  1082. # self.move_file(destpath)
  1083. # return
  1084. ####
  1085. raise NotImplemented
  1086. new_file_name = None
  1087. new_file_extension = None
  1088. # Inspect destpath
  1089. if basename(destpath) != self.getDisplayName():
  1090. new_given_file_name = transform_to_bdd(basename(destpath))
  1091. new_file_name, new_file_extension = \
  1092. os.path.splitext(new_given_file_name)
  1093. workspace_api = WorkspaceApi(
  1094. current_user=self.user,
  1095. session=self.session,
  1096. config=self.provider.app_config,
  1097. )
  1098. content_api = ContentApi(
  1099. current_user=self.user,
  1100. session=self.session,
  1101. config=self.provider.app_config
  1102. )
  1103. destination_workspace = self.provider.get_workspace_from_path(
  1104. destpath,
  1105. workspace_api,
  1106. )
  1107. destination_parent = self.provider.get_parent_from_path(
  1108. destpath,
  1109. content_api,
  1110. destination_workspace,
  1111. )
  1112. workspace = self.content.workspace
  1113. parent = self.content.parent
  1114. new_content = self.content_api.copy(
  1115. item=self.content,
  1116. new_label=new_file_name,
  1117. new_parent=destination_parent,
  1118. )
  1119. self.content_api.copy_children(self.content, new_content)
  1120. transaction.commit()
  1121. def supportRecursiveMove(self, destPath):
  1122. return True
  1123. def delete(self):
  1124. ManageActions(
  1125. action_type=ActionDescription.DELETION,
  1126. api=self.content_api,
  1127. content=self.content,
  1128. session=self.session,
  1129. ).action()
  1130. class HistoryFileResource(FileResource):
  1131. """
  1132. A virtual resource corresponding to a specific tracim's revision's file
  1133. """
  1134. def __init__(self, path: str, environ: dict, content: Content, user: User, session: Session, content_revision: ContentRevisionRO):
  1135. super(HistoryFileResource, self).__init__(path, environ, content, user=user, session=session)
  1136. self.content_revision = content_revision
  1137. def __repr__(self) -> str:
  1138. return "<DAVNonCollection: HistoryFileResource (%s-%s)" % (self.content.content_id, self.content.file_name)
  1139. def getDisplayName(self) -> str:
  1140. left_side = '(%d - %s) ' % (self.content_revision.revision_id, self.content_revision.revision_type)
  1141. return '%s%s' % (left_side, transform_to_display(self.content_revision.file_name))
  1142. def getContent(self):
  1143. filestream = compat.BytesIO()
  1144. filestream.write(self.content_revision.depot_file.file.read())
  1145. filestream.seek(0)
  1146. return filestream
  1147. def getContentLength(self):
  1148. return self.content_revision.depot_file.file.content_length
  1149. def getContentType(self) -> str:
  1150. return self.content_revision.file_mimetype
  1151. def beginWrite(self, contentType=None):
  1152. raise DAVError(HTTP_FORBIDDEN)
  1153. def delete(self):
  1154. raise DAVError(HTTP_FORBIDDEN)
  1155. def copyMoveSingle(self, destpath, ismove):
  1156. raise DAVError(HTTP_FORBIDDEN)
  1157. class OtherFileResource(FileResource):
  1158. """
  1159. FileResource resource corresponding to tracim's page and thread
  1160. """
  1161. def __init__(self, path: str, environ: dict, content: Content, user:User, session: Session):
  1162. super(OtherFileResource, self).__init__(path, environ, content, user=user, session=session)
  1163. self.content_revision = self.content.revision
  1164. self.content_designed = self.design()
  1165. # workaround for consistent request as we have to return a resource with a path ending with .html
  1166. # when entering folder for windows, but only once because when we select it again it would have .html.html
  1167. # which is no good
  1168. if not self.path.endswith('.html'):
  1169. self.path += '.html'
  1170. def getDisplayName(self) -> str:
  1171. return self.content.get_label_as_file()
  1172. def getPreferredPath(self):
  1173. return self.path
  1174. def __repr__(self) -> str:
  1175. return "<DAVNonCollection: OtherFileResource (%s)" % self.content.file_name
  1176. def getContentLength(self) -> int:
  1177. return len(self.content_designed)
  1178. def getContentType(self) -> str:
  1179. return 'text/html'
  1180. def getContent(self):
  1181. filestream = compat.BytesIO()
  1182. filestream.write(bytes(self.content_designed, 'utf-8'))
  1183. filestream.seek(0)
  1184. return filestream
  1185. def design(self):
  1186. if self.content.type == CONTENT_TYPES.Page.slug:
  1187. return designPage(self.content, self.content_revision)
  1188. else:
  1189. return designThread(
  1190. self.content,
  1191. self.content_revision,
  1192. self.content_api.get_all(self.content.content_id, CONTENT_TYPES.Comment.slug)
  1193. )
  1194. class HistoryOtherFile(OtherFileResource):
  1195. """
  1196. A virtual resource corresponding to a specific tracim's revision's page and thread
  1197. """
  1198. def __init__(self,
  1199. path: str,
  1200. environ: dict,
  1201. content: Content,
  1202. user:User,
  1203. content_revision: ContentRevisionRO,
  1204. session: Session):
  1205. super(HistoryOtherFile, self).__init__(
  1206. path,
  1207. environ,
  1208. content,
  1209. user=user,
  1210. session=session
  1211. )
  1212. self.content_revision = content_revision
  1213. self.content_designed = self.design()
  1214. def __repr__(self) -> str:
  1215. return "<DAVNonCollection: HistoryOtherFile (%s-%s)" % (self.content.file_name, self.content.id)
  1216. def getDisplayName(self) -> str:
  1217. left_side = '(%d - %s) ' % (self.content_revision.revision_id, self.content_revision.revision_type)
  1218. return '%s%s' % (left_side, transform_to_display(self.content_revision.get_label_as_file()))
  1219. def getContent(self):
  1220. filestream = compat.BytesIO()
  1221. filestream.write(bytes(self.content_designed, 'utf-8'))
  1222. filestream.seek(0)
  1223. return filestream
  1224. def delete(self):
  1225. raise DAVError(HTTP_FORBIDDEN)
  1226. def copyMoveSingle(self, destpath, ismove):
  1227. raise DAVError(HTTP_FORBIDDEN)