use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use auv_driver_common::error::DriverResult;
use fs2::FileExt;
use crate::error::backend;
static TEMPORARY_SEQUENCE: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum RestoreTokenKind {
ScreenCast,
RemoteDesktopInput,
}
impl RestoreTokenKind {
fn file_name(self) -> &'static str {
match self {
Self::ScreenCast => "screencast-token",
Self::RemoteDesktopInput => "remote-desktop-input-token",
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct RestoreTokenStore {
root: PathBuf,
}
impl RestoreTokenStore {
pub(crate) fn new(root: PathBuf) -> Self {
Self { root }
}
pub(super) fn rotate<T>(
&self,
kind: RestoreTokenKind,
operation: impl FnOnce(Option<&str>) -> DriverResult<(T, Option<String>)>,
) -> DriverResult<T> {
create_private_directory(&self.root)?;
let lock_path = self.root.join(format!("{}.lock", kind.file_name()));
let lock = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&lock_path)
.map_err(|error| backend(format!("failed to open portal restore-token lock {}: {error}", lock_path.display())))?;
set_private_file(&lock_path)?;
lock.lock_exclusive().map_err(|error| backend(format!("failed to lock portal restore-token store {}: {error}", lock_path.display())))?;
let token_path = self.root.join(kind.file_name());
let current = read_token(&token_path)?;
let result = operation(current.as_deref());
let result = match result {
Ok((value, replacement)) => {
replace_token(&token_path, replacement.as_deref())?;
Ok(value)
}
Err(error) => Err(error),
};
let unlock = FileExt::unlock(&lock)
.map_err(|error| backend(format!("failed to unlock portal restore-token store {}: {error}", lock_path.display())));
match (result, unlock) {
(Ok(value), Ok(())) => Ok(value),
(Err(error), Ok(())) => Err(error),
(Ok(_), Err(error)) => Err(error),
(Err(error), Err(unlock_error)) => Err(backend(format!("{error}; additionally {unlock_error}"))),
}
}
}
fn read_token(path: &Path) -> DriverResult<Option<String>> {
match fs::read_to_string(path) {
Ok(token) if token.is_empty() => Err(backend(format!("portal restore-token file {} is empty", path.display()))),
Ok(token) => Ok(Some(token)),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(backend(format!("failed to read portal restore token {}: {error}", path.display()))),
}
}
fn replace_token(path: &Path, replacement: Option<&str>) -> DriverResult<()> {
let Some(replacement) = replacement else {
return match fs::remove_file(path) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(backend(format!("failed to remove consumed portal restore token {}: {error}", path.display()))),
};
};
if replacement.is_empty() {
return Err(backend("portal returned an empty restore token"));
}
let sequence = TEMPORARY_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let temporary = path.with_extension(format!("tmp-{}-{sequence}", std::process::id()));
let write_result = (|| {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&temporary)
.map_err(|error| backend(format!("failed to create portal restore-token temporary file {}: {error}", temporary.display())))?;
set_private_file(&temporary)?;
file
.write_all(replacement.as_bytes())
.map_err(|error| backend(format!("failed to write portal restore-token temporary file {}: {error}", temporary.display())))?;
file
.sync_all()
.map_err(|error| backend(format!("failed to sync portal restore-token temporary file {}: {error}", temporary.display())))?;
fs::rename(&temporary, path).map_err(|error| backend(format!("failed to publish portal restore token {}: {error}", path.display())))?;
Ok(())
})();
if write_result.is_err() {
let _ = fs::remove_file(&temporary);
}
write_result
}
#[cfg(unix)]
fn create_private_directory(path: &Path) -> DriverResult<()> {
use std::os::unix::fs::PermissionsExt;
fs::create_dir_all(path).map_err(|error| backend(format!("failed to create portal state directory {}: {error}", path.display())))?;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))
.map_err(|error| backend(format!("failed to protect portal state directory {}: {error}", path.display())))
}
#[cfg(unix)]
fn set_private_file(path: &Path) -> DriverResult<()> {
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o600))
.map_err(|error| backend(format!("failed to protect portal state file {}: {error}", path.display())))
}
#[cfg(test)]
#[path = "persistence_test.rs"]
mod tests;