use std::path::Path;
use yo_resp::dispatch::{Loaded, Server};
#[derive(Debug)]
pub enum Trouble {
Unreadable(String),
Refused(String),
}
impl core::fmt::Display for Trouble {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Trouble::Unreadable(s) | Trouble::Refused(s) => f.write_str(s),
}
}
}
pub fn restore(path: &Path) -> Result<Loaded, Trouble> {
let image =
std::fs::read(path).map_err(|e| Trouble::Unreadable(format!("{}: {e}", path.display())))?;
let server = Server::new();
yo_alloc::allow(|| server.load_image(&image, true))
.map_err(|refused| Trouble::Refused(format!("{}: {refused}", path.display())))
}
pub fn report(done: &Loaded, out: &mut String) {
use core::fmt::Write as _;
let _ = writeln!(out, "RDB version {}", done.version);
for (db, &keys) in done.keys.iter().enumerate() {
if keys > 0 {
let _ = writeln!(out, "db{db}: {keys} key{}", plural(keys));
}
}
let total = done.total();
let _ = write!(out, "{total} key{} loaded", plural(total));
if done.expired > 0 {
let _ = write!(out, ", {} already expired and dropped", done.expired);
}
if done.libraries > 0 {
let _ = write!(
out,
", {} function librar{}",
done.libraries,
if done.libraries == 1 { "y" } else { "ies" }
);
}
out.push('\n');
}
fn plural(n: usize) -> &'static str {
if n == 1 { "" } else { "s" }
}
#[cfg(test)]
mod tests {
use super::{report, restore};
use yo_resp::dispatch::Loaded;
#[test]
fn a_file_that_is_not_there_says_which_one() {
let missing = std::env::temp_dir().join("yo-restore-nothing-here.rdb");
let _ = std::fs::remove_file(&missing);
let e = restore(&missing).expect_err("there is no file").to_string();
assert!(e.contains("yo-restore-nothing-here.rdb"), "{e}");
}
#[test]
fn a_file_that_is_not_an_rdb_says_so() {
let path = std::env::temp_dir().join(format!("yo-restore-junk-{}.rdb", std::process::id()));
std::fs::write(&path, b"this is not a Redis dump, not even a little bit").unwrap();
let e = restore(&path).expect_err("not an RDB").to_string();
let _ = std::fs::remove_file(&path);
assert!(e.contains("does not start with REDIS"), "{e}");
}
#[test]
fn the_report_names_every_database_that_got_a_key() {
let mut done = Loaded {
version: 12,
..Loaded::default()
};
done.keys[0] = 3;
done.keys[9] = 1;
let mut out = String::new();
report(&done, &mut out);
assert_eq!(
out,
"RDB version 12\ndb0: 3 keys\ndb9: 1 key\n4 keys loaded\n"
);
}
#[test]
fn the_report_says_what_the_file_carried_and_the_keyspace_did_not() {
let mut done = Loaded {
version: 11,
expired: 2,
libraries: 1,
..Loaded::default()
};
done.keys[0] = 1;
let mut out = String::new();
report(&done, &mut out);
assert_eq!(
out,
"RDB version 11\ndb0: 1 key\n1 key loaded, 2 already expired and dropped, 1 function library\n"
);
}
#[test]
fn a_file_with_nothing_in_it_reports_nothing_in_it() {
let done = Loaded {
version: 12,
..Loaded::default()
};
let mut out = String::new();
report(&done, &mut out);
assert_eq!(out, "RDB version 12\n0 keys loaded\n");
}
}