mod manifest;
mod restore;
#[cfg(feature = "s3")]
pub mod s3;
mod ship;
mod target;
#[cfg(test)]
mod tests;
pub use manifest::{Plan, Point, State, Timeline};
pub use restore::{RestoreOptions, Restored, Verified, load_state, restore, verify};
pub(crate) use ship::{Inbox, Shipper};
pub use target::{LocalTarget, Target, open_target};
use std::io;
use std::path::Path;
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
#[derive(Clone, Debug)]
pub struct BackupOptions {
pub upload_interval: Option<Duration>,
pub resolution: Duration,
pub retention: Duration,
}
impl Default for BackupOptions {
fn default() -> Self {
Self {
upload_interval: Some(Duration::from_secs(1)),
resolution: Duration::from_secs(1),
retention: Duration::from_secs(7 * 24 * 3600),
}
}
}
#[derive(Clone)]
pub struct BackupConfig {
pub target: Arc<dyn Target>,
pub options: BackupOptions,
running: Arc<Mutex<Weak<Shipper>>>,
}
impl std::fmt::Debug for BackupConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BackupConfig")
.field("target", &self.target.describe())
.field("options", &self.options)
.finish()
}
}
impl BackupConfig {
pub fn new(target: Arc<dyn Target>, options: BackupOptions) -> Self {
Self {
target,
options,
running: Arc::new(Mutex::new(Weak::new())),
}
}
pub(crate) fn for_engine(&self) -> Self {
Self::new(self.target.clone(), self.options.clone())
}
pub fn handle(&self) -> Backup {
Backup {
shipper: self
.running
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RestorePoint {
pub timeline: String,
pub time_ms: u64,
pub seq: u64,
}
#[derive(Clone, Debug, Default)]
pub struct BackupStatus {
pub timeline: String,
pub restore_point: Option<RestorePoint>,
pub uploaded_seq: u64,
pub acked_seq: u64,
pub objects_uploaded: u64,
pub bytes_uploaded: u64,
pub manifest_writes: u64,
pub objects_deleted: u64,
pub last_error: Option<String>,
}
#[derive(Clone)]
pub struct Backup {
shipper: Weak<Shipper>,
}
impl Backup {
pub async fn sync(&self) -> io::Result<RestorePoint> {
let rx = self
.shipper
.upgrade()
.ok_or_else(|| io::Error::other("the backup has stopped"))?
.request();
rx.await
.map_err(|_| io::Error::other("the backup has stopped"))?
}
pub fn status(&self) -> Option<BackupStatus> {
Some(self.shipper.upgrade()?.status())
}
pub async fn stopped(&self) {
while self.shipper.strong_count() > 0 {
tokio::time::sleep(Duration::from_millis(2)).await;
}
}
}
pub(crate) fn attach(
wal: &Arc<crate::wal::SharedWal>,
dir: &Path,
shards: u32,
config: &BackupConfig,
) -> crate::Result<()> {
wal.set_backup_needs(Some(0))?;
let shipper = Shipper::start(
wal,
dir,
shards,
config.target.clone(),
config.options.clone(),
)
.map_err(|e| crate::Error::Io(format!("backup to {}: {e}", config.target.describe())))?;
*config.running.lock().unwrap_or_else(|e| e.into_inner()) = Arc::downgrade(&shipper);
Ok(())
}
pub fn parse_time(text: &str, now_ms: u64) -> Result<u64, String> {
let t = text.trim();
if let Some(rel) = t.strip_prefix('-') {
let (num, unit) =
rel.split_at(rel.find(|c: char| !c.is_ascii_digit()).unwrap_or(rel.len()));
let n: u64 = num.parse().map_err(|_| format!("bad duration {text:?}"))?;
let ms = match unit {
"ms" => 1,
"s" | "" => 1000,
"m" => 60_000,
"h" => 3_600_000,
"d" => 86_400_000,
_ => return Err(format!("bad duration unit in {text:?} (ms, s, m, h, d)")),
};
return Ok(now_ms.saturating_sub(n.saturating_mul(ms)));
}
if !t.is_empty() && t.bytes().all(|b| b.is_ascii_digit()) {
return t.parse().map_err(|_| format!("bad time {text:?}"));
}
chrono::DateTime::parse_from_rfc3339(t)
.map(|d| d.timestamp_millis().max(0) as u64)
.map_err(|e| format!("bad time {text:?}: {e} (use RFC 3339, Unix ms, or -15m)"))
}
pub fn format_time(ms: u64) -> String {
chrono::DateTime::from_timestamp_millis(ms as i64)
.map(|d| d.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string())
.unwrap_or_else(|| ms.to_string())
}