feat(downloads): one-click Share + downloadable-by-default links (E7 S3)

D2-A: the library row's Share button becomes a single action — quickShareLink()
ensures a public link (minting on demand), copies it to the clipboard, and toasts
with an "Open in new tab" action. A copy failure still toasts (warning) with that
action so the link is never a dead end. Advanced controls (password/expiry/revoke/
user-ACL) stay in the existing ShareDialog, reached via a new "Manage sharing" row
button.

D1 (forward-only): CreateLinkIn.allow_download defaults to true, so a newly minted
link is downloadable by default. The default lives in the API, not a migration —
the download_links column default stays false, so no existing link flips. The
ShareDialog create-default is flipped to match.

Tests: TestCreateLinkDefault (default true; explicit false honored); 4 vitest cases
for quickShareLink (mint-vs-reuse, copy ok/fail, create failure). HU/EN parity.
This commit is contained in:
2026-08-29 06:36:07 +02:00
parent 94fcc40bde
commit fdca12968b
8 changed files with 202 additions and 8 deletions
+6 -1
View File
@@ -823,7 +823,12 @@ def list_links(
class CreateLinkIn(BaseModel):
password: str = ""
allow_download: bool = False
# D1 (E7 S3): a newly shared download is downloadable by default — the common intent is "let the
# recipient have the file", not stream-only. Forward-only: this only sets the default for links
# minted from now on; existing links keep whatever `allow_download` they were created with (the
# column default stays False, so no back-catalogue link silently flips). The owner can still
# uncheck it in the Share dialog, and toggle any link stream-only afterwards.
allow_download: bool = True
expires_days: int | None = None
+18
View File
@@ -93,6 +93,24 @@ class TestPublicMetaAudioFlag:
assert meta["audio"] is False
class TestCreateLinkDefault:
"""D1 (E7 S3): a newly minted share link is downloadable by default. The one-click Share relies on
this — it POSTs no `allow_download`, so the API default decides. Forward-only lives here (the API
default), NOT in a migration: the DownloadLink column default stays False, so no existing back-
catalogue link silently flips to downloadable."""
def test_omitted_allow_download_defaults_downloadable(self):
from app.routes.downloads import CreateLinkIn
assert CreateLinkIn().allow_download is True
def test_explicit_false_is_respected(self):
# The owner can still uncheck it to mint a stream-only link.
from app.routes.downloads import CreateLinkIn
assert CreateLinkIn(allow_download=False).allow_download is False
class TestIsExpired:
def test_none_expiry_never_expires(self):
assert is_expired(SimpleNamespace(expires_at=None)) is False