Files
roam/azosnextcloud.org
2026-10-14 09:41:44 +03:00

125 lines
6.8 KiB
Org Mode

:PROPERTIES:
:ID: f4196b35-5438-4da3-9d34-511291f482b5
:END:
#+title: azos/nextcloud
#+filetags: :project: :knowledge: :nextcloud:
Backlink: [[id:bf4dde56-0967-4374-94ed-771301e47c66][azos]]
* Architecture
=features/nextcloud/default.nix= (home-manager module, =azos.nextcloud-client= /
=azos.nextcloud-mount=) mounts a Nextcloud webdav folder at =~/synced-docs= via
=rclone mount= as a systemd user service (=nextcloud-mount.service=,
=WantedBy = default.target=, not =graphical-session.target= — the latter never
activates under EXWM since nothing explicitly starts it).
Credentials come from =pass= at service start (not baked into the Nix store):
password from =pass zakobar.com=, Nextcloud *user ID* (distinct from login name
on this server) from =pass nextcloud-username/aner=. See [[id:09ae970c-2995-43d9-a597-47bb904c98de][azos/pass]].
Gated on YubiKey smartcard presence via the shared =azos-card-present= overlay
(=overlays/default.nix=, wraps =gpg --card-status=) — same silent presence
check used by the mbsync mail-sync timer (=features/mail/default.nix=). No card
-> loop quietly until plugged in, no pinentry prompt either way.
=Restart = always= means unplugging/replugging the key remounts naturally.
Added in commit 04427dd "Nextcloud sync, also zapzap" (2026-10-11), same commit
that bumped azos-core to add the zapzap WhatsApp client
(=azos-core/features/station/whatsapp.nix=) — unrelated features landed together.
* Gotchas
** PATH-override crash loop (fixed 2026-10-11)
=nextcloud-mount.service= crash-looped (exit code 127, every 10s) because
=Service.Environment = "PATH=${pkgs.fuse3}/bin:${pkgs.fuse}/bin";= in
=features/nextcloud/default.nix= *replaces* the service's PATH rather than
appending. Every command in the mount script used absolute =${pkgs.X}/bin/...=
paths except the YubiKey-poll loop's bare =sleep 10= — which couldn't be found
under the restricted PATH, so bash exited 127. Ran fine manually (inherits full
interactive PATH), which is why it looked like it should work.
Fix: =${pkgs.coreutils}/bin/sleep 10= instead of bare =sleep 10=. General
lesson logged on the [[id:bf4dde56-0967-4374-94ed-771301e47c66][azos]] root —
any systemd unit that restricts PATH this way needs every bare command in its
script converted to an absolute nix-store reference, not just the ones the
override was added for.
** Same crash loop recurred with a different command (2026-10-12)
=nextcloud-mount.service= was 127-looping again, but this time the culprit was
=head=, not =sleep=: =password=$(pass zakobar.com | head -1)= and the matching
=uid=$(pass nextcloud-username/aner | head -1)= line both piped into bare
=head -1=, which isn't on the restricted PATH either. Under =set -eu=, the
pipeline's exit status is =head='s (127), which aborts the script. Fixed by
changing both to =${pkgs.coreutils}/bin/head -1=.
Confirms the general lesson above needs to be applied exhaustively: fixing the
one bare command that happened to be caught isn't enough — *every* token in
the script that isn't an absolute =/nix/store/...= path is a latent 127.
Next time this crash-loops, visually scan the whole script rather than
assuming the previously-fixed command is the only one.
Still 127'd after deploying the =head= fix — turned out =mkdir -p
"$mountpoint"= (the very first line of the script, before the YubiKey loop)
was *also* bare and had been bare the whole time, just masked because it
never got reached/tested in isolation before. Fixed to
=${pkgs.coreutils}/bin/mkdir -p=. Verified by extracting the deployed
=writeShellScript= output, manually running it under the exact restricted
=PATH=/<fuse paths>= from =systemctl --user show nextcloud-mount.service -p
Environment=, with =bash -x= to see exactly which line 127'd — this is the
reliable way to debug this script going forward rather than guessing from
=journalctl= alone (which only reports the exit code, not which line).
** A third, different bug after that: non-setuid fusermount3 (2026-10-12)
After the =mkdir= fix the script got all the way to =rclone mount=, which
then failed with "fusermount3: mount failed: Operation not permitted" (exit
1, not 127 — a new failure mode). Root cause: NixOS's real, usable
=fusermount3= is the *setuid-root* one at =/run/wrappers/bin/fusermount3=
(installed via =security.wrappers=); the plain =${pkgs.fuse3}/bin/fusermount3=
in the nix store has no setuid bit and can't perform the mount syscall
(lacks CAP_SYS_ADMIN). =rclone mount= looks up =fusermount3= via =PATH=
internally rather than an absolute path, and the service's
=Service.Environment = "PATH=...";= override pointed only at the raw
=pkgs.fuse3=/=pkgs.fuse= bin dirs, never =/run/wrappers/bin= — so it always
resolved to the broken non-setuid binary. =ExecStop= had the identical bug
(hardcoded to the non-setuid =${pkgs.fuse3}/bin/fusermount3 -u ...=).
Fix: prepend =/run/wrappers/bin= to the =PATH= override, and point
=ExecStop= at =/run/wrappers/bin/fusermount3= directly. Verified by manually
exporting the rclone env vars and PATH and running =rclone mount= by hand —
failed identically without =/run/wrappers/bin=, succeeded (real files
visible) with it.
General lesson for *any* systemd unit that overrides =PATH= and touches
FUSE: =/run/wrappers/bin= must be included, not just the raw package's
=/bin= — several NixOS tools (fusermount, sudo, etc.) only work from their
setuid wrapper, which lives there, not in the nix store path itself. I
initially misdiagnosed this same "Operation not permitted" error as an
artifact of my own sandboxed test shell lacking mount capabilities — it
was not; always re-test with the real setuid wrapper on PATH before
concluding "not reproducible" for FUSE-related permission errors.
Notably, the *symptom* this time didn't look like a crash-looping systemd
service at first glance — it looked like the Nextcloud desktop GUI client
(the separate =services.nextcloud-client=, webflow login) showing only the
Activity/notifications view with no file browser. That GUI client authenticates
fine on its own (confirmed via =~/.config/Nextcloud/logs=: AccountState
=CredentialsNotReady -> Connected=) — it's unrelated to file access. Actual
file access goes through the rclone webdav mount at =~/synced-docs=, which
was empty because this crash loop kept it unmounted. When the GUI client
looks "connected but empty", check =systemctl --user status
nextcloud-mount.service= rather than assuming an auth/account problem.
* Related: zapzap notification delivery (confirmed working, no bug)
Same session also investigated zapzap (WhatsApp client, added alongside this
feature) not showing desktop notifications. Confirmed the whole pipeline is
correctly wired — see [[id:c52036a9-25ec-4b8b-8b72-90fcd9ea65dd][azos/exwm]]
for the EDNC/D-Bus notification architecture note. No bug found; likely
explanation if it recurs is WhatsApp Web's standard behavior of suppressing
notifications for a chat that's already focused/open in the window.