Skip to content

Commit f0ec9d0

Browse files
authored
feat: convert datetime values to RFC 3339 in WebDAV search literals (#358)
This PR fixes incorrect handling of `datetime` and `date` objects in WebDAV `find()` queries. Previously, passing a `datetime` resulted in an invalid string format like `2025-03-10 12:34:56.123456`, causing Nextcloud to misinterpret the value in `<d:literal>`. **Changes made:** - Added `_dav_literal()` helper to format datetime values as RFC 3339 strings (`2025-03-10T12:34:56Z`) - Updated `build_search_req()` to use this helper when generating `<d:literal>` values This ensures proper comparison logic for operators like `"gt"` and `"lt"` when filtering by `last_modified` or other time-based properties. Fixes issues where files were incorrectly included or excluded due to string-based comparison. --------- Signed-off-by: bigcat88 <[email protected]>
1 parent 62d104c commit f0ec9d0

File tree

2 files changed

+23
-2
lines changed

2 files changed

+23
-2
lines changed

nc_py_api/files/_files.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
"""Helper functions for **FilesAPI** and **AsyncFilesAPI** classes."""
22

33
import enum
4+
from datetime import datetime, timezone
45
from io import BytesIO
56
from json import dumps, loads
7+
from typing import Any
68
from urllib.parse import unquote
79
from xml.etree import ElementTree
810

@@ -69,6 +71,16 @@ def get_propfind_properties(capabilities: dict) -> list:
6971
return r
7072

7173

74+
def _dav_literal(val: Any) -> str:
75+
"""Return a string suitable for <d:literal>."""
76+
if isinstance(val, datetime):
77+
# make timezone-aware, force UTC, second precision
78+
dt = val if val.tzinfo else val.replace(tzinfo=timezone.utc)
79+
dt = dt.astimezone(timezone.utc).replace(microsecond=0)
80+
return dt.isoformat().replace("+00:00", "Z") # 2025-03-10T12:34:56Z
81+
return str(val)
82+
83+
7284
def build_find_request(req: list, path: str | FsNode, user: str, capabilities: dict) -> ElementTree.Element:
7385
path = path.user_path if isinstance(path, FsNode) else path
7486
root = ElementTree.Element(
@@ -126,7 +138,7 @@ def _add_value(xml_element, val=None) -> None:
126138
ElementTree.SubElement(_, SEARCH_PROPERTIES_MAP[req.pop(0)])
127139
_ = ElementTree.SubElement(_root, "d:literal")
128140
value = req.pop(0)
129-
_.text = value if isinstance(value, str) else str(value)
141+
_.text = _dav_literal(value)
130142

131143
while len(req):
132144
where_part = req.pop(0)

tests/actual_tests/files_test.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import math
33
import os
44
import zipfile
5-
from datetime import datetime
5+
from datetime import datetime, timedelta
66
from io import BytesIO
77
from pathlib import Path
88
from random import choice, randbytes
@@ -753,6 +753,15 @@ def test_find_files_listdir_depth(nc_any):
753753
assert len(result) == 4
754754

755755

756+
def test_find_files_datetime(nc_any):
757+
time_in_past = datetime.now() - timedelta(days=1)
758+
time_in_future = datetime.now() + timedelta(days=1)
759+
assert len(nc_any.files.find(["gt", "last_modified", time_in_past], "/test_dir/subdir/"))
760+
assert not len(nc_any.files.find(["gt", "last_modified", time_in_future], "/test_dir/subdir/"))
761+
assert not len(nc_any.files.find(["lt", "last_modified", time_in_past], "/test_dir/subdir/"))
762+
assert len(nc_any.files.find(["lt", "last_modified", time_in_future], "/test_dir/subdir/"))
763+
764+
756765
def test_listdir_depth(nc_any):
757766
result = nc_any.files.listdir("test_dir/", depth=1)
758767
result2 = nc_any.files.listdir("test_dir")

0 commit comments

Comments
 (0)