sql_resources.py 41KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159
  1. # coding: utf8
  2. import logging
  3. import os
  4. import tg
  5. import transaction
  6. import typing
  7. import re
  8. from datetime import datetime
  9. from time import mktime
  10. from os.path import dirname, basename
  11. from tracim.lib.content import ContentApi
  12. from tracim.lib.user import UserApi
  13. from tracim.lib.webdav import HistoryType
  14. from tracim.lib.webdav import FakeFileStream
  15. from tracim.lib.webdav.utils import transform_to_display
  16. from tracim.lib.webdav.utils import transform_to_bdd
  17. from tracim.lib.workspace import WorkspaceApi
  18. from tracim.model import data, new_revision
  19. from tracim.model.data import Content, ActionDescription
  20. from tracim.model.data import ContentType
  21. from tracim.lib.webdav.design import designThread, designPage
  22. from wsgidav import compat
  23. from wsgidav.dav_error import DAVError
  24. from wsgidav.dav_error import HTTP_FORBIDDEN
  25. from wsgidav.dav_error import HTTP_NOT_FOUND
  26. from wsgidav.dav_provider import DAVCollection, DAVNonCollection
  27. from wsgidav.dav_provider import _DAVResource
  28. from tracim.lib.webdav.utils import normpath
  29. from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
  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, action_type: str, api: ContentApi, content: Content):
  37. self.content_api = api
  38. self.content = content
  39. self._actions = {
  40. ActionDescription.ARCHIVING: self.content_api.archive,
  41. ActionDescription.DELETION: self.content_api.delete,
  42. ActionDescription.UNARCHIVING: self.content_api.unarchive,
  43. ActionDescription.UNDELETION: self.content_api.undelete
  44. }
  45. self._type = action_type
  46. def action(self):
  47. with new_revision(self.content):
  48. self._actions[self._type](self.content)
  49. self.content_api.save(self.content, self._type)
  50. transaction.commit()
  51. class Root(DAVCollection):
  52. """
  53. Root ressource that represents tracim's home, which contains all workspaces
  54. """
  55. def __init__(self, path: str, environ: dict):
  56. super(Root, self).__init__(path, environ)
  57. self.user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
  58. # TODO BS 20170221: Web interface should list all workspace to. We
  59. # disable it here for moment. When web interface will be updated to
  60. # list all workspace, change this here to.
  61. self.workspace_api = WorkspaceApi(self.user, force_role=True)
  62. def __repr__(self) -> str:
  63. return '<DAVCollection: Root>'
  64. def getMemberNames(self) -> [str]:
  65. """
  66. This method returns the names (here workspace's labels) of all its children
  67. Though for perfomance issue, we're not using this function anymore
  68. """
  69. return [workspace.label for workspace in self.workspace_api.get_all()]
  70. def getMember(self, label: str) -> DAVCollection:
  71. """
  72. This method returns the child Workspace that corresponds to a given name
  73. Though for perfomance issue, we're not using this function anymore
  74. """
  75. try:
  76. workspace = self.workspace_api.get_one_by_label(label)
  77. workspace_path = '%s%s%s' % (self.path, '' if self.path == '/' else '/', transform_to_display(workspace.label))
  78. return Workspace(workspace_path, self.environ, workspace)
  79. except AttributeError:
  80. return None
  81. def createEmptyResource(self, name: str):
  82. """
  83. This method is called whenever the user wants to create a DAVNonCollection resource (files in our case).
  84. There we don't allow to create files at the root;
  85. only workspaces (thus collection) can be created.
  86. """
  87. raise DAVError(HTTP_FORBIDDEN)
  88. def createCollection(self, name: str):
  89. """
  90. This method is called whenever the user wants to create a DAVCollection resource as a child (in our case,
  91. we create workspaces as this is the root).
  92. [For now] we don't allow to create new workspaces through
  93. webdav client. Though if we come to allow it, deleting the error's raise will
  94. make it possible.
  95. """
  96. # TODO : remove comment here
  97. # raise DAVError(HTTP_FORBIDDEN)
  98. new_workspace = self.workspace_api.create_workspace(name)
  99. self.workspace_api.save(new_workspace)
  100. workspace_path = '%s%s%s' % (
  101. self.path, '' if self.path == '/' else '/', transform_to_display(new_workspace.label))
  102. transaction.commit()
  103. return Workspace(workspace_path, self.environ, new_workspace)
  104. def getMemberList(self):
  105. """
  106. This method is called by wsgidav when requesting with a depth > 0, it will return a list of _DAVResource
  107. of all its direct children
  108. """
  109. members = []
  110. for workspace in self.workspace_api.get_all():
  111. workspace_path = '%s%s%s' % (self.path, '' if self.path == '/' else '/', workspace.label)
  112. members.append(Workspace(workspace_path, self.environ, workspace))
  113. return members
  114. class Workspace(DAVCollection):
  115. """
  116. Workspace resource corresponding to tracim's workspaces.
  117. Direct children can only be folders, though files might come later on and are supported
  118. """
  119. def __init__(self, path: str, environ: dict, workspace: data.Workspace):
  120. super(Workspace, self).__init__(path, environ)
  121. self.workspace = workspace
  122. self.content = None
  123. self.user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
  124. self.content_api = ContentApi(self.user, show_temporary=True)
  125. self._file_count = 0
  126. def __repr__(self) -> str:
  127. return "<DAVCollection: Workspace (%d)>" % self.workspace.workspace_id
  128. def getPreferredPath(self):
  129. return self.path
  130. def getCreationDate(self) -> float:
  131. return mktime(self.workspace.created.timetuple())
  132. def getDisplayName(self) -> str:
  133. return self.workspace.label
  134. def getLastModified(self) -> float:
  135. return mktime(self.workspace.updated.timetuple())
  136. def getMemberNames(self) -> [str]:
  137. retlist = []
  138. children = self.content_api.get_all(
  139. parent_id=self.content.id if self.content is not None else None,
  140. workspace=self.workspace
  141. )
  142. for content in children:
  143. # the purpose is to display .history only if there's at least one content's type that has a history
  144. if content.type != ContentType.Folder:
  145. self._file_count += 1
  146. retlist.append(content.get_label_as_file())
  147. return retlist
  148. def getMember(self, content_label: str) -> _DAVResource:
  149. return self.provider.getResourceInst(
  150. '%s/%s' % (self.path, transform_to_display(content_label)),
  151. self.environ
  152. )
  153. def createEmptyResource(self, file_name: str):
  154. """
  155. [For now] we don't allow to create files right under workspaces.
  156. Though if we come to allow it, deleting the error's raise will make it possible.
  157. """
  158. # TODO : remove commentary here raise DAVError(HTTP_FORBIDDEN)
  159. if '/.deleted/' in self.path or '/.archived/' in self.path:
  160. raise DAVError(HTTP_FORBIDDEN)
  161. content = None
  162. # Note: To prevent bugs, check here again if resource already exist
  163. path = os.path.join(self.path, file_name)
  164. resource = self.provider.getResourceInst(path, self.environ)
  165. if resource:
  166. content = resource.content
  167. if not content:
  168. # INFO - G.M - 21-03-2018 create new empty file and commit it.
  169. new_empty_file = FakeFileStream(
  170. file_name=file_name,
  171. content_api=self.content_api,
  172. workspace=self.workspace,
  173. content=content,
  174. parent=self.content,
  175. path=path,
  176. )
  177. new_empty_file.close()
  178. # INFO - G.M - 21-03-2018 - Obtain true davFile from provider
  179. resource = self.provider.getResourceInst(path, self.environ)
  180. if not resource:
  181. raise DAVError(HTTP_NOT_FOUND)
  182. content = resource.content
  183. return File(path=path, content=content, environ=self.environ)
  184. def createCollection(self, label: str) -> 'Folder':
  185. """
  186. Create a new folder for the current workspace. As it's not possible for the user to choose
  187. which types of content are allowed in this folder, we allow allow all of them.
  188. This method return the DAVCollection created.
  189. """
  190. if '/.deleted/' in self.path or '/.archived/' in self.path:
  191. raise DAVError(HTTP_FORBIDDEN)
  192. folder = self.content_api.create(
  193. content_type=ContentType.Folder,
  194. workspace=self.workspace,
  195. label=label,
  196. parent=self.content
  197. )
  198. subcontent = dict(
  199. folder=True,
  200. thread=True,
  201. file=True,
  202. page=True
  203. )
  204. self.content_api.set_allowed_content(folder, subcontent)
  205. self.content_api.save(folder)
  206. transaction.commit()
  207. return Folder('%s/%s' % (self.path, transform_to_display(label)),
  208. self.environ, folder,
  209. self.workspace)
  210. def delete(self):
  211. """For now, it is not possible to delete a workspace through the webdav client."""
  212. raise DAVError(HTTP_FORBIDDEN)
  213. def supportRecursiveMove(self, destpath):
  214. return True
  215. def moveRecursive(self, destpath):
  216. if dirname(normpath(destpath)) == self.environ['http_authenticator.realm']:
  217. self.workspace.label = basename(normpath(destpath))
  218. transaction.commit()
  219. else:
  220. raise DAVError(HTTP_FORBIDDEN)
  221. def getMemberList(self) -> [_DAVResource]:
  222. members = []
  223. children = self.content_api.get_all(False, ContentType.Any, self.workspace)
  224. for content in children:
  225. content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
  226. if content.type == ContentType.Folder:
  227. members.append(Folder(content_path, self.environ, self.workspace, content))
  228. elif content.type == ContentType.File:
  229. self._file_count += 1
  230. members.append(File(content_path, self.environ, content))
  231. else:
  232. self._file_count += 1
  233. members.append(OtherFile(content_path, self.environ, content))
  234. if self._file_count > 0 and self.provider.show_history():
  235. members.append(
  236. HistoryFolder(
  237. path=self.path + '/' + ".history",
  238. environ=self.environ,
  239. content=self.content,
  240. workspace=self.workspace,
  241. type=HistoryType.Standard
  242. )
  243. )
  244. if self.provider.show_delete():
  245. members.append(
  246. DeletedFolder(
  247. path=self.path + '/' + ".deleted",
  248. environ=self.environ,
  249. content=self.content,
  250. workspace=self.workspace
  251. )
  252. )
  253. if self.provider.show_archive():
  254. members.append(
  255. ArchivedFolder(
  256. path=self.path + '/' + ".archived",
  257. environ=self.environ,
  258. content=self.content,
  259. workspace=self.workspace
  260. )
  261. )
  262. return members
  263. class Folder(Workspace):
  264. """
  265. Folder resource corresponding to tracim's folders.
  266. Direct children can only be either folder, files, pages or threads
  267. By default when creating new folders, we allow them to contain all types of content
  268. """
  269. def __init__(self, path: str, environ: dict, workspace: data.Workspace, content: data.Content):
  270. super(Folder, self).__init__(path, environ, workspace)
  271. self.content = content
  272. def __repr__(self) -> str:
  273. return "<DAVCollection: Folder (%s)>" % self.content.label
  274. def getCreationDate(self) -> float:
  275. return mktime(self.content.created.timetuple())
  276. def getDisplayName(self) -> str:
  277. return transform_to_display(self.content.get_label_as_file())
  278. def getLastModified(self) -> float:
  279. return mktime(self.content.updated.timetuple())
  280. def delete(self):
  281. ManageActions(ActionDescription.DELETION, self.content_api, self.content).action()
  282. def supportRecursiveMove(self, destpath: str):
  283. return True
  284. def moveRecursive(self, destpath: str):
  285. """
  286. As we support recursive move, copymovesingle won't be called, though with copy it'll be called
  287. but i have to check if the client ever call that function...
  288. """
  289. destpath = normpath(destpath)
  290. invalid_path = False
  291. # if content is either deleted or archived, we'll check that we try moving it to the parent
  292. # if yes, then we'll unarchive / undelete them, else the action's not allowed
  293. if self.content.is_deleted or self.content.is_archived:
  294. # we remove all archived and deleted from the path and we check to the destpath
  295. # has to be equal or else path not valid
  296. # ex: /a/b/.deleted/resource, to be valid destpath has to be = /a/b/resource (no other solution)
  297. current_path = re.sub(r'/\.(deleted|archived)', '', self.path)
  298. if current_path == destpath:
  299. ManageActions(
  300. ActionDescription.UNDELETION if self.content.is_deleted else ActionDescription.UNARCHIVING,
  301. self.content_api,
  302. self.content
  303. ).action()
  304. else:
  305. invalid_path = True
  306. # if the content is not deleted / archived, check if we're trying to delete / archive it by
  307. # moving it to a .deleted / .archived folder
  308. elif basename(dirname(destpath)) in ['.deleted', '.archived']:
  309. # same test as above ^
  310. dest_path = re.sub(r'/\.(deleted|archived)', '', destpath)
  311. if dest_path == self.path:
  312. ManageActions(
  313. ActionDescription.DELETION if '.deleted' in destpath else ActionDescription.ARCHIVING,
  314. self.content_api,
  315. self.content
  316. ).action()
  317. else:
  318. invalid_path = True
  319. # else we check if the path is good (not at the root path / not in a deleted/archived path)
  320. # and we move the content
  321. else:
  322. invalid_path = any(x in destpath for x in ['.deleted', '.archived'])
  323. invalid_path = invalid_path or any(x in self.path for x in ['.deleted', '.archived'])
  324. invalid_path = invalid_path or dirname(destpath) == self.environ['http_authenticator.realm']
  325. if not invalid_path:
  326. self.move_folder(destpath)
  327. if invalid_path:
  328. raise DAVError(HTTP_FORBIDDEN)
  329. def move_folder(self, destpath):
  330. workspace_api = WorkspaceApi(self.user)
  331. workspace = self.provider.get_workspace_from_path(
  332. normpath(destpath), workspace_api
  333. )
  334. parent = self.provider.get_parent_from_path(
  335. normpath(destpath),
  336. self.content_api,
  337. workspace
  338. )
  339. with new_revision(self.content):
  340. if basename(destpath) != self.getDisplayName():
  341. self.content_api.update_content(self.content, transform_to_bdd(basename(destpath)))
  342. self.content_api.save(self.content)
  343. else:
  344. if workspace.workspace_id == self.content.workspace.workspace_id:
  345. self.content_api.move(self.content, parent)
  346. else:
  347. self.content_api.move_recursively(self.content, parent, workspace)
  348. transaction.commit()
  349. def getMemberList(self) -> [_DAVResource]:
  350. members = []
  351. content_api = ContentApi(self.user)
  352. visible_children = content_api.get_all(
  353. self.content.content_id,
  354. ContentType.Any,
  355. self.workspace,
  356. )
  357. for content in visible_children:
  358. content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
  359. try:
  360. if content.type == ContentType.Folder:
  361. members.append(Folder(content_path, self.environ, self.workspace, content))
  362. elif content.type == ContentType.File:
  363. self._file_count += 1
  364. members.append(File(content_path, self.environ, content))
  365. else:
  366. self._file_count += 1
  367. members.append(OtherFile(content_path, self.environ, content))
  368. except Exception as exc:
  369. logger.exception(
  370. 'Unable to construct member {}'.format(
  371. content_path,
  372. ),
  373. exc_info=True,
  374. )
  375. if self._file_count > 0 and self.provider.show_history():
  376. members.append(
  377. HistoryFolder(
  378. path=self.path + '/' + ".history",
  379. environ=self.environ,
  380. content=self.content,
  381. workspace=self.workspace,
  382. type=HistoryType.Standard
  383. )
  384. )
  385. if self.provider.show_delete():
  386. members.append(
  387. DeletedFolder(
  388. path=self.path + '/' + ".deleted",
  389. environ=self.environ,
  390. content=self.content,
  391. workspace=self.workspace
  392. )
  393. )
  394. if self.provider.show_archive():
  395. members.append(
  396. ArchivedFolder(
  397. path=self.path + '/' + ".archived",
  398. environ=self.environ,
  399. content=self.content,
  400. workspace=self.workspace
  401. )
  402. )
  403. return members
  404. class HistoryFolder(Folder):
  405. """
  406. A virtual resource which contains a sub-folder for every files (DAVNonCollection) contained in the parent
  407. folder
  408. """
  409. def __init__(self, path, environ, workspace: data.Workspace,
  410. content: data.Content=None, type: str=HistoryType.Standard):
  411. super(HistoryFolder, self).__init__(path, environ, workspace, content)
  412. self._is_archived = type == HistoryType.Archived
  413. self._is_deleted = type == HistoryType.Deleted
  414. self.content_api = ContentApi(
  415. current_user=self.user,
  416. show_archived=self._is_archived,
  417. show_deleted=self._is_deleted
  418. )
  419. def __repr__(self) -> str:
  420. return "<DAVCollection: HistoryFolder (%s)>" % self.content.file_name
  421. def getCreationDate(self) -> float:
  422. return mktime(datetime.now().timetuple())
  423. def getDisplayName(self) -> str:
  424. return '.history'
  425. def getLastModified(self) -> float:
  426. return mktime(datetime.now().timetuple())
  427. def getMember(self, content_label: str) -> _DAVResource:
  428. content = self.content_api.get_one_by_label_and_parent(
  429. content_label=content_label,
  430. content_parent=self.content
  431. )
  432. return HistoryFileFolder(
  433. path='%s/%s' % (self.path, content.get_label_as_file()),
  434. environ=self.environ,
  435. content=content)
  436. def getMemberNames(self) -> [str]:
  437. ret = []
  438. content_id = None if self.content is None else self.content.id
  439. for content in self.content_api.get_all(content_id, ContentType.Any, self.workspace):
  440. if (self._is_archived and content.is_archived or
  441. self._is_deleted and content.is_deleted or
  442. not (content.is_archived or self._is_archived or content.is_deleted or self._is_deleted))\
  443. and content.type != ContentType.Folder:
  444. ret.append(content.get_label_as_file())
  445. return ret
  446. def createEmptyResource(self, name: str):
  447. raise DAVError(HTTP_FORBIDDEN)
  448. def createCollection(self, name: str):
  449. raise DAVError(HTTP_FORBIDDEN)
  450. def delete(self):
  451. raise DAVError(HTTP_FORBIDDEN)
  452. def handleDelete(self):
  453. return True
  454. def handleCopy(self, destPath: str, depthInfinity):
  455. return True
  456. def handleMove(self, destPath: str):
  457. return True
  458. def getMemberList(self) -> [_DAVResource]:
  459. members = []
  460. if self.content:
  461. children = self.content.children
  462. else:
  463. children = self.content_api.get_all(False, ContentType.Any, self.workspace)
  464. for content in children:
  465. if content.is_archived == self._is_archived and content.is_deleted == self._is_deleted:
  466. members.append(HistoryFileFolder(
  467. path='%s/%s' % (self.path, content.get_label_as_file()),
  468. environ=self.environ,
  469. content=content))
  470. return members
  471. class DeletedFolder(HistoryFolder):
  472. """
  473. A virtual resources which exists for every folder or workspaces which contains their deleted children
  474. """
  475. def __init__(self, path: str, environ: dict, workspace: data.Workspace, content: data.Content=None):
  476. super(DeletedFolder, self).__init__(path, environ, workspace, content, HistoryType.Deleted)
  477. self._file_count = 0
  478. def __repr__(self):
  479. return "<DAVCollection: DeletedFolder (%s)" % self.content.file_name
  480. def getCreationDate(self) -> float:
  481. return mktime(datetime.now().timetuple())
  482. def getDisplayName(self) -> str:
  483. return '.deleted'
  484. def getLastModified(self) -> float:
  485. return mktime(datetime.now().timetuple())
  486. def getMember(self, content_label) -> _DAVResource:
  487. content = self.content_api.get_one_by_label_and_parent(
  488. content_label=content_label,
  489. content_parent=self.content
  490. )
  491. return self.provider.getResourceInst(
  492. path='%s/%s' % (self.path, transform_to_display(content.get_label_as_file())),
  493. environ=self.environ
  494. )
  495. def getMemberNames(self) -> [str]:
  496. retlist = []
  497. if self.content:
  498. children = self.content.children
  499. else:
  500. children = self.content_api.get_all(False, ContentType.Any, self.workspace)
  501. for content in children:
  502. if content.is_deleted:
  503. retlist.append(content.get_label_as_file())
  504. if content.type != ContentType.Folder:
  505. self._file_count += 1
  506. return retlist
  507. def getMemberList(self) -> [_DAVResource]:
  508. members = []
  509. if self.content:
  510. children = self.content.children
  511. else:
  512. children = self.content_api.get_all(False, ContentType.Any, self.workspace)
  513. for content in children:
  514. if content.is_deleted:
  515. content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
  516. if content.type == ContentType.Folder:
  517. members.append(Folder(content_path, self.environ, self.workspace, content))
  518. elif content.type == ContentType.File:
  519. self._file_count += 1
  520. members.append(File(content_path, self.environ, content))
  521. else:
  522. self._file_count += 1
  523. members.append(OtherFile(content_path, self.environ, content))
  524. if self._file_count > 0 and self.provider.show_history():
  525. members.append(
  526. HistoryFolder(
  527. path=self.path + '/' + ".history",
  528. environ=self.environ,
  529. content=self.content,
  530. workspace=self.workspace,
  531. type=HistoryType.Standard
  532. )
  533. )
  534. return members
  535. class ArchivedFolder(HistoryFolder):
  536. """
  537. A virtual resources which exists for every folder or workspaces which contains their archived children
  538. """
  539. def __init__(self, path: str, environ: dict, workspace: data.Workspace, content: data.Content=None):
  540. super(ArchivedFolder, self).__init__(path, environ, workspace, content, HistoryType.Archived)
  541. self._file_count = 0
  542. def __repr__(self) -> str:
  543. return "<DAVCollection: ArchivedFolder (%s)" % self.content.file_name
  544. def getCreationDate(self) -> float:
  545. return mktime(datetime.now().timetuple())
  546. def getDisplayName(self) -> str:
  547. return '.archived'
  548. def getLastModified(self) -> float:
  549. return mktime(datetime.now().timetuple())
  550. def getMember(self, content_label) -> _DAVResource:
  551. content = self.content_api.get_one_by_label_and_parent(
  552. content_label=content_label,
  553. content_parent=self.content
  554. )
  555. return self.provider.getResourceInst(
  556. path=self.path + '/' + transform_to_display(content.get_label_as_file()),
  557. environ=self.environ
  558. )
  559. def getMemberNames(self) -> [str]:
  560. retlist = []
  561. for content in self.content_api.get_all_with_filter(
  562. self.content if self.content is None else self.content.id, ContentType.Any):
  563. retlist.append(content.get_label_as_file())
  564. if content.type != ContentType.Folder:
  565. self._file_count += 1
  566. return retlist
  567. def getMemberList(self) -> [_DAVResource]:
  568. members = []
  569. if self.content:
  570. children = self.content.children
  571. else:
  572. children = self.content_api.get_all(False, ContentType.Any, self.workspace)
  573. for content in children:
  574. if content.is_archived:
  575. content_path = '%s/%s' % (self.path, transform_to_display(content.get_label_as_file()))
  576. if content.type == ContentType.Folder:
  577. members.append(Folder(content_path, self.environ, self.workspace, content))
  578. elif content.type == ContentType.File:
  579. self._file_count += 1
  580. members.append(File(content_path, self.environ, content))
  581. else:
  582. self._file_count += 1
  583. members.append(OtherFile(content_path, self.environ, content))
  584. if self._file_count > 0 and self.provider.show_history():
  585. members.append(
  586. HistoryFolder(
  587. path=self.path + '/' + ".history",
  588. environ=self.environ,
  589. content=self.content,
  590. workspace=self.workspace,
  591. type=HistoryType.Standard
  592. )
  593. )
  594. return members
  595. class HistoryFileFolder(HistoryFolder):
  596. """
  597. A virtual resource that contains for a given content (file/page/thread) all its revisions
  598. """
  599. def __init__(self, path: str, environ: dict, content: data.Content):
  600. super(HistoryFileFolder, self).__init__(path, environ, content.workspace, content, HistoryType.All)
  601. def __repr__(self) -> str:
  602. return "<DAVCollection: HistoryFileFolder (%s)" % self.content.file_name
  603. def getDisplayName(self) -> str:
  604. return self.content.get_label_as_file()
  605. def createCollection(self, name):
  606. raise DAVError(HTTP_FORBIDDEN)
  607. def getMemberNames(self) -> [int]:
  608. """
  609. Usually we would return a string, but here as we're working with different
  610. revisions of the same content, we'll work with revision_id
  611. """
  612. ret = []
  613. for content in self.content.revisions:
  614. ret.append(content.revision_id)
  615. return ret
  616. def getMember(self, item_id) -> DAVCollection:
  617. revision = self.content_api.get_one_revision(item_id)
  618. left_side = '%s/(%d - %s) ' % (self.path, revision.revision_id, revision.revision_type)
  619. if self.content.type == ContentType.File:
  620. return HistoryFile(
  621. path='%s%s' % (left_side, transform_to_display(revision.file_name)),
  622. environ=self.environ,
  623. content=self.content,
  624. content_revision=revision)
  625. else:
  626. return HistoryOtherFile(
  627. path='%s%s' % (left_side, transform_to_display(revision.get_label_as_file())),
  628. environ=self.environ,
  629. content=self.content,
  630. content_revision=revision)
  631. def getMemberList(self) -> [_DAVResource]:
  632. members = []
  633. for content in self.content.revisions:
  634. left_side = '%s/(%d - %s) ' % (self.path, content.revision_id, content.revision_type)
  635. if self.content.type == ContentType.File:
  636. members.append(HistoryFile(
  637. path='%s%s' % (left_side, transform_to_display(content.file_name)),
  638. environ=self.environ,
  639. content=self.content,
  640. content_revision=content)
  641. )
  642. else:
  643. members.append(HistoryOtherFile(
  644. path='%s%s' % (left_side, transform_to_display(content.file_name)),
  645. environ=self.environ,
  646. content=self.content,
  647. content_revision=content)
  648. )
  649. return members
  650. class File(DAVNonCollection):
  651. """
  652. File resource corresponding to tracim's files
  653. """
  654. def __init__(self, path: str, environ: dict, content: Content):
  655. super(File, self).__init__(path, environ)
  656. self.content = content
  657. self.user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
  658. self.content_api = ContentApi(self.user)
  659. # this is the property that windows client except to check if the file is read-write or read-only,
  660. # but i wasn't able to set this property so you'll have to look into it >.>
  661. # self.setPropertyValue('Win32FileAttributes', '00000021')
  662. def __repr__(self) -> str:
  663. return "<DAVNonCollection: File (%d)>" % self.content.revision_id
  664. def getContentLength(self) -> int:
  665. return self.content.depot_file.file.content_length
  666. def getContentType(self) -> str:
  667. return self.content.file_mimetype
  668. def getCreationDate(self) -> float:
  669. return mktime(self.content.created.timetuple())
  670. def getDisplayName(self) -> str:
  671. return self.content.file_name
  672. def getLastModified(self) -> float:
  673. return mktime(self.content.updated.timetuple())
  674. def getContent(self) -> typing.BinaryIO:
  675. filestream = compat.BytesIO()
  676. filestream.write(self.content.depot_file.file.read())
  677. filestream.seek(0)
  678. return filestream
  679. def beginWrite(self, contentType: str=None) -> FakeFileStream:
  680. return FakeFileStream(
  681. content=self.content,
  682. content_api=self.content_api,
  683. file_name=self.content.get_label_as_file(),
  684. workspace=self.content.workspace,
  685. path=self.path
  686. )
  687. def moveRecursive(self, destpath):
  688. """As we support recursive move, copymovesingle won't be called, though with copy it'll be called
  689. but i have to check if the client ever call that function..."""
  690. destpath = normpath(destpath)
  691. invalid_path = False
  692. # if content is either deleted or archived, we'll check that we try moving it to the parent
  693. # if yes, then we'll unarchive / undelete them, else the action's not allowed
  694. if self.content.is_deleted or self.content.is_archived:
  695. # we remove all archived and deleted from the path and we check to the destpath
  696. # has to be equal or else path not valid
  697. # ex: /a/b/.deleted/resource, to be valid destpath has to be = /a/b/resource (no other solution)
  698. current_path = re.sub(r'/\.(deleted|archived)', '', self.path)
  699. if current_path == destpath:
  700. ManageActions(
  701. ActionDescription.UNDELETION if self.content.is_deleted else ActionDescription.UNARCHIVING,
  702. self.content_api,
  703. self.content
  704. ).action()
  705. else:
  706. invalid_path = True
  707. # if the content is not deleted / archived, check if we're trying to delete / archive it by
  708. # moving it to a .deleted / .archived folder
  709. elif basename(dirname(destpath)) in ['.deleted', '.archived']:
  710. # same test as above ^
  711. dest_path = re.sub(r'/\.(deleted|archived)', '', destpath)
  712. if dest_path == self.path:
  713. ManageActions(
  714. ActionDescription.DELETION if '.deleted' in destpath else ActionDescription.ARCHIVING,
  715. self.content_api,
  716. self.content
  717. ).action()
  718. else:
  719. invalid_path = True
  720. # else we check if the path is good (not at the root path / not in a deleted/archived path)
  721. # and we move the content
  722. else:
  723. invalid_path = any(x in destpath for x in ['.deleted', '.archived'])
  724. invalid_path = invalid_path or any(x in self.path for x in ['.deleted', '.archived'])
  725. invalid_path = invalid_path or dirname(destpath) == self.environ['http_authenticator.realm']
  726. if not invalid_path:
  727. self.move_file(destpath)
  728. if invalid_path:
  729. raise DAVError(HTTP_FORBIDDEN)
  730. def move_file(self, destpath: str) -> None:
  731. """
  732. Move file mean changing the path to access to a file. This can mean
  733. simple renaming(1), moving file from a directory to one another(2)
  734. but also renaming + moving file from a directory to one another at
  735. the same time (3).
  736. (1): move /dir1/file1 -> /dir1/file2
  737. (2): move /dir1/file1 -> /dir2/file1
  738. (3): move /dir1/file1 -> /dir2/file2
  739. :param destpath: destination path of webdav move
  740. :return: nothing
  741. """
  742. workspace = self.content.workspace
  743. parent = self.content.parent
  744. with new_revision(self.content):
  745. # INFO - G.M - 2018-03-09 - First, renaming file if needed
  746. if basename(destpath) != self.getDisplayName():
  747. new_given_file_name = transform_to_bdd(basename(destpath))
  748. new_file_name, new_file_extension = \
  749. os.path.splitext(new_given_file_name)
  750. self.content_api.update_content(
  751. self.content,
  752. new_file_name,
  753. )
  754. self.content.file_extension = new_file_extension
  755. self.content_api.save(self.content)
  756. # INFO - G.M - 2018-03-09 - Moving file if needed
  757. workspace_api = WorkspaceApi(self.user)
  758. content_api = ContentApi(self.user)
  759. destination_workspace = self.provider.get_workspace_from_path(
  760. destpath,
  761. workspace_api,
  762. )
  763. destination_parent = self.provider.get_parent_from_path(
  764. destpath,
  765. content_api,
  766. destination_workspace,
  767. )
  768. if destination_parent != parent or destination_workspace != workspace: # nopep8
  769. # INFO - G.M - 12-03-2018 - Avoid moving the file "at the same place" # nopep8
  770. # if the request does not result in a real move.
  771. self.content_api.move(
  772. item=self.content,
  773. new_parent=destination_parent,
  774. must_stay_in_same_workspace=False,
  775. new_workspace=destination_workspace
  776. )
  777. transaction.commit()
  778. def copyMoveSingle(self, destpath, isMove):
  779. if isMove:
  780. # INFO - G.M - 12-03-2018 - This case should not happen
  781. # As far as moveRecursive method exist, all move should not go
  782. # through this method. If such case appear, try replace this to :
  783. ####
  784. # self.move_file(destpath)
  785. # return
  786. ####
  787. raise NotImplemented
  788. new_file_name = None
  789. new_file_extension = None
  790. # Inspect destpath
  791. if basename(destpath) != self.getDisplayName():
  792. new_given_file_name = transform_to_bdd(basename(destpath))
  793. new_file_name, new_file_extension = \
  794. os.path.splitext(new_given_file_name)
  795. workspace_api = WorkspaceApi(self.user)
  796. content_api = ContentApi(self.user)
  797. destination_workspace = self.provider.get_workspace_from_path(
  798. destpath,
  799. workspace_api,
  800. )
  801. destination_parent = self.provider.get_parent_from_path(
  802. destpath,
  803. content_api,
  804. destination_workspace,
  805. )
  806. workspace = self.content.workspace
  807. parent = self.content.parent
  808. new_content = self.content_api.copy(
  809. item=self.content,
  810. new_label=new_file_name,
  811. new_parent=destination_parent,
  812. )
  813. self.content_api.copy_children(self.content, new_content)
  814. transaction.commit()
  815. def supportRecursiveMove(self, destPath):
  816. return True
  817. def delete(self):
  818. ManageActions(ActionDescription.DELETION, self.content_api, self.content).action()
  819. class HistoryFile(File):
  820. """
  821. A virtual resource corresponding to a specific tracim's revision's file
  822. """
  823. def __init__(self, path: str, environ: dict, content: data.Content, content_revision: data.ContentRevisionRO):
  824. super(HistoryFile, self).__init__(path, environ, content)
  825. self.content_revision = content_revision
  826. def __repr__(self) -> str:
  827. return "<DAVNonCollection: HistoryFile (%s-%s)" % (self.content.content_id, self.content.file_name)
  828. def getDisplayName(self) -> str:
  829. left_side = '(%d - %s) ' % (self.content_revision.revision_id, self.content_revision.revision_type)
  830. return '%s%s' % (left_side, transform_to_display(self.content_revision.file_name))
  831. def getContent(self):
  832. filestream = compat.BytesIO()
  833. filestream.write(self.content_revision.depot_file.file.read())
  834. filestream.seek(0)
  835. return filestream
  836. def getContentLength(self):
  837. return self.content_revision.depot_file.file.content_length
  838. def getContentType(self) -> str:
  839. return self.content_revision.file_mimetype
  840. def beginWrite(self, contentType=None):
  841. raise DAVError(HTTP_FORBIDDEN)
  842. def delete(self):
  843. raise DAVError(HTTP_FORBIDDEN)
  844. def copyMoveSingle(self, destpath, ismove):
  845. raise DAVError(HTTP_FORBIDDEN)
  846. class OtherFile(File):
  847. """
  848. File resource corresponding to tracim's page and thread
  849. """
  850. def __init__(self, path: str, environ: dict, content: data.Content):
  851. super(OtherFile, self).__init__(path, environ, content)
  852. self.content_revision = self.content.revision
  853. self.content_designed = self.design()
  854. # workaround for consistent request as we have to return a resource with a path ending with .html
  855. # when entering folder for windows, but only once because when we select it again it would have .html.html
  856. # which is no good
  857. if not self.path.endswith('.html'):
  858. self.path += '.html'
  859. def getDisplayName(self) -> str:
  860. return self.content.get_label_as_file()
  861. def getPreferredPath(self):
  862. return self.path
  863. def __repr__(self) -> str:
  864. return "<DAVNonCollection: OtherFile (%s)" % self.content.file_name
  865. def getContentLength(self) -> int:
  866. return len(self.content_designed)
  867. def getContentType(self) -> str:
  868. return 'text/html'
  869. def getContent(self):
  870. filestream = compat.BytesIO()
  871. filestream.write(bytes(self.content_designed, 'utf-8'))
  872. filestream.seek(0)
  873. return filestream
  874. def design(self):
  875. if self.content.type == ContentType.Page:
  876. return designPage(self.content, self.content_revision)
  877. else:
  878. return designThread(
  879. self.content,
  880. self.content_revision,
  881. self.content_api.get_all(self.content.content_id, ContentType.Comment)
  882. )
  883. class HistoryOtherFile(OtherFile):
  884. """
  885. A virtual resource corresponding to a specific tracim's revision's page and thread
  886. """
  887. def __init__(self, path: str, environ: dict, content: data.Content, content_revision: data.ContentRevisionRO):
  888. super(HistoryOtherFile, self).__init__(path, environ, content)
  889. self.content_revision = content_revision
  890. self.content_designed = self.design()
  891. def __repr__(self) -> str:
  892. return "<DAVNonCollection: HistoryOtherFile (%s-%s)" % (self.content.file_name, self.content.id)
  893. def getDisplayName(self) -> str:
  894. left_side = '(%d - %s) ' % (self.content_revision.revision_id, self.content_revision.revision_type)
  895. return '%s%s' % (left_side, transform_to_display(self.content_revision.get_label_as_file()))
  896. def getContent(self):
  897. filestream = compat.BytesIO()
  898. filestream.write(bytes(self.content_designed, 'utf-8'))
  899. filestream.seek(0)
  900. return filestream
  901. def delete(self):
  902. raise DAVError(HTTP_FORBIDDEN)
  903. def copyMoveSingle(self, destpath, ismove):
  904. raise DAVError(HTTP_FORBIDDEN)