forked from
lass.pds.lassul.us/superconfig
This repository has no description
12 kB
345 lines
1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 watchDirs = [
9 "/var/lib/ipfs/download/movies"
10 "/var/lib/ipfs/download/shows"
11 "/var/lib/ipfs/download/games"
12 ];
13
14 cidMapFile = "/var/lib/ipfs/cid-map.txt";
15
16 pinWatcherScript = pkgs.writers.writeBash "ipfs-pin-watcher" ''
17 set -efu
18
19 IPFS="${pkgs.kubo}/bin/ipfs"
20 INOTIFYWAIT="${pkgs.inotify-tools}/bin/inotifywait"
21 CID_MAP="${cidMapFile}"
22
23 touch "$CID_MAP"
24 ${pkgs.coreutils}/bin/chmod 0644 "$CID_MAP"
25
26 log() {
27 echo "[$(date -Iseconds)] $*"
28 }
29
30 add_file() {
31 local path="$1"
32 [ -f "$path" ] || return 0
33 case "$(basename "$path")" in
34 .* | *.part | *.tmp | *.synced.* | *.aria2) return 0 ;;
35 esac
36
37 log "Adding $path"
38 cid=$($IPFS add --nocopy --pin --quieter "$path" 2>/dev/null) || {
39 log "Failed to add $path"
40 return 0
41 }
42 log "Pinned $path -> $cid"
43 ${pkgs.gnused}/bin/sed -i "\|^[^ ]* $path$|d" "$CID_MAP"
44 echo "$cid $path" >> "$CID_MAP"
45 }
46
47 remove_file() {
48 local path="$1"
49 cid=$(${pkgs.gawk}/bin/awk -v p="$path" '$0 ~ p {print $1; exit}' "$CID_MAP")
50 if [ -n "$cid" ]; then
51 log "Unpinning $cid ($path)"
52 $IPFS pin rm "$cid" 2>/dev/null || true
53 ${pkgs.gnused}/bin/sed -i "\|^[^ ]* $path$|d" "$CID_MAP"
54 else
55 log "No CID found for removed file: $path"
56 fi
57 }
58
59 # initial sync: pin any file in the watch dirs not already in cid-map
60 log "Starting initial sync..."
61 for dir in ${lib.escapeShellArgs watchDirs}; do
62 if [ -d "$dir" ]; then
63 ${pkgs.findutils}/bin/find "$dir" -type f | while read -r f; do
64 if ! ${pkgs.gnugrep}/bin/grep -q " $f$" "$CID_MAP" 2>/dev/null; then
65 add_file "$f"
66 fi
67 done
68 fi
69 done
70 log "Initial sync complete"
71
72 # clean stale entries: unpin files that no longer exist on disk
73 log "Cleaning stale entries..."
74 while IFS=' ' read -r cid path; do
75 if [ ! -f "$path" ]; then
76 log "Stale entry: $path (CID: $cid)"
77 $IPFS pin rm "$cid" 2>/dev/null || true
78 ${pkgs.gnused}/bin/sed -i "\|^[^ ]* $path$|d" "$CID_MAP"
79 fi
80 done < "$CID_MAP"
81 log "Cleanup complete"
82
83 # pin all files in a newly appeared directory (covers the inotify
84 # recursive-watch race when a brand-new subdir is created + filled fast)
85 scan_new_dir() {
86 local dir="$1"
87 log "Scanning new directory: $dir"
88 ${pkgs.findutils}/bin/find "$dir" -type f | while read -r f; do
89 add_file "$f"
90 done
91 }
92
93 # watch for changes
94 log "Watching directories: ${lib.concatStringsSep ", " watchDirs}"
95 $INOTIFYWAIT -m -r --format $'%w\t%e\t%f' \
96 -e close_write \
97 -e moved_to \
98 -e moved_from \
99 -e delete \
100 -e create \
101 ${lib.escapeShellArgs watchDirs} |
102 while IFS=$'\t' read -r dir event file; do
103 path="''${dir}''${file}"
104 case "$event" in
105 CREATE,ISDIR*|MOVED_TO,ISDIR*)
106 scan_new_dir "$path"
107 ;;
108 CLOSE_WRITE*|MOVED_TO*)
109 add_file "$path"
110 ;;
111 DELETE*|MOVED_FROM*)
112 remove_file "$path"
113 ;;
114 esac
115 done
116 '';
117
118 uploadServer = pkgs.writers.writePython3Bin "ipfs-upload-server" {
119 flakeIgnore = [ "E501" ];
120 } ./ipfs_upload.py;
121
122 # Periodic reconciliation so cid-map/pins stay in sync with the filesystem
123 # even when the inotify watcher drops events:
124 # reverse - unpin + drop cid-map entries for files no longer on disk.
125 # forward - pin files present on disk but missing from cid-map. The watcher
126 # loop is single-threaded and blocks 1-2 min on each multi-GB
127 # `ipfs add`; while blocked, a concurrent upload's CREATE,ISDIR +
128 # MOVED_TO for a brand-new subdir can overflow the inotify queue
129 # and be dropped, leaving a fully-written file never pinned. The
130 # forward sweep heals that within an hour.
131 reconcileScript = pkgs.writers.writeBash "ipfs-pin-reconcile" ''
132 set -efu
133
134 IPFS="${pkgs.kubo}/bin/ipfs"
135 CID_MAP="${cidMapFile}"
136
137 [ -f "$CID_MAP" ] || exit 0
138
139 # Reverse: rebuild cid-map, unpinning entries whose file is gone. One pass
140 # avoids sed regex issues with special chars like [ ] in paths. Small race
141 # with the watcher's concurrent appends -- a line written during this loop
142 # may be lost, but is re-added on the next inotify event or watcher restart.
143 tmp=$(${pkgs.coreutils}/bin/mktemp)
144 removed=0
145 while IFS=' ' read -r cid path; do
146 [ -n "$cid" ] || continue
147 if [ -e "$path" ]; then
148 echo "$cid $path" >> "$tmp"
149 else
150 $IPFS pin rm "$cid" >/dev/null 2>&1 || true
151 removed=$((removed + 1))
152 fi
153 done < "$CID_MAP"
154
155 if [ "$removed" -gt 0 ]; then
156 ${pkgs.coreutils}/bin/chmod 0644 "$tmp"
157 ${pkgs.coreutils}/bin/mv "$tmp" "$CID_MAP"
158 echo "reconciled: removed $removed stale entries"
159 else
160 ${pkgs.coreutils}/bin/rm -f "$tmp"
161 fi
162
163 # Forward: pin on-disk files missing from cid-map. Only files older than
164 # 10 min -- a file that old and still absent from cid-map was dropped by the
165 # watcher, not in-flight. No lock against the watcher: concurrent
166 # `add --nocopy` of the same file is idempotent (verified), so a rare
167 # double-add is harmless.
168 for dir in ${lib.escapeShellArgs watchDirs}; do
169 [ -d "$dir" ] || continue
170 ${pkgs.findutils}/bin/find "$dir" -type f -mmin +10 | while read -r f; do
171 case "$(${pkgs.coreutils}/bin/basename "$f")" in
172 .* | *.part | *.tmp | *.synced.* | *.aria2) continue ;;
173 esac
174 if ${pkgs.gnugrep}/bin/grep -q " $f$" "$CID_MAP" 2>/dev/null; then
175 continue
176 fi
177 cid=$($IPFS add --nocopy --pin --quieter "$f" 2>/dev/null) || {
178 echo "reconcile: failed to add $f"
179 continue
180 }
181 echo "$cid $f" >> "$CID_MAP"
182 echo "reconcile: pinned missed file $f -> $cid"
183 done
184 done
185 '';
186in
187{
188 # symlink /var/download into IPFS root so --nocopy works
189 systemd.tmpfiles.rules = [
190 "L+ /var/lib/ipfs/download - - - - /var/download"
191 # Staging area for ipfs-upload: same filesystem as the destination, but
192 # outside the pin-watcher's watch dirs (movies/shows/games) so inotify
193 # never fires on in-flight uploads.
194 "d /var/download/incoming 0755 ${config.services.kubo.user} ${config.services.kubo.group} -"
195 "d /var/download/incoming/uploader 0755 ${config.services.kubo.user} ${config.services.kubo.group} -"
196 # Let the ipfs-upload service write into /var/download/games. Owner
197 # = ipfs so the service has write; group = users + setgid so existing
198 # CLI users (lass) keep write via group and new uploads inherit
199 # group=users.
200 "z /var/download/games 2775 ${config.services.kubo.user} users -"
201 ];
202
203 services.kubo = {
204 enable = true;
205 settings = {
206 # kubo 0.40 deprecated Reprovider in favor of Provide; the nixpkgs
207 # module still injects a default Reprovider block, so null it out
208 # explicitly or kubo refuses to start.
209 Reprovider = null;
210 Experimental.FilestoreEnabled = true;
211 Addresses = {
212 API = [
213 "/ip4/127.0.0.1/tcp/5001"
214 "/ip6/::1/tcp/5001"
215 ];
216 Gateway = [
217 "/ip4/0.0.0.0/tcp/8089"
218 "/ip6/::/tcp/8089"
219 ];
220 };
221 Datastore.StorageMax = "100GB";
222 # limit bandwidth: 5MB/s out, 5MB/s in
223 Swarm.ConnMgr = {
224 LowWater = 100;
225 HighWater = 400;
226 GracePeriod = "20s";
227 };
228 Swarm.Transports.Network.TCP = true;
229 Swarm.Transports.Network.QUIC = true;
230 Swarm.ResourceMgr = {
231 Enabled = true;
232 MaxMemory = "2GB";
233 };
234 # disable relay to reduce overhead
235 Swarm.RelayClient.Enabled = false;
236 Swarm.RelayService.Enabled = false;
237 # autoclient: DHT client (no server queries) + IPNI/cid.contact HTTP
238 # announcements. Content is discoverable by both DHT peers and lassie
239 # (which uses IPNI for provider discovery).
240 Routing.Type = "autoclient";
241 Provide = {
242 Strategy = "pinned";
243 DHT.Interval = "12h";
244 };
245 };
246 };
247
248 boot.kernel.sysctl."fs.inotify.max_user_watches" = 1048576;
249
250 systemd.services.ipfs-pin-watcher = {
251 description = "Auto-pin files to IPFS using inotify";
252 after = [ "ipfs.service" ];
253 requires = [ "ipfs.service" ];
254 wantedBy = [ "multi-user.target" ];
255 serviceConfig = {
256 Environment = [ "IPFS_PATH=/var/lib/ipfs" ];
257 ExecStart = pinWatcherScript;
258 Restart = "always";
259 RestartSec = "10s";
260 User = config.services.kubo.user;
261 Group = config.services.kubo.group;
262 SupplementaryGroups = [ "radio_container" ];
263 };
264 };
265
266 systemd.services.ipfs-upload = {
267 description = "HTTP upload endpoint that writes files under /var/lib/ipfs/download and pins them";
268 after = [ "ipfs.service" ];
269 requires = [ "ipfs.service" ];
270 wantedBy = [ "multi-user.target" ];
271 serviceConfig = {
272 Environment = [
273 "IPFS_PATH=/var/lib/ipfs"
274 "IPFS_BIN=${pkgs.kubo}/bin/ipfs"
275 "DOWNLOAD_ROOT=/var/lib/ipfs/download"
276 "BIND=127.0.0.1"
277 "PORT=8090"
278 ];
279 ExecStart = "${uploadServer}/bin/ipfs-upload-server";
280 Restart = "always";
281 RestartSec = "10s";
282 User = config.services.kubo.user;
283 Group = config.services.kubo.group;
284 # /var/download is owned by ipfs:ipfs on neoprism, so no extra
285 # groups needed. Existing subdirs owned by other users (e.g. games
286 # → lass:users) are not writable by this service; uploads to those
287 # paths will get a 500 unless ownership is adjusted out-of-band.
288 };
289 };
290
291 systemd.services.ipfs-pin-reconcile = {
292 description = "Reconcile pins / cid-map against the filesystem";
293 after = [ "ipfs.service" ];
294 requires = [ "ipfs.service" ];
295 serviceConfig = {
296 Type = "oneshot";
297 Environment = [ "IPFS_PATH=/var/lib/ipfs" ];
298 ExecStart = reconcileScript;
299 User = config.services.kubo.user;
300 Group = config.services.kubo.group;
301 };
302 };
303
304 systemd.timers.ipfs-pin-reconcile = {
305 description = "Reconcile pins / cid-map hourly";
306 wantedBy = [ "timers.target" ];
307 timerConfig = {
308 OnBootSec = "15min";
309 OnUnitActiveSec = "1h";
310 Unit = "ipfs-pin-reconcile.service";
311 };
312 };
313
314 networking.firewall.allowedTCPPorts = [
315 4001 # IPFS swarm
316 8089 # IPFS gateway
317 ];
318 networking.firewall.allowedUDPPorts = [
319 4001 # IPFS swarm (QUIC)
320 ];
321
322 # Shadow `ipfs` in PATH with a wrapper that rejects `ipfs add` without
323 # --nocopy. The kubo daemon and pin-watcher use absolute store paths, so
324 # they are unaffected. This blocks confused agents/users from creating
325 # blockstore-only duplicates of files that already live in
326 # /var/lib/ipfs/download.
327 environment.systemPackages = [
328 (lib.hiPrio (
329 pkgs.writeShellScriptBin "ipfs" ''
330 if [ "''${1-}" = "add" ]; then
331 for a in "$@"; do
332 case "$a" in
333 --nocopy|--nocopy=true) exec ${pkgs.kubo}/bin/ipfs "$@" ;;
334 esac
335 done
336 echo "refused: 'ipfs add' without --nocopy is forbidden on this host." >&2
337 echo " Use 'ipfs add --nocopy <path>' so the file is referenced via filestore" >&2
338 echo " instead of duplicated into the blockstore." >&2
339 exit 1
340 fi
341 exec ${pkgs.kubo}/bin/ipfs "$@"
342 ''
343 ))
344 ];
345}