use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::process::Command;
use crate::{
config::Config, error::AppError, lock::LockManager, metadata::MetadataManager, r2::R2Client,
sync::SyncEngine,
};
pub fn find_rpp_file(dir: &Path) -> Result<PathBuf, AppError> {
let entries = std::fs::read_dir(dir).map_err(|e| AppError::IoError {
path: dir.display().to_string(),
source: e,
})?;
let mut rpp_files: Vec<PathBuf> = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| AppError::IoError {
path: dir.display().to_string(),
source: e,
})?;
let path = entry.path();
if !path.is_file() {
continue;
}
if path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.eq_ignore_ascii_case("rpp"))
.unwrap_or(false)
{
rpp_files.push(path);
}
}
match rpp_files.len() {
1 => Ok(rpp_files.remove(0)),
0 => Err(AppError::Other(format!(
"No .rpp or .RPP file found in {}",
dir.display()
))),
_ => {
let names: Vec<String> = rpp_files
.iter()
.filter_map(|p| p.file_name().and_then(|n| n.to_str()).map(str::to_string))
.collect();
Err(AppError::Other(format!(
"Multiple .rpp files found in {} (expected exactly one): {}",
dir.display(),
names.join(", ")
)))
}
}
}
pub async fn run_session(
project: &str,
config: Arc<Config>,
r2: Arc<R2Client>,
) -> Result<(), AppError> {
if !config.reaper.binary_path.exists() {
return Err(AppError::ReaperNotFound {
path: config.reaper.binary_path.display().to_string(),
});
}
let lm = LockManager::new(Arc::clone(&r2), Arc::clone(&config));
let sync_engine = SyncEngine::new(Arc::clone(&r2));
let metadata_manager = MetadataManager::new(Arc::clone(&r2));
let local_dir = config.local.working_dir.join(project);
println!("Acquiring lock for {}...", project);
let lock_guard = lm.acquire(project).await?;
println!("Pulling latest files...");
std::fs::create_dir_all(&local_dir).map_err(|e| AppError::IoError {
path: local_dir.display().to_string(),
source: e,
})?;
sync_engine.pull(project, &local_dir).await?;
let rpp_path = find_rpp_file(&local_dir)?;
println!("Launching Reaper...");
let mut child = Command::new(&config.reaper.binary_path)
.arg(&rpp_path)
.spawn()
.map_err(|e| AppError::ReaperSpawnFailed(e.to_string()))?;
println!(
"Reaper launched (PID {}). Waiting for Reaper to exit...",
child
.id()
.map(|id| id.to_string())
.unwrap_or_else(|| "unknown".to_string())
);
let status = child
.wait()
.await
.map_err(|e| AppError::ReaperSpawnFailed(e.to_string()))?;
println!("Reaper exited ({}). Pushing changes...", status);
let push_result = sync_engine.push(project, &local_dir).await;
match push_result {
Ok(summary) => {
if let Err(e) = metadata_manager
.record_push(
project,
&config.identity.user,
(summary.files_uploaded + summary.files_skipped) as u32,
summary.total_bytes,
)
.await
{
eprintln!("Warning: failed to update project metadata: {}", e);
}
drop(lock_guard);
println!("Session complete. Lock released.");
Ok(())
}
Err(e) => {
std::mem::forget(lock_guard);
eprintln!(
"Reaper exited. Push failed: {}\n\n\
Your lock on {} is still held. Your local changes are safe.\n\
To retry: whirlwind push {}\n\
To give up: whirlwind unlock {}",
e, project, project, project
);
Err(e)
}
}
}