treewide: fix various UI bugs; optimize crypto dependencies & format
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: If8fe8b38c1d9c4fecd40ff71f88d2ae06a6a6964
This commit is contained in:
parent
764aafa88d
commit
3ccddce7fd
178 changed files with 58342 additions and 54241 deletions
|
|
@ -1,144 +1,144 @@
|
|||
//! Conflict detection and resolution for sync.
|
||||
|
||||
use crate::config::ConflictResolution;
|
||||
|
||||
use super::DeviceSyncState;
|
||||
use crate::config::ConflictResolution;
|
||||
|
||||
/// Detect if there's a conflict between local and server state.
|
||||
pub fn detect_conflict(state: &DeviceSyncState) -> Option<ConflictInfo> {
|
||||
// If either side has no hash, no conflict possible
|
||||
let local_hash = state.local_hash.as_ref()?;
|
||||
let server_hash = state.server_hash.as_ref()?;
|
||||
// If either side has no hash, no conflict possible
|
||||
let local_hash = state.local_hash.as_ref()?;
|
||||
let server_hash = state.server_hash.as_ref()?;
|
||||
|
||||
// Same hash = no conflict
|
||||
if local_hash == server_hash {
|
||||
return None;
|
||||
}
|
||||
// Same hash = no conflict
|
||||
if local_hash == server_hash {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Both have different hashes = conflict
|
||||
Some(ConflictInfo {
|
||||
path: state.path.clone(),
|
||||
local_hash: local_hash.clone(),
|
||||
server_hash: server_hash.clone(),
|
||||
local_mtime: state.local_mtime,
|
||||
server_mtime: state.server_mtime,
|
||||
})
|
||||
// Both have different hashes = conflict
|
||||
Some(ConflictInfo {
|
||||
path: state.path.clone(),
|
||||
local_hash: local_hash.clone(),
|
||||
server_hash: server_hash.clone(),
|
||||
local_mtime: state.local_mtime,
|
||||
server_mtime: state.server_mtime,
|
||||
})
|
||||
}
|
||||
|
||||
/// Information about a detected conflict.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConflictInfo {
|
||||
pub path: String,
|
||||
pub local_hash: String,
|
||||
pub server_hash: String,
|
||||
pub local_mtime: Option<i64>,
|
||||
pub server_mtime: Option<i64>,
|
||||
pub path: String,
|
||||
pub local_hash: String,
|
||||
pub server_hash: String,
|
||||
pub local_mtime: Option<i64>,
|
||||
pub server_mtime: Option<i64>,
|
||||
}
|
||||
|
||||
/// Result of resolving a conflict.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ConflictOutcome {
|
||||
/// Use the server version
|
||||
UseServer,
|
||||
/// Use the local version (upload it)
|
||||
UseLocal,
|
||||
/// Keep both versions (rename one)
|
||||
KeepBoth { new_local_path: String },
|
||||
/// Requires manual intervention
|
||||
Manual,
|
||||
/// Use the server version
|
||||
UseServer,
|
||||
/// Use the local version (upload it)
|
||||
UseLocal,
|
||||
/// Keep both versions (rename one)
|
||||
KeepBoth { new_local_path: String },
|
||||
/// Requires manual intervention
|
||||
Manual,
|
||||
}
|
||||
|
||||
/// Resolve a conflict based on the configured strategy.
|
||||
pub fn resolve_conflict(
|
||||
conflict: &ConflictInfo,
|
||||
resolution: ConflictResolution,
|
||||
conflict: &ConflictInfo,
|
||||
resolution: ConflictResolution,
|
||||
) -> ConflictOutcome {
|
||||
match resolution {
|
||||
ConflictResolution::ServerWins => ConflictOutcome::UseServer,
|
||||
ConflictResolution::ClientWins => ConflictOutcome::UseLocal,
|
||||
ConflictResolution::KeepBoth => {
|
||||
let new_path = generate_conflict_path(&conflict.path, &conflict.local_hash);
|
||||
ConflictOutcome::KeepBoth {
|
||||
new_local_path: new_path,
|
||||
}
|
||||
}
|
||||
ConflictResolution::Manual => ConflictOutcome::Manual,
|
||||
}
|
||||
match resolution {
|
||||
ConflictResolution::ServerWins => ConflictOutcome::UseServer,
|
||||
ConflictResolution::ClientWins => ConflictOutcome::UseLocal,
|
||||
ConflictResolution::KeepBoth => {
|
||||
let new_path =
|
||||
generate_conflict_path(&conflict.path, &conflict.local_hash);
|
||||
ConflictOutcome::KeepBoth {
|
||||
new_local_path: new_path,
|
||||
}
|
||||
},
|
||||
ConflictResolution::Manual => ConflictOutcome::Manual,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a new path for the conflicting local file.
|
||||
/// Format: filename.conflict-<short_hash>.ext
|
||||
fn generate_conflict_path(original_path: &str, local_hash: &str) -> String {
|
||||
let short_hash = &local_hash[..8.min(local_hash.len())];
|
||||
let short_hash = &local_hash[..8.min(local_hash.len())];
|
||||
|
||||
if let Some((base, ext)) = original_path.rsplit_once('.') {
|
||||
format!("{}.conflict-{}.{}", base, short_hash, ext)
|
||||
} else {
|
||||
format!("{}.conflict-{}", original_path, short_hash)
|
||||
}
|
||||
if let Some((base, ext)) = original_path.rsplit_once('.') {
|
||||
format!("{}.conflict-{}.{}", base, short_hash, ext)
|
||||
} else {
|
||||
format!("{}.conflict-{}", original_path, short_hash)
|
||||
}
|
||||
}
|
||||
|
||||
/// Automatic conflict resolution based on modification times.
|
||||
/// Useful when ConflictResolution is set to a time-based strategy.
|
||||
pub fn resolve_by_mtime(conflict: &ConflictInfo) -> ConflictOutcome {
|
||||
match (conflict.local_mtime, conflict.server_mtime) {
|
||||
(Some(local), Some(server)) => {
|
||||
if local > server {
|
||||
ConflictOutcome::UseLocal
|
||||
} else {
|
||||
ConflictOutcome::UseServer
|
||||
}
|
||||
}
|
||||
(Some(_), None) => ConflictOutcome::UseLocal,
|
||||
(None, Some(_)) => ConflictOutcome::UseServer,
|
||||
(None, None) => ConflictOutcome::UseServer, // Default to server
|
||||
}
|
||||
match (conflict.local_mtime, conflict.server_mtime) {
|
||||
(Some(local), Some(server)) => {
|
||||
if local > server {
|
||||
ConflictOutcome::UseLocal
|
||||
} else {
|
||||
ConflictOutcome::UseServer
|
||||
}
|
||||
},
|
||||
(Some(_), None) => ConflictOutcome::UseLocal,
|
||||
(None, Some(_)) => ConflictOutcome::UseServer,
|
||||
(None, None) => ConflictOutcome::UseServer, // Default to server
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::sync::FileSyncStatus;
|
||||
use super::*;
|
||||
use crate::sync::FileSyncStatus;
|
||||
|
||||
#[test]
|
||||
fn test_generate_conflict_path() {
|
||||
assert_eq!(
|
||||
generate_conflict_path("/path/to/file.txt", "abc12345"),
|
||||
"/path/to/file.conflict-abc12345.txt"
|
||||
);
|
||||
#[test]
|
||||
fn test_generate_conflict_path() {
|
||||
assert_eq!(
|
||||
generate_conflict_path("/path/to/file.txt", "abc12345"),
|
||||
"/path/to/file.conflict-abc12345.txt"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
generate_conflict_path("/path/to/file", "abc12345"),
|
||||
"/path/to/file.conflict-abc12345"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
generate_conflict_path("/path/to/file", "abc12345"),
|
||||
"/path/to/file.conflict-abc12345"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_conflict() {
|
||||
let state_no_conflict = DeviceSyncState {
|
||||
device_id: super::super::DeviceId::new(),
|
||||
path: "/test".to_string(),
|
||||
local_hash: Some("abc".to_string()),
|
||||
server_hash: Some("abc".to_string()),
|
||||
local_mtime: None,
|
||||
server_mtime: None,
|
||||
sync_status: FileSyncStatus::Synced,
|
||||
last_synced_at: None,
|
||||
conflict_info_json: None,
|
||||
};
|
||||
assert!(detect_conflict(&state_no_conflict).is_none());
|
||||
#[test]
|
||||
fn test_detect_conflict() {
|
||||
let state_no_conflict = DeviceSyncState {
|
||||
device_id: super::super::DeviceId::new(),
|
||||
path: "/test".to_string(),
|
||||
local_hash: Some("abc".to_string()),
|
||||
server_hash: Some("abc".to_string()),
|
||||
local_mtime: None,
|
||||
server_mtime: None,
|
||||
sync_status: FileSyncStatus::Synced,
|
||||
last_synced_at: None,
|
||||
conflict_info_json: None,
|
||||
};
|
||||
assert!(detect_conflict(&state_no_conflict).is_none());
|
||||
|
||||
let state_conflict = DeviceSyncState {
|
||||
device_id: super::super::DeviceId::new(),
|
||||
path: "/test".to_string(),
|
||||
local_hash: Some("abc".to_string()),
|
||||
server_hash: Some("def".to_string()),
|
||||
local_mtime: None,
|
||||
server_mtime: None,
|
||||
sync_status: FileSyncStatus::Conflict,
|
||||
last_synced_at: None,
|
||||
conflict_info_json: None,
|
||||
};
|
||||
assert!(detect_conflict(&state_conflict).is_some());
|
||||
}
|
||||
let state_conflict = DeviceSyncState {
|
||||
device_id: super::super::DeviceId::new(),
|
||||
path: "/test".to_string(),
|
||||
local_hash: Some("abc".to_string()),
|
||||
server_hash: Some("def".to_string()),
|
||||
local_mtime: None,
|
||||
server_mtime: None,
|
||||
sync_status: FileSyncStatus::Conflict,
|
||||
last_synced_at: None,
|
||||
conflict_info_json: None,
|
||||
};
|
||||
assert!(detect_conflict(&state_conflict).is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue