{ config, lib, pkgs, ... }: let cfg = config.services.miniserve; inherit (lib) mkEnableOption mkOption mkIf types optionalString concatMapStringsSep concatStringsSep ; in { options.services.miniserve = { enable = mkEnableOption "miniserve service"; package = mkOption { type = types.package; description = "miniserve package to use"; default = pkgs.miniserve; }; user = mkOption { type = types.str; default = "miniserve"; description = "User account for miniserve service"; }; group = mkOption { type = types.str; default = "miniserve"; description = "Group for miniserve service"; }; path = mkOption { type = types.str; default = "/var/lib/miniserve"; description = "Which path to serve"; }; port = mkOption { type = types.port; default = 8080; description = "Port to use"; }; interfaces = mkOption { type = types.listOf types.str; default = [ "127.0.0.1" ]; description = "Interface to listen on"; }; verbose = mkOption { type = types.bool; default = false; description = "Be verbose, includes emitting access logs"; }; indexFile = mkOption { type = types.nullOr types.str; default = null; description = '' The name of a directory index file to serve, like "index.html" Normally, when miniserve serves a directory, it creates a listing for that directory. However, if a directory contains this file, miniserve will serve that file instead. ''; }; spa = mkOption { type = types.bool; default = false; description = '' Activate SPA (Single Page Application) mode This will cause the file given by --index to be served for all non-existing file paths. In effect, this will serve the index file whenever a 404 would otherwise occur in order to allow the SPA router to handle the request instead. ''; }; prettyUrls = mkOption { type = types.bool; default = false; description = '' Activate Pretty URLs mode This will cause the server to serve the equivalent `.html` file indicated by the path. `/about` will try to find `about.html` and serve it. ''; }; auth = mkOption { type = types.nullOr types.str; default = null; description = '' Set authentication Currently supported formats: username:password, username:sha256:hash, username:sha512:hash (e.g. joe:123, joe:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3) ''; }; authFile = mkOption { type = types.nullOr types.path; default = null; description = '' Read authentication values from a file Example file content: joe:123 bob:sha256:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3 bill: ''; }; routePrefix = mkOption { type = types.nullOr types.str; default = null; description = "Use a specific route prefix"; }; randomRoute = mkOption { type = types.bool; default = false; description = "Generate a random 6-hexdigit route"; }; hideSymlinks = mkOption { type = types.bool; default = false; description = "Hide symlinks in listing and prevent them from being followed"; }; showHidden = mkOption { type = types.bool; default = false; description = "Show hidden files"; }; sortingMethod = mkOption { type = types.enum [ "name" "size" "date" ]; default = "name"; description = '' Default sorting method for file list Possible values: - name: Sort by name - size: Sort by size - date: Sort by last modification date (natural sort: follows alphanumerical order) ''; }; sortingOrder = mkOption { type = types.enum [ "asc" "desc" ]; default = "desc"; description = '' Default sorting order for file list Possible values: - asc: Ascending order - desc: Descending order ''; }; colorScheme = mkOption { type = types.enum [ "squirrel" "archlinux" "zenburn" "monokai" ]; default = "squirrel"; description = '' Default color scheme Possible values: squirrel, archlinux, zenburn, monokai ''; }; colorSchemeDark = mkOption { type = types.enum [ "squirrel" "archlinux" "zenburn" "monokai" ]; default = "archlinux"; description = '' Default color scheme Possible values: squirrel, archlinux, zenburn, monokai ''; }; qrcode = mkOption { type = types.bool; default = false; description = "Enable QR code display"; }; uploadFiles = mkOption { type = types.nullOr types.str; default = null; description = "Enable file uploading (and optionally specify for which directory)"; }; mkdir = mkOption { type = types.bool; default = false; description = "Enable creating directories"; }; mediaType = mkOption { type = types.nullOr ( types.enum [ "image" "audio" "video" ] ); default = null; description = '' Specify uploadable media types Possible values: image, audio, video ''; }; rawMediaType = mkOption { type = types.nullOr types.str; default = null; description = "Directly specify the uploadable media type expression"; }; overwriteFiles = mkOption { type = types.bool; default = false; description = "Enable overriding existing files during file upload"; }; enableTar = mkOption { type = types.bool; default = false; description = "Enable uncompressed tar archive generation"; }; enableTarGz = mkOption { type = types.bool; default = false; description = "Enable gz-compressed tar archive generation"; }; enableZip = mkOption { type = types.bool; default = false; description = '' Enable zip archive generation WARNING: Zipping large directories can result in out-of-memory exception because zip generation is done in memory and cannot be sent on the fly ''; }; compressResponse = mkOption { type = types.bool; default = false; description = '' Compress response WARNING: Enabling this option may slow down transfers due to CPU overhead, so it is disabled by default. Only enable this option if you know that your users have slow connections or if you want to minimize your server's bandwidth usage. ''; }; dirsFirst = mkOption { type = types.bool; default = false; description = "List directories first"; }; title = mkOption { type = types.nullOr types.str; default = null; description = "Shown instead of host in page title and heading"; }; headers = mkOption { type = types.listOf types.str; default = [ ]; description = '' Inserts custom headers into the responses. Specify each header as a 'Header:Value' pair. This parameter can be used multiple times to add multiple headers. Example: --header "Header1:Value1" --header "Header2:Value2" (If a header is already set or previously inserted, it will not be overwritten.) ''; }; showSymlinkInfo = mkOption { type = types.bool; default = false; description = "Visualize symlinks in directory listing"; }; hideVersionFooter = mkOption { type = types.bool; default = false; description = "Hide version footer"; }; hideThemeSelector = mkOption { type = types.bool; default = false; description = "Hide theme selector"; }; showWgetFooter = mkOption { type = types.bool; default = false; description = "If enabled, display a wget command to recursively download the current directory"; }; tlsCert = mkOption { type = types.nullOr types.path; default = null; description = "TLS certificate to use"; }; tlsKey = mkOption { type = types.nullOr types.path; default = null; description = "TLS private key to use"; }; readme = mkOption { type = types.bool; default = false; description = "Enable README.md rendering in directories"; }; disableIndexing = mkOption { type = types.bool; default = false; description = '' Disable indexing This will prevent directory listings from being generated and return an error instead. ''; }; }; config = mkIf cfg.enable { systemd.services.miniserve = { description = "Miniserve File Server"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = { ExecStart = let args = [ (optionalString cfg.verbose "-v") (optionalString (cfg.indexFile != null) "--index '${cfg.indexFile}'") (optionalString cfg.spa "--spa") (optionalString cfg.prettyUrls "--pretty-urls") "-p ${toString cfg.port}" (concatMapStringsSep " " (i: "-i ${i}") cfg.interfaces) (optionalString (cfg.auth != null) "-a '${cfg.auth}'") (optionalString (cfg.authFile != null) "--auth-file ${cfg.authFile}") (optionalString (cfg.routePrefix != null) "--route-prefix '${cfg.routePrefix}'") (optionalString cfg.randomRoute "--random-route") (optionalString cfg.hideSymlinks "-P") (optionalString cfg.showHidden "-H") "-S ${cfg.sortingMethod}" "-O ${cfg.sortingOrder}" "-c ${cfg.colorScheme}" "-d ${cfg.colorSchemeDark}" (optionalString cfg.qrcode "-q") (optionalString (cfg.uploadFiles != null) ( if (cfg.uploadFiles != "") then "-u '${cfg.uploadFiles}'" else "-u" )) (optionalString cfg.mkdir "-U") (optionalString (cfg.mediaType != null) "-m ${cfg.mediaType}") (optionalString (cfg.rawMediaType != null) "-M '${cfg.rawMediaType}'") (optionalString cfg.overwriteFiles "-o") (optionalString cfg.enableTar "-r") (optionalString cfg.enableTarGz "-g") (optionalString cfg.enableZip "-z") (optionalString cfg.compressResponse "-C") (optionalString cfg.dirsFirst "-D") (optionalString (cfg.title != null) "-t '${cfg.title}'") (concatMapStringsSep " " (h: "--header '${h}'") cfg.headers) (optionalString cfg.showSymlinkInfo "-l") (optionalString cfg.hideVersionFooter "-F") (optionalString cfg.hideThemeSelector "--hide-theme-selector") (optionalString cfg.showWgetFooter "-W") (optionalString (cfg.tlsCert != null) "--tls-cert ${cfg.tlsCert}") (optionalString (cfg.tlsKey != null) "--tls-key ${cfg.tlsKey}") (optionalString cfg.readme "--readme") (optionalString cfg.disableIndexing "-I") cfg.path ]; in "${pkgs.miniserve}/bin/miniserve ${concatStringsSep " " args}"; Restart = "on-failure"; User = cfg.user; Group = cfg.group; }; }; users.users.${cfg.user} = { group = cfg.group; home = cfg.path; createHome = true; isSystemUser = true; isNormalUser = false; }; users.groups.${cfg.group} = { }; }; }