{ 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 ]; }; }