From 682c25b72f6f1ea73f3b5d6f4cd0111d795ec965 Mon Sep 17 00:00:00 2001 From: luffah Date: Wed, 16 Jun 2021 23:08:12 +0200 Subject: [PATCH 1/8] Update setup.py to publish properly on PyPi --- AUTHORS.rst | 1 + README.md => README.rst | 48 +++++++++++++++++++++++++++++------------ setup.cfg | 4 ++-- setup.py | 3 ++- 4 files changed, 39 insertions(+), 17 deletions(-) rename README.md => README.rst (56%) diff --git a/AUTHORS.rst b/AUTHORS.rst index e2deb87..e882d75 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -11,6 +11,7 @@ Main contributors Original code ````````````` The repo was originally nammed NEXT-OCS-API-forPy in 2017 + - どまお `@Dosugamea ` diff --git a/README.md b/README.rst similarity index 56% rename from README.md rename to README.rst index 70771a5..373610a 100644 --- a/README.md +++ b/README.rst @@ -1,6 +1,8 @@ -# NextCloud Python api +NextCloud Python API +==================== -## Overview +Overview +-------- Python wrapper for NextCloud's API. @@ -22,42 +24,60 @@ Tested with : * NextCloud 20, python 2.7 * NextCloud 20, python 3.6 -The main lines: +The main lines : * `NextCloud(URL, auth=…)` provide you a connection manager. You can use it with `with … as nxc:` to open a session. * The session is the connection object that make the requests. * The requests are initiated by a requester associated to an API wrapper. * API wrappers are the definition of how to use the NextCloud REST API : it provide functions that will be attached to the `NextCloud` object. * Functions can return : - - Response object with attributes `is_ok`, `data`. If `is_ok` is False, you can use `get_error_message`. - - Data objects (File, Tag…) or None. + - Response object with attributes `is_ok`, `data`. If `is_ok` is False, you can use `get_error_message`. + - Data objects (File, Tag…) or None. * Data objects are useable as dict object or with attribute. They provide operations. If the operation fails, you'll get an exception. -For quick start, check out [examples](examples) and the [unit tests directory](tests). +For quick start, check out `examples`_ and the `tests`_. +Install +------- -## Fork Change +.. code-block:: sh + + # use 'pip3' for python3 or 'python -m pip' instead of pip + pip install nextcloud-api-wrapper + # the associated python lib is nammed 'nextcloud' + # beware the conflicts -This version is a fork (mainly refactoring, fixes and optimization) of [nextcloud-API](https://github.com/EnterpriseyIntranet/nextcloud-API). -#### Testing +Fork Changes +------------ + +This version is a fork (mainly refactoring, fixes and optimization) of `nextcloud-API `_ . + +Testing +~~~~~~~ The integration to Travis and CodeCov provided by the original repository are lost. There is now 2 branches `develop` and `main`. -All [tests](tests) are validated using `test.sh docker` before merging `develop` to `main`. +All `tests`_ are validated using `test.sh docker` before merging `develop` to `main`. -#### Documentation +Documentation +~~~~~~~~~~~~~ The integration with readthedoc.io is lost. You still can build the documentation with Sphinx source. Looking at the code docstrings is recommended. Significative changes will be reported in `CHANGELOG.md` file. -Too, you can check out the original [nextcloud API documentation](https://nextcloud-api.readthedocs.io/en/latest/introduction.html), but the use could have changed. +Too, you can check out the original `nextcloud API documentation `_, but the use could have changed. -## Contribution +Contributing +------------ -### Pull Request +Pull Request +~~~~~~~~~~~~ According to the testing procedure, you shall fork and PR on branch `develop`. + +.. _tests: https://github.com/luffah/nextcloud-API/tree/main/tests +.. _examples: https://github.com/luffah/nextcloud-API/tree/main/examples diff --git a/setup.cfg b/setup.cfg index fe370c0..fda74aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [metadata] name = nextcloud-api-wrapper -version = 0.2.1 +version = 0.2.1.5 description= Python wrapper for NextCloud api -long_description = file: README.md +long_description = file: README.rst keywords = requests, api, wrapper, nextcloud, owncloud license = GPLv3 diff --git a/setup.py b/setup.py index da2469f..c99fe7b 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ # 'setup.py publish' shortcut. if sys.argv[-1] == 'publish': # see https://twine.readthedocs.io/en/latest/ + os.system('rm -rf dist build') os.system('python %s sdist bdist_wheel' % (sys.argv[0])) os.system('python3 %s sdist bdist_wheel' % (sys.argv[0])) os.system('twine upload dist/*') @@ -28,5 +29,5 @@ # some variables are defined here for retro compat with setuptools >= 33 package_dir = {'': 'src'}, packages=find_packages(where=r'./src'), - long_description_content_type = 'text/markdown' + long_description_content_type = 'text/x-rst' ) From 3fe5e3f478920922d01060681d5ca0e62a7447b1 Mon Sep 17 00:00:00 2001 From: luffah Date: Sun, 3 Apr 2022 16:32:07 +0200 Subject: [PATCH 2/8] Fix API_URL if nextcloud url isn't at domain root --- src/nextcloud/__init__.py | 11 +++++++++++ src/nextcloud/common/paths.py | 4 ++-- src/nextcloud/session.py | 7 ++++--- test.sh | 9 +++++++++ 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/nextcloud/__init__.py b/src/nextcloud/__init__.py index 63c6d12..9d8cb66 100644 --- a/src/nextcloud/__init__.py +++ b/src/nextcloud/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """ Nextcloud/OwnCloud client. See NextCloud object. """ import logging +import re from .session import Session from .api_wrappers import API_WRAPPER_CLASSES @@ -51,7 +52,17 @@ def __init__(self, endpoint=None, url=endpoint, user=user, password=password, auth=auth, session_kwargs=session_kwargs ) + # @FIX_API_URL fix api url for case nextcloud is not on server root {{ + url_parts = re.match(r"^((https?://)?[^/]*)(/.*)?", self.session.url) + (server_part_url, api_url_base) = (url_parts.group(1), url_parts.group(3)) + if api_url_base: + self.session.url = server_part_url + # }} for functionality_class in API_WRAPPER_CLASSES: + # @FIX_API_URL {{ + if api_url_base: + functionality_class.API_URL = api_url_base + functionality_class.API_URL + # }} functionality_instance = functionality_class(self) for potential_method in dir(functionality_instance): if not potential_method.startswith('_'): diff --git a/src/nextcloud/common/paths.py b/src/nextcloud/common/paths.py index 907a178..20acfc2 100644 --- a/src/nextcloud/common/paths.py +++ b/src/nextcloud/common/paths.py @@ -32,9 +32,9 @@ def sequenced_paths_list(folder_paths, exclude=None): list_paths = nodes_from_tree(build_tree(folder_paths)) elif isinstance(folder_paths, dict): list_paths = nodes_from_tree(folder_paths) - return _remove_excluded(list_paths, exclude_list) if exclude else list_paths + return _remove_excluded(list_paths, exclude) if exclude else list_paths -def _remove_excluded(list_path, exclude_list): +def _remove_excluded(list_paths, exclude_list): return [p for p in list_paths if p not in exclude_list] def build_tree(paths): diff --git a/src/nextcloud/session.py b/src/nextcloud/session.py index 6520741..1b1a6b1 100644 --- a/src/nextcloud/session.py +++ b/src/nextcloud/session.py @@ -29,6 +29,7 @@ def __init__(self, url=None, user=None, password=None, auth=None, session_kwargs self.user = None self._set_credentials(user, password, auth) self.url = url.rstrip('/') + self.login_url = self.url session_kwargs = session_kwargs or {} self._login_check = session_kwargs.pop('on_session_login', False) self._session_kwargs = session_kwargs @@ -142,9 +143,9 @@ def _raise(retry, error): else: retry = False _LOGGER.warning('Retry session check (%s) in %s seconds', - self.url, delay) + self.login_url, delay) time.sleep(delay) - _LOGGER.warning('Retry session check (%s)', self.url) + _LOGGER.warning('Retry session check (%s)', self.login_url) return self._check_login(client, retry=retry) self.logout() raise error @@ -152,7 +153,7 @@ def _raise(retry, error): try: if not check_func().is_ok: raise NextCloudLoginError( - 'Failed to login to NextCloud', self.url, resp) + 'Failed to login to NextCloud', self.login_url, resp) except NextCloudConnectionError as nxc_error: _raise(retry, nxc_error) except NextCloudLoginError as nxc_error: diff --git a/test.sh b/test.sh index 0465953..008238f 100755 --- a/test.sh +++ b/test.sh @@ -74,6 +74,15 @@ _rerun(){ sh $0 $* ;} RUN_DIR="$PWD" +case $1 in + docker*) + if ! which docker-compose 2> /dev/null; then + echo "docker-compose is missing" + exit 1 + fi + ;; +esac + case $1 in docker) if [ ! -f .test.ready ]; then From 1a1d16030aa6a0485bf67aa04b734e042df5015b Mon Sep 17 00:00:00 2001 From: fangebee Date: Sun, 3 Apr 2022 17:09:33 +0200 Subject: [PATCH 3/8] Add endpoints /acl /manageACL for groupfolders (incl. tests) --- src/nextcloud/api_wrappers/group_folders.py | 29 +++++ tests/test_group_folders.py | 118 ++++++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/src/nextcloud/api_wrappers/group_folders.py b/src/nextcloud/api_wrappers/group_folders.py index 54eaa72..bfa2dd8 100644 --- a/src/nextcloud/api_wrappers/group_folders.py +++ b/src/nextcloud/api_wrappers/group_folders.py @@ -9,6 +9,7 @@ class GroupFolders(base.OCSv1ApiWrapper): """ GroupFolders API wrapper """ + API_URL = "/apps/groupfolders/folders" JSON_ABLE = False @@ -101,3 +102,31 @@ def rename_group_folder(self, fid, mountpoint): """ url = "/".join([str(fid), "mountpoint"]) return self.requester.post(url, data={"mountpoint": mountpoint}) + + def toggle_acl(self, fid, acl=1): + """ + Enable∕disable advanced ACL on given group folder + + :param fid (int/str): group folder id + :param acl (int): 1 for enable, 0 for disable + """ + url = "/".join([str(fid), "acl"]) + return self.requester.post(url, data={"acl": acl}) + + def manage_acl(self, fid, mapping_id, mapping_type="user", manage_acl=1): + """ + Grant/Remove a group or user the ability to manage a groupfolders' advanced permissions + + :param fid (int/str): group folder id + :param mapping_id (str): user id or group id + :param mapping_type (str): "user" or "group" + :param manage_acl (int): 0 to delete ACL, 1 to add ACL + :returns: requester response + """ + url = "/".join([str(fid), "manageACL"]) + data = { + "mappingType": mapping_type, + "manageAcl": manage_acl, + "mappingId": mapping_id, + } + return self.requester.post(url, data=data) diff --git a/tests/test_group_folders.py b/tests/test_group_folders.py index e3171c5..241055c 100644 --- a/tests/test_group_folders.py +++ b/tests/test_group_folders.py @@ -136,3 +136,121 @@ def test_setting_folder_permissions(self): # clear self.clear(nxc=self.nxc, group_ids=[group_id], group_folder_ids=[group_folder_id]) + + def test_grant_revoke_advanced_acl_to_user(self): + # create group to share with + group_id = 'test_folders_' + self.get_random_string(length=4) + self.nxc.add_group(group_id) + + # create a user to manage advanced permissions + username = self.create_new_user("folder_manager") + + # create new group folder + folder_mount_point = "test_folder_advanced_permissions_" + self.get_random_string(length=4) + res = self.nxc.create_group_folder(folder_mount_point) + assert res.is_ok + group_folder_id = res.data['id'] + + # add new group to folder + self.nxc.grant_access_to_group_folder(group_folder_id, group_id) + # assert permissions is ALL by default + res = self.nxc.get_group_folder(group_folder_id) + assert int(res.data['quota']) == QUOTA_UNLIMITED + + # grant advanced ACL + self.nxc.manage_acl(group_folder_id, username) + # XXX We have to wait for commit https://github.com/nextcloud/groupfolders/commit/1c3874e0b980 + #res = self.nxc.get_group_folder(group_folder_id) + # assert username in res.data["manage"] + res = self.nxc.get_group_folders() + assert str(group_folder_id) in res.data + assert username in res.data[str(group_folder_id)]["manage"] + assert res.data[str(group_folder_id)]["manage"][username]["type"] == "user" + + # revoke advanced ACL + self.nxc.manage_acl(group_folder_id, username, manage_acl=0) + # XXX We have to wait for commit https://github.com/nextcloud/groupfolders/commit/1c3874e0b980 + #res = self.nxc.get_group_folder(group_folder_id) + # assert username not in res.data["manage"] + res = self.nxc.get_group_folders() + assert str(group_folder_id) in res.data + assert username not in res.data[str(group_folder_id)]["manage"] + + # clear + self.clear(nxc=self.nxc, group_ids=[group_id], group_folder_ids=[group_folder_id]) + + def test_grant_revoke_advanced_acl_to_group(self): + # create group to share with + group_id = 'test_folders_' + self.get_random_string(length=4) + self.nxc.add_group(group_id) + + # create a second group to manage advanced permissions + admin_group_id = 'admin_group_' + self.get_random_string(length=4) + self.nxc.add_group(admin_group_id) + + # create new group folder + folder_mount_point = "test_folder_advanced_permissions_" + self.get_random_string(length=4) + res = self.nxc.create_group_folder(folder_mount_point) + assert res.is_ok + group_folder_id = res.data['id'] + + # add new group to folder + self.nxc.grant_access_to_group_folder(group_folder_id, group_id) + # assert permissions is ALL by default + res = self.nxc.get_group_folder(group_folder_id) + assert int(res.data['quota']) == QUOTA_UNLIMITED + + # grant advanced ACL + self.nxc.manage_acl(group_folder_id, admin_group_id, mapping_type="group") + # XXX We have to wait for commit https://github.com/nextcloud/groupfolders/commit/1c3874e0b980 + #res = self.nxc.get_group_folder(group_folder_id) + # assert admin_group_id in res.data["manage"] + res = self.nxc.get_group_folders() + assert str(group_folder_id) in res.data + assert admin_group_id in res.data[str(group_folder_id)]["manage"] + assert res.data[str(group_folder_id)]["manage"][admin_group_id]["type"] == "group" + + # revoke advanced ACL + self.nxc.manage_acl(group_folder_id, admin_group_id, mapping_type="group", manage_acl=0) + # XXX We have to wait for commit https://github.com/nextcloud/groupfolders/commit/1c3874e0b980 + #res = self.nxc.get_group_folder(group_folder_id) + # assert admin_group_id not in res.data["manage"] + res = self.nxc.get_group_folders() + assert str(group_folder_id) in res.data + assert admin_group_id not in res.data[str(group_folder_id)]["manage"] + + # clear + self.clear(nxc=self.nxc, group_ids=[group_id], group_folder_ids=[group_folder_id]) + + def test_toggle_advanced_acl(self): + # create group to share with + group_id = 'test_folders_' + self.get_random_string(length=4) + self.nxc.add_group(group_id) + + # create new group folder + folder_mount_point = "test_folder_advanced_permissions_" + self.get_random_string(length=4) + res = self.nxc.create_group_folder(folder_mount_point) + assert res.is_ok + group_folder_id = res.data['id'] + + # add new group to folder + self.nxc.grant_access_to_group_folder(group_folder_id, group_id) + # assert permissions is ALL by default + res = self.nxc.get_group_folder(group_folder_id) + assert int(res.data['quota']) == QUOTA_UNLIMITED + + self.nxc.toggle_acl(group_folder_id, acl=1) + # XXX We have to wait for commit https://github.com/nextcloud/groupfolders/commit/1c3874e0b980 + #res = self.nxc.get_group_folder(group_folder_id) + # assert admin_group_id not in res.data["manage"] + res = self.nxc.get_group_folders() + assert str(group_folder_id) in res.data + assert "1" in res.data[str(group_folder_id)]["acl"] + + self.nxc.toggle_acl(group_folder_id, acl=0) + # XXX We have to wait for commit https://github.com/nextcloud/groupfolders/commit/1c3874e0b980 + #res = self.nxc.get_group_folder(group_folder_id) + # assert admin_group_id not in res.data["manage"] + res = self.nxc.get_group_folders() + assert str(group_folder_id) in res.data + assert "1" not in res.data[str(group_folder_id)]["acl"] From 72e00408268f52e727f8263af713db8c6882bbcd Mon Sep 17 00:00:00 2001 From: luffah Date: Sun, 3 Apr 2022 18:32:28 +0200 Subject: [PATCH 4/8] v0.2.2 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index fda74aa..3ffb5b4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = nextcloud-api-wrapper -version = 0.2.1.5 +version = 0.2.2 description= Python wrapper for NextCloud api long_description = file: README.rst keywords = requests, api, wrapper, nextcloud, owncloud From 108b8f019c22377574767fe7b199bf543a4ca343 Mon Sep 17 00:00:00 2001 From: luffah Date: Mon, 4 Apr 2022 15:51:28 +0200 Subject: [PATCH 5/8] v0.2.3 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 3ffb5b4..13f772b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = nextcloud-api-wrapper -version = 0.2.2 +version = 0.2.3 description= Python wrapper for NextCloud api long_description = file: README.rst keywords = requests, api, wrapper, nextcloud, owncloud From 0566b6e36f0cdbb5832231b5592840b3a235ae00 Mon Sep 17 00:00:00 2001 From: MrCapsLock Date: Thu, 21 Apr 2022 13:31:28 +0430 Subject: [PATCH 6/8] Add hideDownload option into update_share --- src/nextcloud/api_wrappers/share.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/nextcloud/api_wrappers/share.py b/src/nextcloud/api_wrappers/share.py index a295ccc..3657a3d 100644 --- a/src/nextcloud/api_wrappers/share.py +++ b/src/nextcloud/api_wrappers/share.py @@ -129,13 +129,14 @@ def delete_share(self, sid): return self.requester.delete(self.get_local_url(sid)) def update_share(self, sid, - permissions=None, password=None, public_upload=None, expire_date=""): + permissions=None, hide_download=None, password=None, public_upload=None, expire_date=""): """ Update a given share, only one value can be updated per request Args: sid (str): share id permissions (int): sum of selected Permission attributes + hide_download (bool): bool, allow to show download button (true/false) password (str): password to protect public link Share with public_upload (bool): bool, allow public upload to a public shared folder (true/false) expire_date (str): set an expire date for public link shares. Format: ‘YYYY-MM-DD’ @@ -145,6 +146,7 @@ def update_share(self, sid, """ params = dict( permissions=permissions, + hideDownload=hide_download, password=password, expireDate=expire_date ) @@ -153,6 +155,11 @@ def update_share(self, sid, if public_upload is False: params["publicUpload"] = "false" + if hide_download: + params["hideDownload"] = "true" + else: + params["hideDownload"] = "false" + # check if only one param specified specified_params_count = sum([int(bool(each)) for each in params.values()]) if specified_params_count > 1: From 730bb01661e89dc263d1d80370f444bd94550371 Mon Sep 17 00:00:00 2001 From: MrCapsLock Date: Thu, 21 Apr 2022 13:32:06 +0430 Subject: [PATCH 7/8] Remove only one param specified in update_share --- src/nextcloud/api_wrappers/share.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/nextcloud/api_wrappers/share.py b/src/nextcloud/api_wrappers/share.py index 3657a3d..e3fbd95 100644 --- a/src/nextcloud/api_wrappers/share.py +++ b/src/nextcloud/api_wrappers/share.py @@ -160,10 +160,5 @@ def update_share(self, sid, else: params["hideDownload"] = "false" - # check if only one param specified - specified_params_count = sum([int(bool(each)) for each in params.values()]) - if specified_params_count > 1: - raise ValueError("Only one parameter for update can be specified per request") - url = self.get_local_url(sid) return self.requester.put(url, data=params) From 0047ae08f4f023c4e30c108f82aa65ad80f800b9 Mon Sep 17 00:00:00 2001 From: MrCapsLock Date: Thu, 21 Apr 2022 13:32:41 +0430 Subject: [PATCH 8/8] Optimize public_upload checking statement in update_share --- src/nextcloud/api_wrappers/share.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nextcloud/api_wrappers/share.py b/src/nextcloud/api_wrappers/share.py index e3fbd95..70e27ee 100644 --- a/src/nextcloud/api_wrappers/share.py +++ b/src/nextcloud/api_wrappers/share.py @@ -152,7 +152,7 @@ def update_share(self, sid, ) if public_upload: params["publicUpload"] = "true" - if public_upload is False: + else: params["publicUpload"] = "false" if hide_download: