use super::{checksum, decode_hex, encode_hex, validate_name, BackupError, ObjectStore};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PitrRecord {
pub sequence: u64,
pub key: String,
pub value: Option<Vec<u8>>,
}
impl PitrRecord {
pub fn put(sequence: u64, key: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
Self {
sequence,
key: key.into(),
value: Some(value.into()),
}
}
pub fn delete(sequence: u64, key: impl Into<String>) -> Self {
Self {
sequence,
key: key.into(),
value: None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PitrLog {
pub records: Vec<PitrRecord>,
}
impl PitrLog {
pub fn new() -> Self {
Self::default()
}
pub fn push(mut self, record: PitrRecord) -> Self {
self.records.push(record);
self
}
pub fn encode(&self) -> Result<Vec<u8>, BackupError> {
let mut previous = 0;
let mut text = String::from("hydracache-pitr\t1\n");
for record in &self.records {
if record.sequence <= previous {
return Err(BackupError::InvalidManifest(
"pitr sequence must be strictly increasing".to_owned(),
));
}
validate_name(&record.key, "pitr key")?;
previous = record.sequence;
match &record.value {
Some(value) => text.push_str(&format!(
"put\t{}\t{}\t{}\t{}\n",
record.sequence,
encode_hex(record.key.as_bytes()),
checksum(&[value]),
encode_hex(value)
)),
None => text.push_str(&format!(
"delete\t{}\t{}\t0\t\n",
record.sequence,
encode_hex(record.key.as_bytes())
)),
}
}
Ok(text.into_bytes())
}
pub fn decode(bytes: &[u8]) -> Result<Self, BackupError> {
let text = std::str::from_utf8(bytes)
.map_err(|_| BackupError::InvalidManifest("pitr log is not utf-8".to_owned()))?;
let mut lines = text.lines();
if lines.next() != Some("hydracache-pitr\t1") {
return Err(BackupError::InvalidManifest(
"invalid pitr header".to_owned(),
));
}
let mut records = Vec::new();
let mut previous = 0;
for line in lines {
let parts: Vec<_> = line.split('\t').collect();
if parts.len() != 5 {
return Err(BackupError::InvalidManifest(
"invalid pitr entry".to_owned(),
));
}
let sequence = parts[1]
.parse::<u64>()
.map_err(|_| BackupError::InvalidManifest("invalid pitr sequence".to_owned()))?;
if sequence <= previous {
return Err(BackupError::InvalidManifest(
"pitr sequence must be strictly increasing".to_owned(),
));
}
previous = sequence;
let key = String::from_utf8(decode_hex(parts[2])?)
.map_err(|_| BackupError::InvalidManifest("invalid pitr key".to_owned()))?;
let checksum_value = parts[3]
.parse::<u64>()
.map_err(|_| BackupError::InvalidManifest("invalid pitr checksum".to_owned()))?;
let value = match parts[0] {
"put" => {
let value = decode_hex(parts[4])?;
if checksum(&[&value]) != checksum_value {
return Err(BackupError::CorruptObject(format!("pitr/{sequence}")));
}
Some(value)
}
"delete" if checksum_value == 0 && parts[4].is_empty() => None,
_ => return Err(BackupError::InvalidManifest("invalid pitr op".to_owned())),
};
records.push(PitrRecord {
sequence,
key,
value,
});
}
Ok(Self { records })
}
}
pub fn write_pitr_log<S>(
store: &mut S,
backup_id: &str,
log: &PitrLog,
) -> Result<String, BackupError>
where
S: ObjectStore,
{
validate_name(backup_id, "backup id")?;
let key = format!("backups/{backup_id}/pitr-log");
store.put(&key, log.encode()?)?;
Ok(key)
}