Skip to content

Commit 33d8973

Browse files
authored
added Files Locking support (#227)
Signed-off-by: Alexander Piskun <[email protected]>
1 parent 10417cf commit 33d8973

File tree

9 files changed

+242
-20
lines changed

9 files changed

+242
-20
lines changed

.github/workflows/analysis-coverage.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,14 @@ jobs:
505505
ref: "main"
506506
path: apps/notes
507507

508+
- name: Checkout Files Locking
509+
uses: actions/checkout@v4
510+
if: ${{ !startsWith(matrix.nextcloud, 'master') }}
511+
with:
512+
repository: nextcloud/files_lock
513+
ref: ${{ matrix.nextcloud }}
514+
path: apps/files_lock
515+
508516
- name: Set up & run Nextcloud
509517
env:
510518
DB_PORT: 4444
@@ -518,6 +526,10 @@ jobs:
518526
./occ app:enable notifications
519527
PHP_CLI_SERVER_WORKERS=2 php -S localhost:8080 &
520528
529+
- name: Enable Files Locking
530+
if: ${{ !startsWith(matrix.nextcloud, 'master') }}
531+
run: ./occ app:enable files_lock
532+
521533
- name: Enable Notes
522534
if: ${{ !startsWith(matrix.nextcloud, 'master') }}
523535
run: ./occ app:enable notes
@@ -831,6 +843,14 @@ jobs:
831843
ref: "main"
832844
path: apps/notes
833845

846+
- name: Checkout Files Locking
847+
uses: actions/checkout@v4
848+
if: ${{ !startsWith(matrix.nextcloud, 'stable26') && !startsWith(matrix.nextcloud, 'master') }}
849+
with:
850+
repository: nextcloud/files_lock
851+
ref: ${{ matrix.nextcloud }}
852+
path: apps/files_lock
853+
834854
- name: Set up & run Nextcloud
835855
env:
836856
DB_PORT: 4444
@@ -848,6 +868,10 @@ jobs:
848868
if: ${{ !startsWith(matrix.nextcloud, 'master') }}
849869
run: ./occ app:enable notes
850870

871+
- name: Enable Files Locking
872+
if: ${{ !startsWith(matrix.nextcloud, 'stable26') && !startsWith(matrix.nextcloud, 'master') }}
873+
run: ./occ app:enable files_lock
874+
851875
- name: Checkout NcPyApi
852876
uses: actions/checkout@v4
853877
with:

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

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

5+
## [0.11.0 - 2024-0x-xx]
6+
7+
### Added
8+
9+
- Files: `lock` and `unlock` methods, lock file information to `FsNode`. #227
10+
511
## [0.10.0 - 2024-02-14]
612

713
### Added

docs/reference/Files/Files.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,9 @@ All File APIs are designed to work relative to the current user.
2020

2121
.. autoclass:: nc_py_api.files.SystemTag
2222
:members:
23+
24+
.. autoclass:: nc_py_api.files.LockType
25+
:members:
26+
27+
.. autoclass:: nc_py_api.files.FsNodeLockInfo
28+
:members:

nc_py_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
NextcloudMissingCapabilities,
88
)
99
from ._version import __version__
10-
from .files import FilePermissions, FsNode
10+
from .files import FilePermissions, FsNode, LockType
1111
from .files.sharing import ShareType
1212
from .nextcloud import AsyncNextcloud, AsyncNextcloudApp, Nextcloud, NextcloudApp

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.10.0"
3+
__version__ = "0.11.0.dev0"

nc_py_api/files/__init__.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,63 @@
99
from .. import _misc
1010

1111

12+
class LockType(enum.IntEnum):
13+
"""Nextcloud File Locks types."""
14+
15+
MANUAL_LOCK = 0
16+
COLLABORATIVE_LOCK = 1
17+
WEBDAV_TOKEN = 2
18+
19+
20+
@dataclasses.dataclass
21+
class FsNodeLockInfo:
22+
"""File Lock information if Nextcloud `files_lock` is enabled."""
23+
24+
def __init__(self, **kwargs):
25+
self._is_locked = bool(int(kwargs.get("is_locked", False)))
26+
self._lock_owner_type = LockType(int(kwargs.get("lock_owner_type", 0)))
27+
self._lock_owner = kwargs.get("lock_owner", "")
28+
self._owner_display_name = kwargs.get("owner_display_name", "")
29+
self._owner_editor = kwargs.get("lock_owner_editor", "")
30+
self._lock_time = int(kwargs.get("lock_time", 0))
31+
self._lock_ttl = int(kwargs.get("_lock_ttl", 0))
32+
33+
@property
34+
def is_locked(self) -> bool:
35+
"""Returns ``True`` if the file is locked, ``False`` otherwise."""
36+
return self._is_locked
37+
38+
@property
39+
def type(self) -> LockType:
40+
"""Type of the lock."""
41+
return LockType(self._lock_owner_type)
42+
43+
@property
44+
def owner(self) -> str:
45+
"""User id of the lock owner."""
46+
return self._lock_owner
47+
48+
@property
49+
def owner_display_name(self) -> str:
50+
"""Display name of the lock owner."""
51+
return self._owner_display_name
52+
53+
@property
54+
def owner_editor(self) -> str:
55+
"""App id of an app owned lock to allow clients to suggest joining the collaborative editing session."""
56+
return self._owner_editor
57+
58+
@property
59+
def lock_creation_time(self) -> datetime.datetime:
60+
"""Lock creation time."""
61+
return datetime.datetime.utcfromtimestamp(self._lock_time).replace(tzinfo=datetime.timezone.utc)
62+
63+
@property
64+
def lock_ttl(self) -> int:
65+
"""TTL of the lock in seconds staring from the creation time. A value of 0 means the timeout is infinite."""
66+
return self._lock_ttl
67+
68+
1269
@dataclasses.dataclass
1370
class FsNodeInfo:
1471
"""Extra FS object attributes from Nextcloud."""
@@ -116,11 +173,15 @@ class FsNode:
116173
info: FsNodeInfo
117174
"""Additional extra information for the object"""
118175

176+
lock_info: FsNodeLockInfo
177+
"""Class describing `lock` information if any."""
178+
119179
def __init__(self, full_path: str, **kwargs):
120180
self.full_path = full_path
121181
self.file_id = kwargs.get("file_id", "")
122182
self.etag = kwargs.get("etag", "")
123183
self.info = FsNodeInfo(**kwargs)
184+
self.lock_info = FsNodeLockInfo(**kwargs)
124185

125186
@property
126187
def is_dir(self) -> bool:

nc_py_api/files/_files.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from httpx import Response
1111

1212
from .._exceptions import NextcloudException, check_error
13-
from .._misc import clear_from_params_empty
13+
from .._misc import check_capabilities, clear_from_params_empty
1414
from . import FsNode, SystemTag
1515

1616
PROPFIND_PROPERTIES = [
@@ -29,13 +29,16 @@
2929
"oc:share-types",
3030
"oc:favorite",
3131
"nc:is-encrypted",
32+
]
33+
34+
PROPFIND_LOCKING_PROPERTIES = [
3235
"nc:lock",
3336
"nc:lock-owner-displayname",
3437
"nc:lock-owner",
3538
"nc:lock-owner-type",
36-
"nc:lock-owner-editor",
37-
"nc:lock-time",
38-
"nc:lock-timeout",
39+
"nc:lock-owner-editor", # App id of an app owned lock
40+
"nc:lock-time", # Timestamp of the log creation time
41+
"nc:lock-timeout", # TTL of the lock in seconds staring from the creation time
3942
]
4043

4144
SEARCH_PROPERTIES_MAP = {
@@ -57,15 +60,22 @@ class PropFindType(enum.IntEnum):
5760
VERSIONS_FILE_ID = 3
5861

5962

60-
def build_find_request(req: list, path: str | FsNode, user: str) -> ElementTree.Element:
63+
def get_propfind_properties(capabilities: dict) -> list:
64+
r = PROPFIND_PROPERTIES
65+
if not check_capabilities("files.locking", capabilities):
66+
r += PROPFIND_LOCKING_PROPERTIES
67+
return r
68+
69+
70+
def build_find_request(req: list, path: str | FsNode, user: str, capabilities: dict) -> ElementTree.Element:
6171
path = path.user_path if isinstance(path, FsNode) else path
6272
root = ElementTree.Element(
6373
"d:searchrequest",
6474
attrib={"xmlns:d": "DAV:", "xmlns:oc": "http://owncloud.org/ns", "xmlns:nc": "http://nextcloud.org/ns"},
6575
)
6676
xml_search = ElementTree.SubElement(root, "d:basicsearch")
6777
xml_select_prop = ElementTree.SubElement(ElementTree.SubElement(xml_search, "d:select"), "d:prop")
68-
for i in PROPFIND_PROPERTIES:
78+
for i in get_propfind_properties(capabilities):
6979
ElementTree.SubElement(xml_select_prop, i)
7080
xml_from_scope = ElementTree.SubElement(ElementTree.SubElement(xml_search, "d:from"), "d:scope")
7181
href = f"/files/{user}/{path.removeprefix('/')}"
@@ -76,15 +86,17 @@ def build_find_request(req: list, path: str | FsNode, user: str) -> ElementTree.
7686
return root
7787

7888

79-
def build_list_by_criteria_req(properties: list[str] | None, tags: list[int | SystemTag] | None) -> ElementTree.Element:
89+
def build_list_by_criteria_req(
90+
properties: list[str] | None, tags: list[int | SystemTag] | None, capabilities: dict
91+
) -> ElementTree.Element:
8092
if not properties and not tags:
8193
raise ValueError("Either specify 'properties' or 'tags' to filter results.")
8294
root = ElementTree.Element(
8395
"oc:filter-files",
8496
attrib={"xmlns:d": "DAV:", "xmlns:oc": "http://owncloud.org/ns", "xmlns:nc": "http://nextcloud.org/ns"},
8597
)
8698
prop = ElementTree.SubElement(root, "d:prop")
87-
for i in PROPFIND_PROPERTIES:
99+
for i in get_propfind_properties(capabilities):
88100
ElementTree.SubElement(prop, i)
89101
xml_filter_rules = ElementTree.SubElement(root, "oc:filter-rules")
90102
if properties and "favorite" in properties:
@@ -243,7 +255,7 @@ def etag_fileid_from_response(response: Response) -> dict:
243255
return {"etag": response.headers.get("OC-Etag", ""), "file_id": response.headers["OC-FileId"]}
244256

245257

246-
def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
258+
def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode: # noqa pylint: disable = too-many-branches
247259
fs_node_args = {}
248260
for prop_stat in prop_stats:
249261
if str(prop_stat.get("d:status", "")).find("200 OK") == -1:
@@ -274,7 +286,17 @@ def _parse_record(full_path: str, prop_stats: list[dict]) -> FsNode:
274286
fs_node_args["trashbin_original_location"] = prop["nc:trashbin-original-location"]
275287
if "nc:trashbin-deletion-time" in prop_keys:
276288
fs_node_args["trashbin_deletion_time"] = prop["nc:trashbin-deletion-time"]
277-
# xz = prop.get("oc:dDC", "")
289+
for k, v in {
290+
"nc:lock": "is_locked",
291+
"nc:lock-owner-type": "lock_owner_type",
292+
"nc:lock-owner": "lock_owner",
293+
"nc:lock-owner-displayname": "lock_owner_displayname",
294+
"nc:lock-owner-editor": "lock_owner_editor",
295+
"nc:lock-time": "lock_time",
296+
"nc:lock-timeout": "lock_ttl",
297+
}.items():
298+
if k in prop_keys and prop[k] is not None:
299+
fs_node_args[v] = prop[k]
278300
return FsNode(full_path, **fs_node_args)
279301

280302

0 commit comments

Comments
 (0)