Skip to content

Commit ef33ab4

Browse files
authored
added TrashBin API (#106)
- TrashBin API: * `trashbin_list` * `trashbin_restore` * `trashbin_delete` * `trashbin_cleanup` --------- Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent e4d54ef commit ef33ab4

File tree

6 files changed

+139
-6
lines changed

6 files changed

+139
-6
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22

33
All notable changes to this project will be documented in this file.
44

5-
## [0.0.42 - 2023-08-30]
5+
## [0.0.42 - 2023-08-3x]
6+
7+
### Added
8+
9+
- TrashBin API:
10+
* `trashbin_list`
11+
* `trashbin_restore`
12+
* `trashbin_delete`
13+
* `trashbin_cleanup`
614

715
### Fixed
816

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Python library that provides a robust and well-documented API that allows develo
3636
| Text Processing** | N/A |||
3737
| SpeechToText** | N/A |||
3838

39-
&ast;missing `Trash bin` and `File version` support.<br>
39+
&ast;missing `File version` support.<br>
4040
&ast;&ast;available only for NextcloudApp
4141

4242
### Differences between the Nextcloud and NextcloudApp classes

nc_py_api/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version of nc_py_api."""
22

3-
__version__ = "0.0.41"
3+
__version__ = "0.0.42.dev0"

nc_py_api/files/__init__.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class FsNodeInfo:
2222
fileid: int
2323
"""Clear file ID without Nextcloud instance ID."""
2424
_last_modified: datetime.datetime
25+
_trashbin: dict
2526

2627
def __init__(self, **kwargs):
2728
self.size = kwargs.get("size", 0)
@@ -33,6 +34,10 @@ def __init__(self, **kwargs):
3334
self.last_modified = kwargs.get("last_modified", datetime.datetime(1970, 1, 1))
3435
except (ValueError, TypeError):
3536
self.last_modified = datetime.datetime(1970, 1, 1)
37+
self._trashbin: dict[str, typing.Union[str, int]] = {}
38+
for i in ("trashbin_filename", "trashbin_original_location", "trashbin_deletion_time"):
39+
if i in kwargs:
40+
self._trashbin[i] = kwargs[i]
3641

3742
@property
3843
def last_modified(self) -> datetime.datetime:
@@ -49,6 +54,26 @@ def last_modified(self, value: typing.Union[str, datetime.datetime]):
4954
else:
5055
self._last_modified = value
5156

57+
@property
58+
def in_trash(self) -> bool:
59+
"""Returns ``True`` if the object is in trash."""
60+
return bool(self._trashbin)
61+
62+
@property
63+
def trashbin_filename(self) -> str:
64+
"""Returns the name of the object in the trashbin."""
65+
return self._trashbin.get("trashbin_filename", "")
66+
67+
@property
68+
def trashbin_original_location(self) -> str:
69+
"""Returns the original path of the object."""
70+
return self._trashbin.get("trashbin_original_location", "")
71+
72+
@property
73+
def trashbin_deletion_time(self) -> int:
74+
"""Returns deletion time of the object."""
75+
return int(self._trashbin.get("trashbin_deletion_time", 0))
76+
5277

5378
@dataclasses.dataclass
5479
class FsNode:

nc_py_api/files/files.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,17 +326,65 @@ def setfav(self, path: Union[str, FsNode], value: Union[int, bool]) -> None:
326326
)
327327
check_error(webdav_response.status_code, f"setfav: path={path}, value={value}")
328328

329-
def _listdir(self, user: str, path: str, properties: list[str], depth: int, exclude_self: bool) -> list[FsNode]:
329+
def trashbin_list(self) -> list[FsNode]:
330+
"""Returns a list of all entries in the TrashBin."""
331+
properties = PROPFIND_PROPERTIES
332+
properties += ["nc:trashbin-filename", "nc:trashbin-original-location", "nc:trashbin-deletion-time"]
333+
return self._listdir(self._session.user, "", properties=properties, depth=1, exclude_self=False, trashbin=True)
334+
335+
def trashbin_restore(self, path: Union[str, FsNode]) -> None:
336+
"""Restore a file/directory from the TrashBin.
337+
338+
:param path: path to delete, e.g., the ``user_path`` field from ``FsNode`` or the **FsNode** class itself.
339+
"""
340+
restore_name = path.name if isinstance(path, FsNode) else path.split("/", maxsplit=1)[-1]
341+
path = path.user_path if isinstance(path, FsNode) else path
342+
343+
dest = self._session.cfg.dav_endpoint + f"/trashbin/{self._session.user}/restore/{restore_name}"
344+
headers = {"Destination": dest}
345+
response = self._session.dav(
346+
"MOVE",
347+
path=f"/trashbin/{self._session.user}/{path}",
348+
headers=headers,
349+
)
350+
check_error(response.status_code, f"trashbin_restore: user={self._session.user}, src={path}, dest={dest}")
351+
352+
def trashbin_delete(self, path: Union[str, FsNode], not_fail=False) -> None:
353+
"""Deletes a file/directory permanently from the TrashBin.
354+
355+
:param path: path to delete, e.g., the ``user_path`` field from ``FsNode`` or the **FsNode** class itself.
356+
:param not_fail: if set to ``True`` and the object is not found, it does not raise an exception.
357+
"""
358+
path = path.user_path if isinstance(path, FsNode) else path
359+
response = self._session.dav(method="DELETE", path=f"/trashbin/{self._session.user}/{path}")
360+
if response.status_code == 404 and not_fail:
361+
return
362+
check_error(response.status_code, f"delete_from_trashbin: user={self._session.user}, path={path}")
363+
364+
def trashbin_cleanup(self) -> None:
365+
"""Empties the TrashBin."""
366+
response = self._session.dav(method="DELETE", path=f"/trashbin/{self._session.user}/trash")
367+
check_error(response.status_code, f"trashbin_cleanup: user={self._session.user}")
368+
369+
def _listdir(
370+
self, user: str, path: str, properties: list[str], depth: int, exclude_self: bool, trashbin: bool = False
371+
) -> list[FsNode]:
330372
root = ElementTree.Element(
331373
"d:propfind",
332374
attrib={"xmlns:d": "DAV:", "xmlns:oc": "http://owncloud.org/ns", "xmlns:nc": "http://nextcloud.org/ns"},
333375
)
334376
prop = ElementTree.SubElement(root, "d:prop")
335377
for i in properties:
336378
ElementTree.SubElement(prop, i)
337-
headers = {"Depth": "infinity" if depth == -1 else str(depth)}
379+
if trashbin:
380+
dav_path = self._dav_get_obj_path(f"trashbin/{user}/trash", path, root_path="")
381+
else:
382+
dav_path = self._dav_get_obj_path(user, path)
338383
webdav_response = self._session.dav(
339-
"PROPFIND", self._dav_get_obj_path(user, path), data=self._element_tree_as_str(root), headers=headers
384+
"PROPFIND",
385+
dav_path,
386+
self._element_tree_as_str(root),
387+
headers={"Depth": "infinity" if depth == -1 else str(depth)},
340388
)
341389
request_info = f"list: {user}, {path}, {properties}"
342390
result = self._lf_parse_webdav_records(webdav_response, request_info)
@@ -387,6 +435,12 @@ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
387435
fs_node_args["permissions"] = prop["oc:permissions"]
388436
if "oc:favorite" in prop_keys:
389437
fs_node_args["favorite"] = bool(int(prop["oc:favorite"]))
438+
if "nc:trashbin-filename" in prop_keys:
439+
fs_node_args["trashbin_filename"] = prop["nc:trashbin-filename"]
440+
if "nc:trashbin-original-location" in prop_keys:
441+
fs_node_args["trashbin_original_location"] = prop["nc:trashbin-original-location"]
442+
if "nc:trashbin-deletion-time" in prop_keys:
443+
fs_node_args["trashbin_deletion_time"] = prop["nc:trashbin-deletion-time"]
390444
# xz = prop.get("oc:dDC", "")
391445
return FsNode(full_path, **fs_node_args)
392446

tests/files_test.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,3 +594,49 @@ def test_fs_node_last_modified_time():
594594
assert fs_node.info.last_modified == datetime(2023, 7, 29, 11, 56, 31)
595595
fs_node = FsNode("", last_modified=datetime(2022, 4, 5, 1, 2, 3))
596596
assert fs_node.info.last_modified == datetime(2022, 4, 5, 1, 2, 3)
597+
598+
599+
def test_trashbin(nc):
600+
r = nc.files.trashbin_list()
601+
assert isinstance(r, list)
602+
new_file = nc.files.upload("nc_py_api_temp.txt", content=b"")
603+
nc.files.delete(new_file)
604+
# minimum one object now in a trashbin
605+
r = nc.files.trashbin_list()
606+
assert r
607+
# clean up trashbin
608+
nc.files.trashbin_cleanup()
609+
# no objects should be in trashbin
610+
r = nc.files.trashbin_list()
611+
assert not r
612+
new_file = nc.files.upload("nc_py_api_temp.txt", content=b"")
613+
nc.files.delete(new_file)
614+
# one object now in a trashbin
615+
r = nc.files.trashbin_list()
616+
assert len(r) == 1
617+
# check properties types of FsNode
618+
i: FsNode = r[0]
619+
assert i.info.in_trash is True
620+
assert i.info.trashbin_filename.find("nc_py_api_temp.txt") != -1
621+
assert i.info.trashbin_original_location == "nc_py_api_temp.txt"
622+
assert isinstance(i.info.trashbin_deletion_time, int)
623+
# restore that object
624+
nc.files.trashbin_restore(r[0])
625+
# no files in trashbin
626+
r = nc.files.trashbin_list()
627+
assert not r
628+
# move a restored object to trashbin again
629+
nc.files.delete(new_file)
630+
# one object now in a trashbin
631+
r = nc.files.trashbin_list()
632+
assert len(r) == 1
633+
# remove one object from a trashbin
634+
nc.files.trashbin_delete(r[0])
635+
# NextcloudException with status_code 404
636+
with pytest.raises(NextcloudException) as e:
637+
nc.files.trashbin_delete(r[0])
638+
assert e.value.status_code == 404
639+
nc.files.trashbin_delete(r[0], not_fail=True)
640+
# no files in trashbin
641+
r = nc.files.trashbin_list()
642+
assert not r

0 commit comments

Comments
 (0)