{ config, lib, pkgs, ... }: let cfg = config.programs.rclone; targetName = "rclone"; manualVendor = "manual"; owncloudVendor = "owncloud"; owncloudName = "Owncloud"; nextcloudVendor = "nextcloud"; mkIfNotNull = value: name: lib.mkIf (value != null) { ${name} = value; }; mkFileOption = { description }: lib.mkOption { type = lib.types.nullOr (lib.types.either lib.types.path lib.types.str); description = "The path to a file containing ${description}"; default = null; }; mkUsernameOption = { service }: lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The user name for logging in to ${service}."; default = null; }; mkPasswordOption = { service, itemKind ? "password" }: lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The ${itemKind} obscured using the `rclone obscure` command for logging in to ${service}."; default = null; }; mkPasswordFileOption = { service, itemKind ? "password" }: mkFileOption { description = "the ${itemKind} obscured using the `rclone obscure` command for logging in to ${service}."; }; mkServerUsernameOption = { service }: mkUsernameOption { service = "the ${service} server."; }; mkServerPasswordOption = { service }: mkPasswordOption { service = "the ${service} server."; }; mkServerPasswordFileOption = { service }: mkPasswordFileOption { service = "the ${service} server."; }; mkSystemdDependencyOption = { default, global ? false, ... }: lib.mkOption { type = lib.types.attrsOf (lib.types.listOf lib.types.str); description = "The systemd services ${ if global then "all" else "this" } sync${ if global then "s" else "" } depend${ if global then "" else "s" } on."; example = { secrets = [ "sops-nix.service" ]; }; inherit default; }; mkProvider = ( { config, name, ... }: { options = { path = lib.mkOption { type = lib.types.str; description = "The path to mount the remote file system to."; default = "$HOME/.mnt/${lib.escapeShellArg name}"; }; autoStart = lib.mkOption { type = lib.types.bool; description = "Whether to start this sync automatically."; default = true; }; systemdDependencies = mkSystemdDependencyOption { default = cfg.systemdDependencies; }; environment = lib.mkOption { type = lib.types.attrsOf lib.types.envVar; description = "The environment variables to pass to the service."; default = { }; }; secrets = lib.mkOption { type = lib.types.attrsOf (lib.types.either lib.types.path lib.types.str); description = "A set of environment variables to load from files."; default = { }; }; secretsScript = lib.mkOption { type = lib.types.lines; description = "A script for loading secrets before launching the sync."; default = ""; }; vfs = { enable = lib.mkEnableOption "Virtual File System"; mode = lib.mkOption { type = lib.types.nullOr (lib.types.enum [ "minimal" "writes" "full" ]); description = "The cache mode to use."; default = "full"; }; dirCacheTime = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The time to cache directory entries for."; default = null; }; pollInterval = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The time to wait between polling for changes."; default = null; }; bufferSize = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The maximum size of the buffer per size to allocate."; default = null; }; maxAge = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The maximum age of cached files to keep."; default = null; }; }; extraArgs = lib.mkOption { type = lib.types.listOf lib.types.str; description = "A set of additional arguments to pass to `rclone mount`."; default = []; }; config = lib.mkOption { type = lib.types.attrs; description = "The rclone config to use for creating the mount."; visible = false; }; args = lib.mkOption { type = lib.types.listOf lib.types.str; description = "The arguments to pass to `rclone mount`."; visible = false; }; }; config = { secretsScript = lib.strings.concatLines (builtins.attrValues ( builtins.mapAttrs (name: path: "export ${name}=\"$(cat ${lib.escapeShellArg path})\"") config.secrets)); args = config.extraArgs ++ ( let vfs = config.vfs; in ( lib.optionals vfs.enable (builtins.attrValues ( lib.attrsets.concatMapAttrs (name: value: lib.optionalAttrs (value != null) { name = "--${name}=${lib.escapeShellArg value}"; }) { vfs-cache-mode = vfs.mode; vfs-cache-poll-interval = vfs.pollInterval; vfs-cache-max-size = vfs.bufferSize; vfs-cache-max-age = vfs.maxAge; })))); }; }); mkWebdavProvider = ( { displayName, vendor ? null, ... }: ( { config, ... }: { imports = [ mkProvider ]; options = { vendor = lib.mkOption { type = lib.types.nullOr (lib.types.enum [ "fastmail" nextcloudVendor owncloudVendor "sharepoint" "sharepoint-ntlm" "rclone" "other" ]); description = "The vendor of the WebDAV share."; default = vendor; }; url = lib.mkOption { type = lib.types.str; description = "The WebDAV URL of the ${displayName} server."; default = null; }; username = mkServerUsernameOption { service = displayName; }; obscuredPassword = mkServerPasswordOption { service = displayName; }; obscuredPasswordFile = mkServerPasswordFileOption { service = displayName; }; bearerToken = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The bearer token for logging in to the ${displayName} server."; default = null; }; bearerTokenFile = lib.mkOption { type = lib.types.nullOr (lib.types.either lib.types.path lib.types.str); description = "The path to a file containing the bearer token for logging in to the ${displayName} server."; default = null; }; }; config = { config = lib.mkMerge [ { type = "webdav"; url = config.url; } (mkIfNotNull config.vendor "vendor") (mkIfNotNull config.username "user") (mkIfNotNull config.obscuredPassword "pass") (mkIfNotNull config.bearerToken "bearer_token") ]; secrets = lib.mkMerge [ (mkIfNotNull config.obscuredPasswordFile "RCLONE_WEBDAV_PASS") (mkIfNotNull config.bearerTokenFile "RCLONE_WEBDAV_BEARER_TOKEN") ]; }; })); mkOwncloudProvider = { displayName ? owncloudName, vendor ? owncloudVendor }: ( { config, ... }: { imports = [ (mkWebdavProvider { inherit displayName vendor; }) ]; options = { baseUrl = lib.mkOption { type = lib.types.str; description = "The base url of the ${displayName} server for automatically determining the WebDAV url."; }; }; config = { url = "${lib.strings.removeSuffix "/" config.baseUrl}/remote.php/dav/files/${config.username}"; }; }); mkProtonProvider = { displayName }: ( { config, ... }: { imports = [ mkProvider ]; options = { username = mkUsernameOption { service = displayName; }; obscuredPassword = mkPasswordOption { service = displayName; }; obscuredPasswordFile = mkPasswordFileOption { service = displayName; }; webAuthnToken = lib.mkOption { type = lib.types.strMatching "[0-9]{6}"; description = "The 2 Factor Authentication code for logging in to ${displayName}."; }; webAuthnTokenFile = mkFileOption { description = "the 2 Factor Authentication code for logging in to ${displayName}."; }; clientID = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The client key."; default = null; }; accessToken = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The access token."; default = null; }; refreshToken = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The refresh token."; default = null; }; saltedKeyPass = lib.mkOption { type = lib.types.nullOr lib.types.str; description = "The salted key pass."; default = null; }; enableCaching = lib.mkOption { type = lib.types.nullOr lib.types.bool; description = "Whether to enable ${displayName}'s integrated caching."; default = null; }; mailboxPassword = mkPasswordOption { service = displayName; itemKind = "mailbox password"; }; mailboxPasswordFile = mkPasswordFileOption { service = displayName; itemKind = "mailbox password"; }; clientIDFile = mkFileOption { description = "the client key."; }; accessTokenFile = mkFileOption { description = "the access token."; }; refreshTokenFile = mkFileOption { description = "the refresh token."; }; saltedKeyPassFile = mkFileOption { description = "the salted key pass."; }; }; config = { config = lib.mkMerge [ { type = "protondrive"; } (mkIfNotNull config.username "username") (mkIfNotNull config.obscuredPassword "password") (mkIfNotNull config.webAuthnToken "2fa") (mkIfNotNull config.mailboxPassword "mailbox_password") (mkIfNotNull config.clientID "client_uid") (mkIfNotNull config.accessToken "client_access_token") (mkIfNotNull config.refreshToken "client_refresh_token") (mkIfNotNull config.saltedKeyPass "client_salted_key_pass") (mkIfNotNull config.enableCaching "enable_caching") ]; secrets = lib.mkMerge [ (mkIfNotNull config.obscuredPasswordFile "RCLONE_PROTONDRIVE_PASSWORD") (mkIfNotNull config.webAuthnTokenFile "RCLONE_PROTONDRIVE_2FA") (mkIfNotNull config.mailboxPasswordFile "RCLONE_PROTONDRIVE_MAILBOX_PASSWORD") (mkIfNotNull config.clientIDFile "RCLONE_PROTONDRIVE_CLIENT_UID") (mkIfNotNull config.accessTokenFile "RCLONE_PROTONDRIVE_CLIENT_ACCESS_TOKEN") (mkIfNotNull config.refreshTokenFile "RCLONE_PROTONDRIVE_CLIENT_REFRESH_TOKEN") (mkIfNotNull config.saltedKeyPassFile "RCLONE_PROTONDRIVE_CLIENT_SALTED_KEY_PASS") ]; }; }); syncProviders = { ${manualVendor} = { displayName = "Custom"; module = mkProvider; }; ${nextcloudVendor} = rec { displayName = "Nextcloud"; module = mkOwncloudProvider { inherit displayName; vendor = nextcloudVendor; }; }; ${owncloudVendor} = { displayName = owncloudName; module = mkOwncloudProvider { }; }; proton = rec { displayName = "Proton"; module = mkProtonProvider { inherit displayName; }; }; }; in { options = { programs.rclone = { enable = lib.mkEnableOption "rclone"; systemdDependencies = mkSystemdDependencyOption { default = {}; global = true; }; globalArgs = lib.mkOption { type = lib.types.listOf lib.types.str; description = "The arguments to pass to `rclone mount` for each configuration."; default = [ ]; }; configs = (builtins.mapAttrs (name: provider: lib.mkOption { type = lib.types.attrsOf (lib.types.submodule provider.module); description = "The ${provider.displayName} synchronizations to set up."; default = { }; }) syncProviders); }; }; config = { home.packages = lib.optionals cfg.enable [ pkgs.fuse pkgs.rclone ]; systemd.user = lib.optionalAttrs cfg.enable { enable = true; services = { rclone = { Unit = { Description = "rclone Starter"; Documentation = "man:rclone(1)"; }; Service = { Type = "simple"; ExecStartPre = let script = pkgs.writeShellScriptBin "rclone-pre" '' sleep 10 ''; in (lib.getExe script); ExecStart = let script = pkgs.writeShellScriptBin "rclone" '' systemctl --user start rclone.target ''; in (lib.getExe script); }; }; } // ( lib.attrsets.concatMapAttrs (providerName: configs: lib.attrsets.concatMapAttrs (name: sync: let serviceName = "rclone-${providerName}-sync-${name}"; in { ${serviceName} = { Unit = { Description = "rclone sync service for ${name} at using ${providerName}"; After = builtins.concatLists (builtins.attrValues sync.systemdDependencies); }; Service = { Environment = lib.mapAttrsToList (key: val: (lib.escapeShellArg "${key}=${val}")) sync.environment; ExecStart = let configFile = pkgs.writeText "${serviceName}.conf" (lib.generators.toINI { } { ${name} = sync.config; }); script = pkgs.writeShellScriptBin serviceName '' ${sync.secretsScript} mkdir -p ${sync.path} mkdir -p /tmp/rclone ${lib.getExe pkgs.rclone} mount ${ builtins.concatStringsSep " " (cfg.globalArgs ++ sync.args) } --config ${configFile} ${name}: ${sync.path} ''; in (lib.getExe script); ExecStop = let script = pkgs.writeShellScriptBin "${serviceName}-stop" '' furermount -zu ${sync.path} ''; in (lib.getExe script); }; Install = { WantedBy = lib.optional sync.autoStart "${targetName}.target"; }; }; }) configs) cfg.configs); targets.${targetName} = { Unit = { Description = "rclone Mounts"; Documentation = "man:rclone(1)"; }; }; }; }; }