1use std::fs::{self, File, OpenOptions};
10use std::future::Future;
11use std::io::{self, Read, Seek, SeekFrom, Write};
12use std::path::{Path, PathBuf};
13use std::pin::Pin;
14use std::sync::Arc;
15
16use corium_log::{
17 FileLog, LogError, TransactionLog, TxRecord, append_framed_record, decode_framed_records,
18};
19use corium_protocol::pb;
20use corium_store::{
21 BlobId, BlobStore, DbRoot, FORMAT_VERSION, FsStore, RootStore, StoreError, db_root_name,
22};
23use thiserror::Error;
24
25use crate::{LogBackend, NodeStore, StorageConnectionError, StoreSpec};
26
27pub const BACKUP_FORMAT_VERSION: u32 = 1;
29
30const BACKUP_MAGIC: &[u8; 16] = b"CORIUM_BACKUP\0\0\0";
31const BLOB_TAG: [u8; 4] = *b"BLOB";
32const CHECKPOINT_TAG: [u8; 4] = *b"CKPT";
33const CHECKPOINT_END: &[u8; 4] = b"DONE";
34const WRITER_VERSION: &str = env!("CARGO_PKG_VERSION");
35
36#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct BackupReport {
39 pub backup_format_version: u32,
41 pub writer_version: String,
43 pub basis_t: u64,
45 pub index_basis_t: u64,
47 pub copied_blobs: usize,
49 pub reused_blobs: usize,
51 pub replayed_transactions: usize,
53}
54
55#[derive(Clone, Debug, Eq, PartialEq)]
57pub struct RestoreReport {
58 pub backup_format_version: u32,
60 pub writer_version: String,
62 pub source_db: String,
64 pub target_db: String,
66 pub basis_t: u64,
68 pub copied_blobs: usize,
70 pub reused_blobs: usize,
72}
73
74#[derive(Debug, Error)]
76pub enum BackupError {
77 #[error("backup I/O failed: {0}")]
79 Io(#[from] io::Error),
80 #[error(transparent)]
82 Store(#[from] StoreError),
83 #[error(transparent)]
85 Log(#[from] LogError),
86 #[error("database {0:?} does not exist")]
88 MissingDatabase(String),
89 #[error("invalid backup: {0}")]
91 Invalid(String),
92 #[error("database {0:?} already exists in the target")]
94 TargetExists(String),
95 #[error("storage format {found} is newer than supported format {supported}")]
97 UnsupportedFormat {
98 found: u32,
100 supported: u32,
102 },
103 #[error(
105 "backup file format {found} (created by Corium {writer}) is newer than supported format {supported}"
106 )]
107 UnsupportedBackupFormat {
108 found: u32,
110 supported: u32,
112 writer: String,
114 },
115 #[error("storage backend cannot be backed up independently: {0}")]
117 UnsupportedSource(String),
118}
119
120#[derive(Clone, Debug)]
123pub struct BackupSource {
124 store: StoreSpec,
125 data_dir: PathBuf,
126 basis_t: u64,
127}
128
129impl BackupSource {
130 pub fn from_info(info: pb::GetStorageInfoResponse) -> Result<Self, BackupError> {
136 let storage = info
137 .storage
138 .ok_or_else(|| BackupError::Invalid("transactor returned no storage backend".into()))?;
139 let (store, data_dir) =
140 StoreSpec::from_connection(storage).map_err(|error| match error {
141 StorageConnectionError::Missing => {
142 BackupError::Invalid("transactor returned no storage backend".into())
143 }
144 StorageConnectionError::Unsupported(detail) => {
145 BackupError::UnsupportedSource(detail)
146 }
147 })?;
148 Ok(Self {
149 store,
150 data_dir,
151 basis_t: info.basis_t,
152 })
153 }
154
155 #[must_use]
157 pub const fn basis_t(&self) -> u64 {
158 self.basis_t
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq)]
163struct ArchiveHeader {
164 creator_version: String,
165 source_db: String,
166 index_basis_t: u64,
167 storage_format_version: u32,
168 root: Vec<u8>,
169}
170
171#[derive(Debug)]
172struct Archive {
173 header: ArchiveHeader,
174 writer_version: String,
175 basis_t: u64,
176 metadata: Vec<u8>,
177 records: Vec<TxRecord>,
178 blob_offsets: Vec<(u64, u64)>,
179 durable_len: u64,
180}
181
182fn valid_db_name(name: &str) -> bool {
183 !name.is_empty()
184 && name.len() <= 128
185 && name
186 .bytes()
187 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
188}
189
190fn meta_root_name(db: &str) -> String {
191 format!("meta:{db}")
192}
193
194fn log_path(data_dir: &Path, db: &str) -> PathBuf {
195 data_dir.join("logs").join(format!("{db}.log"))
196}
197
198fn invalid(detail: impl Into<String>) -> BackupError {
199 BackupError::Invalid(detail.into())
200}
201
202fn write_u32(writer: &mut impl Write, value: u32) -> Result<(), BackupError> {
203 writer.write_all(&value.to_be_bytes())?;
204 Ok(())
205}
206
207fn write_u64(writer: &mut impl Write, value: u64) -> Result<(), BackupError> {
208 writer.write_all(&value.to_be_bytes())?;
209 Ok(())
210}
211
212fn read_u32(reader: &mut impl Read) -> Result<u32, BackupError> {
213 let mut bytes = [0; 4];
214 reader.read_exact(&mut bytes)?;
215 Ok(u32::from_be_bytes(bytes))
216}
217
218fn read_u64(reader: &mut impl Read) -> Result<u64, BackupError> {
219 let mut bytes = [0; 8];
220 reader.read_exact(&mut bytes)?;
221 Ok(u64::from_be_bytes(bytes))
222}
223
224fn write_bytes(writer: &mut impl Write, bytes: &[u8]) -> Result<(), BackupError> {
225 write_u64(
226 writer,
227 u64::try_from(bytes.len()).map_err(|_| invalid("backup field is too large"))?,
228 )?;
229 writer.write_all(bytes)?;
230 Ok(())
231}
232
233fn read_bytes(reader: &mut impl Read, field: &str) -> Result<Vec<u8>, BackupError> {
234 let len = usize::try_from(read_u64(reader)?)
235 .map_err(|_| invalid(format!("{field} is too large for this platform")))?;
236 let mut bytes = vec![0; len];
237 reader.read_exact(&mut bytes)?;
238 Ok(bytes)
239}
240
241fn read_string(reader: &mut impl Read, field: &str) -> Result<String, BackupError> {
242 String::from_utf8(read_bytes(reader, field)?)
243 .map_err(|_| invalid(format!("{field} is not UTF-8")))
244}
245
246fn write_frame(writer: &mut impl Write, tag: [u8; 4], payload: &[u8]) -> Result<(), BackupError> {
247 writer.write_all(&tag)?;
248 write_u64(
249 writer,
250 u64::try_from(payload.len()).map_err(|_| invalid("backup frame is too large"))?,
251 )?;
252 writer.write_all(payload)?;
253 Ok(())
254}
255
256fn write_header(writer: &mut impl Write, header: &ArchiveHeader) -> Result<(), BackupError> {
257 writer.write_all(BACKUP_MAGIC)?;
258 write_u32(writer, BACKUP_FORMAT_VERSION)?;
259 write_bytes(writer, header.creator_version.as_bytes())?;
260 write_bytes(writer, header.source_db.as_bytes())?;
261 write_u32(writer, header.storage_format_version)?;
262 write_u64(writer, header.index_basis_t)?;
263 write_bytes(writer, &header.root)
264}
265
266fn encode_checkpoint(
267 basis_t: u64,
268 previous_basis_t: u64,
269 metadata: &[u8],
270 records: &[TxRecord],
271) -> Result<Vec<u8>, BackupError> {
272 let mut framed = Vec::new();
273 for record in records {
274 append_framed_record(&mut framed, record)?;
275 }
276 let mut payload = Vec::new();
277 write_bytes(&mut payload, WRITER_VERSION.as_bytes())?;
278 write_u64(&mut payload, basis_t)?;
279 write_u64(
280 &mut payload,
281 if records.is_empty() {
282 0
283 } else {
284 previous_basis_t.saturating_add(1)
285 },
286 )?;
287 write_u64(
288 &mut payload,
289 u64::try_from(records.len()).map_err(|_| invalid("too many transaction records"))?,
290 )?;
291 write_bytes(&mut payload, metadata)?;
292 write_bytes(&mut payload, &framed)?;
293 payload.extend_from_slice(CHECKPOINT_END);
294 Ok(payload)
295}
296
297fn read_frame_bytes(file: &mut File, frame_end: u64, field: &str) -> Result<Vec<u8>, BackupError> {
298 let len = read_u64(file)?;
299 let start = file.stream_position()?;
300 let end = start
301 .checked_add(len)
302 .ok_or_else(|| invalid(format!("{field} length overflowed")))?;
303 if end > frame_end {
304 return Err(invalid(format!("{field} extends past its checkpoint")));
305 }
306 let len = usize::try_from(len)
307 .map_err(|_| invalid(format!("{field} is too large for this platform")))?;
308 let mut bytes = vec![0; len];
309 file.read_exact(&mut bytes)?;
310 Ok(bytes)
311}
312
313fn decode_checkpoint(
314 file: &mut File,
315 frame_end: u64,
316 previous_basis_t: u64,
317 include_records: bool,
318) -> Result<(String, u64, Vec<u8>, Vec<TxRecord>), BackupError> {
319 let writer_version = String::from_utf8(read_frame_bytes(
320 file,
321 frame_end,
322 "checkpoint writer version",
323 )?)
324 .map_err(|_| invalid("checkpoint writer version is not UTF-8"))?;
325 let basis_t = read_u64(file)?;
326 let start_t = read_u64(file)?;
327 let record_count = read_u64(file)?;
328 let metadata = read_frame_bytes(file, frame_end, "checkpoint metadata")?;
329 let framed_len = read_u64(file)?;
330 let framed_start = file.stream_position()?;
331 let records_end = framed_start
332 .checked_add(framed_len)
333 .ok_or_else(|| invalid("checkpoint transaction-record length overflowed"))?;
334 if records_end
335 .checked_add(4)
336 .is_none_or(|checkpoint_end| checkpoint_end != frame_end)
337 {
338 return Err(invalid(
339 "checkpoint transaction records do not fill their frame",
340 ));
341 }
342 let framed = if include_records {
343 let len = usize::try_from(framed_len)
344 .map_err(|_| invalid("transaction records are too large for this platform"))?;
345 let mut bytes = vec![0; len];
346 file.read_exact(&mut bytes)?;
347 bytes
348 } else {
349 file.seek(SeekFrom::Start(records_end))?;
350 Vec::new()
351 };
352 let mut end = [0; 4];
353 file.read_exact(&mut end)?;
354 if &end != CHECKPOINT_END || file.stream_position()? != frame_end {
355 return Err(invalid("checkpoint has invalid trailing bytes"));
356 }
357 let expected_count = basis_t
358 .checked_sub(previous_basis_t)
359 .ok_or_else(|| invalid("checkpoint basis regressed"))?;
360 let expected_start = if expected_count == 0 {
361 0
362 } else {
363 previous_basis_t
364 .checked_add(1)
365 .ok_or_else(|| invalid("checkpoint basis overflowed"))?
366 };
367 if record_count != expected_count || start_t != expected_start {
368 return Err(invalid("checkpoint transaction range is not contiguous"));
369 }
370 let records = if include_records {
371 let records = decode_framed_records(&framed)?;
372 if records.len() as u64 != record_count {
373 return Err(invalid(
374 "checkpoint record count does not match its payload",
375 ));
376 }
377 if record_count > 0 {
378 validate_records(&records, start_t, basis_t)?;
379 }
380 records
381 } else {
382 Vec::new()
383 };
384 Ok((writer_version, basis_t, metadata, records))
385}
386
387fn read_header(file: &mut File) -> Result<ArchiveHeader, BackupError> {
388 let mut magic = [0; 16];
389 file.read_exact(&mut magic)?;
390 if &magic != BACKUP_MAGIC {
391 return Err(invalid("unknown backup file magic"));
392 }
393 let format_version = read_u32(file)?;
394 let creator_version = read_string(file, "archive creator version")?;
395 if format_version > BACKUP_FORMAT_VERSION {
396 return Err(BackupError::UnsupportedBackupFormat {
397 found: format_version,
398 supported: BACKUP_FORMAT_VERSION,
399 writer: creator_version,
400 });
401 }
402 if format_version != BACKUP_FORMAT_VERSION {
403 return Err(invalid(format!(
404 "unsupported backup file format {format_version}"
405 )));
406 }
407 let source_db = read_string(file, "source database name")?;
408 let storage_format_version = read_u32(file)?;
409 if storage_format_version > FORMAT_VERSION {
410 return Err(BackupError::UnsupportedFormat {
411 found: storage_format_version,
412 supported: FORMAT_VERSION,
413 });
414 }
415 Ok(ArchiveHeader {
416 creator_version,
417 source_db,
418 storage_format_version,
419 index_basis_t: read_u64(file)?,
420 root: read_bytes(file, "database root")?,
421 })
422}
423
424fn read_archive(path: &Path, include_records: bool) -> Result<Archive, BackupError> {
425 if !path.is_file() {
426 return Err(invalid(format!(
427 "{} is not a binary backup file",
428 path.display()
429 )));
430 }
431 let mut file = File::open(path)?;
432 let file_len = file.metadata()?.len();
433 let header = read_header(&mut file)?;
434 let mut writer_version = header.creator_version.clone();
435 let mut basis_t = 0;
436 let mut metadata = Vec::new();
437 let mut records = Vec::new();
438 let mut blob_offsets = Vec::new();
439 let mut durable_len = file.stream_position()?;
440 let mut saw_checkpoint = false;
441 loop {
442 let frame_start = file.stream_position()?;
443 let mut tag = [0; 4];
444 match file.read_exact(&mut tag) {
445 Ok(()) => {}
446 Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break,
447 Err(error) => return Err(error.into()),
448 }
449 let frame_len = match read_u64(&mut file) {
450 Ok(len) => len,
451 Err(BackupError::Io(error)) if error.kind() == io::ErrorKind::UnexpectedEof => break,
452 Err(error) => return Err(error),
453 };
454 let payload_offset = file.stream_position()?;
455 let frame_end = payload_offset
456 .checked_add(frame_len)
457 .ok_or_else(|| invalid("backup frame length overflowed"))?;
458 if frame_end > file_len {
459 break;
460 }
461 if tag == BLOB_TAG {
462 if saw_checkpoint {
463 return Err(invalid("blob frame appears after a checkpoint"));
464 }
465 blob_offsets.push((payload_offset, frame_len));
466 file.seek(SeekFrom::Start(frame_end))?;
467 } else if tag == CHECKPOINT_TAG {
468 saw_checkpoint = true;
469 let (checkpoint_writer, checkpoint_basis, checkpoint_meta, checkpoint_records) =
470 decode_checkpoint(&mut file, frame_end, basis_t, include_records)?;
471 writer_version = checkpoint_writer;
472 basis_t = checkpoint_basis;
473 metadata = checkpoint_meta;
474 records.extend(checkpoint_records);
475 } else {
476 return Err(invalid(format!(
477 "unknown backup frame tag at offset {frame_start}"
478 )));
479 }
480 durable_len = frame_end;
481 }
482 if !saw_checkpoint {
483 return Err(invalid("backup contains no complete checkpoint"));
484 }
485 Ok(Archive {
486 header,
487 writer_version,
488 basis_t,
489 metadata,
490 records,
491 blob_offsets,
492 durable_len,
493 })
494}
495
496fn write_blob_tree<'a>(
499 source: &'a dyn BlobStore,
500 id: &'a BlobId,
501 seen: &'a mut std::collections::HashSet<BlobId>,
502 writer: &'a mut File,
503 copied: &'a mut usize,
504) -> Pin<Box<dyn Future<Output = Result<(), BackupError>> + Send + 'a>> {
505 Box::pin(async move {
506 if !seen.insert(id.clone()) {
507 return Ok(());
508 }
509 let bytes = source
510 .get(id)
511 .await?
512 .ok_or_else(|| BackupError::Invalid(format!("root references missing blob {id}")))?;
513 for child in corium_store::index_blob_children(&bytes)? {
514 write_blob_tree(source, &child, seen, writer, copied).await?;
515 }
516 write_frame(writer, BLOB_TAG, &bytes)?;
517 *copied += 1;
518 Ok(())
519 })
520}
521
522fn validate_blob_tree<'a>(
523 store: &'a dyn BlobStore,
524 id: &'a BlobId,
525 seen: &'a mut std::collections::HashSet<BlobId>,
526) -> Pin<Box<dyn Future<Output = Result<(), BackupError>> + Send + 'a>> {
527 Box::pin(async move {
528 if !seen.insert(id.clone()) {
529 return Ok(());
530 }
531 let bytes = store
532 .get(id)
533 .await?
534 .ok_or_else(|| invalid(format!("archive is missing reachable blob {id}")))?;
535 for child in corium_store::index_blob_children(&bytes)? {
536 validate_blob_tree(store, &child, seen).await?;
537 }
538 Ok(())
539 })
540}
541
542fn write_log_atomically(records: &[TxRecord], target: &Path) -> Result<(), BackupError> {
543 if let Some(parent) = target.parent() {
544 fs::create_dir_all(parent)?;
545 }
546 let temporary = target.with_extension("tmp");
547 match fs::remove_file(&temporary) {
548 Ok(()) => {}
549 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
550 Err(error) => return Err(error.into()),
551 }
552 let log = FileLog::open(&temporary)?;
553 for record in records {
554 log.append(record)?;
555 }
556 fs::rename(temporary, target)?;
557 Ok(())
558}
559
560fn bare_root() -> DbRoot {
561 DbRoot {
562 format_version: FORMAT_VERSION,
563 lease_version: 0,
564 owner: String::new(),
565 lease_expires_unix_ms: 0,
566 owner_endpoint: String::new(),
567 index_basis_t: 0,
568 roots: None,
569 next_entity_id: 0,
570 last_tx_instant: i64::MIN,
571 }
572}
573
574fn sanitize_root(mut root: DbRoot) -> DbRoot {
575 root.lease_version = 0;
576 root.owner.clear();
577 root.lease_expires_unix_ms = 0;
578 root.owner_endpoint.clear();
579 root
580}
581
582fn validate_records(
583 records: &[corium_log::TxRecord],
584 start: u64,
585 end_inclusive: u64,
586) -> Result<(), BackupError> {
587 if start > end_inclusive {
588 return Ok(());
589 }
590 let expected = end_inclusive - start + 1;
591 if records.len() as u64 != expected
592 || records.first().map(|record| record.t) != Some(start)
593 || records.last().map(|record| record.t) != Some(end_inclusive)
594 || records.windows(2).any(|pair| pair[1].t != pair[0].t + 1)
595 {
596 return Err(BackupError::Invalid(format!(
597 "source log does not contain a contiguous range {start}..={end_inclusive}"
598 )));
599 }
600 Ok(())
601}
602
603fn append_archive_checkpoint(
604 destination: &Path,
605 archive: &Archive,
606 checkpoint: &[u8],
607 changed: bool,
608) -> Result<String, BackupError> {
609 if !changed {
610 return Ok(archive.writer_version.clone());
611 }
612 let mut file = OpenOptions::new().write(true).open(destination)?;
613 file.set_len(archive.durable_len)?;
614 file.seek(SeekFrom::Start(archive.durable_len))?;
615 write_frame(&mut file, CHECKPOINT_TAG, checkpoint)?;
616 file.sync_all()?;
617 Ok(WRITER_VERSION.to_owned())
618}
619
620async fn create_archive(
621 destination: &Path,
622 db: &str,
623 root: &DbRoot,
624 source_store: &dyn BlobStore,
625 checkpoint: &[u8],
626) -> Result<usize, BackupError> {
627 if let Some(parent) = destination.parent()
628 && !parent.as_os_str().is_empty()
629 {
630 fs::create_dir_all(parent)?;
631 }
632 let mut temporary = destination.as_os_str().to_os_string();
633 temporary.push(".tmp");
634 let temporary = PathBuf::from(temporary);
635 match fs::remove_file(&temporary) {
636 Ok(()) => {}
637 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
638 Err(error) => return Err(error.into()),
639 }
640 let mut file = File::create(&temporary)?;
641 write_header(
642 &mut file,
643 &ArchiveHeader {
644 creator_version: WRITER_VERSION.to_owned(),
645 source_db: db.to_owned(),
646 index_basis_t: root.index_basis_t,
647 storage_format_version: root.format_version,
648 root: root.encode(),
649 },
650 )?;
651 let mut seen = std::collections::HashSet::new();
652 let mut copied = 0;
653 for id in root.roots.iter().flatten() {
654 write_blob_tree(source_store, id, &mut seen, &mut file, &mut copied).await?;
655 }
656 write_frame(&mut file, CHECKPOINT_TAG, checkpoint)?;
657 file.sync_all()?;
658 fs::rename(temporary, destination)?;
659 Ok(copied)
660}
661
662pub async fn backup(
675 source: &BackupSource,
676 db: &str,
677 destination: impl AsRef<Path>,
678) -> Result<BackupReport, BackupError> {
679 if !valid_db_name(db) {
680 return Err(BackupError::MissingDatabase(db.to_owned()));
681 }
682 let destination = destination.as_ref();
683 let source_store = Arc::new(NodeStore::open_existing(&source.store, &source.data_dir).await?);
684 let source_logs =
685 LogBackend::for_spec(&source.store, &source.data_dir, Arc::clone(&source_store));
686 let db_name = db_root_name(db);
687 let meta_name = meta_root_name(db);
688 let source_db_bytes = source_store
689 .get_root(&db_name)
690 .await?
691 .ok_or_else(|| BackupError::MissingDatabase(db.to_owned()))?;
692 let meta = source_store
693 .get_root(&meta_name)
694 .await?
695 .ok_or_else(|| BackupError::MissingDatabase(db.to_owned()))?;
696 let source_root = DbRoot::decode(&source_db_bytes)
697 .ok_or_else(|| BackupError::Invalid("database root cannot be decoded".into()))?;
698 if source_root.format_version > FORMAT_VERSION {
699 return Err(BackupError::UnsupportedFormat {
700 found: source_root.format_version,
701 supported: FORMAT_VERSION,
702 });
703 }
704 let previous = destination
705 .exists()
706 .then(|| read_archive(destination, false))
707 .transpose()?;
708 if let Some(archive) = &previous {
709 if archive.header.source_db != db {
710 return Err(invalid(format!(
711 "backup belongs to database {:?}, not {db:?}",
712 archive.header.source_db
713 )));
714 }
715 if archive.basis_t > source.basis_t {
716 return Err(invalid(format!(
717 "backup basis {} is ahead of source checkpoint {}",
718 archive.basis_t, source.basis_t
719 )));
720 }
721 }
722 let existing_basis = previous.as_ref().map_or(0, |archive| archive.basis_t);
723 let start = existing_basis.saturating_add(1);
724 let records = if start <= source.basis_t {
725 let end = source
726 .basis_t
727 .checked_add(1)
728 .ok_or_else(|| BackupError::Invalid("source basis is too large".into()))?;
729 let log = source_logs.open_read_only(db).await?;
730 log.tx_range_async(start, Some(end)).await?
731 } else {
732 Vec::new()
733 };
734 validate_records(&records, start, source.basis_t)?;
735
736 let root = if let Some(archive) = &previous {
741 let root = DbRoot::decode(&archive.header.root)
742 .ok_or_else(|| invalid("archive database root cannot be decoded"))?;
743 if root.index_basis_t != archive.header.index_basis_t
744 || root.format_version != archive.header.storage_format_version
745 {
746 return Err(invalid("archive header does not match its database root"));
747 }
748 sanitize_root(root)
749 } else if source_root.index_basis_t <= source.basis_t {
750 sanitize_root(source_root)
751 } else {
752 bare_root()
753 };
754 let checkpoint = encode_checkpoint(source.basis_t, existing_basis, &meta, &records)?;
755 let (copied_blobs, writer_version) = if let Some(archive) = &previous {
756 (
757 0,
758 append_archive_checkpoint(destination, archive, &checkpoint, !records.is_empty())?,
759 )
760 } else {
761 (
762 create_archive(destination, db, &root, source_store.as_ref(), &checkpoint).await?,
763 WRITER_VERSION.to_owned(),
764 )
765 };
766 Ok(BackupReport {
767 backup_format_version: BACKUP_FORMAT_VERSION,
768 writer_version,
769 basis_t: source.basis_t,
770 index_basis_t: root.index_basis_t,
771 copied_blobs,
772 reused_blobs: 0,
773 replayed_transactions: records.len(),
774 })
775}
776
777pub async fn restore(
787 source: impl AsRef<Path>,
788 target_data_dir: impl AsRef<Path>,
789 target_db: &str,
790) -> Result<RestoreReport, BackupError> {
791 if !valid_db_name(target_db) {
792 return Err(BackupError::Invalid(format!(
793 "invalid target database name {target_db:?}"
794 )));
795 }
796 let source = source.as_ref();
797 let target_data_dir = target_data_dir.as_ref();
798 let archive = read_archive(source, true)?;
799 let root = DbRoot::decode(&archive.header.root)
800 .ok_or_else(|| invalid("archive database root cannot be decoded"))?;
801 if root.index_basis_t != archive.header.index_basis_t
802 || root.format_version != archive.header.storage_format_version
803 {
804 return Err(invalid("archive header does not match its database root"));
805 }
806
807 let target_store = FsStore::open(target_data_dir.join("store"))?;
808 let target_db_name = db_root_name(target_db);
809 let target_meta_name = meta_root_name(target_db);
810 if target_store.get_root(&target_db_name).await?.is_some()
811 || target_store.get_root(&target_meta_name).await?.is_some()
812 || log_path(target_data_dir, target_db).exists()
813 {
814 return Err(BackupError::TargetExists(target_db.to_owned()));
815 }
816 let mut backup_file = File::open(source)?;
817 let mut copied_blobs = 0;
818 let mut reused_blobs = 0;
819 for (offset, len) in &archive.blob_offsets {
820 backup_file.seek(SeekFrom::Start(*offset))?;
821 let len =
822 usize::try_from(*len).map_err(|_| invalid("blob is too large for this platform"))?;
823 let mut bytes = vec![0; len];
824 backup_file.read_exact(&mut bytes)?;
825 let id = corium_store::digest(&bytes);
826 if target_store.contains(&id).await? {
827 reused_blobs += 1;
828 } else {
829 let stored = target_store.put(&bytes).await?;
830 if stored != id {
831 return Err(invalid(format!("blob digest changed while restoring {id}")));
832 }
833 copied_blobs += 1;
834 }
835 }
836 let mut seen = std::collections::HashSet::new();
837 for id in root.roots.iter().flatten() {
838 validate_blob_tree(&target_store, id, &mut seen).await?;
839 }
840 let target_log = log_path(target_data_dir, target_db);
841 write_log_atomically(&archive.records, &target_log)?;
842 let db_bytes = archive.header.root;
843 let publish = match target_store
846 .cas_root(&target_db_name, None, &db_bytes)
847 .await
848 {
849 Ok(()) => {
850 target_store
851 .cas_root(&target_meta_name, None, &archive.metadata)
852 .await
853 }
854 Err(error) => Err(error),
855 };
856 if let Err(error) = publish {
857 let _ = target_store.delete_root(&target_db_name).await;
858 let _ = target_store.delete_root(&target_meta_name).await;
859 let _ = fs::remove_file(&target_log);
860 return Err(error.into());
861 }
862 Ok(RestoreReport {
863 backup_format_version: BACKUP_FORMAT_VERSION,
864 writer_version: archive.writer_version,
865 source_db: archive.header.source_db,
866 target_db: target_db.to_owned(),
867 basis_t: archive.basis_t,
868 copied_blobs,
869 reused_blobs,
870 })
871}