resources.py 49KB

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