mirror of
https://github.com/9001/copyparty.git
synced 2025-08-16 08:32:13 -06:00
* nix: allow passing extra packages in PATH * nix: allow passing extra python packages I wanted to use https://github.com/9001/copyparty/blob/hovudstraum/bin/hooks/notify.py but that wasn't really possible without this under the nix package. * nix: format all nix files with nixfmt * nix: reduce redundancy in the package For readability * nix: remove unused pyftpdlib import * nix: put makeWrapper into the correct inputs * nix: fill out all of meta * nix: set formatter in flake for nix files This allows contributors to format their nix changes with the `nix fmt` command. * nix: add u2c * nix: add partyfuse One downside of the way the nix ecosystem works is that MacFUSE needs to be installed manually. Luckily the script tells you that already! * nix: add missing cfssl import * nix: add flake check that makes sure it builds with all flags Because sometimes an import might be missing, and if it is an optional then you'll only figure out that it's broken if you set the flag. * nix: use correct overlay argument names Or `nix flake check` will refuse to run the copyparty-full check
376 lines
12 KiB
Nix
376 lines
12 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
...
|
|
}:
|
|
with lib;
|
|
let
|
|
mkKeyValue =
|
|
key: value:
|
|
if value == true then
|
|
# sets with a true boolean value are coerced to just the key name
|
|
key
|
|
else if value == false then
|
|
# or omitted completely when false
|
|
""
|
|
else
|
|
(generators.mkKeyValueDefault { inherit mkValueString; } ": " key value);
|
|
|
|
mkAttrsString = value: (generators.toKeyValue { inherit mkKeyValue; } value);
|
|
|
|
mkValueString =
|
|
value:
|
|
if isList value then
|
|
(concatStringsSep ", " (map mkValueString value))
|
|
else if isAttrs value then
|
|
"\n" + (mkAttrsString value)
|
|
else
|
|
(generators.mkValueStringDefault { } value);
|
|
|
|
mkSectionName = value: "[" + (escape [ "[" "]" ] value) + "]";
|
|
|
|
mkSection = name: attrs: ''
|
|
${mkSectionName name}
|
|
${mkAttrsString attrs}
|
|
'';
|
|
|
|
mkVolume = name: attrs: ''
|
|
${mkSectionName name}
|
|
${attrs.path}
|
|
${mkAttrsString {
|
|
accs = attrs.access;
|
|
flags = attrs.flags;
|
|
}}
|
|
'';
|
|
|
|
passwordPlaceholder = name: "{{password-${name}}}";
|
|
|
|
accountsWithPlaceholders = mapAttrs (name: attrs: passwordPlaceholder name);
|
|
|
|
configStr = ''
|
|
${mkSection "global" cfg.settings}
|
|
${mkSection "accounts" (accountsWithPlaceholders cfg.accounts)}
|
|
${concatStringsSep "\n" (mapAttrsToList mkVolume cfg.volumes)}
|
|
'';
|
|
|
|
cfg = config.services.copyparty;
|
|
configFile = pkgs.writeText "copyparty.conf" configStr;
|
|
runtimeConfigPath = "/run/copyparty/copyparty.conf";
|
|
externalCacheDir = "/var/cache/copyparty";
|
|
externalStateDir = "/var/lib/copyparty";
|
|
defaultShareDir = "${externalStateDir}/data";
|
|
in
|
|
{
|
|
options.services.copyparty = {
|
|
enable = mkEnableOption "web-based file manager";
|
|
|
|
package = mkOption {
|
|
type = types.package;
|
|
default = pkgs.copyparty;
|
|
defaultText = "pkgs.copyparty";
|
|
description = ''
|
|
Package of the application to run, exposed for overriding purposes.
|
|
'';
|
|
};
|
|
|
|
mkHashWrapper = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
description = ''
|
|
Make a shell script wrapper called 'copyparty-hash' with all options set here,
|
|
that launches the hashing cli.
|
|
'';
|
|
};
|
|
|
|
user = mkOption {
|
|
type = types.str;
|
|
default = "copyparty";
|
|
description = ''
|
|
The user that copyparty will run under.
|
|
|
|
If changed from default, you are responsible for making sure the user exists.
|
|
'';
|
|
};
|
|
|
|
group = mkOption {
|
|
type = types.str;
|
|
default = "copyparty";
|
|
description = ''
|
|
The group that copyparty will run under.
|
|
|
|
If changed from default, you are responsible for making sure the user exists.
|
|
'';
|
|
};
|
|
|
|
openFilesLimit = mkOption {
|
|
default = 4096;
|
|
type = types.either types.int types.str;
|
|
description = "Number of files to allow copyparty to open.";
|
|
};
|
|
|
|
settings = mkOption {
|
|
type = types.attrs;
|
|
description = ''
|
|
Global settings to apply.
|
|
Directly maps to values in the [global] section of the copyparty config.
|
|
Cannot set "c" or "hist", those are set by this module.
|
|
See `${getExe cfg.package} --help` for more details.
|
|
'';
|
|
default = {
|
|
i = "127.0.0.1";
|
|
no-reload = true;
|
|
hist = externalCacheDir;
|
|
};
|
|
example = literalExpression ''
|
|
{
|
|
i = "0.0.0.0";
|
|
no-reload = true;
|
|
hist = ${externalCacheDir};
|
|
}
|
|
'';
|
|
};
|
|
|
|
accounts = mkOption {
|
|
type = types.attrsOf (
|
|
types.submodule (
|
|
{ ... }:
|
|
{
|
|
options = {
|
|
passwordFile = mkOption {
|
|
type = types.str;
|
|
description = ''
|
|
Runtime file path to a file containing the user password.
|
|
Must be readable by the copyparty user.
|
|
'';
|
|
example = "/run/keys/copyparty/ed";
|
|
};
|
|
};
|
|
}
|
|
)
|
|
);
|
|
description = ''
|
|
A set of copyparty accounts to create.
|
|
'';
|
|
default = { };
|
|
example = literalExpression ''
|
|
{
|
|
ed.passwordFile = "/run/keys/copyparty/ed";
|
|
};
|
|
'';
|
|
};
|
|
|
|
volumes = mkOption {
|
|
type = types.attrsOf (
|
|
types.submodule (
|
|
{ ... }:
|
|
{
|
|
options = {
|
|
path = mkOption {
|
|
type = types.path;
|
|
description = ''
|
|
Path of a directory to share.
|
|
'';
|
|
};
|
|
access = mkOption {
|
|
type = types.attrs;
|
|
description = ''
|
|
Attribute list of permissions and the users to apply them to.
|
|
|
|
The key must be a string containing any combination of allowed permission:
|
|
"r" (read): list folder contents, download files
|
|
"w" (write): upload files; need "r" to see the uploads
|
|
"m" (move): move files and folders; need "w" at destination
|
|
"d" (delete): permanently delete files and folders
|
|
"g" (get): download files, but cannot see folder contents
|
|
"G" (upget): "get", but can see filekeys of their own uploads
|
|
"h" (html): "get", but folders return their index.html
|
|
"a" (admin): can see uploader IPs, config-reload
|
|
|
|
For example: "rwmd"
|
|
|
|
The value must be one of:
|
|
an account name, defined in `accounts`
|
|
a list of account names
|
|
"*", which means "any account"
|
|
'';
|
|
example = literalExpression ''
|
|
{
|
|
# wG = write-upget = see your own uploads only
|
|
wG = "*";
|
|
# read-write-modify-delete for users "ed" and "k"
|
|
rwmd = ["ed" "k"];
|
|
};
|
|
'';
|
|
};
|
|
flags = mkOption {
|
|
type = types.attrs;
|
|
description = ''
|
|
Attribute list of volume flags to apply.
|
|
See `${getExe cfg.package} --help-flags` for more details.
|
|
'';
|
|
example = literalExpression ''
|
|
{
|
|
# "fk" enables filekeys (necessary for upget permission) (4 chars long)
|
|
fk = 4;
|
|
# scan for new files every 60sec
|
|
scan = 60;
|
|
# volflag "e2d" enables the uploads database
|
|
e2d = true;
|
|
# "d2t" disables multimedia parsers (in case the uploads are malicious)
|
|
d2t = true;
|
|
# skips hashing file contents if path matches *.iso
|
|
nohash = "\.iso$";
|
|
};
|
|
'';
|
|
default = { };
|
|
};
|
|
};
|
|
}
|
|
)
|
|
);
|
|
description = "A set of copyparty volumes to create";
|
|
default = {
|
|
"/" = {
|
|
path = defaultShareDir;
|
|
access = {
|
|
r = "*";
|
|
};
|
|
};
|
|
};
|
|
example = literalExpression ''
|
|
{
|
|
"/" = {
|
|
path = ${defaultShareDir};
|
|
access = {
|
|
# wG = write-upget = see your own uploads only
|
|
wG = "*";
|
|
# read-write-modify-delete for users "ed" and "k"
|
|
rwmd = ["ed" "k"];
|
|
};
|
|
};
|
|
};
|
|
'';
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable (
|
|
let
|
|
command = "${getExe cfg.package} -c ${runtimeConfigPath}";
|
|
in
|
|
{
|
|
systemd.services.copyparty = {
|
|
description = "http file sharing hub";
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
environment = {
|
|
PYTHONUNBUFFERED = "true";
|
|
XDG_CONFIG_HOME = externalStateDir;
|
|
};
|
|
|
|
preStart =
|
|
let
|
|
replaceSecretCommand =
|
|
name: attrs:
|
|
"${getExe pkgs.replace-secret} '${passwordPlaceholder name}' '${attrs.passwordFile}' ${runtimeConfigPath}";
|
|
in
|
|
''
|
|
set -euo pipefail
|
|
install -m 600 ${configFile} ${runtimeConfigPath}
|
|
${concatStringsSep "\n" (mapAttrsToList replaceSecretCommand cfg.accounts)}
|
|
'';
|
|
|
|
serviceConfig = {
|
|
Type = "simple";
|
|
ExecStart = command;
|
|
# Hardening options
|
|
User = cfg.user;
|
|
Group = cfg.group;
|
|
RuntimeDirectory = [ "copyparty" ];
|
|
RuntimeDirectoryMode = "0700";
|
|
StateDirectory = [ "copyparty" ];
|
|
StateDirectoryMode = "0700";
|
|
CacheDirectory = lib.mkIf (cfg.settings ? hist) [ "copyparty" ];
|
|
CacheDirectoryMode = lib.mkIf (cfg.settings ? hist) "0700";
|
|
WorkingDirectory = externalStateDir;
|
|
BindReadOnlyPaths = [
|
|
"/nix/store"
|
|
"-/etc/resolv.conf"
|
|
"-/etc/nsswitch.conf"
|
|
"-/etc/group"
|
|
"-/etc/hosts"
|
|
"-/etc/localtime"
|
|
] ++ (mapAttrsToList (k: v: "-${v.passwordFile}") cfg.accounts);
|
|
BindPaths =
|
|
(if cfg.settings ? hist then [ cfg.settings.hist ] else [ ])
|
|
++ [ externalStateDir ]
|
|
++ (mapAttrsToList (k: v: v.path) cfg.volumes);
|
|
# ProtectSystem = "strict";
|
|
# Note that unlike what 'ro' implies,
|
|
# this actually makes it impossible to read anything in the root FS,
|
|
# except for things explicitly mounted via `RuntimeDirectory`, `StateDirectory`, `CacheDirectory`, and `BindReadOnlyPaths`.
|
|
# This is because TemporaryFileSystem creates a *new* *empty* filesystem for the process, so only bindmounts are visible.
|
|
TemporaryFileSystem = "/:ro";
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectControlGroups = true;
|
|
RestrictSUIDSGID = true;
|
|
PrivateMounts = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelLogs = true;
|
|
ProtectHostname = true;
|
|
ProtectClock = true;
|
|
ProtectProc = "invisible";
|
|
ProcSubset = "pid";
|
|
RestrictNamespaces = true;
|
|
RemoveIPC = true;
|
|
UMask = "0077";
|
|
LimitNOFILE = cfg.openFilesLimit;
|
|
NoNewPrivileges = true;
|
|
LockPersonality = true;
|
|
RestrictRealtime = true;
|
|
MemoryDenyWriteExecute = true;
|
|
};
|
|
};
|
|
|
|
# ensure volumes exist:
|
|
systemd.tmpfiles.settings."copyparty" = (
|
|
lib.attrsets.mapAttrs' (
|
|
name: value:
|
|
lib.attrsets.nameValuePair (value.path) {
|
|
d = {
|
|
#: in front of things means it wont change it if the directory already exists.
|
|
group = ":${cfg.group}";
|
|
user = ":${cfg.user}";
|
|
mode = ":755";
|
|
};
|
|
}
|
|
) cfg.volumes
|
|
);
|
|
|
|
users.groups.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") { };
|
|
users.users.copyparty = lib.mkIf (cfg.user == "copyparty" && cfg.group == "copyparty") {
|
|
description = "Service user for copyparty";
|
|
group = "copyparty";
|
|
home = externalStateDir;
|
|
isSystemUser = true;
|
|
};
|
|
environment.systemPackages = lib.mkIf cfg.mkHashWrapper [
|
|
(pkgs.writeShellScriptBin "copyparty-hash" ''
|
|
set -a # automatically export variables
|
|
# set same environment variables as the systemd service
|
|
${lib.pipe config.systemd.services.copyparty.environment [
|
|
(lib.filterAttrs (n: v: v != null && n != "PATH"))
|
|
(lib.mapAttrs (_: v: "${v}"))
|
|
(lib.toShellVars)
|
|
]}
|
|
PATH=${config.systemd.services.copyparty.environment.PATH}:$PATH
|
|
|
|
exec ${command} --ah-cli
|
|
'')
|
|
];
|
|
}
|
|
);
|
|
}
|