From 04427ddcda90f0985da7643801d61516a4dab41f Mon Sep 17 00:00:00 2001 From: Aner Zakobar Date: Sun, 11 Oct 2026 09:22:42 +0300 Subject: [PATCH] Nextcloud sync, also zapzap --- azos-core | 2 +- features/mail/default.nix | 10 ++--- features/nextcloud/default.nix | 71 ++++++++++++++++++++++++++++++---- nixos/configuration.nix | 5 +++ overlays/default.nix | 10 +++++ 5 files changed, 85 insertions(+), 13 deletions(-) diff --git a/azos-core b/azos-core index b1e92ea..2c7d32e 160000 --- a/azos-core +++ b/azos-core @@ -1 +1 @@ -Subproject commit b1e92ea6c04d76c9a1ea4f5222420d9da52da303 +Subproject commit 2c7d32e10be27ff7b7e19edb53bd4d12ded71beb diff --git a/features/mail/default.nix b/features/mail/default.nix index b1c082a..8a04202 100644 --- a/features/mail/default.nix +++ b/features/mail/default.nix @@ -29,10 +29,10 @@ }; # mbsync's PassCmd shells out to `pass`, which needs the GPG smartcard to # decrypt. On a timer, a missing card would otherwise surface as a - # pinentry "please insert card" prompt every run. `gpg --card-status` - # only queries the card reader (no pinentry involved either way), so it's - # safe to use as a silent presence check: skip the sync entirely when the - # card isn't there, instead of letting mbsync hit that prompt. + # pinentry "please insert card" prompt every run. azos-card-present is a + # silent presence check (see overlays/default.nix): skip the sync + # entirely when the card isn't there, instead of letting mbsync hit that + # prompt. # # New mail is detected by counting files under every account's Maildir # "new" subdirectory before and after the sync -- that's where messages @@ -42,7 +42,7 @@ # notify-send -- intentionally separate from mu4e's own "mbsync -a" (its # mu4e-get-mail-command), which doesn't go through this script at all. mbsyncSafe = pkgs.writeShellScript "mbsync-safe" '' - if ! ${pkgs.gnupg}/bin/gpg --card-status >/dev/null 2>&1; then + if ! ${pkgs.azos-card-present}/bin/azos-card-present; then exit 0 fi before=$(find "$HOME/Maildir" -path '*/new/*' -type f 2>/dev/null | wc -l) diff --git a/features/nextcloud/default.nix b/features/nextcloud/default.nix index a33d02d..3784af5 100644 --- a/features/nextcloud/default.nix +++ b/features/nextcloud/default.nix @@ -4,17 +4,74 @@ config, pkgs, ... - }: { + }: let + mountDir = "synced-docs"; + remoteSubfolder = "synced-docs"; + remoteName = "ncsynceddocs"; + remoteNameEnv = lib.toUpper remoteName; + # rclone's webdav backend needs the account's Nextcloud *user ID*, not + # its login name, in the /dav/files// path -- the two differ on + # this server. The login name ("aner") still goes in the `user` field + # for Basic Auth. Both the uid and password come from `pass` at service + # start, same as mbsync in features/mail, so neither lands in the Nix + # store. + # + # Shares the azos-card-present silent presence check (overlays/default.nix) + # with the mail sync timer: loop quietly until the YubiKey is plugged in + # (no pinentry prompt either way), then exec rclone in the foreground. + # Restart=always means unplugging/replugging the key remounts naturally. + nextcloudMountSafe = pkgs.writeShellScript "nextcloud-mount-safe" '' + set -eu + mountpoint="$HOME/${mountDir}" + mkdir -p "$mountpoint" + while ! ${pkgs.azos-card-present}/bin/azos-card-present; do + sleep 10 + done + password=$(${pkgs.pass}/bin/pass zakobar.com | head -1) + uid=$(${pkgs.pass}/bin/pass nextcloud-username/aner | head -1) + export RCLONE_CONFIG_${remoteNameEnv}_TYPE=webdav + export RCLONE_CONFIG_${remoteNameEnv}_URL="https://nextcloud.zakobar.com/remote.php/dav/files/$uid/" + export RCLONE_CONFIG_${remoteNameEnv}_VENDOR=nextcloud + export RCLONE_CONFIG_${remoteNameEnv}_USER=aner + export RCLONE_CONFIG_${remoteNameEnv}_PASS="$(${pkgs.rclone}/bin/rclone obscure "$password")" + exec ${pkgs.rclone}/bin/rclone mount ${remoteName}:${remoteSubfolder} "$mountpoint" \ + --vfs-cache-mode writes + ''; + in { options.azos.nextcloud-client.enable = lib.mkOption { default = true; type = lib.types.bool; }; - config = lib.mkIf config.azos.nextcloud-client.enable { - home.packages = with pkgs; [nextcloud-client]; - services.nextcloud-client = { - enable = true; - startInBackground = false; - }; + options.azos.nextcloud-mount.enable = lib.mkOption { + default = true; + type = lib.types.bool; }; + config = lib.mkMerge [ + (lib.mkIf config.azos.nextcloud-client.enable { + home.packages = with pkgs; [nextcloud-client]; + services.nextcloud-client = { + enable = true; + startInBackground = false; + }; + }) + (lib.mkIf config.azos.nextcloud-mount.enable { + home.packages = with pkgs; [rclone]; + # default.target, not graphical-session.target: the latter never + # activates under EXWM in this setup (nothing explicitly starts it), + # which is why nextcloud-client itself silently never autostarts. + systemd.user.services.nextcloud-mount = { + Unit.Description = "Mount Nextcloud ${remoteSubfolder} via rclone (waits silently for YubiKey)"; + Service = { + Type = "simple"; + Environment = "PATH=${pkgs.fuse3}/bin:${pkgs.fuse}/bin"; + ExecStart = "${nextcloudMountSafe}"; + ExecStop = "${pkgs.fuse3}/bin/fusermount3 -u %h/${mountDir}"; + Restart = "always"; + RestartSec = "10s"; + }; + Install.WantedBy = ["default.target"]; + }; + }) + ]; }; } diff --git a/nixos/configuration.nix b/nixos/configuration.nix index 3e1c6dc..b774494 100644 --- a/nixos/configuration.nix +++ b/nixos/configuration.nix @@ -98,6 +98,11 @@ azos.printing.enable = true; azos.hardware-auth.enable = true; + # Needed for unprivileged FUSE mounts (e.g. rclone mount) -- without this, + # fusermount3 has no setuid wrapper and mounting fails with + # "Operation not permitted" even though /dev/fuse itself is world-writable. + programs.fuse.enable = true; + home-manager = { extraSpecialArgs = {inherit inputs outputs suiteModules pkgs;}; users = { diff --git a/overlays/default.nix b/overlays/default.nix index 63d4f5d..3184ca0 100755 --- a/overlays/default.nix +++ b/overlays/default.nix @@ -11,4 +11,14 @@ config.allowUnfree = true; }; }; + + # Shared silent presence check for the GPG smartcard (the YubiKey's OpenPGP + # applet): `gpg --card-status` only queries the reader, never invoking + # pinentry, so it's safe to poll without ever producing a "please insert + # card" prompt. Used by both the mail sync timer and the Nextcloud mount + # service to gate on `pass` being decryptable. + config.flake.overlayPkgs.azos-card-present = pkgs: + pkgs.writeShellScriptBin "azos-card-present" '' + exec ${pkgs.gnupg}/bin/gpg --card-status >/dev/null 2>&1 + ''; }