M6 1900: any-file attachments + audio memos (broaden beyond images)
A note can now carry any file, not just images — PDFs, documents, audio memos, etc. "Dump anything" capture. Backend: - note_attachments.filename (migration 0019) records the original name for download + display. - Upload drops the image-only mime gate: accepts any type, derives the storage extension from the filename, and enforces a DB-backed per-file cap — new setting max_attachment_mb (default 25, rule 25). App body ceiling raised 12→64 MB (also lifts the import-zip / sync-push limits); the per-file cap is the effective attachment limit. - Serve sets Content-Disposition: images inline, everything else downloads with its original (header-sanitized) filename. - Import (native + Keep Takeout) now brings in ANY attachment, not just images — completing the Keep audio-memo gap; preserves filename + sha256. - Attachment metadata (delta feed + REST) carries filename. Frontend: - Editor renders attachments by kind: images inline (thumbnail), audio via an inline <audio> player, any other file as a download chip (paperclip + filename + size). File picker accepts any type; "Attach a file". - Card previews the first image; non-image files show as compact chips. Tests (DB-free): _safe_filename, _attachment_ext, _header_filename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -3,10 +3,13 @@ import pytest
|
||||
from thoughtsync.app import create_app
|
||||
from thoughtsync.models.note import NOTE_COLORS, Note
|
||||
from thoughtsync.notes import (
|
||||
_attachment_ext,
|
||||
_escape_like,
|
||||
_header_filename,
|
||||
_keep_spec,
|
||||
_native_spec,
|
||||
_parse_iso_dt,
|
||||
_safe_filename,
|
||||
_slugify,
|
||||
_usec_to_dt,
|
||||
derive_display_title,
|
||||
@@ -234,6 +237,28 @@ async def test_import_requires_auth(app):
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_safe_filename():
|
||||
assert _safe_filename("report.pdf") == "report.pdf"
|
||||
assert _safe_filename("/etc/passwd") == "passwd" # path components stripped
|
||||
assert _safe_filename("a\\b\\c.doc") == "c.doc" # windows separators too
|
||||
assert _safe_filename("") == "file" # fallback
|
||||
assert _safe_filename(None) == "file"
|
||||
|
||||
|
||||
def test_attachment_ext():
|
||||
assert _attachment_ext("report.pdf", "application/pdf") == ".pdf"
|
||||
assert _attachment_ext("memo.m4a", "audio/mp4") == ".m4a"
|
||||
# no extension in the name → fall back to a known image mime, else empty
|
||||
assert _attachment_ext("noext", "image/png") == ".png"
|
||||
assert _attachment_ext("noext", "application/octet-stream") == ""
|
||||
|
||||
|
||||
def test_header_filename():
|
||||
# Quotes/newlines are stripped so the Content-Disposition header can't be broken.
|
||||
assert _header_filename('a"b\r\n.pdf') == "ab.pdf"
|
||||
assert _header_filename("") == "file"
|
||||
|
||||
|
||||
def test_usec_to_dt():
|
||||
# Google Keep timestamps are microseconds since the epoch (UTC).
|
||||
d = _usec_to_dt(1600000000000000)
|
||||
|
||||
Reference in New Issue
Block a user