diff --git a/README.md b/README.md index e5923b22..fbf3e062 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Project cloud-py-api was **abandoned** and divided into two parts: * User status manipulation * Weather status * ~~Nextcloud notifications support~~ - * ~~Shares operations support~~ + * Shares support * ~~Talk support~~ ### Extended Features with installed App_ecosystem_v2: diff --git a/docs/reference/Shares.rst b/docs/reference/Shares.rst new file mode 100644 index 00000000..eb3ca4b2 --- /dev/null +++ b/docs/reference/Shares.rst @@ -0,0 +1,13 @@ +.. py:currentmodule:: nc_py_api.files_sharing + +File Sharing +============ + +The Shares API is universal for both modes and provides all the necessary methods for working with the Nextcloud Shares system. +Refer to the **share examples** to see how to use them nicely. + +.. autoclass:: Share + :members: + +.. autoclass:: FilesSharingAPI + :members: diff --git a/docs/reference/index.rst b/docs/reference/index.rst index 62367da6..5cbe6d02 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -5,5 +5,6 @@ Reference :maxdepth: 2 Files + Shares Session constants diff --git a/nc_py_api/__init__.py b/nc_py_api/__init__.py index 31a4873b..3cae6a98 100644 --- a/nc_py_api/__init__.py +++ b/nc_py_api/__init__.py @@ -6,6 +6,7 @@ from .constants import ApiScope, LogLvl, SharePermissions, ShareStatus, ShareType from .exceptions import NextcloudException, NextcloudExceptionNotFound, check_error from .files import FsNode, FsNodeInfo +from .files_sharing import Share from .integration_fastapi import ( enable_heartbeat, nc_app, diff --git a/nc_py_api/constants.py b/nc_py_api/constants.py index 677b9b05..34e4efb4 100644 --- a/nc_py_api/constants.py +++ b/nc_py_api/constants.py @@ -1,6 +1,6 @@ """Common constants. Do not use them directly, all public ones are imported to __init__.py""" -from enum import IntEnum +from enum import IntEnum, IntFlag from typing import TypedDict @@ -36,10 +36,8 @@ class OCSRespond(IntEnum): RESPOND_UNKNOWN_ERROR = 999 -class SharePermissions(IntEnum): - """The share permissions to be set. - - All permissions can be combined with each other, except ``PERMISSION_ALL``""" +class SharePermissions(IntFlag): + """The share permissions to be set""" PERMISSION_READ = 1 """Access to read""" @@ -51,8 +49,6 @@ class SharePermissions(IntEnum): """Access to remove objects in the share""" PERMISSION_SHARE = 16 """Access to re-share objects in the share""" - PERMISSION_ALL = 31 - """Full access to the shared object""" class ShareType(IntEnum): diff --git a/nc_py_api/files.py b/nc_py_api/files.py index 4f8908fc..995ba4da 100644 --- a/nc_py_api/files.py +++ b/nc_py_api/files.py @@ -1,5 +1,5 @@ """ -Nextcloud API for working with file system. +Nextcloud API for working with the file system. """ import builtins @@ -168,6 +168,8 @@ def is_creatable(self) -> bool: class FilesAPI: + """This class provides all WebDAV functionality related to the files.""" + def __init__(self, session: NcSessionBasic): self._session = session diff --git a/nc_py_api/files_sharing.py b/nc_py_api/files_sharing.py index 2f09f911..46b1c8c8 100644 --- a/nc_py_api/files_sharing.py +++ b/nc_py_api/files_sharing.py @@ -1,5 +1,5 @@ """ -Nextcloud API for working with files shares. +Nextcloud API for working with the files shares. """ from typing import Union @@ -7,40 +7,95 @@ from ._session import NcSessionBasic from .constants import SharePermissions, ShareType from .files import FsNode +from .misc import check_capabilities, require_capabilities -ENDPOINT_BASE_SHARES = "/ocs/v1.php/shares" -ENDPOINT_BASE_SHAREES = "/ocs/v1.php/sharees" -ENDPOINT_BASE_DELETED = "/ocs/v1.php/deletedshares" -ENDPOINT_BASE_REMOTE = "/ocs/v1.php/remote_shares" +ENDPOINT_BASE = "/ocs/v1.php/apps/files_sharing/api/v1/" + + +class Share: + """Class represents one Nextcloud Share.""" + + def __init__(self, raw_data: dict): + self.raw_data = raw_data + + @property + def share_id(self) -> int: + return int(self.raw_data["id"]) + + @property + def type(self) -> ShareType: + return ShareType(int(self.raw_data["share_type"])) + + @property + def permissions(self) -> SharePermissions: + """Recipient permissions""" + + return SharePermissions(int(self.raw_data["permissions"])) + + @property + def url(self) -> str: + return self.raw_data.get("url", "") + + @property + def path(self) -> str: + return self.raw_data.get("path", "") + + @property + def label(self) -> str: + return self.raw_data.get("label", "") + + @property + def note(self) -> str: + return self.raw_data.get("note", "") + + @property + def mimetype(self) -> str: + return self.raw_data.get("mimetype", "") class FilesSharingAPI: + """This class provides all File Sharing functionality.""" + def __init__(self, session: NcSessionBasic): self._session = session - def get_list(self, shared_with_me=False, reshares=False, subfiles=False, path: Union[str, FsNode] = ""): + @property + def available(self) -> bool: + """Returns True if the Nextcloud instance supports this feature, False otherwise.""" + + return not check_capabilities("files_sharing", self._session.capabilities) + + def get_list( + self, shared_with_me=False, reshares=False, subfiles=False, path: Union[str, FsNode] = "" + ) -> list[Share]: + """Returns lists of shares.""" + + require_capabilities("files_sharing", self._session.capabilities) + path = path.path if isinstance(path, FsNode) else path params = { "shared_with_me": "true" if shared_with_me else "false", "reshares": "true" if reshares else "false", "subfiles": "true" if subfiles else "false", - "path": path, } - return self._session.ocs(method="GET", path=f"{ENDPOINT_BASE_SHARES}", params=params) + if path: + params["path"] = path + result = self._session.ocs(method="GET", path=f"{ENDPOINT_BASE}/shares", params=params) + return [Share(i) for i in result] def create( self, path: Union[str, FsNode], permissions: SharePermissions, share_type: ShareType, - share_with: str, + share_with: str = "", **kwargs, - ): + ) -> Share: """Creates a new share. :param path: The path of an existing file/directory. :param permissions: combination of the :py:class:`~nc_py_api.SharePermissions` object values. :param share_type: :py:class:`~nc_py_api.ShareType` value. - :param share_with: string representing object name to where send share. + :param share_with: the recipient of the shared object. :param kwargs: *Additionally supported arguments* Additionally supported arguments: @@ -48,13 +103,44 @@ def create( default = ``False`` ``password`` - string with password to protect share. default = ``""`` - ``sendPasswordByTalk`` - boolean indicating should password be automatically delivered using Talk. + ``send_password_by_talk`` - boolean indicating should password be automatically delivered using Talk. default = ``False`` - ``expireDate`` - to-do, choose format. + ``expire_date`` - py:class:`datetime` time when share should expire. `hours, minutes, seconds` are ignored. + default = None ``note`` - string with note, if any. default = ``""`` ``label`` - string with label, if any. default = ``""`` """ - pass # noqa # pylint: disable=unnecessary-pass + require_capabilities("files_sharing", self._session.capabilities) + path = path.path if isinstance(path, FsNode) else path + params = { + "path": path, + "permissions": int(permissions), + "shareType": int(share_type), + } + if share_with: + kwargs["shareWith"] = share_with + if kwargs.get("public", False): + params["publicUpload"] = "true" + if "password" in kwargs: + params["publicUpload"] = kwargs["password"] + if kwargs.get("send_password_by_talk", False): + params["sendPasswordByTalk"] = "true" + if "expire_date" in kwargs: + params["expireDate"] = kwargs["expire_date"].isoformat() + if "note" in kwargs: + params["note"] = kwargs["note"] + if "label" in kwargs: + params["label"] = kwargs["label"] + return Share(self._session.ocs(method="POST", path=f"{ENDPOINT_BASE}/shares", params=params)) + + def delete(self, share_id: Union[int, Share]) -> None: + """Removes the given share. + + :param share_id: The Share object or an ID of the share. + """ + + share_id = share_id.share_id if isinstance(share_id, Share) else share_id + self._session.ocs(method="DELETE", path=f"{ENDPOINT_BASE}/shares/{share_id}") diff --git a/nc_py_api/users_status.py b/nc_py_api/users_status.py index 4679bb6c..281d672a 100644 --- a/nc_py_api/users_status.py +++ b/nc_py_api/users_status.py @@ -38,6 +38,8 @@ def __init__(self, session: NcSessionBasic): @property def available(self) -> bool: + """Returns True if the Nextcloud instance supports this feature, False otherwise.""" + return not check_capabilities("user_status", self._session.capabilities) def get_list(self, limit: Optional[int] = None, offset: Optional[int] = None) -> list[UserStatus]: diff --git a/nc_py_api/weather_status.py b/nc_py_api/weather_status.py index e72a8cef..84640aa3 100644 --- a/nc_py_api/weather_status.py +++ b/nc_py_api/weather_status.py @@ -30,6 +30,8 @@ def __init__(self, session: NcSessionBasic): @property def available(self) -> bool: + """Returns True if the Nextcloud instance supports this feature, False otherwise.""" + return not check_capabilities("weather_status", self._session.capabilities) def get_location(self) -> WeatherLocation: diff --git a/tests/files_sharing_test.py b/tests/files_sharing_test.py new file mode 100644 index 00000000..ac76cf71 --- /dev/null +++ b/tests/files_sharing_test.py @@ -0,0 +1,20 @@ +import pytest +from gfixture import NC_TO_TEST + +from nc_py_api import Share, SharePermissions, ShareType + + +@pytest.mark.parametrize("nc", NC_TO_TEST) +def test_create_list_delete_shares(nc): + nc.files.upload("share_test", content="") + try: + result = nc.files_sharing.get_list() + assert isinstance(result, list) + n_shares = len(result) + new_share = nc.files_sharing.create("share_test", SharePermissions.PERMISSION_READ, ShareType.TYPE_LINK) + assert isinstance(new_share, Share) + assert n_shares + 1 == len(nc.files_sharing.get_list()) + nc.files_sharing.delete(new_share) + assert n_shares == len(nc.files_sharing.get_list()) + finally: + nc.files.delete("share_test")