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

WebDAV.download_file()   B

Complexity

Conditions 7

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 33
rs 8
c 0
b 0
f 0
cc 7
nop 3
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
        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 = list(tree.parents)
161
        ret = True
162
        subfolders = parents[:-1][::-1] + [tree]
163
        for subf in subfolders:
164
            ret = self.assure_folder_exists(uid, str(subf))
165
        return ret
166
167
    def delete_path(self, uid, path):
168
        """
169
        Delete file or folder with all content of given user by path
170
171
        Args:
172
            uid (str): uid of user
173
            path (str): file or folder path to delete
174
        """
175
        url = "/".join([uid, path])
176
        return self.requester.delete(url=url)
177
178
    def move_path(self, uid, path, destination_path, overwrite=False):
179
        """
180
        Move file or folder to destination
181
182
        Args:
183
            uid (str): uid of user
184
            path (str): file or folder path to move
185
            destionation_path (str): destination where to move
186
            overwrite (bool): allow destination path overriding
187
        """
188
        path_url = "/".join([uid, path])
189
        destination_path_url = "/".join([uid, destination_path])
190
        return self.requester.move(url=path_url,
191
                                   destination=destination_path_url, overwrite=overwrite)
192
193
    def copy_path(self, uid, path, destination_path, overwrite=False):
194
        """
195
        Copy file or folder to destination
196
197
        Args:
198
            uid (str): uid of user
199
            path (str): file or folder path to copy
200
            destionation_path (str): destination where to copy
201
            overwrite (bool): allow destination path overriding
202
        """
203
        path_url = "/".join([uid, path])
204
        destination_path_url = "/".join([uid, destination_path])
205
        return self.requester.copy(url=path_url,
206
                                   destination=destination_path_url, overwrite=overwrite)
207
208
    def set_favorites(self, uid, path):
209
        """
210
        Set files of a user favorite
211
212
        Args:
213
            uid (str): uid of user
214
            path (str): file or folder path to make favorite
215
        """
216
        data = """<?xml version="1.0"?>
217
        <d:propertyupdate xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
218
          <d:set>
219
                <d:prop>
220
                  <oc:favorite>1</oc:favorite>
221
                </d:prop>
222
          </d:set>
223
        </d:propertyupdate>
224
        """
225
        url = "/".join([uid, path])
226
        return self.requester.proppatch(additional_url=url, data=data)
227
228
    def list_favorites(self, uid, path=""):
229
        """
230
        Set files of a user favorite
231
232
        Args:
233
            uid (str): uid of user
234
            path (str): file or folder path to make favorite
235
        """
236
        data = """<?xml version="1.0"?>
237
        <oc:filter-files xmlns:d="DAV:"
238
                         xmlns:oc="http://owncloud.org/ns"
239
                         xmlns:nc="http://nextcloud.org/ns">
240
                 <oc:filter-rules>
241
                         <oc:favorite>1</oc:favorite>
242
                 </oc:filter-rules>
243
         </oc:filter-files>
244
        """
245
        url = "/".join([uid, path])
246
        res = self.requester.report(additional_url=url, data=data)
247
        if not res.is_ok:
248
            res.data = None
249
            return res
250
        response_xml_data = ET.fromstring(res.data)
251
        files_data = [File(single_file) for single_file in response_xml_data]
252
        res.data = files_data if not self.json_output else [each.as_dict() for each in files_data]
253
        return res
254
255
256
class File(object):
257
258
    SUCCESS_STATUS = 'HTTP/1.1 200 OK'
259
260
    # key is NextCloud property, value is python variable name
261
    FILE_PROPERTIES = {
262
        # d:
263
        "getlastmodified": "last_modified",
264
        "getetag": "etag",
265
        "getcontenttype": "content_type",
266
        "resourcetype": "resource_type",
267
        "getcontentlength": "content_length",
268
        # oc:
269
        "id": "id",
270
        "fileid": "file_id",
271
        "favorite": "favorite",
272
        "comments-href": "comments_href",
273
        "comments-count": "comments_count",
274
        "comments-unread": "comments_unread",
275
        "owner-id": "owner_id",
276
        "owner-display-name": "owner_display_name",
277
        "share-types": "share_types",
278
        "checksums": "check_sums",
279
        "size": "size",
280
        "href": "href",
281
        # nc:
282
        "has-preview": "has_preview",
283
    }
284
    xml_namespaces_map = {
285
        "d": "DAV:",
286
        "oc": "http://owncloud.org/ns",
287
        "nc": "http://nextcloud.org/ns"
288
    }
289
    COLLECTION_RESOURCE_TYPE = 'collection'
290
291
    def __init__(self, xml_data):
292
        self.href = xml_data.find('d:href', self.xml_namespaces_map).text
293
        for propstat in xml_data.iter('{DAV:}propstat'):
294
            if propstat.find('d:status', self.xml_namespaces_map).text != self.SUCCESS_STATUS:
295
                continue
296
            for file_property in propstat.find('d:prop', self.xml_namespaces_map):
297
                file_property_name = re.sub("{.*}", "", file_property.tag)
298
                if file_property_name not in self.FILE_PROPERTIES:
299
                    continue
300
                if file_property_name == 'resourcetype':
301
                    value = self._extract_resource_type(file_property)
302
                else:
303
                    value = file_property.text
304
                setattr(self, self.FILE_PROPERTIES[file_property_name], value)
305
306
    def _extract_resource_type(self, file_property):
307
        file_type = list(file_property)
308
        if file_type:
309
            return re.sub("{.*}", "", file_type[0].tag)
310
        return None
311
312
    def as_dict(self):
313
        return {key: value
314
                for key, value in self.__dict__.items()
315
                if key in self.FILE_PROPERTIES.values()}
316
317
318
class WebDAVStatusCodes(object):
319
    CREATED_CODE = 201
320
    NO_CONTENT_CODE = 204
321
    MULTISTATUS_CODE = 207
322
    ALREADY_EXISTS_CODE = 405
323
    PRECONDITION_FAILED_CODE = 412
324