nc-py-api 0.11.0__py3-none-any.whl → 0.18.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nc_py_api/__init__.py +1 -1
- nc_py_api/_session.py +27 -14
- nc_py_api/_version.py +1 -1
- nc_py_api/apps.py +0 -13
- nc_py_api/ex_app/__init__.py +13 -3
- nc_py_api/ex_app/defs.py +17 -29
- nc_py_api/ex_app/events_listener.py +137 -0
- nc_py_api/ex_app/integration_fastapi.py +25 -14
- nc_py_api/ex_app/logging.py +46 -0
- nc_py_api/ex_app/misc.py +6 -1
- nc_py_api/ex_app/occ_commands.py +153 -0
- nc_py_api/ex_app/providers/providers.py +7 -21
- nc_py_api/ex_app/providers/task_processing.py +261 -0
- nc_py_api/ex_app/ui/files_actions.py +45 -61
- nc_py_api/files/__init__.py +76 -6
- nc_py_api/files/_files.py +12 -0
- nc_py_api/files/files.py +26 -488
- nc_py_api/files/files_async.py +528 -0
- nc_py_api/loginflow_v2.py +161 -0
- nc_py_api/nextcloud.py +77 -21
- nc_py_api/talk_bot.py +5 -0
- nc_py_api/users.py +3 -3
- nc_py_api/webhooks.py +224 -0
- {nc_py_api-0.11.0.dist-info → nc_py_api-0.18.1.dist-info}/METADATA +35 -23
- nc_py_api-0.18.1.dist-info/RECORD +53 -0
- {nc_py_api-0.11.0.dist-info → nc_py_api-0.18.1.dist-info}/WHEEL +1 -1
- {nc_py_api-0.11.0.dist-info → nc_py_api-0.18.1.dist-info}/licenses/AUTHORS +1 -0
- nc_py_api/ex_app/providers/speech_to_text.py +0 -128
- nc_py_api/ex_app/providers/text_processing.py +0 -135
- nc_py_api/ex_app/providers/translations.py +0 -165
- nc_py_api-0.11.0.dist-info/RECORD +0 -49
- {nc_py_api-0.11.0.dist-info → nc_py_api-0.18.1.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
"""Nextcloud API for working with the file system."""
|
|
2
|
+
|
|
3
|
+
import builtins
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from httpx import Headers
|
|
9
|
+
|
|
10
|
+
from .._exceptions import NextcloudException, NextcloudExceptionNotFound, check_error
|
|
11
|
+
from .._misc import random_string, require_capabilities
|
|
12
|
+
from .._session import AsyncNcSessionBasic
|
|
13
|
+
from . import FsNode, LockType, SystemTag
|
|
14
|
+
from ._files import (
|
|
15
|
+
PROPFIND_PROPERTIES,
|
|
16
|
+
PropFindType,
|
|
17
|
+
build_find_request,
|
|
18
|
+
build_list_by_criteria_req,
|
|
19
|
+
build_list_tag_req,
|
|
20
|
+
build_list_tags_response,
|
|
21
|
+
build_listdir_req,
|
|
22
|
+
build_listdir_response,
|
|
23
|
+
build_setfav_req,
|
|
24
|
+
build_tags_ids_for_object,
|
|
25
|
+
build_update_tag_req,
|
|
26
|
+
dav_get_obj_path,
|
|
27
|
+
element_tree_as_str,
|
|
28
|
+
etag_fileid_from_response,
|
|
29
|
+
get_propfind_properties,
|
|
30
|
+
lf_parse_webdav_response,
|
|
31
|
+
)
|
|
32
|
+
from .sharing import _AsyncFilesSharingAPI
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AsyncFilesAPI:
|
|
36
|
+
"""Class that encapsulates async file system and file sharing API."""
|
|
37
|
+
|
|
38
|
+
sharing: _AsyncFilesSharingAPI
|
|
39
|
+
"""API for managing Files Shares"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, session: AsyncNcSessionBasic):
|
|
42
|
+
self._session = session
|
|
43
|
+
self.sharing = _AsyncFilesSharingAPI(session)
|
|
44
|
+
|
|
45
|
+
async def listdir(self, path: str | FsNode = "", depth: int = 1, exclude_self=True) -> list[FsNode]:
|
|
46
|
+
"""Returns a list of all entries in the specified directory.
|
|
47
|
+
|
|
48
|
+
:param path: path to the directory to get the list.
|
|
49
|
+
:param depth: how many directory levels should be included in output. Default = **1** (only specified directory)
|
|
50
|
+
:param exclude_self: boolean value indicating whether the `path` itself should be excluded from the list or not.
|
|
51
|
+
Default = **True**.
|
|
52
|
+
"""
|
|
53
|
+
if exclude_self and not depth:
|
|
54
|
+
raise ValueError("Wrong input parameters, query will return nothing.")
|
|
55
|
+
properties = get_propfind_properties(await self._session.capabilities)
|
|
56
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
57
|
+
return await self._listdir(
|
|
58
|
+
await self._session.user, path, properties=properties, depth=depth, exclude_self=exclude_self
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def by_id(self, file_id: int | str | FsNode) -> FsNode | None:
|
|
62
|
+
"""Returns :py:class:`~nc_py_api.files.FsNode` by file_id if any.
|
|
63
|
+
|
|
64
|
+
:param file_id: can be full file ID with Nextcloud instance ID or only clear file ID.
|
|
65
|
+
"""
|
|
66
|
+
file_id = file_id.file_id if isinstance(file_id, FsNode) else file_id
|
|
67
|
+
result = await self.find(req=["eq", "fileid", file_id])
|
|
68
|
+
return result[0] if result else None
|
|
69
|
+
|
|
70
|
+
async def by_path(self, path: str | FsNode) -> FsNode | None:
|
|
71
|
+
"""Returns :py:class:`~nc_py_api.files.FsNode` by exact path if any."""
|
|
72
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
73
|
+
result = await self.listdir(path, depth=0, exclude_self=False)
|
|
74
|
+
return result[0] if result else None
|
|
75
|
+
|
|
76
|
+
async def find(self, req: list, path: str | FsNode = "") -> list[FsNode]:
|
|
77
|
+
"""Searches a directory for a file or subdirectory with a name.
|
|
78
|
+
|
|
79
|
+
:param req: list of conditions to search for. Detailed description here...
|
|
80
|
+
:param path: path where to search from. Default = **""**.
|
|
81
|
+
"""
|
|
82
|
+
# `req` possible keys: "name", "mime", "last_modified", "size", "favorite", "fileid"
|
|
83
|
+
root = build_find_request(req, path, await self._session.user, await self._session.capabilities)
|
|
84
|
+
webdav_response = await self._session.adapter_dav.request(
|
|
85
|
+
"SEARCH", "", content=element_tree_as_str(root), headers={"Content-Type": "text/xml"}
|
|
86
|
+
)
|
|
87
|
+
request_info = f"find: {await self._session.user}, {req}, {path}"
|
|
88
|
+
return lf_parse_webdav_response(self._session.cfg.dav_url_suffix, webdav_response, request_info)
|
|
89
|
+
|
|
90
|
+
async def download(self, path: str | FsNode) -> bytes:
|
|
91
|
+
"""Downloads and returns the content of a file."""
|
|
92
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
93
|
+
response = await self._session.adapter_dav.get(quote(dav_get_obj_path(await self._session.user, path)))
|
|
94
|
+
check_error(response, f"download: user={await self._session.user}, path={path}")
|
|
95
|
+
return response.content
|
|
96
|
+
|
|
97
|
+
async def download2stream(self, path: str | FsNode, fp, **kwargs) -> None:
|
|
98
|
+
"""Downloads file to the given `fp` object.
|
|
99
|
+
|
|
100
|
+
:param path: path to download file.
|
|
101
|
+
:param fp: filename (string), pathlib.Path object or a file object.
|
|
102
|
+
The object must implement the ``file.write`` method and be able to write binary data.
|
|
103
|
+
:param kwargs: **chunk_size** an int value specifying chunk size to write. Default = **5Mb**
|
|
104
|
+
"""
|
|
105
|
+
path = quote(dav_get_obj_path(await self._session.user, path.user_path if isinstance(path, FsNode) else path))
|
|
106
|
+
await self._session.download2stream(path, fp, dav=True, **kwargs)
|
|
107
|
+
|
|
108
|
+
async def download_directory_as_zip(
|
|
109
|
+
self, path: str | FsNode, local_path: str | Path | None = None, **kwargs
|
|
110
|
+
) -> Path:
|
|
111
|
+
"""Downloads a remote directory as zip archive.
|
|
112
|
+
|
|
113
|
+
:param path: path to directory to download.
|
|
114
|
+
:param local_path: relative or absolute file path to save zip file.
|
|
115
|
+
:returns: Path to the saved zip archive.
|
|
116
|
+
|
|
117
|
+
.. note:: This works only for directories, you should not use this to download a file.
|
|
118
|
+
"""
|
|
119
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
120
|
+
result_path = local_path if local_path else os.path.basename(path)
|
|
121
|
+
with open(result_path, "wb") as fp:
|
|
122
|
+
if (await self._session.nc_version)["major"] >= 31:
|
|
123
|
+
full_path = dav_get_obj_path(await self._session.user, path)
|
|
124
|
+
accept_header = f"application/{kwargs.get('format', 'zip')}"
|
|
125
|
+
await self._session.download2fp(quote(full_path), fp, dav=True, headers={"Accept": accept_header})
|
|
126
|
+
else:
|
|
127
|
+
await self._session.download2fp(
|
|
128
|
+
"/index.php/apps/files/ajax/download.php", fp, dav=False, params={"dir": path}, **kwargs
|
|
129
|
+
)
|
|
130
|
+
return Path(result_path)
|
|
131
|
+
|
|
132
|
+
async def upload(self, path: str | FsNode, content: bytes | str) -> FsNode:
|
|
133
|
+
"""Creates a file with the specified content at the specified path.
|
|
134
|
+
|
|
135
|
+
:param path: file's upload path.
|
|
136
|
+
:param content: content to create the file. If it is a string, it will be encoded into bytes using UTF-8.
|
|
137
|
+
"""
|
|
138
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
139
|
+
full_path = dav_get_obj_path(await self._session.user, path)
|
|
140
|
+
response = await self._session.adapter_dav.put(quote(full_path), content=content)
|
|
141
|
+
check_error(response, f"upload: user={await self._session.user}, path={path}, size={len(content)}")
|
|
142
|
+
return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
|
|
143
|
+
|
|
144
|
+
async def upload_stream(self, path: str | FsNode, fp, **kwargs) -> FsNode:
|
|
145
|
+
"""Creates a file with content provided by `fp` object at the specified path.
|
|
146
|
+
|
|
147
|
+
:param path: file's upload path.
|
|
148
|
+
:param fp: filename (string), pathlib.Path object or a file object.
|
|
149
|
+
The object must implement the ``file.read`` method providing data with str or bytes type.
|
|
150
|
+
:param kwargs: **chunk_size** an int value specifying chunk size to read. Default = **5Mb**
|
|
151
|
+
"""
|
|
152
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
153
|
+
chunk_size = kwargs.get("chunk_size", 5 * 1024 * 1024)
|
|
154
|
+
if isinstance(fp, str | Path):
|
|
155
|
+
with builtins.open(fp, "rb") as f:
|
|
156
|
+
return await self.__upload_stream(path, f, chunk_size)
|
|
157
|
+
elif hasattr(fp, "read"):
|
|
158
|
+
return await self.__upload_stream(path, fp, chunk_size)
|
|
159
|
+
else:
|
|
160
|
+
raise TypeError("`fp` must be a path to file or an object with `read` method.")
|
|
161
|
+
|
|
162
|
+
async def mkdir(self, path: str | FsNode) -> FsNode:
|
|
163
|
+
"""Creates a new directory.
|
|
164
|
+
|
|
165
|
+
:param path: path of the directory to be created.
|
|
166
|
+
"""
|
|
167
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
168
|
+
full_path = dav_get_obj_path(await self._session.user, path)
|
|
169
|
+
response = await self._session.adapter_dav.request("MKCOL", quote(full_path))
|
|
170
|
+
check_error(response)
|
|
171
|
+
full_path += "/" if not full_path.endswith("/") else ""
|
|
172
|
+
return FsNode(full_path.lstrip("/"), **etag_fileid_from_response(response))
|
|
173
|
+
|
|
174
|
+
async def makedirs(self, path: str | FsNode, exist_ok=False) -> FsNode | None:
|
|
175
|
+
"""Creates a new directory and subdirectories.
|
|
176
|
+
|
|
177
|
+
:param path: path of the directories to be created.
|
|
178
|
+
:param exist_ok: ignore error if any of pathname components already exists.
|
|
179
|
+
:returns: `FsNode` if directory was created or ``None`` if it was already created.
|
|
180
|
+
"""
|
|
181
|
+
_path = ""
|
|
182
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
183
|
+
path = path.lstrip("/")
|
|
184
|
+
result = None
|
|
185
|
+
for i in Path(path).parts:
|
|
186
|
+
_path = os.path.join(_path, i)
|
|
187
|
+
if not exist_ok:
|
|
188
|
+
result = await self.mkdir(_path)
|
|
189
|
+
else:
|
|
190
|
+
try:
|
|
191
|
+
result = await self.mkdir(_path)
|
|
192
|
+
except NextcloudException as e:
|
|
193
|
+
if e.status_code != 405:
|
|
194
|
+
raise e from None
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
async def delete(self, path: str | FsNode, not_fail=False) -> None:
|
|
198
|
+
"""Deletes a file/directory (moves to trash if trash is enabled).
|
|
199
|
+
|
|
200
|
+
:param path: path to delete.
|
|
201
|
+
:param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
|
|
202
|
+
"""
|
|
203
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
204
|
+
response = await self._session.adapter_dav.delete(quote(dav_get_obj_path(await self._session.user, path)))
|
|
205
|
+
if response.status_code == 404 and not_fail:
|
|
206
|
+
return
|
|
207
|
+
check_error(response)
|
|
208
|
+
|
|
209
|
+
async def move(self, path_src: str | FsNode, path_dest: str | FsNode, overwrite=False) -> FsNode:
|
|
210
|
+
"""Moves an existing file or a directory.
|
|
211
|
+
|
|
212
|
+
:param path_src: path of an existing file/directory.
|
|
213
|
+
:param path_dest: name of the new one.
|
|
214
|
+
:param overwrite: if ``True`` and the destination object already exists, it gets overwritten.
|
|
215
|
+
Default = **False**.
|
|
216
|
+
"""
|
|
217
|
+
path_src = path_src.user_path if isinstance(path_src, FsNode) else path_src
|
|
218
|
+
full_dest_path = dav_get_obj_path(
|
|
219
|
+
await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
220
|
+
)
|
|
221
|
+
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
222
|
+
headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
|
|
223
|
+
response = await self._session.adapter_dav.request(
|
|
224
|
+
"MOVE",
|
|
225
|
+
quote(dav_get_obj_path(await self._session.user, path_src)),
|
|
226
|
+
headers=headers,
|
|
227
|
+
)
|
|
228
|
+
check_error(response, f"move: user={await self._session.user}, src={path_src}, dest={dest}, {overwrite}")
|
|
229
|
+
return (await self.find(req=["eq", "fileid", response.headers["OC-FileId"]]))[0]
|
|
230
|
+
|
|
231
|
+
async def copy(self, path_src: str | FsNode, path_dest: str | FsNode, overwrite=False) -> FsNode:
|
|
232
|
+
"""Copies an existing file/directory.
|
|
233
|
+
|
|
234
|
+
:param path_src: path of an existing file/directory.
|
|
235
|
+
:param path_dest: name of the new one.
|
|
236
|
+
:param overwrite: if ``True`` and the destination object already exists, it gets overwritten.
|
|
237
|
+
Default = **False**.
|
|
238
|
+
"""
|
|
239
|
+
path_src = path_src.user_path if isinstance(path_src, FsNode) else path_src
|
|
240
|
+
full_dest_path = dav_get_obj_path(
|
|
241
|
+
await self._session.user, path_dest.user_path if isinstance(path_dest, FsNode) else path_dest
|
|
242
|
+
)
|
|
243
|
+
dest = self._session.cfg.dav_endpoint + quote(full_dest_path)
|
|
244
|
+
headers = Headers({"Destination": dest, "Overwrite": "T" if overwrite else "F"}, encoding="utf-8")
|
|
245
|
+
response = await self._session.adapter_dav.request(
|
|
246
|
+
"COPY",
|
|
247
|
+
quote(dav_get_obj_path(await self._session.user, path_src)),
|
|
248
|
+
headers=headers,
|
|
249
|
+
)
|
|
250
|
+
check_error(response, f"copy: user={await self._session.user}, src={path_src}, dest={dest}, {overwrite}")
|
|
251
|
+
return (await self.find(req=["eq", "fileid", response.headers["OC-FileId"]]))[0]
|
|
252
|
+
|
|
253
|
+
async def list_by_criteria(
|
|
254
|
+
self, properties: list[str] | None = None, tags: list[int | SystemTag] | None = None
|
|
255
|
+
) -> list[FsNode]:
|
|
256
|
+
"""Returns a list of all files/directories for the current user filtered by the specified values.
|
|
257
|
+
|
|
258
|
+
:param properties: List of ``properties`` that should have been set for the file.
|
|
259
|
+
Supported values: **favorite**
|
|
260
|
+
:param tags: List of ``tags ids`` or ``SystemTag`` that should have been set for the file.
|
|
261
|
+
"""
|
|
262
|
+
root = build_list_by_criteria_req(properties, tags, await self._session.capabilities)
|
|
263
|
+
webdav_response = await self._session.adapter_dav.request(
|
|
264
|
+
"REPORT", dav_get_obj_path(await self._session.user), content=element_tree_as_str(root)
|
|
265
|
+
)
|
|
266
|
+
request_info = f"list_files_by_criteria: {await self._session.user}"
|
|
267
|
+
check_error(webdav_response, request_info)
|
|
268
|
+
return lf_parse_webdav_response(self._session.cfg.dav_url_suffix, webdav_response, request_info)
|
|
269
|
+
|
|
270
|
+
async def setfav(self, path: str | FsNode, value: int | bool) -> None:
|
|
271
|
+
"""Sets or unsets favourite flag for specific file.
|
|
272
|
+
|
|
273
|
+
:param path: path to the object to set the state.
|
|
274
|
+
:param value: value to set for the ``favourite`` state.
|
|
275
|
+
"""
|
|
276
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
277
|
+
root = build_setfav_req(value)
|
|
278
|
+
webdav_response = await self._session.adapter_dav.request(
|
|
279
|
+
"PROPPATCH", quote(dav_get_obj_path(await self._session.user, path)), content=element_tree_as_str(root)
|
|
280
|
+
)
|
|
281
|
+
check_error(webdav_response, f"setfav: path={path}, value={value}")
|
|
282
|
+
|
|
283
|
+
async def trashbin_list(self) -> list[FsNode]:
|
|
284
|
+
"""Returns a list of all entries in the TrashBin."""
|
|
285
|
+
properties = PROPFIND_PROPERTIES
|
|
286
|
+
properties += ["nc:trashbin-filename", "nc:trashbin-original-location", "nc:trashbin-deletion-time"]
|
|
287
|
+
return await self._listdir(
|
|
288
|
+
await self._session.user,
|
|
289
|
+
"",
|
|
290
|
+
properties=properties,
|
|
291
|
+
depth=1,
|
|
292
|
+
exclude_self=False,
|
|
293
|
+
prop_type=PropFindType.TRASHBIN,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
async def trashbin_restore(self, path: str | FsNode) -> None:
|
|
297
|
+
"""Restore a file/directory from the TrashBin.
|
|
298
|
+
|
|
299
|
+
:param path: path to delete, e.g., the ``user_path`` field from ``FsNode`` or the **FsNode** class itself.
|
|
300
|
+
"""
|
|
301
|
+
restore_name = path.name if isinstance(path, FsNode) else path.split("/", maxsplit=1)[-1]
|
|
302
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
303
|
+
|
|
304
|
+
dest = self._session.cfg.dav_endpoint + f"/trashbin/{await self._session.user}/restore/{restore_name}"
|
|
305
|
+
headers = Headers({"Destination": dest}, encoding="utf-8")
|
|
306
|
+
response = await self._session.adapter_dav.request(
|
|
307
|
+
"MOVE",
|
|
308
|
+
quote(f"/trashbin/{await self._session.user}/{path}"),
|
|
309
|
+
headers=headers,
|
|
310
|
+
)
|
|
311
|
+
check_error(response, f"trashbin_restore: user={await self._session.user}, src={path}, dest={dest}")
|
|
312
|
+
|
|
313
|
+
async def trashbin_delete(self, path: str | FsNode, not_fail=False) -> None:
|
|
314
|
+
"""Deletes a file/directory permanently from the TrashBin.
|
|
315
|
+
|
|
316
|
+
:param path: path to delete, e.g., the ``user_path`` field from ``FsNode`` or the **FsNode** class itself.
|
|
317
|
+
:param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
|
|
318
|
+
"""
|
|
319
|
+
path = path.user_path if isinstance(path, FsNode) else path
|
|
320
|
+
response = await self._session.adapter_dav.delete(quote(f"/trashbin/{await self._session.user}/{path}"))
|
|
321
|
+
if response.status_code == 404 and not_fail:
|
|
322
|
+
return
|
|
323
|
+
check_error(response)
|
|
324
|
+
|
|
325
|
+
async def trashbin_cleanup(self) -> None:
|
|
326
|
+
"""Empties the TrashBin."""
|
|
327
|
+
check_error(await self._session.adapter_dav.delete(f"/trashbin/{await self._session.user}/trash"))
|
|
328
|
+
|
|
329
|
+
async def get_versions(self, file_object: FsNode) -> list[FsNode]:
|
|
330
|
+
"""Returns a list of all file versions if any."""
|
|
331
|
+
require_capabilities("files.versioning", await self._session.capabilities)
|
|
332
|
+
return await self._listdir(
|
|
333
|
+
await self._session.user,
|
|
334
|
+
str(file_object.info.fileid) if file_object.info.fileid else file_object.file_id,
|
|
335
|
+
properties=PROPFIND_PROPERTIES,
|
|
336
|
+
depth=1,
|
|
337
|
+
exclude_self=False,
|
|
338
|
+
prop_type=PropFindType.VERSIONS_FILEID if file_object.info.fileid else PropFindType.VERSIONS_FILE_ID,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
async def restore_version(self, file_object: FsNode) -> None:
|
|
342
|
+
"""Restore a file with specified version.
|
|
343
|
+
|
|
344
|
+
:param file_object: The **FsNode** class from :py:meth:`~nc_py_api.files.files.FilesAPI.get_versions`.
|
|
345
|
+
"""
|
|
346
|
+
require_capabilities("files.versioning", await self._session.capabilities)
|
|
347
|
+
dest = self._session.cfg.dav_endpoint + f"/versions/{await self._session.user}/restore/{file_object.name}"
|
|
348
|
+
headers = Headers({"Destination": dest}, encoding="utf-8")
|
|
349
|
+
response = await self._session.adapter_dav.request(
|
|
350
|
+
"MOVE",
|
|
351
|
+
quote(f"/versions/{await self._session.user}/{file_object.user_path}"),
|
|
352
|
+
headers=headers,
|
|
353
|
+
)
|
|
354
|
+
check_error(response, f"restore_version: user={await self._session.user}, src={file_object.user_path}")
|
|
355
|
+
|
|
356
|
+
async def list_tags(self) -> list[SystemTag]:
|
|
357
|
+
"""Returns list of the avalaible Tags."""
|
|
358
|
+
root = build_list_tag_req()
|
|
359
|
+
response = await self._session.adapter_dav.request("PROPFIND", "/systemtags", content=element_tree_as_str(root))
|
|
360
|
+
return build_list_tags_response(response)
|
|
361
|
+
|
|
362
|
+
async def get_tags(self, file_id: FsNode | int) -> list[SystemTag]:
|
|
363
|
+
"""Returns list of Tags assigned to the File or Directory."""
|
|
364
|
+
fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
|
|
365
|
+
url_to_fetch = f"/systemtags-relations/files/{fs_object}/"
|
|
366
|
+
response = await self._session.adapter_dav.request("PROPFIND", url_to_fetch)
|
|
367
|
+
object_tags_ids = build_tags_ids_for_object(self._session.cfg.dav_url_suffix + url_to_fetch, response)
|
|
368
|
+
if not object_tags_ids:
|
|
369
|
+
return []
|
|
370
|
+
all_tags = await self.list_tags()
|
|
371
|
+
return [tag for tag in all_tags if tag.tag_id in object_tags_ids]
|
|
372
|
+
|
|
373
|
+
async def create_tag(self, name: str, user_visible: bool = True, user_assignable: bool = True) -> None:
|
|
374
|
+
"""Creates a new Tag.
|
|
375
|
+
|
|
376
|
+
:param name: Name of the tag.
|
|
377
|
+
:param user_visible: Should be Tag visible in the UI.
|
|
378
|
+
:param user_assignable: Can Tag be assigned from the UI.
|
|
379
|
+
"""
|
|
380
|
+
response = await self._session.adapter_dav.post(
|
|
381
|
+
"/systemtags",
|
|
382
|
+
json={
|
|
383
|
+
"name": name,
|
|
384
|
+
"userVisible": user_visible,
|
|
385
|
+
"userAssignable": user_assignable,
|
|
386
|
+
},
|
|
387
|
+
)
|
|
388
|
+
check_error(response, info=f"create_tag({name})")
|
|
389
|
+
|
|
390
|
+
async def update_tag(
|
|
391
|
+
self,
|
|
392
|
+
tag_id: int | SystemTag,
|
|
393
|
+
name: str | None = None,
|
|
394
|
+
user_visible: bool | None = None,
|
|
395
|
+
user_assignable: bool | None = None,
|
|
396
|
+
) -> None:
|
|
397
|
+
"""Updates the Tag information."""
|
|
398
|
+
tag_id = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
|
|
399
|
+
root = build_update_tag_req(name, user_visible, user_assignable)
|
|
400
|
+
response = await self._session.adapter_dav.request(
|
|
401
|
+
"PROPPATCH", f"/systemtags/{tag_id}", content=element_tree_as_str(root)
|
|
402
|
+
)
|
|
403
|
+
check_error(response)
|
|
404
|
+
|
|
405
|
+
async def delete_tag(self, tag_id: int | SystemTag) -> None:
|
|
406
|
+
"""Deletes the tag."""
|
|
407
|
+
tag_id = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
|
|
408
|
+
response = await self._session.adapter_dav.delete(f"/systemtags/{tag_id}")
|
|
409
|
+
check_error(response)
|
|
410
|
+
|
|
411
|
+
async def tag_by_name(self, tag_name: str) -> SystemTag:
|
|
412
|
+
"""Returns Tag info by its name if found or ``None`` otherwise."""
|
|
413
|
+
r = [i for i in await self.list_tags() if i.display_name == tag_name]
|
|
414
|
+
if not r:
|
|
415
|
+
raise NextcloudExceptionNotFound(f"Tag with name='{tag_name}' not found.")
|
|
416
|
+
return r[0]
|
|
417
|
+
|
|
418
|
+
async def assign_tag(self, file_id: FsNode | int, tag_id: SystemTag | int) -> None:
|
|
419
|
+
"""Assigns Tag to a file/directory."""
|
|
420
|
+
await self._file_change_tag_state(file_id, tag_id, True)
|
|
421
|
+
|
|
422
|
+
async def unassign_tag(self, file_id: FsNode | int, tag_id: SystemTag | int) -> None:
|
|
423
|
+
"""Removes Tag from a file/directory."""
|
|
424
|
+
await self._file_change_tag_state(file_id, tag_id, False)
|
|
425
|
+
|
|
426
|
+
async def lock(self, path: FsNode | str, lock_type: LockType = LockType.MANUAL_LOCK) -> None:
|
|
427
|
+
"""Locks the file.
|
|
428
|
+
|
|
429
|
+
.. note:: Exception codes: 423 - existing lock present.
|
|
430
|
+
"""
|
|
431
|
+
require_capabilities("files.locking", await self._session.capabilities)
|
|
432
|
+
full_path = dav_get_obj_path(await self._session.user, path.user_path if isinstance(path, FsNode) else path)
|
|
433
|
+
response = await self._session.adapter_dav.request(
|
|
434
|
+
"LOCK",
|
|
435
|
+
quote(full_path),
|
|
436
|
+
headers={"X-User-Lock": "1", "X-User-Lock-Type": str(lock_type.value)},
|
|
437
|
+
)
|
|
438
|
+
check_error(response, f"lock: user={self._session.user}, path={full_path}")
|
|
439
|
+
|
|
440
|
+
async def unlock(self, path: FsNode | str) -> None:
|
|
441
|
+
"""Unlocks the file.
|
|
442
|
+
|
|
443
|
+
.. note:: Exception codes: 412 - the file is not locked, 423 - the lock is owned by another user.
|
|
444
|
+
"""
|
|
445
|
+
require_capabilities("files.locking", await self._session.capabilities)
|
|
446
|
+
full_path = dav_get_obj_path(await self._session.user, path.user_path if isinstance(path, FsNode) else path)
|
|
447
|
+
response = await self._session.adapter_dav.request(
|
|
448
|
+
"UNLOCK",
|
|
449
|
+
quote(full_path),
|
|
450
|
+
headers={"X-User-Lock": "1"},
|
|
451
|
+
)
|
|
452
|
+
check_error(response, f"unlock: user={self._session.user}, path={full_path}")
|
|
453
|
+
|
|
454
|
+
async def _file_change_tag_state(self, file_id: FsNode | int, tag_id: SystemTag | int, tag_state: bool) -> None:
|
|
455
|
+
fs_object = file_id.info.fileid if isinstance(file_id, FsNode) else file_id
|
|
456
|
+
tag = tag_id.tag_id if isinstance(tag_id, SystemTag) else tag_id
|
|
457
|
+
response = await self._session.adapter_dav.request(
|
|
458
|
+
"PUT" if tag_state else "DELETE", f"/systemtags-relations/files/{fs_object}/{tag}"
|
|
459
|
+
)
|
|
460
|
+
check_error(
|
|
461
|
+
response,
|
|
462
|
+
info=f"({'Adding' if tag_state else 'Removing'} `{tag}` {'to' if tag_state else 'from'} {fs_object})",
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
async def _listdir(
|
|
466
|
+
self,
|
|
467
|
+
user: str,
|
|
468
|
+
path: str,
|
|
469
|
+
properties: list[str],
|
|
470
|
+
depth: int,
|
|
471
|
+
exclude_self: bool,
|
|
472
|
+
prop_type: PropFindType = PropFindType.DEFAULT,
|
|
473
|
+
) -> list[FsNode]:
|
|
474
|
+
root, dav_path = build_listdir_req(user, path, properties, prop_type)
|
|
475
|
+
webdav_response = await self._session.adapter_dav.request(
|
|
476
|
+
"PROPFIND",
|
|
477
|
+
quote(dav_path),
|
|
478
|
+
content=element_tree_as_str(root),
|
|
479
|
+
headers={"Depth": "infinity" if depth == -1 else str(depth)},
|
|
480
|
+
)
|
|
481
|
+
return build_listdir_response(
|
|
482
|
+
self._session.cfg.dav_url_suffix, webdav_response, user, path, properties, exclude_self, prop_type
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
async def __upload_stream(self, path: str, fp, chunk_size: int) -> FsNode:
|
|
486
|
+
_tmp_path = "nc-py-api-" + random_string(56)
|
|
487
|
+
_dav_path = quote(dav_get_obj_path(await self._session.user, _tmp_path, root_path="/uploads"))
|
|
488
|
+
_v2 = bool(self._session.cfg.options.upload_chunk_v2 and chunk_size >= 5 * 1024 * 1024)
|
|
489
|
+
full_path = dav_get_obj_path(await self._session.user, path)
|
|
490
|
+
headers = Headers({"Destination": self._session.cfg.dav_endpoint + quote(full_path)}, encoding="utf-8")
|
|
491
|
+
if _v2:
|
|
492
|
+
response = await self._session.adapter_dav.request("MKCOL", _dav_path, headers=headers)
|
|
493
|
+
else:
|
|
494
|
+
response = await self._session.adapter_dav.request("MKCOL", _dav_path)
|
|
495
|
+
check_error(response)
|
|
496
|
+
try:
|
|
497
|
+
start_bytes = end_bytes = chunk_number = 0
|
|
498
|
+
while True:
|
|
499
|
+
piece = fp.read(chunk_size)
|
|
500
|
+
if not piece:
|
|
501
|
+
break
|
|
502
|
+
end_bytes = start_bytes + len(piece)
|
|
503
|
+
if _v2:
|
|
504
|
+
response = await self._session.adapter_dav.put(
|
|
505
|
+
_dav_path + "/" + str(chunk_number), content=piece, headers=headers
|
|
506
|
+
)
|
|
507
|
+
else:
|
|
508
|
+
_filename = str(start_bytes).rjust(15, "0") + "-" + str(end_bytes).rjust(15, "0")
|
|
509
|
+
response = await self._session.adapter_dav.put(_dav_path + "/" + _filename, content=piece)
|
|
510
|
+
check_error(
|
|
511
|
+
response,
|
|
512
|
+
f"upload_stream(v={_v2}): user={await self._session.user}, path={path}, cur_size={end_bytes}",
|
|
513
|
+
)
|
|
514
|
+
start_bytes = end_bytes
|
|
515
|
+
chunk_number += 1
|
|
516
|
+
|
|
517
|
+
response = await self._session.adapter_dav.request(
|
|
518
|
+
"MOVE",
|
|
519
|
+
_dav_path + "/.file",
|
|
520
|
+
headers=headers,
|
|
521
|
+
)
|
|
522
|
+
check_error(
|
|
523
|
+
response,
|
|
524
|
+
f"upload_stream(v={_v2}): user={await self._session.user}, path={path}, total_size={end_bytes}",
|
|
525
|
+
)
|
|
526
|
+
return FsNode(full_path.strip("/"), **etag_fileid_from_response(response))
|
|
527
|
+
finally:
|
|
528
|
+
await self._session.adapter_dav.delete(_dav_path)
|