Skip to main content

max / audiofiles

Repoint stale mirror symlinks instead of failing EEXIST The mirror guarded link creation with `!link_path.exists()`, which follows the symlink, so a link whose blob had moved read as absent while still holding the name and symlink(2) failed EEXIST. No later sync could repair it either, because every one hit the same error. The blob-layout migration moves every blob at once, so this broke the whole mirror: verified against a real 1761-sample flat vault, all 1761 links dangled after the sweep and the rebuild reported failure.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 00:22 UTC
Signed with PGP, not checked
Commit: 8f810b2bb13d7418b0cc267df7214cbee37ebaa5
Parent: a67309e
1 file changed, +57 insertions, -0 deletions
@@ -112,6 +112,20 @@
112 112 existing.remove(&link_path);
113 113
114 114 if !link_path.exists() {
115 + // `exists` follows the link, so a symlink whose target has
116 + // moved reads as absent here while still occupying the
117 + // name, and `symlink(2)` would fail EEXIST forever after.
118 + // The blob-layout migration moves every blob at once, so
119 + // without this the first sync after it leaves the whole
120 + // mirror dangling and no later sync can repair it. Unlink
121 + // the stale link and let it be recreated at the new target.
122 + if std::fs::symlink_metadata(&link_path).is_ok()
123 + && let Err(e) = std::fs::remove_file(&link_path)
124 + {
125 + warn!(path = %link_path.display(), "mirror: stale link unlink failed: {e}");
126 + first_err.get_or_insert_with(|| io_err(&link_path, e));
127 + continue;
128 + }
115 129 // Ensure parent directory exists.
116 130 if let Some(parent) = link_path.parent()
117 131 && !parent.exists()
@@ -403,6 +417,49 @@
403 417 assert!(target.to_string_lossy().contains("abc123.wav"));
404 418 }
405 419
420 + #[cfg(unix)]
421 + #[test]
422 + fn stale_symlink_is_repointed_after_the_blob_moves() {
423 + // Regression: the blob-layout migration relocates every blob, which leaves
424 + // each mirror symlink dangling. `exists` follows the link, so the stale one
425 + // read as absent while still holding the name, and `symlink(2)` failed
426 + // EEXIST for every entry, permanently: the rebuild after the migration
427 + // errored out and no later sync could repair it either. Measured against a
428 + // real 1761-sample vault, all 1761 links dangled.
429 + let (db, mirror_dir, store_dir) = setup();
430 + let hash = "ab".repeat(32);
431 + insert_fake_sample(&db, &hash);
432 + create_store_file(&store_dir, &hash, "wav");
433 +
434 + let vfs_id = create_vfs(&db, "Library").unwrap();
435 + create_sample_link(
436 + &db,
437 + vfs_id,
438 + None,
439 + "kick.wav",
440 + &crate::SampleHash::from_trusted(hash.clone()),
441 + )
442 + .unwrap();
443 +
444 + let config = make_config(&mirror_dir, &store_dir);
445 + assert_eq!(sync_mirror(&db, &config).unwrap().links_created, 1);
446 + let link = mirror_dir.path().join("Library/kick.wav");
447 + let flat = crate::store::legacy_flat_blob_path(store_dir.path(), &hash, "wav");
448 + assert_eq!(std::fs::read_link(&link).unwrap(), flat);
449 +
450 + // Exactly what the layout sweep does to it.
451 + let sharded = crate::store::store_blob_path(store_dir.path(), &hash, "wav");
452 + std::fs::create_dir_all(sharded.parent().unwrap()).unwrap();
453 + std::fs::rename(&flat, &sharded).unwrap();
454 + assert!(!link.exists(), "the link is dangling before the resync");
455 +
456 + let stats = sync_mirror(&db, &config).unwrap();
457 +
458 + assert_eq!(stats.links_created, 1, "the stale link is recreated");
459 + assert_eq!(std::fs::read_link(&link).unwrap(), sharded);
460 + assert!(link.exists(), "and it resolves again");
461 + }
462 +
406 463 #[cfg(unix)]
407 464 #[test]
408 465 fn loose_files_sample_symlinks_to_source_path() {