dotnix/modules/miniserve/default.nix

439 lines
12 KiB
Nix

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