Files
364039cf28 nixos/pdfding: init
Co-authored-by: Sandro <sandro.jaeckel@gmail.com>
Co-Authored-By: eljamm <fedi.jamoussi@protonmail.ch>
Signed-off-by: phanirithvij <phanirithvij2000@gmail.com>
2026-02-10 12:05:41 +05:30

452 lines
13 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
concatStringsSep
mapAttrsToList
mkEnableOption
mkPackageOption
mkOption
optionalAttrs
optionalString
types
;
cfg = config.services.pdfding;
stateDir = "/var/lib/pdfding";
usePostgres = cfg.database.type == "postgres";
envVars = {
# HOST_IP is used in the package derivation
HOST_IP = cfg.hostName;
HOST_NAME = concatStringsSep "," cfg.allowedHosts;
HOST_PORT = toString cfg.port;
DATABASE_TYPE = "";
DATA_DIR = stateDir;
}
// optionalAttrs usePostgres {
DATABASE_TYPE = "POSTGRES";
POSTGRES_PORT = toString cfg.database.port;
# Django Uses the unix domain socket
# if host is set to empty see https://docs.djangoproject.com/en/6.0/ref/settings/#host
POSTGRES_HOST = lib.optionalString (!cfg.database.createLocally) cfg.database.host;
POSTGRES_NAME = cfg.database.name;
POSTGRES_USER = cfg.database.user;
}
// optionalAttrs cfg.consume.enable {
CONSUME_ENABLE = "TRUE";
CONSUME_SCHEDULE = cfg.consume.schedule;
}
// optionalAttrs cfg.backup.enable {
BACKUP_ENABLE = "TRUE";
BACKUP_ENDPOINT = cfg.backup.endpoint;
BACKUP_SCHEDULE = cfg.backup.schedule;
}
// cfg.extraEnvironment;
envFile = pkgs.writeText "pdfding.env" (
lib.pipe envVars [
(mapAttrsToList (name: value: "${name}=\"${toString value}\""))
(concatStringsSep "\n")
]
);
loadCreds =
optionalString (usePostgres && !cfg.database.createLocally) ''
export POSTGRES_PASSWORD="$(<${cfg.database.passwordFile})"
''
+ ''
export SECRET_KEY="$(<${cfg.secretKeyFile})"
'';
secretRecommendation = "Consider using a secret managing scheme such as `agenix` or `sops-nix` to generate this file.";
in
{
options.services.pdfding = {
enable = mkEnableOption "PdfDing service" // {
description = ''
Whether to enable PdfDing service.
To use the pdfding-manage CLI, add your user to the pdfding group:
users.users.<youruser>.extraGroups = [ "pdfding" ];
'';
};
package = mkPackageOption pkgs "pdfding" { };
user = mkOption {
type = types.str;
default = "pdfding";
description = "User account under which PdfDing runs";
};
group = mkOption {
type = types.str;
default = "pdfding";
description = "Group under which PdfDing runs";
};
hostName = mkOption {
type = types.str;
default = "0.0.0.0";
example = "pdfding.example.com";
description = "Listen address for PdfDing";
};
port = mkOption {
type = types.port;
default = 8000;
description = "Port on which PdfDing listens";
};
allowedHosts = mkOption {
type = types.listOf types.str;
default = [
"127.0.0.1"
"localhost"
];
description = "Domains where PdfDing is allowed to run";
};
gunicorn.extraArgs = mkOption {
type = types.listOf types.str;
description = "Command line arguments passed to Gunicorn server.";
default = [ ];
};
extraEnvironment = mkOption {
type = types.attrsOf types.str;
default = { };
description = "Additional environment variables";
};
envFiles = mkOption {
type = types.listOf types.path;
description = "Environment variable files";
default = [ ];
};
secretKeyFile = mkOption {
type = types.path;
default = null;
description = "File containing the Django SECRET_KEY. ${secretRecommendation}";
example = "/run/secrets/pdfding-secret-key";
};
database = {
type = mkOption {
type = types.enum [
"sqlite"
"postgres"
];
default = "sqlite";
description = "Database type to use";
};
host = mkOption {
type = types.str;
default = "";
description = "PostgreSQL host";
};
port = mkOption {
type = types.port;
default = 5432;
description = "PostgreSQL port";
};
name = mkOption {
type = types.str;
default = "pdfding";
description = "PostgreSQL database name";
};
user = mkOption {
type = types.str;
default = "pdfding";
description = "PostgreSQL user";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "File containing POSTGRES_PASSWORD. ${secretRecommendation}";
example = "/run/secrets/pdfding-db-password";
};
createLocally = mkOption {
type = types.bool;
default = false;
description = "Whether to create a local PostgreSQL database automatically";
};
};
consume = {
enable = mkEnableOption "Consume functionality" // {
description = ''
Bulk PDF import from consume directory.
When enabled, administrators can create per-user directories like /var/lib/pdfding/consume/<user_id>
with permissions allowing the pdfding user to read and write.
PDFs placed in these directories are automatically imported into user accounts.
PDFs are imported periodically via cronjob and successfully imported files
are automatically deleted from the consume directory.
'';
};
schedule = mkOption {
type = types.str;
default = "*/5 * * * *";
description = ''
The cron schedule for the consume task to trigger.
The format is "minute hour day month day_of_week"
Read
- https://github.com/mrmn2/PdfDing/blob/d0f21ec2f9fbee4b1a2f6b7e0e6c7ea7784ab1bc/pdfding/base/task_helpers.py#L5
- https://huey.readthedocs.io/en/latest/api.html#crontab
'';
};
};
backup = {
enable = mkEnableOption "Backup functionality" // {
description = ''
Automatic backup of important data to a AWS S3 (or compatible) instance.
When enabled and properly configured via environment variables,
important data is periodically uploaded to the specified s3
instance via cronjob.
'';
};
schedule = mkOption {
type = types.str;
default = "0 2 * * *";
description = ''
The cron schedule for the consume task to trigger.
The format is "minute hour day month day_of_week"
Read
- https://github.com/mrmn2/PdfDing/blob/d0f21ec2f9fbee4b1a2f6b7e0e6c7ea7784ab1bc/pdfding/base/task_helpers.py#L5
- https://huey.readthedocs.io/en/latest/api.html#crontab
'';
};
endpoint = mkOption {
type = types.nullOr types.str;
default = null;
description = "The s3 endpoint for backups";
example = "127.0.0.1:9000";
};
};
openFirewall = lib.mkOption {
type = types.bool;
default = false;
description = "Open ports in the firewall for the PdfDing web interface.";
};
installTestHelpers = mkOption {
type = types.bool;
default = false;
internal = true;
description = "Adds a few helper commands to systemPackages for nixos tests";
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.secretKeyFile != null;
message = "services.pdfding.secretKeyFile must be set when using PdfDing";
}
{
assertion = cfg.backup.enable -> envVars.BACKUP_ENDPOINT != null;
message = "services.pdfding.extraEnvironment.BACKUP_ENDPOINT must be set when backup is enabled";
}
{
assertion = cfg.database.createLocally -> usePostgres;
message = "services.pdfding.database.createLocally is enabled but not database.type is not postgres";
}
{
assertion = cfg.database.createLocally -> cfg.database.host == "";
message = "services.pdfding.database.host must be empty when services.pdfding.database.createLocally is enabled";
}
{
assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
message = "specifying services.pdfding.database.passwordFile is not supported when used along with a local db setup";
}
{
assertion =
cfg.database.createLocally
-> cfg.database.user == cfg.user && cfg.database.user == cfg.database.name;
message = "services.pdfding.database.user should be the same as services.pdfding.user as well as services.pdfding.database.name when running a local db setup";
}
];
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ];
};
environment.systemPackages =
let
genWrapper =
name: cmd:
pkgs.writeShellScriptBin name ''
set -eou pipefail
set -a
${lib.toShellVars cfg.extraEnvironment}
${lib.concatMapStringsSep "\n" (f: "source ${f}") cfg.envFiles}
set +a
${loadCreds}
sudo=exec
if [[ "$USER" != ${cfg.user} ]]; then
sudo='${config.security.wrapperDir}/sudo -E -u ${cfg.user}'
fi
${cmd}
'';
commands.pdfding-manage = ''
$sudo ${lib.getExe cfg.package} "$@"
'';
commands.consume-immediate = ''
echo "from pdf.tasks import consume_function; consume_function(True)" | \
$sudo ${lib.getExe cfg.package} shell
'';
commands.backup-immediate = ''
echo "from backup.tasks import backup_function; backup_function()" | \
$sudo ${lib.getExe cfg.package} shell
'';
packages = lib.genAttrs (lib.attrNames commands) (name: genWrapper name commands.${name});
in
lib.mkMerge [
[
packages.pdfding-manage
]
(lib.mkIf cfg.installTestHelpers [
packages.consume-immediate
packages.backup-immediate
])
];
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
};
users.groups.${cfg.group} = { };
services.pdfding.envFiles = [ envFile ];
services.pdfding.extraEnvironment = {
DEFAULT_THEME = "dark";
DEFAULT_THEME_COLOR = "green";
};
services.pdfding.gunicorn.extraArgs = [
"--workers=4"
"--max-requests=1200"
"--max-requests-jitter=50"
"--log-level=error"
];
systemd.services.pdfding = {
description = "PdfDing Web Service";
after = [
"network.target"
]
++ lib.optionals (usePostgres && cfg.database.createLocally) [ "postgresql.target" ];
wantedBy = [ "multi-user.target" ];
preStart = ''
${loadCreds}
${optionalString (usePostgres && cfg.database.createLocally)
# bash
''
count=0
timeout=30
until ${pkgs.postgresql}/bin/pg_isready -p ${toString cfg.database.port}; do
if [ $count -ge $timeout ]; then
echo "Timed out waiting for PostgreSQL after $timeout seconds."
exit 1
fi
echo "Waiting for PostgreSQL... ($count/$timeout)"
sleep 1
count=$((count+1))
done
''
}
${cfg.package}/bin/pdfding-manage migrate
${cfg.package}/bin/pdfding-manage clean_up
'';
script = ''
${loadCreds}
exec ${cfg.package}/bin/pdfding-start ${toString cfg.gunicorn.extraArgs}
'';
serviceConfig = {
Type = "exec";
User = cfg.user;
Group = cfg.group;
EnvironmentFile = cfg.envFiles;
StateDirectory = [
"pdfding"
"pdfding/db"
"pdfding/media"
]
++ lib.optional cfg.consume.enable "pdfding/consume";
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectSystem = "strict";
ProtectHome = true;
Restart = "on-failure";
RestartSec = "5s";
};
};
systemd.services.pdfding-background = lib.mkIf (cfg.consume.enable || cfg.backup.enable) {
description = "PdfDing Background Tasks (Huey)";
after = [ "pdfding.service" ];
wantedBy = [ "multi-user.target" ];
script = ''
${loadCreds}
exec ${cfg.package}/bin/pdfding-manage run_huey
'';
serviceConfig = {
Type = "exec";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = stateDir;
EnvironmentFile = cfg.envFiles;
NoNewPrivileges = true;
PrivateTmp = true;
PrivateDevices = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ stateDir ];
Restart = "on-failure";
RestartSec = "5s";
TimeoutStopSec = 30;
};
};
services.postgresql = lib.mkIf cfg.database.createLocally {
enable = true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [
{
name = cfg.database.user;
ensureDBOwnership = true;
}
];
};
};
meta.maintainers = lib.teams.ngi.members;
}