#!/usr/bin/env bash set -Eeuo pipefail # === Settings === : "${XDG_CONFIG_HOME:="$HOME/.config"}" RCLONE_CFG_DIR="$XDG_CONFIG_HOME/rclone" MOUNT_POINTS_FILE="$RCLONE_CFG_DIR/mount_points.cfg" SYSTEMD_USER_DIR="$XDG_CONFIG_HOME/systemd/user" # Defaults for mount behavior (can be overridden via environment) : "${RCLONE_VFS_CACHE_MODE:=writes}" : "${RCLONE_DIR_CACHE_TIME:=5s}" # === Helpers === _die() { echo "Error: $*" >&2; exit 1; } _need() { command -v "$1" >/dev/null 2>&1 || _die "Required command '$1' not found"; } _mkdirp() { mkdir -p "$1"; } _yesno() { # _yesno "Prompt" default_yes|default_no local prompt="$1" def="$2" ans defc case "$def" in default_yes) defc="Y/n";; default_no) defc="y/N";; *) _die "_yesno: second arg must be default_yes|default_no";; esac read -r -p "$prompt [$defc] " ans || true ans="${ans,,}" if [[ -z "$ans" ]]; then [[ "$def" == "default_yes" ]] && return 0 || return 1 fi [[ "$ans" == "y" || "$ans" == "yes" ]] } # Read mountpoint for a remote from ~/.config/rclone/mount_points.cfg _get_mountpoint() { local name="$1" [[ -f "$MOUNT_POINTS_FILE" ]] || return 1 # lines like: name=/path/to/mount awk -F= -v k="$name" '$1==k {print $2}' "$MOUNT_POINTS_FILE" | tail -n1 } # Set/replace mountpoint entry _set_mountpoint() { local name="$1" mp="$2" _mkdirp "$(dirname "$MOUNT_POINTS_FILE")" touch "$MOUNT_POINTS_FILE" # remove old line(s) for name grep -v -e "^${name}=" "$MOUNT_POINTS_FILE" > "${MOUNT_POINTS_FILE}.tmp" || true echo "${name}=${mp}" >> "${MOUNT_POINTS_FILE}.tmp" mv "${MOUNT_POINTS_FILE}.tmp" "$MOUNT_POINTS_FILE" } # Systemd unit filename for a remote _unit_name() { local name="$1" # keep it simple; allow letters, digits, dash/underscore echo "rclone-${name}.service" } # Create (or overwrite) systemd --user unit for a remote _write_unit() { local name="$1" local mp="$2" local unit="$(_unit_name "$name")" _mkdirp "$SYSTEMD_USER_DIR" cat > "${SYSTEMD_USER_DIR}/${unit}" < [args] Commands: help Show this help text. config Interactive setup: creates/updates an rclone WebDAV remote and saves its mount point in ~/.config/rclone/mount_points.cfg. mount Mount the remote now (foreground). Uses saved mount point. enable Install and enable a systemd --user service for the remote. disable Disable and stop the systemd --user service for the remote. uninstall Disable, stop, and remove the systemd --user service file. status Check status of the systemd unit. log Show log from the systemd unit. Notes: - mTLS (client cert/key) is optional. If enabled in the wizard, the paths are stored as global.client_cert/global.client_key in rclone.conf for the remote. - Mount options default to RCLONE_VFS_CACHE_MODE="writes" and RCLONE_DIR_CACHE_TIME="5s". Override by exporting env vars before running, or edit the generated unit. - Mount point paths are stored as entries in ~/.config/rclone/mount_points.cfg (format: NAME=/path). EOF } _prompt_default() { local prompt="$1" default="${2:-}" local ans if [[ -n "$default" ]]; then read -r -p "$prompt [$default]: " ans || true echo "${ans:-$default}" else read -r -p "$prompt: " ans || true echo "$ans" fi } _prompt_secret() { local prompt="$1" local ans read -r -s -p "$prompt: " ans || true echo echo "$ans" } _cmd_config() { _need rclone echo "== Interactive rclone WebDAV setup (idempotent) ==" local name="${1:-}"; [[ -n "$name" ]] || _die "Usage: $0 config " local url vendor user pass enable_mtls client_cert client_key mount_point # --- prompts --- url="$(_prompt_default 'WebDAV URL (e.g., https://copyparty.example.com)')" vendor="$(_prompt_default 'Vendor (copyparty|owncloud|nextcloud|other)' 'copyparty')" user="$(_prompt_default 'Username (HTTP Auth; username ignored by copyparty)')" pass="$(_prompt_secret 'Password (HTTP Auth)')" echo if _yesno "Use mutual TLS (client certificate)?" default_no; then enable_mtls=1 client_cert="$(_prompt_default 'Client certificate PEM path' "$HOME/.config/rclone/client.crt")" client_key="$(_prompt_default 'Client key PEM path' "$HOME/.config/rclone/client.key")" echo # best-effort warnings if files missing; rclone will error if truly required [[ -f "$client_cert" ]] || echo "Warning: client cert not found at '$client_cert' (continuing)">&2 [[ -f "$client_key" ]] || echo "Warning: client key not found at '$client_key' (continuing)">&2 else enable_mtls=0 fi local mp_default mp_default="$(realpath "$HOME/${name}" 2>/dev/null || echo "$HOME/${name}")" mount_point="$(_prompt_default 'Mount point (absolute)' "$mp_default")" # --- sanitize values to avoid CR/LF sneaking in --- sanitize() { printf %s "$1" | tr -d '\r\n'; } name=$(sanitize "$name"); url=$(sanitize "$url"); vendor=$(sanitize "$vendor") user=$(sanitize "$user"); pass=$(sanitize "$pass") client_cert=$(sanitize "${client_cert:-}"); client_key=$(sanitize "${client_key:-}") # vendor alias [[ "$vendor" == "copyparty" ]] && vendor="owncloud" echo echo "Creating/updating rclone remote '$name'..." # if exists, replace to keep idempotent if rclone config show 2>/dev/null | grep -qxF "[$name]"; then rclone config delete "$name" >/dev/null || true fi # build args array # include mTLS settings only if enabled args=( "$name" webdav "url=$url" "vendor=$vendor" "pacer_min_sleep=0.01ms" ) if [[ "$enable_mtls" == "1" ]]; then args+=( "global.client_cert=$client_cert" "global.client_key=$client_key" ) fi if [[ -n "$user" ]]; then args+=( "user=$user" ) fi if [[ -n "$pass" ]]; then # use --obscure so rclone stores it safely rclone config create "${args[@]}" "pass=$pass" --obscure else rclone config create "${args[@]}" fi echo "Saving mount point mapping: ${name} -> ${mount_point}" _set_mountpoint "$name" "$mount_point" echo "Done. Try:" echo " $(basename "$0") mount $name" echo " $(basename "$0") enable $name" } _cmd_mount() { _need rclone local name="${1:-}"; [[ -n "$name" ]] || _die "Usage: $0 mount " local mp="$(_get_mountpoint "$name")" || _die "No mount point stored for '$name'. Run '$0 config' first." # Ensure directory exists _mkdirp "$mp" echo "Temporarily mounting ${name}: at $mp" echo "This process will now block as it services the mount." echo "If you want to run in the background, you should enable the systemd unit instead." echo "e.g., \`${BASH_SOURCE} enable ${name}\`" exec rclone mount "${name}:" "$mp" \ --vfs-cache-mode "$RCLONE_VFS_CACHE_MODE" \ --dir-cache-time "$RCLONE_DIR_CACHE_TIME" } _cmd_enable() { _need systemctl _need rclone local name="${1:-}"; [[ -n "$name" ]] || _die "Usage: $0 enable " local mp="$(_get_mountpoint "$name")" || _die "No mount point stored for '$name'. Run '$0 config' first." _write_unit "$name" "$mp" systemctl --user daemon-reload systemctl --user enable "$(_unit_name "$name")" echo "Enabled systemd/User service: $(_unit_name "$name")" systemctl --user start "$(_unit_name "$name")" echo "Started systemd/User service: $(_unit_name "$name")" echo "Waiting 5 seconds before checking status ..." sleep 5 systemctl --user status "$(_unit_name "$name")" } _cmd_disable() { _need systemctl local name="${1:-}"; [[ -n "$name" ]] || _die "Usage: $0 disable " local unit="$(_unit_name "$name")" systemctl --user disable "$unit" || true systemctl --user stop "$unit" || true systemctl --user daemon-reload systemctl --user status "$unit" || true echo echo "Disabled service: $unit." echo "Stopped service: $unit" } _cmd_uninstall() { _need systemctl local name="${1:-}"; [[ -n "$name" ]] || _die "Usage: $0 uninstall " local unit="$(_unit_name "$name")" systemctl --user disable "$unit" || true systemctl --user stop "$unit" || true rm -f "${SYSTEMD_USER_DIR}/${unit}" systemctl --user daemon-reload systemctl --user status "$unit" || true echo echo "Removed ${unit}." } _cmd_status() { _need systemctl local name="${1:-}"; [[ -n "$name" ]] || _die "Usage: $0 status " systemctl --user status "$(_unit_name "$name")" } _cmd_log() { _need journalctl local name="${1:-}"; [[ -n "$name" ]] || _die "Usage: $0 log " journalctl --user -u "$(_unit_name "$name")" } # === Main === cmd="${1:-help}" case "$cmd" in help|-h|--help) _print_help ;; config) shift; _cmd_config "$@" ;; mount) shift; _cmd_mount "$@" ;; enable) shift; _cmd_enable "$@" ;; disable) shift; _cmd_disable "$@" ;; uninstall) shift; _cmd_uninstall "$@" ;; status) shift; _cmd_status "$@" ;; log|logs) shift; _cmd_log "$@" ;; *) _die "Unknown command: $cmd. Try '$0 help'." ;; esac