- Zig 75%
- Nix 25%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
emblem-ok-symbolic is not in the Adwaita icon theme, so rows for added documents showed the missing-image placeholder. object-select-symbolic is its checkmark. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017wLGgvLQgBNCNLz7ZDEYR3 |
||
| .forgejo/workflows | ||
| dbus | ||
| LICENSES | ||
| nautilus | ||
| nix | ||
| src | ||
| tests/nixos | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
paperless-upload
Sends documents to a Paperless-ngx server and waits for each one to be consumed, in two ways:
paperless-upload, a command-line tool;- a Nautilus extension that adds Send to Paperless… to the menu you get by right-clicking files in GNOME Files, uploads them itself, and shows how each one is going in a small window.
Both are written in Zig 0.17. The HTTP and the Paperless API are zig-paperless, the notifications go over D-Bus through a client zig-dbus-service generates, and the extension's Nautilus, GTK 4 and libadwaita bindings are generated by zig-gobject from their GObject introspection data.
Where this lives
The repository lives in three places that carry the same history. The Forgejo instance at https://git.jcollie.dev/jeff/paperless-upload is the web-visible one, and is where the continuous integration runs:
$ git clone https://git.jcollie.dev/jeff/paperless-upload.git
It is mirrored on Tangled at https://tangled.org/jcollie.dev/paperless-upload, and it is also on the Radicle network, where a repository is found by its identifier and nothing else:
rad:z2zEu2VT4wLxhGgqYVyRbxfp2FHRr
rad clone rad:z2zEu2VT4wLxhGgqYVyRbxfp2FHRr fetches it from any node that
seeds it. Any of the three is the whole project.
Configuration
The server and the API token are read from a ZON file, and from nowhere else: not the environment and not the command line.
.{
.url = "https://paperless.example.com",
.token = "0123456789abcdef0123456789abcdef01234567",
}
It is paperless-upload/config.zon in your configuration directory, as
known-folders finds it. On Linux that is $XDG_CONFIG_HOME,
or ~/.config when that is unset, as the XDG Base Directory Specification
describes. On macOS it is ~/Library/Application Support. --config PATH
names another file for the command-line tool.
url is the server's address, including any path prefix it is served under.
The token is created in the Paperless web interface, under your user's
profile. To keep it out of the configuration file (in a file a secrets
manager writes, say), name that file with token_file instead of giving
token:
.{
.url = "https://paperless.example.com",
.token_file = "/run/user/1000/secrets/paperless-token",
}
Exactly one of token and token_file must be given. $NAME and ${NAME}
in token_file are replaced with that environment variable, because secrets
managers hand out paths like /run/user/$UID/secrets/paperless-token (homeage)
or ${XDG_RUNTIME_DIR}/agenix/paperless-token (agenix) and leave the expansion
to a shell. Shells set UID without exporting it, so when the environment has
no UID the process's own user ID is used; any other variable that is not set
is an error. A relative token_file is relative to the directory the
configuration file is in.
Whitespace around the token, such as a final newline, is ignored, but
anything more than the token is an error.
Whichever file holds the token is a credential, so keep it to yourself:
$ chmod 600 ~/.config/paperless-upload/config.zon
Both the tool and the extension warn when other users can read it. A field that is missing or misspelled is an error that names the line and column.
The command-line tool
$ paperless-upload scan-001.pdf receipt.jpg
scan-001.pdf document 812
receipt.jpg already in Paperless as document 790
While it works, and when stderr is a terminal, it draws Zig's std.Progress
display below those lines. It shows how many files are done, which file is
being sent, and what is happening to it: kibibytes sent while uploading, then
the wait while Paperless consumes it.
[1/2] Sending to Paperless
└─ [0/2] receipt.jpg
└─ [1432/2918] uploading (KiB)
usage: paperless-upload [OPTION]... FILE...
-c, --config PATH the configuration file
-t, --tag ID add the tag with this ID; may be repeated
--title TITLE the document's title (only with a single FILE)
--no-wait print each task ID instead of waiting
-h, --help show the help and exit
Each file is streamed from disk rather than read into memory. A file that
fails does not stop the ones after it. The exit status is 0 when every file
was consumed (or queued, with --no-wait), 1 when any was not, and 2 for a
usage or configuration error.
The Nautilus extension
Select one or more files in Nautilus, right-click, and choose Send to Paperless…. The item appears only when every selected file is a local file of a kind Paperless consumes. It does not appear for folders, unsupported types, or files without a local path (the trash, a phone over MTP, an unmounted network share).
Choosing it opens a small window with a row for each file. Each row shows whether the file is waiting, uploading, or being consumed by Paperless, and then how it went:
- Added, with the new document's number and a button that opens it in Paperless.
- Already in Paperless, when the server refuses duplicates, with the number of the document that has it and the same button.
- Not added, with Paperless's reason and a button that copies it.
A missing or broken configuration file is shown the same way. Cancel skips the files that have not started yet. A file already being sent or consumed cannot be recalled from Paperless. Once everything is done, the button becomes Close.
The uploads run on a thread of their own inside Nautilus, so neither
Nautilus nor the window ever waits on them. Closing the window does not stop
them; whatever finishes after it is closed is reported as a desktop
notification instead. An upload still running when Nautilus quits
(nautilus -q, or logging out) is abandoned.
The extension is lib/nautilus/extensions-4/libpaperless-upload-nautilus.so
in the package. Nautilus loads extensions from a single directory, which on
NixOS is the system profile's lib/nautilus/extensions-4 (that is where
NAUTILUS_4_EXTENSION_DIR points). That is why the extension is installed by
the NixOS module, below, and not by the home-manager module. Restart Nautilus
with nautilus -q after installing it. To try a build without installing
it:
$ nix build
$ nautilus -q
$ NAUTILUS_4_EXTENSION_DIR=$PWD/result/lib/nautilus/extensions-4 nautilus
Elsewhere, copy the library into the directory your distribution's
Nautilus loads extensions from, usually /usr/lib64/nautilus/extensions-4 or
/usr/lib/x86_64-linux-gnu/nautilus/extensions-4.
Installing with Nix
The flake has two modules, since the parts belong in two places. The NixOS module installs the package system-wide, which puts the extension where Nautilus looks. The home-manager module installs the tool for a user and writes that user's configuration file.
{
inputs.paperless-upload.url = "git+https://git.jcollie.dev/jeff/paperless-upload.git";
}
In the NixOS configuration:
{
imports = [ inputs.paperless-upload.nixosModules.default ];
programs.paperless-upload.enable = true;
}
It also sets NAUTILUS_4_EXTENSION_DIR to the system profile's extension
directory, as a default, for Nautilus run outside GNOME, where nothing else
sets it.
In the home-manager configuration:
{
imports = [ inputs.paperless-upload.homeModules.default ];
programs.paperless-upload = {
enable = true;
settings = {
url = "https://paperless.example.com";
tokenFile = "/run/user/1000/secrets/paperless-token";
};
};
}
The module writes the configuration with token_file and never token.
The configuration file is in the Nix store, which anyone on the machine can
read, so the token has to live in a file of its own: a sops-nix or agenix
secret, or a file you keep with chmod 600. tokenFile is a string rather
than a Nix path for the same reason, since a path would be copied into the
store.
What Paperless will take
The kinds Paperless-ngx 3.2.1's parsers consume, by extension. Anything else is refused before it is sent:
| Kind | Extensions |
|---|---|
| PDF and images | pdf, png, jpg/jpeg/jpe, tif/tiff, gif, bmp, webp, heic |
| Text | txt, text, csv |
eml |
|
| Office documents | doc, docx, xls, xlsx, ppt, pptx, ppsx, odt, ods, odp, odg, rtf |
Mail and office documents are consumed only by a server with Tika (and
Gotenberg) enabled. A server without them refuses those files, and its reason
is what gets reported. Paperless also checks the content itself, so a file
whose name does not match its contents can still be refused: a gzip stream
named .pdf comes back as
BadRequest: {"document":["File type application/gzip not supported"]}.
Building
The Nix flake's development shell has Zig 0.17.0 (the release binary, from
zig-overlay), pkg-config, the
Nautilus, GTK 4, libadwaita and GLib headers, their GIR files (named in
GIR_PATH, which the binding generator reads), xsltproc, reuse and
zon2nix:
$ nix develop
$ zig build
That writes zig-out/bin/paperless-upload and
zig-out/lib/nautilus/extensions-4/libpaperless-upload-nautilus.so.
-Dnautilus=false leaves the extension out. It is off by default on
anything but Linux. nix build builds the package.
Development
$ zig build test --summary all
$ zig build check
$ zig fmt --check --exclude zig-pkg .
$ reuse lint
$ nix flake check
testruns the unit tests: the configuration file, the table of file kinds, the argument parsing, and the paths an upload takes before anything reaches a server.checkcompiles everything, including the extension, whichtestdoes not build.nix flake checkboots Paperless-ngx from the NixOS module (Redis, Celery and the consumer) in a virtual machine. paperless-upload is installed there with this project's NixOS module, and root's configuration is written by its home-manager module. The test obtains a token, writes it to the file the configuration names, and runs the tool against the server. The run covers where the two modules put things, a document consumed, a duplicate, a file the server refuses, a file refused locally,--no-wait, a missing configuration or token file, a token given in the configuration itself, and the warnings about readable ones.
The Forgejo workflow in .forgejo/workflows/test.yaml runs all of it on
every push, with the virtual machine on a runner that has KVM.
After changing a dependency in build.zig.zon, regenerate the Nix
expression for it:
$ nix develop -c zon2nix --17 --nix=build.zig.zon.nix build.zig.zon
Dependencies
- zig-paperless: the Paperless-ngx REST API client.
- known-folders: finds the configuration directory.
- zig-dbus-service and mdbus: the generated
org.freedesktop.Notificationsclient and the D-Bus transport under it. - zig-gobject (the
zig-0.17branch): generates the Nautilus, libadwaita, GTK 4 and GLib bindings at build time. Its GIR fixes include one this extension depends on. Nautilus callsget_file_itemswith a null list whenever nothing is selected, which is every time a folder is opened, and Nautilus's introspection data does not mark the list as nullable. An extension built against bindings without that fix crashes Nautilus. - libnautilus-extension-4, libadwaita, GTK 4 and GLib, through
pkg-config, for the extension. These have to be the very libraries the Nautilus that loads it uses, since two copies of GLib in one process is a crash. That is why the NixOS and home-manager modules build the package with the nixpkgs they are evaluated with (throughoverlays.default), rather than taking the flake's ownpackages.
License
MIT. The project follows the REUSE specification.
The license text is in LICENSES/MIT.txt.
References cited
- Hearn, M., Hammond, C., & McCann, W. J. (2024). Desktop Notifications Specification (Version 1.3). freedesktop.org. https://specifications.freedesktop.org/notification/latest/
- Masinter, L. (2015). Returning Values from Forms: multipart/form-data (RFC 7578; Proposed Standard). Internet Engineering Task Force (IETF). https://www.rfc-editor.org/info/rfc7578
- Paperless-ngx contributors. (n.d.). Paperless-ngx (Version 3.2.1) [Computer software]. https://github.com/paperless-ngx/paperless-ngx
- Paperless-ngx contributors. (n.d.). REST API. Paperless-ngx documentation. Retrieved October 2, 2026, from https://docs.paperless-ngx.com/api/
- Pennington, H., Carlsson, A., Larsson, A., Herzberg, S., McVittie, S., & Zeuthen, D. (n.d.). D-Bus Specification. freedesktop.org. https://dbus.freedesktop.org/doc/dbus-specification.html
- The GNOME Project. (n.d.). Nautilus Extension API Reference. Nautilus. Retrieved October 5, 2026, from https://gnome.pages.gitlab.gnome.org/nautilus/
- XDG Base Directory Specification (Version 0.8). (n.d.). freedesktop.org. https://specifications.freedesktop.org/basedir-spec/latest/