浏览代码

A lot of optimization in database calls, new way to get content from path to limit calls, a lot of comments and integrated css

Nonolost 8 年前
父节点
当前提交
08a086ed26

+ 1 - 1
install/requirements.txt 查看文件

59
 vobject==0.9.2
59
 vobject==0.9.2
60
 waitress==0.8.9
60
 waitress==0.8.9
61
 who-ldap==3.1.0
61
 who-ldap==3.1.0
62
--e git+git@github.com:Nonolost/wsgidav.git@73f832e6bd94ac21ae608af43a9516ff31abc9a2#egg=wsgidav
62
+-e git+git@github.com:Nonolost/wsgidav.git@a5df223d5c66c252baa89ec5de8073f9f1494a8f#egg=wsgidav
63
 zope.interface==4.1.3
63
 zope.interface==4.1.3
64
 zope.sqlalchemy==0.7.6
64
 zope.sqlalchemy==0.7.6
65
 
65
 

+ 38 - 8
tracim/tracim/lib/content.py 查看文件

408
 
408
 
409
         resultset = resultset.filter(Content.parent_id == parent_id)
409
         resultset = resultset.filter(Content.parent_id == parent_id)
410
 
410
 
411
-        try:
412
-            return resultset.filter(Content.label == content_label).one()
413
-        except:
414
-            try:
415
-                return resultset.filter(Content.file_name == content_label).one()
416
-            except:
417
-                import re
418
-                return resultset.filter(Content.label == re.sub(r'\.[^.]+$', '', content_label)).one()
411
+        return resultset.filter(or_(
412
+            Content.label == content_label,
413
+            Content.file_name == content_label,
414
+            Content.label == re.sub(r'\.[^.]+$', '', content_label)
415
+        )).one()
416
+
417
+    def get_one_by_label_and_parent_label(self, content_label: str, content_parent_label: [str]=None, workspace: Workspace=None):
418
+        assert content_label is not None  # DYN_REMOVE
419
+        resultset = self._base_query(workspace)
420
+
421
+        res =  resultset.filter(or_(
422
+            Content.label == content_label,
423
+            Content.file_name == content_label,
424
+            Content.label == re.sub(r'\.[^.]+$', '', content_label)
425
+        )).all()
426
+
427
+        if content_parent_label:
428
+            tmp = dict()
429
+            for content in res:
430
+                tmp[content] = content.parent
431
+
432
+            for parent_label in reversed(content_parent_label):
433
+                a = []
434
+                tmp = {content: parent.parent for content, parent in tmp.items()
435
+                       if parent and parent.label == parent_label}
436
+
437
+                if len(tmp) == 1:
438
+                    content, last_parent = tmp.popitem()
439
+                    return content
440
+                elif len(tmp) == 0:
441
+                    return None
442
+
443
+            for content, parent_content in tmp.items():
444
+                if not parent_content:
445
+                    return content
446
+
447
+            return None
448
+        return res[0]
419
 
449
 
420
     def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> [Content]:
450
     def get_all(self, parent_id: int=None, content_type: str=ContentType.Any, workspace: Workspace=None) -> [Content]:
421
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
451
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE

+ 11 - 25
tracim/tracim/lib/daemons.py 查看文件

231
 from wsgidav._version import __version__
231
 from wsgidav._version import __version__
232
 
232
 
233
 from tracim.lib.webdav.sql_dav_provider import Provider
233
 from tracim.lib.webdav.sql_dav_provider import Provider
234
-from tracim.lib.webdav.sql_domain_controller import SQLDomainController
234
+from tracim.lib.webdav.sql_domain_controller import TracimDomainController
235
 
235
 
236
 from inspect import isfunction
236
 from inspect import isfunction
237
 import traceback
237
 import traceback
239
 DEFAULT_CONFIG_FILE = "wsgidav.conf"
239
 DEFAULT_CONFIG_FILE = "wsgidav.conf"
240
 PYTHON_VERSION = "%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
240
 PYTHON_VERSION = "%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
241
 
241
 
242
-def _get_checked_path(path, mustExist=True, allowNone=True):
243
-    """Convert path to absolute if not None."""
244
-    if path in (None, ""):
245
-        if allowNone:
246
-            return None
247
-        else:
248
-            raise ValueError("Invalid path %r" % path)
249
-    path = os.path.abspath(path)
250
-    if mustExist and not os.path.exists(path):
251
-        raise ValueError("Invalid path %r" % path)
252
-    return path
253
 
242
 
254
 class WsgiDavDaemon(Daemon):
243
 class WsgiDavDaemon(Daemon):
255
 
244
 
280
 
269
 
281
         config['middleware_stack'] = [ WsgiDavDirBrowser, TracimHTTPAuthenticator, ErrorPrinter, WsgiDavDebugFilter ]
270
         config['middleware_stack'] = [ WsgiDavDirBrowser, TracimHTTPAuthenticator, ErrorPrinter, WsgiDavDebugFilter ]
282
 
271
 
283
-        config['provider_mapping'] = \
284
-            {
285
-                config['root_path']: Provider(
286
-                    show_archived=config['show_archived'],
287
-                    show_deleted=config['show_deleted'],
288
-                    show_history=config['show_history'],
289
-                    manage_locks=config['manager_locks']
290
-                )
291
-            }
292
-
293
-        config['domaincontroller'] = SQLDomainController(presetdomain=None, presetserver=None)
294
-        config['defaultdigest'] = True
295
-        config['acceptdigest'] = True
272
+        config['provider_mapping'] = {
273
+            config['root_path']: Provider(
274
+                show_archived=config['show_archived'],
275
+                show_deleted=config['show_deleted'],
276
+                show_history=config['show_history'],
277
+                manage_locks=config['manager_locks']
278
+            )
279
+        }
280
+
281
+        config['domaincontroller'] = TracimDomainController(presetdomain=None, presetserver=None)
296
 
282
 
297
         return config
283
         return config
298
 
284
 

+ 77 - 28
tracim/tracim/lib/webdav/__init__.py 查看文件

1
-from wsgidav.compat import to_bytes
1
+# coding: utf8
2
+
3
+import transaction
4
+from wsgidav import util
5
+from wsgidav import compat
2
 
6
 
3
 from tracim.lib.content import ContentApi
7
 from tracim.lib.content import ContentApi
4
 from tracim.model import new_revision
8
 from tracim.model import new_revision
5
-from tracim.model.data import ActionDescription, ContentType, Content, Workspace
6
-from wsgidav import util
9
+from tracim.model.data import ActionDescription
10
+from tracim.model.data import ContentType
11
+from tracim.model.data import Content
12
+from tracim.model.data import Workspace
7
 
13
 
8
-import transaction
9
-from wsgidav import compat
10
 
14
 
11
 class HistoryType(object):
15
 class HistoryType(object):
12
     Deleted = 'deleted'
16
     Deleted = 'deleted'
15
     All = 'all'
19
     All = 'all'
16
 
20
 
17
 
21
 
22
+class SpecialFolderExtension(object):
23
+    Deleted = '/.deleted'
24
+    Archived = '/.archived'
25
+    History = '/.history'
26
+
27
+
18
 class FakeFileStream(object):
28
 class FakeFileStream(object):
19
-    """Fake a FileStream object that is needed by wsgidav to create or update existing files
20
-    with new content"""
29
+    """
30
+    Fake a FileStream that we're giving to wsgidav to receive data and create files / new revisions
21
 
31
 
22
-    def __init__(self, content_api: ContentApi, workspace: Workspace,
32
+    There's two scenarios :
33
+    - when a new file is created, wsgidav will call the method createEmptyResource and except to get a _DAVResource
34
+    which should have both 'beginWrite' and 'endWrite' method implemented
35
+    - when a file which already exists is updated, he's going to call the 'beginWrite' function of the _DAVResource
36
+    to get a filestream and write content in it
37
+
38
+    In the first case scenario, the transfer takes two part : it first create the resource (createEmptyResource)
39
+    then add its content (beginWrite, write, close..). If we went without this class, we would create two revision
40
+    of the file upon creating a new file, which is not what we want.
41
+    """
42
+
43
+    def __init__(self, content_api: ContentApi, workspace: Workspace, path: str,
23
                  file_name: str='', content: Content=None, parent: Content=None):
44
                  file_name: str='', content: Content=None, parent: Content=None):
24
         """
45
         """
25
 
46
 
26
-        :param file_name: the filename if the file is new
27
         :param content_api:
47
         :param content_api:
28
-        :param workspace: content's workspace, necessary if the file is new as we've got no other way to get it
29
-        :param content: either the content to be updated or None if it's a new file
48
+        :param workspace:
49
+        :param path:
50
+        :param file_name:
51
+        :param content:
52
+        :param parent:
30
         """
53
         """
31
-        self._buff = compat.BytesIO()
54
+        self._file_stream = compat.BytesIO()
32
 
55
 
33
         self._file_name = file_name if file_name != '' else self._content.file_name
56
         self._file_name = file_name if file_name != '' else self._content.file_name
34
         self._content = content
57
         self._content = content
35
         self._api = content_api
58
         self._api = content_api
36
         self._workspace = workspace
59
         self._workspace = workspace
37
         self._parent = parent
60
         self._parent = parent
61
+        self._path = path
62
+
63
+    def getRefUrl(self) -> str:
64
+        """
65
+        As wsgidav expect to receive a _DAVResource upon creating a new resource, this method's result is used
66
+        by Windows client to establish both file's path and file's name
67
+        """
68
+        return self._path
38
 
69
 
39
     def beginWrite(self, contentType) -> 'FakeFileStream':
70
     def beginWrite(self, contentType) -> 'FakeFileStream':
40
-        """Called by request_server to user as a file stream to write bits by bits content into a filestream"""
71
+        """
72
+        Called by wsgidav, it expect a filestream which possess both 'write' and 'close' operation to write
73
+        the file content.
74
+        """
41
         return self
75
         return self
42
 
76
 
43
     def endWrite(self, withErrors: bool):
77
     def endWrite(self, withErrors: bool):
44
-        """Called by request_server when finished writing everythin, wdc"""
78
+        """
79
+        Called by request_server when finished writing everything.
80
+        As we call operation to create new content or revision in the close operation, called before endWrite, there
81
+        is nothing to do here.
82
+        """
45
         pass
83
         pass
46
 
84
 
47
     def write(self, s: str):
85
     def write(self, s: str):
48
-        """Called by request_server when writing content to files, we stock it in our file"""
49
-        self._buff.write(s)
86
+        """
87
+        Called by request_server when writing content to files, we put it inside a filestream
88
+        """
89
+        self._file_stream.write(s)
50
 
90
 
51
     def close(self):
91
     def close(self):
52
-        """Called by request_server when everything has been written and we either update the file or
53
-        create a new file"""
92
+        """
93
+        Called by request_server when the file content has been written. We either add a new content or create
94
+        a new revision
95
+        """
54
 
96
 
55
-        self._buff.seek(0)
97
+        self._file_stream.seek(0)
56
 
98
 
57
         if self._content is None:
99
         if self._content is None:
58
-            self.create_file(self._buff)
100
+            self.create_file()
59
         else:
101
         else:
60
-            self.update_file(self._buff)
102
+            self.update_file()
103
+
104
+        transaction.commit()
105
+
106
+    def create_file(self):
107
+        """
108
+        Called when this is a new file; will create a new Content initialized with the correct content
109
+        """
61
 
110
 
62
-    def create_file(self, item_content):
63
         is_temporary = self._file_name.startswith('.~') or self._file_name.startswith('~')
111
         is_temporary = self._file_name.startswith('.~') or self._file_name.startswith('~')
64
 
112
 
65
         file = self._api.create(
113
         file = self._api.create(
73
             file,
121
             file,
74
             self._file_name,
122
             self._file_name,
75
             util.guessMimeType(self._file_name),
123
             util.guessMimeType(self._file_name),
76
-            item_content.read()
124
+            self._file_stream.read()
77
         )
125
         )
78
 
126
 
79
         self._api.save(file, ActionDescription.CREATION)
127
         self._api.save(file, ActionDescription.CREATION)
80
 
128
 
81
-        transaction.commit()
129
+    def update_file(self):
130
+        """
131
+        Called when we're updating an existing content; we create a new revision and update the file content
132
+        """
82
 
133
 
83
-    def update_file(self, item_content):
84
         with new_revision(self._content):
134
         with new_revision(self._content):
85
             self._api.update_file_data(
135
             self._api.update_file_data(
86
                 self._content,
136
                 self._content,
87
                 self._file_name,
137
                 self._file_name,
88
                 util.guessMimeType(self._content.file_name),
138
                 util.guessMimeType(self._content.file_name),
89
-                item_content.read()
139
+                self._file_stream.read()
90
             )
140
             )
91
-            self._api.save(self._content, ActionDescription.EDITION)
92
 
141
 
93
-        transaction.commit()
142
+            self._api.save(self._content, ActionDescription.EDITION)

+ 17 - 12
tracim/tracim/lib/webdav/design.py 查看文件

1
+#coding: utf8
2
+from datetime import datetime
3
+
1
 from tracim.model.data import VirtualEvent
4
 from tracim.model.data import VirtualEvent
2
-from tracim.model import data
3
 from tracim.model.data import ContentType
5
 from tracim.model.data import ContentType
4
-from datetime import datetime
6
+from tracim.model import data
5
 
7
 
6
 def create_readable_date(created, delta_from_datetime: datetime = None):
8
 def create_readable_date(created, delta_from_datetime: datetime = None):
7
-    aff = ''
8
-
9
     if not delta_from_datetime:
9
     if not delta_from_datetime:
10
         delta_from_datetime = datetime.now()
10
         delta_from_datetime = datetime.now()
11
 
11
 
29
     return aff
29
     return aff
30
 
30
 
31
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
31
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
32
-    # f = open('wsgidav/addons/webdav/style.css', 'r')
33
-    style = ''  # f.read()
34
-    # f.close()
32
+    f = open('tracim/lib/webdav/style.css', 'r')
33
+    style = f.read()
34
+    f.close()
35
 
35
 
36
     hist = content.get_history()
36
     hist = content.get_history()
37
     histHTML = '<table class="table table-striped table-hover">'
37
     histHTML = '<table class="table table-striped table-hover">'
65
                        label,
65
                        label,
66
                        date,
66
                        date,
67
                        event.owner.display_name,
67
                        event.owner.display_name,
68
-                       '<i class="fa fa-caret-left"></i> shown' if event.id == content_revision.revision_id else '''<span><a class="revision-link" href="/.history/%s/%s-%s">(View revision)</a></span>''' % (
69
-                       content.label, event.id, event.ref_object.label) if event.type.id in ['revision', 'creation', 'edition'] else '')
68
+                       '<i class="fa fa-caret-left"></i> shown' if event.id == content_revision.revision_id else '''<span><a class="revision-link" href="/.history/%s/(%s - %s) %s.html">(View revision)</a></span>''' % (
69
+                       content.label, event.id, event.type.id, event.ref_object.label) if event.type.id in ['revision', 'creation', 'edition'] else '')
70
 
70
 
71
     histHTML += '</table>'
71
     histHTML += '</table>'
72
 
72
 
76
 	<title>%s</title>
76
 	<title>%s</title>
77
 	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
77
 	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
78
 	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
78
 	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
79
-	<link rel="stylesheet" href="/home/arnaud/Documents/css/style.css">
79
+	<style>%s</style>
80
 	<script type="text/javascript" src="/home/arnaud/Documents/css/script.js"></script>
80
 	<script type="text/javascript" src="/home/arnaud/Documents/css/script.js"></script>
81
 	<script
81
 	<script
82
 			  src="https://code.jquery.com/jquery-3.1.0.min.js"
82
 			  src="https://code.jquery.com/jquery-3.1.0.min.js"
121
 </body>
121
 </body>
122
 </html>
122
 </html>
123
         ''' % (content_revision.label,
123
         ''' % (content_revision.label,
124
+               style,
124
                content_revision.label,
125
                content_revision.label,
125
                content.created.strftime("%B %d, %Y at %H:%m"),
126
                content.created.strftime("%B %d, %Y at %H:%m"),
126
                content.owner.display_name,
127
                content.owner.display_name,
130
     return file
131
     return file
131
 
132
 
132
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
133
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
134
+        f = open('tracim/lib/webdav/style.css', 'r')
135
+        style = f.read()
136
+        f.close()
133
 
137
 
134
         hist = content.get_history()
138
         hist = content.get_history()
135
 
139
 
205
 	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
209
 	<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
206
 	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
210
 	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
207
 	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
211
 	<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css">
208
-	<link rel="stylesheet" href="/home/arnaud/Documents/css/style.css">
212
+	<style>%s</style>
209
 	<script type="text/javascript" src="/home/arnaud/Documents/css/script.js"></script>
213
 	<script type="text/javascript" src="/home/arnaud/Documents/css/script.js"></script>
210
 </head>
214
 </head>
211
 <body>
215
 <body>
277
 </body>
281
 </body>
278
 </html>
282
 </html>
279
         ''' % (content_revision.label,
283
         ''' % (content_revision.label,
284
+               style,
280
                content_revision.label,
285
                content_revision.label,
281
                content.created.strftime("%B %d, %Y at %H:%m"),
286
                content.created.strftime("%B %d, %Y at %H:%m"),
282
                content.owner.display_name,
287
                content.owner.display_name,
283
                content_revision.description,
288
                content_revision.description,
284
                disc)
289
                disc)
285
 
290
 
286
-        return page
291
+        return page

+ 0 - 0
tracim/tracim/lib/webdav/lock_manager.py 查看文件


+ 155 - 202
tracim/tracim/lib/webdav/sql_dav_provider.py 查看文件

1
 # coding: utf8
1
 # coding: utf8
2
 
2
 
3
-from tracim.lib.webdav import HistoryType
4
-from tracim.lib.webdav.lock_storage import LockStorage
5
-
6
 import re
3
 import re
7
 from os.path import basename, dirname, normpath
4
 from os.path import basename, dirname, normpath
8
-from tracim.lib.content import ContentApi
9
-from tracim.lib.webdav import sql_resources
10
-from tracim.lib.user import UserApi
11
-from tracim.lib.workspace import WorkspaceApi
12
-from wsgidav import util
5
+
13
 from wsgidav.dav_provider import DAVProvider
6
 from wsgidav.dav_provider import DAVProvider
14
 from wsgidav.lock_manager import LockManager
7
 from wsgidav.lock_manager import LockManager
15
-from tracim.model.data import ContentType
16
-
17
-from tracim.lib.content import ContentRevisionRO
18
-######################################
19
-
20
-__docformat__ = "reStructuredText"
21
-_logger = util.getModuleLogger(__name__)
22
-
23
-
24
-def wsgi_decode(s):
25
-    return s.encode('latin1').decode()
26
-
27
-def wsgi_encode(s):
28
-    if isinstance(s, bytes):
29
-        return s.decode('latin1')
30
-    return s.encode().decode('latin1')
31
 
8
 
9
+from tracim.lib.webdav import HistoryType, SpecialFolderExtension
10
+from tracim.lib.webdav import sql_resources
11
+from tracim.lib.webdav.lock_storage import LockStorage
32
 
12
 
13
+from tracim.lib.content import ContentApi
14
+from tracim.lib.content import ContentRevisionRO
15
+from tracim.lib.user import UserApi
16
+from tracim.lib.workspace import WorkspaceApi
17
+from tracim.model.data import Content, Workspace
18
+from tracim.model.data import ContentType
33
 
19
 
34
 
20
 
35
-# ============================================================
36
-# PostgreSQLProvider
37
-# ============================================================
38
 class Provider(DAVProvider):
21
 class Provider(DAVProvider):
22
+    """
23
+    This class' role is to provide to wsgidav _DAVResource. Wsgidav will then use them to execute action and send
24
+    informations to the client
25
+    """
26
+
39
     def __init__(self, show_history=True, show_deleted=True, show_archived=True, manage_locks=True):
27
     def __init__(self, show_history=True, show_deleted=True, show_archived=True, manage_locks=True):
40
         super(Provider, self).__init__()
28
         super(Provider, self).__init__()
41
 
29
 
55
     def show_archive(self):
43
     def show_archive(self):
56
         return self._show_archive
44
         return self._show_archive
57
 
45
 
58
-    def __repr__(self):
59
-        return 'Provider'
60
-
61
     #########################################################
46
     #########################################################
62
     # Everything override from DAVProvider
47
     # Everything override from DAVProvider
63
-    def getResourceInst(self, path, environ):
64
-        #if not self.exists(path, environ):
65
-        #    return None
66
-        if not self.exists(path, environ):
67
-            return None
68
-
69
-        norm_path = normpath(path)
70
-
48
+    def getResourceInst(self, path: str, environ: dict):
49
+        """
50
+        Called by wsgidav whenever a request is called to get the _DAVResource corresponding to the path
51
+        """
52
+        path = normpath(path)
71
         root_path = environ['http_authenticator.realm']
53
         root_path = environ['http_authenticator.realm']
72
-        parent_path = dirname(norm_path)
73
 
54
 
74
-        user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
55
+        # If the requested path is the root, then we return a Root resource
56
+        if path == root_path:
57
+            return sql_resources.Root(path, environ)
75
 
58
 
59
+        user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
76
         workspace_api = WorkspaceApi(user)
60
         workspace_api = WorkspaceApi(user)
61
+        workspace = self.get_workspace_from_path(path, workspace_api)
62
+
63
+        # If the request path is in the form root/name, then we return a Workspace resource
64
+        parent_path = dirname(path)
65
+        if parent_path == root_path:
66
+            return sql_resources.Workspace(path, environ, workspace)
67
+
68
+        # And now we'll work on the path to establish which type or resource is requested
69
+
77
         content_api = ContentApi(
70
         content_api = ContentApi(
78
             user,
71
             user,
79
             show_archived=self._show_archive,
72
             show_archived=self._show_archive,
80
             show_deleted=self._show_delete
73
             show_deleted=self._show_delete
81
         )
74
         )
82
 
75
 
83
-        # case we're requesting the root racine of webdav
84
-        if path == root_path:
85
-            return sql_resources.Root(path, environ)
86
-        # case we're at the root racine of a workspace
87
-        elif parent_path == root_path:
88
-            return sql_resources.Workspace(
89
-                path=norm_path,
90
-                environ=environ,
91
-                workspace=self.get_workspace_from_path(
92
-                    norm_path,
93
-                    workspace_api
94
-                )
95
-            )
96
-
97
         content = self.get_content_from_path(
76
         content = self.get_content_from_path(
98
-            path=norm_path,
77
+            path=path,
99
             content_api=content_api,
78
             content_api=content_api,
100
-            workspace_api=workspace_api
79
+            workspace=workspace
101
         )
80
         )
102
 
81
 
103
-        # is archive
104
-        is_archived_folder = re.search(r'/\.archived$', norm_path) is not None
105
 
82
 
106
-        if self._show_archive and is_archived_folder:
107
-            return sql_resources.ArchivedFolder(
108
-                path=norm_path,
109
-                environ=environ,
110
-                content=content,
111
-                workspace=self.get_workspace_from_path(norm_path, workspace_api)
112
-            )
113
-
114
-        # is delete
115
-        is_deleted_folder = re.search(r'/\.deleted$', norm_path) is not None
116
-
117
-        if self._show_delete and is_deleted_folder:
118
-            return sql_resources.DeletedFolder(
119
-                path=norm_path,
120
-                environ=environ,
121
-                content=content,
122
-                workspace=self.get_workspace_from_path(norm_path, workspace_api)
123
-            )
83
+        # Easy cases : path either end with /.deleted, /.archived or /.history, then we return corresponding resources
84
+        if path.endswith(SpecialFolderExtension.Archived) and self._show_archive:
85
+            return sql_resources.ArchivedFolder(path, environ, workspace, content)
124
 
86
 
125
-        # is history
126
-        is_history_folder = re.search(r'/\.history$', norm_path) is not None
87
+        if path.endswith(SpecialFolderExtension.Deleted) and self._show_delete:
88
+            return sql_resources.DeletedFolder(path, environ, workspace, content)
127
 
89
 
128
-        if self._show_history and is_history_folder:
129
-            is_deleted_folder = re.search(r'/\.deleted/\.history$', norm_path) is not None
130
-            is_archived_folder = re.search(r'/\.archived/\.history$', norm_path) is not None
90
+        if path.endswith(SpecialFolderExtension.History) and self._show_history:
91
+            is_deleted_folder = re.search(r'/\.deleted/\.history$', path) is not None
92
+            is_archived_folder = re.search(r'/\.archived/\.history$', path) is not None
131
 
93
 
132
             type = HistoryType.Deleted if is_deleted_folder \
94
             type = HistoryType.Deleted if is_deleted_folder \
133
                 else HistoryType.Archived if is_archived_folder \
95
                 else HistoryType.Archived if is_archived_folder \
134
                 else HistoryType.Standard
96
                 else HistoryType.Standard
135
 
97
 
136
-            return sql_resources.HistoryFolder(
137
-                path=norm_path,
138
-                environ=environ,
139
-                content=content,
140
-                type=type,
141
-                workspace=self.get_workspace_from_path(norm_path, workspace_api)
142
-            )
98
+            return sql_resources.HistoryFolder(path, environ, workspace, content, type)
143
 
99
 
144
-        # is history
145
-        is_history_file_folder = re.search(r'/\.history/([^/]+)$', norm_path) is not None
100
+        # Now that's more complicated, we're trying to find out if the path end with /.history/file_name
101
+        is_history_file_folder = re.search(r'/\.history/([^/]+)$', path) is not None
146
 
102
 
147
-        if self._show_history and is_history_file_folder:
103
+        if is_history_file_folder and self._show_history:
148
             return sql_resources.HistoryFileFolder(
104
             return sql_resources.HistoryFileFolder(
149
-                path=norm_path,
105
+                path=path,
150
                 environ=environ,
106
                 environ=environ,
151
                 content=content
107
                 content=content
152
             )
108
             )
153
 
109
 
154
-        # is history
155
-        is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', norm_path) is not None
110
+        # And here next step :
111
+        is_history_file = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', path) is not None
156
 
112
 
157
         if self._show_history and is_history_file:
113
         if self._show_history and is_history_file:
158
-            content_revision = content_api.get_one_revision(re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) .+', norm_path).group(1))
114
+
115
+            revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path).group(1)
116
+
117
+            content_revision = content_api.get_one_revision(revision_id)
159
             content = self.get_content_from_revision(content_revision, content_api)
118
             content = self.get_content_from_revision(content_revision, content_api)
160
 
119
 
161
             if content.type == ContentType.File:
120
             if content.type == ContentType.File:
162
-                return sql_resources.HistoryFile(norm_path, environ, content, content_revision)
121
+                return sql_resources.HistoryFile(path, environ, content, content_revision)
163
             else:
122
             else:
164
-                return sql_resources.HistoryOtherFile(norm_path, environ, content, content_revision)
123
+                return sql_resources.HistoryOtherFile(path, environ, content, content_revision)
124
+
125
+        # And if we're still going, the client is asking for a standard Folder/File/Page/Thread so we check the type7
126
+        # and return the corresponding resource
165
 
127
 
166
-        # other
167
         if content is None:
128
         if content is None:
168
             return None
129
             return None
169
         if content.type == ContentType.Folder:
130
         if content.type == ContentType.Folder:
170
-            return sql_resources.Folder(norm_path, environ, content, content.workspace)
131
+            return sql_resources.Folder(path, environ, content.workspace, content)
171
         elif content.type == ContentType.File:
132
         elif content.type == ContentType.File:
172
-            return sql_resources.File(norm_path, environ, content)
173
-        elif content.type in [ContentType.Page, ContentType.Thread]:
174
-            return sql_resources.OtherFile(norm_path, environ, content)
133
+            return sql_resources.File(path, environ, content)
175
         else:
134
         else:
176
-            return None
135
+            return sql_resources.OtherFile(path, environ, content)
177
 
136
 
178
-    def exists(self, path, environ):
179
-        norm_path = normpath(path)
180
-        parent_path = dirname(norm_path)
181
-        root_path = environ['http_authenticator.realm']
137
+    def exists(self, path, environ) -> bool:
138
+        """
139
+        Called by wsgidav to check if a certain path is linked to a _DAVResource
140
+        """
182
 
141
 
183
-        user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
184
-        workspace_api = WorkspaceApi(user)
185
-        content_api = ContentApi(
186
-            current_user=user,
187
-            show_archived=True,
188
-            show_deleted=True
189
-        )
142
+        path = normpath(path)
143
+        working_path = self.reduce_path(path)
144
+        root_path = environ['http_authenticator.realm']
145
+        parent_path = dirname(working_path)
190
 
146
 
191
         if path == root_path:
147
         if path == root_path:
192
             return True
148
             return True
193
-        elif parent_path == root_path:
194
-            return self.get_workspace_from_path(
195
-                    norm_path,
196
-                    workspace_api
197
-                ) is not None
198
 
149
 
199
-        is_archived = re.search(r'/\.archived/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$', norm_path) is not None
150
+        user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
200
 
151
 
201
-        is_deleted = re.search(r'/\.deleted/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$', norm_path) is not None
152
+        workspace = self.get_workspace_from_path(path, WorkspaceApi(user))
202
 
153
 
203
-        revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', norm_path)
154
+        if parent_path == root_path:
155
+            return workspace is not None
204
 
156
 
205
-        blbl = self.reduce_path(norm_path)
206
-        if dirname(blbl) == '/':
207
-            return self.get_workspace_from_path(norm_path, workspace_api) is not None
157
+        content_api = ContentApi(user, show_archived=True, show_deleted=True)
158
+
159
+        revision_id = re.search(r'/\.history/[^/]+/\((\d+) - [a-zA-Z]+\) ([^/].+)$', path)
160
+
161
+        is_archived = self.is_path_archive(path)
162
+
163
+        is_deleted = self.is_path_delete(path)
208
 
164
 
209
         if revision_id:
165
         if revision_id:
210
             revision_id = revision_id.group(1)
166
             revision_id = revision_id.group(1)
211
             content = content_api.get_one_revision(revision_id)
167
             content = content_api.get_one_revision(revision_id)
212
         else:
168
         else:
213
-            content = self.get_content_from_path(norm_path, content_api, workspace_api)
169
+            content = self.get_content_from_path(working_path, content_api, workspace)
214
 
170
 
215
         return content is not None \
171
         return content is not None \
216
             and content.is_deleted == is_deleted \
172
             and content.is_deleted == is_deleted \
217
             and content.is_archived == is_archived
173
             and content.is_archived == is_archived
218
 
174
 
219
-    def reduce_path(self, path):
175
+    def is_path_archive(self, path):
176
+        """
177
+        This function will check if a given path is linked to a file that's archived or not. We're checking if the
178
+        given path end with one of these string :
179
+
180
+        ex:
181
+            - /a/b/.archived/my_file
182
+            - /a/b/.archived/.history/my_file
183
+            - /a/b/.archived/.history/my_file/(3615 - edition) my_file
184
+        """
185
+
186
+        return re.search(
187
+            r'/\.archived/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$',
188
+            path
189
+        ) is not None
190
+
191
+    def is_path_delete(self, path):
192
+        """
193
+        This function will check if a given path is linked to a file that's deleted or not. We're checking if the
194
+        given path end with one of these string :
195
+
196
+        ex:
197
+            - /a/b/.deleted/my_file
198
+            - /a/b/.deleted/.history/my_file
199
+            - /a/b/.deleted/.history/my_file/(3615 - edition) my_file
200
+        """
201
+
202
+        return re.search(
203
+            r'/\.deleted/(\.history/)?(?!\.history)[^/]*(/\.)?(history|deleted|archived)?$',
204
+            path
205
+        ) is not None
206
+
207
+    def reduce_path(self, path: str) -> str:
208
+        """
209
+        As we use the given path to request the database
210
+
211
+        ex: if the path is /a/b/.deleted/c/.archived, we're trying to get the archived content of the 'c' resource,
212
+        we need to keep the path /a/b/c
213
+
214
+        ex: if the path is /a/b/.history/my_file, we're trying to get the history of the file my_file, thus we need
215
+        the path /a/b/my_file
216
+
217
+        ex: if the path is /a/b/.history/my_file/(1985 - edition) my_old_name, we're looking for,
218
+        thus we remove all useless information
219
+        """
220
         path = re.sub(r'/\.archived', r'', path)
220
         path = re.sub(r'/\.archived', r'', path)
221
         path = re.sub(r'/\.deleted', r'', path)
221
         path = re.sub(r'/\.deleted', r'', path)
222
         path = re.sub(r'/\.history/[^/]+/(\d+)-.+', r'/\1', path)
222
         path = re.sub(r'/\.history/[^/]+/(\d+)-.+', r'/\1', path)
225
 
225
 
226
         return path
226
         return path
227
 
227
 
228
-    def get_content_from_path(self, path, content_api: ContentApi, workspace_api: WorkspaceApi):
228
+    def get_content_from_path(self, path, content_api: ContentApi, workspace: Workspace) -> Content:
229
+        """
230
+        Called whenever we want to get the Content item from the database for a given path
231
+        """
229
         path = self.reduce_path(path)
232
         path = self.reduce_path(path)
233
+        parent_path = dirname(path)
230
 
234
 
231
-        workspace = self.get_workspace_from_path(path, workspace_api)
235
+        blbl = parent_path.replace('/'+workspace.label, '')
236
+
237
+        parents = blbl.split('/')
238
+
239
+        parents.remove('')
240
+        parents = [self.transform_to_bdd(x) for x in parents]
232
 
241
 
233
         try:
242
         try:
234
-            if dirname(dirname(path)) == '/':
235
-                return content_api.get_one_by_label_and_parent(
236
-                    self.transform_to_bdd(basename(path)),
237
-                    workspace=workspace
238
-                )
239
-            else:
240
-                parent = self.get_parent_from_path(path, content_api, workspace_api)
241
-                if parent is not None:
242
-                    return content_api.get_one_by_label_and_parent(self.transform_to_bdd(basename(path)), content_parent=parent)
243
-                return None
243
+            return content_api.get_one_by_label_and_parent_label(
244
+                self.transform_to_bdd(basename(path)),
245
+                parents,
246
+                workspace
247
+            )
244
         except:
248
         except:
245
             return None
249
             return None
246
 
250
 
247
-    def get_content_from_revision(self, revision: ContentRevisionRO, api:ContentApi):
251
+    def get_content_from_revision(self, revision: ContentRevisionRO, api: ContentApi) -> Content:
248
         try:
252
         try:
249
             return api.get_one(revision.content_id, ContentType.Any)
253
             return api.get_one(revision.content_id, ContentType.Any)
250
         except:
254
         except:
251
             return None
255
             return None
252
 
256
 
253
-    def get_parent_from_path(self, path, api: ContentApi, workspace_api: WorkspaceApi):
254
-
255
-        return self.get_content_from_path(dirname(path), api, workspace_api)
256
-
257
-    def get_workspace_from_path(self, path: str, api: WorkspaceApi):
258
-        assert path.startswith('/')
257
+    def get_parent_from_path(self, path, api: ContentApi, workspace) -> Content:
258
+        return self.get_content_from_path(dirname(path), api, workspace)
259
 
259
 
260
+    def get_workspace_from_path(self, path: str, api: WorkspaceApi) -> Workspace:
260
         try:
261
         try:
261
             return api.get_one_by_label(self.transform_to_bdd(path.split('/')[1]))
262
             return api.get_one_by_label(self.transform_to_bdd(path.split('/')[1]))
262
         except:
263
         except:
263
             return None
264
             return None
264
 
265
 
265
     def transform_to_display(self, string):
266
     def transform_to_display(self, string):
267
+        """
268
+        As characters that Windows does not support may have been inserted through Tracim in names, before displaying
269
+        information we update path so that all these forbidden characters are replaced with similar shape character
270
+        that are allowed so that the user isn't trouble and isn't limited in his naming choice
271
+        """
266
         _TO_DISPLAY = {
272
         _TO_DISPLAY = {
267
             '/':'⧸',
273
             '/':'⧸',
268
             '\\': '⧹',
274
             '\\': '⧹',
281
         return string
287
         return string
282
 
288
 
283
     def transform_to_bdd(self, string):
289
     def transform_to_bdd(self, string):
290
+        """
291
+        Called before sending request to the database to recover the right names
292
+        """
284
         _TO_BDD = {
293
         _TO_BDD = {
285
             '⧸': '/',
294
             '⧸': '/',
286
             '⧹': '\\',
295
             '⧹': '\\',
297
             string = string.replace(key, value)
306
             string = string.replace(key, value)
298
 
307
 
299
         return string
308
         return string
300
-
301
-
302
-"""
303
-
304
-{'wsgidav.dump_request_body': False,
305
- 'wsgi.run_once': False,
306
- 'wsgi.multiprocess': False,
307
- 'wsgi.multithread': True,
308
- 'QUERY_STRING': '',
309
- 'REQUEST_URI': b'/nouveau/',
310
- 'wsgidav.dump_response_body': False,
311
- 'SERVER_PROTOCOL': 'HTTP/1.1',
312
- 'REMOTE_ADDR': '127.0.0.1',
313
- 'wsgidav.verbose': 1,
314
- 'wsgi.version': (1, 0),
315
- 'wsgidav.config': {
316
-     'middleware_stack':[],
317
-                       'propsmanager': None,
318
-                       'add_header_MS_Author_Via': True,
319
-                       'acceptbasic': True,
320
-                       'user_mapping': {},
321
-                       'enable_loggers': [],
322
-                       'locksmanager': True,
323
-                       'mount_path': None,
324
-                       'catchall': False,
325
-                       'unquote_path_info': False,
326
-                       'provider_mapping': {'': Provider},
327
-                       'port': 3030,
328
-                       'Provider': [],
329
-                       'verbose': 1,
330
-                       'SQLDomainController': [],
331
-                       'domaincontroller': [],
332
-     'acceptdigest': True,
333
-     'dir_browser': {
334
-         'ms_sharepoint_urls': False,
335
-         'ms_mount': False,
336
-         'davmount': False,
337
-         'enable': True,
338
-         'ms_sharepoint_plugin': True,
339
-         'response_trailer': ''
340
-     },
341
-     'defaultdigest': True,
342
-     'host': '0.0.0.0',
343
-     'ext_servers': ['cherrypy-bundled', 'wsgidav']
344
- },
345
- 'http_authenticator.realm': '/',
346
- 'HTTP_AUTHORIZATION': 'Digest username="admin@admin.admin",
347
- realm="/", nonce="=",
348
- uri="/nouveau/",
349
- algorithm=MD5,
350
- response="9c78c484263409b3385ead95ea7bf65b", '
351
- 'cnonce="MHgyMzNkZjkwOjQ4OTU6MTQ2OTc3OTI1NQ==", nc=00000471, qop=auth',
352
- 'HTTP_ACCEPT_ENCODING': 'gzip, deflate',
353
- 'HTTP_USER_AGENT': 'gvfs/1.22.2', 'wsgidav.debug_break': False,
354
- 'HTTP_CONNECTION': 'Keep-Alive', 'SERVER_PORT': '3030', 'CONTENT_LENGTH': '235', 'HTTP_HOST': '127.0.0.1:3030', 'REQUEST_METHOD': 'PROPFIND', 'HTTP_APPLY_TO_REDIRECT_REF': 'T', 'SERVER_NAME': 'WsgiDAV/3.0.0pre1 CherryPy/3.2.4 Python/3.4.2', 'wsgi.errors': <_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, 'wsgi.url_scheme': 'http', 'user': <User: email='admin@admin.admin', display='Global manager'>, 'HTTP_ACCEPT_LANGUAGE': 'en-us, en;q=0.9', 'ACTUAL_SERVER_PROTOCOL': 'HTTP/1.1', 'REMOTE_PORT': '48375', 'CONTENT_TYPE': 'application/xml', 'SCRIPT_NAME': '', 'wsgi.input': <wsgidav.server.cherrypy.wsgiserver.wsgiserver3.KnownLengthRFile object at 0x7fbc8410ce48>, 'wsgidav.username': 'admin@admin.admin', 'http_authenticator.username': 'admin@admin.admin', 'wsgidav.provider': Provider, 'PATH_INFO': '/nouveau/', 'HTTP_DEPTH': '1', 'SERVER_SOFTWARE': 'WsgiDAV/3.0.0pre1 CherryPy/3.2.4 Python/3.4.2 Server'}
355
-"""

+ 17 - 19
tracim/tracim/lib/webdav/sql_domain_controller.py 查看文件

2
 
2
 
3
 from tracim.lib.user import UserApi
3
 from tracim.lib.user import UserApi
4
 
4
 
5
-class SQLDomainController(object):
6
-
5
+class TracimDomainController(object):
6
+    """
7
+    The domain controller is used by http_authenticator to authenticate the user every time a request is
8
+    sent
9
+    """
7
     def __init__(self, presetdomain = None, presetserver = None):
10
     def __init__(self, presetdomain = None, presetserver = None):
8
         self._api = UserApi(None)
11
         self._api = UserApi(None)
9
 
12
 
10
     def getDomainRealm(self, inputURL, environ):
13
     def getDomainRealm(self, inputURL, environ):
11
-
12
-        '''On va récupérer le workspace de travail pour travailler sur les droits'''
13
-
14
         return '/'
14
         return '/'
15
 
15
 
16
     def requireAuthentication(self, realmname, environ):
16
     def requireAuthentication(self, realmname, environ):
17
         return True
17
         return True
18
 
18
 
19
     def isRealmUser(self, realmname, username, environ):
19
     def isRealmUser(self, realmname, username, environ):
20
-
21
-        '''travailler dans la bdd pour vérifier si utilisateur existe'''
22
-
20
+        """
21
+        Called to check if for a given root, the username exists (though here we don't make difference between
22
+        root as we're always starting at tracim's root
23
+        """
23
         try:
24
         try:
24
             self._api.get_one_by_email(username)
25
             self._api.get_one_by_email(username)
25
             return True
26
             return True
26
         except:
27
         except:
27
             return False
28
             return False
28
 
29
 
29
-    def getRealmUserPassword(self, realmname, username, environ):
30
-        '''Retourne le mdp pour l'utilisateur pour ce real'''
31
-        try:
32
-            user = self._api.get_one_by_email(username)
33
-            return user.password
34
-        except:
35
-            return None
36
-
37
     def get_left_digest_response_hash(self, realmname, username, environ):
30
     def get_left_digest_response_hash(self, realmname, username, environ):
31
+        """
32
+        Called by our http_authenticator to get the hashed md5 digest for the current user that is also sent by
33
+        the webdav client
34
+        """
38
         try:
35
         try:
39
             user = self._api.get_one_by_email(username)
36
             user = self._api.get_one_by_email(username)
40
-            environ['user_api'] = UserApi(user)
41
-            print("hey ! ", realmname)
42
             return user.webdav_left_digest_response_hash
37
             return user.webdav_left_digest_response_hash
43
         except:
38
         except:
44
             return None
39
             return None
45
 
40
 
46
     def authDomainUser(self, realmname, username, password, environ):
41
     def authDomainUser(self, realmname, username, password, environ):
47
-        '''Vérifier que l'utilisateur est valide pour ce domaine'''
42
+        """
43
+        If you ever feel the need to send a request al-mano with a curl, this is the function that'll be called by
44
+        http_authenticator to validate the password sent
45
+        """
48
 
46
 
49
         return self.isRealmUser(realmname, username, environ) and \
47
         return self.isRealmUser(realmname, username, environ) and \
50
             self._api.get_one_by_email(username).validate_password(password)
48
             self._api.get_one_by_email(username).validate_password(password)

+ 3 - 64
tracim/tracim/lib/webdav/sql_model.py 查看文件

1
-from datetime import datetime
1
+#coding: utf8
2
 
2
 
3
 from sqlalchemy import Column
3
 from sqlalchemy import Column
4
 from sqlalchemy import ForeignKey
4
 from sqlalchemy import ForeignKey
5
-from sqlalchemy import Sequence
6
-from sqlalchemy.orm import deferred
7
-from sqlalchemy.types import DateTime, Integer, LargeBinary, Unicode, UnicodeText, Float
8
-
9
-from wsgidav.compat import to_unicode, to_bytes
10
-
11
-# ==================================================
12
-# Content
13
-# ==================================================
14
-class Workspace(object):
15
-    __tablename__ = 'my_workspaces'
16
-
17
-    workspace_id = Column(Integer, Sequence('my_seq__workspace__id'), autoincrement=True, primary_key=True)
18
-    label = Column(Unicode(255), unique=False, nullable=False, default=to_unicode(''))
19
-
20
-    created = Column(DateTime, unique=False, nullable=False, default=datetime.now)
21
-    updated = Column(DateTime, unique=False, nullable=False, default=datetime.now)
22
-
23
-    def __repr__(self):
24
-        return "<Workspace %s : %s>" % (self.workspace_id, self.label)
25
-
26
-
27
-class User(object):
28
-    __tablename__ = 'my_users'
29
-
30
-    user_id = Column(Integer, Sequence('my_seq__users__id'), autoincrement=True, primary_key=True)
31
-    display_name = Column(Unicode(255), unique=True, nullable=False, default=to_unicode(''))
32
-    password = Column(Unicode(255), unique=False, nullable=False, default=to_unicode(''))
33
-
34
-    def __repr__(self):
35
-        return "<User %s : %s>" % (self.user_id, self.display_name)
36
-
37
-
38
-class UserWorkspace(object):
39
-    __tablename__ = 'my_user_workspace'
40
-
41
-    workspace_id = Column(Integer, ForeignKey('my_workspaces.workspace_id', ondelete="CASCADE"), nullable=False, primary_key=True)
42
-    user_id = Column(Integer, ForeignKey('my_users.user_id', ondelete="CASCADE"), nullable=False, primary_key=True)
43
-    role = Column(Unicode(255), unique=False, nullable=False, default=u'NOT_APPLICABLE')
44
-
45
-    def __repr__(self):
46
-        return "<Role (W:%s, U:%s) : %s" % (self.workspace_id, self.user_id, self.role)
47
-
48
-
49
-class ItemRevision(object):
50
-    __tablename__ = 'my_items_revisions'
51
-
52
-    id = Column(Integer, Sequence('my_seq__items__id'), autoincrement=True, primary_key=True)
53
-    workspace_id = Column(Integer, ForeignKey('my_workspaces.workspace_id', ondelete="CASCADE"), nullable=False)
54
-    parent_id = Column(Integer, ForeignKey('my_items_revisions.id', ondelete="CASCADE"), nullable=True, default=None)
55
-
56
-    item_type = Column(Unicode(32), unique=False, nullable=False)
57
-    item_name = Column(Unicode(255), unique=False, nullable=False, default=to_unicode(''))
58
-    item_content = deferred(Column(LargeBinary(), unique=False, nullable=True, default=to_bytes('')))
59
-
60
-    created = Column(DateTime, unique=False, nullable=False, default=datetime.now)
61
-    updated = Column(DateTime, unique=False, nullable=False, default=datetime.now)
62
-
63
-    parent_revision_id = Column(Integer, ForeignKey('my_items_revisions.id', ondelete="CASCADE"), nullable=True, default=None)
64
-    child_revision_id = Column(Integer, ForeignKey('my_items_revisions.id', ondelete="CASCADE"), nullable=True, default=None)
65
-
66
-    def __repr__(self):
67
-        return "<Content %s : %s in %s>" % (self.id, self.item_name, self.parent_id)
5
+from sqlalchemy.types import Unicode, UnicodeText, Float
68
 
6
 
7
+from wsgidav.compat import to_unicode
69
 
8
 
70
 class Lock(object):
9
 class Lock(object):
71
     __tablename__ = 'my_locks'
10
     __tablename__ = 'my_locks'

+ 194 - 126
tracim/tracim/lib/webdav/sql_resources.py 查看文件

23
 
23
 
24
 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
24
 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
25
 
25
 
26
-class Manage(object):
27
-    """Objet qui sert à encapsuler l'exécution des actions de l'api archive/delete..."""
26
+class ManageActions(object):
27
+    """
28
+    This object is used to encapsulate all Deletion/Archiving related method as to not duplicate too much code
29
+    """
28
     def __init__(self, action_type: str, api: ContentApi, content: Content):
30
     def __init__(self, action_type: str, api: ContentApi, content: Content):
29
         self.content_api = api
31
         self.content_api = api
30
         self.content = content
32
         self.content = content
46
 
48
 
47
     def action(self):
49
     def action(self):
48
         try:
50
         try:
51
+            # When undeleting/unarchiving we except a content with the new name to not exist, thus if we
52
+            # don't get an error and the database request send back a result, we stop the action
49
             self.content_api.get_one_by_label_and_parent(self._new_name, self.content.parent, self.content.workspace)
53
             self.content_api.get_one_by_label_and_parent(self._new_name, self.content.parent, self.content.workspace)
50
             raise DAVError(HTTP_FORBIDDEN)
54
             raise DAVError(HTTP_FORBIDDEN)
51
         except NoResultFound:
55
         except NoResultFound:
52
-            """Exécute l'action"""
53
             with new_revision(self.content):
56
             with new_revision(self.content):
54
                 self.content_api.update_content(self.content, self._new_name)
57
                 self.content_api.update_content(self.content, self._new_name)
55
                 self._actions[self._type](self.content)
58
                 self._actions[self._type](self.content)
57
 
60
 
58
             transaction.commit()
61
             transaction.commit()
59
 
62
 
60
-    def make_name(self):
61
-        """Créer le nouveau nom : rajoute de - archive date / retrait de - archive date suivant l'action"""
63
+    def make_name(self) -> str:
64
+        """
65
+        Will create the new name, either by adding '- deleted the [date]' after the name when archiving/deleting or
66
+        removing this string when undeleting/unarchiving
67
+        """
62
         new_name = self.content.get_label()
68
         new_name = self.content.get_label()
63
         extension = ''
69
         extension = ''
64
-        is_file_name = self.content.label == ''
65
-
66
 
70
 
71
+        # if the content has no label, the last .ext is important
72
+        # thus we want to rename a file from 'file.txt' to 'file - deleted... .txt' and not 'file.txt - deleted...'
73
+        is_file_name = self.content.label == ''
67
         if is_file_name:
74
         if is_file_name:
68
             extension = re.search(r'(\.[^.]+)$', new_name).group(0)
75
             extension = re.search(r'(\.[^.]+)$', new_name).group(0)
69
             new_name = re.sub(r'(\.[^.]+)$', '', new_name)
76
             new_name = re.sub(r'(\.[^.]+)$', '', new_name)
71
         if self._type in [ActionDescription.ARCHIVING, ActionDescription.DELETION]:
78
         if self._type in [ActionDescription.ARCHIVING, ActionDescription.DELETION]:
72
             new_name += ' - %s the %s' % (self._to_name[self._type], datetime.now().strftime('%d-%m-%Y at %H:%M'))
79
             new_name += ' - %s the %s' % (self._to_name[self._type], datetime.now().strftime('%d-%m-%Y at %H:%M'))
73
         else:
80
         else:
74
-            new_name = re.sub(r'( - (%s|%s) the .*)$' % (self._to_name[ActionDescription.DELETION], self._to_name[ActionDescription.ARCHIVING]), '', new_name)
81
+            new_name = re.sub(
82
+                r'( - (%s|%s) the .*)$' % (self._to_name[ActionDescription.DELETION], self._to_name[ActionDescription.ARCHIVING]),
83
+                '',
84
+                new_name
85
+            )
75
 
86
 
76
         new_name += extension
87
         new_name += extension
77
 
88
 
79
 
90
 
80
 
91
 
81
 class Root(DAVCollection):
92
 class Root(DAVCollection):
82
-    """Root ressource that represents tracim's home, which contains all workspaces"""
93
+    """
94
+    Root ressource that represents tracim's home, which contains all workspaces
95
+    """
83
 
96
 
84
     def __init__(self, path: str, environ: dict):
97
     def __init__(self, path: str, environ: dict):
85
         super(Root, self).__init__(path, environ)
98
         super(Root, self).__init__(path, environ)
93
     def getMemberNames(self) -> [str]:
106
     def getMemberNames(self) -> [str]:
94
         """
107
         """
95
         This method returns the names (here workspace's labels) of all its children
108
         This method returns the names (here workspace's labels) of all its children
109
+
110
+        Though for perfomance issue, we're not using this function anymore
96
         """
111
         """
97
         return [workspace.label for workspace in self.workspace_api.get_all()]
112
         return [workspace.label for workspace in self.workspace_api.get_all()]
98
 
113
 
99
     def getMember(self, label: str) -> DAVCollection:
114
     def getMember(self, label: str) -> DAVCollection:
100
         """
115
         """
101
         This method returns the child Workspace that corresponds to a given name
116
         This method returns the child Workspace that corresponds to a given name
117
+
118
+        Though for perfomance issue, we're not using this function anymore
102
         """
119
         """
103
         try:
120
         try:
104
             workspace = self.workspace_api.get_one_by_label(label)
121
             workspace = self.workspace_api.get_one_by_label(label)
113
         This method is called whenever the user wants to create a DAVNonCollection resource (files in our case).
130
         This method is called whenever the user wants to create a DAVNonCollection resource (files in our case).
114
 
131
 
115
         There we don't allow to create files at the root;
132
         There we don't allow to create files at the root;
116
-        only workspaces (thus collection) can be created."""
133
+        only workspaces (thus collection) can be created.
134
+        """
117
         raise DAVError(HTTP_FORBIDDEN)
135
         raise DAVError(HTTP_FORBIDDEN)
118
 
136
 
119
     def createCollection(self, name: str):
137
     def createCollection(self, name: str):
123
 
141
 
124
         [For now] we don't allow to create new workspaces through
142
         [For now] we don't allow to create new workspaces through
125
         webdav client. Though if we come to allow it, deleting the error's raise will
143
         webdav client. Though if we come to allow it, deleting the error's raise will
126
-        make it possible."""
144
+        make it possible.
145
+        """
127
         # TODO : remove comment here
146
         # TODO : remove comment here
128
         # raise DAVError(HTTP_FORBIDDEN)
147
         # raise DAVError(HTTP_FORBIDDEN)
129
 
148
 
137
         return Workspace(workspace_path, self.environ, new_workspace)
156
         return Workspace(workspace_path, self.environ, new_workspace)
138
 
157
 
139
     def getMemberList(self):
158
     def getMemberList(self):
140
-        # De base on appellerait getMemberNames puis getMember, mais ça fait trop de requête alors on optimise :
141
-        # on fait une seule requête en BDD; get_all et voilà !
159
+        """
160
+        This method is called by wsgidav when requesting with a depth > 0, it will return a list of _DAVResource
161
+        of all its direct children
162
+        """
142
 
163
 
143
         members = []
164
         members = []
144
         for workspace in self.workspace_api.get_all():
165
         for workspace in self.workspace_api.get_all():
149
 
170
 
150
 
171
 
151
 class Workspace(DAVCollection):
172
 class Workspace(DAVCollection):
152
-    """Workspace resource corresponding to tracim's workspaces.
153
-    Direct children can only be folders, though files might come later on"""
173
+    """
174
+    Workspace resource corresponding to tracim's workspaces.
175
+    Direct children can only be folders, though files might come later on and are supported
176
+    """
154
 
177
 
155
     def __init__(self, path: str, environ: dict, workspace: data.Workspace):
178
     def __init__(self, path: str, environ: dict, workspace: data.Workspace):
156
         super(Workspace, self).__init__(path, environ)
179
         super(Workspace, self).__init__(path, environ)
202
         )
225
         )
203
 
226
 
204
     def createEmptyResource(self, file_name: str):
227
     def createEmptyResource(self, file_name: str):
205
-        """[For now] we don't allow to create files right under workspaces.
206
-        Though if we come to allow it, deleting the error's raise will make it possible."""
228
+        """
229
+        [For now] we don't allow to create files right under workspaces.
230
+        Though if we come to allow it, deleting the error's raise will make it possible.
231
+        """
207
         # TODO : remove commentary here raise DAVError(HTTP_FORBIDDEN)
232
         # TODO : remove commentary here raise DAVError(HTTP_FORBIDDEN)
208
         if '/.deleted/' in self.path or '/.archived/' in self.path:
233
         if '/.deleted/' in self.path or '/.archived/' in self.path:
209
             raise DAVError(HTTP_FORBIDDEN)
234
             raise DAVError(HTTP_FORBIDDEN)
213
             content_api=self.content_api,
238
             content_api=self.content_api,
214
             workspace=self.workspace,
239
             workspace=self.workspace,
215
             content=None,
240
             content=None,
216
-            parent=self.content
241
+            parent=self.content,
242
+            path=self.path + '/' + file_name
217
         )
243
         )
218
 
244
 
219
     def createCollection(self, label: str) -> 'Folder':
245
     def createCollection(self, label: str) -> 'Folder':
220
-        """Create a new folder for the current workspace. As it's not possible for the user to choose
246
+        """
247
+        Create a new folder for the current workspace. As it's not possible for the user to choose
221
         which types of content are allowed in this folder, we allow allow all of them.
248
         which types of content are allowed in this folder, we allow allow all of them.
222
 
249
 
223
-        This method return the DAVCollection created."""
250
+        This method return the DAVCollection created.
251
+        """
224
 
252
 
225
         if '/.deleted/' in self.path or '/.archived/' in self.path:
253
         if '/.deleted/' in self.path or '/.archived/' in self.path:
226
             raise DAVError(HTTP_FORBIDDEN)
254
             raise DAVError(HTTP_FORBIDDEN)
256
         return True
284
         return True
257
 
285
 
258
     def moveRecursive(self, destpath):
286
     def moveRecursive(self, destpath):
259
-        if dirname(normpath(destpath)) == self.provider.root:
287
+        if dirname(normpath(destpath)) == self.environ['http_authenticator.realm']:
260
             self.workspace.label = basename(normpath(destpath))
288
             self.workspace.label = basename(normpath(destpath))
261
             transaction.commit()
289
             transaction.commit()
262
         else:
290
         else:
264
 
292
 
265
     def getMemberList(self) -> [_DAVResource]:
293
     def getMemberList(self) -> [_DAVResource]:
266
         members = []
294
         members = []
267
-        parent_id = None if self.content is None else self.content.id
268
 
295
 
269
-        childs = self.content_api.get_all(parent_id, ContentType.Any, self.workspace)
296
+        children = self.content_api.get_all(None, ContentType.Any, self.workspace)
270
 
297
 
271
-        for content in childs:
298
+        for content in children:
272
             content_path = '%s/%s' % (self.path, self.provider.transform_to_display(content.get_label()))
299
             content_path = '%s/%s' % (self.path, self.provider.transform_to_display(content.get_label()))
273
 
300
 
274
             if content.type == ContentType.Folder:
301
             if content.type == ContentType.Folder:
275
-                members.append(Folder(content_path, self.environ, content, self.workspace))
302
+                members.append(Folder(content_path, self.environ, self.workspace, content))
276
             elif content.type == ContentType.File:
303
             elif content.type == ContentType.File:
277
                 self._file_count += 1
304
                 self._file_count += 1
278
                 members.append(File(content_path, self.environ, content))
305
                 members.append(File(content_path, self.environ, content))
310
                     workspace=self.workspace
337
                     workspace=self.workspace
311
                 )
338
                 )
312
             )
339
             )
340
+
313
         return members
341
         return members
314
 
342
 
315
 
343
 
316
 class Folder(Workspace):
344
 class Folder(Workspace):
317
-    """Folder resource corresponding to tracim's folders.
345
+    """
346
+    Folder resource corresponding to tracim's folders.
318
     Direct children can only be either folder, files, pages or threads
347
     Direct children can only be either folder, files, pages or threads
319
-    By default when creating new folders, we allow them to contain all types of content"""
348
+    By default when creating new folders, we allow them to contain all types of content
349
+    """
320
 
350
 
321
-    def __init__(self, path: str, environ: dict, content: data.Content, workspace: data.Workspace):
351
+    def __init__(self, path: str, environ: dict, workspace: data.Workspace, content: data.Content):
322
         super(Folder, self).__init__(path, environ, workspace)
352
         super(Folder, self).__init__(path, environ, workspace)
323
 
353
 
324
         self.content = content
354
         self.content = content
336
         return mktime(self.content.updated.timetuple())
366
         return mktime(self.content.updated.timetuple())
337
 
367
 
338
     def delete(self):
368
     def delete(self):
339
-        Manage(ActionDescription.DELETION, self.content_api, self.content).action()
369
+        ManageActions(ActionDescription.DELETION, self.content_api, self.content).action()
340
 
370
 
341
     def supportRecursiveMove(self, destpath: str):
371
     def supportRecursiveMove(self, destpath: str):
342
         return True
372
         return True
343
 
373
 
344
     def moveRecursive(self, destpath: str):
374
     def moveRecursive(self, destpath: str):
345
-        """As we support recursive move, copymovesingle won't be called, though with copy it'll be called
346
-        but i have to check if the client ever call that function..."""
375
+        """
376
+        As we support recursive move, copymovesingle won't be called, though with copy it'll be called
377
+        but i have to check if the client ever call that function...
378
+        """
347
         destpath = normpath(destpath)
379
         destpath = normpath(destpath)
348
 
380
 
349
         invalid_path = False
381
         invalid_path = False
357
             current_path = re.sub(r'/\.(deleted|archived)', '', self.path)
389
             current_path = re.sub(r'/\.(deleted|archived)', '', self.path)
358
 
390
 
359
             if current_path == destpath:
391
             if current_path == destpath:
360
-                Manage(
392
+                ManageActions(
361
                     ActionDescription.UNDELETION if self.content.is_deleted else ActionDescription.UNARCHIVING,
393
                     ActionDescription.UNDELETION if self.content.is_deleted else ActionDescription.UNARCHIVING,
362
                     self.content_api,
394
                     self.content_api,
363
                     self.content
395
                     self.content
371
             dest_path = re.sub(r'/\.(deleted|archived)', '', destpath)
403
             dest_path = re.sub(r'/\.(deleted|archived)', '', destpath)
372
 
404
 
373
             if dest_path == self.path:
405
             if dest_path == self.path:
374
-                Manage(
406
+                ManageActions(
375
                     ActionDescription.DELETION if '.deleted' in destpath else ActionDescription.ARCHIVING,
407
                     ActionDescription.DELETION if '.deleted' in destpath else ActionDescription.ARCHIVING,
376
                     self.content_api,
408
                     self.content_api,
377
                     self.content
409
                     self.content
392
             raise DAVError(HTTP_FORBIDDEN)
424
             raise DAVError(HTTP_FORBIDDEN)
393
 
425
 
394
     def move_folder(self, destpath):
426
     def move_folder(self, destpath):
395
-        parent = self.provider.get_parent_from_path(
396
-            normpath(destpath),
397
-            self.content_api,
398
-            WorkspaceApi(self.user)
399
-        )
400
 
427
 
428
+        workspace_api = WorkspaceApi(self.user)
401
         workspace = self.provider.get_workspace_from_path(
429
         workspace = self.provider.get_workspace_from_path(
430
+            normpath(destpath), workspace_api
431
+        )
432
+
433
+        parent = self.provider.get_parent_from_path(
402
             normpath(destpath),
434
             normpath(destpath),
403
-            WorkspaceApi(self.user)
435
+            self.content_api,
436
+            workspace
404
         )
437
         )
405
 
438
 
406
         with new_revision(self.content):
439
         with new_revision(self.content):
408
                 self.content_api.update_content(self.content, self.provider.transform_to_bdd(basename(destpath)))
441
                 self.content_api.update_content(self.content, self.provider.transform_to_bdd(basename(destpath)))
409
                 self.content_api.save(self.content)
442
                 self.content_api.save(self.content)
410
             else:
443
             else:
411
-                try:
412
-                    workspace_id = parent.workspace.workspace_id
413
-                except AttributeError:
414
-                    workspace_id = self.provider.get_workspace_from_path(
415
-                        destpath, WorkspaceApi(self.user)
416
-                    ).workspace_id
417
-
418
-                if workspace_id == self.content.workspace.workspace_id:
444
+                if workspace.workspace_id == self.content.workspace.workspace_id:
419
                     self.content_api.move(self.content, parent)
445
                     self.content_api.move(self.content, parent)
420
                 else:
446
                 else:
421
-                    try:
422
-                        self.content_api.move_recursively(self.content, parent, parent.workspace)
423
-                    except AttributeError:
424
-                        self.content_api.move_recursively(self.content, parent, workspace)
447
+                    self.content_api.move_recursively(self.content, parent, workspace)
425
 
448
 
426
         transaction.commit()
449
         transaction.commit()
427
 
450
 
451
+    def getMemberList(self) -> [_DAVResource]:
452
+        members = []
453
+
454
+        for content in self.content.children:
455
+            content_path = '%s/%s' % (self.path, self.provider.transform_to_display(content.get_label()))
428
 
456
 
429
-class HistoryFolder(Folder):
457
+            if content.type == ContentType.Folder:
458
+                members.append(Folder(content_path, self.environ, self.workspace, content))
459
+            elif content.type == ContentType.File:
460
+                self._file_count += 1
461
+                members.append(File(content_path, self.environ, content))
462
+            else:
463
+                self._file_count += 1
464
+                members.append(OtherFile(content_path, self.environ, content))
465
+
466
+        if self._file_count > 0 and self.provider.show_history():
467
+            members.append(
468
+                HistoryFolder(
469
+                    path=self.path + '/' + ".history",
470
+                    environ=self.environ,
471
+                    content=self.content,
472
+                    workspace=self.workspace,
473
+                    type=HistoryType.Standard
474
+                )
475
+            )
476
+
477
+        if self.provider.show_delete():
478
+            members.append(
479
+                DeletedFolder(
480
+                    path=self.path + '/' + ".deleted",
481
+                    environ=self.environ,
482
+                    content=self.content,
483
+                    workspace=self.workspace
484
+                )
485
+            )
486
+
487
+        if self.provider.show_archive():
488
+            members.append(
489
+                ArchivedFolder(
490
+                    path=self.path + '/' + ".archived",
491
+                    environ=self.environ,
492
+                    content=self.content,
493
+                    workspace=self.workspace
494
+                )
495
+            )
496
+
497
+        return members
430
 
498
 
431
-    def __init__(self, path, environ, workspace: data.Workspace, type: str, content: data.Content=None):
432
-        super(HistoryFolder, self).__init__(path, environ, content, workspace)
499
+
500
+class HistoryFolder(Folder):
501
+    """
502
+    A virtual resource which contains a sub-folder for every files (DAVNonCollection) contained in the parent
503
+    folder
504
+    """
505
+    
506
+    def __init__(self, path, environ, workspace: data.Workspace,
507
+                 content: data.Content=None, type: str=HistoryType.Standard):
508
+        super(HistoryFolder, self).__init__(path, environ, workspace, content)
433
 
509
 
434
         self._is_archived = type == HistoryType.Archived
510
         self._is_archived = type == HistoryType.Archived
435
         self._is_deleted = type == HistoryType.Deleted
511
         self._is_deleted = type == HistoryType.Deleted
496
 
572
 
497
     def getMemberList(self) -> [_DAVResource]:
573
     def getMemberList(self) -> [_DAVResource]:
498
         members = []
574
         members = []
499
-
500
-        parent_id = None if self.content is None else self.content.id
501
-
502
-        for content in self.content_api.get_all_with_filter(parent_id, ContentType.Any, self.workspace):
575
+        
576
+        if self.content:
577
+            children = self.content.children
578
+        else:
579
+            children = self.content_api.get_all(None, ContentType.Any, self.workspace)
580
+        
581
+        for content in children:
503
             members.append(HistoryFileFolder(
582
             members.append(HistoryFileFolder(
504
                 path='%s/%s' % (self.path, content.get_label()),
583
                 path='%s/%s' % (self.path, content.get_label()),
505
                 environ=self.environ,
584
                 environ=self.environ,
509
 
588
 
510
 
589
 
511
 class DeletedFolder(HistoryFolder):
590
 class DeletedFolder(HistoryFolder):
591
+    """
592
+    A virtual resources which exists for every folder or workspaces which contains their deleted children
593
+    """
594
+
512
     def __init__(self, path: str, environ: dict, workspace: data.Workspace, content: data.Content=None):
595
     def __init__(self, path: str, environ: dict, workspace: data.Workspace, content: data.Content=None):
513
-        super(DeletedFolder, self).__init__(path, environ, workspace, HistoryType.Deleted, content)
596
+        super(DeletedFolder, self).__init__(path, environ, workspace, content, HistoryType.Deleted)
514
 
597
 
515
         self._file_count = 0
598
         self._file_count = 0
516
 
599
 
541
     def getMemberNames(self) -> [str]:
624
     def getMemberNames(self) -> [str]:
542
         retlist = []
625
         retlist = []
543
 
626
 
544
-        for content in self.content_api.get_all(
545
-                parent_id=self.content if self.content is None else self.content.id, content_type=ContentType.Any):
627
+        if self.content:
628
+            children = self.content.children
629
+        else:
630
+            children = self.content_api.get_all(None, ContentType.Any, self.workspace)
631
+
632
+        for content in children:
546
             if content.is_deleted:
633
             if content.is_deleted:
547
                 retlist.append(content.get_label())
634
                 retlist.append(content.get_label())
548
 
635
 
551
 
638
 
552
         return retlist
639
         return retlist
553
 
640
 
554
-    def createEmptyResource(self, name: str):
555
-        raise DAVError(HTTP_FORBIDDEN)
556
-
557
-    def createCollection(self, name: str):
558
-        raise DAVError(HTTP_FORBIDDEN)
559
-
560
-    def delete(self):
561
-        raise DAVError(HTTP_FORBIDDEN)
562
-
563
     def getMemberList(self) -> [_DAVResource]:
641
     def getMemberList(self) -> [_DAVResource]:
564
         members = []
642
         members = []
565
 
643
 
566
-        parent_id = None if self.content is None else self.content.id
644
+        if self.content:
645
+            children = self.content.children
646
+        else:
647
+            children = self.content_api.get_all(None, ContentType.Any, self.workspace)
567
 
648
 
568
-        for content in self.content_api.get_all_with_filter(parent_id, ContentType.Any, self.workspace):
649
+        for content in children:
569
             content_path = '%s/%s' % (self.path, self.provider.transform_to_display(content.get_label()))
650
             content_path = '%s/%s' % (self.path, self.provider.transform_to_display(content.get_label()))
570
 
651
 
571
             if content.type == ContentType.Folder:
652
             if content.type == ContentType.Folder:
572
-                members.append(Folder(content_path, self.environ, content, self.workspace))
653
+                members.append(Folder(content_path, self.environ, self.workspace, content))
573
             elif content.type == ContentType.File:
654
             elif content.type == ContentType.File:
574
                 self._file_count += 1
655
                 self._file_count += 1
575
                 members.append(File(content_path, self.environ, content))
656
                 members.append(File(content_path, self.environ, content))
592
 
673
 
593
 
674
 
594
 class ArchivedFolder(HistoryFolder):
675
 class ArchivedFolder(HistoryFolder):
676
+    """
677
+    A virtual resources which exists for every folder or workspaces which contains their archived children
678
+    """
595
     def __init__(self, path: str, environ: dict, workspace: data.Workspace, content: data.Content=None):
679
     def __init__(self, path: str, environ: dict, workspace: data.Workspace, content: data.Content=None):
596
-        super(ArchivedFolder, self).__init__(path, environ, workspace, HistoryType.Archived, content)
680
+        super(ArchivedFolder, self).__init__(path, environ, workspace, content, HistoryType.Archived)
597
 
681
 
598
         self._file_count = 0
682
         self._file_count = 0
599
 
683
 
633
 
717
 
634
         return retlist
718
         return retlist
635
 
719
 
636
-    def createEmptyResource(self, name):
637
-        raise DAVError(HTTP_FORBIDDEN)
638
-
639
-    def createCollection(self, name):
640
-        raise DAVError(HTTP_FORBIDDEN)
641
-
642
-    def delete(self):
643
-        raise DAVError(HTTP_FORBIDDEN)
644
-
645
     def getMemberList(self) -> [_DAVResource]:
720
     def getMemberList(self) -> [_DAVResource]:
646
         members = []
721
         members = []
647
 
722
 
648
-        parent_id = None if self.content is None else self.content.id
723
+        if self.content:
724
+            children = self.content.children
725
+        else:
726
+            children = self.content_api.get_all(None, ContentType.Any, self.workspace)
649
 
727
 
650
-        for content in self.content_api.get_all_with_filter(parent_id, ContentType.Any, self.workspace):
728
+        for content in children:
651
             content_path = '%s/%s' % (self.path, self.provider.transform_to_display(content.get_label()))
729
             content_path = '%s/%s' % (self.path, self.provider.transform_to_display(content.get_label()))
652
 
730
 
653
             if content.type == ContentType.Folder:
731
             if content.type == ContentType.Folder:
654
-                members.append(Folder(content_path, self.environ, content, self.workspace))
732
+                members.append(Folder(content_path, self.environ, self.workspace, content))
655
             elif content.type == ContentType.File:
733
             elif content.type == ContentType.File:
656
                 self._file_count += 1
734
                 self._file_count += 1
657
                 members.append(File(content_path, self.environ, content))
735
                 members.append(File(content_path, self.environ, content))
674
 
752
 
675
 
753
 
676
 class HistoryFileFolder(HistoryFolder):
754
 class HistoryFileFolder(HistoryFolder):
755
+    """
756
+    A virtual resource that contains for a given content (file/page/thread) all its revisions
757
+    """
758
+
677
     def __init__(self, path: str, environ: dict, content: data.Content):
759
     def __init__(self, path: str, environ: dict, content: data.Content):
678
-        super(HistoryFileFolder, self).__init__(path, environ, content.workspace, HistoryType.All, content)
760
+        super(HistoryFileFolder, self).__init__(path, environ, content.workspace, content, HistoryType.All)
679
 
761
 
680
     def __repr__(self) -> str:
762
     def __repr__(self) -> str:
681
         return "<DAVCollection: HistoryFileFolder (%s)" % self.content.file_name
763
         return "<DAVCollection: HistoryFileFolder (%s)" % self.content.file_name
686
     def createCollection(self, name):
768
     def createCollection(self, name):
687
         raise DAVError(HTTP_FORBIDDEN)
769
         raise DAVError(HTTP_FORBIDDEN)
688
 
770
 
689
-    def createEmptyResource(self, name) ->FakeFileStream:
690
-        return FakeFileStream(
691
-            content=self.content,
692
-            content_api=self.content_api,
693
-            file_name=name,
694
-            workspace=self.content.workspace
695
-        )
696
-
697
     def getMemberNames(self) -> [int]:
771
     def getMemberNames(self) -> [int]:
698
-        """ Usually we would return a string, but here as it can be the same name because that's history, we get the id"""
772
+        """
773
+        Usually we would return a string, but here as we're working with different
774
+        revisions of the same content, we'll work with revision_id
775
+        """
699
         ret = []
776
         ret = []
700
 
777
 
701
         for content in self.content.revisions:
778
         for content in self.content.revisions:
722
                 content=self.content,
799
                 content=self.content,
723
                 content_revision=revision)
800
                 content_revision=revision)
724
 
801
 
725
-    def delete(self):
726
-        raise DAVError(HTTP_FORBIDDEN)
727
-
728
     def getMemberList(self) -> [_DAVResource]:
802
     def getMemberList(self) -> [_DAVResource]:
729
         members = []
803
         members = []
730
 
804
 
751
 
825
 
752
 
826
 
753
 class File(DAVNonCollection):
827
 class File(DAVNonCollection):
828
+    """
829
+    File resource corresponding to tracim's files
830
+    """
754
     def __init__(self, path: str, environ: dict, content: Content):
831
     def __init__(self, path: str, environ: dict, content: Content):
755
         super(File, self).__init__(path, environ)
832
         super(File, self).__init__(path, environ)
756
 
833
 
758
         self.user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
835
         self.user = UserApi(None).get_one_by_email(environ['http_authenticator.username'])
759
         self.content_api = ContentApi(self.user)
836
         self.content_api = ContentApi(self.user)
760
 
837
 
838
+        # this is the property that windows client except to check if the file is read-write or read-only,
839
+        # but i wasn't able to set this property so you'll have to look into it >.>
840
+        # self.setPropertyValue('Win32FileAttributes', '00000021')
761
 
841
 
762
     def getPreferredPath(self):
842
     def getPreferredPath(self):
763
         fix_txt = '.txt' if self.getContentType() == 'text/plain' else mimetypes.guess_extension(self.getContentType())
843
         fix_txt = '.txt' if self.getContentType() == 'text/plain' else mimetypes.guess_extension(self.getContentType())
797
             content=self.content,
877
             content=self.content,
798
             content_api=self.content_api,
878
             content_api=self.content_api,
799
             file_name=self.content.get_label(),
879
             file_name=self.content.get_label(),
800
-            workspace=self.content.workspace
880
+            workspace=self.content.workspace,
881
+            path=self.path
801
         )
882
         )
802
 
883
 
803
     def moveRecursive(self, destpath):
884
     def moveRecursive(self, destpath):
816
             current_path = re.sub(r'/\.(deleted|archived)', '', self.path)
897
             current_path = re.sub(r'/\.(deleted|archived)', '', self.path)
817
 
898
 
818
             if current_path == destpath:
899
             if current_path == destpath:
819
-                Manage(
900
+                ManageActions(
820
                     ActionDescription.UNDELETION if self.content.is_deleted else ActionDescription.UNARCHIVING,
901
                     ActionDescription.UNDELETION if self.content.is_deleted else ActionDescription.UNARCHIVING,
821
                     self.content_api,
902
                     self.content_api,
822
                     self.content
903
                     self.content
830
             dest_path = re.sub(r'/\.(deleted|archived)', '', destpath)
911
             dest_path = re.sub(r'/\.(deleted|archived)', '', destpath)
831
 
912
 
832
             if dest_path == self.path:
913
             if dest_path == self.path:
833
-                Manage(
914
+                ManageActions(
834
                     ActionDescription.DELETION if '.deleted' in destpath else ActionDescription.ARCHIVING,
915
                     ActionDescription.DELETION if '.deleted' in destpath else ActionDescription.ARCHIVING,
835
                     self.content_api,
916
                     self.content_api,
836
                     self.content
917
                     self.content
880
         return True
961
         return True
881
 
962
 
882
     def delete(self):
963
     def delete(self):
883
-        Manage(ActionDescription.DELETION, self.content_api, self.content).action()
964
+        ManageActions(ActionDescription.DELETION, self.content_api, self.content).action()
884
 
965
 
885
 
966
 
886
 class HistoryFile(File):
967
 class HistoryFile(File):
968
+    """
969
+    A virtual resource corresponding to a specific tracim's revision's file
970
+    """
887
     def __init__(self, path: str, environ: dict, content: data.Content, content_revision: data.ContentRevisionRO):
971
     def __init__(self, path: str, environ: dict, content: data.Content, content_revision: data.ContentRevisionRO):
888
         super(HistoryFile, self).__init__(path, environ, content)
972
         super(HistoryFile, self).__init__(path, environ, content)
889
         self.content_revision = content_revision
973
         self.content_revision = content_revision
914
     def delete(self):
998
     def delete(self):
915
         raise DAVError(HTTP_FORBIDDEN)
999
         raise DAVError(HTTP_FORBIDDEN)
916
 
1000
 
917
-    def handleDelete(self):
918
-        return False
919
-
920
-    def handleCopy(self, destpath, depth_infinity):
921
-        return True
922
-
923
-    def handleMove(self, destpath):
924
-        return True
925
-
926
     def copyMoveSingle(self, destpath, ismove):
1001
     def copyMoveSingle(self, destpath, ismove):
927
         raise DAVError(HTTP_FORBIDDEN)
1002
         raise DAVError(HTTP_FORBIDDEN)
928
 
1003
 
929
 
1004
 
930
 class OtherFile(File):
1005
 class OtherFile(File):
1006
+    """
1007
+    File resource corresponding to tracim's page and thread
1008
+    """
931
     def __init__(self, path: str, environ: dict, content: data.Content):
1009
     def __init__(self, path: str, environ: dict, content: data.Content):
932
         super(OtherFile, self).__init__(path, environ, content)
1010
         super(OtherFile, self).__init__(path, environ, content)
933
 
1011
 
935
 
1013
 
936
         self.content_designed = self.design()
1014
         self.content_designed = self.design()
937
 
1015
 
938
-        # workaroung for consistent request as we have to return a resource with a path ending with .html
1016
+        # workaround for consistent request as we have to return a resource with a path ending with .html
939
         # when entering folder for windows, but only once because when we select it again it would have .html.html
1017
         # when entering folder for windows, but only once because when we select it again it would have .html.html
940
         # which is no good
1018
         # which is no good
941
         if not self.path.endswith('.html'):
1019
         if not self.path.endswith('.html'):
975
 
1053
 
976
 
1054
 
977
 class HistoryOtherFile(OtherFile):
1055
 class HistoryOtherFile(OtherFile):
1056
+    """
1057
+    A virtual resource corresponding to a specific tracim's revision's page and thread
1058
+    """
978
     def __init__(self, path: str, environ: dict, content: data.Content, content_revision: data.ContentRevisionRO):
1059
     def __init__(self, path: str, environ: dict, content: data.Content, content_revision: data.ContentRevisionRO):
979
         super(HistoryOtherFile, self).__init__(path, environ, content)
1060
         super(HistoryOtherFile, self).__init__(path, environ, content)
980
         self.content_revision = content_revision
1061
         self.content_revision = content_revision
995
 
1076
 
996
         return filestream
1077
         return filestream
997
 
1078
 
998
-    def beginWrite(self, contentType=None):
999
-        raise DAVError(HTTP_FORBIDDEN)
1000
-
1001
     def delete(self):
1079
     def delete(self):
1002
         raise DAVError(HTTP_FORBIDDEN)
1080
         raise DAVError(HTTP_FORBIDDEN)
1003
 
1081
 
1004
-    def handleDelete(self):
1005
-        return True
1006
-
1007
-    def handleCopy(self, destpath, depth_infinity):
1008
-        return True
1009
-
1010
-    def handleMove(self, destpath):
1011
-        return True
1012
-
1013
     def copyMoveSingle(self, destpath, ismove):
1082
     def copyMoveSingle(self, destpath, ismove):
1014
         raise DAVError(HTTP_FORBIDDEN)
1083
         raise DAVError(HTTP_FORBIDDEN)
1015
-

+ 99 - 2
tracim/tracim/lib/webdav/style.css 查看文件

1
-body {
2
-    background: red
1
+.title {
2
+	background:#F5F5F5;
3
+	padding-right:15px;
4
+	padding-left:15px;
5
+	padding-top:10px;
6
+	border-bottom:1px solid #CCCCCC;
7
+	overflow:auto;
8
+} .title h1 { margin-top:0; }
9
+
10
+.content {
11
+	padding: 15px;
12
+}
13
+
14
+#left{ padding:0; }
15
+
16
+#right {
17
+	background:#F5F5F5;
18
+	border-left:1px solid #CCCCCC;
19
+	border-bottom: 1px solid #CCCCCC;
20
+	padding-top:15px;
21
+}
22
+@media (max-width: 1200px) {
23
+	#right {
24
+		border-top:1px solid #CCCCCC;
25
+		border-left: none;
26
+		border-bottom: none;
27
+	}
28
+}
29
+
30
+body { overflow:auto; }
31
+
32
+.btn {
33
+	text-align: left;
34
+}
35
+
36
+.table tbody tr .my-align {
37
+	vertical-align:middle;
38
+}
39
+
40
+.title-icon {
41
+	font-size:2.5em;
42
+	float:left;
43
+	margin-right:10px;
44
+}
45
+.title.page, .title-icon.page { color:#00CC00; }
46
+.title.thread, .title-icon.thread { color:#428BCA; }
47
+
48
+/* ****************************** */
49
+.description-icon {
50
+	color:#999;
51
+	font-size:3em;
52
+}
53
+
54
+.description {
55
+	border-left: 5px solid #999;
56
+	padding-left: 10px;
57
+	margin-left: 10px;
58
+	margin-bottom:10px;
59
+}
60
+
61
+.description-text {
62
+	display:block;
63
+	overflow:hidden;
64
+	color:#999;
65
+}
66
+
67
+.comment-row:nth-child(2n) {
68
+	background-color:#F5F5F5;
69
+}
70
+
71
+.comment-row:nth-child(2n+1) {
72
+	background-color:#FFF;
73
+}
74
+
75
+.comment-icon {
76
+	color:#CCC;
77
+	font-size:3em;
78
+	display:inline-block;
79
+	margin-right: 10px;
80
+	float:left;
81
+}
82
+
83
+.comment-content {
84
+	display:block;
85
+	overflow:hidden;
86
+}
87
+
88
+.comment, .comment-revision {
89
+	padding:10px;
90
+	border-top: 1px solid #999;
91
+}
92
+
93
+.comment-revision-icon {
94
+	color:#777;
95
+	margin-right: 10px;
96
+}
97
+
98
+.title-text {
99
+	display: inline-block;
3
 }
100
 }