Local File System
The local file system is the default data source (--source fs, or ZHANG_DATA_SOURCE=fs). Zhang reads the ledger from the folder given to zhang serve and writes the entries recorded in the web UI back to it. It is the only data source that notices when you edit the files yourself.
zhang serve /home/me/ledgerWith Docker, the folder is the one mounted at /data, see Installation.
Layout
Section titled “Layout”Zhang starts from the main file, main.zhang unless you pass --endpoint, and loads every file it includes, directly or through other included files. Paths in an include are relative to the file that contains it. Zhang reads and writes only inside the folder: an include of a file outside it, by an absolute path or by a relative one climbing out with .., is reported as IncludeNotFound, and the rest of the ledger loads. A folder served by Zhang can look like this:
ledger/├── main.zhang the main file (--endpoint)├── accounts.zhang included from main.zhang├── data/│ └── 2024/│ ├── 01.zhang written by the web UI│ └── 02.zhang├── attachments/ documents uploaded in the web UI│ └── 6f1c…/receipt.pdf└── .zhang/ └── passkeys.json registered passkeysThe extension of the main file selects the format of the whole ledger: .zhang, or .bean, .beancount and .bc for beancount. Every file the ledger includes is read in that format, and its rules apply to all of them, whatever their own extension. Any other extension of the main file, or none, is an error that names the main file, and zhang serve exits with code 1. If the main file does not exist, Zhang starts with an empty ledger.
The files are UTF-8. A file that starts with a byte order mark (BOM), as some Windows editors write it, is read as if it started without one, and Zhang keeps the mark when it writes the file. A file of the ledger that is not UTF-8, such as one saved as Latin-1 or GBK, stops the load with an error that names the file and the line of the first byte that is not UTF-8; Zhang never reads it with that byte replaced. Save the file as UTF-8 to fix it. See When the ledger does not load.
The web UI writes to these files:
- A new transaction, balance check or document is appended to the file that the
directive_output_pathoption names for its date,data/{year}/{month}.zhangby default (with the extension of the main file). The first time a file is used, Zhang appends anincludefor it to the main file. - Editing a transaction rewrites it in the file it comes from.
- An uploaded document is stored as
attachments/<random id>/<file name>, and linked from the account or the transaction. - Raw Editing saves the whole file you edited. It does not overwrite a file that changed since you opened it, by a transaction or balance check recorded in the web UI, an uploaded document or an edit outside: the save is refused, and the editor offers to reload the file (discarding your edits) or to keep editing. Through the API,
GET /api/files/{path}answers with thesha256of the content it serves; aPUTthat sends it back asexpected_sha256is refused with 409, writing nothing, when the file no longer matches it, and aPUTwithout it overwrites the file. AGETof a file that does not exist is answered with 404, except for the main file of a ledger started without one, which is served empty so that you can write it. AGETof a file that is not UTF-8 text, such as an image in the ledger folder, is answered with 415 and a message naming the file. A file the storage refuses to read or write, such as one without read or write permission for the user Zhang runs as, is answered with 403 and the messagethe storage refused to read <file>(orto write), on every route that reads or writes it: the editor, an edit in place, a recorded transaction or balance, and a document download. - Registering a passkey writes
.zhang/passkeys.json, see Authentication.
When Zhang reloads
Section titled “When Zhang reloads”Zhang watches the ledger folder and its subfolders, and reloads the ledger when:
- one of the ledger’s files (the main file or a file it includes) is modified;
- a file an
includenames, which did not exist, is created; - a file that a plugin depends on changes: its module file when it is inside the ledger folder, or a file or folder the plugin read through its
allowed_paths(creating, modifying or removing a file in a folder it listed counts too); - you record or change something in the web UI, which reloads after writing;
- you use the reload button of the web UI;
- the date changes (at midnight, in the ledger’s timezone), but only if a plugin read the current date during the load.
Events are grouped: Zhang waits half a second after the first change, then reloads once for everything that changed meanwhile. The web UI refreshes by itself after a reload.
Some changes do not trigger a reload:
- A new file that matches a wildcard include, such as
include "data/*.zhang", is loaded with the next reload only. Use the reload button, or save one of the ledger’s files. - Changes in
.zhang/and.cache/inside the ledger folder, and in files that are not part of the ledger.
The folder can be named any way: zhang serve ., a relative path such as zhang serve ../ledger, or a path through a symbolic link such as /tmp/… on macOS. Zhang resolves it to its real location when it starts, and watches that.
When a reload fails
Section titled “When a reload fails”If a file cannot be read at all, for example because of a syntax error, the reload fails and Zhang keeps serving the ledger as it was before the change. The web UI shows a notice with the reason at the top of every page, with a link to the file editor, and the reload button reports it too; the reason is also written to the log. Fix the file and save it again: the notice goes away once a reload succeeds. A transaction, balance or document recorded in the web UI while the files cannot be loaded writes nothing: it reloads the files first, and the reload’s failure is the answer (HTTP 409), shown the same way. Problems in the books, such as an unbalanced transaction, do not make the reload fail: they are listed in the web UI.
At startup, a ledger that cannot be read makes zhang serve exit with code 1.
File permissions
Section titled “File permissions”Zhang needs to read every file of the ledger. To record anything from the web UI, it also needs to write to the ledger folder: it appends to existing files and creates folders and files such as data/2024/ and attachments/. Without write access, recording from the web UI fails while reading keeps working.
The Docker image runs Zhang as root, so the files and folders it creates in the mounted folder belong to root on the host. Change their owner with chown if you edit them as another user.
The .cache folder
Section titled “The .cache folder”Zhang keeps a .cache folder in its working directory, which is not necessarily the ledger folder:
.cache/documents/holds a copy of every document the web UI has opened from a remote data source (S3, WebDAV or GitHub), which is served from there afterwards. Documents on the local disk are read from the disk each time. Earlier versions kept copies in.cache/data/, which is no longer read.- Earlier versions also kept a copy of every plugin module in
.cache/plugins/. Plugin modules are now read from the ledger’s storage on every load, and that folder is no longer written or read: you can delete it.
When you start zhang serve from inside the ledger folder, .cache appears next to your files, and Zhang ignores the changes it makes there. In the Docker image, the working directory is /application, so the cache stays inside the container and starts empty when the container is re-created.
The folder can be deleted while Zhang is stopped. Do so when you replace a document of a remote data source with a new file under the same name: the web UI keeps showing the cached copy otherwise.
