diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index cd6bb9019b18..5198bedc1387 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -339,6 +339,7 @@
rss2email = 312;
cockroachdb = 313;
zoneminder = 314;
+ paperless = 315;
# When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
@@ -638,6 +639,7 @@
rss2email = 312;
cockroachdb = 313;
zoneminder = 314;
+ paperless = 315;
# When adding a gid, make sure it doesn't match an existing
# uid. Users and groups with the same name should have equal
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index a07461022a31..111addcc8790 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -435,6 +435,7 @@
./services/misc/octoprint.nix
./services/misc/osrm.nix
./services/misc/packagekit.nix
+ ./services/misc/paperless.nix
./services/misc/parsoid.nix
./services/misc/phd.nix
./services/misc/plex.nix
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
new file mode 100644
index 000000000000..4e6cd80e2425
--- /dev/null
+++ b/nixos/modules/services/misc/paperless.nix
@@ -0,0 +1,185 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+let
+ cfg = config.services.paperless;
+
+ defaultUser = "paperless";
+
+ manage = cfg.package.withConfig {
+ config = {
+ PAPERLESS_CONSUMPTION_DIR = cfg.consumptionDir;
+ PAPERLESS_INLINE_DOC = "true";
+ PAPERLESS_DISABLE_LOGIN = "true";
+ } // cfg.extraConfig;
+ inherit (cfg) dataDir ocrLanguages;
+ paperlessPkg = cfg.package;
+ };
+in
+{
+ options.services.paperless = {
+ enable = mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Enable Paperless.
+
+ When started, the Paperless database is automatically created if it doesn't
+ exist and updated if the Paperless package has changed.
+ Both tasks are achieved by running a Django migration.
+ '';
+ };
+
+ dataDir = mkOption {
+ type = types.str;
+ default = "/var/lib/paperless";
+ description = "Directory to store the Paperless data.";
+ };
+
+ consumptionDir = mkOption {
+ type = types.str;
+ default = "${cfg.dataDir}/consume";
+ defaultText = "\${dataDir}/consume";
+ description = "Directory from which new documents are imported.";
+ };
+
+ consumptionDirIsPublic = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Whether all users can write to the consumption dir.";
+ };
+
+ ocrLanguages = mkOption {
+ type = with types; nullOr (listOf string);
+ default = null;
+ description = ''
+ Languages available for OCR via Tesseract, specified as
+ ISO 639-2/T language codes.
+ If unset, defaults to all available languages.
+ '';
+ example = [ "eng" "spa" "jpn" ];
+ };
+
+ address = mkOption {
+ type = types.str;
+ default = "localhost";
+ description = "Server listening address.";
+ };
+
+ port = mkOption {
+ type = types.int;
+ default = 28981;
+ description = "Server port to listen on.";
+ };
+
+ extraConfig = mkOption {
+ type = types.attrs;
+ default = {};
+ description = ''
+ Extra paperless config options.
+
+ The config values are evaluated as double-quoted Bash string literals.
+
+ See paperless-src/paperless.conf.example for available options.
+
+ To enable user authentication, set PAPERLESS_DISABLE_LOGIN = "false"
+ and run the shell command $dataDir/paperless-manage createsuperuser.
+
+ To define secret options without storing them in /nix/store, use the following pattern:
+ PAPERLESS_PASSPHRASE = "$(< /etc/my_passphrase_file)"
+ '';
+ example = literalExample ''
+ {
+ PAPERLESS_OCR_LANGUAGE = "deu";
+ }
+ '';
+ };
+
+ user = mkOption {
+ type = types.str;
+ default = defaultUser;
+ description = "User under which Paperless runs.";
+ };
+
+ package = mkOption {
+ type = types.package;
+ default = pkgs.paperless;
+ defaultText = "pkgs.paperless";
+ description = "The Paperless package to use.";
+ };
+
+ manage = mkOption {
+ type = types.package;
+ readOnly = true;
+ default = manage;
+ description = ''
+ A script to manage the Paperless instance.
+ It wraps Django's manage.py and is also available at
+ $dataDir/manage-paperless
+ '';
+ };
+ };
+
+ config = mkIf cfg.enable {
+
+ systemd.tmpfiles.rules = [
+ "d '${cfg.dataDir}' - ${cfg.user} ${cfg.user} - -"
+ ] ++ (optional cfg.consumptionDirIsPublic
+ "d '${cfg.consumptionDir}' 777 ${cfg.user} ${cfg.user} - -"
+ # If the consumption dir is not created here, it's automatically created by
+ # 'manage' with the default permissions.
+ );
+
+ systemd.services.paperless-consumer = {
+ description = "Paperless document consumer";
+ serviceConfig = {
+ User = cfg.user;
+ ExecStart = "${manage} document_consumer";
+ Restart = "always";
+ };
+ after = [ "systemd-tmpfiles-setup.service" ];
+ wantedBy = [ "multi-user.target" ];
+ preStart = ''
+ if [[ $(readlink ${cfg.dataDir}/paperless-manage) != ${manage} ]]; then
+ ln -sf ${manage} ${cfg.dataDir}/paperless-manage
+ fi
+
+ ${manage.setupEnv}
+ # Auto-migrate on first run or if the package has changed
+ versionFile="$PAPERLESS_DBDIR/src-version"
+ if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
+ python $paperlessSrc/manage.py migrate
+ echo ${cfg.package} > "$versionFile"
+ fi
+ '';
+ };
+
+ systemd.services.paperless-server = {
+ description = "Paperless document server";
+ serviceConfig = {
+ User = cfg.user;
+ ExecStart = "${manage} runserver --noreload ${cfg.address}:${toString cfg.port}";
+ Restart = "always";
+ };
+ # Bind to `paperless-consumer` so that the server never runs
+ # during migrations
+ bindsTo = [ "paperless-consumer.service" ];
+ after = [ "paperless-consumer.service" ];
+ wantedBy = [ "multi-user.target" ];
+ };
+
+ users = optionalAttrs (cfg.user == defaultUser) {
+ users = [{
+ name = defaultUser;
+ group = defaultUser;
+ uid = config.ids.uids.paperless;
+ home = cfg.dataDir;
+ }];
+
+ groups = [{
+ name = defaultUser;
+ gid = config.ids.gids.paperless;
+ }];
+ };
+ };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 95654b679609..d495b2fa6333 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -189,6 +189,7 @@ in
pam-oath-login = handleTest ./pam-oath-login.nix {};
pam-u2f = handleTest ./pam-u2f.nix {};
pantheon = handleTest ./pantheon.nix {};
+ paperless = handleTest ./paperless.nix {};
peerflix = handleTest ./peerflix.nix {};
pgjwt = handleTest ./pgjwt.nix {};
pgmanage = handleTest ./pgmanage.nix {};
diff --git a/nixos/tests/paperless.nix b/nixos/tests/paperless.nix
new file mode 100644
index 000000000000..860ad0a6218f
--- /dev/null
+++ b/nixos/tests/paperless.nix
@@ -0,0 +1,29 @@
+import ./make-test.nix ({ lib, ... } : {
+ name = "paperless";
+ meta = with lib.maintainers; {
+ maintainers = [ earvstedt ];
+ };
+
+ machine = { pkgs, ... }: {
+ environment.systemPackages = with pkgs; [ imagemagick jq ];
+ services.paperless = {
+ enable = true;
+ ocrLanguages = [ "eng" ];
+ };
+ };
+
+ testScript = ''
+ $machine->waitForUnit("paperless-consumer.service");
+ # Create test doc
+ $machine->succeed('convert -size 400x40 xc:white -font "DejaVu-Sans" -pointsize 20 -fill black \
+ -annotate +5+20 "hello world 16-10-2005" /var/lib/paperless/consume/doc.png');
+
+ $machine->waitForUnit("paperless-server.service");
+ # Wait until server accepts connections
+ $machine->waitUntilSucceeds("curl -s localhost:28981");
+ # Wait until document is consumed
+ $machine->waitUntilSucceeds('(($(curl -s localhost:28981/api/documents/ | jq .count) == 1))');
+ $machine->succeed("curl -s localhost:28981/api/documents/ | jq '.results | .[0] | .created'")
+ =~ /2005-10-16/ or die;
+ '';
+})
diff --git a/pkgs/applications/office/paperless/default.nix b/pkgs/applications/office/paperless/default.nix
new file mode 100644
index 000000000000..0b6ae285bc03
--- /dev/null
+++ b/pkgs/applications/office/paperless/default.nix
@@ -0,0 +1,170 @@
+{ stdenv
+, lib
+, fetchFromGitHub
+, makeWrapper
+, callPackage
+
+, python3
+, file
+, imagemagick7
+, ghostscript
+, optipng
+, poppler
+, tesseract
+, unpaper
+}:
+
+## Usage
+
+# ${paperless}/bin/paperless wraps manage.py
+
+# ${paperless}/share/paperless/setup-env.sh can be sourced from a
+# shell script to setup a Paperless environment
+
+# paperless.withConfig is a convenience function to setup a
+# configured Paperless instance. (See ./withConfig.nix)
+
+# For WSGI with gunicorn, use a shell script like this:
+# let
+# pythonEnv = paperless.python.withPackages (ps: paperless.runtimePackages ++ [ ps.gunicorn ]);
+# in
+# writers.writeBash "run-gunicorn" ''
+# source ${paperless}/share/paperless/setup-env.sh
+# PYTHONPATH=$paperlessSrc ${pythonEnv}/bin/gunicorn paperless.wsgi
+# ''
+
+let
+ paperless = stdenv.mkDerivation rec {
+ name = "paperless-${version}";
+ version = "2.7.0";
+
+ src = fetchFromGitHub {
+ owner = "the-paperless-project";
+ repo = "paperless";
+ rev = version;
+ sha256 = "0pkmyky1crjnsg7r0gfk0fadisfsgzlsq6afpz16wx4hp6yvkkf7";
+ };
+
+ nativeBuildInputs = [ makeWrapper ];
+
+ doCheck = true;
+ dontInstall = true;
+
+ pythonEnv = python.withPackages (_: runtimePackages);
+ pythonCheckEnv = python.withPackages (_: (runtimePackages ++ checkPackages));
+
+ unpackPhase = ''
+ srcDir=$out/share/paperless
+ mkdir -p $srcDir
+ cp -r --no-preserve=mode $src/src/* $src/LICENSE $srcDir
+ '';
+
+ buildPhase = let
+ # Paperless has explicit runtime checks that expect these binaries to be in PATH
+ extraBin = lib.makeBinPath [ imagemagick7 ghostscript optipng tesseract unpaper ];
+ in ''
+ ${python.interpreter} -m compileall $srcDir
+
+ makeWrapper $pythonEnv/bin/python $out/bin/paperless \
+ --set PATH ${extraBin} --add-flags $out/share/paperless/manage.py
+
+ # A shell snippet that can be sourced to setup a paperless env
+ cat > $out/share/paperless/setup-env.sh < {}).paperless.withConfig {
+# dataDir = /tmp/paperless-data;
+# config = {
+# PAPERLESS_DISABLE_LOGIN = "true";
+# };
+# }'
+#
+# Setup DB
+# ./paperless migrate
+#
+# Consume documents in ${dataDir}/consume
+# ./paperless document_consumer --oneshot
+#
+# Start web interface
+# ./paperless runserver --noreload localhost:8000
+
+{ config ? {}, dataDir ? null, ocrLanguages ? null
+, paperlessPkg ? paperless, extraCmds ? "" }:
+with lib;
+let
+ paperless = if ocrLanguages == null then
+ paperlessPkg
+ else
+ (paperlessPkg.override {
+ tesseract = paperlessPkg.tesseract.override {
+ enableLanguages = ocrLanguages;
+ };
+ }).overrideDerivation (_: {
+ # `ocrLanguages` might be missing some languages required by the tests.
+ doCheck = false;
+ });
+
+ envVars = (optionalAttrs (dataDir != null) {
+ PAPERLESS_CONSUMPTION_DIR = "${dataDir}/consume";
+ PAPERLESS_MEDIADIR = "${dataDir}/media";
+ PAPERLESS_STATICDIR = "${dataDir}/static";
+ PAPERLESS_DBDIR = "${dataDir}";
+ }) // config;
+
+ envVarDefs = mapAttrsToList (n: v: ''export ${n}="${toString v}"'') envVars;
+ setupEnvVars = builtins.concatStringsSep "\n" envVarDefs;
+
+ setupEnv = ''
+ source ${paperless}/share/paperless/setup-env.sh
+ ${setupEnvVars}
+ ${optionalString (dataDir != null) ''
+ mkdir -p "$PAPERLESS_CONSUMPTION_DIR" \
+ "$PAPERLESS_MEDIADIR" \
+ "$PAPERLESS_STATICDIR" \
+ "$PAPERLESS_DBDIR"
+ ''}
+ '';
+
+ runPaperless = writers.writeBash "paperless" ''
+ set -e
+ ${setupEnv}
+ ${extraCmds}
+ exec python $paperlessSrc/manage.py "$@"
+ '';
+in
+ runPaperless // {
+ inherit paperless setupEnv;
+ }
diff --git a/pkgs/development/python-modules/djangoql/default.nix b/pkgs/development/python-modules/djangoql/default.nix
new file mode 100644
index 000000000000..98f9c04c09fc
--- /dev/null
+++ b/pkgs/development/python-modules/djangoql/default.nix
@@ -0,0 +1,28 @@
+{ lib, buildPythonPackage, fetchPypi, python
+, django, ply }:
+
+buildPythonPackage rec {
+ pname = "djangoql";
+ version = "0.12.6";
+
+ src = fetchPypi {
+ inherit pname version;
+ sha256 = "1mwv1ljznj9mn74ncvcyfmj6ygs8xm2rajpxm88gcac9hhdmk5gs";
+ };
+
+ propagatedBuildInputs = [ ply ];
+
+ checkInputs = [ django ];
+
+ checkPhase = ''
+ export PYTHONPATH=test_project:$PYTHONPATH
+ ${python.executable} test_project/manage.py test core.tests
+ '';
+
+ meta = with lib; {
+ description = "Advanced search language for Django";
+ homepage = https://github.com/ivelum/djangoql;
+ license = licenses.mit;
+ maintainers = with maintainers; [ earvstedt ];
+ };
+}
diff --git a/pkgs/development/python-modules/filemagic/default.nix b/pkgs/development/python-modules/filemagic/default.nix
new file mode 100644
index 000000000000..731b83294a8f
--- /dev/null
+++ b/pkgs/development/python-modules/filemagic/default.nix
@@ -0,0 +1,29 @@
+{ stdenv, lib, buildPythonPackage, fetchFromGitHub, file
+, isPy3k, mock, unittest2 }:
+
+buildPythonPackage rec {
+ pname = "filemagic";
+ version = "1.6";
+
+ # Don't use the PyPI source because it's missing files required for testing
+ src = fetchFromGitHub {
+ owner = "aliles";
+ repo = "filemagic";
+ rev = "138649062f769fb10c256e454a3e94ecfbf3017b";
+ sha256 = "1jxf928jjl2v6zv8kdnfqvywdwql1zqkm1v5xn1d5w0qjcg38d4n";
+ };
+
+ postPatch = ''
+ substituteInPlace magic/api.py --replace "ctypes.util.find_library('magic')" \
+ "'${file}/lib/libmagic${stdenv.hostPlatform.extensions.sharedLibrary}'"
+ '';
+
+ checkInputs = [ (if isPy3k then mock else unittest2) ];
+
+ meta = with lib; {
+ description = "File type identification using libmagic";
+ homepage = https://github.com/aliles/filemagic;
+ license = licenses.asl20;
+ maintainers = with maintainers; [ earvstedt ];
+ };
+}
diff --git a/pkgs/development/python-modules/inotify-simple/default.nix b/pkgs/development/python-modules/inotify-simple/default.nix
new file mode 100644
index 000000000000..2d5e9d780945
--- /dev/null
+++ b/pkgs/development/python-modules/inotify-simple/default.nix
@@ -0,0 +1,22 @@
+{ lib, buildPythonPackage, fetchPypi }:
+
+buildPythonPackage rec {
+ pname = "inotify-simple";
+ version = "1.1.8";
+
+ src = fetchPypi {
+ pname = "inotify_simple";
+ inherit version;
+ sha256 = "1pfqvnynwh318cakldhg7535kbs02asjsgv6s0ki12i7fgfi0b7w";
+ };
+
+ # The package has no tests
+ doCheck = false;
+
+ meta = with lib; {
+ description = "A simple Python wrapper around inotify";
+ homepage = https://github.com/chrisjbillington/inotify_simple;
+ license = licenses.bsd2;
+ maintainers = with maintainers; [ earvstedt ];
+ };
+}
diff --git a/pkgs/development/python-modules/langdetect/default.nix b/pkgs/development/python-modules/langdetect/default.nix
new file mode 100644
index 000000000000..dcd90dd89bdd
--- /dev/null
+++ b/pkgs/development/python-modules/langdetect/default.nix
@@ -0,0 +1,21 @@
+{ lib, buildPythonPackage, fetchPypi, six }:
+
+buildPythonPackage rec {
+ pname = "langdetect";
+ version = "1.0.7";
+
+ src = fetchPypi {
+ inherit pname version;
+ extension = "zip";
+ sha256 = "0c5zm6c7xzsigbb9c7v4r33fcpz911zscfwvh3dq1qxdy3ap18ci";
+ };
+
+ propagatedBuildInputs = [ six ];
+
+ meta = with lib; {
+ description = "Python port of Google's language-detection library";
+ homepage = https://github.com/Mimino666/langdetect;
+ license = licenses.asl20;
+ maintainers = with maintainers; [ earvstedt ];
+ };
+}
diff --git a/pkgs/development/python-modules/pdftotext/default.nix b/pkgs/development/python-modules/pdftotext/default.nix
new file mode 100644
index 000000000000..6c3b1c0cb925
--- /dev/null
+++ b/pkgs/development/python-modules/pdftotext/default.nix
@@ -0,0 +1,20 @@
+{ lib, buildPythonPackage, fetchPypi, poppler }:
+
+buildPythonPackage rec {
+ pname = "pdftotext";
+ version = "2.1.1";
+
+ src = fetchPypi {
+ inherit pname version;
+ sha256 = "1jwc2zpss0983wqqi0kpichasljsxar9c4ma8vycn8maw3pi3bg3";
+ };
+
+ buildInputs = [ poppler ];
+
+ meta = with lib; {
+ description = "Simple PDF text extraction";
+ homepage = https://github.com/jalan/pdftotext;
+ license = licenses.mit;
+ maintainers = with maintainers; [ earvstedt ];
+ };
+}
diff --git a/pkgs/development/python-modules/pytest-env/default.nix b/pkgs/development/python-modules/pytest-env/default.nix
new file mode 100644
index 000000000000..909a193bd7b1
--- /dev/null
+++ b/pkgs/development/python-modules/pytest-env/default.nix
@@ -0,0 +1,20 @@
+{ lib, buildPythonPackage, fetchPypi, pytest }:
+
+buildPythonPackage rec {
+ pname = "pytest-env";
+ version = "0.6.2";
+
+ src = fetchPypi {
+ inherit pname version;
+ sha256 = "1hl0ln0cicdid4qjk7mv90lw9xkb0v71dlj7q7rn89vzxxm9b53y";
+ };
+
+ checkInputs = [ pytest ];
+
+ meta = with lib; {
+ description = "Pytest plugin used to set environment variables";
+ homepage = https://github.com/MobileDynasty/pytest-env;
+ license = licenses.mit;
+ maintainers = with maintainers; [ earvstedt ];
+ };
+}
diff --git a/pkgs/development/python-modules/python-dotenv/default.nix b/pkgs/development/python-modules/python-dotenv/default.nix
new file mode 100644
index 000000000000..37c7d0dae686
--- /dev/null
+++ b/pkgs/development/python-modules/python-dotenv/default.nix
@@ -0,0 +1,20 @@
+{ lib, buildPythonPackage, fetchPypi, click, ipython }:
+
+buildPythonPackage rec {
+ pname = "python-dotenv";
+ version = "0.10.1";
+
+ src = fetchPypi {
+ inherit pname version;
+ sha256 = "1q4sp6ppjiqlsz3h43q9iya4n3qkhx6ng16bcbacfxdyrp9xvcf9";
+ };
+
+ checkInputs = [ click ipython ];
+
+ meta = with lib; {
+ description = "Add .env support to your django/flask apps in development and deployments";
+ homepage = https://github.com/theskumar/python-dotenv;
+ license = licenses.bsdOriginal;
+ maintainers = with maintainers; [ earvstedt ];
+ };
+}
diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix
index 1fd52108366d..dfcb33bfa207 100644
--- a/pkgs/top-level/all-packages.nix
+++ b/pkgs/top-level/all-packages.nix
@@ -4988,6 +4988,8 @@ in
paper-gtk-theme = callPackage ../misc/themes/paper { };
+ paperless = callPackage ../applications/office/paperless { };
+
paperwork = callPackage ../applications/office/paperwork { };
papertrail = callPackage ../tools/text/papertrail { };
diff --git a/pkgs/top-level/python-packages.nix b/pkgs/top-level/python-packages.nix
index 38d82984c488..facfe3ee6859 100644
--- a/pkgs/top-level/python-packages.nix
+++ b/pkgs/top-level/python-packages.nix
@@ -408,6 +408,8 @@ in {
fdint = callPackage ../development/python-modules/fdint { };
+ filemagic = callPackage ../development/python-modules/filemagic { };
+
fuse = callPackage ../development/python-modules/fuse-python {
inherit (pkgs) fuse pkgconfig;
};
@@ -470,6 +472,8 @@ in {
imutils = callPackage ../development/python-modules/imutils { };
+ inotify-simple = callPackage ../development/python-modules/inotify-simple { };
+
intake = callPackage ../development/python-modules/intake { };
intelhex = callPackage ../development/python-modules/intelhex { };
@@ -482,6 +486,8 @@ in {
mpi = pkgs.openmpi;
};
+ langdetect = callPackage ../development/python-modules/langdetect { };
+
libmr = callPackage ../development/python-modules/libmr { };
limitlessled = callPackage ../development/python-modules/limitlessled { };
@@ -578,6 +584,8 @@ in {
pdfminer = callPackage ../development/python-modules/pdfminer_six { };
+ pdftotext = callPackage ../development/python-modules/pdftotext { };
+
pdfx = callPackage ../development/python-modules/pdfx { };
perf = callPackage ../development/python-modules/perf { };
@@ -761,6 +769,8 @@ in {
pytest-click = callPackage ../development/python-modules/pytest-click { };
+ pytest-env = callPackage ../development/python-modules/pytest-env { };
+
pytest-mypy = callPackage ../development/python-modules/pytest-mypy { };
pytest-pylint = callPackage ../development/python-modules/pytest-pylint { };
@@ -771,6 +781,8 @@ in {
python-dbusmock = callPackage ../development/python-modules/python-dbusmock { };
+ python-dotenv = callPackage ../development/python-modules/python-dotenv { };
+
python-engineio = callPackage ../development/python-modules/python-engineio { };
python-hosts = callPackage ../development/python-modules/python-hosts { };
@@ -2491,6 +2503,8 @@ in {
django_pipeline = callPackage ../development/python-modules/django-pipeline { };
+ djangoql = callPackage ../development/python-modules/djangoql { };
+
dj-database-url = callPackage ../development/python-modules/dj-database-url { };
dj-email-url = callPackage ../development/python-modules/dj-email-url { };