halkyon/docs/adr/0043-shared-file-store-todo-attachments.md

126 lines
8.1 KiB
Markdown
Raw Normal View History

# ADR 0043: One file store in halkyon_base; Todo's attachments
- **Status:** decided 10 Nov 2026 by the developer: D1 and D2 are their own requests,
D3 their choice over the agent's recommendation (20 MB), D4 and D5 the agent's
feat(todo,app): files attached to tasks and comments Halkyon's ADR 0043, D1, D3, D5 and D8: any file up to 100 MB, attached to a task's description or to a comment, sealed as everything of Todo's. Server: the attachment table, synced and sealed as the others, with the sealed file's hash readable for the store; todoFiles hands halkyon_base its rows, readable by their space's members, and takes sealed files alone; the files no attachment names go daily. Its tests hold downloads to the row's space, refuse a readable file and prune what no live row names. Migration attachments: CREATE TABLE "attachment" with its indexes and the foreign key to its space, cascading as every synced table's. App: AttachmentRepository seals each file for its row, keeps it sealed and opened in the person's folder, uploads it after each sync and fetches it as it shows, both held back by Without files on mobile data but for a file tapped. Under the description and a comment's text, a row of them: pictures as thumbnails opening full-size, other files as chips opening in the system's app, else saved; the paperclip beside the description and in the comment's composer adds them, the composer holding them until it is sent. A task deleted takes its files and its comments' along, a comment its own. halkyon_app opens a file in the system's app (FileOpener): on Android through its plugin and a FileProvider of its own class, sharing the cache's halkyon_opened/ folder alone; on the desktop as a file: URI. Whats-New: Attach files to a task's description or a comment with the paperclip: pictures show as thumbnails, other files open in their app. Whats-New-De: Hänge mit der Büroklammer Dateien an die Beschreibung einer Aufgabe oder an einen Kommentar: Bilder erscheinen als Vorschau, andere Dateien öffnen sich in ihrer App. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 01:17:22 +01:00
recommendations, which they chose. D2, D4, D6 and D7 built 10 Nov 2026, Todo's
attachments (D1, D3, D5, D8) 11 Nov 2026 (below).
- **Builds on:** ADR 0004 (Learn's assets, rule 5), ADR 0014 (the module), ADR 0017
(the account's keys), ADR 0019 (sealed files), ADR 0033 (Todo), ADR 0042 (mobile data).
## Context
Learn keeps people's files (images, sounds, PDFs) in the server's private storage by
space and SHA-256 (`AssetEndpoint`, `file_limits.dart`, `file_pruning.dart`), counts
each person's room against `FILE_LIMIT_IDENTIFIED_MB` and the operator's allowances,
and seals a private studyset's files on the device (`HLKF`, ADR 0019). Todo has
descriptions and comments as rich text (ADR 0033, D36, D38) and no files. The developer
asked for attachments in both, now (10 Nov 2026).
## Decisions
- **D1.** Todo gets attachments in a task's description and in comments: an image shows
inline, any other file as a chip with its name and size that opens in the system's
app (or saves, on a desktop).
- **D2.** The file store moves into **halkyon_base**: the endpoint (upload by hash into
the caller's own space, finish, usage, download by a row a member may read), the
limits, the room counted and the pruning. Learn's server uses the module's and drops
its own; the tables keep their names and data (a move with the version rows alone,
as ADR 0014's moves). The module asks each app, through a hook as `deletePersonData`,
which synced rows name a file, for download rights and the pruning: Learn's `asset`
rows, Todo's attachment rows.
- **D3.** Todo takes **any file up to 100 MB**, as Learn's PDFs. Every Todo file is
sealed (everything of Todo's is, ADR 0033, D5), so the server can't look into it and
checks only its size and the sealed header; Learn's rule for readable files (over 20
MB only a PDF) stays Learn's.
- **D4.** **Room per service**, Learn's rule: each server counts its own people's files
against its own `FILE_LIMIT_IDENTIFIED_MB` (500 MB by default), `FILE_LIMIT_INSTANCE_MB`
and allowances. The operator's grants screen (ADR 0021) grants a service's room on
that service.
- **D5.** A device fetches a file **when it is shown**, then keeps it in the person's
folder; offline, it has what was seen. On mobile data, *Without files* (ADR 0042)
holds back uploads and downloads alike; a file not fetched shows as its chip with
*Download*.
refactor(base,learn): people's files kept by halkyon_base ADR 0043, D2, D4, D6 and D7, the first step towards Todo's attachments: Learn's file store moves into the module, for every server that keeps files, and Learn becomes its first host. halkyon_base gets the store as Learn had it: uploads into the caller's own space by SHA-256, downloads by a row the caller may read, each person's room (FILE_LIMIT_IDENTIFIED_MB, FILE_LIMIT_INSTANCE_MB, the operator's file_allowance, which moves into the module unchanged) and the pruning of files no live row names. It stays free of the sync engine: a host hands it a FileStore saying which of its rows name a file and what it takes (HalkyonBase.files); without one the files endpoint refuses. Its tests, with refusals, run on a stand-in host. Learn hands it learnFiles: its asset rows, read by their space's members, a readable file over 20 MB only as a PDF, up to 100 MB. Its app calls the module; its own asset endpoint stays for released apps alone, forwarding to the module in Learn's own models, whose names are on the wire. Its file tests still pass through it, end to end. Migrations: the module's creates file_allowance. Learn's server migration holds version rows alone, as tables are matched by name (ADR 0014); its device migration creates the module's empty table, as every module's. Todo's, Train's and Meditate's create the empty table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 00:11:36 +01:00
## Decided while building (10 Nov 2026, the developer's choices of the agent's recommendations)
- **D6.** **The module stays free of the sync engine.** Each app's server hands it hooks,
as `deletePersonData`: which file a row the caller may read names (for downloads, by
the row's ID, as Learn's `download(assetId)`), which hashes a space's live rows name
(for the room counted and the pruning), and what it accepts (Learn: a readable file
over 20 MB only as a PDF; Todo: sealed files alone). A server without them offers no
files (Train, Meditate).
- **D7.** **Older Learn apps keep working:** Learn's `asset` endpoint stays as a thin
forwarder to the module's, with its own `FileUsage` and `FileLimitExceededException`
(their names on the wire are Learn's), until a later release raises
`minimumAppBuild` and drops it.
- **D8.** **Todo shows attachments in a row below the text** of a description or a
comment, as Todoist does: images as thumbnails opening full-size, other files as chips;
added with a paperclip button. halkyon_editor doesn't change.
## Todo's rows
An `attachment` table, synced and sealed as every Todo table: the file's hash, sealed
size and kind (image or file), its name and the task or comment it belongs to, all in
the sealed payload but the hash, which the server needs to count and serve. The rich
text names the attachment by its row ID, as Learn's editor names an asset. Deleting a
task or a comment blanks its attachment rows (the payload first, ADR 0002, rule 2); the
module's pruning removes a file no row names any more.
## To work out when building
- The move of Learn's tables into the module: their migration SQL (version rows only,
or a rename where the module's naming differs), explained before it is applied.
- Whether the sealed header (`HLKF`) and Learn's `SealedFile` move to `halkyon_keys`
for Todo to share, which the recommendation is.
- The quota's text in Todo's Settings: Learn shows the room used in its Sync card.
refactor(base,learn): people's files kept by halkyon_base ADR 0043, D2, D4, D6 and D7, the first step towards Todo's attachments: Learn's file store moves into the module, for every server that keeps files, and Learn becomes its first host. halkyon_base gets the store as Learn had it: uploads into the caller's own space by SHA-256, downloads by a row the caller may read, each person's room (FILE_LIMIT_IDENTIFIED_MB, FILE_LIMIT_INSTANCE_MB, the operator's file_allowance, which moves into the module unchanged) and the pruning of files no live row names. It stays free of the sync engine: a host hands it a FileStore saying which of its rows name a file and what it takes (HalkyonBase.files); without one the files endpoint refuses. Its tests, with refusals, run on a stand-in host. Learn hands it learnFiles: its asset rows, read by their space's members, a readable file over 20 MB only as a PDF, up to 100 MB. Its app calls the module; its own asset endpoint stays for released apps alone, forwarding to the module in Learn's own models, whose names are on the wire. Its file tests still pass through it, end to end. Migrations: the module's creates file_allowance. Learn's server migration holds version rows alone, as tables are matched by name (ADR 0014); its device migration creates the module's empty table, as every module's. Todo's, Train's and Meditate's create the empty table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 00:11:36 +01:00
## As built: the store in halkyon_base (10 Nov 2026)
- `packages/halkyon_base_server/lib/src/files/`: `FileStore` (the host's hooks
`fileOfRow`, `liveHashes`, `accepts`; `maxBytes`, 100 MB unless said), the store's
logic as functions (`startFileUpload`, `finishFileUpload`, `fileUsageOf`,
`downloadFile`) under the `files` endpoint, the room (`file_limits.dart`) and the
pruning (`pruneUnreferencedFiles`, scheduled by each host in a future call of its own),
moved from Learn with their tests; `FileAllowance` moved as it was (its table keeps its
name), deleted with the person by the module now. Models of the module's own:
`FileUsage`, `FileLimitExceededException`, `StoredFileNotFoundException`.
- Migrations: the module's `file-store` creates `file_allowance`; Learn's holds version
rows alone (Serverpod matches tables by name, as ADR 0014 found), its device migration
creating the module's empty table, as every module's; Todo's, Train's and Meditate's
create the empty table, which only Todo will use.
- Learn: `learnFiles` (its `asset` rows, their spaces' members, a readable file over
20 MB only as a PDF); `AssetEndpoint` a forwarder in Learn's models for released apps;
the app calls the module (`client.modules.base.files`).
feat(todo,app): files attached to tasks and comments Halkyon's ADR 0043, D1, D3, D5 and D8: any file up to 100 MB, attached to a task's description or to a comment, sealed as everything of Todo's. Server: the attachment table, synced and sealed as the others, with the sealed file's hash readable for the store; todoFiles hands halkyon_base its rows, readable by their space's members, and takes sealed files alone; the files no attachment names go daily. Its tests hold downloads to the row's space, refuse a readable file and prune what no live row names. Migration attachments: CREATE TABLE "attachment" with its indexes and the foreign key to its space, cascading as every synced table's. App: AttachmentRepository seals each file for its row, keeps it sealed and opened in the person's folder, uploads it after each sync and fetches it as it shows, both held back by Without files on mobile data but for a file tapped. Under the description and a comment's text, a row of them: pictures as thumbnails opening full-size, other files as chips opening in the system's app, else saved; the paperclip beside the description and in the comment's composer adds them, the composer holding them until it is sent. A task deleted takes its files and its comments' along, a comment its own. halkyon_app opens a file in the system's app (FileOpener): on Android through its plugin and a FileProvider of its own class, sharing the cache's halkyon_opened/ folder alone; on the desktop as a file: URI. Whats-New: Attach files to a task's description or a comment with the paperclip: pictures show as thumbnails, other files open in their app. Whats-New-De: Hänge mit der Büroklammer Dateien an die Beschreibung einer Aufgabe oder an einen Kommentar: Bilder erscheinen als Vorschau, andere Dateien öffnen sich in ihrer App. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-11-11 01:17:22 +01:00
## As built: Todo's attachments (11 Nov 2026)
- Server: the `attachment` table (`lib/src/attachment/attachment.spy.yaml`), synced and
sealed as Todo's other tables, with the sealed file's hash readable (`sha256`, indexed
by space and hash); `todoFiles` (`todo_files.dart`): its rows, their spaces' members,
sealed files alone; `todoBase(files: todoFiles)`; the pruning daily
(`FilePruningFutureCall`, `schedulePruning`). Migration `attachments`: `CREATE TABLE
"attachment"` with its two indexes and the foreign key to the space, cascading on its
deletion, as every synced table; nothing else changes.
- Shared in halkyon_app, moved from Learn, which uses them since: `FileCache`
(`DirectoryFileCache`), `FileServerGateway` (`ServerpodFileServerGateway` on the
module's `files`, `FileLimitReached`, `FileRoom`), their fakes (`MemoryFileCache`,
`FakeFileServerGateway`), and the sealed file's format (`SealedFile`, the recommendation
above: halkyon_app rather than halkyon_keys, as it seals with halkyon_app's
`ContentCipher`; Learn's `Privacy` seals with it, its context unchanged).
- Opening a file in the system's app (D1): halkyon_app's `FileOpener`,
`SystemFileOpener` opening a copy under its name in the cache's `halkyon_opened/`, on
Android through halkyon_app's plugin (a FileProvider of its own class,
`HalkyonFileProvider`, sharing that folder alone, so that it never clashes with an app's
own, Train's), on the desktop as a `file:` URI; where none opens it (iOS), a copy is
saved where the person chooses.
- Todo's app (`lib/src/feature/attachment/`): `AttachmentRepository` sealing each file for
its row (`attachment|<id>|file`), keeping the sealed and the opened file in the person's
folder (`files/`), uploading after each sync and downloading as shown, both held back by
*Without files* on mobile data but for a file the person taps; `AttachmentRow` under the
description (`TaskAttachments`, shown once there is one, the paperclip beside the
description, `TaskAttachButton`, so a sheet without files keeps its height) and under a
comment's text, the comment's composer holding the files picked until it is sent, a
comment of files alone allowed. A task deleted takes its comments' and its own attachments along, a comment
its own.
- Not built: the room used, in Todo's Settings; a file's upload or download progress.