proper quote text/plain output

This commit is contained in:
Peter Bieringer
2026-03-05 07:40:14 +01:00
parent bc3c2dfd78
commit 6848e852ca
3 changed files with 94 additions and 82 deletions

View File

@@ -145,7 +145,7 @@ Shows what kind of ShareTypes are supported
```
curl -u user:pass -H "accept: text/plain" -d "" http://localhost:5232/.sharing/v1/all/info
ApiVersion=1
Status=success
Status='success'
FeatureEnabledCollectionByMap=True
PermittedCreateCollectionByMap=True
FeatureEnabledCollectionByToken=True
@@ -183,29 +183,28 @@ Authenticated user as `Owner` has at least read access to `PathMapped`
* Input
| Parameter | Owner |
| - | - |
| Owner | implicit(by authentication) |
| PathMapped | mandatory |
| User | optional(default:owner) |
| Permissions | optional(default:r) |
| Enabled | optional(owner) |
| Hidden | optional(owner) |
| Properties | optional |
| Parameter | Type | Requirement |
| - | - | - |
| PathMapped | str | mandatory |
| User | str | optional(default:owner) |
| Permissions | str | optional(default:r) |
| Enabled | bool | optional(owner/default:False) |
| Hidden | bool | optional(owner/default:True) |
| Properties | str | optional |
* Output
| Parameter | Value |
| Parameter | Type | Value |
| - | - |
| PathOrToken | (autogenerated token) |
| PathOrToken | str | (autogenerated token) |
* Example: TEXT
```
curl -u user:pass -d "PathMapped=/user/testcalendar1/" http://localhost:5232/.sharing/v1/token/create
ApiVersion=1
Status=success
PathOrToken=v1/VQR7AmsVRi2ZlFj_JwGpFx-ES5Goyku-gP_YkLh1zUw=
Status='success'
PathOrToken='v1/VQR7AmsVRi2ZlFj_JwGpFx-ES5Goyku-gP_YkLh1zUw='
```
###### API Hook "map/create"
@@ -220,15 +219,14 @@ Provided `User` has at least read access to `PathOrToken`
* Input
| Parameter | Value |
| Parameter | Type | Requirement |
| - | - |
| Owner | implicit(by authentication) |
| PathOrToken | mandatory |
| PathMapped | mandatory |
| User | mandatory |
| Permissions | optional(default:r) |
| Enabled | optional(owner) |
| Hidden | optional(owner) |
| PathOrToken | str | mandatory |
| PathMapped | str | mandatory |
| User | str | mandatory |
| Permissions | str | optional(default:r) |
| Enabled | bool | optional(owner/default:False) |
| Hidden | bool | optional(owner/default:True) |
| Properties | optional |
* Output: result status
@@ -252,12 +250,10 @@ Authenticated user as `Owner` or `User`
* Input
| Parameter | Filter |
| - | - |
| Owner | implicit(by authentication) |
| User | implicit(by authentication) |
| PathOrToken | optional |
| PathMapped | optional |
| Parameter | Type | Used for |
| - | - | - |
| PathOrToken | str | optional |
| PathMapped | str | optional |
* Output: plain/csv/json
@@ -265,8 +261,8 @@ Authenticated user as `Owner` or `User`
```
curl -H "accept: text/csv" -u owner:pass -d "" http://localhost:5232/.sharing/v1/map/list
ShareType,PathOrToken,PathMapped,Owner,User,Permissions,EnabledByOwner,EnabledByUser,HiddenByOwner,HiddenByUser,TimestampCreated,TimestampUpdated
map,/user/cal1-from-owner/,/owner/cal1/,owner,user,r,False,False,True,True,1771962120,1771962120
ShareType,PathOrToken,PathMapped,Owner,User,Permissions,EnabledByOwner,EnabledByUser,HiddenByOwner,HiddenByUser,TimestampCreated,TimestampUpdated,Properties
map,/user/cal1-from-owner/,/owner/cal1/,owner,user,r,False,False,True,True,1771962120,1771962120,{}
```
@@ -280,9 +276,9 @@ Authenticated user is `Owner`
* Input
| Parameter | Type | Owner | User |
| - | - | - | - |
| PathOrToken | selector | mandatory | not-permitted |
| Parameter | Type | Used for | as Owner | as User |
| - | - | - | - | - |
| PathOrToken | str | selection | mandatory | not-permitted |
* Output: result status
@@ -298,16 +294,15 @@ Authenticated user is `Owner` or `User`
* Input
| Parameter | Type | Owner | User |
| - | - | - | - |
| PathOrToken | selector | mandatory | mandatory |
| Owner | by authentication | not-permitted | not-permitted |
| PathMapped | adjustable | optional | not-permitted |
| User | adjustable | optional | not-permitted |
| Permissions | adjustable | optional | not-permitted |
| Enabled | adjustable | optional(owner) | optional(user) |
| Hidden | adjustable | optional(owner) | optional(user) |
| Properties | adjustable | optional | optional |
| Parameter | Type | Used for | Owner | User |
| - | - | - | - | - |
| PathOrToken | str | selection | mandatory | mandatory |
| PathMapped | str | adjust | optional | not-permitted |
| User | str | adjust | optional | not-permitted |
| Permissions | str | adjust | optional | not-permitted |
| Enabled | bool | adjust | optional(owner) | optional(user) |
| Hidden | bool | adjust | optional(owner) | optional(user) |
| Properties | str | adjust | optional | optional |
* Output: result status
@@ -321,9 +316,9 @@ Authenticated user is `Owner` or `User`
* Input
| Parameter | Type | Owner | User |
| - | - | - | - |
| PathOrToken | selector | mandatory | mandatory |
| Parameter | Type | Used for | Owner | User |
| - | - | - | - | - |
| PathOrToken | selection | mandatory | mandatory |
* Output: result status
@@ -340,7 +335,7 @@ Status=success
```
curl -u owner:pass -d '{"PathOrToken": "/user/cal1-from-owner/", "PathMapped": "/owner/cal1/", "User": "user"} http://localhost:5232/.sharing/v1/map/unhide
ApiVersion=1
Status=success
Status='success'
```
## Properties Overlay

View File

@@ -72,6 +72,20 @@ API_HOOKS_V1: Sequence[str] = ('list', 'create', 'delete', 'update', 'hide', 'un
API_SHARE_TOGGLES_V1: Sequence[str] = ('hide', 'unhide', 'enable', 'disable')
API_TYPES_V1: dict[str, type] = {
"ApiVersion": int,
"Status": str,
"Lines": int,
"ShareType": str,
"PathOrToken": str,
"PathMapped:": str,
"Owner": str,
"User": str,
"Permissions": str,
"Enabled": bool,
"Hidden": bool,
"Properties": str}
TOKEN_PATTERN_V1: str = "(v1/[a-zA-Z0-9_=\\-]{44})"
PATH_PATTERN: str = "([a-zA-Z0-9/.\\-]+)" # TODO: extend or find better source
@@ -1056,7 +1070,10 @@ class BaseSharing:
if output_format == "txt":
for key in answer:
if key != 'Content':
answer_array.append(key + '=' + str(answer[key]))
if API_TYPES_V1[key] is bool or API_TYPES_V1[key] is int:
answer_array.append(key + '=' + str(answer[key]))
else:
answer_array.append(key + "='" + str(answer[key]) + "'")
if 'Content' in answer and answer['Content'] is not None:
csv = io.StringIO()
writer = DictWriter(csv, fieldnames=DB_FIELDS_V1, delimiter=';')

View File

@@ -319,13 +319,13 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** list (form->csv)")
form_array = []
_, headers, answer = self._sharing_api_form(sharing_type, "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=not-found" in answer
assert "Status='not-found'" in answer
assert "Lines=0" in answer
logging.info("\n*** list (json->text)")
json_dict = {}
_, headers, answer = self._sharing_api_json(sharing_type, "list", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/plain")
assert "Status=not-found" in answer
assert "Status='not-found'" in answer
assert "Lines=0" in answer
logging.info("\n*** list (json->json)")
@@ -338,10 +338,10 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** create a token -> 200")
form_array = ["PathMapped=/owner/collectionL1/"]
_, headers, answer = self._sharing_api_form("token", "create", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "PathOrToken=" in answer
assert "Status='success'" in answer
assert "PathOrToken='" in answer
# extract token
match = re.search('PathOrToken=(.+)', answer)
match = re.search("PathOrToken='(.+)'", answer)
if match:
token = match.group(1)
logging.info("received token %r", token)
@@ -360,20 +360,20 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** list/all (form->csv)")
form_array = []
_, headers, answer = self._sharing_api_form("all", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=2" in answer
logging.info("\n*** delete token -> 200")
form_array = ["PathOrToken=" + token]
_, headers, answer = self._sharing_api_form("token", "delete", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
logging.info("\n*** delete share -> 200")
form_array = []
form_array.append("PathOrToken=/user/collectionL2-shared-by-owner/")
form_array.append("PathMapped=/owner/collectionL2/")
_, headers, answer = self._sharing_api_form("map", "delete", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
def test_sharing_api_token_basic(self) -> None:
"""share-by-token API tests."""
@@ -409,10 +409,10 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** create token#1 (form->text)")
form_array = ["PathMapped=/owner/collection1/"]
_, headers, answer = self._sharing_api_form("token", "create", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "PathOrToken=" in answer
assert "Status='success'" in answer
assert "PathOrToken='" in answer
# extract token
match = re.search('PathOrToken=(.+)', answer)
match = re.search("PathOrToken='(.+)'", answer)
if match:
token1 = match.group(1)
logging.info("received token %r", token1)
@@ -422,10 +422,10 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** create token#2 (json->text)")
json_dict = {'PathMapped': "/owner/collection2/"}
_, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/plain")
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Token=" in answer
# extract token
match = re.search('Token=(.+)', answer)
match = re.search("Token='(.+)'", answer)
if match:
token2 = match.group(1)
logging.info("received token %r", token2)
@@ -435,14 +435,14 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** lookup token#1 (form->text)")
form_array = ["PathOrToken=" + token1]
_, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=1" in answer
assert "/owner/collection1/" in answer
logging.info("\n*** lookup token#2 (json->text")
json_dict = {'PathOrToken': token2}
_, headers, answer = self._sharing_api_json("token", "list", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/plain")
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=1" in answer
assert "/owner/collection2/" in answer
@@ -457,7 +457,7 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** lookup tokens (form->text)")
form_array = []
_, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=2" in answer
assert "/owner/collection1/" in answer
assert "/owner/collection2/" in answer
@@ -465,7 +465,7 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** lookup tokens (form->csv)")
form_array = []
_, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array, accept="text/csv")
assert "Status=success" not in answer
assert "Status='success'" not in answer
assert "Lines=2" not in answer
assert ";".join(sharing.DB_FIELDS_V1) in answer
assert "/owner/collection1/" in answer
@@ -474,24 +474,24 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** delete token#1 (form->text)")
form_array = ["PathOrToken=" + token1]
_, headers, answer = self._sharing_api_form("token", "delete", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
logging.info("\n*** lookup token#1 (form->text) -> should not be there anymore")
form_array = ["PathOrToken=" + token1]
_, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=not-found" in answer
assert "Status='not-found'" in answer
assert "Lines=0" in answer
logging.info("\n*** lookup tokens (form->text) -> still one should be there")
form_array = []
_, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=1" in answer
logging.info("\n*** disable token#2 as owner (form->text)")
form_array = ["PathOrToken=" + token2]
_, headers, answer = self._sharing_api_form("token", "disable", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
logging.info("\n*** lookup token#2 (json->json) -> check for not enabled")
json_dict = {'PathOrToken': token2}
@@ -512,7 +512,7 @@ class TestSharingApiSanity(BaseTest):
form_array = []
form_array.append("PathOrToken=" + token2)
_, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=1" in answer
assert "True;True;True;True" in answer
@@ -520,13 +520,13 @@ class TestSharingApiSanity(BaseTest):
form_array = []
form_array.append("PathOrToken=" + token2)
_, headers, answer = self._sharing_api_form("token", "hide", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
logging.info("\n*** lookup token#2 (form->text) -> check for hidden")
form_array = []
form_array.append("PathOrToken=" + token2)
_, headers, answer = self._sharing_api_form("token", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=1" in answer
assert "True;True;True;True" in answer
@@ -606,10 +606,10 @@ class TestSharingApiSanity(BaseTest):
form_array = []
form_array.append("PathMapped=" + path_base)
_, headers, answer = self._sharing_api_form("token", "create", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "PathOrToken=" in answer
# extract token
match = re.search('PathOrToken=(.+)', answer)
match = re.search("PathOrToken='(.+)'", answer)
if match:
token = match.group(1)
logging.info("received token %r", token)
@@ -620,10 +620,10 @@ class TestSharingApiSanity(BaseTest):
form_array = []
form_array.append("PathMapped=" + path_base2)
_, headers, answer = self._sharing_api_form("token", "create", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "PathOrToken=" in answer
# extract token
match = re.search('PathOrToken=(.+)', answer)
match = re.search("PathOrToken='(.+)'", answer)
if match:
token2 = match.group(1)
logging.info("received token %r", token2)
@@ -633,7 +633,7 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** enable token (form->text)")
form_array = ["PathOrToken=" + token]
_, headers, answer = self._sharing_api_form("token", "enable", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
logging.info("\n*** fetch collection using invalid token (without credentials)")
_, headers, answer = self.request("GET", path_token + "v1/invalidtoken", check=401)
@@ -645,7 +645,7 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** disable token (form->text)")
form_array = ["PathOrToken=" + token]
_, headers, answer = self._sharing_api_form("token", "disable", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
logging.info("\n*** fetch collection using disabled token (without credentials)")
_, headers, answer = self.request("GET", path_token + token, check=401)
@@ -653,7 +653,7 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** enable token (form->text)")
form_array = ["PathOrToken=" + token]
_, headers, answer = self._sharing_api_form("token", "enable", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
logging.info("\n*** fetch collection using token (without credentials)")
_, headers, answer = self.request("GET", path_token + token, check=200)
@@ -2207,7 +2207,7 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** list/all (form->csv)")
form_array = []
_, headers, answer = self._sharing_api_form("map", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=1" in answer
# read collection
@@ -2227,7 +2227,7 @@ class TestSharingApiSanity(BaseTest):
logging.info("\n*** list/all (form->csv)")
form_array = []
_, headers, answer = self._sharing_api_form("map", "list", check=200, login="owner:ownerpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
assert "Lines=1" in answer
# read collection
@@ -2987,7 +2987,7 @@ permissions: RrWw""")
form_array.append("Properties='C:calendar-description'='ICAL-USER-NEW'")
form_array.append("Properties='ICAL:calendar-color'='#CCCCCC'")
_, headers, answer = self._sharing_api_form("map", "update", check=200, login="user:userpw", form_array=form_array)
assert "Status=success" in answer
assert "Status='success'" in answer
# verify overlay as user
logging.info("\n*** PROPFIND collection user (overlay) -> ok")