use std::{
io::{self, ErrorKind, Read, Write},
path::{Component, Path, PathBuf},
};
use cap_fs_ext::{
DirExt, FollowSymlinks, MetadataExt as CrossPlatformMetadataExt, OpenOptionsFollowExt,
};
use cap_std::{
ambient_authority,
fs::{Dir, File, OpenOptions, Permissions},
};
use uuid::Uuid;
use crate::{SyncClientError, device_api::is_subject_id};
const OWNER_FILE: &str = ".sync-owner-user-id";
const OWNER_TEMP_PREFIX: &str = ".sync-owner-tmp-";
const MAX_SUBJECT_ID_BYTES: usize = 128;
#[derive(Clone, Debug)]
pub struct SyncOwnerStore {
root: PathBuf,
}
impl SyncOwnerStore {
pub fn new(browser_data_root: &Path) -> Self {
Self { root: browser_data_root.to_path_buf() }
}
pub fn claim(&self, user_id: &str) -> Result<(), SyncClientError> {
validate_subject_id(user_id)?;
let directory = open_root(&self.root, true)?
.ok_or_else(|| storage("browser data root is unavailable"))?;
if let Some(owner) = read_owner(&directory)? {
return verify_owner(&owner, user_id);
}
persist_owner(&directory, user_id)
}
pub fn verify(&self, user_id: &str) -> Result<(), SyncClientError> {
validate_subject_id(user_id)?;
let Some(directory) = open_root(&self.root, false)? else {
return Err(SyncClientError::SyncOwnerUnclaimed);
};
let owner = read_owner(&directory)?.ok_or(SyncClientError::SyncOwnerUnclaimed)?;
verify_owner(&owner, user_id)
}
}
fn persist_owner(directory: &Dir, user_id: &str) -> Result<(), SyncClientError> {
let temporary = format!("{OWNER_TEMP_PREFIX}{}", Uuid::now_v7().simple());
let mut options = private_open_options();
options.write(true).create_new(true);
let mut file = directory.open_with(&temporary, &options).map_err(storage_io)?;
validate_private_file(&file)?;
set_private_file_permissions(&file)?;
let write_result = file
.write_all(user_id.as_bytes())
.and_then(|()| file.write_all(b"\n"))
.and_then(|()| file.sync_all());
drop(file);
if let Err(error) = write_result {
let _ = remove_temporary(directory, &temporary);
return Err(storage_io(error));
}
match publish_owner(directory, &temporary) {
Ok(()) => sync_directory(directory)?,
Err(error) if error.kind() == ErrorKind::AlreadyExists => {
remove_temporary(directory, &temporary)?;
}
Err(error) => {
let _ = remove_temporary(directory, &temporary);
return Err(storage_io(error));
}
}
let owner = read_owner(directory)?.ok_or_else(|| storage("sync owner disappeared"))?;
verify_owner(&owner, user_id)
}
fn read_owner(directory: &Dir) -> Result