diff --git a/flake.nix b/flake.nix index 96f523e..0b7b2e8 100644 --- a/flake.nix +++ b/flake.nix @@ -249,6 +249,7 @@ inputs.foundry.nixosModules.foundryvtt inputs.cfg-playground.nixosModules.default ./modules/tclip + ./modules/miniserve ./machines/dragonwell ]; services.tclip.package = inputs.tclip.packages.${pkgs.system}.tclipd; diff --git a/machines/dragonwell/caddy.nix b/machines/dragonwell/caddy.nix index 68fdeee..2ef0c23 100644 --- a/machines/dragonwell/caddy.nix +++ b/machines/dragonwell/caddy.nix @@ -80,7 +80,6 @@ in "dnd.jolheiser.com".extraConfig = '' reverse_proxy localhost:30000 ''; - }; }; } diff --git a/machines/dragonwell/default.nix b/machines/dragonwell/default.nix index 6c7d7a9..c452874 100644 --- a/machines/dragonwell/default.nix +++ b/machines/dragonwell/default.nix @@ -12,6 +12,7 @@ in ./git-pr.nix ./golink.nix ./gotosocial.nix + ./miniserve.nix ./restic.nix ./soju.nix ./tandoor.nix diff --git a/machines/dragonwell/miniserve.nix b/machines/dragonwell/miniserve.nix new file mode 100644 index 0000000..8905b13 --- /dev/null +++ b/machines/dragonwell/miniserve.nix @@ -0,0 +1,26 @@ +{ + services = { + miniserve = { + enable = true; + port = 3453; + showHidden = true; + uploadFiles = ""; + mkdir = true; + overwriteFiles = true; + enableTar = true; + enableTarGz = true; + enableZip = true; + dirsFirst = true; + title = "Files"; + hideThemeSelector = true; + hideVersionFooter = true; + readme = true; + }; + tailproxy.miniserve = { + enable = true; + hostname = "files"; + port = 3453; + authKey = "tskey-auth-kNNZJXfSDb11CNTRL-DsdZPygdA7Lrye5WJjnr6LGNffgzo3PUH"; # One-time key + }; + }; +} diff --git a/modules/miniserve/default.nix b/modules/miniserve/default.nix new file mode 100644 index 0000000..9b6fcf6 --- /dev/null +++ b/modules/miniserve/default.nix @@ -0,0 +1,438 @@ +{ + 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} = { }; + }; +}