use std::fs::{self, OpenOptions};
use std::io::{ErrorKind, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use anyhow::{anyhow, Context, Result};
pub const FIRST_RUN_SENTINEL: &str = "First Run";
pub const LOCAL_STATE_FILE: &str = "Local State";
pub const INITIAL_LOCAL_STATE: &str =
r#"{"browser":{"last_whats_new_version":9999},"fre":{"has_user_seen_fre":true}}"#;
pub const TEMPORARY_PROFILE_PREFIX: &str = "browser-commander-profile-";
const REMOVE_ATTEMPTS: usize = 5;
const REMOVE_RETRY_DELAY: Duration = Duration::from_millis(200);
pub fn prepare_user_data_dir(user_data_dir: &Path) -> Result<PathBuf> {
fs::create_dir_all(user_data_dir).with_context(|| {
format!(
"Could not create user data directory {}",
user_data_dir.display()
)
})?;
OpenOptions::new()
.create(true)
.append(true)
.open(user_data_dir.join(FIRST_RUN_SENTINEL))
.with_context(|| format!("Could not write {FIRST_RUN_SENTINEL} sentinel"))?;
match OpenOptions::new()
.write(true)
.create_new(true)
.open(user_data_dir.join(LOCAL_STATE_FILE))
{
Ok(mut file) => file
.write_all(INITIAL_LOCAL_STATE.as_bytes())
.with_context(|| format!("Could not write {LOCAL_STATE_FILE}"))?,
Err(error) if error.kind() == ErrorKind::AlreadyExists => {}
Err(error) => {
return Err(anyhow!(error).context(format!("Could not write {LOCAL_STATE_FILE}")))
}
}
Ok(user_data_dir.to_path_buf())
}
fn unique_suffix() -> String {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|elapsed| elapsed.as_nanos())
.unwrap_or_default();
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{}-{nanos:x}-{count}", std::process::id())
}
pub fn create_temporary_user_data_dir(parent: Option<&Path>) -> Result<PathBuf> {
let parent = parent.map_or_else(std::env::temp_dir, Path::to_path_buf);
fs::create_dir_all(&parent)
.with_context(|| format!("Could not create {}", parent.display()))?;
loop {
let candidate = parent.join(format!("{TEMPORARY_PROFILE_PREFIX}{}", unique_suffix()));
match fs::create_dir(&candidate) {
Ok(()) => return prepare_user_data_dir(&candidate),
Err(error) if error.kind() == ErrorKind::AlreadyExists => continue,
Err(error) => {
return Err(anyhow!(error).context(format!(
"Could not create a temporary profile in {}",
parent.display()
)))
}
}
}
}
pub async fn remove_user_data_dir(user_data_dir: &Path) -> Result<()> {
let mut attempt = 0;
loop {
match fs::remove_dir_all(user_data_dir) {
Ok(()) => return Ok(()),
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()),
Err(error) => {
attempt += 1;
if attempt >= REMOVE_ATTEMPTS {
return Err(anyhow!(error).context(format!(
"Could not remove temporary profile {}",
user_data_dir.display()
)));
}
tokio::time::sleep(REMOVE_RETRY_DELAY).await;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn temporary_profiles_are_seeded_and_removed() {
let directory = create_temporary_user_data_dir(None).unwrap();
let name = directory
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
assert!(name.starts_with(TEMPORARY_PROFILE_PREFIX), "{name}");
assert_eq!(
fs::read_to_string(directory.join(FIRST_RUN_SENTINEL)).unwrap(),
""
);
assert_eq!(
fs::read_to_string(directory.join(LOCAL_STATE_FILE)).unwrap(),
INITIAL_LOCAL_STATE
);
let local_state: serde_json::Value = serde_json::from_str(INITIAL_LOCAL_STATE).unwrap();
assert_eq!(
local_state,
serde_json::json!({
"browser": {"last_whats_new_version": 9999},
"fre": {"has_user_seen_fre": true},
})
);
let other = create_temporary_user_data_dir(None).unwrap();
assert_ne!(directory, other);
remove_user_data_dir(&directory).await.unwrap();
remove_user_data_dir(&other).await.unwrap();
assert!(!directory.exists());
remove_user_data_dir(&directory).await.unwrap();
}
#[tokio::test]
async fn preparing_keeps_existing_profile_files() {
let directory = create_temporary_user_data_dir(None).unwrap();
fs::write(directory.join(FIRST_RUN_SENTINEL), "kept").unwrap();
fs::write(directory.join(LOCAL_STATE_FILE), "{}").unwrap();
prepare_user_data_dir(&directory).unwrap();
assert_eq!(
fs::read_to_string(directory.join(FIRST_RUN_SENTINEL)).unwrap(),
"kept"
);
assert_eq!(
fs::read_to_string(directory.join(LOCAL_STATE_FILE)).unwrap(),
"{}"
);
remove_user_data_dir(&directory).await.unwrap();
}
}