feat(downloads): public share-by-link + watch page backend
Share a download by a capability URL (/watch/{token}) that anyone can play on a login-free page —
distinct from the registered-user ACL share. Per-link controls: optional expiry, allow-download
toggle (stream-only vs downloadable), optional argon2 password. Revoke = delete.
- migration 0042 + DownloadLink model; app/downloads/links.py (token, HMAC grant sign/verify)
- owner endpoints (POST/GET/PATCH/DELETE links) + /recipients for the internal user picker
- public router (no auth): watch meta / password unlock→signed grant / range-aware file serve
(inline vs attachment); unlock is rate-limited; meta verifies the file exists on disk
Verified end-to-end: 206 range, inline vs attachment, wrong-password 403, tampered-grant 403.
This commit is contained in:
parent
12e610659e
commit
d672583830
6 changed files with 381 additions and 1 deletions
|
|
@ -867,6 +867,33 @@ class DownloadQuota(Base, UpdatedAtMixin):
|
|||
)
|
||||
|
||||
|
||||
class DownloadLink(Base, TimestampMixin):
|
||||
"""A public capability link to one download — the "share by link" surface.
|
||||
|
||||
Anyone holding the unguessable `token` can watch the file on the login-free `/watch/{token}`
|
||||
player page (Google-Drive style). Optional per-link controls: `expires_at`, `allow_download`
|
||||
(stream-only vs downloadable), and a `password_hash` (argon2, like a user password). Revoke =
|
||||
delete the row. Distinct from `DownloadShare`, which grants a *registered* user access via the
|
||||
in-app "Shared with me" list."""
|
||||
|
||||
__tablename__ = "download_links"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
job_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("download_jobs.id", ondelete="CASCADE"), index=True
|
||||
)
|
||||
token: Mapped[str] = mapped_column(String(64), unique=True)
|
||||
created_by: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("users.id", ondelete="CASCADE")
|
||||
)
|
||||
allow_download: Mapped[bool] = mapped_column(
|
||||
Boolean, default=False, server_default="false"
|
||||
)
|
||||
password_hash: Mapped[str | None] = mapped_column(Text)
|
||||
expires_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
view_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
||||
|
||||
|
||||
class MessageKey(Base, TimestampMixin):
|
||||
"""A user's end-to-end-encryption key material for direct messaging (phase 2).
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue