Skip to content

13. Files

A library receives paper: delivery notes, supplier invoices, scanned membership cards. The module has no business inventing an attachments table: Kewos has a Drive — versioned, attachable to any record, with its own permissions.

This step uploads a delivery note, attaches it to a catalogue title, corrects it, throws it away and gets it back.

The rule

A module never writes its own file table. It calls kewos/drive, from Python or through the API. What that gives you, and what you will not be writing: versioning, content addressing, a bin, sharing, expiring public links, auditing.

Two things are worth understanding before writing a line.

A document has a stable identity and immutable versions. kws_doc carries the name, the owner, the links. Every uploaded content becomes a kws_doc_version that is never modified again. Correcting a document means adding a version, not replacing bytes. The previous one stays readable, and that is what makes recovery possible when somebody scanned the wrong page.

A link is not a folder. You do not file a document into a tree: you attach it to a record (library_title no. 1). The folders in the Drive screen are derived from those links. No module creates a folder, and nothing is left orphaned when a record goes.

Upload, attaching it at once

Fenêtre de terminal
curl -X POST /api/v1/drive/docs \
-H "Authorization: Bearer $TOKEN" \
-F "file=@delivery-note.pdf" \
-F "name=Delivery note 2026-09" \
-F "model=library_title" \
-F "record_id=1"
{
"id": 1,
"name": "Delivery note 2026-09",
"content_type": "text/plain",
"size": 56,
"current_version_id": 1,
"deleted": false,
"links": [{ "model": "library_title", "record_id": "1" }]
}

Attaching on upload is not a writing shortcut: the server first checks that the caller may see that record. Without it, anyone could plant a document on a record they are not allowed to read.

From a module it is the same gesture in Python:

from kewos.drive import create_doc, link_doc
with engine.begin() as conn:
doc_id = create_doc(
conn,
storage,
name="Delivery note 2026-09",
content=pdf_bytes,
content_type="application/pdf",
owner_id=user_id,
now=now,
)
# The link is what makes it findable from the record, and what builds the folder.
link_doc(conn, doc_id, model="library_title", record_id="1", by=user_id, now=now)

Correcting: one more version

Fenêtre de terminal
curl -X POST /api/v1/drive/docs/1/versions \
-H "Authorization: Bearer $TOKEN" \
-F "file=@delivery-note-corrected.pdf"
[
{ "number": 2, "size": 77, "content_type": "text/plain" },
{ "number": 1, "size": 56, "content_type": "text/plain" }
]

Version 1 is still there and still readable (GET /api/v1/drive/docs/1/versions/1/content). restore_version makes it current again, which — again — adds a version instead of erasing one.

Discarding, and discarding for good

Two gestures, and the difference is deliberate:

Fenêtre de terminal
curl -X POST /api/v1/drive/docs/1/trash # 200 -- reversible
curl -X POST /api/v1/drive/docs/1/untrash # 200 -- and back
curl -X DELETE /api/v1/drive/docs/1 # 403 -- "data purger role required"

The bin is a reversible state any user with write access can set. Permanent deletion requires the kws_data_purger right, and it is audited. It is the same rule as everywhere else in Kewos: destroying data is not a variant of editing it.

What it gives you

The document, its attachment, and the folder the record grew on its own:

The Drive screen: the document attached to "Catalogue · Dune", and the derived folder carrying the same name

The folder reads “Catalogue · Dune”: the model’s translated label, then the title’s own name. The server resolves both, in one query per model, because the browser holds only a model name and an id and would need one request per folder to manage on its own. Until yesterday this screen read library_title · 1.

You do nothing to get this: the name comes from what your model already declares, and the translation key is the one your module already translates (model.library_title).

It chains with reading documents

This is where step 9 pays off: you upload the scanned delivery note into the Drive, then read its text.

from kewos.drive import read_current
from kewos.ocr import extract_text
content = read_current(conn, storage, doc_id)
text = extract_text(content, content_type="application/pdf")

The document stays the evidence; the extracted text is only a reading of it. Never replace one with the other: character recognition is wrong sometimes, the original is not.

Next

The module is complete: data, screens, permissions, reports, AI, automation and files. What is left is wrapping it up for somebody else: Shipping.