This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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 downloadRoot = "/var/lib/ipfs/download"; 16 flixIndexMfs = "/flix-index"; 17 dirtyFlag = "/var/lib/ipfs/flix-index.dirty"; 18 ipnsNameFile = "/var/lib/ipfs/flix-index.name"; 19 20 pinWatcherScript = pkgs.writers.writeBash "ipfs-pin-watcher" '' 21 set -efu 22 23 IPFS="${pkgs.kubo}/bin/ipfs" 24 INOTIFYWAIT="${pkgs.inotify-tools}/bin/inotifywait" 25 CID_MAP="${cidMapFile}" 26 DOWNLOAD_ROOT="${downloadRoot}" 27 FLIX_INDEX="${flixIndexMfs}" 28 DIRTY_FLAG="${dirtyFlag}" 29 IPNS_NAME_FILE="${ipnsNameFile}" 30 31 touch "$CID_MAP" 32 ${pkgs.coreutils}/bin/chmod 0644 "$CID_MAP" 33 34 log() { 35 echo "[$(date -Iseconds)] $*" 36 } 37 38 mark_dirty() { 39 touch "$DIRTY_FLAG" 40 } 41 42 # strip $DOWNLOAD_ROOT/ prefix to get the relative path used inside MFS 43 relpath() { 44 echo "''${1#$DOWNLOAD_ROOT/}" 45 } 46 47 mfs_add() { 48 local cid="$1" path="$2" 49 local rel dir 50 rel=$(relpath "$path") 51 dir=$(${pkgs.coreutils}/bin/dirname "$rel") 52 # kubo's `files rm/cp/mkdir` write detailed errors to stdout, not 53 # stderr, so both FDs need redirection to keep backfill quiet. 54 $IPFS files mkdir -p "$FLIX_INDEX/$dir" >/dev/null 2>&1 || true 55 $IPFS files rm "$FLIX_INDEX/$rel" >/dev/null 2>&1 || true 56 $IPFS files cp "/ipfs/$cid" "$FLIX_INDEX/$rel" >/dev/null 2>&1 || true 57 mark_dirty 58 } 59 60 mfs_rm() { 61 local path="$1" 62 local rel 63 rel=$(relpath "$path") 64 $IPFS files rm "$FLIX_INDEX/$rel" >/dev/null 2>&1 || true 65 mark_dirty 66 } 67 68 # ensure IPNS key + MFS root exist (idempotent) 69 if ! $IPFS key list | ${pkgs.gnugrep}/bin/grep -qx flix-index; then 70 log "Generating flix-index IPNS key" 71 $IPFS key gen --type=ed25519 flix-index > "$IPNS_NAME_FILE" || true 72 fi 73 if [ -f "$IPNS_NAME_FILE" ]; then 74 log "flix-index IPNS name: $(cat "$IPNS_NAME_FILE")" 75 fi 76 $IPFS files mkdir -p "$FLIX_INDEX" >/dev/null 2>&1 || true 77 78 add_file() { 79 local path="$1" 80 [ -f "$path" ] || return 0 81 case "$(basename "$path")" in 82 .* | *.part | *.tmp | *.synced.* | *.aria2) return 0 ;; 83 esac 84 85 log "Adding $path" 86 cid=$($IPFS add --nocopy --pin --quieter "$path" 2>/dev/null) || { 87 log "Failed to add $path" 88 return 0 89 } 90 log "Pinned $path -> $cid" 91 ${pkgs.gnused}/bin/sed -i "\|^[^ ]* $path$|d" "$CID_MAP" 92 echo "$cid $path" >> "$CID_MAP" 93 mfs_add "$cid" "$path" 94 } 95 96 remove_file() { 97 local path="$1" 98 cid=$(${pkgs.gawk}/bin/awk -v p="$path" '$0 ~ p {print $1; exit}' "$CID_MAP") 99 if [ -n "$cid" ]; then 100 log "Unpinning $cid ($path)" 101 # remove from MFS first so the implicit MFS pin doesn't keep it alive 102 mfs_rm "$path" 103 $IPFS pin rm "$cid" 2>/dev/null || true 104 ${pkgs.gnused}/bin/sed -i "\|^[^ ]* $path$|d" "$CID_MAP" 105 else 106 log "No CID found for removed file: $path" 107 fi 108 } 109 110 # initial sync 111 log "Starting initial sync..." 112 for dir in ${lib.escapeShellArgs watchDirs}; do 113 if [ -d "$dir" ]; then 114 ${pkgs.findutils}/bin/find "$dir" -type f | while read -r f; do 115 if ! ${pkgs.gnugrep}/bin/grep -q " $f$" "$CID_MAP" 2>/dev/null; then 116 add_file "$f" 117 fi 118 done 119 fi 120 done 121 log "Initial sync complete" 122 123 # clean stale entries 124 log "Cleaning stale entries..." 125 while IFS=' ' read -r cid path; do 126 if [ ! -f "$path" ]; then 127 log "Stale entry: $path (CID: $cid)" 128 mfs_rm "$path" 129 $IPFS pin rm "$cid" 2>/dev/null || true 130 ${pkgs.gnused}/bin/sed -i "\|^[^ ]* $path$|d" "$CID_MAP" 131 fi 132 done < "$CID_MAP" 133 log "Cleanup complete" 134 135 # backfill MFS index from cid-map.txt once (sentinel-guarded so we run 136 # exactly once per machine, independently of whether the initial sync 137 # happened to create any MFS entries incrementally). 138 BACKFILL_SENTINEL="/var/lib/ipfs/flix-index.backfilled" 139 if [ ! -f "$BACKFILL_SENTINEL" ]; then 140 log "Backfilling MFS index from cid-map.txt..." 141 n=0 142 while IFS=' ' read -r cid path; do 143 [ -f "$path" ] || continue 144 # skip entries already present (idempotent re-runs) 145 rel=$(relpath "$path") 146 if $IPFS files stat "$FLIX_INDEX/$rel" >/dev/null 2>&1; then 147 continue 148 fi 149 mfs_add "$cid" "$path" 150 n=$((n + 1)) 151 done < "$CID_MAP" 152 touch "$BACKFILL_SENTINEL" 153 log "Backfill complete ($n entries added)" 154 fi 155 156 # add all files in a newly appeared directory 157 scan_new_dir() { 158 local dir="$1" 159 log "Scanning new directory: $dir" 160 ${pkgs.findutils}/bin/find "$dir" -type f | while read -r f; do 161 add_file "$f" 162 done 163 } 164 165 # watch for changes 166 log "Watching directories: ${lib.concatStringsSep ", " watchDirs}" 167 $INOTIFYWAIT -m -r --format $'%w\t%e\t%f' \ 168 -e close_write \ 169 -e moved_to \ 170 -e moved_from \ 171 -e delete \ 172 -e create \ 173 ${lib.escapeShellArgs watchDirs} | 174 while IFS=$'\t' read -r dir event file; do 175 path="''${dir}''${file}" 176 case "$event" in 177 CREATE,ISDIR*|MOVED_TO,ISDIR*) 178 scan_new_dir "$path" 179 ;; 180 CLOSE_WRITE*|MOVED_TO*) 181 add_file "$path" 182 ;; 183 DELETE*|MOVED_FROM*) 184 remove_file "$path" 185 ;; 186 esac 187 done 188 ''; 189 190 publishScript = pkgs.writers.writeBash "flix-index-publish" '' 191 set -efu 192 193 IPFS="${pkgs.kubo}/bin/ipfs" 194 CID_MAP="${cidMapFile}" 195 DOWNLOAD_ROOT="${downloadRoot}" 196 FLIX_INDEX="${flixIndexMfs}" 197 DIRTY_FLAG="${dirtyFlag}" 198 199 [ -f "$DIRTY_FLAG" ] || exit 0 200 201 # regenerate index.tsv from cid-map.txt 202 tmp=$(${pkgs.coreutils}/bin/mktemp) 203 printf 'cid\tpath\tsize\n' > "$tmp" 204 while IFS=' ' read -r cid path; do 205 [ -f "$path" ] || continue 206 rel="''${path#$DOWNLOAD_ROOT/}" 207 size=$(${pkgs.coreutils}/bin/stat -c %s "$path" 2>/dev/null || echo 0) 208 printf '%s\t%s\t%s\n' "$cid" "$rel" "$size" >> "$tmp" 209 done < "$CID_MAP" 210 211 $IPFS files rm "$FLIX_INDEX/index.tsv" >/dev/null 2>&1 || true 212 $IPFS files write --create --truncate "$FLIX_INDEX/index.tsv" < "$tmp" 213 ${pkgs.coreutils}/bin/rm -f "$tmp" 214 215 root=$($IPFS files stat --hash "$FLIX_INDEX") 216 echo "Publishing $root to IPNS (flix-index)" 217 $IPFS name publish --key=flix-index --lifetime=48h --ttl=1m "/ipfs/$root" 218 # Announce the MFS root to the DHT so public gateways can find us. 219 # MFS directory blocks are only kept alive by an implicit pin that 220 # Provide.Strategy="pinned" ignores, so we provide them explicitly here. 221 echo "Announcing $root to DHT" 222 $IPFS routing provide "$root" >/dev/null 2>&1 || true 223 ${pkgs.coreutils}/bin/rm -f "$DIRTY_FLAG" 224 ''; 225 226 # Periodic reconciliation: walks cid-map.txt and repairs stale state for 227 # files that no longer exist on disk. Protects against inotify event 228 # drops (e.g. queue overflow during bulk renames/deletes) and any other 229 # path that leaves cid-map/MFS out of sync with the filesystem. 230 uploadServer = pkgs.writers.writePython3Bin "ipfs-upload-server" { 231 flakeIgnore = [ "E501" ]; 232 } ./ipfs_upload.py; 233 234 reconcileScript = pkgs.writers.writeBash "flix-index-reconcile" '' 235 set -efu 236 237 IPFS="${pkgs.kubo}/bin/ipfs" 238 CID_MAP="${cidMapFile}" 239 DOWNLOAD_ROOT="${downloadRoot}" 240 FLIX_INDEX="${flixIndexMfs}" 241 DIRTY_FLAG="${dirtyFlag}" 242 243 [ -f "$CID_MAP" ] || exit 0 244 245 # Rebuild cid-map in one pass, collecting stale entries to clean up. 246 # Avoids sed regex issues with special chars like [ ] in paths. 247 # Small race with pin-watcher's concurrent appends — any line the 248 # watcher writes during this loop may be lost, but will be re-added 249 # on the next inotify event or watcher restart. 250 tmp=$(${pkgs.coreutils}/bin/mktemp) 251 removed=0 252 while IFS=' ' read -r cid path; do 253 [ -n "$cid" ] || continue 254 if [ -e "$path" ]; then 255 echo "$cid $path" >> "$tmp" 256 else 257 rel="''${path#$DOWNLOAD_ROOT/}" 258 $IPFS files rm "$FLIX_INDEX/$rel" >/dev/null 2>&1 || true 259 $IPFS pin rm "$cid" >/dev/null 2>&1 || true 260 removed=$((removed + 1)) 261 fi 262 done < "$CID_MAP" 263 264 if [ "$removed" -gt 0 ]; then 265 ${pkgs.coreutils}/bin/chmod 0644 "$tmp" 266 ${pkgs.coreutils}/bin/mv "$tmp" "$CID_MAP" 267 echo "reconciled: removed $removed stale entries" 268 touch "$DIRTY_FLAG" 269 else 270 ${pkgs.coreutils}/bin/rm -f "$tmp" 271 fi 272 ''; 273in 274{ 275 # symlink /var/download into IPFS root so --nocopy works 276 systemd.tmpfiles.rules = [ 277 "L+ /var/lib/ipfs/download - - - - /var/download" 278 # Staging area for ipfs-upload: same filesystem as the destination, but 279 # outside the pin-watcher's watch dirs (movies/shows/games) so inotify 280 # never fires on in-flight uploads. 281 "d /var/download/incoming 0755 ${config.services.kubo.user} ${config.services.kubo.group} -" 282 "d /var/download/incoming/uploader 0755 ${config.services.kubo.user} ${config.services.kubo.group} -" 283 # Let the ipfs-upload service write into /var/download/games. Owner 284 # = ipfs so the service has write; group = users + setgid so existing 285 # CLI users (lass) keep write via group and new uploads inherit 286 # group=users. 287 "z /var/download/games 2775 ${config.services.kubo.user} users -" 288 ]; 289 290 services.kubo = { 291 enable = true; 292 settings = { 293 # kubo 0.40 deprecated Reprovider in favor of Provide; the nixpkgs 294 # module still injects a default Reprovider block, so null it out 295 # explicitly or kubo refuses to start. 296 Reprovider = null; 297 Experimental.FilestoreEnabled = true; 298 Addresses = { 299 API = [ 300 "/ip4/127.0.0.1/tcp/5001" 301 "/ip6/::1/tcp/5001" 302 ]; 303 Gateway = [ 304 "/ip4/0.0.0.0/tcp/8089" 305 "/ip6/::/tcp/8089" 306 ]; 307 }; 308 Datastore.StorageMax = "100GB"; 309 # limit bandwidth: 5MB/s out, 5MB/s in 310 Swarm.ConnMgr = { 311 LowWater = 100; 312 HighWater = 400; 313 GracePeriod = "20s"; 314 }; 315 Swarm.Transports.Network.TCP = true; 316 Swarm.Transports.Network.QUIC = true; 317 Swarm.ResourceMgr = { 318 Enabled = true; 319 MaxMemory = "2GB"; 320 }; 321 # disable relay to reduce overhead 322 Swarm.RelayClient.Enabled = false; 323 Swarm.RelayService.Enabled = false; 324 # autoclient: DHT client (no server queries) + IPNI/cid.contact HTTP 325 # announcements. Content is discoverable by both DHT peers and lassie 326 # (which uses IPNI for provider discovery). 327 Routing.Type = "autoclient"; 328 Provide = { 329 Strategy = "pinned"; 330 DHT.Interval = "12h"; 331 }; 332 }; 333 }; 334 335 boot.kernel.sysctl."fs.inotify.max_user_watches" = 1048576; 336 337 systemd.services.ipfs-pin-watcher = { 338 description = "Auto-pin files to IPFS using inotify"; 339 after = [ "ipfs.service" ]; 340 requires = [ "ipfs.service" ]; 341 wantedBy = [ "multi-user.target" ]; 342 serviceConfig = { 343 Environment = [ "IPFS_PATH=/var/lib/ipfs" ]; 344 ExecStart = pinWatcherScript; 345 Restart = "always"; 346 RestartSec = "10s"; 347 User = config.services.kubo.user; 348 Group = config.services.kubo.group; 349 SupplementaryGroups = [ "radio_container" ]; 350 }; 351 }; 352 353 systemd.services.ipfs-upload = { 354 description = "HTTP upload endpoint that writes files under /var/lib/ipfs/download and pins them"; 355 after = [ "ipfs.service" ]; 356 requires = [ "ipfs.service" ]; 357 wantedBy = [ "multi-user.target" ]; 358 serviceConfig = { 359 Environment = [ 360 "IPFS_PATH=/var/lib/ipfs" 361 "IPFS_BIN=${pkgs.kubo}/bin/ipfs" 362 "DOWNLOAD_ROOT=/var/lib/ipfs/download" 363 "BIND=127.0.0.1" 364 "PORT=8090" 365 ]; 366 ExecStart = "${uploadServer}/bin/ipfs-upload-server"; 367 Restart = "always"; 368 RestartSec = "10s"; 369 User = config.services.kubo.user; 370 Group = config.services.kubo.group; 371 # /var/download is owned by ipfs:ipfs on neoprism, so no extra 372 # groups needed. Existing subdirs owned by other users (e.g. games 373 # → lass:users) are not writable by this service; uploads to those 374 # paths will get a 500 unless ownership is adjusted out-of-band. 375 }; 376 }; 377 378 systemd.services.flix-index-publish = { 379 description = "Publish flix MFS index to IPNS"; 380 after = [ "ipfs.service" ]; 381 requires = [ "ipfs.service" ]; 382 serviceConfig = { 383 Type = "oneshot"; 384 Environment = [ "IPFS_PATH=/var/lib/ipfs" ]; 385 ExecStart = publishScript; 386 User = config.services.kubo.user; 387 Group = config.services.kubo.group; 388 }; 389 }; 390 391 systemd.timers.flix-index-publish = { 392 description = "Publish flix MFS index to IPNS periodically"; 393 wantedBy = [ "timers.target" ]; 394 timerConfig = { 395 OnBootSec = "5min"; 396 OnUnitActiveSec = "2min"; 397 Unit = "flix-index-publish.service"; 398 }; 399 }; 400 401 systemd.services.flix-index-reconcile = { 402 description = "Reconcile flix MFS index / cid-map against filesystem"; 403 after = [ "ipfs.service" ]; 404 requires = [ "ipfs.service" ]; 405 serviceConfig = { 406 Type = "oneshot"; 407 Environment = [ "IPFS_PATH=/var/lib/ipfs" ]; 408 ExecStart = reconcileScript; 409 User = config.services.kubo.user; 410 Group = config.services.kubo.group; 411 }; 412 }; 413 414 systemd.timers.flix-index-reconcile = { 415 description = "Reconcile flix MFS index hourly"; 416 wantedBy = [ "timers.target" ]; 417 timerConfig = { 418 OnBootSec = "15min"; 419 OnUnitActiveSec = "1h"; 420 Unit = "flix-index-reconcile.service"; 421 }; 422 }; 423 424 networking.firewall.allowedTCPPorts = [ 425 4001 # IPFS swarm 426 8089 # IPFS gateway 427 ]; 428 networking.firewall.allowedUDPPorts = [ 429 4001 # IPFS swarm (QUIC) 430 ]; 431 432 # Shadow `ipfs` in PATH with a wrapper that rejects `ipfs add` without 433 # --nocopy. The kubo daemon and pin-watcher use absolute store paths, so 434 # they are unaffected. This blocks confused agents/users from creating 435 # blockstore-only duplicates of files that already live in 436 # /var/lib/ipfs/download. 437 environment.systemPackages = [ 438 (lib.hiPrio ( 439 pkgs.writeShellScriptBin "ipfs" '' 440 if [ "''${1-}" = "add" ]; then 441 for a in "$@"; do 442 case "$a" in 443 --nocopy|--nocopy=true) exec ${pkgs.kubo}/bin/ipfs "$@" ;; 444 esac 445 done 446 echo "refused: 'ipfs add' without --nocopy is forbidden on this host." >&2 447 echo " Use 'ipfs add --nocopy <path>' so the file is referenced via filestore" >&2 448 echo " instead of duplicated into the blockstore." >&2 449 exit 1 450 fi 451 exec ${pkgs.kubo}/bin/ipfs "$@" 452 '' 453 )) 454 ]; 455}