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