1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.locate;
7 isMLocate = hasPrefix "mlocate" cfg.locate.name;
8 isPLocate = hasPrefix "plocate" cfg.locate.name;
9 isMorPLocate = (isMLocate || isPLocate);
10 isFindutils = hasPrefix "findutils" cfg.locate.name;
11in
12{
13 imports = [
14 (mkRenamedOptionModule [ "services" "locate" "period" ] [ "services" "locate" "interval" ])
15 (mkRemovedOptionModule [ "services" "locate" "includeStore" ] "Use services.locate.prunePaths")
16 ];
17
18 options.services.locate = with types; {
19 enable = mkOption {
20 type = bool;
21 default = false;
22 description = lib.mdDoc ''
23 If enabled, NixOS will periodically update the database of
24 files used by the {command}`locate` command.
25 '';
26 };
27
28 locate = mkOption {
29 type = package;
30 default = pkgs.findutils.locate;
31 defaultText = literalExpression "pkgs.findutils";
32 example = literalExpression "pkgs.mlocate";
33 description = lib.mdDoc ''
34 The locate implementation to use
35 '';
36 };
37
38 interval = mkOption {
39 type = str;
40 default = "02:15";
41 example = "hourly";
42 description = lib.mdDoc ''
43 Update the locate database at this interval. Updates by
44 default at 2:15 AM every day.
45
46 The format is described in
47 {manpage}`systemd.time(7)`.
48
49 To disable automatic updates, set to `"never"`
50 and run {command}`updatedb` manually.
51 '';
52 };
53
54 extraFlags = mkOption {
55 type = listOf str;
56 default = [ ];
57 description = lib.mdDoc ''
58 Extra flags to pass to {command}`updatedb`.
59 '';
60 };
61
62 output = mkOption {
63 type = path;
64 default = "/var/cache/locatedb";
65 description = lib.mdDoc ''
66 The database file to build.
67 '';
68 };
69
70 localuser = mkOption {
71 type = nullOr str;
72 default = "nobody";
73 description = lib.mdDoc ''
74 The user to search non-network directories as, using
75 {command}`su`.
76 '';
77 };
78
79 pruneFS = mkOption {
80 type = listOf str;
81 default = [
82 "afs"
83 "anon_inodefs"
84 "auto"
85 "autofs"
86 "bdev"
87 "binfmt"
88 "binfmt_misc"
89 "ceph"
90 "cgroup"
91 "cgroup2"
92 "cifs"
93 "coda"
94 "configfs"
95 "cramfs"
96 "cpuset"
97 "curlftpfs"
98 "debugfs"
99 "devfs"
100 "devpts"
101 "devtmpfs"
102 "ecryptfs"
103 "eventpollfs"
104 "exofs"
105 "futexfs"
106 "ftpfs"
107 "fuse"
108 "fusectl"
109 "fusesmb"
110 "fuse.ceph"
111 "fuse.glusterfs"
112 "fuse.gvfsd-fuse"
113 "fuse.mfs"
114 "fuse.rclone"
115 "fuse.rozofs"
116 "fuse.sshfs"
117 "gfs"
118 "gfs2"
119 "hostfs"
120 "hugetlbfs"
121 "inotifyfs"
122 "iso9660"
123 "jffs2"
124 "lustre"
125 "lustre_lite"
126 "misc"
127 "mfs"
128 "mqueue"
129 "ncpfs"
130 "nfs"
131 "NFS"
132 "nfs4"
133 "nfsd"
134 "nnpfs"
135 "ocfs"
136 "ocfs2"
137 "pipefs"
138 "proc"
139 "ramfs"
140 "rpc_pipefs"
141 "securityfs"
142 "selinuxfs"
143 "sfs"
144 "shfs"
145 "smbfs"
146 "sockfs"
147 "spufs"
148 "sshfs"
149 "subfs"
150 "supermount"
151 "sysfs"
152 "tmpfs"
153 "tracefs"
154 "ubifs"
155 "udev"
156 "udf"
157 "usbfs"
158 "vboxsf"
159 "vperfctrfs"
160 ];
161 description = lib.mdDoc ''
162 Which filesystem types to exclude from indexing
163 '';
164 };
165
166 prunePaths = mkOption {
167 type = listOf path;
168 default = [
169 "/tmp"
170 "/var/tmp"
171 "/var/cache"
172 "/var/lock"
173 "/var/run"
174 "/var/spool"
175 "/nix/store"
176 "/nix/var/log/nix"
177 ];
178 description = lib.mdDoc ''
179 Which paths to exclude from indexing
180 '';
181 };
182
183 pruneNames = mkOption {
184 type = listOf str;
185 default = lib.optionals (!isFindutils) [ ".bzr" ".cache" ".git" ".hg" ".svn" ];
186 defaultText = literalMD ''
187 `[ ".bzr" ".cache" ".git" ".hg" ".svn" ]`, if
188 supported by the locate implementation (i.e. mlocate or plocate).
189 '';
190 description = lib.mdDoc ''
191 Directory components which should exclude paths containing them from indexing
192 '';
193 };
194
195 pruneBindMounts = mkOption {
196 type = bool;
197 default = false;
198 description = lib.mdDoc ''
199 Whether not to index bind mounts
200 '';
201 };
202
203 };
204
205 config = mkIf cfg.enable {
206 users.groups = mkMerge [
207 (mkIf isMLocate { mlocate = { }; })
208 (mkIf isPLocate { plocate = { }; })
209 ];
210
211 security.wrappers =
212 let
213 common = {
214 owner = "root";
215 permissions = "u+rx,g+x,o+x";
216 setgid = true;
217 setuid = false;
218 };
219 mlocate = (mkIf isMLocate {
220 group = "mlocate";
221 source = "${cfg.locate}/bin/locate";
222 });
223 plocate = (mkIf isPLocate {
224 group = "plocate";
225 source = "${cfg.locate}/bin/plocate";
226 });
227 in
228 mkIf isMorPLocate {
229 locate = mkMerge [ common mlocate plocate ];
230 plocate = (mkIf isPLocate (mkMerge [ common plocate ]));
231 };
232
233 nixpkgs.config = { locate.dbfile = cfg.output; };
234
235 environment.systemPackages = [ cfg.locate ];
236
237 environment.variables = mkIf (!isMorPLocate) { LOCATE_PATH = cfg.output; };
238
239 environment.etc = {
240 # write /etc/updatedb.conf for manual calls to `updatedb`
241 "updatedb.conf" = {
242 text = ''
243 PRUNEFS="${lib.concatStringsSep " " cfg.pruneFS}"
244 PRUNENAMES="${lib.concatStringsSep " " cfg.pruneNames}"
245 PRUNEPATHS="${lib.concatStringsSep " " cfg.prunePaths}"
246 PRUNE_BIND_MOUNTS="${if cfg.pruneBindMounts then "yes" else "no"}"
247 '';
248 };
249 };
250
251 warnings = optional (isMorPLocate && cfg.localuser != null)
252 "mlocate and plocate do not support the services.locate.localuser option. updatedb will run as root. Silence this warning by setting services.locate.localuser = null."
253 ++ optional (isFindutils && cfg.pruneNames != [ ])
254 "findutils locate does not support pruning by directory component"
255 ++ optional (isFindutils && cfg.pruneBindMounts)
256 "findutils locate does not support skipping bind mounts";
257
258 systemd.services.update-locatedb = {
259 description = "Update Locate Database";
260 path = mkIf (!isMorPLocate) [ pkgs.su ];
261
262 # mlocate's updatedb takes flags via a configuration file or
263 # on the command line, but not by environment variable.
264 script =
265 if isMorPLocate then
266 let
267 toFlags = x:
268 optional (cfg.${x} != [ ])
269 "--${lib.toLower x} '${concatStringsSep " " cfg.${x}}'";
270 args = concatLists (map toFlags [ "pruneFS" "pruneNames" "prunePaths" ]);
271 in
272 ''
273 exec ${cfg.locate}/bin/updatedb \
274 --output ${toString cfg.output} ${concatStringsSep " " args} \
275 --prune-bind-mounts ${if cfg.pruneBindMounts then "yes" else "no"} \
276 ${concatStringsSep " " cfg.extraFlags}
277 ''
278 else ''
279 exec ${cfg.locate}/bin/updatedb \
280 ${optionalString (cfg.localuser != null && !isMorPLocate) "--localuser=${cfg.localuser}"} \
281 --output=${toString cfg.output} ${concatStringsSep " " cfg.extraFlags}
282 '';
283 environment = optionalAttrs (!isMorPLocate) {
284 PRUNEFS = concatStringsSep " " cfg.pruneFS;
285 PRUNEPATHS = concatStringsSep " " cfg.prunePaths;
286 PRUNENAMES = concatStringsSep " " cfg.pruneNames;
287 PRUNE_BIND_MOUNTS = if cfg.pruneBindMounts then "yes" else "no";
288 };
289 serviceConfig.Nice = 19;
290 serviceConfig.IOSchedulingClass = "idle";
291 serviceConfig.PrivateTmp = "yes";
292 serviceConfig.PrivateNetwork = "yes";
293 serviceConfig.NoNewPrivileges = "yes";
294 serviceConfig.ReadOnlyPaths = "/";
295 # Use dirOf cfg.output because mlocate creates temporary files next to
296 # the actual database. We could specify and create them as well,
297 # but that would make this quite brittle when they change something.
298 # NOTE: If /var/cache does not exist, this leads to the misleading error message:
299 # update-locatedb.service: Failed at step NAMESPACE spawning …/update-locatedb-start: No such file or directory
300 serviceConfig.ReadWritePaths = dirOf cfg.output;
301 };
302
303 systemd.timers.update-locatedb = mkIf (cfg.interval != "never") {
304 description = "Update timer for locate database";
305 partOf = [ "update-locatedb.service" ];
306 wantedBy = [ "timers.target" ];
307 timerConfig.OnCalendar = cfg.interval;
308 };
309 };
310
311 meta.maintainers = with lib.maintainers; [ SuperSandro2000 ];
312}