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