Some more fixes for reconcile with remote

This commit is contained in:
Alexander
2026-07-01 14:53:21 +02:00
parent 0e50d8a2a6
commit d35aa2aecb
6 changed files with 111 additions and 21 deletions
+6
View File
@@ -6,6 +6,12 @@ build:
up profile: up profile:
devenv up --profile {{profile}} devenv up --profile {{profile}}
vm command:
scripts/vm.sh {{command}}
redeploy:
scripts/vm.sh redeploy
e2e: e2e:
scripts/e2e/run.sh scripts/e2e/run.sh
+1 -1
View File
@@ -1,6 +1,6 @@
use symphonia::core::meta::{MetadataRevision, StandardTagKey}; use symphonia::core::meta::{MetadataRevision, StandardTagKey};
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct MusicMetadata { pub struct MusicMetadata {
pub artist: Vec<String>, pub artist: Vec<String>,
pub album_artist: Option<String>, pub album_artist: Option<String>,
+1
View File
@@ -115,6 +115,7 @@ impl NetworkOrigin {
io_err(e) io_err(e)
})? })?
.into_iter() .into_iter()
.filter(|m| m.file_type == "file")
.map(|m| (m.inode as u64, m.hash as u64)) .map(|m| (m.inode as u64, m.hash as u64))
.collect(); .collect();
+1
View File
@@ -132,6 +132,7 @@ impl WatcherState {
.all(&self.client) .all(&self.client)
.await? .await?
.into_iter() .into_iter()
.filter(|m| m.file_type == "file")
.map(|m| (m.inode as u64, m.hash as u64)) .map(|m| (m.inode as u64, m.hash as u64))
.collect(); .collect();
+86 -8
View File
@@ -15,7 +15,7 @@ use tracing::info;
/// Server-side entry for one file. Built once on startup from a directory /// Server-side entry for one file. Built once on startup from a directory
/// scan and refreshed by the watcher on inotify events. /// scan and refreshed by the watcher on inotify events.
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct FileEntry { pub struct FileEntry {
pub abs_path: PathBuf, pub abs_path: PathBuf,
pub rel_path: String, pub rel_path: String,
@@ -37,16 +37,17 @@ impl ServerState {
/// Replace the entire map with a fresh scan of `source`. Used on startup /// Replace the entire map with a fresh scan of `source`. Used on startup
/// and on watcher-driven reconciliations. /// and on watcher-driven reconciliations.
pub fn replace_all(&self, source: &Path) -> io::Result<()> { pub fn replace_all(&self, source: &Path) -> io::Result<bool> {
let entries = scan_directory(source)?; let entries = scan_directory(source)?;
let count = entries.len(); let new_map: HashMap<u64, FileEntry> = entries.into_iter().collect();
let mut map = self.inner.lock().unwrap(); let mut map = self.inner.lock().unwrap();
map.clear(); if *map == new_map {
for (id, entry) in entries { return Ok(false);
map.insert(id, entry);
} }
info!(count, "server state: scan complete"); let count = new_map.len();
return Ok(()); *map = new_map;
info!(count, "server state: scan complete (changed)");
return Ok(true);
} }
/// Snapshot the current state into a manifest. Order is by inode ascending /// Snapshot the current state into a manifest. Order is by inode ascending
@@ -179,4 +180,81 @@ mod tests {
state.replace_all(source).unwrap(); state.replace_all(source).unwrap();
assert_eq!(state.manifest().len(), 0); assert_eq!(state.manifest().len(), 0);
} }
#[test]
fn replace_all_returns_true_on_first_scan() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"x").unwrap();
let state = ServerState::new();
assert!(state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_false_when_unchanged() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"hello").unwrap();
fs::write(source.join("b.txt"), b"world").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
assert!(!state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_true_after_file_added() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"x").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
fs::write(source.join("b.txt"), b"y").unwrap();
assert!(state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_true_after_file_removed() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"x").unwrap();
fs::write(source.join("b.txt"), b"y").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
fs::remove_file(source.join("a.txt")).unwrap();
assert!(state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_true_after_file_renamed() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("old.txt"), b"x").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
assert!(!state.replace_all(source).unwrap());
fs::rename(source.join("old.txt"), source.join("new.txt")).unwrap();
assert!(state.replace_all(source).unwrap());
}
#[test]
fn replace_all_returns_true_after_content_modified() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
fs::write(source.join("a.txt"), b"original").unwrap();
let state = ServerState::new();
state.replace_all(source).unwrap();
assert!(!state.replace_all(source).unwrap());
fs::write(source.join("a.txt"), b"modified content").unwrap();
assert!(state.replace_all(source).unwrap());
}
} }
+16 -12
View File
@@ -90,25 +90,29 @@ fn run_watcher_loop(
EventKind::Remove(_) => ChangeKind::Remove, EventKind::Remove(_) => ChangeKind::Remove,
_ => continue, _ => continue,
}; };
if let Err(e) = state.replace_all(&source) { match state.replace_all(&source) {
error!(error = %e, "server watcher: state refresh failed"); Ok(true) => {
continue; let _ = events_tx.send(ChangeEvent { kind });
}
Ok(false) => {}
Err(e) => {
error!(error = %e, "server watcher: state refresh failed");
}
} }
let _ = events_tx.send(ChangeEvent { kind });
} }
_ => {} _ => {}
}, },
Err(e) => error!(error = %e, "server watcher: inotify error"), Err(e) => error!(error = %e, "server watcher: inotify error"),
}, },
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => { Err(std::sync::mpsc::RecvTimeoutError::Timeout) => match state.replace_all(&source) {
if let Err(e) = state.replace_all(&source) { Ok(true) => {
error!(error = %e, "server watcher: poll rescan failed"); let _ = events_tx.send(ChangeEvent {
continue; kind: ChangeKind::Modify,
});
} }
let _ = events_tx.send(ChangeEvent { Ok(false) => {}
kind: ChangeKind::Modify, Err(e) => error!(error = %e, "server watcher: poll rescan failed"),
}); },
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break, Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
} }
} }