smaller, simpler webdav service
This commit is contained in:
parent
e1ba549dd2
commit
d603d83d77
3 changed files with 87 additions and 285 deletions
|
|
@ -125,7 +125,6 @@
|
||||||
type = "server";
|
type = "server";
|
||||||
extraModules = [
|
extraModules = [
|
||||||
self.nixosModules.qbittorrent
|
self.nixosModules.qbittorrent
|
||||||
self.nixosModules.rclone-webdav
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
trantor = mkHost {
|
trantor = mkHost {
|
||||||
|
|
@ -201,7 +200,6 @@
|
||||||
|
|
||||||
nixosModules = {
|
nixosModules = {
|
||||||
qbittorrent = import ./modules/qbittorrent.nix;
|
qbittorrent = import ./modules/qbittorrent.nix;
|
||||||
rclone-webdav = import ./modules/rclone-webdav.nix;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
{ config, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
ports = {
|
ports = {
|
||||||
|
|
@ -6,8 +11,45 @@ let
|
||||||
librespeed = "8000";
|
librespeed = "8000";
|
||||||
radicale = "8001";
|
radicale = "8001";
|
||||||
vaultwarden = "8002";
|
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
|
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 = {
|
vaultwarden = {
|
||||||
enable = true;
|
enable = true;
|
||||||
config = {
|
config = {
|
||||||
|
|
@ -142,9 +175,47 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# TODO: remove when bug fix
|
systemd.services = {
|
||||||
# serokell/deploy-rs/issues/57
|
# TODO: remove when bug fix
|
||||||
# NixOS/nixpkgs/issues/180175
|
# serokell/deploy-rs/issues/57
|
||||||
# Workaround for upstream bug in NetworkManager-wait-online.service
|
# NixOS/nixpkgs/issues/180175
|
||||||
systemd.services.NetworkManager-wait-online.enable = false;
|
# 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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue