{ config, lib, pkgs, ... }: with lib; let cfg = config.roles.rclone-mount; # Generate systemd service for a single mount mkMountService = name: mountCfg: { description = "rclone mount for ${name}"; after = [ "network-online.target" ]; wants = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; # Wait for parent mount points (e.g., ZFS pools) to be available unitConfig = mkIf (mountCfg.requiresMountsFor != []) { RequiresMountsFor = mountCfg.requiresMountsFor; }; serviceConfig = { Type = "notify"; ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p ${mountCfg.mountPoint}"; ExecStart = concatStringsSep " " ([ "${pkgs.rclone}/bin/rclone mount" ":webdav:${mountCfg.remotePath}" "${mountCfg.mountPoint}" "--webdav-url=${mountCfg.webdavUrl}" "--webdav-vendor=${mountCfg.webdavVendor}" "--webdav-user=${mountCfg.username}" "--allow-other" "--vfs-cache-mode=${mountCfg.vfsCacheMode}" "--dir-cache-time=${mountCfg.dirCacheTime}" "--poll-interval=${mountCfg.pollInterval}" "--log-level=${mountCfg.logLevel}" ] ++ mountCfg.extraArgs); ExecStop = "${pkgs.fuse}/bin/fusermount -uz ${mountCfg.mountPoint}"; Restart = "on-failure"; RestartSec = "10s"; EnvironmentFile = mountCfg.environmentFile; }; }; in { options.roles.rclone-mount = { enable = mkEnableOption "Enable rclone WebDAV mounts"; mounts = mkOption { type = types.attrsOf (types.submodule { options = { webdavUrl = mkOption { type = types.str; description = "WebDAV server URL (e.g., https://webdav.torbox.app)"; }; webdavVendor = mkOption { type = types.enum [ "other" "nextcloud" "owncloud" "sharepoint" "sharepoint-ntlm" "fastmail" ]; default = "other"; description = "WebDAV server vendor for optimizations"; }; username = mkOption { type = types.str; description = "WebDAV username (often email address)"; }; environmentFile = mkOption { type = types.path; description = '' Path to environment file containing RCLONE_WEBDAV_PASS. The password should be obscured using: rclone obscure File format: RCLONE_WEBDAV_PASS= ''; }; mountPoint = mkOption { type = types.str; description = "Local mount point path"; }; remotePath = mkOption { type = types.str; default = "/"; description = "Remote path on WebDAV server to mount"; }; vfsCacheMode = mkOption { type = types.enum [ "off" "minimal" "writes" "full" ]; default = "full"; description = '' VFS cache mode. For streaming media, 'full' is recommended. - off: No caching (direct reads/writes) - minimal: Cache open files only - writes: Cache writes and open files - full: Full caching of all files ''; }; dirCacheTime = mkOption { type = types.str; default = "5m"; description = "Time to cache directory entries"; }; pollInterval = mkOption { type = types.str; default = "1m"; description = "Poll interval for remote changes"; }; logLevel = mkOption { type = types.enum [ "DEBUG" "INFO" "NOTICE" "ERROR" ]; default = "INFO"; description = "rclone log level"; }; extraArgs = mkOption { type = types.listOf types.str; default = []; description = "Extra arguments to pass to rclone mount"; }; requiresMountsFor = mkOption { type = types.listOf types.str; default = []; description = '' List of mount points that must be available before this service starts. Use this when the mount point's parent is on a ZFS pool or other filesystem that may not be mounted at boot time. Example: [ "/media" ] to wait for the media ZFS pool to mount. ''; }; }; }); default = {}; description = "Attribute set of rclone WebDAV mounts to configure"; }; }; config = mkIf cfg.enable { # Ensure FUSE is available environment.systemPackages = [ pkgs.rclone pkgs.fuse ]; programs.fuse.userAllowOther = true; # Create systemd services for each mount systemd.services = mapAttrs' (name: mountCfg: nameValuePair "rclone-mount-${name}" (mkMountService name mountCfg) ) cfg.mounts; }; }