Passed
Pull Request — master (#43)
by Matěj
01:16
created

nextcloud.api_wrappers.webdav   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 296
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 33
eloc 136
dl 0
loc 296
rs 9.76
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A WebDAV.__init__() 0 3 1
A File.as_dict() 0 4 1
B File.__init__() 0 14 6
A WebDAV.delete_path() 0 10 1
B WebDAV.download_file() 0 33 7
A File._extract_resource_type() 0 5 2
A WebDAV.upload_file() 0 12 2
A WebDAV.list_folders() 0 51 5
A WebDAV.set_favorites() 0 19 1
A WebDAV.create_folder() 0 9 1
A WebDAV.list_favorites() 0 26 3
A WebDAV.upload_file_contents() 0 11 1
A WebDAV.copy_path() 0 14 1
A WebDAV.move_path() 0 14 1
1
# -*- coding: utf-8 -*-
2
import re
3
import os
4
5
import xml.etree.ElementTree as ET
6
7
from nextcloud.base import WithRequester
8
9
10
class WebDAV(WithRequester):
11
12
    API_URL = "/remote.php/dav/files"
13
14
    def __init__(self, *args, **kwargs):
15
        super(WebDAV, self).__init__(*args)
16
        self.json_output = kwargs.get('json_output')
17
18
    def list_folders(self, uid, path=None, depth=1, all_properties=False):
19
        """
20
        Get path files list with files properties for given user, with given depth
21
22
        Args:
23
            uid (str): uid of user
24
            path (str/None): files path
25
            depth (int): depth of listing files (directories content for example)
26
            all_properties (bool): list all available file properties in Nextcloud
27
28
        Returns:
29
            list of dicts if json_output
30
            list of File objects if not json_output
31
        """
32
        if all_properties:
33
            data = """<?xml version="1.0"?>
34
                <d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"
35
                            xmlns:nc="http://nextcloud.org/ns">
36
                  <d:prop>
37
                        <d:getlastmodified />
38
                        <d:getetag />
39
                        <d:getcontenttype />
40
                        <d:resourcetype />
41
                        <oc:fileid />
42
                        <oc:permissions />
43
                        <oc:size />
44
                        <d:getcontentlength />
45
                        <nc:has-preview />
46
                        <oc:favorite />
47
                        <oc:comments-unread />
48
                        <oc:owner-display-name />
49
                        <oc:share-types />
50
                  </d:prop>
51
                </d:propfind>
52
            """
53
        else:
54
            data = None
55
        additional_url = uid
56
        if path:
57
            additional_url = "{}/{}".format(additional_url, path)
58
        resp = self.requester.propfind(additional_url=additional_url,
59
                                       headers={"Depth": str(depth)},
60
                                       data=data)
61
        if not resp.is_ok:
62
            resp.data = None
63
            return resp
64
        response_data = resp.data
65
        response_xml_data = ET.fromstring(response_data)
66
        files_data = [File(single_file) for single_file in response_xml_data]
67
        resp.data = files_data if not self.json_output else [each.as_dict() for each in files_data]
68
        return resp
69
70
    def download_file(self, uid, path):
71
        """
72
        Download file of given user by path
73
        File will be saved to working directory
74
        path argument must be valid file path
75
76
        Exception will be raised if:
77
            * path doesn't exist,
78
            * path is a directory, or if
79
            * file with same name already exists in working directory
80
81
        Args:
82
            uid (str): uid of user
83
            path (str): file path
84
85
        Returns:
86
            None
87
        """
88
        additional_url = "/".join([uid, path])
89
        filename = path.split('/')[-1] if '/' in path else path
90
        file_data = self.list_folders(uid=uid, path=path, depth=0)
91
        if not file_data:
92
            raise ValueError("Given path doesn't exist")
93
        file_resource_type = (file_data.data[0].get('resource_type')
94
                              if self.json_output
95
                              else file_data.data[0].resource_type)
96
        if file_resource_type == File.COLLECTION_RESOURCE_TYPE:
97
            raise ValueError("This is a collection, please specify file path")
98
        if filename in os.listdir('./'):
99
            raise ValueError("File with such name already exists in this directory")
100
        res = self.requester.download(additional_url)
101
        with open(filename, 'wb') as f:
102
            f.write(res.data)
103
104
    def upload_file(self, uid, local_filepath, remote_filepath):
105
        """
106
        Upload file to Nextcloud storage
107
108
        Args:
109
            uid (str): uid of user
110
            local_filepath (str): path to file on local storage
111
            remote_filepath (str): path where to upload file on Nextcloud storage
112
        """
113
        with open(local_filepath, 'rb') as f:
114
            file_contents = f.read()
115
        return self.upload_file_contents(uid, file_contents, remote_filepath)
116
117
    def upload_file_contents(self, uid, file_contents, remote_filepath):
118
        """
119
        Upload file to Nextcloud storage
120
121
        Args:
122
            uid (str): uid of user
123
            file_contents (bytes): Bytes the file to be uploaded consists of
124
            remote_filepath (str): path where to upload file on Nextcloud storage
125
        """
126
        additional_url = "/".join([uid, remote_filepath])
127
        return self.requester.put(additional_url, data=file_contents)
128
129
    def create_folder(self, uid, folder_path):
130
        """
131
        Create folder on Nextcloud storage
132
133
        Args:
134
            uid (str): uid of user
135
            folder_path (str): folder path
136
        """
137
        return self.requester.make_collection(additional_url="/".join([uid, folder_path]))
138
139
    def delete_path(self, uid, path):
140
        """
141
        Delete file or folder with all content of given user by path
142
143
        Args:
144
            uid (str): uid of user
145
            path (str): file or folder path to delete
146
        """
147
        url = "/".join([uid, path])
148
        return self.requester.delete(url=url)
149
150
    def move_path(self, uid, path, destination_path, overwrite=False):
151
        """
152
        Move file or folder to destination
153
154
        Args:
155
            uid (str): uid of user
156
            path (str): file or folder path to move
157
            destionation_path (str): destination where to move
158
            overwrite (bool): allow destination path overriding
159
        """
160
        path_url = "/".join([uid, path])
161
        destination_path_url = "/".join([uid, destination_path])
162
        return self.requester.move(url=path_url,
163
                                   destination=destination_path_url, overwrite=overwrite)
164
165
    def copy_path(self, uid, path, destination_path, overwrite=False):
166
        """
167
        Copy file or folder to destination
168
169
        Args:
170
            uid (str): uid of user
171
            path (str): file or folder path to copy
172
            destionation_path (str): destination where to copy
173
            overwrite (bool): allow destination path overriding
174
        """
175
        path_url = "/".join([uid, path])
176
        destination_path_url = "/".join([uid, destination_path])
177
        return self.requester.copy(url=path_url,
178
                                   destination=destination_path_url, overwrite=overwrite)
179
180
    def set_favorites(self, uid, path):
181
        """
182
        Set files of a user favorite
183
184
        Args:
185
            uid (str): uid of user
186
            path (str): file or folder path to make favorite
187
        """
188
        data = """<?xml version="1.0"?>
189
        <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
190
          <d:set>
191
                <d:prop>
192
                  <oc:favorite>1</oc:favorite>
193
                </d:prop>
194
          </d:set>
195
        </d:propertyupdate>
196
        """
197
        url = "/".join([uid, path])
198
        return self.requester.proppatch(additional_url=url, data=data)
199
200
    def list_favorites(self, uid, path=""):
201
        """
202
        Set files of a user favorite
203
204
        Args:
205
            uid (str): uid of user
206
            path (str): file or folder path to make favorite
207
        """
208
        data = """<?xml version="1.0"?>
209
        <oc:filter-files xmlns:d="DAV:"
210
                         xmlns:oc="http://owncloud.org/ns"
211
                         xmlns:nc="http://nextcloud.org/ns">
212
                 <oc:filter-rules>
213
                         <oc:favorite>1</oc:favorite>
214
                 </oc:filter-rules>
215
         </oc:filter-files>
216
        """
217
        url = "/".join([uid, path])
218
        res = self.requester.report(additional_url=url, data=data)
219
        if not res.is_ok:
220
            res.data = None
221
            return res
222
        response_xml_data = ET.fromstring(res.data)
223
        files_data = [File(single_file) for single_file in response_xml_data]
224
        res.data = files_data if not self.json_output else [each.as_dict() for each in files_data]
225
        return res
226
227
228
class File(object):
229
230
    SUCCESS_STATUS = 'HTTP/1.1 200 OK'
231
232
    # key is NextCloud property, value is python variable name
233
    FILE_PROPERTIES = {
234
        # d:
235
        "getlastmodified": "last_modified",
236
        "getetag": "etag",
237
        "getcontenttype": "content_type",
238
        "resourcetype": "resource_type",
239
        "getcontentlength": "content_length",
240
        # oc:
241
        "id": "id",
242
        "fileid": "file_id",
243
        "favorite": "favorite",
244
        "comments-href": "comments_href",
245
        "comments-count": "comments_count",
246
        "comments-unread": "comments_unread",
247
        "owner-id": "owner_id",
248
        "owner-display-name": "owner_display_name",
249
        "share-types": "share_types",
250
        "checksums": "check_sums",
251
        "size": "size",
252
        "href": "href",
253
        # nc:
254
        "has-preview": "has_preview",
255
    }
256
    xml_namespaces_map = {
257
        "d": "DAV:",
258
        "oc": "http://owncloud.org/ns",
259
        "nc": "http://nextcloud.org/ns"
260
    }
261
    COLLECTION_RESOURCE_TYPE = 'collection'
262
263
    def __init__(self, xml_data):
264
        self.href = xml_data.find('d:href', self.xml_namespaces_map).text
265
        for propstat in xml_data.iter('{DAV:}propstat'):
266
            if propstat.find('d:status', self.xml_namespaces_map).text != self.SUCCESS_STATUS:
267
                continue
268
            for file_property in propstat.find('d:prop', self.xml_namespaces_map):
269
                file_property_name = re.sub("{.*}", "", file_property.tag)
270
                if file_property_name not in self.FILE_PROPERTIES:
271
                    continue
272
                if file_property_name == 'resourcetype':
273
                    value = self._extract_resource_type(file_property)
274
                else:
275
                    value = file_property.text
276
                setattr(self, self.FILE_PROPERTIES[file_property_name], value)
277
278
    def _extract_resource_type(self, file_property):
279
        file_type = list(file_property)
280
        if file_type:
281
            return re.sub("{.*}", "", file_type[0].tag)
282
        return None
283
284
    def as_dict(self):
285
        return {key: value
286
                for key, value in self.__dict__.items()
287
                if key in self.FILE_PROPERTIES.values()}
288
289
290
class WebDAVStatusCodes(object):
291
    CREATED_CODE = 201
292
    NO_CONTENT_CODE = 204
293
    MULTISTATUS_CODE = 207
294
    ALREADY_EXISTS_CODE = 405
295
    PRECONDITION_FAILED_CODE = 412
296