Merge remote-tracking branch 'gitea/main' into e5-s3-toolbar-relocation

This commit is contained in:
2026-08-17 22:14:50 +02:00
4 changed files with 97 additions and 1 deletions
+1 -1
View File
@@ -1 +1 @@
0.65.1
0.65.2
+10
View File
@@ -14,6 +14,14 @@ from app.models import Notification
# rows are never pruned (the user hasn't seen them yet).
READ_SOFT_CAP = 200
# Hard limit of the Notification.title column, read straight from the model so the clamp
# can never drift from the actual column width (a later migration is the single source of
# truth). Titles are only an English fallback — the client renders the translated message
# from `data` — so clamping an over-long one (e.g. a FB/IG reel whose "title" is the whole
# caption, hundreds of chars) is harmless to the UI and prevents a StringDataRightTruncation
# that would abort the caller's whole transaction (the batched download_gc run in particular).
TITLE_MAX_LEN = Notification.__table__.c.title.type.length
def create_notification(
db: Session,
@@ -27,6 +35,8 @@ def create_notification(
) -> Notification:
"""Insert one notification for a user. Callers that batch many inserts in a single
transaction can pass commit=False and commit once themselves."""
if title is not None and len(title) > TITLE_MAX_LEN:
title = title[: TITLE_MAX_LEN - 1] + "…" # clamp to the column limit, add an ellipsis
notif = Notification(
user_id=user_id, type=type, title=title, body=body, data=data
)
+75
View File
@@ -0,0 +1,75 @@
"""create_notification title-clamp regression.
A downloaded FB/IG reel's "title" is the whole caption (hundreds to ~1900 chars). Before the
clamp that string went straight into Notification.title (String(255)) and, at the batched
download_gc commit, raised psycopg StringDataRightTruncation — aborting the entire GC run so
no expiry deletion / LRU eviction / orphan reap ever ran. The clamp trims the title to the
column limit centrally, defending all callers regardless of DB backend."""
from app.models import Notification, User
from app.notifications import TITLE_MAX_LEN, create_notification
class _CaptureDB:
"""Captures the added row without a real DB, so the clamp is verified on every lane
(the truncation only reproduces on Postgres; the clamp logic is backend-agnostic)."""
def __init__(self):
self.added: list[Notification] = []
def add(self, obj):
self.added.append(obj)
def commit(self):
pass
def refresh(self, _obj):
pass
def test_title_clamped_to_column_limit():
long_title = "x" * 2000 # e.g. a reel caption
notif = create_notification(
_CaptureDB(), user_id=1, type="download_expiring", title=long_title
)
assert len(notif.title) == TITLE_MAX_LEN
assert notif.title.endswith("…")
# The untruncated title still reaches the client via `data`, which is never clamped.
notif2 = create_notification(
_CaptureDB(),
user_id=1,
type="download_expiring",
title=long_title,
data={"kind": "download_expiring", "title": long_title},
)
assert notif2.data["title"] == long_title
def test_short_title_is_untouched():
notif = create_notification(_CaptureDB(), user_id=1, type="t", title="short")
assert notif.title == "short"
def test_title_at_exactly_the_limit_is_untouched():
exact = "y" * TITLE_MAX_LEN
notif = create_notification(_CaptureDB(), user_id=1, type="t", title=exact)
assert notif.title == exact
def test_limit_tracks_the_actual_column_width():
# The clamp is derived from the model, so it can't silently drift from the column a
# migration would change (which is exactly what re-introduces the truncation bug).
assert TITLE_MAX_LEN == Notification.__table__.c.title.type.length
def test_over_long_title_actually_commits_on_postgres(db):
# The stub tests above prove the clamp *logic*; this proves the real fix end-to-end —
# a caption-length title now COMMITS into varchar(TITLE_MAX_LEN) instead of raising
# StringDataRightTruncation. Uses the opt-in Postgres lane (skipped with no DB).
user = User(email="reel@example.com")
db.add(user)
db.flush()
notif = create_notification(
db, user_id=user.id, type="download_expiring", title="z" * 2000
)
db.refresh(notif)
assert len(notif.title) == TITLE_MAX_LEN # persisted, not truncated by the DB
+11
View File
@@ -14,6 +14,17 @@ export interface ReleaseEntry {
}
export const RELEASE_NOTES: ReleaseEntry[] = [
{
version: "0.65.2",
date: "2026-08-16",
summary: "Fixes the automatic cleanup of expired downloads, which had quietly stopped running.",
fixes: [
"The scheduled cleanup of downloaded videos works again. A download with a very long title — for example a Facebook or Instagram reel, whose entire caption is used as the title — could jam the cleanup job, so expired downloads stopped being removed and their disk space wasn't reclaimed. Over-long titles are now trimmed for the notification only (the full title is kept everywhere else), so the cleanup runs to completion on schedule.",
],
chores: [
"Added a regression test that a very long download title can no longer break the cleanup job, and enabled a Postgres query-statistics extension on the server so a transient slow-loading report can be diagnosed if it recurs.",
],
},
{
version: "0.65.1",
date: "2026-08-16",