Attachments
Photos, logos, signatures and PDFs are stored apart from records. A payload references a file by the lowercase hexadecimal SHA-256 of its bytes. The same bytes always have the same name, so a file is uploaded once however many records use it, and it can never change.
{ "image": "35a12c5c6f0150bef53f609d70846b1a7568a44d5565b903d2f536b0c4ac5fdf" }Which payload keys hold hashes
Section titled “Which payload keys hold hashes”| Entity | Key | Content |
|---|---|---|
article |
image |
JPEG photo of the article |
company_profile |
logo |
PNG (or JPEG) logo, the reference app stores PNG at most 800 pixels on the long side |
delivery_note |
signatures[].image |
PNG signature |
delivery_note |
pdf |
The rendered delivery note |
loan |
items[].photos[] |
JPEG photos of the condition at handover |
loan |
signatures[].image |
PNG signature at handover |
loan |
pdf |
The handover protocol |
loan |
returns[].items[].photos[] |
JPEG photos of the condition at return |
loan |
returns[].signatures[].image |
PNG signature at return |
loan |
returns[].pdf |
The return protocol |
The reference app finds hashes generically: it walks the whole payload and collects every string of 64 hexadecimal characters found under a key named image, logo, pdf or photos (directly or inside an array), at any depth. New fields that hold attachments SHOULD use one of these key names so that older clients upload and download them too.
Upload
Section titled “Upload”Clients MUST upload every attachment a record references before they push the record, so that a server never holds a record whose file it does not have.
sequenceDiagram
autonumber
participant C as Client
participant S as Server
Note over C: outbox batch references photo 35a1...5fdf
C->>S: HEAD /lve/v1/attachments/35a1...5fdf
alt stored
S-->>C: 200
else not stored
S-->>C: 404
C->>S: PUT /lve/v1/attachments/35a1...5fdf (JPEG bytes)
Note over S: stream to a temporary file,<br/>hash while reading,<br/>compare with the path
S-->>C: 201 {sha256, size, content_type}
end
C->>S: POST /lve/v1/sync/push (record with "image": "35a1...5fdf")
HEAD /lve/v1/attachments/{sha256} answers 200 (with Content-Type and Content-Length) when the attachment is stored and 404 when not. It is optional: a client may upload without checking, but checking saves bandwidth because most attachments are already there after the first sync.
PUT /lve/v1/attachments/35a12c5c6f0150bef53f609d70846b1a7568a44d5565b903d2f536b0c4ac5fdf HTTP/1.1Authorization: Bearer 7|kM2p...Content-Type: image/jpegContent-Length: 184311
<raw bytes>{ "sha256": "35a12c5c6f0150bef53f609d70846b1a7568a44d5565b903d2f536b0c4ac5fdf", "size": 184311, "content_type": "image/jpeg" }| Status | Meaning |
|---|---|
201 |
Stored now |
200 |
Was already stored; the body was not needed. The response describes the stored attachment, including the content type of its first upload. |
413 payload_too_large |
Larger than the limit |
422 hash_mismatch |
The SHA-256 of the body differs from the path |
401 unauthenticated |
Token missing or revoked |
Rules:
- The path holds the hash in lowercase hexadecimal. The reference server also accepts upper case and stores it as lower case.
- The body is the raw file, not JSON and not multipart.
- The limit is 20 MiB (20,971,520 bytes) in version 1. The reference server refuses a larger
Content-Lengthbefore reading, and stops reading as soon as a body without length passes the limit. A proxy in front of the server must allow bodies of that size. - The server MUST verify the hash and refuse a mismatch; otherwise one client could replace another's file.
Content-Typeis stored with the attachment and returned on download. A missing type is stored asapplication/octet-stream. The reference server accepts any type.
The reference app sends the type it detects from the first bytes: image/png, image/jpeg, application/pdf, otherwise application/octet-stream. It uploads only attachments it holds itself; a hash it received from another device but has not downloaded yet is not uploaded again.
Download
Section titled “Download”GET /lve/v1/attachments/35a12c5c6f0150bef53f609d70846b1a7568a44d5565b903d2f536b0c4ac5fdf HTTP/1.1Authorization: Bearer 7|kM2p...Answers 200 with the bytes and the stored Content-Type, or 404 with not_found. The reference server also sends Cache-Control: private, max-age=31536000, immutable.
After a pull, clients download every attachment that a record they hold references and that they do not have. They MUST compute the SHA-256 of what they received and discard bytes that do not match. The reference app downloads missing attachments at the end of every sync round, ignores failures (a missing file is tried again next round) and renders documents again from the record when a PDF is not available yet.
Clients store attachments by hash as well. The reference app keeps them in its application support folder, one file per hash, with file protection until first unlock.
Garbage and retention
Section titled “Garbage and retention”Attachments are never deleted through the protocol. When a record stops referencing a file (a new article photo, a re-rendered PDF after a signature was added), the old file stays on the server and on devices. This keeps every version of a record usable: a device that is behind may still reference the old hash.
A server MAY remove attachments that no current record version references, after a grace period long enough for all devices to sync (weeks, not hours). It MUST keep every attachment that the current version of any record references. The reference server does not remove attachments. Re-rendered PDFs make up most of the unreferenced data: every added signature produces a new PDF of a delivery note.