Files
narl-workspace/migrations/0024_file_text.sql
T
nvrl 909abf5fcb search: a file is found by what it says
The text of every file in the tree that is text, by its type or by its
first 8 KiB reading as UTF-8 without a NUL, now goes into `file_text` and a
second FTS5 index beside the documents', up to its first 256 KiB, the same
part the viewer shows. A search asks both, lists a node found in both once,
and marks a file found by its text with `in_text`; the list shows the words
around the match under its name.

There is one way in, `file_text::catch_up`, which reads every unsealed file
that has no row yet. An upload, an import, an SFTP save and a decrypt call
it once their own transaction is done, and the server runs it at start for
anything that slipped past. A file that is not text gets an empty row, so it
is read once and not again.

The triggers keep it honest the other way. New bytes drop the old text, so
an SFTP save is read again, and `:encrypt` writes a file under a new media
id, so its plaintext is gone in the same statement that seals it. The text
of a sealed file is refused outright, whatever path tries to store it;
otherwise it would sit in `diary.db` and its mirror beside the sealed bytes.

PDFs and office documents are not read: their text needs a parser this
binary does not carry.
2026-10-21 01:12:42 +02:00

49 lines
2.0 KiB
SQL

-- The text of the files in the tree, so a search finds a sentence in a `.txt`
-- or a `.csv` the way it finds one in a document. One row per file node, read
-- from the bytes `media_id` names; a file that is not text has a row with no
-- text, so it is read once and not again on every pass.
CREATE TABLE file_text (
node_id INTEGER PRIMARY KEY REFERENCES nodes (id) ON DELETE CASCADE,
media_id TEXT NOT NULL,
text TEXT NOT NULL
);
CREATE VIRTUAL TABLE files_fts USING fts5 (
text, content = 'file_text', content_rowid = 'node_id', tokenize = 'unicode61'
);
CREATE TRIGGER file_text_ai AFTER INSERT ON file_text BEGIN
INSERT INTO files_fts (rowid, text) VALUES (new.node_id, new.text);
END;
CREATE TRIGGER file_text_ad AFTER DELETE ON file_text BEGIN
INSERT INTO files_fts (files_fts, rowid, text) VALUES ('delete', old.node_id, old.text);
END;
CREATE TRIGGER file_text_au AFTER UPDATE ON file_text BEGIN
INSERT INTO files_fts (files_fts, rowid, text) VALUES ('delete', old.node_id, old.text);
INSERT INTO files_fts (rowid, text) VALUES (new.node_id, new.text);
END;
-- New bytes make the old text wrong: an SFTP save, and `:encrypt`, which
-- re-writes a file under a new media id. Dropped in the same statement, so
-- the plaintext of a file being sealed is gone the moment it is sealed.
CREATE TRIGGER file_text_stale AFTER UPDATE OF media_id ON nodes
WHEN new.media_id IS NOT old.media_id
BEGIN
DELETE FROM file_text WHERE node_id = new.id;
END;
CREATE TRIGGER file_text_sealed_media AFTER UPDATE OF encrypted ON media
WHEN new.encrypted <> 0
BEGIN
DELETE FROM file_text WHERE media_id = new.id;
END;
-- The last word, as in 0015: the text of a sealed file is never stored,
-- whatever path tries. Otherwise it would sit in `diary.db` and its mirror in
-- the clear while the bytes are sealed.
CREATE TRIGGER file_text_never_sealed BEFORE INSERT ON file_text
WHEN (SELECT encrypted FROM media WHERE id = new.media_id) IS NOT 0
BEGIN
SELECT RAISE(ABORT, 'a sealed file has no text in the index');
END;