265 lines
8.8 KiB
Rust
265 lines
8.8 KiB
Rust
//! Filesystem-backed managed temporary store.
|
|
//!
|
|
//! Allocations live under `<app_data_dir>/managed-tmp/<opaque-handle>/`.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use async_trait::async_trait;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use domain::ports::{LocalPath, ManagedTempStore};
|
|
use domain::{
|
|
ManagedTempAllocation, ManagedTempError, ManagedTempGcReport, ManagedTempHandle,
|
|
ManagedTempKind, ManagedTempLease, ManagedTempState, ProjectId,
|
|
};
|
|
|
|
const MANAGED_TMP_DIR: &str = "managed-tmp";
|
|
const METADATA_FILE: &str = "metadata.json";
|
|
const DATA_NAME: &str = "data";
|
|
|
|
/// Filesystem implementation for global IdeA-managed temporary allocations.
|
|
#[derive(Debug, Clone)]
|
|
pub struct FsManagedTempStore {
|
|
root: PathBuf,
|
|
}
|
|
|
|
impl FsManagedTempStore {
|
|
/// Builds the store under `<app_data_dir>/managed-tmp`.
|
|
#[must_use]
|
|
pub fn new(app_data_dir: impl Into<PathBuf>) -> Self {
|
|
Self {
|
|
root: app_data_dir.into().join(MANAGED_TMP_DIR),
|
|
}
|
|
}
|
|
|
|
/// Returns the global managed temp root.
|
|
#[must_use]
|
|
pub fn root(&self) -> &Path {
|
|
&self.root
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct StoredLease {
|
|
handle: ManagedTempHandle,
|
|
kind: ManagedTempKind,
|
|
owner_project_id: Option<ProjectId>,
|
|
purpose: String,
|
|
state: ManagedTempState,
|
|
created_at_ms: u64,
|
|
last_access_at_ms: u64,
|
|
expires_at_ms: Option<u64>,
|
|
released_at_ms: Option<u64>,
|
|
}
|
|
|
|
impl StoredLease {
|
|
fn from_allocation(allocation: ManagedTempAllocation) -> Self {
|
|
Self {
|
|
handle: allocation.handle,
|
|
kind: allocation.kind,
|
|
owner_project_id: allocation.owner_project_id,
|
|
purpose: allocation.purpose,
|
|
state: ManagedTempState::Active,
|
|
created_at_ms: allocation.created_at_ms,
|
|
last_access_at_ms: allocation.created_at_ms,
|
|
expires_at_ms: allocation.expires_at_ms,
|
|
released_at_ms: None,
|
|
}
|
|
}
|
|
|
|
fn is_expired(&self, now_ms: u64) -> bool {
|
|
self.expires_at_ms.is_some_and(|expires| expires <= now_ms)
|
|
}
|
|
|
|
fn to_lease(&self, path: PathBuf) -> ManagedTempLease {
|
|
ManagedTempLease {
|
|
handle: self.handle.clone(),
|
|
kind: self.kind,
|
|
owner_project_id: self.owner_project_id,
|
|
purpose: self.purpose.clone(),
|
|
path: LocalPath::new(path.to_string_lossy().into_owned()),
|
|
state: self.state,
|
|
created_at_ms: self.created_at_ms,
|
|
last_access_at_ms: self.last_access_at_ms,
|
|
expires_at_ms: self.expires_at_ms,
|
|
released_at_ms: self.released_at_ms,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn io_error(err: std::io::Error) -> ManagedTempError {
|
|
ManagedTempError::Store(err.to_string())
|
|
}
|
|
|
|
fn serde_error(err: serde_json::Error) -> ManagedTempError {
|
|
ManagedTempError::Store(err.to_string())
|
|
}
|
|
|
|
fn allocation_dir(root: &Path, handle: &ManagedTempHandle) -> PathBuf {
|
|
root.join(handle.as_str())
|
|
}
|
|
|
|
fn metadata_path(root: &Path, handle: &ManagedTempHandle) -> PathBuf {
|
|
allocation_dir(root, handle).join(METADATA_FILE)
|
|
}
|
|
|
|
fn data_path(root: &Path, handle: &ManagedTempHandle) -> PathBuf {
|
|
allocation_dir(root, handle).join(DATA_NAME)
|
|
}
|
|
|
|
fn ensure_under_root(root: &Path, candidate: &Path) -> Result<(), ManagedTempError> {
|
|
if candidate.starts_with(root) {
|
|
Ok(())
|
|
} else {
|
|
Err(ManagedTempError::Invalid(
|
|
"managed temp path escaped root".to_owned(),
|
|
))
|
|
}
|
|
}
|
|
|
|
async fn read_stored(
|
|
root: &Path,
|
|
handle: &ManagedTempHandle,
|
|
) -> Result<StoredLease, ManagedTempError> {
|
|
let path = metadata_path(root, handle);
|
|
let bytes = tokio::fs::read(&path).await.map_err(|err| {
|
|
if err.kind() == std::io::ErrorKind::NotFound {
|
|
ManagedTempError::NotFound
|
|
} else {
|
|
io_error(err)
|
|
}
|
|
})?;
|
|
let stored: StoredLease = serde_json::from_slice(&bytes).map_err(serde_error)?;
|
|
if stored.handle != *handle {
|
|
return Err(ManagedTempError::Invalid(
|
|
"managed temp metadata handle mismatch".to_owned(),
|
|
));
|
|
}
|
|
Ok(stored)
|
|
}
|
|
|
|
async fn write_stored(root: &Path, stored: &StoredLease) -> Result<(), ManagedTempError> {
|
|
let path = metadata_path(root, &stored.handle);
|
|
let bytes = serde_json::to_vec_pretty(stored).map_err(serde_error)?;
|
|
tokio::fs::write(path, bytes).await.map_err(io_error)
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ManagedTempStore for FsManagedTempStore {
|
|
async fn allocate(
|
|
&self,
|
|
allocation: ManagedTempAllocation,
|
|
) -> Result<ManagedTempLease, ManagedTempError> {
|
|
let handle = allocation.handle.clone();
|
|
let dir = allocation_dir(&self.root, &handle);
|
|
let data = data_path(&self.root, &handle);
|
|
ensure_under_root(&self.root, &dir)?;
|
|
ensure_under_root(&self.root, &data)?;
|
|
tokio::fs::create_dir_all(&self.root)
|
|
.await
|
|
.map_err(io_error)?;
|
|
tokio::fs::create_dir(&dir).await.map_err(io_error)?;
|
|
match allocation.kind {
|
|
ManagedTempKind::Directory => tokio::fs::create_dir(&data).await.map_err(io_error)?,
|
|
ManagedTempKind::File => {
|
|
tokio::fs::OpenOptions::new()
|
|
.write(true)
|
|
.create_new(true)
|
|
.open(&data)
|
|
.await
|
|
.map_err(io_error)?;
|
|
}
|
|
}
|
|
let stored = StoredLease::from_allocation(allocation);
|
|
write_stored(&self.root, &stored).await?;
|
|
Ok(stored.to_lease(data))
|
|
}
|
|
|
|
async fn resolve(
|
|
&self,
|
|
handle: &ManagedTempHandle,
|
|
now_ms: u64,
|
|
) -> Result<ManagedTempLease, ManagedTempError> {
|
|
let mut stored = read_stored(&self.root, handle).await?;
|
|
if stored.state != ManagedTempState::Active || stored.is_expired(now_ms) {
|
|
return Err(ManagedTempError::NotFound);
|
|
}
|
|
let data = data_path(&self.root, handle);
|
|
ensure_under_root(&self.root, &data)?;
|
|
if tokio::fs::metadata(&data).await.is_err() {
|
|
return Err(ManagedTempError::NotFound);
|
|
}
|
|
stored.last_access_at_ms = now_ms;
|
|
write_stored(&self.root, &stored).await?;
|
|
Ok(stored.to_lease(data))
|
|
}
|
|
|
|
async fn release(
|
|
&self,
|
|
handle: &ManagedTempHandle,
|
|
now_ms: u64,
|
|
) -> Result<ManagedTempLease, ManagedTempError> {
|
|
let mut stored = read_stored(&self.root, handle).await?;
|
|
stored.state = ManagedTempState::Released;
|
|
stored.released_at_ms = Some(now_ms);
|
|
stored.last_access_at_ms = now_ms;
|
|
write_stored(&self.root, &stored).await?;
|
|
Ok(stored.to_lease(data_path(&self.root, handle)))
|
|
}
|
|
|
|
async fn gc(&self, now_ms: u64) -> Result<ManagedTempGcReport, ManagedTempError> {
|
|
let mut deleted = 0usize;
|
|
let mut entries = match tokio::fs::read_dir(&self.root).await {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
return Ok(ManagedTempGcReport { deleted: 0 })
|
|
}
|
|
Err(err) => return Err(io_error(err)),
|
|
};
|
|
while let Some(entry) = entries.next_entry().await.map_err(io_error)? {
|
|
let file_type = entry.file_type().await.map_err(io_error)?;
|
|
if !file_type.is_dir() {
|
|
continue;
|
|
}
|
|
let raw_handle = entry.file_name().to_string_lossy().into_owned();
|
|
let Ok(handle) = ManagedTempHandle::new(raw_handle) else {
|
|
continue;
|
|
};
|
|
let Ok(stored) = read_stored(&self.root, &handle).await else {
|
|
continue;
|
|
};
|
|
if stored.state == ManagedTempState::Released || stored.is_expired(now_ms) {
|
|
tokio::fs::remove_dir_all(entry.path())
|
|
.await
|
|
.map_err(io_error)?;
|
|
deleted += 1;
|
|
}
|
|
}
|
|
Ok(ManagedTempGcReport { deleted })
|
|
}
|
|
|
|
async fn purge_all(&self) -> Result<ManagedTempGcReport, ManagedTempError> {
|
|
let mut deleted = 0usize;
|
|
let mut entries = match tokio::fs::read_dir(&self.root).await {
|
|
Ok(entries) => entries,
|
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
|
return Ok(ManagedTempGcReport { deleted: 0 });
|
|
}
|
|
Err(err) => return Err(io_error(err)),
|
|
};
|
|
while let Some(entry) = entries.next_entry().await.map_err(io_error)? {
|
|
let path = entry.path();
|
|
let removed = match entry.file_type().await {
|
|
Ok(file_type) if file_type.is_dir() => tokio::fs::remove_dir_all(&path).await,
|
|
Ok(_) => tokio::fs::remove_file(&path).await,
|
|
Err(err) => Err(err),
|
|
};
|
|
if removed.is_ok() {
|
|
deleted += 1;
|
|
}
|
|
}
|
|
Ok(ManagedTempGcReport { deleted })
|
|
}
|
|
}
|