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