From 05b83364df7f84e1095af4c4a0e65f570a6aae63 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 20 May 2026 12:38:04 +0200 Subject: [PATCH] feat: add NixOS module Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent) Co-authored-by: Sisyphus --- flake.nix | 26 +++--- nix/module.nix | 220 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 236 insertions(+), 10 deletions(-) create mode 100644 nix/module.nix diff --git a/flake.nix b/flake.nix index 7423734..1caa32b 100644 --- a/flake.nix +++ b/flake.nix @@ -8,6 +8,12 @@ }; outputs = { self, nixpkgs, flake-utils, git-hooks }: + { + nixosModules = { + musicfs = import ./nix/module.nix; + default = self.nixosModules.musicfs; + }; + } // flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { @@ -31,15 +37,15 @@ clippy = pkgs.clippy; }; }; - embedme = { - enable = true; - name = "embedme"; - description = "Keep README code blocks in sync with source files"; - entry = "${pkgs.nodePackages.embedme}/bin/embedme"; - args = [ "README.md" ]; - pass_filenames = false; - language = "system"; - }; + # embedme = { + # enable = true; + # name = "embedme"; + # description = "Keep README code blocks in sync with source files"; + # entry = "${pkgs.embedme}/bin/embedme"; + # args = [ "README.md" ]; + # pass_filenames = false; + # language = "system"; + # }; }; }; in { @@ -83,7 +89,7 @@ protobuf grpcurl - nodePackages.embedme + # embedme ]; }; }); diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..5770c8c --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,220 @@ +{ + 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 ]; + }; +}