1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.scanservjs;
10 settings = {
11 scanimage = lib.getExe' config.hardware.sane.backends-package "scanimage";
12 convert = lib.getExe' pkgs.imagemagick "convert";
13 tesseract = lib.getExe pkgs.tesseract;
14 # it defaults to config/devices.json, but "config" dir doesn't exist by default and scanservjs doesn't create it
15 devicesPath = "devices.json";
16 } // cfg.settings;
17 settingsFormat = pkgs.formats.json { };
18
19 leafs =
20 attrs:
21 builtins.concatLists (
22 lib.mapAttrsToList (k: v: if builtins.isAttrs v then leafs v else [ v ]) attrs
23 );
24
25 package = pkgs.scanservjs;
26
27 configFile = pkgs.writeText "config.local.js" ''
28 /* eslint-disable no-unused-vars */
29 module.exports = {
30 afterConfig(config) {
31 ${builtins.concatStringsSep "" (
32 leafs (
33 lib.mapAttrsRecursive (path: val: ''
34 ${builtins.concatStringsSep "." path} = ${builtins.toJSON val};
35 '') { config = settings; }
36 )
37 )}
38 ${cfg.extraConfig}
39 },
40
41 afterDevices(devices) {
42 ${cfg.extraDevicesConfig}
43 },
44
45 async afterScan(fileInfo) {
46 ${cfg.runAfterScan}
47 },
48
49 actions: [
50 ${builtins.concatStringsSep ",\n" cfg.extraActions}
51 ],
52 };
53 '';
54
55in
56{
57 options.services.scanservjs = {
58 enable = lib.mkEnableOption "scanservjs";
59 stateDir = lib.mkOption {
60 type = lib.types.str;
61 default = "/var/lib/scanservjs";
62 description = ''
63 State directory for scanservjs.
64 '';
65 };
66 settings = lib.mkOption {
67 default = { };
68 description = ''
69 Config to set in config.local.js's `afterConfig`.
70 '';
71 type = lib.types.submodule {
72 freeformType = settingsFormat.type;
73 options.host = lib.mkOption {
74 type = lib.types.str;
75 description = "The IP to listen on.";
76 default = "127.0.0.1";
77 };
78 options.port = lib.mkOption {
79 type = lib.types.port;
80 description = "The port to listen on.";
81 default = 8080;
82 };
83 };
84 };
85 extraConfig = lib.mkOption {
86 default = "";
87 type = lib.types.lines;
88 description = ''
89 Extra code to add to config.local.js's `afterConfig`.
90 '';
91 };
92 extraDevicesConfig = lib.mkOption {
93 default = "";
94 type = lib.types.lines;
95 description = ''
96 Extra code to add to config.local.js's `afterDevices`.
97 '';
98 };
99 runAfterScan = lib.mkOption {
100 default = "";
101 type = lib.types.lines;
102 description = ''
103 Extra code to add to config.local.js's `afterScan`.
104 '';
105 };
106 extraActions = lib.mkOption {
107 default = [ ];
108 type = lib.types.listOf lib.types.lines;
109 description = "Actions to add to config.local.js's `actions`.";
110 };
111 };
112
113 config = lib.mkIf cfg.enable {
114 hardware.sane.enable = true;
115 users.users.scanservjs = {
116 group = "scanservjs";
117 extraGroups = [
118 "scanner"
119 "lp"
120 ];
121 home = cfg.stateDir;
122 isSystemUser = true;
123 createHome = true;
124 };
125 users.groups.scanservjs = { };
126
127 systemd.services.scanservjs = {
128 description = "scanservjs";
129 after = [ "network.target" ];
130 wantedBy = [ "multi-user.target" ];
131 # yes, those paths are configurable, but the config option isn't always used...
132 # a lot of the time scanservjs just takes those from PATH
133 path = with pkgs; [
134 coreutils
135 config.hardware.sane.backends-package
136 imagemagick
137 tesseract
138 ];
139 environment = {
140 NIX_SCANSERVJS_CONFIG_PATH = configFile;
141 SANE_CONFIG_DIR = "/etc/sane-config";
142 LD_LIBRARY_PATH = "/etc/sane-libs";
143 };
144 serviceConfig = {
145 ExecStart = lib.getExe package;
146 Restart = "always";
147 User = "scanservjs";
148 Group = "scanservjs";
149 WorkingDirectory = cfg.stateDir;
150 };
151 };
152 };
153}