From d603d83d7705188f0002e8013104421e0c8e74ff Mon Sep 17 00:00:00 2001 From: William Date: Sat, 12 Jul 2025 15:56:59 -0300 Subject: [PATCH] smaller, simpler webdav service --- flake.nix | 2 - hosts/modules/alexandria/services.nix | 103 ++++++++-- modules/rclone-webdav.nix | 267 -------------------------- 3 files changed, 87 insertions(+), 285 deletions(-) delete mode 100644 modules/rclone-webdav.nix diff --git a/flake.nix b/flake.nix index b805c8a..fc1c8be 100644 --- a/flake.nix +++ b/flake.nix @@ -125,7 +125,6 @@ type = "server"; extraModules = [ self.nixosModules.qbittorrent - self.nixosModules.rclone-webdav ]; }; trantor = mkHost { @@ -201,7 +200,6 @@ nixosModules = { qbittorrent = import ./modules/qbittorrent.nix; - rclone-webdav = import ./modules/rclone-webdav.nix; }; }; } diff --git a/hosts/modules/alexandria/services.nix b/hosts/modules/alexandria/services.nix index 3fdcb2d..709dd6d 100644 --- a/hosts/modules/alexandria/services.nix +++ b/hosts/modules/alexandria/services.nix @@ -1,4 +1,9 @@ -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let ports = { @@ -6,8 +11,45 @@ let librespeed = "8000"; radicale = "8001"; vaultwarden = "8002"; - webdav = "8003"; }; + rclone-webdav-start = pkgs.writeShellScript "rclone-webdav-start.sh" '' + #!/bin/bash + + # Configuration + CREDS_FILE="/run/agenix/webdav" + SERVE_DIR="/data/webdav" + SOCKET_PATH="/run/rclone-webdav/webdav.sock" + + # Check if credentials file exists + if [ ! -f "$CREDS_FILE" ]; then + echo "Error: Credentials file $CREDS_FILE not found" + exit 1 + fi + + # Read credentials from file (format: username:password) + CREDENTIALS=$(cat "$CREDS_FILE") + USERNAME=$(echo "$CREDENTIALS" | cut -d':' -f1) + PASSWORD=$(echo "$CREDENTIALS" | cut -d':' -f2) + + # Validate credentials + if [ -z "$USERNAME" ] || [ -z "$PASSWORD" ]; then + echo "Error: Invalid credentials format. Expected username:password" + exit 1 + fi + + # Ensure serve directory exists + mkdir -p "$SERVE_DIR" + + # Remove existing socket if it exists + rm -f "$SOCKET_PATH" + + # Start rclone serve webdav + exec ${pkgs.rclone}/bin/rclone serve webdav "$SERVE_DIR" \ + --addr unix://"$SOCKET_PATH" \ + --user "$USERNAME" \ + --pass "$PASSWORD" \ + --verbose + ''; in { @@ -84,15 +126,6 @@ in }; }; - rclone-webdav = { - enable = true; - authFile = config.age.secrets.webdav.path; - dataDirectory = "/data/webdav"; - maxFileSize = "5G"; - listenAddresses = [ "0.0.0.0" ]; - port = lib.toInt ports.webdav; - }; - vaultwarden = { enable = true; config = { @@ -142,9 +175,47 @@ in }; }; - # TODO: remove when bug fix - # serokell/deploy-rs/issues/57 - # NixOS/nixpkgs/issues/180175 - # Workaround for upstream bug in NetworkManager-wait-online.service - systemd.services.NetworkManager-wait-online.enable = false; + systemd.services = { + # TODO: remove when bug fix + # serokell/deploy-rs/issues/57 + # NixOS/nixpkgs/issues/180175 + # Workaround for upstream bug in NetworkManager-wait-online.service + NetworkManager-wait-online.enable = false; + rclone-webdav = { + description = "RClone WebDAV Server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "exec"; + User = "user"; + Group = "users"; + ExecStart = "${rclone-webdav-start}"; + Restart = "always"; + RestartSec = "10"; + + # Security settings + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ + "/data/webdav" + "/run" + ]; + + # Create runtime directory for socket + RuntimeDirectory = "rclone-webdav"; + RuntimeDirectoryMode = "0755"; + }; + + # Ensure the user exists + preStart = '' + # Create webdav directory if it doesn't exist + mkdir -p /data/webdav + chown user:users /data/webdav + chmod 755 /data/webdav + ''; + }; + }; } diff --git a/modules/rclone-webdav.nix b/modules/rclone-webdav.nix deleted file mode 100644 index 097537c..0000000 --- a/modules/rclone-webdav.nix +++ /dev/null @@ -1,267 +0,0 @@ -{ - config, - lib, - pkgs, - ... -}: - -with lib; - -let - cfg = config.services.rclone-webdav; - - parseUserFile = - userFile: - let - content = builtins.readFile userFile; - lines = filter (line: line != "" && !hasPrefix "#" line) (splitString "\n" content); - parseUser = - line: - let - parts = splitString ":" line; - in - { - username = elemAt parts 0; - password = elemAt parts 1; - }; - in - map parseUser lines; - - users = if cfg.authFile != null then parseUserFile cfg.authFile else [ ]; - usernames = map (u: u.username) users; - - socketDirectory = "/var/lib/webdav"; - - # Generate rclone service for each user - mkRcloneService = - user: - nameValuePair "rclone-webdav-${user.username}" { - description = "rclone WebDAV service for ${user.username}"; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - - serviceConfig = { - Type = "simple"; - User = cfg.user; - Group = cfg.group; - Restart = "always"; - RestartSec = "5s"; - - # Ensure directories exist - ExecStartPre = [ - "${pkgs.coreutils}/bin/mkdir -p ${cfg.dataDirectory}/${user.username}" - "${pkgs.coreutils}/bin/mkdir -p ${socketDirectory}" - "${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.dataDirectory}/${user.username}" - "${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} ${socketDirectory}" - ]; - - ExecStart = '' - ${pkgs.rclone}/bin/rclone serve webdav ${cfg.dataDirectory}/${user.username} \ - --addr unix:${socketDirectory}/rclone-${user.username}.sock \ - --user ${user.username} \ - --pass ${user.password} \ - --log-level INFO - ''; - - # Security settings - NoNewPrivileges = true; - PrivateTmp = true; - ProtectSystem = "strict"; - ProtectHome = true; - ReadWritePaths = [ - cfg.dataDirectory - socketDirectory - ]; - }; - }; - - # Generate nginx upstream for each user - mkNginxUpstream = user: { - name = "rclone-${user.username}"; - value = { - servers = { - "unix:${socketDirectory}/rclone-${user.username}.sock" = { }; - }; - }; - }; - - # Generate nginx location for each user - mkNginxLocation = user: { - name = "/${user.username}/"; - value = { - proxyPass = "http://rclone-${user.username}"; - extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # Remove the username prefix from the path - rewrite ^/${user.username}/(.*) /$1 break; - - # WebDAV specific headers - proxy_set_header Destination $http_destination; - proxy_set_header Depth $http_depth; - proxy_set_header Overwrite $http_overwrite; - proxy_set_header Lock-Token $http_lock_token; - proxy_set_header If $http_if; - - # Allow WebDAV methods - proxy_method $request_method; - - # Increase timeouts for large file uploads - proxy_connect_timeout 300s; - proxy_send_timeout 300s; - proxy_read_timeout 300s; - - # Set maximum file size for this location - client_max_body_size ${cfg.maxFileSize}; - ''; - }; - }; - -in -{ - options.services.rclone-webdav = { - enable = mkEnableOption "rclone WebDAV multi-user service"; - - authFile = mkOption { - type = types.nullOr types.path; - default = null; - description = "Path to file containing username:password pairs, one per line"; - example = "/etc/rclone-webdav-users"; - }; - - dataDirectory = mkOption { - type = types.str; - default = "/srv/webdav"; - description = "Base directory where user subdirectories will be created"; - }; - - user = mkOption { - type = types.str; - default = "webdav"; - description = "User to run rclone services as"; - }; - - group = mkOption { - type = types.str; - default = "webdav"; - description = "Group to run rclone services as"; - }; - - listenAddresses = mkOption { - type = types.listOf types.str; - default = [ "localhost" ]; - description = "List of addresses for nginx to listen on"; - example = [ - "localhost" - "127.0.0.1" - "::1" - ]; - }; - - port = mkOption { - type = types.port; - default = 8000; - description = "Port for nginx to listen on"; - }; - - maxFileSize = mkOption { - type = types.str; - default = "0"; - description = "Maximum file size for uploads (nginx client_max_body_size). Use '0' for unlimited."; - example = "100M"; - }; - }; - - config = mkIf cfg.enable { - assertions = [ - { - assertion = cfg.authFile != null; - message = "services.rclone-webdav.authFile must be specified"; - } - { - assertion = users != [ ]; - message = "Auth file must contain at least one user"; - } - ]; - - # Create user and group - users.users = mkIf (cfg.user == "webdav") { - webdav = { - isSystemUser = true; - group = cfg.group; - description = "rclone WebDAV service user"; - home = cfg.dataDirectory; - createHome = true; - }; - }; - - users.groups = mkIf (cfg.group == "webdav") { - webdav = { - gid = null; - }; - }; - - # Create systemd services for each user - systemd.services = listToAttrs (map mkRcloneService users); - - # Configure nginx - services.nginx = { - enable = true; - - upstreams = listToAttrs (map mkNginxUpstream users); - - virtualHosts."rclone-webdav" = { - listen = map (addr: { - addr = addr; - port = cfg.port; - }) cfg.listenAddresses; - - locations = listToAttrs (map mkNginxLocation users) // { - "/" = { - return = "200 'rclone WebDAV Multi-user Server'"; - extraConfig = '' - add_header Content-Type text/plain; - ''; - }; - - # Catch-all location for non-existent users - return 400 - "~* ^/([^/]+)/" = { - extraConfig = '' - # Check if the requested user exists - set $user_exists 0; - ${concatStringsSep "\n" ( - map (username: "if ($1 = \"${username}\") { set $user_exists 1; }") usernames - )} - - # If user doesn't exist, return 400 - if ($user_exists = 0) { - return 400 "User not found"; - } - - # This should not be reached for valid users - return 404; - ''; - }; - }; - - extraConfig = '' - # Enable WebDAV methods - dav_methods PUT DELETE MKCOL COPY MOVE; - dav_ext_methods PROPFIND PROPPATCH LOCK UNLOCK; - - # Set default maximum file size - client_max_body_size ${cfg.maxFileSize}; - ''; - }; - }; - - # Ensure directories exist - systemd.tmpfiles.rules = [ - "d ${cfg.dataDirectory} 0755 ${cfg.user} ${cfg.group} -" - "d ${socketDirectory} 0755 ${cfg.user} ${cfg.group} -" - ] ++ map (user: "d ${cfg.dataDirectory}/${user.username} 0755 ${cfg.user} ${cfg.group} -") users; - }; -}