sql_resources.py 40KB

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