HistoryFolderResource.createCollection()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 2
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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]:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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]:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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