Files
Alexander 05b83364df feat: add NixOS module
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-20 12:38:04 +02:00

221 lines
5.7 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.musicfs;
toml = pkgs.formats.toml { };
originSubmodule = lib.types.submodule {
options = {
id = lib.mkOption {
type = lib.types.str;
description = "Unique identifier for this origin.";
};
originType = lib.mkOption {
type = lib.types.enum [ "local" "nfs" "smb" ];
default = "local";
description = "Storage backend type.";
};
priority = lib.mkOption {
type = lib.types.int;
default = 1;
description = "Failover priority (lower = preferred).";
};
enabled = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether this origin is active.";
};
path = lib.mkOption {
type = lib.types.str;
description = "Filesystem path to the music directory.";
};
extraSettings = lib.mkOption {
type = lib.types.attrsOf toml.type;
default = { };
description = "Additional origin-specific settings passed to the config.";
};
};
};
mkOriginConfig = o:
{
id = o.id;
origin_type = o.originType;
priority = o.priority;
enabled = o.enabled;
path = o.path;
}
// o.extraSettings;
configFile = toml.generate "musicfs.toml" (
{
mount_point = cfg.mountPoint;
cache_dir = cfg.cacheDir;
origins = map mkOriginConfig cfg.origins;
}
// lib.optionalAttrs (cfg.cache != { }) { cache = cfg.cache; }
// lib.optionalAttrs (cfg.health != { }) { health = cfg.health; }
// lib.optionalAttrs (cfg.logging != { }) { logging = cfg.logging; }
// cfg.extraConfig
);
inherit (builtins) map;
in
{
options.services.musicfs = {
enable = lib.mkEnableOption "MusicFS virtual FUSE filesystem";
package = lib.mkPackageOption pkgs "musicfs" {
default = null;
};
mountPoint = lib.mkOption {
type = lib.types.str;
default = "/mnt/music";
description = "Where to mount the virtual filesystem.";
};
cacheDir = lib.mkOption {
type = lib.types.str;
default = "/var/cache/musicfs";
description = "Directory for cache data (CAS chunks, metadata, search index).";
};
grpcPort = lib.mkOption {
type = lib.types.port;
default = 50052;
description = "Port for the gRPC control API.";
};
origins = lib.mkOption {
type = lib.types.listOf originSubmodule;
default = [ ];
description = "Music storage origins.";
example = lib.literalExpression ''
[
{
id = "local-music";
originType = "local";
path = "/srv/music";
}
]
'';
};
cache = lib.mkOption {
type = lib.types.attrsOf toml.type;
default = { };
description = "Cache settings passed directly to [cache] in config.";
example = {
metadata_cache_mb = 100;
content_cache_gb = 10;
};
};
health = lib.mkOption {
type = lib.types.attrsOf toml.type;
default = { };
description = "Health monitoring settings passed directly to [health] in config.";
};
logging = lib.mkOption {
type = lib.types.attrsOf toml.type;
default = { };
description = "Logging settings passed directly to [logging] in config.";
};
extraConfig = lib.mkOption {
type = lib.types.attrsOf toml.type;
default = { };
description = "Additional top-level config keys merged into the generated TOML.";
};
user = lib.mkOption {
type = lib.types.str;
default = "musicfs";
description = "User account under which musicfs runs.";
};
group = lib.mkOption {
type = lib.types.str;
default = "musicfs";
description = "Group under which musicfs runs.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to open the gRPC port in the firewall.";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.origins != [ ];
message = "services.musicfs.origins must have at least one entry.";
}
];
users.users.${cfg.user} = lib.mkIf (cfg.user == "musicfs") {
isSystemUser = true;
group = cfg.group;
home = cfg.cacheDir;
};
users.groups.${cfg.group} = lib.mkIf (cfg.group == "musicfs") { };
systemd.tmpfiles.rules = [
"d ${cfg.mountPoint} 0755 ${cfg.user} ${cfg.group} -"
"d ${cfg.cacheDir} 0750 ${cfg.user} ${cfg.group} -"
];
systemd.services.musicfs = {
description = "MusicFS - Virtual FUSE Filesystem for Music";
after = [ "network.target" "local-fs.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
ExecStart = "${lib.getExe cfg.package} mount --config ${configFile} --grpc-port ${toString cfg.grpcPort}";
ExecStopPost = "${pkgs.fuse3}/bin/fusermount3 -u ${cfg.mountPoint}";
User = cfg.user;
Group = cfg.group;
Restart = "on-failure";
RestartSec = 5;
# Hardening
ProtectSystem = "strict";
ReadWritePaths = [
cfg.mountPoint
cfg.cacheDir
] ++ map (o: o.path) (builtins.filter (o: o.enabled) cfg.origins);
PrivateTmp = true;
NoNewPrivileges = true;
ProtectHome = "read-only";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
# FUSE needs /dev/fuse
DeviceAllow = [ "/dev/fuse rw" ];
SupplementaryGroups = [ "fuse" ];
};
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.grpcPort ];
};
}