Rust-centric architecture with TypeScript facade following smartproxy/smartstorage pattern. Core engine in Rust (FastCDC chunking, SHA-256, gzip, AES-256-GCM + Argon2id, binary pack files, global index, snapshots, locking, verification, pruning, repair). TypeScript provides npm interface via @push.rocks/smartrust RustBridge IPC with Unix socket streaming for ingest/restore. All 14 integration tests pass.
195 lines
6.0 KiB
Rust
195 lines
6.0 KiB
Rust
/// Advisory file-based locking for repository write operations.
|
|
|
|
use std::path::Path;
|
|
use serde::{Deserialize, Serialize};
|
|
use crate::error::ArchiveError;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct LockEntry {
|
|
pub lock_id: String,
|
|
pub pid: u32,
|
|
pub hostname: String,
|
|
pub created_at: String,
|
|
pub operation: String,
|
|
pub stale_after_seconds: u64,
|
|
}
|
|
|
|
/// Acquire a lock for the given operation.
|
|
pub async fn acquire(repo_path: &str, operation: &str) -> Result<LockEntry, ArchiveError> {
|
|
let locks_dir = Path::new(repo_path).join("locks");
|
|
tokio::fs::create_dir_all(&locks_dir).await?;
|
|
|
|
// Check for existing locks
|
|
if let Some(existing) = get_active_lock(repo_path).await? {
|
|
return Err(ArchiveError::Locked(format!(
|
|
"Repository locked by PID {} on {} for operation '{}' since {}",
|
|
existing.pid, existing.hostname, existing.operation, existing.created_at
|
|
)));
|
|
}
|
|
|
|
let lock_id = uuid::Uuid::new_v4().to_string();
|
|
let hostname = std::env::var("HOSTNAME")
|
|
.or_else(|_| std::env::var("HOST"))
|
|
.unwrap_or_else(|_| "unknown".to_string());
|
|
|
|
let entry = LockEntry {
|
|
lock_id: lock_id.clone(),
|
|
pid: std::process::id(),
|
|
hostname,
|
|
created_at: chrono::Utc::now().to_rfc3339(),
|
|
operation: operation.to_string(),
|
|
stale_after_seconds: 21600, // 6 hours
|
|
};
|
|
|
|
let lock_path = locks_dir.join(format!("{}.json", lock_id));
|
|
let json = serde_json::to_string_pretty(&entry)?;
|
|
|
|
// Use create_new for atomic lock creation
|
|
tokio::fs::write(&lock_path, json).await?;
|
|
|
|
tracing::info!("Acquired lock {} for operation '{}'", lock_id, operation);
|
|
Ok(entry)
|
|
}
|
|
|
|
/// Release a specific lock.
|
|
pub async fn release(repo_path: &str, lock_id: &str) -> Result<(), ArchiveError> {
|
|
let lock_path = Path::new(repo_path).join("locks").join(format!("{}.json", lock_id));
|
|
if lock_path.exists() {
|
|
tokio::fs::remove_file(&lock_path).await?;
|
|
tracing::info!("Released lock {}", lock_id);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if the repository is locked.
|
|
pub async fn is_locked(repo_path: &str) -> Result<bool, ArchiveError> {
|
|
Ok(get_active_lock(repo_path).await?.is_some())
|
|
}
|
|
|
|
/// Get the active (non-stale) lock, if any.
|
|
async fn get_active_lock(repo_path: &str) -> Result<Option<LockEntry>, ArchiveError> {
|
|
let locks_dir = Path::new(repo_path).join("locks");
|
|
if !locks_dir.exists() {
|
|
return Ok(None);
|
|
}
|
|
|
|
let mut dir = tokio::fs::read_dir(&locks_dir).await?;
|
|
while let Some(entry) = dir.next_entry().await? {
|
|
let path = entry.path();
|
|
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
|
continue;
|
|
}
|
|
|
|
let data = tokio::fs::read_to_string(&path).await?;
|
|
let lock: LockEntry = match serde_json::from_str(&data) {
|
|
Ok(l) => l,
|
|
Err(_) => {
|
|
// Corrupted lock file — remove it
|
|
let _ = tokio::fs::remove_file(&path).await;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
if is_stale(&lock) {
|
|
tracing::warn!("Removing stale lock {} (from {})", lock.lock_id, lock.created_at);
|
|
let _ = tokio::fs::remove_file(&path).await;
|
|
continue;
|
|
}
|
|
|
|
return Ok(Some(lock));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
/// Check and break all stale locks. Returns the number of locks removed.
|
|
pub async fn check_and_break_stale(repo_path: &str) -> Result<u32, ArchiveError> {
|
|
let locks_dir = Path::new(repo_path).join("locks");
|
|
if !locks_dir.exists() {
|
|
return Ok(0);
|
|
}
|
|
|
|
let mut removed = 0u32;
|
|
let mut dir = tokio::fs::read_dir(&locks_dir).await?;
|
|
while let Some(entry) = dir.next_entry().await? {
|
|
let path = entry.path();
|
|
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
|
continue;
|
|
}
|
|
|
|
let data = match tokio::fs::read_to_string(&path).await {
|
|
Ok(d) => d,
|
|
Err(_) => continue,
|
|
};
|
|
let lock: LockEntry = match serde_json::from_str(&data) {
|
|
Ok(l) => l,
|
|
Err(_) => {
|
|
let _ = tokio::fs::remove_file(&path).await;
|
|
removed += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
if is_stale(&lock) {
|
|
tracing::warn!("Breaking stale lock {} (from {})", lock.lock_id, lock.created_at);
|
|
let _ = tokio::fs::remove_file(&path).await;
|
|
removed += 1;
|
|
}
|
|
}
|
|
|
|
Ok(removed)
|
|
}
|
|
|
|
/// Break all locks (forced unlock).
|
|
pub async fn break_all_locks(repo_path: &str, force: bool) -> Result<u32, ArchiveError> {
|
|
let locks_dir = Path::new(repo_path).join("locks");
|
|
if !locks_dir.exists() {
|
|
return Ok(0);
|
|
}
|
|
|
|
let mut removed = 0u32;
|
|
let mut dir = tokio::fs::read_dir(&locks_dir).await?;
|
|
while let Some(entry) = dir.next_entry().await? {
|
|
let path = entry.path();
|
|
if path.extension().and_then(|e| e.to_str()) != Some("json") {
|
|
continue;
|
|
}
|
|
|
|
if force {
|
|
let _ = tokio::fs::remove_file(&path).await;
|
|
removed += 1;
|
|
} else {
|
|
// Only break stale locks
|
|
let data = match tokio::fs::read_to_string(&path).await {
|
|
Ok(d) => d,
|
|
Err(_) => continue,
|
|
};
|
|
let lock: LockEntry = match serde_json::from_str(&data) {
|
|
Ok(l) => l,
|
|
Err(_) => {
|
|
let _ = tokio::fs::remove_file(&path).await;
|
|
removed += 1;
|
|
continue;
|
|
}
|
|
};
|
|
|
|
if is_stale(&lock) {
|
|
let _ = tokio::fs::remove_file(&path).await;
|
|
removed += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(removed)
|
|
}
|
|
|
|
fn is_stale(lock: &LockEntry) -> bool {
|
|
if let Ok(created) = chrono::DateTime::parse_from_rfc3339(&lock.created_at) {
|
|
let age = chrono::Utc::now().signed_duration_since(created);
|
|
age.num_seconds() > lock.stale_after_seconds as i64
|
|
} else {
|
|
true // Can't parse timestamp — treat as stale
|
|
}
|
|
}
|