Skip to content

feat: expose S3 presigned download URL on FsNode#419

Open
bigcat88 wants to merge 2 commits intomainfrom
feat/s3-presigned-url
Open

feat: expose S3 presigned download URL on FsNode#419
bigcat88 wants to merge 2 commits intomainfrom
feat/s3-presigned-url

Conversation

@bigcat88
Copy link
Contributor

@bigcat88 bigcat88 commented Mar 24, 2026

Add download_url and download_url_expiration properties to FsNodeInfo. The oc:downloadURL property was already requested via PROPFIND but never parsed - now it is. Also request nc:download-url-expiration property.

When the Nextcloud storage backend is S3 with use_presigned_url enabled, these provide a direct download URL bypassing the Nextcloud proxy. Returns empty string / zero when not available (non-S3 backends).

Closes #418

Summary by CodeRabbit

  • New Features
    • File and folder info now expose temporary download URLs and expiration timestamps, allowing time-limited direct downloads.
  • Tests
    • Added tests validating default values, preservation of provided download info, parsing behavior, and resilience to malformed expiration values.

Add download_url and download_url_expiration properties to FsNodeInfo.
The oc:downloadURL property was already requested via PROPFIND but never
parsed — now it is. Also request nc:download-url-expiration property.

When the Nextcloud storage backend is S3 with use_presigned_url enabled,
these provide a direct download URL bypassing the Nextcloud proxy.
Returns empty string / zero when not available (non-S3 backends).

Closes #418
@coderabbitai
Copy link

coderabbitai bot commented Mar 24, 2026

📝 Walkthrough

Walkthrough

Adds S3 presigned download URL capture and exposure: PROPFIND parsing now reads oc:downloadURL and nc:download-url-expiration, these values are stored on FsNodeInfo and exposed via read-only properties; unit tests added for parsing and propagation.

Changes

Cohort / File(s) Summary
FsNodeInfo data model
nc_py_api/files/__init__.py
Added _download_url and _download_url_expiration fields and read-only properties download_url: str and download_url_expiration: int (defaults: "", 0).
WebDAV parsing / PROPFIND
nc_py_api/files/_files.py
Added nc:download-url-expiration to PROPFIND properties; _parse_record now extracts oc:downloadURLdownload_url and nc:download-url-expirationdownload_url_expiration (int conversion with ValueError/TypeError suppression). Minor string-formatting change in NextcloudException message only.
Tests
tests_unit/test_download_url.py
New unit tests verifying default values, explicit preservation, propagation to FsNode, parsing of present/absent/false/malformed oc:downloadURL and nc:download-url-expiration cases.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: exposing S3 presigned download URLs on FsNode through new properties.
Linked Issues check ✅ Passed The PR fulfills issue #418's objective by exposing download_url and download_url_expiration properties for S3 presigned URL retrieval, enabling direct S3 downloads without proxying.
Out of Scope Changes check ✅ Passed All changes are in-scope: FsNodeInfo property additions, WebDAV property parsing, and comprehensive unit tests directly address the S3 presigned URL feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/s3-presigned-url

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nc_py_api/files/__init__.py`:
- Around line 110-111: The constructor assigns self._download_url_expiration
from kwargs without coercion, allowing a string to slip past the intended int
contract; update the code that sets self._download_url_expiration (in the class
__init__ where self._download_url and self._download_url_expiration are
initialized) to coerce the value to an int (e.g.,
int(kwargs.get("download_url_expiration", 0))) and defensively handle
non-numeric inputs (fallback to 0 or raise a clear error) so the attribute is
always an int at runtime.

In `@nc_py_api/files/_files.py`:
- Around line 312-313: The code attempts to parse
prop["nc:download-url-expiration"] into an int unguarded which can raise
ValueError on malformed/non-numeric values; update the block that sets
fs_node_args["download_url_expiration"] to validate or wrap the int(...)
conversion in a try/except ValueError (and TypeError) and on failure assign 0 so
parsing degrades safely; use the existing prop_keys/prop checks and ensure you
reference the same symbols (prop_keys, prop, fs_node_args,
"nc:download-url-expiration") when implementing the guard.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c699e9bc-52be-4512-9abc-efbf722d7ec6

📥 Commits

Reviewing files that changed from the base of the PR and between ef519ae and 6e9c581.

📒 Files selected for processing (2)
  • nc_py_api/files/__init__.py
  • nc_py_api/files/_files.py

Comment on lines +110 to +111
self._download_url: str = kwargs.get("download_url", "")
self._download_url_expiration: int = kwargs.get("download_url_expiration", 0)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Coerce download_url_expiration to int at construction

Line 111 currently passes through raw kwargs value, so download_url_expiration can become a str at runtime despite the int API contract.

Proposed fix
-        self._download_url_expiration: int = kwargs.get("download_url_expiration", 0)
+        try:
+            self._download_url_expiration = int(kwargs.get("download_url_expiration", 0))
+        except (TypeError, ValueError):
+            self._download_url_expiration = 0

As per coding guidelines, "Type annotations correctness (Pydantic models are used extensively)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nc_py_api/files/__init__.py` around lines 110 - 111, The constructor assigns
self._download_url_expiration from kwargs without coercion, allowing a string to
slip past the intended int contract; update the code that sets
self._download_url_expiration (in the class __init__ where self._download_url
and self._download_url_expiration are initialized) to coerce the value to an int
(e.g., int(kwargs.get("download_url_expiration", 0))) and defensively handle
non-numeric inputs (fallback to 0 or raise a clear error) so the attribute is
always an int at runtime.

@codecov
Copy link

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 66.66667% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.63%. Comparing base (ef519ae) to head (6e9c581).

Files with missing lines Patch % Lines
nc_py_api/files/__init__.py 75.00% 2 Missing ⚠️
nc_py_api/files/_files.py 50.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #419      +/-   ##
==========================================
- Coverage   94.69%   94.63%   -0.07%     
==========================================
  Files          47       47              
  Lines        5575     5587      +12     
==========================================
+ Hits         5279     5287       +8     
- Misses        296      300       +4     
Files with missing lines Coverage Δ
nc_py_api/files/__init__.py 99.15% <75.00%> (-0.56%) ⬇️
nc_py_api/files/_files.py 95.59% <50.00%> (-0.82%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Add 7 unit tests covering FsNodeInfo defaults, value pass-through,
_parse_record with/without presigned URL data, false values from
non-S3 backends, and malformed expiration values.

Guard int() conversion of download-url-expiration with
contextlib.suppress to handle malformed server responses gracefully.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@nc_py_api/files/_files.py`:
- Around line 311-312: The code currently treats any non-empty oc:downloadURL
value as truthy, causing the literal string "false" from xmltodict to be stored
in fs_node_args["download_url"]; update the check around prop_keys/prop for
"oc:downloadURL" (in _files.py) to only assign fs_node_args["download_url"] when
prop["oc:downloadURL"] is neither empty nor the literal string "false"
(case-insensitive), otherwise leave/set the default empty string to conform to
the FSNode API.

In `@tests_unit/test_download_url.py`:
- Around line 59-70: The fixture in test_parse_record_with_false_download_url
uses boolean False for "oc:downloadURL" and "nc:download-url-expiration" but
real payloads contain the string "false"; update the values in the test fixture
inside the test_parse_record_with_false_download_url function to use the string
"false" for both "oc:downloadURL" and "nc:download-url-expiration" so the test
mirrors actual XML text parsing and exercises the intended regression path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9de4e952-0337-4db5-a0df-33a79cc19d28

📥 Commits

Reviewing files that changed from the base of the PR and between 6e9c581 and e0cf706.

📒 Files selected for processing (2)
  • nc_py_api/files/_files.py
  • tests_unit/test_download_url.py

Comment on lines +311 to +312
if "oc:downloadURL" in prop_keys and prop["oc:downloadURL"]:
fs_node_args["download_url"] = prop["oc:downloadURL"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import xmltodict
xml = '<d:prop xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns"><oc:downloadURL>false</oc:downloadURL></d:prop>'
value = xmltodict.parse(xml)["d:prop"]["oc:downloadURL"]
print(type(value).__name__, repr(value))
PY

Repository: cloud-py-api/nc_py_api

Length of output: 77


🏁 Script executed:

# Check the actual code at lines 311-312 and surrounding context
sed -n '300,320p' nc_py_api/files/_files.py | cat -n

Repository: cloud-py-api/nc_py_api

Length of output: 1496


🏁 Script executed:

# Find the Pydantic model definition for FSNode to understand the download_url field
rg -A 10 "class FSNode" nc_py_api/ | head -50

Repository: cloud-py-api/nc_py_api

Length of output: 48


🏁 Script executed:

# Search for download_url field definition and type annotation
rg "download_url" nc_py_api/files/ -B 2 -A 2

Repository: cloud-py-api/nc_py_api

Length of output: 2249


🏁 Script executed:

# Check for existing tests related to download_url
rg -l "download_url" nc_py_api/tests/ --type py

Repository: cloud-py-api/nc_py_api

Length of output: 122


🏁 Script executed:

# Look for similar patterns in the file that might have the same issue
rg "if \".*\" in prop_keys and prop\[" nc_py_api/files/_files.py | head -20

Repository: cloud-py-api/nc_py_api

Length of output: 227


Fix truthiness check for literal "false" string in download_url.

When WebDAV returns <oc:downloadURL>false</oc:downloadURL>, xmltodict parses it to the literal string "false". The condition at line 311 treats this non-empty string as truthy, storing "false" instead of the documented default empty string, violating the FSNode API contract.

Proposed fix
-        if "oc:downloadURL" in prop_keys and prop["oc:downloadURL"]:
-            fs_node_args["download_url"] = prop["oc:downloadURL"]
+        if "oc:downloadURL" in prop_keys:
+            download_url = prop["oc:downloadURL"]
+            if isinstance(download_url, str):
+                download_url = download_url.strip()
+            if download_url not in (None, "", False) and str(download_url).lower() != "false":
+                fs_node_args["download_url"] = str(download_url)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if "oc:downloadURL" in prop_keys and prop["oc:downloadURL"]:
fs_node_args["download_url"] = prop["oc:downloadURL"]
if "oc:downloadURL" in prop_keys:
download_url = prop["oc:downloadURL"]
if isinstance(download_url, str):
download_url = download_url.strip()
if download_url not in (None, "", False) and str(download_url).lower() != "false":
fs_node_args["download_url"] = str(download_url)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nc_py_api/files/_files.py` around lines 311 - 312, The code currently treats
any non-empty oc:downloadURL value as truthy, causing the literal string "false"
from xmltodict to be stored in fs_node_args["download_url"]; update the check
around prop_keys/prop for "oc:downloadURL" (in _files.py) to only assign
fs_node_args["download_url"] when prop["oc:downloadURL"] is neither empty nor
the literal string "false" (case-insensitive), otherwise leave/set the default
empty string to conform to the FSNode API.

Comment on lines +59 to +70
def test_parse_record_with_false_download_url():
"""When storage doesn't support presigned URLs, server returns 'false'."""
prop_stat = {
"d:status": "HTTP/1.1 200 OK",
"d:prop": {
"oc:id": "00000123",
"oc:fileid": "123",
"oc:permissions": "RGDNVW",
"d:getetag": '"abc123"',
"oc:downloadURL": False,
"nc:download-url-expiration": False,
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use string "false" in this fixture to match real payload shape.

At Line 68-69, using False (bool) does not mirror typical XML text parsing. This test should use "false" so it can catch the exact regression path.

Proposed fix
-            "oc:downloadURL": False,
-            "nc:download-url-expiration": False,
+            "oc:downloadURL": "false",
+            "nc:download-url-expiration": "false",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_parse_record_with_false_download_url():
"""When storage doesn't support presigned URLs, server returns 'false'."""
prop_stat = {
"d:status": "HTTP/1.1 200 OK",
"d:prop": {
"oc:id": "00000123",
"oc:fileid": "123",
"oc:permissions": "RGDNVW",
"d:getetag": '"abc123"',
"oc:downloadURL": False,
"nc:download-url-expiration": False,
},
def test_parse_record_with_false_download_url():
"""When storage doesn't support presigned URLs, server returns 'false'."""
prop_stat = {
"d:status": "HTTP/1.1 200 OK",
"d:prop": {
"oc:id": "00000123",
"oc:fileid": "123",
"oc:permissions": "RGDNVW",
"d:getetag": '"abc123"',
"oc:downloadURL": "false",
"nc:download-url-expiration": "false",
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests_unit/test_download_url.py` around lines 59 - 70, The fixture in
test_parse_record_with_false_download_url uses boolean False for
"oc:downloadURL" and "nc:download-url-expiration" but real payloads contain the
string "false"; update the values in the test fixture inside the
test_parse_record_with_false_download_url function to use the string "false" for
both "oc:downloadURL" and "nc:download-url-expiration" so the test mirrors
actual XML text parsing and exercises the intended regression path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

S3 presigned_url

1 participant