File Triggers
addFileTrigger(action, path, handler) mirrors addDbTrigger, and is what closes the loop on anything a file rule reads out of the data tree: a rule can check root.usage[uid].bytes, but only a trigger can move that number. Paths are matched by prefix, so a trigger on ["posts"] sees every file operation beneath it.
| Action | Fires |
|---|---|
"reserve" | Book the cost. Once with the declared descriptor before a byte is accepted, and again with the actual descriptor once the real size is known. |
"release" | Unbook it. Whenever an upload fails, is rejected, or the connection aborts — and once mid-upload, immediately before the second reserve. |
"write" | After the bytes are committed. Notification only; ctx.reserved is the booked size. |
"remove" | After a delete commits. |
The handler receives one context object:
ctx.path— the full path array of the filectx.file— the descriptor described above, ornullon a deletectx.existing— the stat of what was already there, orundefinedctx.action— the action name that firedctx.reserved— the committed size, on"write"ctx.admin— the caller's auth context, ornullfor a server-side call
reserve is a gate; the other three are notifications. If a reserve handler throws, the write is refused (500). Anything else would store the file and charge nothing for it — the quota would stop enforcing with no error anywhere. write, remove and release fire after the operation has already committed, so a throw there is logged and the operation still succeeds.
Matching and scope
- Paths match by prefix, so a trigger on
["posts"]sees every file written anywhere beneath it. - They fire for both entry points — an HTTP upload and a server-side
setFile()— so a privileged server-side write is not a door around your accounting. - They do not fire for operations that were refused, so a rejected upload never reaches your handler.
- They run synchronously inside the operation. Keep them short; a slow handler is time the single-threaded engine is not spending on anyone else.