Some more fixes for reconcile with remote
This commit is contained in:
@@ -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,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>,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user