krabka_restore/
backend.rs1use {
8 crate::{args::RestoreArgs, error::RestoreError},
9 krabka_object_store::{
10 ObjectOps, ObjectStoreClient, ObjectStoreConfig, build_object_store, normalize_prefix,
11 prefixed_key,
12 },
13 object_store::path::Path,
14 std::sync::Arc,
15};
16
17#[derive(Clone)]
23pub struct ArchiveStore {
24 store: Arc<dyn object_store::ObjectStore>,
25 client: ObjectStoreClient,
26 prefix: Option<String>,
27}
28
29impl ArchiveStore {
30 #[must_use]
32 pub fn ops(&self) -> &dyn ObjectOps { &self.client }
33
34 #[must_use]
36 pub fn store(&self) -> &Arc<dyn object_store::ObjectStore> { &self.store }
37
38 #[must_use]
41 pub fn prefix(&self) -> Option<&str> { self.prefix.as_deref() }
42
43 #[must_use]
45 pub fn key(&self, relative: &str) -> Path { prefixed_key(self.prefix.as_deref(), relative) }
46
47 #[must_use]
49 pub fn root(&self) -> Option<Path> { self.prefix.as_deref().map(Path::from) }
50
51 #[must_use]
57 pub fn with_store(store: Arc<dyn object_store::ObjectStore>, prefix: Option<&str>) -> Self {
58 Self {
59 client: ObjectStoreClient::new(Arc::clone(&store)),
60 store,
61 prefix: normalize_prefix(prefix),
62 }
63 }
64}
65
66pub fn open_archive(args: &RestoreArgs) -> Result<ArchiveStore, RestoreError> {
74 let config = object_store_config(args)?;
75 let store: Arc<dyn object_store::ObjectStore> = build_object_store(&config)?;
76 Ok(ArchiveStore {
77 client: ObjectStoreClient::new(Arc::clone(&store)),
78 store,
79 prefix: args.archive.location.normalized_prefix(),
80 })
81}
82
83pub fn object_store_config(args: &RestoreArgs) -> Result<ObjectStoreConfig, RestoreError> {
94 args.archive
95 .location
96 .to_config()
97 .map_err(|error| RestoreError::InvalidArgument(error.to_string()))
98}
99
100#[cfg(test)]
101mod tests {
102 use {super::*, crate::Cli, assert2::check, clap::Parser as _, object_store::memory::InMemory};
103
104 fn args_from(extra: &[&str]) -> RestoreArgs {
105 let mut argv = vec!["krabka-restore", "--log-dir", "/target"];
106 argv.extend_from_slice(extra);
107 Cli::parse_from(argv).args
108 }
109
110 krabka_macros::s3_archive_config_fixture!(expected_s3_config, krabka_object_store::S3Config);
111
112 #[test]
115 fn s3_flags_map_onto_the_s3_config() {
116 let config = object_store_config(&args_from(&[
117 "--archive-s3-bucket",
118 "backups",
119 "--archive-s3-region",
120 "eu-west-1",
121 "--archive-s3-endpoint",
122 "http://minio:9000",
123 "--archive-s3-access-key-id",
124 "key",
125 "--archive-s3-secret-access-key",
126 "secret",
127 "--archive-s3-allow-http",
128 ]))
129 .expect("config");
130 let ObjectStoreConfig::S3(s3) = config else {
131 panic!("expected an S3 config");
132 };
133 check!(s3 == expected_s3_config());
134 }
135
136 #[test]
137 fn a_hand_built_args_without_a_backend_is_rejected() {
138 let mut args = args_from(&["--archive-local", "/archive"]);
139 args.archive.location.local = None;
140 check!(matches!(
141 object_store_config(&args),
142 Err(RestoreError::InvalidArgument(_))
143 ));
144 }
145
146 #[yare::parameterized(
147 prefixed = { Some("/tier/") },
148 bare = { None },
149 )]
150 fn keys_are_built_with_an_optional_prefix(prefix: Option<&str>) {
151 let archive = tempfile::tempdir().expect("temp dir");
153 let local = archive.path().display().to_string();
154 let mut args = vec!["--archive-local", local.as_str()];
155 if let Some(prefix) = prefix {
156 args.extend(["--archive-prefix", prefix]);
157 }
158 let store = open_archive(&args_from(&args)).expect("store");
159 let expected_prefix = prefix.map(|_| Path::from("tier"));
160 let expected_key = match prefix {
161 Some(_) => "tier/orders-0-abc/000.log",
162 None => "orders-0-abc/000.log",
163 };
164 check!(store.prefix() == prefix.map(|_| "tier"));
165 check!(store.key("orders-0-abc/000.log") == Path::from(expected_key));
166 check!(store.root() == expected_prefix);
167 }
168
169 #[tokio::test]
172 async fn a_handle_on_a_store_built_elsewhere_reads_it_under_the_prefix() {
173 let inner = Arc::new(InMemory::new());
174 let store = ArchiveStore::with_store(Arc::<InMemory>::clone(&inner), Some("/tier/"));
175
176 check!(store.prefix() == Some("tier"));
177 check!(store.root() == Some(Path::from("tier")));
178 check!(store.key("orders-0-abc/000.log") == Path::from("tier/orders-0-abc/000.log"));
179
180 object_store::ObjectStoreExt::put(
181 inner.as_ref(),
182 &store.key("orders-0-abc/000.log"),
183 object_store::PutPayload::from_static(b"segment bytes"),
184 )
185 .await
186 .expect("write an object");
187 let listed = store.ops().list(store.root()).await.expect("list");
188 check!(
189 listed
190 .iter()
191 .map(|meta| meta.location.clone())
192 .collect::<Vec<_>>()
193 == vec![Path::from("tier/orders-0-abc/000.log")]
194 );
195
196 let bare = ArchiveStore::with_store(inner, None);
198 check!(bare.prefix().is_none());
199 check!(bare.root().is_none());
200 check!(bare.key("orders-0-abc/000.log") == Path::from("orders-0-abc/000.log"));
201 }
202}