diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 7e1127ba6cb11..23fe9774237a8 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -34,6 +34,8 @@ - [realm](https://github.com/zhboner/realm), a simple, high performance relay server written in rust. Available as [services.realm.enable](#opt-services.realm.enable). +- [Gotenberg](https://gotenberg.dev), an API server for converting files to PDFs that can be used alongside Paperless-ngx. Available as [services.gotenberg](options.html#opt-services.gotenberg). + - [Playerctld](https://github.com/altdesktop/playerctl), a daemon to track media player activity. Available as [services.playerctld](option.html#opt-services.playerctld). - [Glance](https://github.com/glanceapp/glance), a self-hosted dashboard that puts all your feeds in one place. Available as [services.glance](option.html#opt-services.glance). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 3019b23bc870c..a2fe394ba607f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -741,6 +741,7 @@ ./services/misc/gitweb.nix ./services/misc/gogs.nix ./services/misc/gollum.nix + ./services/misc/gotenberg.nix ./services/misc/gpsd.nix ./services/misc/graphical-desktop.nix ./services/misc/greenclip.nix diff --git a/nixos/modules/services/misc/gotenberg.nix b/nixos/modules/services/misc/gotenberg.nix new file mode 100644 index 0000000000000..57932c656d632 --- /dev/null +++ b/nixos/modules/services/misc/gotenberg.nix @@ -0,0 +1,258 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.gotenberg; + + args = + [ + "--api-port=${toString cfg.port}" + "--api-timeout=${cfg.timeout}" + "--api-root-path=${cfg.rootPath}" + "--log-level=${cfg.logLevel}" + "--chromium-max-queue-size=${toString cfg.chromium.maxQueueSize}" + "--libreoffice-restart-after=${toString cfg.libreoffice.restartAfter}" + "--libreoffice-max-queue-size=${toString cfg.libreoffice.maxQueueSize}" + "--pdfengines-engines=${lib.concatStringsSep "," cfg.pdfEngines}" + ] + ++ optional cfg.enableBasicAuth "--api-enable-basic-auth" + ++ optional cfg.chromium.autoStart "--chromium-auto-start" + ++ optional cfg.chromium.disableJavascript "--chromium-disable-javascript" + ++ optional cfg.chromium.disableRoutes "--chromium-disable-routes" + ++ optional cfg.libreoffice.autoStart "--libreoffice-auto-start" + ++ optional cfg.libreoffice.disableRoutes "--libreoffice-disable-routes"; + + inherit (lib) + mkEnableOption + mkPackageOption + mkOption + types + mkIf + optional + optionalAttrs + ; +in +{ + options = { + services.gotenberg = { + enable = mkEnableOption "Gotenberg, a stateless API for PDF files"; + + # Users can override only gotenberg, libreoffice and chromium if they want to (eg. ungoogled-chromium, different LO version, etc) + # Don't allow setting the qpdf, pdftk, or unoconv paths, as those are very stable + # and there's only one version of each. + package = mkPackageOption pkgs "gotenberg" { }; + + port = mkOption { + type = types.port; + default = 3000; + description = "Port on which the API should listen."; + }; + + timeout = mkOption { + type = types.nullOr types.str; + default = "30s"; + description = "Timeout for API requests."; + }; + + rootPath = mkOption { + type = types.str; + default = "/"; + description = "Root path for the Gotenberg API."; + }; + + enableBasicAuth = mkOption { + type = types.bool; + default = false; + description = '' + HTTP Basic Authentication. + + If you set this, be sure to set `GOTENBERG_API_BASIC_AUTH_USERNAME`and `GOTENBERG_API_BASIC_AUTH_PASSWORD` + in your `services.gotenberg.environmentFile` file. + ''; + }; + + extraFontPackages = mkOption { + type = types.listOf types.package; + default = [ ]; + description = "Extra fonts to make available."; + }; + + chromium = { + package = mkPackageOption pkgs "chromium" { }; + + maxQueueSize = mkOption { + type = types.int; + default = 0; + description = "Maximum queue size for chromium-based conversions. Setting to 0 disables the limit."; + }; + + autoStart = mkOption { + type = types.bool; + default = false; + description = "Automatically start chromium when Gotenberg starts. If false, Chromium will start on the first conversion request that uses it."; + }; + + disableJavascript = mkOption { + type = types.bool; + default = false; + description = "Disable Javascript execution."; + }; + + disableRoutes = mkOption { + type = types.bool; + default = false; + description = "Disable all routes allowing Chromium-based conversion."; + }; + }; + + libreoffice = { + package = mkPackageOption pkgs "libreoffice" { }; + + restartAfter = mkOption { + type = types.int; + default = 10; + description = "Restart LibreOffice after this many conversions. Setting to 0 disables this feature."; + }; + + maxQueueSize = mkOption { + type = types.int; + default = 0; + description = "Maximum queue size for LibreOffice-based conversions. Setting to 0 disables the limit."; + }; + + autoStart = mkOption { + type = types.bool; + default = false; + description = "Automatically start LibreOffice when Gotenberg starts. If false, Chromium will start on the first conversion request that uses it."; + }; + + disableRoutes = mkOption { + type = types.bool; + default = false; + description = "Disable all routes allowing LibreOffice-based conversion."; + }; + }; + + pdfEngines = mkOption { + type = types.listOf ( + types.enum [ + "pdftk" + "qpdf" + "libreoffice-pdfengine" + "exiftool" + "pdfcpu" + ] + ); + default = [ + "pdftk" + "qpdf" + "libreoffice-pdfengine" + "exiftool" + "pdfcpu" + ]; + description = '' + PDF engines to enable. Each one can be used to perform a specific task. + See [the documentation](https://gotenberg.dev/docs/configuration#pdf-engines) for more details. + Defaults to all possible PDF engines. + ''; + }; + + logLevel = mkOption { + type = types.enum [ + "error" + "warn" + "info" + "debug" + ]; + default = "info"; + description = "The logging level for Gotenberg."; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Environment file to load extra environment variables from."; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Any extra command-line flags to pass to the Gotenberg service."; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.enableBasicAuth -> cfg.environmentFile != null; + message = '' + When enabling HTTP Basic Authentication with `services.gotenberg.enableBasicAuth`, + you must provide an environment file via `services.gotenberg.environmentFile` with the appropriate environment variables set in it. + + See `services.gotenberg.enableBasicAuth` for the names of those variables. + ''; + } + ]; + + systemd.services.gotenberg = { + description = "Gotenberg API server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + path = [ cfg.package ]; + environment = { + LIBREOFFICE_BIN_PATH = "${cfg.libreoffice.package}/lib/libreoffice/program/soffice.bin"; + CHROMIUM_BIN_PATH = lib.getExe cfg.chromium.package; + FONTCONFIG_FILE = pkgs.makeFontsConf { + fontDirectories = [ pkgs.liberation_ttf_v2 ] ++ cfg.extraFontPackages; + }; + }; + serviceConfig = { + Type = "simple"; + DynamicUser = true; + ExecStart = "${lib.getExe cfg.package} ${lib.escapeShellArgs args}"; + + # Hardening options + PrivateDevices = true; + PrivateIPC = true; + PrivateUsers = true; + + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + + LockPersonality = true; + MemoryDenyWriteExecute = true; + + SystemCallFilter = [ + "@system-service" + "~@resources" + "~@privileged" + ]; + SystemCallArchitectures = "native"; + + UMask = 77; + } // optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = cfg.environmentFile; }; + }; + }; + + meta.maintainers = with lib.maintainers; [ pyrox0 ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index c22c3e961d082..466a8e2dc21ba 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -381,6 +381,7 @@ in { gonic = handleTest ./gonic.nix {}; google-oslogin = handleTest ./google-oslogin {}; goss = handleTest ./goss.nix {}; + gotenberg = handleTest ./gotenberg.nix {}; gotify-server = handleTest ./gotify-server.nix {}; gotosocial = runTest ./web-apps/gotosocial.nix; grafana = handleTest ./grafana {}; diff --git a/nixos/tests/gotenberg.nix b/nixos/tests/gotenberg.nix new file mode 100644 index 0000000000000..aa39b2d349d79 --- /dev/null +++ b/nixos/tests/gotenberg.nix @@ -0,0 +1,26 @@ +import ./make-test-python.nix ( + { lib, ... }: + + { + name = "gotenberg"; + meta.maintainers = with lib.maintainers; [ pyrox0 ]; + + nodes.machine = { + services.gotenberg = { + enable = true; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("gotenberg.service") + + # Gotenberg startup + machine.wait_for_open_port(3000) + + # Ensure healthcheck endpoint succeeds + machine.succeed("curl http://localhost:3000/health") + ''; + } +) diff --git a/pkgs/by-name/go/gotenberg/package.nix b/pkgs/by-name/go/gotenberg/package.nix new file mode 100644 index 0000000000000..889d2109d4e71 --- /dev/null +++ b/pkgs/by-name/go/gotenberg/package.nix @@ -0,0 +1,92 @@ +{ + lib, + buildGoModule, + chromium, + fetchFromGitHub, + libreoffice, + makeBinaryWrapper, + pdftk, + qpdf, + unoconv, + mktemp, + makeFontsConf, + liberation_ttf_v2, + exiftool, + nix-update-script, +}: +let + fontsConf = makeFontsConf { fontDirectories = [ liberation_ttf_v2 ]; }; + jre' = libreoffice.unwrapped.jdk; + libreoffice' = "${libreoffice}/lib/libreoffice/program/soffice.bin"; + inherit (lib) getExe; +in +buildGoModule rec { + pname = "gotenberg"; + version = "8.8.0"; + + src = fetchFromGitHub { + owner = "gotenberg"; + repo = "gotenberg"; + rev = "refs/tags/v${version}"; + hash = "sha256-OHvtdUFZYm+I8DN8lAdlNAIRLWNRC/GbVEueRfyrwDA="; + }; + + vendorHash = "sha256-w9Q3hK8d5NwDYp8kydrWrlBlOmMyDqDixdv3z79yhK8="; + + postPatch = '' + find ./pkg -name '*_test.go' -exec sed -i -e 's#/tests#${src}#g' {} \; + ''; + + nativeBuildInputs = [ makeBinaryWrapper ]; + + ldflags = [ + "-s" + "-w" + "-X github.com/gotenberg/gotenberg/v8/cmd.Version=${version}" + ]; + + checkInputs = [ + chromium + libreoffice + pdftk + qpdf + unoconv + mktemp + jre' + ]; + + preCheck = '' + export CHROMIUM_BIN_PATH=${getExe chromium} + export PDFTK_BIN_PATH=${getExe pdftk} + export QPDF_BIN_PATH=${getExe qpdf} + export UNOCONVERTER_BIN_PATH=${getExe unoconv} + export EXIFTOOL_BIN_PATH=${getExe exiftool} + # LibreOffice needs all of these set to work properly + export LIBREOFFICE_BIN_PATH=${libreoffice'} + export FONTCONFIG_FILE=${fontsConf} + export HOME=$(mktemp -d) + export JAVA_HOME=${jre'} + ''; + + # These tests fail with a panic, so disable them. + checkFlags = [ "-skip=^TestChromiumBrowser_(screenshot|pdf)$" ]; + + preFixup = '' + wrapProgram $out/bin/gotenberg \ + --set PDFTK_BIN_PATH "${getExe pdftk}" \ + --set QPDF_BIN_PATH "${getExe qpdf}" \ + --set UNOCONVERTER_BIN_PATH "${getExe unoconv}" \ + --set EXIFTOOL_BIN_PATH "${getExe exiftool}" \ + --set JAVA_HOME "${jre'}" + ''; + + passthru.updateScript = nix-update-script { }; + + meta = { + description = "Converts numerous document formats into PDF files"; + homepage = "https://gotenberg.dev"; + changelog = "https://github.com/gotenberg/gotenberg/releases/tag/v${version}"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ pyrox0 ]; + }; +} diff --git a/pkgs/development/libraries/qpdf/default.nix b/pkgs/development/libraries/qpdf/default.nix index b2858944ef7c6..41d21bd7dcc67 100644 --- a/pkgs/development/libraries/qpdf/default.nix +++ b/pkgs/development/libraries/qpdf/default.nix @@ -53,5 +53,6 @@ stdenv.mkDerivation rec { maintainers = with maintainers; [ abbradar ]; platforms = platforms.all; changelog = "https://github.com/qpdf/qpdf/blob/v${version}/ChangeLog"; + mainProgram = "qpdf"; }; } diff --git a/pkgs/tools/text/unoconv/default.nix b/pkgs/tools/text/unoconv/default.nix index 490b961f97101..81e97d1a13fde 100644 --- a/pkgs/tools/text/unoconv/default.nix +++ b/pkgs/tools/text/unoconv/default.nix @@ -34,5 +34,6 @@ stdenv.mkDerivation rec { license = licenses.gpl2Only; platforms = platforms.linux; maintainers = [ maintainers.bjornfor ]; + mainProgram = "unoconv"; }; }