From f8c6e1bd9ac5309e027fd3da56168e444f9a383f Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Thu, 11 Sep 2025 11:00:47 +0200 Subject: [PATCH 1/5] added nix's flake --- flake.lock | 61 +++++++++++++ flake.nix | 252 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 313 insertions(+) create mode 100644 flake.lock create mode 100644 flake.nix diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..e5bbead --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1757347588, + "narHash": "sha256-tLdkkC6XnsY9EOZW9TlpesTclELy8W7lL2ClL+nma8o=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b599843bad24621dcaa5ab60dac98f9b0eb1cabe", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..60bfcc7 --- /dev/null +++ b/flake.nix @@ -0,0 +1,252 @@ +{ + description = "Paperless-AI - AI-powered extension for Paperless-ngx"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { + nixpkgs, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + + # Python dependencies + pythonEnv = pkgs.python313.withPackages (ps: + with ps; [ + fastapi + uvicorn + python-dotenv + requests + numpy + pytorch + sentence-transformers + chromadb + rank-bm25 + nltk + tqdm + pydantic + ]); + + # Node.js dependencies are handled by npm/package.json + nodejs = pkgs.nodejs; + + # Build the package + paperless-ai = pkgs.buildNpmPackage { + pname = "paperless-ai"; + version = "1.0.0"; + + src = ./.; + + # The hash of the npm dependencies + # Run `nix build` and it will tell you the correct hash to use + npmDepsHash = "sha256-nAcI3L0fvVI/CdUxWYg8ZiPRDjF7dW+dcIKC3KlHjNQ="; + + nativeBuildInputs = with pkgs; [ + python313 + pythonEnv + sqlite + ]; + + buildInputs = with pkgs; [ + sqlite + ]; + + # Don't run the default npm build script + dontNpmBuild = true; + + postInstall = '' + # Create wrapper scripts + mkdir -p $out/bin + + cat > $out/bin/paperless-ai << EOF + #!${pkgs.bash}/bin/bash + export PATH="${nodejs}/bin:${pythonEnv}/bin:\$PATH" + export NODE_ENV=production + + # Create a writable working directory + WORK_DIR="\''${XDG_DATA_HOME:-\$HOME/.local/share}/paperless-ai" + mkdir -p "\$WORK_DIR" + + # Copy application files if they don't exist + if [ ! -f "\$WORK_DIR/server.js" ]; then + echo "Setting up Paperless-AI in \$WORK_DIR..." + cp -r $out/lib/node_modules/paperless-ai/* "\$WORK_DIR/" + chmod -R u+w "\$WORK_DIR" + fi + + # Ensure data directory exists + mkdir -p "\$WORK_DIR/data" + + cd "\$WORK_DIR" + exec ${nodejs}/bin/node server.js "\$@" + EOF + chmod +x $out/bin/paperless-ai + + cat > $out/bin/paperless-ai-rag << EOF + #!${pkgs.bash}/bin/bash + export PATH="${pythonEnv}/bin:\$PATH" + + # Create a writable working directory + WORK_DIR="\''${XDG_DATA_HOME:-\$HOME/.local/share}/paperless-ai" + mkdir -p "\$WORK_DIR" + + # Copy application files if they don't exist + if [ ! -f "\$WORK_DIR/main.py" ]; then + echo "Setting up Paperless-AI RAG service in \$WORK_DIR..." + cp -r $out/lib/node_modules/paperless-ai/* "\$WORK_DIR/" + chmod -R u+w "\$WORK_DIR" + fi + + # Ensure data directory exists + mkdir -p "\$WORK_DIR/data" + + cd "\$WORK_DIR" + exec ${pythonEnv}/bin/python main.py "\$@" + EOF + chmod +x $out/bin/paperless-ai-rag + ''; + + meta = with pkgs.lib; { + description = "AI-powered extension for Paperless-ngx that brings automatic document classification, smart tagging, and semantic search"; + homepage = "https://github.com/clusterzx/paperless-ai"; + license = licenses.mit; + maintainers = []; + platforms = platforms.linux ++ platforms.darwin; + }; + }; + in { + packages = { + default = paperless-ai; + }; + + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + # Node.js development + nodejs + nodePackages.npm + nodePackages.nodemon + nodePackages.eslint + nodePackages.prettier + + # Python development + pythonEnv + python313Packages.pip + python313Packages.black + python313Packages.flake8 + python313Packages.mypy + + # Database + sqlite + + # Development tools + git + curl + jq + + # Docker (if available) + docker + docker-compose + ]; + + shellHook = '' + echo "🚀 Paperless-AI Development Environment" + echo "Node.js version: $(node --version)" + echo "Python version: $(python --version)" + echo "SQLite version: $(sqlite3 --version)" + echo "" + echo "Available commands:" + echo " npm install - Install Node.js dependencies" + echo " npm test - Start development server with nodemon" + echo " python main.py - Start RAG service" + echo " docker-compose up - Start with Docker" + echo "" + echo "Environment variables to consider setting:" + echo " PAPERLESS_URL - URL to your Paperless-ngx instance" + echo " OPENAI_API_KEY - OpenAI API key (optional)" + echo " RAG_SERVICE_URL - RAG service URL (default: http://localhost:8000)" + echo "" + + # Ensure node_modules exists for development + if [ ! -d "node_modules" ]; then + echo "Installing Node.js dependencies..." + npm install + fi + + # Download NLTK data if needed + python -c " + import nltk + try: + nltk.data.find('tokenizers/punkt') + nltk.data.find('corpora/stopwords') + except LookupError: + print('Downloading required NLTK data...') + nltk.download('punkt') + nltk.download('stopwords') + print('NLTK data downloaded successfully!') + " + ''; + + # Set environment variables + NODE_ENV = "development"; + RAG_SERVICE_URL = "http://localhost:8000"; + RAG_SERVICE_ENABLED = "true"; + }; + + # Additional development shells for specific purposes + devShells.python-only = pkgs.mkShell { + buildInputs = with pkgs; [ + pythonEnv + python313Packages.pip + python313Packages.black + python313Packages.flake8 + python313Packages.mypy + sqlite + ]; + + shellHook = '' + echo "🐍 Python-only development environment for RAG service" + echo "Python version: $(python --version)" + echo "" + echo "Run: python main.py" + ''; + }; + + devShells.nodejs-only = pkgs.mkShell { + buildInputs = with pkgs; [ + nodejs + nodePackages.npm + nodePackages.nodemon + nodePackages.eslint + nodePackages.prettier + sqlite + ]; + + shellHook = '' + echo "📦 Node.js-only development environment for web service" + echo "Node.js version: $(node --version)" + echo "" + echo "Run: npm test (or nodemon server.js)" + ''; + }; + + # Apps for easy running + apps = { + default = { + type = "app"; + program = "${paperless-ai}/bin/paperless-ai"; + }; + paperless-ai = { + type = "app"; + program = "${paperless-ai}/bin/paperless-ai"; + }; + rag-service = { + type = "app"; + program = "${paperless-ai}/bin/paperless-ai-rag"; + }; + }; + }); +} From 5ecb9f84b7edbfe3bf2a0966e313e09e0aae11ba Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Thu, 11 Sep 2025 11:35:56 +0200 Subject: [PATCH 2/5] updated flake with service --- flake.nix | 316 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 202 insertions(+), 114 deletions(-) diff --git a/flake.nix b/flake.nix index 60bfcc7..aeff964 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Paperless-AI - AI-powered extension for Paperless-ngx"; + description = "Paperless-AI - AI-powered extension for Paperless-ngx with automatic document classification, smart tagging, and semantic search"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; @@ -14,35 +14,33 @@ flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; - # Python dependencies + # Python environment with all required packages for RAG service pythonEnv = pkgs.python313.withPackages (ps: with ps; [ - fastapi - uvicorn - python-dotenv - requests - numpy - pytorch - sentence-transformers - chromadb - rank-bm25 - nltk - tqdm - pydantic + fastapi # Web framework for RAG API + uvicorn # ASGI server + python-dotenv # Environment file loading + requests # HTTP client + numpy # Numerical computing + pytorch # Machine learning framework + sentence-transformers # Text embeddings + chromadb # Vector database + rank-bm25 # BM25 ranking algorithm + nltk # Natural language processing + tqdm # Progress bars + pydantic # Data validation ]); - # Node.js dependencies are handled by npm/package.json + # Node.js runtime (dependencies managed via npm) nodejs = pkgs.nodejs; - # Build the package + # Main Paperless-AI package built with buildNpmPackage for proper npm dependency management paperless-ai = pkgs.buildNpmPackage { pname = "paperless-ai"; version = "1.0.0"; src = ./.; - # The hash of the npm dependencies - # Run `nix build` and it will tell you the correct hash to use npmDepsHash = "sha256-nAcI3L0fvVI/CdUxWYg8ZiPRDjF7dW+dcIKC3KlHjNQ="; nativeBuildInputs = with pkgs; [ @@ -59,26 +57,28 @@ dontNpmBuild = true; postInstall = '' - # Create wrapper scripts + # Create wrapper scripts that handle the read-only Nix store limitation + # by copying the application to a writable directory on first run mkdir -p $out/bin + # Web service wrapper (Node.js + Express) cat > $out/bin/paperless-ai << EOF #!${pkgs.bash}/bin/bash export PATH="${nodejs}/bin:${pythonEnv}/bin:\$PATH" export NODE_ENV=production - # Create a writable working directory + # Create a writable working directory using XDG Base Directory specification WORK_DIR="\''${XDG_DATA_HOME:-\$HOME/.local/share}/paperless-ai" mkdir -p "\$WORK_DIR" - # Copy application files if they don't exist + # Copy application files if they don't exist (first run setup) if [ ! -f "\$WORK_DIR/server.js" ]; then echo "Setting up Paperless-AI in \$WORK_DIR..." cp -r $out/lib/node_modules/paperless-ai/* "\$WORK_DIR/" chmod -R u+w "\$WORK_DIR" fi - # Ensure data directory exists + # Ensure data directory exists for SQLite database and config files mkdir -p "\$WORK_DIR/data" cd "\$WORK_DIR" @@ -86,22 +86,23 @@ EOF chmod +x $out/bin/paperless-ai + # RAG service wrapper (Python + FastAPI) cat > $out/bin/paperless-ai-rag << EOF #!${pkgs.bash}/bin/bash export PATH="${pythonEnv}/bin:\$PATH" - # Create a writable working directory + # Create a writable working directory using XDG Base Directory specification WORK_DIR="\''${XDG_DATA_HOME:-\$HOME/.local/share}/paperless-ai" mkdir -p "\$WORK_DIR" - # Copy application files if they don't exist + # Copy application files if they don't exist (first run setup) if [ ! -f "\$WORK_DIR/main.py" ]; then echo "Setting up Paperless-AI RAG service in \$WORK_DIR..." cp -r $out/lib/node_modules/paperless-ai/* "\$WORK_DIR/" chmod -R u+w "\$WORK_DIR" fi - # Ensure data directory exists + # Ensure data directory exists for vector database and embeddings mkdir -p "\$WORK_DIR/data" cd "\$WORK_DIR" @@ -118,122 +119,209 @@ platforms = platforms.linux ++ platforms.darwin; }; }; + # NixOS service module for production deployment + # Creates systemd services for both web interface and RAG API + paperless-ai-service = { + config, + lib, + pkgs, + ... + }: + with lib; let + cfg = config.services.paperless-ai; + stateDir = "/var/lib/paperless-ai"; + in { + options.services.paperless-ai = { + enable = mkEnableOption "Paperless-AI service"; + + user = mkOption { + type = types.str; + default = "paperless-ai"; + description = "User account under which Paperless-AI runs."; + }; + + group = mkOption { + type = types.str; + default = "paperless-ai"; + description = "Group account under which Paperless-AI runs."; + }; + + webPort = mkOption { + type = types.port; + default = 3000; + description = "Port for the web service."; + }; + + ragPort = mkOption { + type = types.port; + default = 8000; + description = "Port for the RAG service."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for Paperless-AI ports."; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Environment file containing secrets like API keys."; + }; + + extraEnvironment = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Extra environment variables for Paperless-AI."; + }; + }; + + config = mkIf cfg.enable { + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = stateDir; + createHome = true; + }; + + users.groups.${cfg.group} = {}; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ + cfg.webPort + cfg.ragPort + ]; + + systemd.services.paperless-ai-web = { + description = "Paperless-AI Web Service"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = stateDir; + ExecStart = "${paperless-ai}/bin/paperless-ai"; + Restart = "always"; + RestartSec = "10"; + + # Security settings + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [stateDir]; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + + environment = + { + NODE_ENV = "production"; + PORT = toString cfg.webPort; + RAG_SERVICE_URL = "http://localhost:${toString cfg.ragPort}"; + RAG_SERVICE_ENABLED = "true"; + XDG_DATA_HOME = stateDir; + } + // cfg.extraEnvironment; + + serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + preStart = '' + # Ensure proper permissions + chmod 755 ${stateDir} + mkdir -p ${stateDir}/data + chmod 755 ${stateDir}/data + ''; + }; + + systemd.services.paperless-ai-rag = { + description = "Paperless-AI RAG Service"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = stateDir; + ExecStart = "${paperless-ai}/bin/paperless-ai-rag"; + Restart = "always"; + RestartSec = "10"; + + # Security settings + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [stateDir]; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + + environment = + { + PORT = toString cfg.ragPort; + XDG_DATA_HOME = stateDir; + } + // cfg.extraEnvironment; + + serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + preStart = '' + # Ensure proper permissions + chmod 755 ${stateDir} + mkdir -p ${stateDir}/data + chmod 755 ${stateDir}/data + ''; + }; + }; + }; in { + # Package outputs packages = { default = paperless-ai; + paperless-ai = paperless-ai; + }; + + # NixOS service modules for production deployment + nixosModules = { + default = paperless-ai-service; + paperless-ai = paperless-ai-service; }; + # Development environments devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ - # Node.js development + # Node.js development stack nodejs nodePackages.npm nodePackages.nodemon nodePackages.eslint nodePackages.prettier - # Python development + # Python development stack pythonEnv python313Packages.pip python313Packages.black python313Packages.flake8 python313Packages.mypy - # Database + # Database tools sqlite - # Development tools + # Development utilities git curl jq - - # Docker (if available) - docker - docker-compose ]; - - shellHook = '' - echo "🚀 Paperless-AI Development Environment" - echo "Node.js version: $(node --version)" - echo "Python version: $(python --version)" - echo "SQLite version: $(sqlite3 --version)" - echo "" - echo "Available commands:" - echo " npm install - Install Node.js dependencies" - echo " npm test - Start development server with nodemon" - echo " python main.py - Start RAG service" - echo " docker-compose up - Start with Docker" - echo "" - echo "Environment variables to consider setting:" - echo " PAPERLESS_URL - URL to your Paperless-ngx instance" - echo " OPENAI_API_KEY - OpenAI API key (optional)" - echo " RAG_SERVICE_URL - RAG service URL (default: http://localhost:8000)" - echo "" - - # Ensure node_modules exists for development - if [ ! -d "node_modules" ]; then - echo "Installing Node.js dependencies..." - npm install - fi - - # Download NLTK data if needed - python -c " - import nltk - try: - nltk.data.find('tokenizers/punkt') - nltk.data.find('corpora/stopwords') - except LookupError: - print('Downloading required NLTK data...') - nltk.download('punkt') - nltk.download('stopwords') - print('NLTK data downloaded successfully!') - " - ''; - - # Set environment variables - NODE_ENV = "development"; - RAG_SERVICE_URL = "http://localhost:8000"; - RAG_SERVICE_ENABLED = "true"; - }; - - # Additional development shells for specific purposes - devShells.python-only = pkgs.mkShell { - buildInputs = with pkgs; [ - pythonEnv - python313Packages.pip - python313Packages.black - python313Packages.flake8 - python313Packages.mypy - sqlite - ]; - - shellHook = '' - echo "🐍 Python-only development environment for RAG service" - echo "Python version: $(python --version)" - echo "" - echo "Run: python main.py" - ''; - }; - - devShells.nodejs-only = pkgs.mkShell { - buildInputs = with pkgs; [ - nodejs - nodePackages.npm - nodePackages.nodemon - nodePackages.eslint - nodePackages.prettier - sqlite - ]; - - shellHook = '' - echo "📦 Node.js-only development environment for web service" - echo "Node.js version: $(node --version)" - echo "" - echo "Run: npm test (or nodemon server.js)" - ''; }; - # Apps for easy running apps = { default = { type = "app"; From 41fcfef7cc931771d6bba8748b050f7c9abb8d5e Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Thu, 11 Sep 2025 11:54:21 +0200 Subject: [PATCH 3/5] move module to seperat file --- flake.nix | 174 ++--------------------------------------------------- module.nix | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 168 deletions(-) create mode 100644 module.nix diff --git a/flake.nix b/flake.nix index aeff964..e8ab225 100644 --- a/flake.nix +++ b/flake.nix @@ -7,11 +7,16 @@ }; outputs = { + self, nixpkgs, flake-utils, ... }: - flake-utils.lib.eachDefaultSystem (system: let + { + nixosModules.default = import ./module.nix; + nixosModule = self.nixosModules.default; + } + // flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; # Python environment with all required packages for RAG service @@ -121,167 +126,6 @@ }; # NixOS service module for production deployment # Creates systemd services for both web interface and RAG API - paperless-ai-service = { - config, - lib, - pkgs, - ... - }: - with lib; let - cfg = config.services.paperless-ai; - stateDir = "/var/lib/paperless-ai"; - in { - options.services.paperless-ai = { - enable = mkEnableOption "Paperless-AI service"; - - user = mkOption { - type = types.str; - default = "paperless-ai"; - description = "User account under which Paperless-AI runs."; - }; - - group = mkOption { - type = types.str; - default = "paperless-ai"; - description = "Group account under which Paperless-AI runs."; - }; - - webPort = mkOption { - type = types.port; - default = 3000; - description = "Port for the web service."; - }; - - ragPort = mkOption { - type = types.port; - default = 8000; - description = "Port for the RAG service."; - }; - - openFirewall = mkOption { - type = types.bool; - default = false; - description = "Whether to open the firewall for Paperless-AI ports."; - }; - - environmentFile = mkOption { - type = types.nullOr types.path; - default = null; - description = "Environment file containing secrets like API keys."; - }; - - extraEnvironment = mkOption { - type = types.attrsOf types.str; - default = {}; - description = "Extra environment variables for Paperless-AI."; - }; - }; - - config = mkIf cfg.enable { - users.users.${cfg.user} = { - isSystemUser = true; - group = cfg.group; - home = stateDir; - createHome = true; - }; - - users.groups.${cfg.group} = {}; - - networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ - cfg.webPort - cfg.ragPort - ]; - - systemd.services.paperless-ai-web = { - description = "Paperless-AI Web Service"; - wantedBy = ["multi-user.target"]; - after = ["network.target"]; - - serviceConfig = { - Type = "simple"; - User = cfg.user; - Group = cfg.group; - WorkingDirectory = stateDir; - ExecStart = "${paperless-ai}/bin/paperless-ai"; - Restart = "always"; - RestartSec = "10"; - - # Security settings - NoNewPrivileges = true; - PrivateTmp = true; - ProtectSystem = "strict"; - ProtectHome = true; - ReadWritePaths = [stateDir]; - PrivateDevices = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - }; - - environment = - { - NODE_ENV = "production"; - PORT = toString cfg.webPort; - RAG_SERVICE_URL = "http://localhost:${toString cfg.ragPort}"; - RAG_SERVICE_ENABLED = "true"; - XDG_DATA_HOME = stateDir; - } - // cfg.extraEnvironment; - - serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; - - preStart = '' - # Ensure proper permissions - chmod 755 ${stateDir} - mkdir -p ${stateDir}/data - chmod 755 ${stateDir}/data - ''; - }; - - systemd.services.paperless-ai-rag = { - description = "Paperless-AI RAG Service"; - wantedBy = ["multi-user.target"]; - after = ["network.target"]; - - serviceConfig = { - Type = "simple"; - User = cfg.user; - Group = cfg.group; - WorkingDirectory = stateDir; - ExecStart = "${paperless-ai}/bin/paperless-ai-rag"; - Restart = "always"; - RestartSec = "10"; - - # Security settings - NoNewPrivileges = true; - PrivateTmp = true; - ProtectSystem = "strict"; - ProtectHome = true; - ReadWritePaths = [stateDir]; - PrivateDevices = true; - ProtectKernelTunables = true; - ProtectKernelModules = true; - ProtectControlGroups = true; - }; - - environment = - { - PORT = toString cfg.ragPort; - XDG_DATA_HOME = stateDir; - } - // cfg.extraEnvironment; - - serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; - - preStart = '' - # Ensure proper permissions - chmod 755 ${stateDir} - mkdir -p ${stateDir}/data - chmod 755 ${stateDir}/data - ''; - }; - }; - }; in { # Package outputs packages = { @@ -289,12 +133,6 @@ paperless-ai = paperless-ai; }; - # NixOS service modules for production deployment - nixosModules = { - default = paperless-ai-service; - paperless-ai = paperless-ai-service; - }; - # Development environments devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ diff --git a/module.nix b/module.nix new file mode 100644 index 0000000..e32c711 --- /dev/null +++ b/module.nix @@ -0,0 +1,165 @@ +{ + config, + lib, + ... +}: +with lib; let + cfg = config.services.paperless-ai; + stateDir = "/var/lib/paperless-ai"; +in { + options.services.paperless-ai = { + enable = mkEnableOption "Paperless-AI service"; + + package = mkOption { + type = types.package; + description = "The Helios package to use."; + }; + + user = mkOption { + type = types.str; + default = "paperless-ai"; + description = "User account under which Paperless-AI runs."; + }; + + group = mkOption { + type = types.str; + default = "paperless-ai"; + description = "Group account under which Paperless-AI runs."; + }; + + webPort = mkOption { + type = types.port; + default = 3000; + description = "Port for the web service."; + }; + + ragPort = mkOption { + type = types.port; + default = 8000; + description = "Port for the RAG service."; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for Paperless-AI ports."; + }; + + environmentFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Environment file containing secrets like API keys."; + }; + + extraEnvironment = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Extra environment variables for Paperless-AI."; + }; + }; + + config = mkIf cfg.enable { + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + home = stateDir; + createHome = true; + }; + + users.groups.${cfg.group} = {}; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ + cfg.webPort + cfg.ragPort + ]; + + systemd.services.paperless-ai-web = { + description = "Paperless-AI Web Service"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = stateDir; + ExecStart = "${cfg.package}/bin/paperless-ai"; + Restart = "always"; + RestartSec = "10"; + + # Security settings + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [stateDir]; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + + environment = + { + NODE_ENV = "production"; + PORT = toString cfg.webPort; + RAG_SERVICE_URL = "http://localhost:${toString cfg.ragPort}"; + RAG_SERVICE_ENABLED = "true"; + XDG_DATA_HOME = stateDir; + } + // cfg.extraEnvironment; + + serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + preStart = '' + # Ensure proper permissions + chmod 755 ${stateDir} + mkdir -p ${stateDir}/data + chmod 755 ${stateDir}/data + ''; + }; + + systemd.services.paperless-ai-rag = { + description = "Paperless-AI RAG Service"; + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = stateDir; + ExecStart = "${cfg.package}/bin/paperless-ai-rag"; + Restart = "always"; + RestartSec = "10"; + + # Security settings + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [stateDir]; + PrivateDevices = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + }; + + environment = + { + PORT = toString cfg.ragPort; + XDG_DATA_HOME = stateDir; + } + // cfg.extraEnvironment; + + serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + preStart = '' + # Ensure proper permissions + chmod 755 ${stateDir} + mkdir -p ${stateDir}/data + chmod 755 ${stateDir}/data + ''; + }; + }; +} From 7bc21c0797c00889bf6c64366bb8d43300e18aa6 Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Thu, 11 Sep 2025 15:09:35 +0200 Subject: [PATCH 4/5] fix npm package --- flake.nix | 108 ++++++++++++--------------------------------------- module.nix | 7 +++- package.json | 3 ++ 3 files changed, 33 insertions(+), 85 deletions(-) diff --git a/flake.nix b/flake.nix index e8ab225..f773849 100644 --- a/flake.nix +++ b/flake.nix @@ -49,8 +49,6 @@ npmDepsHash = "sha256-nAcI3L0fvVI/CdUxWYg8ZiPRDjF7dW+dcIKC3KlHjNQ="; nativeBuildInputs = with pkgs; [ - python313 - pythonEnv sqlite ]; @@ -61,61 +59,6 @@ # Don't run the default npm build script dontNpmBuild = true; - postInstall = '' - # Create wrapper scripts that handle the read-only Nix store limitation - # by copying the application to a writable directory on first run - mkdir -p $out/bin - - # Web service wrapper (Node.js + Express) - cat > $out/bin/paperless-ai << EOF - #!${pkgs.bash}/bin/bash - export PATH="${nodejs}/bin:${pythonEnv}/bin:\$PATH" - export NODE_ENV=production - - # Create a writable working directory using XDG Base Directory specification - WORK_DIR="\''${XDG_DATA_HOME:-\$HOME/.local/share}/paperless-ai" - mkdir -p "\$WORK_DIR" - - # Copy application files if they don't exist (first run setup) - if [ ! -f "\$WORK_DIR/server.js" ]; then - echo "Setting up Paperless-AI in \$WORK_DIR..." - cp -r $out/lib/node_modules/paperless-ai/* "\$WORK_DIR/" - chmod -R u+w "\$WORK_DIR" - fi - - # Ensure data directory exists for SQLite database and config files - mkdir -p "\$WORK_DIR/data" - - cd "\$WORK_DIR" - exec ${nodejs}/bin/node server.js "\$@" - EOF - chmod +x $out/bin/paperless-ai - - # RAG service wrapper (Python + FastAPI) - cat > $out/bin/paperless-ai-rag << EOF - #!${pkgs.bash}/bin/bash - export PATH="${pythonEnv}/bin:\$PATH" - - # Create a writable working directory using XDG Base Directory specification - WORK_DIR="\''${XDG_DATA_HOME:-\$HOME/.local/share}/paperless-ai" - mkdir -p "\$WORK_DIR" - - # Copy application files if they don't exist (first run setup) - if [ ! -f "\$WORK_DIR/main.py" ]; then - echo "Setting up Paperless-AI RAG service in \$WORK_DIR..." - cp -r $out/lib/node_modules/paperless-ai/* "\$WORK_DIR/" - chmod -R u+w "\$WORK_DIR" - fi - - # Ensure data directory exists for vector database and embeddings - mkdir -p "\$WORK_DIR/data" - - cd "\$WORK_DIR" - exec ${pythonEnv}/bin/python main.py "\$@" - EOF - chmod +x $out/bin/paperless-ai-rag - ''; - meta = with pkgs.lib; { description = "AI-powered extension for Paperless-ngx that brings automatic document classification, smart tagging, and semantic search"; homepage = "https://github.com/clusterzx/paperless-ai"; @@ -124,54 +67,53 @@ platforms = platforms.linux ++ platforms.darwin; }; }; - # NixOS service module for production deployment - # Creates systemd services for both web interface and RAG API + + paperless-ai-rag = + pkgs.writeShellScriptBin "paperless-ai-rag" '' + export PATH="${pythonEnv}/bin:$PATH" + exec ${pythonEnv}/bin/python main.py "$@" + '' + // { + meta = with pkgs.lib; { + description = "RAG (Retrieval-Augmented Generation) service for Paperless-AI - semantic search and document Q&A"; + homepage = "https://github.com/clusterzx/paperless-ai"; + license = licenses.mit; + maintainers = []; + platforms = platforms.linux ++ platforms.darwin; + mainProgram = "paperless-ai-rag"; + }; + }; in { # Package outputs packages = { default = paperless-ai; paperless-ai = paperless-ai; + paperless-ai-rag = paperless-ai-rag; }; # Development environments devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ - # Node.js development stack nodejs nodePackages.npm - nodePackages.nodemon - nodePackages.eslint - nodePackages.prettier - - # Python development stack pythonEnv - python313Packages.pip - python313Packages.black - python313Packages.flake8 - python313Packages.mypy - - # Database tools - sqlite - - # Development utilities - git - curl - jq ]; }; apps = { - default = { - type = "app"; - program = "${paperless-ai}/bin/paperless-ai"; - }; paperless-ai = { type = "app"; program = "${paperless-ai}/bin/paperless-ai"; + meta = { + description = "AI-powered extension for Paperless-ngx with automatic document classification, smart tagging, and semantic search"; + }; }; - rag-service = { + paperless-ai-rag = { type = "app"; - program = "${paperless-ai}/bin/paperless-ai-rag"; + program = "${paperless-ai-rag}/bin/paperless-ai-rag"; + meta = { + description = "RAG (Retrieval-Augmented Generation) service for Paperless-AI - semantic search and document Q&A"; + }; }; }; }); diff --git a/module.nix b/module.nix index e32c711..0dd8b74 100644 --- a/module.nix +++ b/module.nix @@ -12,7 +12,10 @@ in { package = mkOption { type = types.package; - description = "The Helios package to use."; + }; + + rag-package = mkOption { + type = types.package; }; user = mkOption { @@ -129,7 +132,7 @@ in { User = cfg.user; Group = cfg.group; WorkingDirectory = stateDir; - ExecStart = "${cfg.package}/bin/paperless-ai-rag"; + ExecStart = "${cfg.rag-package}/bin/paperless-ai-rag"; Restart = "always"; RestartSec = "10"; diff --git a/package.json b/package.json index c823d6a..f3cf607 100644 --- a/package.json +++ b/package.json @@ -64,5 +64,8 @@ "@scarf/scarf", "better-sqlite3" ] + }, + "bin": { + "paperless-ai": "./server.js" } } From d2c2ade9403673b4efb3d3b7243e7087a41679b7 Mon Sep 17 00:00:00 2001 From: Martin Becze Date: Thu, 11 Sep 2025 15:28:58 +0200 Subject: [PATCH 5/5] fixed port --- flake.nix | 35 +++++++++++++++++++++-------------- module.nix | 4 ++-- 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/flake.nix b/flake.nix index f773849..e95a664 100644 --- a/flake.nix +++ b/flake.nix @@ -68,21 +68,28 @@ }; }; - paperless-ai-rag = - pkgs.writeShellScriptBin "paperless-ai-rag" '' - export PATH="${pythonEnv}/bin:$PATH" - exec ${pythonEnv}/bin/python main.py "$@" - '' - // { - meta = with pkgs.lib; { - description = "RAG (Retrieval-Augmented Generation) service for Paperless-AI - semantic search and document Q&A"; - homepage = "https://github.com/clusterzx/paperless-ai"; - license = licenses.mit; - maintainers = []; - platforms = platforms.linux ++ platforms.darwin; - mainProgram = "paperless-ai-rag"; - }; + paperless-ai-rag = pkgs.writeShellApplication { + name = "paperless-ai-rag"; + runtimeInputs = [pythonEnv]; + text = '' + WORK_DIR="$HOME/.local/share/paperless-ai-rag" + mkdir -p "$WORK_DIR/data" + + if [ ! -f "$WORK_DIR/main.py" ]; then + echo "Setting up Paperless-AI RAG service in $WORK_DIR..." + cp ${./main.py} "$WORK_DIR/main.py" + chmod u+w "$WORK_DIR/main.py" + fi + + cd "$WORK_DIR" + exec python main.py "$@" + ''; + meta = with pkgs.lib; { + description = "RAG service for Paperless-AI - semantic search and document Q&A"; + license = licenses.mit; + platforms = platforms.linux ++ platforms.darwin; }; + }; in { # Package outputs packages = { diff --git a/module.nix b/module.nix index 0dd8b74..1b54a9e 100644 --- a/module.nix +++ b/module.nix @@ -105,14 +105,14 @@ in { environment = { NODE_ENV = "production"; - PORT = toString cfg.webPort; + PAPERLESS_AI_PORT = toString cfg.webPort; RAG_SERVICE_URL = "http://localhost:${toString cfg.ragPort}"; RAG_SERVICE_ENABLED = "true"; XDG_DATA_HOME = stateDir; } // cfg.extraEnvironment; - serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + # serviceConfig.EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; preStart = '' # Ensure proper permissions