Passed
Pull Request — master (#44)
by Matěj
01:17
created

nextcloud.api_wrappers.webdav.WebDAV.copy_path()   A

Complexity

Conditions 1

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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