mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-05 18:58:40 +02:00
feat: e2e encrypted sync
This commit is contained in:
+188
-26
@@ -8,9 +8,132 @@ lazy_static::lazy_static! {
|
||||
static ref EPHEMERAL_DIRS: Mutex<HashMap<String, PathBuf>> = Mutex::new(HashMap::new());
|
||||
}
|
||||
|
||||
/// Get or create the RAM-backed base directory for ephemeral profiles.
|
||||
/// Linux: /dev/shm (always tmpfs). macOS: RAM disk via hdiutil. Windows: imdisk RAM disk.
|
||||
fn get_ephemeral_base_dir() -> Result<PathBuf, String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let base = PathBuf::from("/dev/shm/donut-ephemeral");
|
||||
std::fs::create_dir_all(&base)
|
||||
.map_err(|e| format!("Failed to create ephemeral base in /dev/shm: {e}"))?;
|
||||
return Ok(base);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if let Ok(mount) = get_or_create_macos_ramdisk() {
|
||||
return Ok(mount);
|
||||
}
|
||||
log::warn!("Failed to create macOS RAM disk, ephemeral profiles may use disk");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(mount) = get_or_create_windows_ramdisk() {
|
||||
return Ok(mount);
|
||||
}
|
||||
log::warn!("Failed to create Windows RAM disk, ephemeral profiles may use disk");
|
||||
}
|
||||
|
||||
// Fallback
|
||||
let base = std::env::temp_dir().join("donut-ephemeral");
|
||||
std::fs::create_dir_all(&base)
|
||||
.map_err(|e| format!("Failed to create ephemeral base dir: {e}"))?;
|
||||
Ok(base)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn get_or_create_macos_ramdisk() -> Result<PathBuf, String> {
|
||||
let mount_point = PathBuf::from("/Volumes/DonutEphemeral");
|
||||
|
||||
// Reuse existing RAM disk from a previous session
|
||||
if mount_point.exists() && mount_point.is_dir() {
|
||||
return Ok(mount_point);
|
||||
}
|
||||
|
||||
// 256 MB in 512-byte sectors
|
||||
let sectors = 256 * 2048;
|
||||
let output = std::process::Command::new("hdiutil")
|
||||
.args(["attach", "-nomount", &format!("ram://{sectors}")])
|
||||
.output()
|
||||
.map_err(|e| format!("hdiutil attach failed: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(format!(
|
||||
"hdiutil attach failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
let dev = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
|
||||
let fmt = std::process::Command::new("diskutil")
|
||||
.args(["erasevolume", "HFS+", "DonutEphemeral", &dev])
|
||||
.output()
|
||||
.map_err(|e| format!("diskutil erasevolume failed: {e}"))?;
|
||||
|
||||
if !fmt.status.success() {
|
||||
let _ = std::process::Command::new("hdiutil")
|
||||
.args(["detach", &dev])
|
||||
.output();
|
||||
return Err(format!(
|
||||
"diskutil erasevolume failed: {}",
|
||||
String::from_utf8_lossy(&fmt.stderr)
|
||||
));
|
||||
}
|
||||
|
||||
log::info!("Created macOS RAM disk at {}", mount_point.display());
|
||||
Ok(mount_point)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_or_create_windows_ramdisk() -> Result<PathBuf, String> {
|
||||
// Check if a previous RAM disk with our directory already exists
|
||||
for letter in ['R', 'Q', 'P', 'O'] {
|
||||
let base = PathBuf::from(format!("{}:\\DonutEphemeral", letter));
|
||||
if base.exists() && base.is_dir() {
|
||||
return Ok(base);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to create a RAM disk using imdisk (open-source RAM disk driver)
|
||||
for letter in ['R', 'Q', 'P', 'O'] {
|
||||
let drive = format!("{}:", letter);
|
||||
if PathBuf::from(format!("{}\\", drive)).exists() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let output = std::process::Command::new("imdisk")
|
||||
.args(["-a", "-s", "256M", "-m", &drive, "-p", "/fs:ntfs /q /y"])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) if out.status.success() => {
|
||||
let base = PathBuf::from(format!("{}\\DonutEphemeral", drive));
|
||||
std::fs::create_dir_all(&base)
|
||||
.map_err(|e| format!("Failed to create dir on RAM disk: {e}"))?;
|
||||
log::info!("Created Windows RAM disk at {}", base.display());
|
||||
return Ok(base);
|
||||
}
|
||||
Ok(out) => {
|
||||
log::debug!(
|
||||
"imdisk failed for drive {}: {}",
|
||||
drive,
|
||||
String::from_utf8_lossy(&out.stderr)
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(format!("imdisk not available: {e}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Could not create Windows RAM disk".to_string())
|
||||
}
|
||||
|
||||
pub fn create_ephemeral_dir(profile_id: &str) -> Result<PathBuf, String> {
|
||||
let dir_name = format!("donut-ephemeral-{profile_id}");
|
||||
let dir_path = std::env::temp_dir().join(dir_name);
|
||||
let base = get_ephemeral_base_dir()?;
|
||||
let dir_path = base.join(profile_id);
|
||||
|
||||
std::fs::create_dir_all(&dir_path).map_err(|e| format!("Failed to create ephemeral dir: {e}"))?;
|
||||
|
||||
@@ -53,26 +176,60 @@ pub fn remove_ephemeral_dir(profile_id: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cleanup_stale_dirs() {
|
||||
/// Recover ephemeral dir mappings on startup by scanning the RAM-backed base dir.
|
||||
/// Dir names are profile UUIDs, so we re-populate the in-memory HashMap.
|
||||
/// Also cleans up old disk-based dirs from previous versions.
|
||||
pub fn recover_ephemeral_dirs() {
|
||||
cleanup_legacy_dirs();
|
||||
|
||||
let base = match get_ephemeral_base_dir() {
|
||||
Ok(base) => base,
|
||||
Err(e) => {
|
||||
log::warn!("Cannot recover ephemeral dirs: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let entries = match std::fs::read_dir(&base) {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let mut dirs = match EPHEMERAL_DIRS.lock() {
|
||||
Ok(dirs) => dirs,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
if entry.path().is_dir() {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if uuid::Uuid::parse_str(name).is_ok() {
|
||||
dirs.insert(name.to_string(), entry.path());
|
||||
log::info!("Recovered ephemeral dir for profile {}", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove old-format ephemeral dirs from /tmp (pre-tmpfs migration).
|
||||
fn cleanup_legacy_dirs() {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let entries = match std::fs::read_dir(&temp_dir) {
|
||||
Ok(entries) => entries,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to read temp dir for ephemeral cleanup: {e}");
|
||||
return;
|
||||
}
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
if name.starts_with("donut-ephemeral-") && entry.path().is_dir() {
|
||||
if let Err(e) = std::fs::remove_dir_all(entry.path()) {
|
||||
log::warn!(
|
||||
"Failed to clean up stale ephemeral dir {}: {e}",
|
||||
log::warn!("Failed to clean up legacy ephemeral dir: {e}");
|
||||
} else {
|
||||
log::info!(
|
||||
"Cleaned up legacy ephemeral dir: {}",
|
||||
entry.path().display()
|
||||
);
|
||||
} else {
|
||||
log::info!("Cleaned up stale ephemeral dir: {}", entry.path().display());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,7 +265,8 @@ mod tests {
|
||||
group_id: None,
|
||||
tags: Vec::new(),
|
||||
note: None,
|
||||
sync_enabled: false,
|
||||
sync_mode: crate::profile::types::SyncMode::Disabled,
|
||||
encryption_salt: None,
|
||||
last_sync: None,
|
||||
host_os: None,
|
||||
ephemeral,
|
||||
@@ -117,17 +275,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_ephemeral_dir_lifecycle() {
|
||||
// Test create, get, effective path, remove, and cleanup all in sequence
|
||||
// to avoid race conditions between parallel tests.
|
||||
|
||||
// 1. Create and get
|
||||
let profile_id = uuid::Uuid::new_v4();
|
||||
let id_str = profile_id.to_string();
|
||||
|
||||
let dir = create_ephemeral_dir(&id_str).unwrap();
|
||||
assert!(dir.is_dir());
|
||||
assert_eq!(get_ephemeral_dir(&id_str), Some(dir.clone()));
|
||||
|
||||
// 2. Effective path for ephemeral profile returns ephemeral dir
|
||||
let ephemeral_profile = make_test_profile(profile_id, true);
|
||||
let profiles_dir = std::env::temp_dir().join("test_profiles_ephemeral");
|
||||
assert_eq!(
|
||||
@@ -135,25 +289,33 @@ mod tests {
|
||||
dir
|
||||
);
|
||||
|
||||
// 3. Remove cleans up dir and map entry
|
||||
remove_ephemeral_dir(&id_str);
|
||||
assert!(!dir.exists());
|
||||
assert!(get_ephemeral_dir(&id_str).is_none());
|
||||
|
||||
// 4. Effective path for persistent profile returns normal path
|
||||
let persistent_profile = make_test_profile(uuid::Uuid::new_v4(), false);
|
||||
let expected = persistent_profile.get_profile_data_path(&profiles_dir);
|
||||
assert_eq!(
|
||||
get_effective_profile_path(&persistent_profile, &profiles_dir),
|
||||
expected
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Cleanup stale dirs
|
||||
let stale_id = uuid::Uuid::new_v4().to_string();
|
||||
let stale_dir = std::env::temp_dir().join(format!("donut-ephemeral-{stale_id}"));
|
||||
std::fs::create_dir_all(&stale_dir).unwrap();
|
||||
assert!(stale_dir.exists());
|
||||
cleanup_stale_dirs();
|
||||
assert!(!stale_dir.exists());
|
||||
#[test]
|
||||
fn test_recover_ephemeral_dirs() {
|
||||
let base = get_ephemeral_base_dir().unwrap();
|
||||
let test_id = uuid::Uuid::new_v4().to_string();
|
||||
let test_dir = base.join(&test_id);
|
||||
std::fs::create_dir_all(&test_dir).unwrap();
|
||||
|
||||
// Clear the HashMap so recovery has something to find
|
||||
EPHEMERAL_DIRS.lock().unwrap().remove(&test_id);
|
||||
assert!(get_ephemeral_dir(&test_id).is_none());
|
||||
|
||||
recover_ephemeral_dirs();
|
||||
assert_eq!(get_ephemeral_dir(&test_id), Some(test_dir.clone()));
|
||||
|
||||
// Clean up
|
||||
remove_ephemeral_dir(&test_id);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user