Files
infra-nix/modules/fileserver.nix
T
2026-05-23 21:50:03 +02:00

348 lines
11 KiB
Nix

{ config, pkgs, ... }:
{
########################################
# Samba File Sharing
########################################
services.samba = {
enable = true;
openFirewall = true;
# Enable nmbd for NetBIOS name resolution and browsing (legacy Windows/macOS)
nmbd.enable = true;
# Enable WSD discovery (Windows) and mDNS/NetBIOS advertisement improvements via wsdd + Avahi.
# "smb encrypt" set to desired to avoid discovery issues with some clients while still allowing encryption.
settings = {
global = {
workgroup = "WORKGROUP";
"server string" = "NixFiles";
"netbios name" = "fileserver";
security = "user";
"map to guest" = "bad user";
"unix extensions" = "no";
# macOS optimizations
"vfs objects" = "catia fruit streams_xattr";
"fruit:aapl" = "yes";
"fruit:metadata" = "stream";
"fruit:resource" = "stream";
"fruit:model" = "MacSamba";
"fruit:advertise_fullsync" = "yes";
"ea support" = "yes";
# Time Machine support
"fruit:time machine" = "yes";
"fruit:time machine max size" = "500G";
# Prefer encryption but do not require it for basic browsing/guest visibility
"smb encrypt" = "desired";
# Better macOS discovery
"min protocol" = "SMB2";
"max protocol" = "SMB3";
"server role" = "standalone server";
# Allow Apple extended attributes & full sync advertisement already configured above.
};
daten = {
path = "/data/daten";
browseable = "yes";
"read only" = "no";
"valid users" = "danlin";
"fruit:time machine" = "no";
};
daten-share = {
path = "/data/daten/share";
browseable = "yes";
"read only" = "no";
"guest ok" = "yes"; # Gastzugriff erlaubt
public = "yes"; # Alias für guest ok
# Removed "valid users" to allow true guest access; danlin can still write via group permissions
"fruit:time machine" = "no";
};
timemachine = {
path = "/data/backup/timemachine";
browseable = "yes";
"read only" = "no";
"valid users" = "danlin";
"fruit:time machine" = "yes";
"fruit:time machine max size" = "500G";
};
};
};
########################################
# Network Discovery Services (Avahi + WSD)
########################################
# Avahi for macOS Finder discovery (mDNS) and general _smb._tcp advertising
services.avahi = {
enable = true;
nssmdns4 = true; # Provide .local resolution
openFirewall = true;
publish = {
enable = true;
userServices = true;
};
# Explicit SMB service advertisement for macOS Finder
extraServiceFiles = {
smb = ''
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h</name>
<service>
<type>_smb._tcp</type>
<port>445</port>
</service>
<service>
<type>_device-info._tcp</type>
<port>0</port>
<txt-record>model=TimeCapsule8,119</txt-record>
</service>
<service>
<type>_adisk._tcp</type>
<port>9</port>
<txt-record>sys=waMa=0,adVF=0x100</txt-record>
<txt-record>dk0=adVN=timemachine,adVF=0x82</txt-record>
</service>
</service-group>
'';
};
};
# Windows network neighborhood discovery via Web Services for Devices (WSD)
# NixOS does not provide a `services.wsdd` option in this release, so define a systemd unit.
systemd.services.wsdd = {
description = "Web Services Discovery daemon for Samba (wsdd)";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" "samba.service" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.wsdd}/bin/wsdd --hostname fileserver --workgroup WORKGROUP --interface ens18";
Restart = "on-failure";
RestartSec = 5;
};
};
########################################
# Directory Structure
########################################
systemd.tmpfiles.rules = [
"d /data 0775 root root -"
"d /data/daten 0775 danlin users -"
"d /data/daten/share 0775 danlin users -"
# Gitea (runs as git user inside container, uid 1000)
"d /data/gitea 0755 root root -"
# DJ-Struktur
"d /data/daten/DJing 0775 danlin users -"
"d /data/daten/DJing/Music 0775 danlin users -"
"d /data/daten/DJing/Data 0775 danlin users -"
"d /data/daten/DJing/Inbox 0775 danlin users -"
# Time Machine backup
"d /data/backup 0775 root root -"
"d /data/backup/timemachine 0775 danlin users -"
# Secrets
"d /data/secrets 0700 root root -"
# dj-beets project
"d /opt/dj-beets 0755 root root -"
];
########################################
# Virtualization & Containers
########################################
# Virtualization
virtualisation.podman.enable = true;
virtualisation.oci-containers.backend = "podman";
virtualisation.oci-containers.containers.filebrowser = {
image = "gtstef/filebrowser:stable";
autoStart = true;
ports = [ "8080:80" ];
volumes = [ "/data:/srv" ];
environment = {
TZ = "Europe/Berlin";
};
environmentFiles = [ "/data/secrets/filebrowser.env" ];
};
virtualisation.oci-containers.containers.gitea = {
image = "gitea/gitea:latest";
autoStart = true;
ports = [ "3000:3000" "2222:2222" ];
volumes = [
"/data/gitea:/data"
];
environment = {
USER_UID = "1000";
USER_GID = "1000";
TZ = "Europe/Berlin";
GITEA__server__DOMAIN = "git.home.lindenfelser.de";
GITEA__server__ROOT_URL = "https://git.home.lindenfelser.de/";
GITEA__server__SSH_DOMAIN = "git.home.lindenfelser.de";
GITEA__server__SSH_PORT = "2222";
GITEA__server__SSH_LISTEN_PORT = "2222";
GITEA__server__START_SSH_SERVER = "true";
};
};
virtualisation.oci-containers.containers.dj-beets-cli = {
image = "dj-beets:latest";
autoStart = false; # Run manually or via systemd service
volumes = [
"/data/daten/DJing:/data/daten/DJing"
"/opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro"
"/opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json"
];
environment = {
BEETS_CONFIG = "/etc/beets/config.yaml";
};
cmd = [ "tail" "-f" "/dev/null" ]; # Keep container alive
};
virtualisation.oci-containers.containers.dj-beets-web = {
image = "dj-beets:latest";
autoStart = true;
ports = [ "8337:8337" ];
volumes = [
"/data/daten/DJing:/data/daten/DJing"
"/opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro"
"/opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json"
];
environment = {
BEETS_CONFIG = "/etc/beets/config.yaml";
};
cmd = [ "beet" "web" ];
};
########################################
# Backup Services
########################################
systemd.services.rclone-backup = {
description = "Backup /data to Google Drive via rclone";
serviceConfig = {
Type = "oneshot";
ExecStart = ''
${pkgs.rclone}/bin/rclone sync /data gdrive:backup-daten \
--fast-list \
--drive-stop-on-upload-limit \
--log-file=/var/log/rclone-backup.log \
--log-level=INFO
'';
User = "root";
};
};
systemd.timers.rclone-backup = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "03:00";
Persistent = true;
};
};
########################################
# System Packages
########################################
environment.systemPackages = [
pkgs.git
pkgs.tmux
pkgs.htop
pkgs.podman-compose
# Beets CLI wrapper
(pkgs.writeShellScriptBin "beet" ''
exec ${pkgs.podman}/bin/podman run --rm -it \
-v /data/daten/DJing:/data/daten/DJing \
-v /opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro \
-v /opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json \
-e BEETS_CONFIG=/etc/beets/config.yaml \
-u $(id -u):$(id -g) \
dj-beets:latest \
beet "$@"
'')
];
########################################
# DJ-Beets Services
########################################
# Systemd service to build dj-beets image on deploy
systemd.services.dj-beets-build = {
description = "Build dj-beets Docker image";
wantedBy = [ "multi-user.target" ];
before = [ "podman-dj-beets-web.service" ];
after = [ "podman.service" ];
wants = [ "podman.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "build-dj-beets" ''
set -euo pipefail
if [ ! -f /opt/dj-beets/Dockerfile ]; then
echo "ERROR: Dockerfile not found at /opt/dj-beets" >&2
echo "Deploy with 'make deploy-fileserver' first" >&2
exit 1
fi
cd /opt/dj-beets
${pkgs.podman}/bin/podman build -t dj-beets:latest .
echo "dj-beets image built successfully"
'';
User = "root";
};
};
# Auto-import service using Docker container
systemd.services.dj-beets-autoimport = {
description = "Auto-import music into beets library via Docker";
after = [ "dj-beets-build.service" ];
wants = [ "dj-beets-build.service" ];
serviceConfig = {
Type = "oneshot";
ExecStart = pkgs.writeShellScript "beets-autoimport" ''
set -euo pipefail
INBOX="/data/daten/DJing/Inbox"
LOG="/data/daten/DJing/Data/BeetsAutoImport.log"
# Check if inbox has files
if ! find "$INBOX" -mindepth 1 -maxdepth 1 -type f -print -quit | grep -q .; then
echo "[$(date)] No files in Inbox, skipping" >> "$LOG"
exit 0
fi
echo "[$(date)] Starting beets import" >> "$LOG"
${pkgs.podman}/bin/podman run --rm \
-v /data/daten/DJing:/data/daten/DJing \
-v /opt/dj-beets/config.yaml:/etc/beets/config.yaml:ro \
-v /opt/dj-beets/beatport_token.json:/opt/dj-beets/beatport_token.json \
-e BEETS_CONFIG=/etc/beets/config.yaml \
localhost/dj-beets:latest \
beet import -q /data/daten/DJing/Inbox >> "$LOG" 2>&1
'';
# Use root so the service can access the root Podman image storage
User = "root";
Group = "root";
};
};
systemd.timers.dj-beets-autoimport = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = "*:0/10"; # Every 10 minutes
Persistent = true;
};
};
########################################
# Firewall
########################################
networking.firewall.enable = true;
networking.firewall.allowedTCPPorts = [ 22 139 445 8080 8337 5357 3000 2222 ]; # 5357 WSD
networking.firewall.allowedUDPPorts = [ 137 138 3702 5353 ]; # NetBIOS + WSD (3702) + mDNS (5353)
}