{ config, lib, pkgs, utils, ... }: let cfg = config.services.archivebox; jobUnitName = name: "archivebox-job-${utils.escapeSystemdPath name}"; jobType = { name, options, ... }: { options = { urls = lib.mkOption { type = with lib.types; listOf str; description = "List of links to archive."; example = lib.literalExpression '' [ "https://guix.gnu.org/feeds/blog.atom" "https://nixos.org/blog/announcements-rss.xml" ] ''; }; extraArgs = lib.mkOption { type = with lib.types; listOf str; description = '' Additional arguments for adding links (i.e., {command}`archivebox add $LINK`) from {option}`links`. ''; default = [ ]; example = lib.literalExpression '' [ "--depth" "1" ] ''; }; startAt = lib.mkOption { type = with lib.types; str; description = '' Indicates how frequent the scheduled archiving will occur. Should be a valid string format as described from {manpage}`systemd.time(5)`. ''; default = "weekly"; defaultText = "weekly"; example = "*-*-01/2"; }; }; }; mkJobService = name: value: lib.nameValuePair (jobUnitName name) { description = "Archivebox download group '${name}'"; after = [ "network.target" ]; documentation = [ "https://docs.archivebox.io/" ]; preStart = '' mkdir -p ${lib.escapeShellArg cfg.archivePath} ''; path = [ cfg.package ] ++ cfg.extraPackages; script = '' echo "${lib.concatStringsSep "\n" value.urls}" \ | archivebox add ${lib.concatStringsSep " " value.extraArgs} ''; serviceConfig = { User = "archivebox"; Group = "archivebox"; LockPersonality = true; NoNewPrivileges = true; CapabilityBoundingSet = [ ]; AmbientCapabilities = [ ]; PrivateTmp = true; PrivateDevices = true; ProtectControlGroups = true; ProtectClock = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; ProtectProc = "invisible"; ProtectHome = true; ProtectSystem = "strict"; RestrictAddressFamilies = [ "AF_LOCAL" "AF_INET" "AF_INET6" ]; RestrictNamespaces = true; SystemCallFilter = [ "@system-service" ]; SystemCallErrorNumber = "EPERM"; StateDirectory = "archivebox"; }; }; mkTimerUnit = name: value: lib.nameValuePair (jobUnitName name) { description = "Archivebox download job '${name}'"; after = [ "network.target" ]; documentation = [ "https://docs.archivebox.io/" ]; timerConfig = { Persistent = true; OnCalendar = value.startAt; RandomizedDelaySec = 120; }; wantedBy = [ "timers.target" ]; }; in { options.services.archivebox = { enable = lib.mkEnableOption "Archivebox service"; package = lib.mkPackageOption pkgs "archivebox" { }; jobs = lib.mkOption { type = with lib.types; attrsOf (submodule jobType); description = "A map of archiving tasks for the service."; default = { }; defaultText = lib.literalExpression "{}"; example = { illustration = { urls = [ "https://www.davidrevoy.com/" "https://www.youtube.com/c/ronillust" ]; startAt = "weekly"; }; research = { urls = [ "https://arxiv.org/rss/cs" "https://distill.pub/" ]; extraArgs = [ "--depth 1" ]; startAt = "daily"; }; }; }; extraPackages = lib.mkOption { type = with lib.types; listOf package; description = '' A list of additional packages to be set within the download jobs. By default, it sets the optional dependencies of ArchiveBox for additional download formats and capabilities. ''; default = with pkgs; [ chromium nodejs_latest wget curl yt-dlp ] ++ lib.optional config.programs.git.enable config.programs.git.package; example = lib.literalExpression '' with pkgs; [ curl yt-dlp ] ''; }; webserver = { enable = lib.mkEnableOption "ArchiveBox web server"; port = lib.mkOption { type = lib.types.port; description = "The port number to be used for the server at localhost."; default = 8000; example = 8888; }; }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ { systemd.services = lib.mapAttrs' mkJobService cfg.jobs; systemd.timers = lib.mapAttrs' mkTimerUnit cfg.jobs; users.users.archivebox = { group = config.users.groups.archivebox.name; isNormalUser = true; home = "/var/lib/archivebox"; }; } (lib.mkIf cfg.webserver.enable { systemd.services.archivebox-server = { description = "Archivebox web server"; after = [ "network.target" ]; documentation = [ "https://docs.archivebox.io/" ]; wantedBy = [ "graphical-session.target" ]; serviceConfig = { User = "archivebox"; Group = "archivebox"; ExecStart = "${pkgs.archivebox}/bin/archivebox server localhost:${ toString cfg.webserver.port }"; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; Restart = "on-failure"; LockPersonality = true; NoNewPrivileges = true; PrivateTmp = true; PrivateUsers = true; PrivateDevices = true; ProtectControlGroups = true; ProtectClock = true; ProtectKernelLogs = true; ProtectKernelModules = true; ProtectKernelTunables = true; RestrictAddressFamilies = [ "AF_LOCAL" "AF_INET" "AF_INET6" ]; RestrictNamespaces = true; SystemCallFilter = [ "@system-service" ]; SystemCallErrorNumber = "EPERM"; StateDirectory = "archivebox"; }; }; }) ]); }