Extend nix flake setup

This commit is contained in:
eikek 2024-03-09 01:28:44 +01:00
parent 4167b64e31
commit 2e18274803
15 changed files with 824 additions and 491 deletions

130
flake.lock generated Normal file
View File

@ -0,0 +1,130 @@
{
"nodes": {
"devshell-tools": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1709936258,
"narHash": "sha256-ziYmDU/5v++oYSSwyMqEOr2V75rO+dMQA5aEdwH8amw=",
"owner": "eikek",
"repo": "devshell-tools",
"rev": "59900a3731a88508257525754a704ad2f8a3278e",
"type": "github"
},
"original": {
"owner": "eikek",
"repo": "devshell-tools",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1709309926,
"narHash": "sha256-VZFBtXGVD9LWTecGi6eXrE0hJ/mVB3zGUlHImUs2Qak=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "79baff8812a0d68e24a836df0a364c678089e2c7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1709677081,
"narHash": "sha256-tix36Y7u0rkn6mTm0lA45b45oab2cFLqAzDbJxeXS+c=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "880992dcc006a5e00dd0591446fdf723e6a51a64",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devshell-tools": "devshell-tools",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

164
flake.nix Normal file
View File

@ -0,0 +1,164 @@
{
description = "Docspell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
devshell-tools.url = "github:eikek/devshell-tools";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
devshell-tools,
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
devshellPkgs = with pkgs; [
jq
scala-cli
sbt
netcat
jdk17
wget
which
dpkg
elmPackages.elm
fakeroot
zola
yarn
];
docspellPkgs = pkgs.callPackage (import ./nix/pkg.nix) {};
dockerAmd64 = pkgs.pkgsCross.gnu64.callPackage (import ./nix/docker.nix) {
inherit (docspellPkgs) docspell-server docspell-joex;
};
dockerArm64 = pkgs.pkgsCross.aarch64-multiplatform.callPackage (import ./nix/docker.nix) {
inherit (docspellPkgs) docspell-server docspell-joex;
};
in {
formatter = pkgs.alejandra;
packages = {
inherit (docspellPkgs) docspell-server docspell-joex;
};
legacyPackages = {
docker = {
amd64 = {
inherit (dockerAmd64) docspell-server docspell-joex;
};
arm64 = {
inherit (dockerArm64) docspell-server docspell-joex;
};
};
};
checks = {
build-server = self.packages.${system}.docspell-server;
build-joex = self.packages.${system}.docspell-joex;
test = with import (nixpkgs + "/nixos/lib/testing-python.nix")
{
inherit system;
};
makeTest {
name = "docspell";
nodes = {
machine = {...}: {
nixpkgs.overlays = [self.overlays.default];
imports = [
self.nixosModules.default
./nix/checks
];
};
};
testScript = builtins.readFile ./nix/checks/testScript.py;
};
};
devShells = {
dev-cnt = pkgs.mkShellNoCC {
buildInputs =
(builtins.attrValues devshell-tools.legacyPackages.${system}.cnt-scripts)
++ devshellPkgs;
DEV_CONTAINER = "docsp-dev";
SBT_OPTS = "-Xmx2G -Xss4m";
};
dev-vm = pkgs.mkShellNoCC {
buildInputs =
(builtins.attrValues devshell-tools.legacyPackages.${system}.vm-scripts)
++ devshellPkgs;
SBT_OPTS = "-Xmx2G -Xss4m";
DEV_VM = "dev-vm";
VM_SSH_PORT = "10022";
};
};
})
// {
nixosModules = {
default = {...}: {
imports = [
./nix/modules/server.nix
./nix/modules/joex.nix
];
};
server = import ./nix/modules/server.nix;
joex = import ./nix/modules/joex.nix;
};
overlays.default = final: prev: let
docspellPkgs = final.callPackage (import ./nix/pkg.nix) {};
in {
inherit (docspellPkgs) docspell-server docspell-joex;
};
nixosConfigurations = {
test-vm = devshell-tools.lib.mkVm {
system = "x86_64-linux";
modules = [
self.nixosModules.default
{
nixpkgs.overlays = [self.overlays.default];
}
./nix/test-vm.nix
];
};
docsp-dev = devshell-tools.lib.mkContainer {
system = "x86_64-linux";
modules = [
{
services.dev-postgres.enable = true;
services.dev-email.enable = true;
services.dev-minio.enable = true;
services.dev-solr.enable = true;
}
];
};
dev-vm = devshell-tools.lib.mkVm {
system = "x86_64-linux";
modules = [
{
services.dev-postgres.enable = true;
services.dev-email.enable = true;
services.dev-minio.enable = true;
services.dev-solr.enable = true;
port-forward.ssh = 10022;
port-forward.dev-postgres = 6534;
port-forward.dev-smtp = 10025;
port-forward.dev-imap = 10143;
port-forward.dev-webmail = 8080;
port-forward.dev-minio-api = 9000;
port-forward.dev-minio-console = 9001;
port-forward.dev-solr = 8983;
networking.hostName = "dev-vm";
}
];
};
};
};
}

View File

@ -1,5 +1,8 @@
{ config, pkgs, ... }: {
let config,
pkgs,
...
}: let
full-text-search = { full-text-search = {
enabled = true; enabled = true;
backend = "postgresql"; backend = "postgresql";
@ -9,9 +12,7 @@ let
}; };
}; };
}; };
in in {
{
i18n = { i18n = {
defaultLocale = "en_US.UTF-8"; defaultLocale = "en_US.UTF-8";
}; };
@ -21,12 +22,11 @@ in
password = "root"; password = "root";
}; };
services.docspell-joex = { services.docspell-joex = {
enable = true; enable = true;
bind.address = "0.0.0.0"; bind.address = "0.0.0.0";
base-url = "http://localhost:7878"; base-url = "http://localhost:7878";
jvmArgs = [ "-J-Xmx1536M" ]; jvmArgs = ["-J-Xmx1536M"];
inherit full-text-search; inherit full-text-search;
}; };
services.docspell-restserver = { services.docspell-restserver = {
@ -69,27 +69,24 @@ in
}; };
}; };
environment.systemPackages = environment.systemPackages = [
[
pkgs.jq pkgs.jq
pkgs.inetutils pkgs.inetutils
pkgs.htop pkgs.htop
pkgs.jdk17 pkgs.jdk17
]; ];
services.xserver = { services.xserver = {
enable = false; enable = false;
}; };
networking = { networking = {
hostName = "docspelltest"; hostName = "docspelltest";
firewall.allowedTCPPorts = [ 7880 ]; firewall.allowedTCPPorts = [7880];
}; };
system.stateVersion = "22.11"; system.stateVersion = "22.11";
# This slows down the build of a vm # This slows down the build of a vm
documentation.enable = false; documentation.enable = false;
} }

View File

@ -1,5 +1,4 @@
{ ... }: {...}: {
{
imports = [ imports = [
./configuration-test.nix ./configuration-test.nix
]; ];

View File

@ -1,28 +0,0 @@
# NOTE: modulesPath and imports are taken from nixpkgs#59219
{ modulesPath, pkgs, lib, ... }: {
imports = [ (modulesPath + "/virtualisation/qemu-vm.nix") ];
services.openssh = {
enable = true;
permitRootLogin = "yes";
};
services.docspell-restserver = {
openid = lib.mkForce [ ];
backend = lib.mkForce {
signup = {
mode = "open";
};
};
};
# Otherwise oomkiller kills docspell
virtualisation.memorySize = 4096;
virtualisation.forwardPorts = [
# SSH
{ from = "host"; host.port = 64022; guest.port = 22; }
# Docspell
{ from = "host"; host.port = 64080; guest.port = 7880; }
];
}

79
nix/docker.nix Normal file
View File

@ -0,0 +1,79 @@
{
dockerTools,
busybox,
cacert,
wget,
unpaper,
ghostscript,
ocrmypdf,
tesseract4,
python3Packages,
unoconv,
docspell-server,
docspell-joex,
}: let
mkImage = {
name,
port,
pkg,
tools,
}:
dockerTools.buildLayeredImage {
inherit name;
created = "now";
contents =
[
busybox
cacert
wget
pkg
]
++ tools;
extraCommands = "mkdir -m 0777 tmp";
#https://github.com/moby/docker-image-spec/blob/main/spec.md#image-json-description
config = {
Entrypoint = ["bin/${name}" "-Dconfig.file="];
#Cmd = ["bin/${name}" "-Dconfig.file="];
ExposedPorts = {
"${builtins.toString port}/tcp" = {};
};
Env = [
"PATH=/bin"
"SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt"
];
Healthcheck = {
Test = [
"CMD"
"wget"
"--spider"
"http://localhost:${builtins.toString port}/api/info/version"
];
Interval = 60000000000; #1min
Timeout = 10000000000; #10s
Retries = 2;
StartInterval = 10000000000;
};
Labels = {
#https://github.com/microscaling/microscaling/blob/55a2d7b91ce7513e07f8b1fd91bbed8df59aed5a/Dockerfile#L22-L33
"org.label-schema.vcs-ref" = "v${pkg.version}";
"org.label-schema.vcs-url" = "https://github.com/eikek/docspell";
};
};
tag = "v${pkg.version}";
};
in {
docspell-server = mkImage {
name = "docspell-restserver";
port = 7880;
pkg = docspell-server;
tools = [];
};
docspell-joex = mkImage {
name = "docspell-joex";
port = 7878;
pkg = docspell-joex;
tools = [unpaper ghostscript ocrmypdf tesseract4 python3Packages.weasyprint unoconv];
};
}

26
nix/flake.lock generated
View File

@ -1,26 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1706373441,
"narHash": "sha256-S1hbgNbVYhuY2L05OANWqmRzj4cElcbLuIkXTb69xkk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "56911ef3403a9318b7621ce745f5452fb9ef6867",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-23.11",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@ -1,134 +0,0 @@
{
description = "Docspell flake";
inputs = {
nixpkgs.url = "nixpkgs/nixos-23.11";
};
outputs = { self, nixpkgs }:
let
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
# Version config
cfg = {
v0_41_0 = rec {
version = "0.41.0";
server = {
url = "https://github.com/eikek/docspell/releases/download/v${version}/docspell-restserver-${version}.zip";
sha256 = "sha256-JFftIzI94UNLLh96I++qFsBZhOkquPIPhNhtS2Ov8wI=";
};
joex = {
url = "https://github.com/eikek/docspell/releases/download/v${version}/docspell-joex-${version}.zip";
sha256 = "sha256-flKWjEsMd2/XT3Bu6EjFgf3lCojvLbKFDEXemP1K+/8=";
};
};
};
current_version = cfg.v0_41_0;
inherit (current_version) version;
in
rec
{
overlays.default = final: prev: {
docspell-server = with final; stdenv.mkDerivation {
inherit version;
pname = "docspell-server";
src = fetchzip current_version.server;
buildInputs = [ jdk17 ];
buildPhase = "true";
installPhase = ''
mkdir -p $out/{bin,docspell-restserver-${version}}
cp -R * $out/docspell-restserver-${version}/
cat > $out/bin/docspell-restserver <<-EOF
#!${bash}/bin/bash
$out/docspell-restserver-${version}/bin/docspell-restserver -java-home ${jdk17} "\$@"
EOF
chmod 755 $out/bin/docspell-restserver
'';
};
docspell-joex = with final; stdenv.mkDerivation rec {
inherit version;
pname = "docspell-joex";
src = fetchzip current_version.joex;
buildInputs = [ jdk17 ];
buildPhase = "true";
installPhase = ''
mkdir -p $out/{bin,docspell-joex-${version}}
cp -R * $out/docspell-joex-${version}/
cat > $out/bin/docspell-joex <<-EOF
#!${bash}/bin/bash
$out/docspell-joex-${version}/bin/docspell-joex -java-home ${jdk17} "\$@"
EOF
chmod 755 $out/bin/docspell-joex
'';
};
};
packages = forAllSystems (system:
{
default = (import nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
}).docspell-joex;
});
checks = forAllSystems
(system: {
build = self.packages.${system}.default;
test =
with import (nixpkgs + "/nixos/lib/testing-python.nix")
{
inherit system;
};
makeTest {
name = "docspell";
nodes = {
machine = { ... }: {
imports = [
self.nixosModules.default
./checks
];
};
};
testScript = builtins.readFile ./checks/testScript.py;
};
});
nixosModules = {
default = { ... }: {
imports = [
((import ./modules/server.nix) self.overlays.default)
((import ./modules/joex.nix) self.overlays.default)
];
};
server = ((import ./modules/server.nix) self.overlays.default);
joex = ((import ./modules/joex.nix) self.overlays.default);
};
nixosConfigurations =
let
lib = nixpkgs.lib;
in
{
dev-vm = lib.makeOverridable nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
self.nixosModules.default
./checks
# nixos-shell specific module. Should be kept outside nix flake checks
./dev-vm
];
};
};
};
}

View File

@ -1,11 +1,17 @@
overlay: { config, lib, pkgs, ... }: {
config,
with lib; lib,
let pkgs,
...
}:
with lib; let
cfg = config.services.docspell-joex; cfg = config.services.docspell-joex;
# Extract the config without the extraConfig attribute. It will be merged later # Extract the config without the extraConfig attribute. It will be merged later
declared_config = attrsets.filterAttrs (n: v: n != "extraConfig") cfg; declared_config = attrsets.filterAttrs (n: v: n != "extraConfig") cfg;
user = if cfg.runAs == null then "docspell" else cfg.runAs; user =
if cfg.runAs == null
then "docspell"
else cfg.runAs;
configFile = pkgs.writeText "docspell-joex.conf" '' configFile = pkgs.writeText "docspell-joex.conf" ''
{"docspell": { "joex": {"docspell": { "joex":
${builtins.toJSON (lib.recursiveUpdate declared_config cfg.extraConfig)} ${builtins.toJSON (lib.recursiveUpdate declared_config cfg.extraConfig)}
@ -85,7 +91,7 @@ let
schedule = "Sun *-*-* 00:00:00 UTC"; schedule = "Sun *-*-* 00:00:00 UTC";
sender-account = ""; sender-account = "";
smtp-id = ""; smtp-id = "";
recipients = [ ]; recipients = [];
subject = "Docspell {{ latestVersion }} is available"; subject = "Docspell {{ latestVersion }} is available";
body = '' body = ''
Hello, Hello,
@ -116,21 +122,21 @@ let
working-dir = "/tmp/docspell-extraction"; working-dir = "/tmp/docspell-extraction";
command = { command = {
program = "${pkgs.ghostscript}/bin/gs"; program = "${pkgs.ghostscript}/bin/gs";
args = [ "-dNOPAUSE" "-dBATCH" "-dSAFER" "-sDEVICE=tiffscaled8" "-sOutputFile={{outfile}}" "{{infile}}" ]; args = ["-dNOPAUSE" "-dBATCH" "-dSAFER" "-sDEVICE=tiffscaled8" "-sOutputFile={{outfile}}" "{{infile}}"];
timeout = "5 minutes"; timeout = "5 minutes";
}; };
}; };
unpaper = { unpaper = {
command = { command = {
program = "${pkgs.unpaper}/bin/unpaper"; program = "${pkgs.unpaper}/bin/unpaper";
args = [ "{{infile}}" "{{outfile}}" ]; args = ["{{infile}}" "{{outfile}}"];
timeout = "5 minutes"; timeout = "5 minutes";
}; };
}; };
tesseract = { tesseract = {
command = { command = {
program = "${pkgs.tesseract4}/bin/tesseract"; program = "${pkgs.tesseract4}/bin/tesseract";
args = [ "{{file}}" "stdout" "-l" "{{lang}}" ]; args = ["{{file}}" "stdout" "-l" "{{lang}}"];
timeout = "5 minutes"; timeout = "5 minutes";
}; };
}; };
@ -179,7 +185,7 @@ let
wkhtmlpdf = { wkhtmlpdf = {
command = { command = {
program = ""; program = "";
args = [ "--encoding" "UTF-8" "-" "{{outfile}}" ]; args = ["--encoding" "UTF-8" "-" "{{outfile}}"];
timeout = "2 minutes"; timeout = "2 minutes";
}; };
working-dir = "/tmp/docspell-convert"; working-dir = "/tmp/docspell-convert";
@ -204,7 +210,7 @@ let
tesseract = { tesseract = {
command = { command = {
program = "${pkgs.tesseract4}/bin/tesseract"; program = "${pkgs.tesseract4}/bin/tesseract";
args = [ "{{infile}}" "out" "-l" "{{lang}}" "pdf" "txt" ]; args = ["{{infile}}" "out" "-l" "{{lang}}" "pdf" "txt"];
timeout = "5 minutes"; timeout = "5 minutes";
}; };
working-dir = "/tmp/docspell-convert"; working-dir = "/tmp/docspell-convert";
@ -213,7 +219,7 @@ let
unoconv = { unoconv = {
command = { command = {
program = "${pkgs.unoconv}/bin/unoconv"; program = "${pkgs.unoconv}/bin/unoconv";
args = [ "-f" "pdf" "-o" "{{outfile}}" "{{infile}}" ]; args = ["-f" "pdf" "-o" "{{outfile}}" "{{infile}}"];
timeout = "2 minutes"; timeout = "2 minutes";
}; };
working-dir = "/tmp/docspell-convert"; working-dir = "/tmp/docspell-convert";
@ -240,7 +246,7 @@ let
}; };
files = { files = {
chunk-size = 524288; chunk-size = 524288;
valid-mime-types = [ ]; valid-mime-types = [];
}; };
full-text-search = { full-text-search = {
enabled = false; enabled = false;
@ -259,9 +265,9 @@ let
user = "pguser"; user = "pguser";
password = ""; password = "";
}; };
pg-config = { }; pg-config = {};
pg-query-parser = "websearch_to_tsquery"; pg-query-parser = "websearch_to_tsquery";
pg-rank-normalization = [ 4 ]; pg-rank-normalization = [4];
}; };
migration = { migration = {
index-all-chunk = 10; index-all-chunk = 10;
@ -291,9 +297,7 @@ let
}; };
}; };
}; };
in in {
{
## interface ## interface
options = { options = {
services.docspell-joex = { services.docspell-joex = {
@ -321,12 +325,11 @@ in
}; };
jvmArgs = mkOption { jvmArgs = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = [ ]; default = [];
example = [ "-J-Xmx1G" ]; example = ["-J-Xmx1G"];
description = "The options passed to the executable for setting jvm arguments."; description = "The options passed to the executable for setting jvm arguments.";
}; };
app-id = mkOption { app-id = mkOption {
type = types.str; type = types.str;
default = defaults.app-id; default = defaults.app-id;
@ -340,7 +343,7 @@ in
}; };
bind = mkOption { bind = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
address = mkOption { address = mkOption {
type = types.str; type = types.str;
@ -353,13 +356,13 @@ in
description = "The port to bind the REST server"; description = "The port to bind the REST server";
}; };
}; };
}); };
default = defaults.bind; default = defaults.bind;
description = "Address and port bind the rest server."; description = "Address and port bind the rest server.";
}; };
logging = mkOption { logging = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
minimum-level = mkOption { minimum-level = mkOption {
type = types.str; type = types.str;
@ -377,7 +380,7 @@ in
description = "Set of logger and their levels"; description = "Set of logger and their levels";
}; };
}; };
}); };
default = defaults.logging; default = defaults.logging;
description = "Settings for logging"; description = "Settings for logging";
}; };
@ -394,7 +397,7 @@ in
}; };
jdbc = mkOption { jdbc = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
url = mkOption { url = mkOption {
type = types.str; type = types.str;
@ -421,13 +424,13 @@ in
description = "The password to connect to the database."; description = "The password to connect to the database.";
}; };
}; };
}); };
default = defaults.jdbc; default = defaults.jdbc;
description = "Database connection settings"; description = "Database connection settings";
}; };
send-mail = mkOption { send-mail = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
list-id = mkOption { list-id = mkOption {
type = types.str; type = types.str;
@ -443,15 +446,14 @@ in
https://tools.ietf.org/html/rfc2919 for a formal specification https://tools.ietf.org/html/rfc2919 for a formal specification
''; '';
}; };
}; };
}); };
default = defaults.send-mail; default = defaults.send-mail;
description = "Settings for sending mails."; description = "Settings for sending mails.";
}; };
scheduler = mkOption { scheduler = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
pool-size = mkOption { pool-size = mkOption {
type = types.int; type = types.int;
@ -501,13 +503,13 @@ in
''; '';
}; };
}; };
}); };
default = defaults.scheduler; default = defaults.scheduler;
description = "Settings for the scheduler"; description = "Settings for the scheduler";
}; };
periodic-scheduler = mkOption { periodic-scheduler = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
wakeup-period = mkOption { wakeup-period = mkOption {
type = types.str; type = types.str;
@ -520,7 +522,7 @@ in
''; '';
}; };
}; };
}); };
default = defaults.periodic-scheduler; default = defaults.periodic-scheduler;
description = '' description = ''
Settings for the periodic scheduler. Settings for the periodic scheduler.
@ -528,10 +530,10 @@ in
}; };
user-tasks = mkOption { user-tasks = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
scan-mailbox = mkOption { scan-mailbox = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
max-folders = mkOption { max-folders = mkOption {
type = types.int; type = types.int;
@ -565,18 +567,18 @@ in
''; '';
}; };
}; };
}); };
default = defaults.user-tasks.scan-mailbox; default = defaults.user-tasks.scan-mailbox;
description = "Allows to import e-mails by scanning a mailbox."; description = "Allows to import e-mails by scanning a mailbox.";
}; };
}; };
}); };
default = defaults.user-tasks; default = defaults.user-tasks;
description = "Configuration for the user tasks."; description = "Configuration for the user tasks.";
}; };
house-keeping = mkOption { house-keeping = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
schedule = mkOption { schedule = mkOption {
type = types.str; type = types.str;
@ -587,7 +589,7 @@ in
''; '';
}; };
cleanup-invites = mkOption { cleanup-invites = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -600,7 +602,7 @@ in
description = "The minimum age of invites to be deleted."; description = "The minimum age of invites to be deleted.";
}; };
}; };
}); };
default = defaults.house-keeping.cleanup-invites; default = defaults.house-keeping.cleanup-invites;
description = '' description = ''
This task removes invitation keys that have been created but not This task removes invitation keys that have been created but not
@ -609,7 +611,7 @@ in
''; '';
}; };
cleanup-jobs = mkOption { cleanup-jobs = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -633,9 +635,8 @@ in
whether more or less memory should be used. whether more or less memory should be used.
''; '';
}; };
}; };
}); };
default = defaults.house-keeping.cleanup-jobs; default = defaults.house-keeping.cleanup-jobs;
description = '' description = ''
Jobs store their log output in the database. Normally this data Jobs store their log output in the database. Normally this data
@ -644,7 +645,7 @@ in
''; '';
}; };
cleanup-remember-me = mkOption { cleanup-remember-me = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -657,13 +658,13 @@ in
description = "The miminum age of remember me tokens to delete."; description = "The miminum age of remember me tokens to delete.";
}; };
}; };
}); };
default = defaults.house-keeping.cleanup-remember-me; default = defaults.house-keeping.cleanup-remember-me;
description = "Settings for cleaning up remember me tokens."; description = "Settings for cleaning up remember me tokens.";
}; };
cleanup-downloads = mkOption { cleanup-downloads = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -676,13 +677,13 @@ in
description = "The miminum age of a download file to delete."; description = "The miminum age of a download file to delete.";
}; };
}; };
}); };
default = defaults.house-keeping.cleanup-downloads; default = defaults.house-keeping.cleanup-downloads;
description = ""; description = "";
}; };
check-nodes = mkOption { check-nodes = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -695,12 +696,12 @@ in
description = "How often the node must be unreachable, before it is removed."; description = "How often the node must be unreachable, before it is removed.";
}; };
}; };
}); };
default = defaults.house-keeping.cleanup-nodes; default = defaults.house-keeping.cleanup-nodes;
description = "Removes node entries that are not reachable anymore."; description = "Removes node entries that are not reachable anymore.";
}; };
}; };
}); };
default = defaults.house-keeping; default = defaults.house-keeping;
description = '' description = ''
Docspell uses periodic house keeping tasks, like cleaning expired Docspell uses periodic house keeping tasks, like cleaning expired
@ -709,7 +710,7 @@ in
}; };
update-check = mkOption { update-check = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -753,7 +754,7 @@ in
recipients = mkOption { recipients = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = defaults.update-check.recipients; default = defaults.update-check.recipients;
example = [ "josh.doe@gmail.com" ]; example = ["josh.doe@gmail.com"];
description = '' description = ''
A list of recipient e-mail addresses. A list of recipient e-mail addresses.
''; '';
@ -781,7 +782,7 @@ in
''; '';
}; };
}; };
}); };
default = defaults.update-check; default = defaults.update-check;
description = '' description = ''
A periodic task to check for new releases of docspell. It can A periodic task to check for new releases of docspell. It can
@ -791,10 +792,10 @@ in
}; };
extraction = mkOption { extraction = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
pdf = mkOption { pdf = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
min-text-len = mkOption { min-text-len = mkOption {
type = types.int; type = types.int;
@ -808,12 +809,12 @@ in
''; '';
}; };
}; };
}); };
default = defaults.extraction.pdf; default = defaults.extraction.pdf;
description = "Settings for PDF extraction"; description = "Settings for PDF extraction";
}; };
preview = mkOption { preview = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
dpi = mkOption { dpi = mkOption {
type = types.int; type = types.int;
@ -830,12 +831,12 @@ in
''; '';
}; };
}; };
}); };
default = defaults.extraction.preview; default = defaults.extraction.preview;
description = ""; description = "";
}; };
ocr = mkOption { ocr = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
max-image-size = mkOption { max-image-size = mkOption {
type = types.int; type = types.int;
@ -846,7 +847,7 @@ in
''; '';
}; };
page-range = mkOption { page-range = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
begin = mkOption { begin = mkOption {
type = types.int; type = types.int;
@ -854,7 +855,7 @@ in
description = "Specifies the first N pages of a file to process."; description = "Specifies the first N pages of a file to process.";
}; };
}; };
}); };
default = defaults.extraction.page-range; default = defaults.extraction.page-range;
description = '' description = ''
Defines what pages to process. If a PDF with 600 pages is Defines what pages to process. If a PDF with 600 pages is
@ -871,7 +872,7 @@ in
''; '';
}; };
ghostscript = mkOption { ghostscript = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
working-dir = mkOption { working-dir = mkOption {
type = types.str; type = types.str;
@ -879,7 +880,7 @@ in
description = "Directory where the extraction processes can put their temp files"; description = "Directory where the extraction processes can put their temp files";
}; };
command = mkOption { command = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
program = mkOption { program = mkOption {
type = types.str; type = types.str;
@ -897,20 +898,20 @@ in
description = "The timeout when executing the command"; description = "The timeout when executing the command";
}; };
}; };
}); };
default = defaults.extraction.ghostscript.command; default = defaults.extraction.ghostscript.command;
description = "The system command"; description = "The system command";
}; };
}; };
}); };
default = defaults.extraction.ghostscript; default = defaults.extraction.ghostscript;
description = "The ghostscript command."; description = "The ghostscript command.";
}; };
unpaper = mkOption { unpaper = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
command = mkOption { command = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
program = mkOption { program = mkOption {
type = types.str; type = types.str;
@ -928,20 +929,20 @@ in
description = "The timeout when executing the command"; description = "The timeout when executing the command";
}; };
}; };
}); };
default = defaults.extraction.unpaper.command; default = defaults.extraction.unpaper.command;
description = "The system command"; description = "The system command";
}; };
}; };
}); };
default = defaults.extraction.unpaper; default = defaults.extraction.unpaper;
description = "The unpaper command."; description = "The unpaper command.";
}; };
tesseract = mkOption { tesseract = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
command = mkOption { command = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
program = mkOption { program = mkOption {
type = types.str; type = types.str;
@ -959,23 +960,22 @@ in
description = "The timeout when executing the command"; description = "The timeout when executing the command";
}; };
}; };
}); };
default = defaults.extraction.tesseract.command; default = defaults.extraction.tesseract.command;
description = "The system command"; description = "The system command";
}; };
}; };
}); };
default = defaults.extraction.tesseract; default = defaults.extraction.tesseract;
description = "The tesseract command."; description = "The tesseract command.";
}; };
}; };
}); };
default = defaults.extraction.ocr; default = defaults.extraction.ocr;
description = ""; description = "";
}; };
}; };
}); };
default = defaults.extraction; default = defaults.extraction;
description = '' description = ''
Configuration of text extraction Configuration of text extraction
@ -990,7 +990,7 @@ in
}; };
text-analysis = mkOption { text-analysis = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
max-length = mkOption { max-length = mkOption {
type = types.int; type = types.int;
@ -1015,7 +1015,7 @@ in
}; };
nlp = mkOption { nlp = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
mode = mkOption { mode = mkOption {
type = types.str; type = types.str;
@ -1073,7 +1073,7 @@ in
}; };
regex-ner = mkOption { regex-ner = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
max-entries = mkOption { max-entries = mkOption {
type = types.int; type = types.int;
@ -1104,18 +1104,18 @@ in
''; '';
}; };
}; };
}); };
default = defaults.text-analysis.nlp.regex-ner; default = defaults.text-analysis.nlp.regex-ner;
description = ""; description = "";
}; };
}; };
}); };
default = defaults.text-analysis.nlp; default = defaults.text-analysis.nlp;
description = "Configure NLP"; description = "Configure NLP";
}; };
classification = mkOption { classification = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -1147,9 +1147,8 @@ in
good results with *my* dataset. good results with *my* dataset.
''; '';
}; };
}; };
}); };
default = defaults.text-analysis.classification; default = defaults.text-analysis.classification;
description = '' description = ''
Settings for doing document classification. Settings for doing document classification.
@ -1167,13 +1166,13 @@ in
''; '';
}; };
}; };
}); };
default = defaults.text-analysis; default = defaults.text-analysis;
description = "Settings for text analysis"; description = "Settings for text analysis";
}; };
convert = mkOption { convert = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
chunk-size = mkOption { chunk-size = mkOption {
type = types.int; type = types.int;
@ -1202,7 +1201,7 @@ in
''; '';
}; };
markdown = mkOption { markdown = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
internal-css = mkOption { internal-css = mkOption {
type = types.str; type = types.str;
@ -1212,7 +1211,7 @@ in
''; '';
}; };
}; };
}); };
default = defaults.convert.markdown; default = defaults.convert.markdown;
description = '' description = ''
Settings when processing markdown files (and other text files) Settings when processing markdown files (and other text files)
@ -1224,12 +1223,12 @@ in
''; '';
}; };
html-converter = mkOption { html-converter = mkOption {
type = types.enum [ "wkhtmlpdf" "weasyprint" ]; type = types.enum ["wkhtmlpdf" "weasyprint"];
default = "weasyprint"; default = "weasyprint";
description = "Which tool to use for converting html to pdfs"; description = "Which tool to use for converting html to pdfs";
}; };
wkhtmlpdf = mkOption { wkhtmlpdf = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
working-dir = mkOption { working-dir = mkOption {
type = types.str; type = types.str;
@ -1237,7 +1236,7 @@ in
description = "Directory where the conversion processes can put their temp files"; description = "Directory where the conversion processes can put their temp files";
}; };
command = mkOption { command = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
program = mkOption { program = mkOption {
type = types.str; type = types.str;
@ -1255,12 +1254,12 @@ in
description = "The timeout when executing the command"; description = "The timeout when executing the command";
}; };
}; };
}); };
default = defaults.convert.wkhtmlpdf.command; default = defaults.convert.wkhtmlpdf.command;
description = "The system command"; description = "The system command";
}; };
}; };
}); };
default = defaults.convert.wkhtmlpdf; default = defaults.convert.wkhtmlpdf;
description = '' description = ''
To convert HTML files into PDF files, the external tool To convert HTML files into PDF files, the external tool
@ -1268,7 +1267,7 @@ in
''; '';
}; };
weasyprint = mkOption { weasyprint = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
working-dir = mkOption { working-dir = mkOption {
type = types.str; type = types.str;
@ -1276,7 +1275,7 @@ in
description = "Directory where the conversion processes can put their temp files"; description = "Directory where the conversion processes can put their temp files";
}; };
command = mkOption { command = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
program = mkOption { program = mkOption {
type = types.str; type = types.str;
@ -1294,12 +1293,12 @@ in
description = "The timeout when executing the command"; description = "The timeout when executing the command";
}; };
}; };
}); };
default = defaults.convert.weasyprint.command; default = defaults.convert.weasyprint.command;
description = "The system command"; description = "The system command";
}; };
}; };
}); };
default = defaults.convert.weasyprint; default = defaults.convert.weasyprint;
description = '' description = ''
To convert HTML files into PDF files, the external tool To convert HTML files into PDF files, the external tool
@ -1307,7 +1306,7 @@ in
''; '';
}; };
tesseract = mkOption { tesseract = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
working-dir = mkOption { working-dir = mkOption {
type = types.str; type = types.str;
@ -1315,7 +1314,7 @@ in
description = "Directory where the conversion processes can put their temp files"; description = "Directory where the conversion processes can put their temp files";
}; };
command = mkOption { command = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
program = mkOption { program = mkOption {
type = types.str; type = types.str;
@ -1333,12 +1332,12 @@ in
description = "The timeout when executing the command"; description = "The timeout when executing the command";
}; };
}; };
}); };
default = defaults.convert.tesseract.command; default = defaults.convert.tesseract.command;
description = "The system command"; description = "The system command";
}; };
}; };
}); };
default = defaults.convert.tesseract; default = defaults.convert.tesseract;
description = '' description = ''
To convert image files to PDF files, tesseract is used. This To convert image files to PDF files, tesseract is used. This
@ -1346,7 +1345,7 @@ in
''; '';
}; };
unoconv = mkOption { unoconv = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
working-dir = mkOption { working-dir = mkOption {
type = types.str; type = types.str;
@ -1354,7 +1353,7 @@ in
description = "Directory where the conversion processes can put their temp files"; description = "Directory where the conversion processes can put their temp files";
}; };
command = mkOption { command = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
program = mkOption { program = mkOption {
type = types.str; type = types.str;
@ -1372,12 +1371,12 @@ in
description = "The timeout when executing the command"; description = "The timeout when executing the command";
}; };
}; };
}); };
default = defaults.convert.unoconv.command; default = defaults.convert.unoconv.command;
description = "The system command"; description = "The system command";
}; };
}; };
}); };
default = defaults.convert.unoconv; default = defaults.convert.unoconv;
description = '' description = ''
To convert "office" files to PDF files, the external tool To convert "office" files to PDF files, the external tool
@ -1392,7 +1391,7 @@ in
}; };
ocrmypdf = mkOption { ocrmypdf = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -1405,7 +1404,7 @@ in
description = "Directory where the conversion processes can put their temp files"; description = "Directory where the conversion processes can put their temp files";
}; };
command = mkOption { command = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
program = mkOption { program = mkOption {
type = types.str; type = types.str;
@ -1423,12 +1422,12 @@ in
description = "The timeout when executing the command"; description = "The timeout when executing the command";
}; };
}; };
}); };
default = defaults.convert.ocrmypdf.command; default = defaults.convert.ocrmypdf.command;
description = "The system command"; description = "The system command";
}; };
}; };
}); };
default = defaults.convert.ocrmypdf; default = defaults.convert.ocrmypdf;
description = '' description = ''
The tool ocrmypdf can be used to convert pdf files to pdf files The tool ocrmypdf can be used to convert pdf files to pdf files
@ -1449,9 +1448,8 @@ in
converted to PDF/A. converted to PDF/A.
''; '';
}; };
}; };
}); };
default = defaults.convert; default = defaults.convert;
description = '' description = ''
Configuration for converting files into PDFs. Configuration for converting files into PDFs.
@ -1462,7 +1460,7 @@ in
''; '';
}; };
files = mkOption { files = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
chunk-size = mkOption { chunk-size = mkOption {
type = types.int; type = types.int;
@ -1489,12 +1487,12 @@ in
''; '';
}; };
}; };
}); };
default = defaults.files; default = defaults.files;
description = "Settings for how files are stored."; description = "Settings for how files are stored.";
}; };
full-text-search = mkOption { full-text-search = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -1514,7 +1512,7 @@ in
}; };
solr = mkOption { solr = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
url = mkOption { url = mkOption {
type = types.str; type = types.str;
@ -1546,13 +1544,13 @@ in
description = "The default combiner for tokens. One of {AND, OR}."; description = "The default combiner for tokens. One of {AND, OR}.";
}; };
}; };
}); };
default = defaults.full-text-search.solr; default = defaults.full-text-search.solr;
description = "Configuration for the SOLR backend."; description = "Configuration for the SOLR backend.";
}; };
postgresql = mkOption { postgresql = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
use-default-connection = mkOption { use-default-connection = mkOption {
type = types.bool; type = types.bool;
@ -1560,7 +1558,7 @@ in
description = "Whether to use the primary db connection."; description = "Whether to use the primary db connection.";
}; };
jdbc = mkOption { jdbc = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
url = mkOption { url = mkOption {
type = types.str; type = types.str;
@ -1580,7 +1578,7 @@ in
description = "The password to connect to the database."; description = "The password to connect to the database.";
}; };
}; };
}); };
default = defaults.full-text-search.postgresql.jdbc; default = defaults.full-text-search.postgresql.jdbc;
description = "Database connection settings"; description = "Database connection settings";
}; };
@ -1600,13 +1598,13 @@ in
description = ""; description = "";
}; };
}; };
}); };
default = defaults.full-text-search.postgresql; default = defaults.full-text-search.postgresql;
description = "PostgreSQL for fulltext search"; description = "PostgreSQL for fulltext search";
}; };
migration = mkOption { migration = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
index-all-chunk = mkOption { index-all-chunk = mkOption {
type = types.int; type = types.int;
@ -1618,17 +1616,17 @@ in
''; '';
}; };
}; };
}); };
default = defaults.full-text-search.migration; default = defaults.full-text-search.migration;
description = "Settings for running the index migration tasks"; description = "Settings for running the index migration tasks";
}; };
}; };
}); };
default = defaults.full-text-search; default = defaults.full-text-search;
description = "Configuration for full-text search."; description = "Configuration for full-text search.";
}; };
addons = mkOption { addons = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
working-dir = mkOption { working-dir = mkOption {
type = types.str; type = types.str;
@ -1641,7 +1639,7 @@ in
description = "Cache directory"; description = "Cache directory";
}; };
executor-config = mkOption { executor-config = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
runner = mkOption { runner = mkOption {
type = types.str; type = types.str;
@ -1659,7 +1657,7 @@ in
description = ""; description = "";
}; };
nspawn = mkOption { nspawn = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -1682,12 +1680,12 @@ in
description = ""; description = "";
}; };
}; };
}); };
default = defaults.addons.executor-config.nspawn; default = defaults.addons.executor-config.nspawn;
description = ""; description = "";
}; };
nix-runner = mkOption { nix-runner = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
nix-binary = mkOption { nix-binary = mkOption {
type = types.str; type = types.str;
@ -1700,12 +1698,12 @@ in
description = ""; description = "";
}; };
}; };
}); };
default = defaults.addons.executor-config.nix-runner; default = defaults.addons.executor-config.nix-runner;
description = ""; description = "";
}; };
docker-runner = mkOption { docker-runner = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
docker-binary = mkOption { docker-binary = mkOption {
type = types.str; type = types.str;
@ -1718,24 +1716,24 @@ in
description = ""; description = "";
}; };
}; };
}); };
default = defaults.addons.executor-config.docker-runner; default = defaults.addons.executor-config.docker-runner;
description = ""; description = "";
}; };
}; };
}); };
default = defaults.addons.executor-config; default = defaults.addons.executor-config;
description = ""; description = "";
}; };
}; };
}); };
default = defaults.addons; default = defaults.addons;
description = "Addon executor config"; description = "Addon executor config";
}; };
extraConfig = mkOption { extraConfig = mkOption {
type = types.attrs; type = types.attrs;
description = "Extra configuration for docspell server. Overwrites values in case of a conflict."; description = "Extra configuration for docspell server. Overwrites values in case of a conflict.";
default = { }; default = {};
example = '' example = ''
{ {
files = { files = {
@ -1754,9 +1752,6 @@ in
## implementation ## implementation
config = mkIf config.services.docspell-joex.enable { config = mkIf config.services.docspell-joex.enable {
nixpkgs.overlays = [ overlay ];
users.users."${user}" = mkIf (cfg.runAs == null) { users.users."${user}" = mkIf (cfg.runAs == null) {
name = user; name = user;
isSystemUser = true; isSystemUser = true;
@ -1765,43 +1760,35 @@ in
description = "Docspell user"; description = "Docspell user";
group = user; group = user;
}; };
users.groups."${user}" = mkIf (cfg.runAs == null) { }; users.groups."${user}" = mkIf (cfg.runAs == null) {};
# Setting up a unoconv listener to improve conversion performance # Setting up a unoconv listener to improve conversion performance
systemd.services.unoconv = systemd.services.unoconv = let
let
cmd = "${pkgs.unoconv}/bin/unoconv --listener -v"; cmd = "${pkgs.unoconv}/bin/unoconv --listener -v";
in in {
{
description = "Unoconv Listener"; description = "Unoconv Listener";
after = [ "networking.target" ]; after = ["networking.target"];
wantedBy = [ "multi-user.target" ]; wantedBy = ["multi-user.target"];
serviceConfig = { serviceConfig = {
Restart = "always"; Restart = "always";
}; };
script = script = "${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${user} -c \"${cmd}\"";
"${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${user} -c \"${cmd}\"";
}; };
systemd.services.docspell-joex = systemd.services.docspell-joex = let
let
args = builtins.concatStringsSep " " cfg.jvmArgs; args = builtins.concatStringsSep " " cfg.jvmArgs;
cmd = "${pkgs.docspell-joex}/bin/docspell-joex ${args} -- ${configFile}"; cmd = "${pkgs.docspell-joex}/bin/docspell-joex ${args} -- ${configFile}";
waitTarget = waitTarget =
if cfg.waitForTarget != null if cfg.waitForTarget != null
then then [cfg.waitForTarget]
[ cfg.waitForTarget ] else [];
else in {
[ ];
in
{
description = "Docspell Joex"; description = "Docspell Joex";
after = ([ "networking.target" ] ++ waitTarget); after = ["networking.target"] ++ waitTarget;
wantedBy = [ "multi-user.target" ]; wantedBy = ["multi-user.target"];
path = [ pkgs.gawk ]; path = [pkgs.gawk];
script = script = "${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${user} -c \"${cmd}\"";
"${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${user} -c \"${cmd}\"";
}; };
}; };
} }

View File

@ -1,11 +1,17 @@
overlay: { config, lib, pkgs, ... }: {
config,
with lib; lib,
let pkgs,
...
}:
with lib; let
cfg = config.services.docspell-restserver; cfg = config.services.docspell-restserver;
# Extract the config without the extraConfig attribute. It will be merged later # Extract the config without the extraConfig attribute. It will be merged later
declared_config = attrsets.filterAttrs (n: v: n != "extraConfig") cfg; declared_config = attrsets.filterAttrs (n: v: n != "extraConfig") cfg;
user = if cfg.runAs == null then "docspell" else cfg.runAs; user =
if cfg.runAs == null
then "docspell"
else cfg.runAs;
configFile = pkgs.writeText "docspell-server.conf" '' configFile = pkgs.writeText "docspell-server.conf" ''
{"docspell": {"server": {"docspell": {"server":
${builtins.toJSON (lib.recursiveUpdate declared_config cfg.extraConfig)} ${builtins.toJSON (lib.recursiveUpdate declared_config cfg.extraConfig)}
@ -44,7 +50,7 @@ let
source-name = "integration"; source-name = "integration";
allowed-ips = { allowed-ips = {
enabled = false; enabled = false;
ips = [ "127.0.0.1" ]; ips = ["127.0.0.1"];
}; };
http-basic = { http-basic = {
enabled = false; enabled = false;
@ -78,9 +84,9 @@ let
user = "pguser"; user = "pguser";
password = ""; password = "";
}; };
pg-config = { }; pg-config = {};
pg-query-parser = "websearch_to_tsquery"; pg-query-parser = "websearch_to_tsquery";
pg-rank-normalization = [ 4 ]; pg-rank-normalization = [4];
}; };
}; };
auth = { auth = {
@ -126,19 +132,17 @@ let
}; };
files = { files = {
chunk-size = 524288; chunk-size = 524288;
valid-mime-types = [ ]; valid-mime-types = [];
}; };
addons = { addons = {
enabled = false; enabled = false;
allow-impure = true; allow-impure = true;
allowed-urls = [ "*" ]; allowed-urls = ["*"];
denied-urls = [ ]; denied-urls = [];
}; };
}; };
}; };
in in {
{
## interface ## interface
options = { options = {
services.docspell-restserver = { services.docspell-restserver = {
@ -156,12 +160,11 @@ in
}; };
jvmArgs = mkOption { jvmArgs = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = [ ]; default = [];
example = [ "-J-Xmx1G" ]; example = ["-J-Xmx1G"];
description = "The options passed to the executable for setting jvm arguments."; description = "The options passed to the executable for setting jvm arguments.";
}; };
app-name = mkOption { app-name = mkOption {
type = types.str; type = types.str;
default = defaults.app-name; default = defaults.app-name;
@ -232,7 +235,7 @@ in
}; };
bind = mkOption { bind = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
address = mkOption { address = mkOption {
type = types.str; type = types.str;
@ -245,13 +248,13 @@ in
description = "The port to bind the REST server"; description = "The port to bind the REST server";
}; };
}; };
}); };
default = defaults.bind; default = defaults.bind;
description = "Address and port bind the rest server."; description = "Address and port bind the rest server.";
}; };
server-options = mkOption { server-options = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enable-http-2 = mkOption { enable-http-2 = mkOption {
type = types.bool; type = types.bool;
@ -269,13 +272,13 @@ in
description = "Timeout when waiting for the response."; description = "Timeout when waiting for the response.";
}; };
}; };
}); };
default = defaults.server-options; default = defaults.server-options;
description = "Tuning the http server"; description = "Tuning the http server";
}; };
logging = mkOption { logging = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
minimum-level = mkOption { minimum-level = mkOption {
type = types.str; type = types.str;
@ -293,13 +296,13 @@ in
description = "Set of logger and their levels"; description = "Set of logger and their levels";
}; };
}; };
}); };
default = defaults.logging; default = defaults.logging;
description = "Settings for logging"; description = "Settings for logging";
}; };
auth = mkOption { auth = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
server-secret = mkOption { server-secret = mkOption {
type = types.str; type = types.str;
@ -320,7 +323,7 @@ in
''; '';
}; };
remember-me = mkOption { remember-me = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -333,18 +336,18 @@ in
description = "The time a remember-me token is valid."; description = "The time a remember-me token is valid.";
}; };
}; };
}); };
default = defaults.auth.remember-me; default = defaults.auth.remember-me;
description = "Settings for Remember-Me"; description = "Settings for Remember-Me";
}; };
}; };
}); };
default = defaults.auth; default = defaults.auth;
description = "Authentication"; description = "Authentication";
}; };
download-all = mkOption { download-all = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
max-files = mkOption { max-files = mkOption {
type = types.int; type = types.int;
@ -357,7 +360,7 @@ in
description = "The maximum (uncompressed) size of the zip file contents."; description = "The maximum (uncompressed) size of the zip file contents.";
}; };
}; };
}); };
default = defaults.download-all; default = defaults.download-all;
description = ""; description = "";
}; };
@ -387,7 +390,7 @@ in
description = "How to retrieve the collective name."; description = "How to retrieve the collective name.";
}; };
provider = mkOption { provider = mkOption {
type = (types.submodule { type = types.submodule {
options = { options = {
provider-id = mkOption { provider-id = mkOption {
type = types.str; type = types.str;
@ -436,18 +439,18 @@ in
description = "The expected algorithm used to sign the token."; description = "The expected algorithm used to sign the token.";
}; };
}; };
}); };
default = defaults.openid.provider; default = defaults.openid.provider;
description = "The config for an OpenID Connect provider."; description = "The config for an OpenID Connect provider.";
}; };
}; };
}); });
default = [ ]; default = [];
description = "A list of OIDC provider configurations."; description = "A list of OIDC provider configurations.";
}; };
integration-endpoint = mkOption { integration-endpoint = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -467,7 +470,7 @@ in
''; '';
}; };
allowed-ips = mkOption { allowed-ips = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -480,7 +483,7 @@ in
description = "The ips/ip patterns to allow"; description = "The ips/ip patterns to allow";
}; };
}; };
}); };
default = defaults.integration-endpoint.allowed-ips; default = defaults.integration-endpoint.allowed-ips;
description = '' description = ''
IPv4 addresses to allow access. An empty list, if enabled, IPv4 addresses to allow access. An empty list, if enabled,
@ -491,7 +494,7 @@ in
''; '';
}; };
http-basic = mkOption { http-basic = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -514,14 +517,14 @@ in
description = "The password to check."; description = "The password to check.";
}; };
}; };
}); };
default = defaults.integration-endpoint.http-basic; default = defaults.integration-endpoint.http-basic;
description = '' description = ''
Requests are expected to use http basic auth when uploading files. Requests are expected to use http basic auth when uploading files.
''; '';
}; };
http-header = mkOption { http-header = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -539,7 +542,7 @@ in
description = "The value of the header to check."; description = "The value of the header to check.";
}; };
}; };
}); };
default = defaults.integration-endpoint.http-header; default = defaults.integration-endpoint.http-header;
description = '' description = ''
Requests are expected to supply some specific header when Requests are expected to supply some specific header when
@ -547,7 +550,7 @@ in
''; '';
}; };
}; };
}); };
default = defaults.integration-endpoint; default = defaults.integration-endpoint;
description = '' description = ''
This endpoint allows to upload files to any collective. The This endpoint allows to upload files to any collective. The
@ -566,7 +569,7 @@ in
}; };
admin-endpoint = mkOption { admin-endpoint = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
secret = mkOption { secret = mkOption {
type = types.str; type = types.str;
@ -574,13 +577,13 @@ in
description = "The secret used to call admin endpoints."; description = "The secret used to call admin endpoints.";
}; };
}; };
}); };
default = defaults.admin-endpoint; default = defaults.admin-endpoint;
description = "An endpoint for administration tasks."; description = "An endpoint for administration tasks.";
}; };
full-text-search = mkOption { full-text-search = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -597,7 +600,7 @@ in
description = "The backend to use, either solr or postgresql"; description = "The backend to use, either solr or postgresql";
}; };
solr = mkOption { solr = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
url = mkOption { url = mkOption {
type = types.str; type = types.str;
@ -629,13 +632,13 @@ in
description = "The default combiner for tokens. One of {AND, OR}."; description = "The default combiner for tokens. One of {AND, OR}.";
}; };
}; };
}); };
default = defaults.full-text-search.solr; default = defaults.full-text-search.solr;
description = "Configuration for the SOLR backend."; description = "Configuration for the SOLR backend.";
}; };
postgresql = mkOption { postgresql = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
use-default-connection = mkOption { use-default-connection = mkOption {
type = types.bool; type = types.bool;
@ -643,7 +646,7 @@ in
description = "Whether to use the primary db connection."; description = "Whether to use the primary db connection.";
}; };
jdbc = mkOption { jdbc = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
url = mkOption { url = mkOption {
type = types.str; type = types.str;
@ -663,7 +666,7 @@ in
description = "The password to connect to the database."; description = "The password to connect to the database.";
}; };
}; };
}); };
default = defaults.full-text-search.postgresql.jdbc; default = defaults.full-text-search.postgresql.jdbc;
description = "Database connection settings"; description = "Database connection settings";
}; };
@ -683,18 +686,18 @@ in
description = ""; description = "";
}; };
}; };
}); };
default = defaults.full-text-search.postgresql; default = defaults.full-text-search.postgresql;
description = "PostgreSQL for fulltext search"; description = "PostgreSQL for fulltext search";
}; };
}; };
}); };
default = defaults.full-text-search; default = defaults.full-text-search;
description = "Configuration for full-text search."; description = "Configuration for full-text search.";
}; };
backend = mkOption { backend = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
mail-debug = mkOption { mail-debug = mkOption {
type = types.bool; type = types.bool;
@ -707,7 +710,7 @@ in
''; '';
}; };
jdbc = mkOption { jdbc = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
url = mkOption { url = mkOption {
type = types.str; type = types.str;
@ -734,12 +737,12 @@ in
description = "The password to connect to the database."; description = "The password to connect to the database.";
}; };
}; };
}); };
default = defaults.backend.jdbc; default = defaults.backend.jdbc;
description = "Database connection settings"; description = "Database connection settings";
}; };
signup = mkOption { signup = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
mode = mkOption { mode = mkOption {
type = types.str; type = types.str;
@ -772,12 +775,12 @@ in
''; '';
}; };
}; };
}); };
default = defaults.backend.signup; default = defaults.backend.signup;
description = "Registration settings"; description = "Registration settings";
}; };
files = mkOption { files = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
chunk-size = mkOption { chunk-size = mkOption {
type = types.int; type = types.int;
@ -804,12 +807,12 @@ in
''; '';
}; };
}; };
}); };
default = defaults.backend.files; default = defaults.backend.files;
description = "Settings for how files are stored."; description = "Settings for how files are stored.";
}; };
addons = mkOption { addons = mkOption {
type = types.submodule ({ type = types.submodule {
options = { options = {
enabled = mkOption { enabled = mkOption {
type = types.bool; type = types.bool;
@ -832,19 +835,19 @@ in
description = "Url patterns to deny to install"; description = "Url patterns to deny to install";
}; };
}; };
}); };
default = defaults.backend.addons; default = defaults.backend.addons;
description = "Addon config"; description = "Addon config";
}; };
}; };
}); };
default = defaults.backend; default = defaults.backend;
description = "Configuration for the backend"; description = "Configuration for the backend";
}; };
extraConfig = mkOption { extraConfig = mkOption {
type = types.attrs; type = types.attrs;
description = "Extra configuration for docspell server. Overwrites values in case of a conflict."; description = "Extra configuration for docspell server. Overwrites values in case of a conflict.";
default = { }; default = {};
example = '' example = ''
{ {
files = { files = {
@ -863,8 +866,6 @@ in
## implementation ## implementation
config = mkIf config.services.docspell-restserver.enable { config = mkIf config.services.docspell-restserver.enable {
nixpkgs.overlays = [ overlay ];
users.users."${user}" = mkIf (cfg.runAs == null) { users.users."${user}" = mkIf (cfg.runAs == null) {
name = user; name = user;
isSystemUser = true; isSystemUser = true;
@ -873,24 +874,20 @@ in
description = "Docspell user"; description = "Docspell user";
group = user; group = user;
}; };
users.groups."${user}" = mkIf (cfg.runAs == null) { }; users.groups."${user}" = mkIf (cfg.runAs == null) {};
systemd.services.docspell-restserver = let
systemd.services.docspell-restserver =
let
args = builtins.concatStringsSep " " cfg.jvmArgs; args = builtins.concatStringsSep " " cfg.jvmArgs;
cmd = "${pkgs.docspell-server}/bin/docspell-restserver ${args} -- ${configFile}"; cmd = "${pkgs.docspell-server}/bin/docspell-restserver ${args} -- ${configFile}";
in in {
{
description = "Docspell Rest Server"; description = "Docspell Rest Server";
after = [ "networking.target" ]; after = ["networking.target"];
wantedBy = [ "multi-user.target" ]; wantedBy = ["multi-user.target"];
path = [ pkgs.gawk ]; path = [pkgs.gawk];
preStart = '' preStart = ''
''; '';
script = script = "${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${user} -c \"${cmd}\"";
"${pkgs.su}/bin/su -s ${pkgs.bash}/bin/sh ${user} -c \"${cmd}\"";
}; };
}; };
} }

57
nix/pkg.nix Normal file
View File

@ -0,0 +1,57 @@
{
stdenv,
bash,
fetchzip,
jdk17,
}: let
version = "0.41.0";
server = {
url = "https://github.com/eikek/docspell/releases/download/v${version}/docspell-restserver-${version}.zip";
sha256 = "sha256-JFftIzI94UNLLh96I++qFsBZhOkquPIPhNhtS2Ov8wI=";
};
joex = {
url = "https://github.com/eikek/docspell/releases/download/v${version}/docspell-joex-${version}.zip";
sha256 = "sha256-flKWjEsMd2/XT3Bu6EjFgf3lCojvLbKFDEXemP1K+/8=";
};
in {
docspell-server = stdenv.mkDerivation {
inherit version;
pname = "docspell-server";
src = fetchzip server;
buildInputs = [jdk17];
buildPhase = "true";
installPhase = ''
mkdir -p $out/{bin,docspell-restserver-${version}}
cp -R * $out/docspell-restserver-${version}/
cat > $out/bin/docspell-restserver <<-EOF
#!${bash}/bin/bash
$out/docspell-restserver-${version}/bin/docspell-restserver -java-home ${jdk17} "\$@"
EOF
chmod 755 $out/bin/docspell-restserver
'';
};
docspell-joex = stdenv.mkDerivation rec {
inherit version;
pname = "docspell-joex";
src = fetchzip joex;
buildInputs = [jdk17];
buildPhase = "true";
installPhase = ''
mkdir -p $out/{bin,docspell-joex-${version}}
cp -R * $out/docspell-joex-${version}/
cat > $out/bin/docspell-joex <<-EOF
#!${bash}/bin/bash
$out/docspell-joex-${version}/bin/docspell-joex -java-home ${jdk17} "\$@"
EOF
chmod 755 $out/bin/docspell-joex
'';
};
}

77
nix/test-vm.nix Normal file
View File

@ -0,0 +1,77 @@
{
config,
pkgs,
...
}: let
full-text-search = {
enabled = true;
backend = "solr";
solr.url = "http://localhost:8983/solr/docspell";
};
jdbc = {
url = "jdbc:postgresql://localhost:5432/docspell";
user = "dev";
password = "dev";
};
in {
services.dev-postgres = {
enable = true;
databases = ["docspell"];
};
services.dev-email.enable = true;
services.dev-solr = {
enable = true;
cores = ["docspell"];
};
port-forward.dev-webmail = 8080;
port-forward.dev-solr = 8983;
networking = {
hostName = "docspell-test-vm";
firewall.allowedTCPPorts = [7880];
};
virtualisation.memorySize = 6144;
virtualisation.forwardPorts = [
{
from = "host";
host.port = 7881;
guest.port = 7880;
}
];
services.docspell-restserver = {
enable = true;
bind.address = "0.0.0.0";
backend = {
addons.enabled = true;
signup.mode = "open";
inherit jdbc;
};
integration-endpoint = {
enabled = true;
http-header = {
enabled = true;
header-value = "test123";
};
};
admin-endpoint = {
secret = "admin123";
};
inherit full-text-search;
};
services.docspell-joex = {
enable = true;
bind.address = "0.0.0.0";
inherit jdbc full-text-search;
addons = {
executor-config = {
runner = "nix-flake,trivial";
nspawn.enabled = true;
};
};
};
}

View File

@ -3,21 +3,33 @@ let
#url = "https://github.com/NixOS/nixpkgs/archive/92e990a8d6bc35f1089c76dd8ba68b78da90da59.tar.gz"; #url = "https://github.com/NixOS/nixpkgs/archive/92e990a8d6bc35f1089c76dd8ba68b78da90da59.tar.gz";
url = "channel:nixos-23.05"; url = "channel:nixos-23.05";
}; };
pkgs = import nixpkgs { }; pkgs = import nixpkgs {};
initScript = pkgs.writeScript "docspell-build-init" '' initScript = pkgs.writeScript "docspell-build-init" ''
export LD_LIBRARY_PATH= export LD_LIBRARY_PATH=
${pkgs.bash}/bin/bash -c "sbt -mem 4096 -java-home ${pkgs.openjdk17}/lib/openjdk" ${pkgs.bash}/bin/bash -c "sbt -mem 4096 -java-home ${pkgs.openjdk17}/lib/openjdk"
''; '';
in with pkgs; in
with pkgs;
buildFHSUserEnv { buildFHSUserEnv {
name = "docspell-sbt"; name = "docspell-sbt";
targetPkgs = pkgs: with pkgs; [ targetPkgs = pkgs:
netcat jdk17 wget which dpkg sbt git elmPackages.elm ncurses fakeroot mc with pkgs; [
zola yarn netcat
jdk17
wget
which
dpkg
sbt
git
elmPackages.elm
ncurses
fakeroot
mc
zola
yarn
# haskells http client needs this (to download elm packages) # haskells http client needs this (to download elm packages)
iana-etc iana-etc
]; ];
runScript = initScript; runScript = initScript;
} }

View File

@ -4,10 +4,9 @@ let
#url = "https://github.com/NixOS/nixpkgs/archive/e6badb26fc0d238fda2432c45b7dd4e782eb8200.tar.gz"; #url = "https://github.com/NixOS/nixpkgs/archive/e6badb26fc0d238fda2432c45b7dd4e782eb8200.tar.gz";
#url = "https://github.com/NixOs/nixpkgs/archive/0f316e4d72daed659233817ffe52bf08e081b5de.tar.gz"; #21.11 #url = "https://github.com/NixOs/nixpkgs/archive/0f316e4d72daed659233817ffe52bf08e081b5de.tar.gz"; #21.11
}; };
pkgs = import nixpkgs { }; pkgs = import nixpkgs {};
in in
with pkgs; with pkgs;
mkShell { mkShell {
buildInputs = [ buildInputs = [
zola zola

View File

@ -8,9 +8,9 @@ weight = 24
## Install via Nix ## Install via Nix
Docspell can be installed via the [nix](https://nixos.org/nix) package Docspell can be installed via the [nix](https://nixos.org/nix) package
manager, which is available for Linux and OSX. Docspell is currently not manager. Docspell is currently not part of the [nixpkgs
part of the [nixpkgs collection](https://nixos.org/nixpkgs/), but you collection](https://nixos.org/nixpkgs/), but you can use the flake
can use the flake from this repository. from this repository.
## Upgrading ## Upgrading
@ -31,7 +31,7 @@ There are the following modules provided:
```nix ```nix
# flake.nix # flake.nix
inputs.docspell.url = "github:eikek/docspell?dir=nix/"; inputs.docspell.url = "github:eikek/docspell";
# in modules # in modules
imports = [ imports = [
@ -134,10 +134,33 @@ inputs.docspell.url = "github:eikek/docspell?dir=nix/";
''; '';
}; };
networking = { networking = {
hostName = "docspellexample"; hostName = "docspellexample";
firewall.allowedTCPPorts = [7880]; firewall.allowedTCPPorts = [7880];
}; };
} }
``` ```
You can also look at `nix/test-vm.nix` for another example.
## Without Flakes
Of course, you can also use it without flakes. There is `nix/pkg.nix`
which contains the derivation of both packages, `docspell-server` and
`docspell-joex`. Just call it with your nixpkgs instance as usual:
``` nix
let
repo = fetchurl {
url = "https://github.com/eikek/docspell";
sha256 = "sha256-X2mM+Z5s8Xm1E6zrZ0wcRaivLEvqbk5Dn+GSXkZHdLM=";
};
docspellPkgs = pkgs.callPackage (import "${repo}/nix/pkg.nix") {};
in
#
# use docspellPkgs.docspell-server or docspellPkgs.docspell-joex
#
```
The same way import NixOS modules from
`nix/modules/{joex|server}.nix`.