diff --git a/machines/john-endesktop/configuration.nix b/machines/john-endesktop/configuration.nix index d124bfa..21d7690 100644 --- a/machines/john-endesktop/configuration.nix +++ b/machines/john-endesktop/configuration.nix @@ -90,6 +90,8 @@ with lib; htop tmux zfs + rclone + custom.rclone-torbox-setup # Helper script to set up TorBox credentials via rbw ]; # Enable SSH @@ -126,6 +128,26 @@ with lib; roles.virtualisation.enable = true; + # TorBox WebDAV mount for rdt-client and Jellyfin + roles.rclone-mount = { + enable = true; + mounts.torbox = { + webdavUrl = "https://webdav.torbox.app"; + username = "john@ogle.fyi"; # TorBox account email + mountPoint = "/media/media/torbox-rclone"; + environmentFile = "/etc/rclone/torbox.env"; + vfsCacheMode = "full"; # Best for streaming media + dirCacheTime = "5m"; + extraArgs = [ + "--buffer-size=64M" + "--vfs-read-chunk-size=32M" + "--vfs-read-chunk-size-limit=off" + ]; + # Wait for ZFS media pool to be mounted before starting + requiresMountsFor = [ "/media" ]; + }; + }; + # Time zone time.timeZone = "America/Los_Angeles"; # Adjust as needed diff --git a/packages/default.nix b/packages/default.nix index 89b08ed..2ef8623 100644 --- a/packages/default.nix +++ b/packages/default.nix @@ -4,4 +4,5 @@ app-launcher-server = pkgs.callPackage ./app-launcher-server {}; claude-code = pkgs.callPackage ./claude-code {}; mcrcon-rbw = pkgs.callPackage ./mcrcon-rbw {}; + rclone-torbox-setup = pkgs.callPackage ./rclone-torbox-setup {}; } diff --git a/packages/rclone-torbox-setup/default.nix b/packages/rclone-torbox-setup/default.nix new file mode 100644 index 0000000..04d17cc --- /dev/null +++ b/packages/rclone-torbox-setup/default.nix @@ -0,0 +1,98 @@ +{ pkgs, ... }: + +pkgs.writeShellScriptBin "rclone-torbox-setup" '' + set -euo pipefail + + # Default values + RBW_ENTRY="''${1:-torbox}" + ENV_FILE="''${2:-/etc/rclone/torbox.env}" + + usage() { + echo "Usage: rclone-torbox-setup [rbw-entry] [env-file]" + echo "" + echo "Sets up rclone credentials for TorBox WebDAV mount." + echo "Retrieves password from rbw (Bitwarden), obscures it for rclone," + echo "and writes it to the environment file for the systemd service." + echo "" + echo "Arguments:" + echo " rbw-entry Name of the Bitwarden entry containing the password (default: torbox)" + echo " env-file Path to write the environment file (default: /etc/rclone/torbox.env)" + echo "" + echo "The Bitwarden entry should contain your TorBox password as the password field." + echo "" + echo "Example:" + echo " rclone-torbox-setup torbox-password /etc/rclone/torbox.env" + exit 1 + } + + if [[ "''${1:-}" == "-h" ]] || [[ "''${1:-}" == "--help" ]]; then + usage + fi + + echo "rclone TorBox credential setup" + echo "==============================" + echo "" + + # Check if rbw is available + if ! command -v rbw &> /dev/null; then + echo "Error: rbw is not available. Please ensure rbw is installed and configured." + exit 1 + fi + + # Check if rclone is available + if ! command -v rclone &> /dev/null; then + echo "Error: rclone is not available. Please ensure rclone is installed." + exit 1 + fi + + echo "Retrieving password from rbw entry: $RBW_ENTRY" + + # Retrieve password from Bitwarden + if ! TORBOX_PASS=$(rbw get "$RBW_ENTRY" 2>/dev/null); then + echo "" + echo "Error: Failed to retrieve password from rbw entry '$RBW_ENTRY'" + echo "" + echo "Please ensure:" + echo " 1. The entry '$RBW_ENTRY' exists in Bitwarden" + echo " 2. rbw is unlocked: rbw unlock" + echo " 3. rbw is synced: rbw sync" + echo "" + echo "To create the entry in Bitwarden:" + echo " - Name: $RBW_ENTRY" + echo " - Password: Your TorBox password" + exit 1 + fi + + echo "Password retrieved successfully" + + # Obscure the password for rclone + echo "Obscuring password for rclone..." + if ! OBSCURED_PASS=$(echo -n "$TORBOX_PASS" | rclone obscure -); then + echo "Error: Failed to obscure password with rclone" + exit 1 + fi + + # Create the directory if needed (requires sudo) + ENV_DIR=$(dirname "$ENV_FILE") + if [[ ! -d "$ENV_DIR" ]]; then + echo "Creating directory $ENV_DIR (requires sudo)..." + sudo mkdir -p "$ENV_DIR" + fi + + # Write the environment file + echo "Writing environment file to $ENV_FILE (requires sudo)..." + echo "RCLONE_WEBDAV_PASS=$OBSCURED_PASS" | sudo tee "$ENV_FILE" > /dev/null + sudo chmod 600 "$ENV_FILE" + + echo "" + echo "Setup complete!" + echo "" + echo "The environment file has been created at: $ENV_FILE" + echo "The rclone-mount-torbox systemd service will use this file." + echo "" + echo "To activate the mount after NixOS rebuild:" + echo " sudo systemctl start rclone-mount-torbox" + echo "" + echo "To check status:" + echo " sudo systemctl status rclone-mount-torbox" +'' diff --git a/roles/default.nix b/roles/default.nix index a56fd02..2a02e65 100644 --- a/roles/default.nix +++ b/roles/default.nix @@ -14,6 +14,7 @@ with lib; ./nfs-mounts ./nvidia ./printing + ./rclone-mount ./remote-build ./spotifyd ./users diff --git a/roles/rclone-mount/default.nix b/roles/rclone-mount/default.nix new file mode 100644 index 0000000..3d44261 --- /dev/null +++ b/roles/rclone-mount/default.nix @@ -0,0 +1,149 @@ +{ 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; + }; +}