max / audiofiles
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
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() { |