Passed
Pull Request — master (#69)
by
unknown
01:00
created

nextcloud.api_wrappers.webdav   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 478
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 58
eloc 210
dl 0
loc 478
rs 4.5599
c 0
b 0
f 0

30 Methods

Rating   Name   Duplication   Size   Complexity  
A WebDAV.get_file() 0 13 3
A WebDAV.assure_tree_exists() 0 16 2
A File.upload_file() 0 10 1
A File.get_relative_path() 0 3 1
A WebDAV.delete_path() 0 11 1
A WebDAV.get_file_property() 0 30 5
B WebDAV.download_file() 0 45 8
A WebDAV._get_path() 0 5 2
A WebDAV.get_relative_path() 0 9 1
A File.dirname() 0 4 2
A File._extract_resource_type() 0 6 2
A WebDAV.upload_file() 0 17 3
A File.basename() 0 4 2
A WebDAV.list_folders() 0 25 2
A File.get_folder() 0 9 1
A File.__eq__() 0 2 1
A WebDAV.set_favorites() 0 12 1
A WebDAV.create_folder() 0 11 1
A WebDAV.upload_file_contents() 0 14 1
A WebDAV.list_favorites() 0 16 1
A File._get_remote_path() 0 3 2
A File.isdir() 0 3 1
A WebDAV.get_folder() 0 17 3
A WebDAV.copy_path() 0 16 1
A WebDAV.assure_folder_exists() 0 10 1
A File.list() 0 18 4
A WebDAV.move_path() 0 16 1
A File.delete() 0 8 1
A File.download() 0 11 1
A File.isfile() 0 3 1

How to fix   Complexity   

Complexity

Complex classes like nextcloud.api_wrappers.webdav often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
"""
3
WebDav API wrapper
4
See https://doc.owncloud.com/server/developer_manual/webdav_api/tags.html
5
"""
6
import re
7
import os
8
try:
9
    import pathlib
10
except:
11
    import pathlib2 as pathlib
12
13
import xml.etree.ElementTree as ET
14
from datetime import datetime
15
from nextcloud.base import WebDAVApiWrapper
16
from nextcloud.common.collections import PropertySet
17
from nextcloud.common.properties import Property as Prop, NAMESPACES_MAP
18
from nextcloud.common.value_parsing import (
19
    timestamp_to_epoch_time,
20
    datetime_to_timestamp
21
)
22
23
24
class NextCloudFileConflict(Exception):
25
    """ Exception to raise when you try to create a File that alreay exists """
26
27
28
class File(PropertySet):
29
    """
30
    Define properties on a WebDav file/folder
31
32
    Additionnally, provide an objective CRUD API
33
    (that probably consume more energy than fetching specific attributes)
34
35
    Example : 
36
    >>> root = nxc.get_folder()  # get root
37
    >>> def _list_rec(d, indent=""):
38
    >>>     # list files recursively
39
    >>>     print("%s%s%s" % (indent, d.basename(), '/' if d.isdir() else ''))
40
    >>>     if d.isdir():
41
    >>>         for i in d.list():
42
    >>>             _list_rec(i, indent=indent+"  ")
43
    >>>
44
    >>> _list_rec(root)
45
    """
46
    _attrs = [
47
        Prop('d:getlastmodified'),
48
        Prop('d:getetag'),
49
        Prop('d:getcontenttype'),
50
        Prop('d:resourcetype', parse_xml_value=(
51
            lambda p: File._extract_resource_type(p))),
52
        Prop('d:getcontentlength'),
53
        Prop('oc:id'),
54
        Prop('oc:fileid'),
55
        Prop('oc:favorite'),
56
        Prop('oc:comments-href'),
57
        Prop('oc:comments-count'),
58
        Prop('oc:comments-unread'),
59
        Prop('oc:owner-id'),
60
        Prop('oc:owner-display-name'),
61
        Prop('oc:share-types'),
62
        Prop('oc:checksums'),
63
        Prop('oc:size'),
64
        Prop('oc:href'),
65
        Prop('nc:has-preview')
66
    ]
67
68
    @staticmethod
69
    def _extract_resource_type(file_property):
70
        file_type = list(file_property)
71
        if file_type:
72
            return re.sub('{.*}', '', file_type[0].tag)
73
        return None
74
75
    def isfile(self):
76
        """ say if the file is a file /!\\ ressourcetype property shall be loaded """
77
        return not self.resource_type
78
79
    def isdir(self):
80
        """ say if the file is a directory /!\\ ressourcetype property shall be loaded """
81
        return self.resource_type == self.COLLECTION_RESOURCE_TYPE
82
83
    def get_relative_path(self):
84
        """ get path relative to user root """
85
        return self._wrapper.get_relative_path(self.href)
86
87
    def _get_remote_path(self, path=None):
88
        _url = self.get_relative_path()
89
        return '/'.join([_url, path]) if path else _url
90
91
    def basename(self):
92
        """ basename """
93
        _path = self._get_remote_path()
94
        return _path.split('/')[-2] if _path.endswith('/') else _path.split('/')[-1]
95
96
    def dirname(self):
97
        """ dirname """
98
        _path = self._get_remote_path()
99
        return '/'.join(_path.split('/')[:-2]) if _path.endswith('/') else '/'.join(_path.split('/')[:-1])
100
101
    def __eq__(self, b):
102
        return self.href == b.href
103
104
    # MINIMAL SET OF CRUD OPERATIONS
105
    def get_folder(self, path=None):
106
        """
107
        Get folder (see WebDav wrapper)
108
        :param subpath: if empty list current dir
109
        :returns: a folder (File object)
110
111
        Note : To check if sub folder exists, use get_file method
112
        """
113
        return self._wrapper.get_folder(self._get_remote_path(path))
114
115
    def get_folder(self, path=None):
116
        """
117
        Get folder (see WebDav wrapper)
118
        :param subpath: if empty list current dir
119
        :returns: a file or folder (File object)
120
        """
121
        return self._wrapper.get_file(self._get_remote_path(path))
122
123
    def list(self, subpath=''):
124
        """
125
        List folder (see WebDav wrapper)
126
        :param subpath: if empty list current dir
127
        :returns: list of Files
128
        """
129
        resp = self._wrapper.list_folders(
130
            self._get_remote_path(subpath),
131
            depth=1,
132
            all_properties=True
133
        )
134
        if resp.is_ok and resp.data:
135
            _dirs = resp.data
136
            # remove current dir
137
            if _dirs[0] == self:
138
                _dirs = _dirs[1:]
139
            return _dirs
140
        return []
141
142
    def upload_file(self, local_filepath, name, timestamp=None):
143
        """
144
        Upload file (see WebDav wrapper)
145
        :param name: name of the new file
146
        :returns: True if success
147
        """
148
        resp = self._wrapper.upload_file(local_filepath,
149
                                 self._get_remote_path(name),
150
                                 timestamp=timestamp)
151
        return resp.is_ok
152
153
    def download(self, name=None, target_dir=None):
154
        """
155
         file (see WebDav wrapper)
156
        :param name: name of the new file
157
        :returns: True if success
158
        """
159
        path = self._get_remote_path(name)
160
        target_path, _file_info = self._wrapper.download_file(path,
161
                                                        target_dir=target_dir)
162
        assert os.path.isfile(target_path), "Download failed"
163
        return target_path
164
165
    def delete(self, subpath=''):
166
        """
167
        Delete file or folder (see WebDav wrapper)
168
        :param subpath: if empty, delete current file
169
        :returns: True if success
170
        """
171
        resp = self._wrapper.delete_path(self._get_remote_path(subpath))
172
        return resp.is_ok
173
174
175
class WebDAV(WebDAVApiWrapper):
176
    """ WebDav API wrapper """
177
    API_URL = "/remote.php/dav/files"
178
179
    def _get_path(self, path):
180
        if path:
181
            return '/'.join([self.client.user, path]).replace('//', '/')
182
        else:
183
            return self.client.user
184
185
    def list_folders(self, path=None, depth=1, all_properties=False,
186
                     fields=None):
187
        """
188
        Get path files list with files properties with given depth
189
        (for current user)
190
191
        Args:
192
            path (str/None): files path
193
            depth (int): depth of listing files (directories content for example)
194
            all_properties (bool): list all available file properties in Nextcloud
195
            fields (str list): file properties to fetch
196
197
        Returns:
198
            list of dicts if json_output
199
            list of File objects if not json_output
200
        """
201
        data = File.build_xml_propfind(
202
            use_default=all_properties,
203
            fields=fields
204
        ) if (fields or all_properties) else None
205
        resp = self.requester.propfind(additional_url=self._get_path(path),
206
                                       headers={'Depth': str(depth)},
207
                                       data=data)
208
        return File.from_response(resp, json_output=self.json_output,
209
                                  wrapper=self)
210
211
    def download_file(self, path, target_dir=None):
212
        """
213
        Download file by path (for current user)
214
        File will be saved to working directory
215
        path argument must be valid file path
216
        Modified time of saved file will be synced with the file properties in Nextcloud
217
218
        Exception will be raised if:
219
            * path doesn't exist,
220
            * path is a directory, or if
221
            * file with same name already exists in working directory
222
223
        Args:
224
            path (str): file path
225
226
        Returns:
227
            a tuple (target_path, File object)
228
        """
229
        if not target_dir:
230
            target_dir='./'
231
        filename = path.split('/')[(-1)] if '/' in path else path
232
        file_data = self.get_file(path)
233
        if not file_data:
234
            raise ValueError("Given path doesn't exist")
235
        file_resource_type = file_data.resource_type
236
        if file_resource_type == File.COLLECTION_RESOURCE_TYPE:
237
            raise ValueError("This is a collection, please specify file path")
238
        if filename in os.listdir(target_dir):
239
            raise ValueError(
240
                "File with such name already exists in this directory")
241
        filename = os.path.join(target_dir, filename)
242
        res = self.requester.download(self._get_path(path))
243
        with open(filename, 'wb') as f:
244
            f.write(res.data)
245
246
        # get timestamp of downloaded file from file property on Nextcloud
247
        # If it succeeded, set the timestamp to saved local file
248
        # If the timestamp string is invalid or broken, the timestamp is downloaded time.
249
        file_timestamp_str = file_data.last_modified
250
        file_timestamp = timestamp_to_epoch_time(file_timestamp_str)
251
        if isinstance(file_timestamp, int):
252
            os.utime(filename, (
253
                datetime_to_timestamp(datetime.now()),
254
                file_timestamp))
255
        return (filename, file_data)
256
257
    def upload_file(self, local_filepath, remote_filepath, timestamp=None):
258
        """
259
        Upload file to Nextcloud storage
260
261
        Args:
262
            local_filepath (str): path to file on local storage
263
            remote_filepath (str): path where to upload file on Nextcloud storage
264
            timestamp (int): timestamp of upload file. If None, get time by local file.
265
266
        Returns:
267
            requester response
268
        """
269
        with open(local_filepath, 'rb') as f:
270
            file_contents = f.read()
271
        if timestamp is None:
272
            timestamp = int(os.path.getmtime(local_filepath))
273
        return self.upload_file_contents(file_contents, remote_filepath, timestamp)
274
275
    def upload_file_contents(self, file_contents, remote_filepath, timestamp=None):
276
        """
277
        Upload file to Nextcloud storage
278
279
        Args:
280
            file_contents (bytes): Bytes the file to be uploaded consists of
281
            remote_filepath (str): path where to upload file on Nextcloud storage
282
            timestamp (int):  mtime of upload file
283
284
        Returns:
285
            requester response
286
        """
287
        return self.requester.put_with_timestamp((self._get_path(remote_filepath)), data=file_contents,
288
                                                 timestamp=timestamp)
289
290
    def create_folder(self, folder_path):
291
        """
292
        Create folder on Nextcloud storage
293
294
        Args:
295
            folder_path (str): folder path
296
297
        Returns:
298
            requester response
299
        """
300
        return self.requester.make_collection(additional_url=(self._get_path(folder_path)))
301
302
    def assure_folder_exists(self, folder_path):
303
        """
304
        Create folder on Nextcloud storage, don't do anything if the folder already exists.
305
        Args:
306
            folder_path (str): folder path
307
        Returns:
308
            requester response
309
        """
310
        self.create_folder(folder_path)
311
        return True
312
313
    def assure_tree_exists(self, tree_path):
314
        """
315
        Make sure that the folder structure on Nextcloud storage exists
316
        Args:
317
            folder_path (str): The folder tree
318
        Returns:
319
            requester response
320
        """
321
        tree = pathlib.PurePath(tree_path)
322
        parents = list(tree.parents)
323
        ret = True
324
        subfolders = parents[:-1][::-1] + [tree]
325
        for subf in subfolders:
326
            ret = self.assure_folder_exists(str(subf))
327
328
        return ret
329
330
    def delete_path(self, path):
331
        """
332
        Delete file or folder with all content of given user by path
333
334
        Args:
335
            path (str): file or folder path to delete
336
337
        Returns:
338
            requester response
339
        """
340
        return self.requester.delete(url=self._get_path(path))
341
342
    def move_path(self, path, destination_path, overwrite=False):
343
        """
344
        Move file or folder to destination
345
346
        Args:
347
            path (str): file or folder path to move
348
            destionation_path (str): destination where to move
349
            overwrite (bool): allow destination path overriding
350
351
        Returns:
352
            requester response
353
        """
354
        return self.requester.move(url=self._get_path(path),
355
                                   destination=self._get_path(
356
                                       destination_path),
357
                                   overwrite=overwrite)
358
359
    def copy_path(self, path, destination_path, overwrite=False):
360
        """
361
        Copy file or folder to destination
362
363
        Args:
364
            path (str): file or folder path to copy
365
            destionation_path (str): destination where to copy
366
            overwrite (bool): allow destination path overriding
367
368
        Returns:
369
            requester response
370
        """
371
        return self.requester.copy(url=self._get_path(path),
372
                                   destination=self._get_path(
373
                                       destination_path),
374
                                   overwrite=overwrite)
375
376
    def set_favorites(self, path):
377
        """
378
        Set files of a user favorite
379
380
        Args:
381
            path (str): file or folder path to make favorite
382
383
        Returns:
384
            requester response
385
        """
386
        data = File.build_xml_propupdate({'oc': {'favorite': 1}})
387
        return self.requester.proppatch(additional_url=self._get_path(path), data=data)
388
389
    def list_favorites(self, path=''):
390
        """
391
        List favorites (files) of the user
392
393
        Args:
394
            path (str): file or folder path to make favorite
395
396
        Returns:
397
            requester response with <list>File in data
398
        """
399
        data = File.build_xml_propfind(
400
            instr='oc:filter-files', filter_rules={'oc': {'favorite': 1}})
401
        resp = self.requester.report(
402
            additional_url=self._get_path(path), data=data)
403
        return File.from_response(resp, json_output=self.json_output,
404
                                  wrapper=self)
405
406
    def get_file_property(self, path, field, tag='oc'):
407
        """
408
        Fetch asked properties from a file path.
409
410
        Args:
411
            path (str): file or folder path to make favorite
412
            field (str): field name
413
414
        Returns:
415
            requester response with asked value in data
416
        """
417
        if ':' in field:
418
            tag, field = field.split(':')
419
        get_file_prop_xpath = '{DAV:}propstat/d:prop/%s:%s' % (tag, field)
420
        data = File.build_xml_propfind(fields={tag: [field]})
421
        resp = self.requester.propfind(additional_url=(self._get_path(path)), headers={'Depth': str(0)},
422
                                       data=data)
423
        response_data = resp.data
424
        resp.data = None
425
        if not resp.is_ok:
426
            return resp
427
428
        response_xml_data = ET.fromstring(response_data)
429
        for xml_data in response_xml_data:
430
            for prop in xml_data.findall(get_file_prop_xpath,
431
                                         NAMESPACES_MAP):
432
                resp.data = prop.text
433
            break
434
435
        return resp
436
437
    def get_file(self, path):
438
        """
439
        Return the File object associated to the path
440
441
        :param path: path to the file
442
        :returns: File object or None
443
        """
444
        resp = self.client.with_attr(json_output=False).list_folders(
445
            path, all_properties=True, depth=0)
446
        if resp.is_ok:
447
            if resp.data:
448
                return resp.data[0]
449
        return None
450
451
    def get_folder(self, path=None):
452
        """
453
        Return the File object associated to the path
454
        If the file (folder or 'collection') doesn't exists, create it.
455
456
        :param path: path to the file/folder, if empty use root
457
        :returns: File object
458
        """
459
        fileobj = self.get_file(path)
460
        if fileobj:
461
            if not fileobj.isdir():
462
                raise NextCloudFileConflict(fileobj.href)
463
        else:
464
            self.client.create_folder(path)
465
            fileobj = self.get_file(path)
466
467
        return fileobj
468
469
    def get_relative_path(self, href):
470
        """
471
        Returns relative (to application / user) path
472
473
        :param href(str):  file href
474
        :returns   (str):  relative path
475
        """
476
        _app_root = '/'.join([self.API_URL, self.client.user])
477
        return href[len(_app_root):]
478