use super::config::S3Config;
use super::error::{Error, Result};
use super::s3_backend::S3Backend;
use flate2::read::GzDecoder;
use serde_json::Value;
use std::io::{Read, Write};
use std::path::Path;
pub struct Restore {
backend: S3Backend,
prefix: String,
}
#[derive(Debug)]
struct LatestPointer {
base_id: String,
last_offset: u64,
}
impl Restore {
pub async fn new(s3_config: &S3Config, prefix: &str) -> Result<Self> {
let backend = S3Backend::new(s3_config).await?;
Ok(Self {
backend,
prefix: prefix.to_string(),
})
}
pub async fn restore_to(&self, output_path: &Path, at_offset: Option<u64>) -> Result<()> {
let pointer = self.load_latest_pointer().await?;
let target_offset = at_offset.unwrap_or(pointer.last_offset);
eprintln!(
"Restoring from base: {}, target offset: {}",
pointer.base_id, target_offset
);
let base_key = format!("{}/base/{}.snap", self.prefix, pointer.base_id);
let base_key_gz = format!("{}.gz", base_key);
let base_data = if self
.backend
.list_objects(&base_key_gz)
.await?
.contains(&base_key_gz)
{
self.download_and_decompress(&base_key_gz).await?
} else {
self.backend.download_data(&base_key).await?
};
let mut output_file = std::fs::File::create(output_path)?;
output_file.write_all(&base_data)?;
if target_offset > base_data.len() as u64 {
self.apply_segments(
&mut output_file,
&pointer.base_id,
base_data.len() as u64,
target_offset,
)
.await?;
}
output_file.sync_all()?;
eprintln!("Restore complete: {}", output_path.display());
Ok(())
}
async fn load_latest_pointer(&self) -> Result<LatestPointer> {
let key = format!("{}/latest.json", self.prefix);
let data = self.backend.download_data(&key).await?;
let json: Value = serde_json::from_slice(&data)
.map_err(|e| Error::Restore(format!("Failed to parse latest.json: {}", e)))?;
Ok(LatestPointer {
base_id: json["base_id"]
.as_str()
.ok_or_else(|| Error::Restore("Missing base_id in latest.json".to_string()))?
.to_string(),
last_offset: json["last_offset"]
.as_u64()
.ok_or_else(|| Error::Restore("Missing last_offset in latest.json".to_string()))?,
})
}
async fn download_and_decompress(&self, key: &str) -> Result<Vec<u8>> {
let compressed = self.backend.download_data(key).await?;
let mut decoder = GzDecoder::new(&compressed[..]);
let mut decompressed = Vec::new();
decoder.read_to_end(&mut decompressed)?;
Ok(decompressed)
}
async fn apply_segments(
&self,
output_file: &mut std::fs::File,
base_id: &str,
start_offset: u64,
target_offset: u64,
) -> Result<()> {
let segment_prefix = format!("{}/segments/{}/", self.prefix, base_id);
let mut segment_keys = self.backend.list_objects(&segment_prefix).await?;
segment_keys.retain(|k| {
if let Some(stripped) = k.strip_suffix(".gz") {
stripped.starts_with(&segment_prefix) && stripped.ends_with(".seg")
} else {
k.starts_with(&segment_prefix) && k.ends_with(".seg")
}
});
let mut segments: Vec<(u64, u64, String)> = segment_keys
.iter()
.filter_map(|k| {
let name = k.strip_suffix(".gz").unwrap_or(k);
let name = name.strip_prefix(&segment_prefix)?;
let name = name.strip_suffix(".seg")?;
let parts: Vec<&str> = name.split('-').collect();
if parts.len() == 2 {
let start = parts[0].parse::<u64>().ok()?;
let end = parts[1].parse::<u64>().ok()?;
if start >= start_offset && end <= target_offset {
Some((start, end, k.clone()))
} else {
None
}
} else {
None
}
})
.collect();
segments.sort_by_key(|(start, _, _)| *start);
let mut current_offset = start_offset;
for (seg_start, seg_end, seg_key) in segments {
if seg_start != current_offset {
return Err(Error::Restore(format!(
"Segment gap: expected offset {}, got {}",
current_offset, seg_start
)));
}
eprintln!(
"Applying segment: {} (offset {} to {})",
seg_key, seg_start, seg_end
);
let segment_data = if seg_key.ends_with(".gz") {
self.download_and_decompress(&seg_key).await?
} else {
self.backend.download_data(&seg_key).await?
};
if segment_data.len() as u64 != (seg_end - seg_start) {
return Err(Error::Restore(format!(
"Segment size mismatch: expected {}, got {}",
seg_end - seg_start,
segment_data.len()
)));
}
output_file.write_all(&segment_data)?;
current_offset = seg_end;
if current_offset >= target_offset {
break;
}
}
if current_offset < target_offset {
eprintln!(
"Warning: Restored to offset {}, but target was {}",
current_offset, target_offset
);
}
Ok(())
}
pub async fn list(&self) -> Result<()> {
let base_prefix = format!("{}/base/", self.prefix);
let bases = self.backend.list_objects(&base_prefix).await?;
println!("Base snapshots:");
for base in bases {
if base.ends_with(".snap") || base.ends_with(".snap.gz") {
println!(" {}", base);
}
}
let segment_prefix = format!("{}/segments/", self.prefix);
let segments = self.backend.list_objects(&segment_prefix).await?;
println!("\nSegments: {}", segments.len());
for seg in segments.iter().take(10) {
println!(" {}", seg);
}
if segments.len() > 10 {
println!(" ... ({} more)", segments.len() - 10);
}
Ok(())
}
}