1use std::path::Path;
9
10use crate::alias::WriteTarget;
11use crate::error::{StorageError, io_err};
12use crate::real_fs::RealFs;
13use crate::vfs::Vfs;
14
15use super::files::{
16 copy_city_files, copy_tree, count_files, count_records, head_of, only_city_files, walk,
17};
18use super::history::{self, Carried, History};
19use super::landing::{Bits, land};
20use super::manifest::{CAS, CITY, LEDGER, MANIFEST, Manifest};
21
22fn agree(what: &str, source: u64, bundle: u64) -> Result<(), StorageError> {
25 if source == bundle {
26 return Ok(());
27 }
28 Err(StorageError::Bundle {
29 op: "export",
30 detail: format!("the city holds {source} {what}(s) and the bundle holds {bundle}"),
31 })
32}
33
34pub struct Bundle;
37
38impl Bundle {
39 pub fn export(city_root: &Path, dest: &Path) -> Result<Manifest, StorageError> {
46 Bundle::export_with(Box::new(RealFs::new()), city_root, dest)
47 }
48
49 pub(crate) fn export_with(
50 mut vfs: Box<dyn Vfs>,
51 city_root: &Path,
52 dest: &Path,
53 ) -> Result<Manifest, StorageError> {
54 let layout = kernel::layout::CityLayout::new(city_root);
55 let ledger_dir = layout.ledger();
56 let cas_dir = layout.cas();
57 let (dest_ledger, dest_cas, dest_city) =
58 (dest.join(LEDGER), dest.join(CAS), dest.join(CITY));
59 let ledger_files = copy_tree(vfs.as_mut(), dest, &ledger_dir, &dest_ledger)?;
60 let cas_copied = copy_tree(vfs.as_mut(), dest, &cas_dir, &dest_cas)?;
61 let city_copied = copy_city_files(vfs.as_mut(), dest, city_root, &dest_city)?;
62 history::export(vfs.as_mut(), city_root, dest)?;
63 let manifest = Manifest {
66 history: Carried::of(vfs.as_ref(), dest)?,
67 ..Manifest::of(vfs.as_ref(), &dest_ledger, &dest_cas, &dest_city)?
68 };
69 agree(
75 "ledger file",
76 ledger_files,
77 count_files(vfs.as_ref(), &dest_ledger)?,
78 )?;
79 agree("cas object", cas_copied, manifest.cas_objects)?;
80 agree("city file", city_copied, manifest.files)?;
81 agree(
82 "ledger record",
83 count_records(vfs.as_ref(), &ledger_dir)?,
84 manifest.records,
85 )?;
86 let source_head = head_of(vfs.as_ref(), &ledger_dir)?;
87 if source_head != manifest.head {
88 return Err(StorageError::Bundle {
89 op: "export",
90 detail: format!(
91 "the city's history ends {source_head} and the bundle's ends {}",
92 manifest.head
93 ),
94 });
95 }
96 let target = WriteTarget::within("write a bundle manifest", dest, &dest.join(MANIFEST))?;
97 land(
98 vfs.as_mut(),
99 target,
100 manifest.to_json().as_bytes(),
101 Bits::OfReplaced,
102 )?;
103 Ok(manifest)
104 }
105
106 pub fn read_manifest(bundle: &Path) -> Result<Manifest, StorageError> {
111 let vfs = RealFs::new();
112 let at = bundle.join(MANIFEST);
113 let bytes = vfs
114 .read(&at)
115 .map_err(io_err("read a bundle manifest", &at))?;
116 Manifest::from_json(&bytes, &at)
117 }
118
119 pub fn restore(bundle: &Path, city_root: &Path) -> Result<Manifest, StorageError> {
131 Bundle::restore_with(Box::new(RealFs::new()), bundle, city_root)
132 }
133
134 pub(crate) fn restore_with(
135 mut vfs: Box<dyn Vfs>,
136 bundle: &Path,
137 city_root: &Path,
138 ) -> Result<Manifest, StorageError> {
139 let mut claimed = {
140 let at = bundle.join(MANIFEST);
141 let bytes = vfs
142 .read(&at)
143 .map_err(io_err("read a bundle manifest", &at))?;
144 Manifest::from_json(&bytes, &at)?
145 };
146 let repository = only_city_files(vfs.as_ref(), &bundle.join(CITY))?;
152 claimed.files = claimed.files.saturating_sub(repository);
153 let layout = kernel::layout::CityLayout::new(city_root);
154 let ledger_dir = layout.ledger();
155 let cas_dir = layout.cas();
156 if !walk(vfs.as_ref(), &ledger_dir)?.is_empty() {
157 return Err(StorageError::Bundle {
158 op: "restore",
159 detail: format!("{} already holds a ledger", ledger_dir.display()),
160 });
161 }
162 let carried = Carried::of(vfs.as_ref(), bundle)?;
165 if carried != claimed.history {
166 return Err(StorageError::Bundle {
167 op: "restore",
168 detail: format!(
169 "the bundle claims {} history pack(s) and {} ref(s), and holds {} and {}",
170 claimed.history.packs, claimed.history.refs, carried.packs, carried.refs
171 ),
172 });
173 }
174 let history = History::read(vfs.as_ref(), bundle, city_root)?;
175 copy_tree(vfs.as_mut(), city_root, &bundle.join(LEDGER), &ledger_dir)?;
176 copy_tree(vfs.as_mut(), city_root, &bundle.join(CAS), &cas_dir)?;
177 copy_city_files(vfs.as_mut(), city_root, &bundle.join(CITY), city_root)?;
178 if let Some(history) = history {
179 history.land(city_root)?;
180 }
181
182 let restored = Manifest {
187 history: carried,
188 ..Manifest::of(vfs.as_ref(), &ledger_dir, &cas_dir, city_root)?
189 };
190 if restored != claimed {
191 return Err(StorageError::Bundle {
192 op: "restore",
193 detail: format!(
194 "the bundle claims {} record(s) ending {} with {} cas object(s) and {} file(s), \
195 and {} record(s) ending {} with {} cas object(s) and {} file(s) arrived",
196 claimed.records,
197 claimed.head,
198 claimed.cas_objects,
199 claimed.files,
200 restored.records,
201 restored.head,
202 restored.cas_objects,
203 restored.files
204 ),
205 });
206 }
207 Ok(restored)
208 }
209}
210
211#[cfg(test)]
212#[allow(
213 clippy::unwrap_used,
214 clippy::expect_used,
215 clippy::panic,
216 clippy::indexing_slicing,
217 reason = "test code"
218)]
219mod tests;
220
221#[cfg(test)]
222#[allow(
223 clippy::unwrap_used,
224 clippy::expect_used,
225 clippy::panic,
226 clippy::indexing_slicing,
227 reason = "test code"
228)]
229mod history_tests;