| 8d803563 Andy McCall ↶ |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
mod aws; |
| 16 |
mod azure; |
| 17 |
mod gcp; |
| 2cd40ea1 Andy McCall ↶ |
18 |
mod http; |
| 8d803563 Andy McCall ↶ |
19 |
mod ini; |
| 20 |
|
| 21 |
use futures::TryStreamExt; |
| 22 |
use object_store::path::Path as ObjectPath; |
| 23 |
use object_store::{ |
| 24 |
GetOptions, ObjectStore, ObjectStoreExt, PutMode, PutOptions, PutPayload, UpdateVersion, |
| 25 |
}; |
| 26 |
use oxy_core::strata::{check_key, Entry, Error, Result, Store, Version}; |
| 27 |
use std::collections::BTreeMap; |
| 28 |
use std::future::Future; |
| 29 |
use std::ops::Range; |
| 30 |
use std::sync::{Arc, Mutex}; |
| 31 |
|
| 32 |
|
| 33 |
|
| 34 |
pub fn register() { |
| 35 |
|
| 36 |
|
| 37 |
let _ = rustls::crypto::ring::default_provider().install_default(); |
| 38 |
oxy_core::protocol::register_store_opener(open_url); |
| 39 |
} |
| 40 |
|
| 41 |
fn open_url( |
| 42 |
url: &str, |
| 43 |
_options: &oxy_core::protocol::RemoteOptions, |
| 44 |
) -> Option<Result<Box<dyn Store>>> { |
| 45 |
let scheme = url.split_once("://")?.0; |
| 46 |
if !matches!(scheme, "s3" | "gs" | "az") { |
| 47 |
return None; |
| 48 |
} |
| 7026bd7d Andy McCall ↶ |
49 |
let lookup = *CREDENTIALS.lock().unwrap_or_else(|p| p.into_inner()); |
| 50 |
let given = lookup.and_then(|f| f(url)); |
| 2ddd4716 Andy McCall ↶ |
51 |
Some(shared(url, given).map(|s| Box::new(Shared(s)) as Box<dyn Store>)) |
| 8d803563 Andy McCall ↶ |
52 |
} |
| 53 |
|
| 2ddd4716 Andy McCall ↶ |
54 |
|
| 55 |
|
| 56 |
|
| 57 |
|
| 58 |
type OpenStores = std::collections::HashMap<(String, Option<Credentials>), Arc<CloudStore>>; |
| 59 |
|
| 60 |
fn shared(url: &str, given: Option<Credentials>) -> Result<Arc<CloudStore>> { |
| 61 |
static OPEN: Mutex<Option<OpenStores>> = Mutex::new(None); |
| 62 |
let key = (url.to_string(), given); |
| 63 |
let mut open = OPEN.lock().unwrap_or_else(|p| p.into_inner()); |
| 64 |
let map = open.get_or_insert_with(Default::default); |
| 65 |
if let Some(s) = map.get(&key) { |
| 66 |
return Ok(s.clone()); |
| 67 |
} |
| 68 |
let store = Arc::new(CloudStore::open_with(url, key.1.as_ref())?); |
| 69 |
map.insert(key, store.clone()); |
| 70 |
Ok(store) |
| 71 |
} |
| 72 |
|
| 73 |
|
| 74 |
struct Shared(Arc<CloudStore>); |
| 75 |
|
| 76 |
impl Store for Shared { |
| 77 |
fn describe(&self) -> String { |
| 78 |
self.0.describe() |
| 79 |
} |
| 80 |
fn has(&self, key: &str) -> Result<bool> { |
| 81 |
self.0.has(key) |
| 82 |
} |
| 83 |
fn get(&self, key: &str) -> Result<Vec<u8>> { |
| 84 |
self.0.get(key) |
| 85 |
} |
| 86 |
fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> { |
| 87 |
self.0.get_range(key, range) |
| 88 |
} |
| 89 |
fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> { |
| 90 |
self.0.put_new(key, bytes) |
| 91 |
} |
| 92 |
fn list(&self, prefix: &str) -> Result<Vec<Entry>> { |
| 93 |
self.0.list(prefix) |
| 94 |
} |
| 7e0e8e5a Andy McCall ↶ |
95 |
fn stat(&self, key: &str) -> Result<Option<Entry>> { |
| 96 |
self.0.stat(key) |
| 97 |
} |
| 2ddd4716 Andy McCall ↶ |
98 |
fn delete(&self, key: &str) -> Result<()> { |
| 99 |
self.0.delete(key) |
| 100 |
} |
| 101 |
fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> { |
| 102 |
self.0.read_ref(key) |
| 103 |
} |
| 104 |
fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> { |
| 105 |
self.0.write_ref(key, bytes, expected) |
| 106 |
} |
| 107 |
} |
| 108 |
|
| 109 |
static OPENS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); |
| 110 |
static CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); |
| 111 |
static MICROS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); |
| 1e0fbb47 Andy McCall ↶ |
112 |
static BY_OP: [std::sync::atomic::AtomicU64; Op::ALL.len()] = |
| 113 |
[const { std::sync::atomic::AtomicU64::new(0) }; Op::ALL.len()]; |
| 2ddd4716 Andy McCall ↶ |
114 |
|
| 1e0fbb47 Andy McCall ↶ |
115 |
|
| 116 |
|
| 117 |
#[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 118 |
pub enum Op { |
| 119 |
|
| 120 |
Has, |
| 121 |
|
| 122 |
HasMissing, |
| 123 |
Get, |
| 124 |
Range, |
| 125 |
Put, |
| 126 |
List, |
| 30644f1e Andy McCall ↶ |
127 |
|
| 128 |
ListPacks, |
| 129 |
|
| 130 |
ListKept, |
| 1e0fbb47 Andy McCall ↶ |
131 |
Delete, |
| 132 |
ReadRef, |
| ef7d4ea6 Andy McCall ↶ |
133 |
|
| 134 |
|
| 135 |
|
| 136 |
ReadJournal, |
| 1e0fbb47 Andy McCall ↶ |
137 |
WriteRef, |
| 138 |
} |
| 139 |
|
| 140 |
impl Op { |
| ef7d4ea6 Andy McCall ↶ |
141 |
pub const ALL: [Op; 12] = [ |
| 1e0fbb47 Andy McCall ↶ |
142 |
Op::Has, |
| 143 |
Op::HasMissing, |
| 144 |
Op::Get, |
| 145 |
Op::Range, |
| 146 |
Op::Put, |
| 147 |
Op::List, |
| 30644f1e Andy McCall ↶ |
148 |
Op::ListPacks, |
| 149 |
Op::ListKept, |
| 1e0fbb47 Andy McCall ↶ |
150 |
Op::Delete, |
| 151 |
Op::ReadRef, |
| ef7d4ea6 Andy McCall ↶ |
152 |
Op::ReadJournal, |
| 1e0fbb47 Andy McCall ↶ |
153 |
Op::WriteRef, |
| 154 |
]; |
| 155 |
|
| 156 |
pub fn name(self) -> &'static str { |
| 157 |
match self { |
| 158 |
Op::Has => "has", |
| 159 |
Op::HasMissing => "has_missing", |
| 160 |
Op::Get => "get", |
| 161 |
Op::Range => "range", |
| 162 |
Op::Put => "put", |
| 163 |
Op::List => "list", |
| 30644f1e Andy McCall ↶ |
164 |
Op::ListPacks => "list_packs", |
| 165 |
Op::ListKept => "list_kept", |
| 1e0fbb47 Andy McCall ↶ |
166 |
Op::Delete => "delete", |
| 167 |
Op::ReadRef => "read_ref", |
| ef7d4ea6 Andy McCall ↶ |
168 |
Op::ReadJournal => "read_journal", |
| 1e0fbb47 Andy McCall ↶ |
169 |
Op::WriteRef => "write_ref", |
| 170 |
} |
| 171 |
} |
| 172 |
} |
| 173 |
|
| 174 |
fn counted(op: Op) { |
| 175 |
BY_OP[op as usize].fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
| 176 |
} |
| 177 |
|
| 2ddd4716 Andy McCall ↶ |
178 |
|
| 179 |
|
| 180 |
|
| 181 |
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] |
| 182 |
pub struct Stats { |
| 183 |
pub opens: u64, |
| 184 |
pub calls: u64, |
| 185 |
pub millis: u64, |
| 1e0fbb47 Andy McCall ↶ |
186 |
|
| 187 |
pub by_op: [u64; Op::ALL.len()], |
| 2ddd4716 Andy McCall ↶ |
188 |
} |
| 189 |
|
| 1e0fbb47 Andy McCall ↶ |
190 |
impl Stats { |
| 191 |
|
| 192 |
pub fn ops(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ { |
| 193 |
Op::ALL |
| 194 |
.iter() |
| 195 |
.map(|op| (op.name(), self.by_op[*op as usize])) |
| 196 |
} |
| 197 |
} |
| 198 |
|
| 2ddd4716 Andy McCall ↶ |
199 |
pub fn stats() -> Stats { |
| 200 |
use std::sync::atomic::Ordering::Relaxed; |
| 201 |
Stats { |
| 202 |
opens: OPENS.load(Relaxed), |
| 203 |
calls: CALLS.load(Relaxed), |
| 204 |
millis: MICROS.load(Relaxed) / 1000, |
| 1e0fbb47 Andy McCall ↶ |
205 |
by_op: std::array::from_fn(|i| BY_OP[i].load(Relaxed)), |
| 2ddd4716 Andy McCall ↶ |
206 |
} |
| 207 |
} |
| 208 |
|
| 7026bd7d Andy McCall ↶ |
209 |
|
| 210 |
|
| 211 |
|
| 212 |
|
| 2ddd4716 Andy McCall ↶ |
213 |
#[derive(Clone, PartialEq, Eq, Hash)] |
| 7026bd7d Andy McCall ↶ |
214 |
pub enum Credentials { |
| 215 |
|
| 216 |
Aws { |
| 217 |
access_key_id: String, |
| 218 |
secret_access_key: String, |
| 219 |
session_token: Option<String>, |
| 220 |
}, |
| 221 |
|
| 222 |
Gcp { service_account_key: String }, |
| 223 |
|
| 224 |
Azure { |
| 225 |
tenant_id: String, |
| 226 |
client_id: String, |
| 227 |
client_secret: String, |
| 228 |
}, |
| 229 |
} |
| 230 |
|
| 231 |
impl std::fmt::Debug for Credentials { |
| 232 |
|
| 233 |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| 234 |
match self { |
| 235 |
Credentials::Aws { access_key_id, .. } => { |
| 236 |
write!(f, "Aws({access_key_id}, secret hidden)") |
| 237 |
} |
| 238 |
Credentials::Gcp { .. } => write!(f, "Gcp(service account key hidden)"), |
| 239 |
Credentials::Azure { client_id, .. } => write!(f, "Azure({client_id}, secret hidden)"), |
| 240 |
} |
| 241 |
} |
| 242 |
} |
| 243 |
|
| 244 |
impl Credentials { |
| 245 |
|
| 246 |
pub fn scheme(&self) -> &'static str { |
| 247 |
match self { |
| 248 |
Credentials::Aws { .. } => "s3", |
| 249 |
Credentials::Gcp { .. } => "gs", |
| 250 |
Credentials::Azure { .. } => "az", |
| 251 |
} |
| 252 |
} |
| 253 |
|
| 254 |
|
| 255 |
pub fn to_json(&self) -> String { |
| 256 |
let v = match self { |
| 257 |
Credentials::Aws { |
| 258 |
access_key_id, |
| 259 |
secret_access_key, |
| 260 |
session_token, |
| 261 |
} => serde_json::json!({ |
| 262 |
"kind": "aws", "access_key_id": access_key_id, "secret_access_key": secret_access_key, |
| 263 |
"session_token": session_token, |
| 264 |
}), |
| 265 |
Credentials::Gcp { |
| 266 |
service_account_key, |
| 267 |
} => { |
| 268 |
serde_json::json!({ "kind": "gcp", "service_account_key": service_account_key }) |
| 269 |
} |
| 270 |
Credentials::Azure { |
| 271 |
tenant_id, |
| 272 |
client_id, |
| 273 |
client_secret, |
| 274 |
} => serde_json::json!({ |
| 275 |
"kind": "azure", "tenant_id": tenant_id, "client_id": client_id, "client_secret": client_secret, |
| 276 |
}), |
| 277 |
}; |
| 278 |
v.to_string() |
| 279 |
} |
| 280 |
|
| 281 |
pub fn from_json(text: &str) -> Result<Credentials> { |
| 282 |
let v: serde_json::Value = serde_json::from_str(text) |
| 283 |
.map_err(|e| Error::Backend(format!("credentials are not JSON: {e}")))?; |
| 284 |
let field = |k: &str| -> Result<String> { |
| 285 |
v.get(k) |
| 286 |
.and_then(|x| x.as_str()) |
| 287 |
.filter(|x| !x.trim().is_empty()) |
| 288 |
.map(|x| x.trim().to_string()) |
| 289 |
.ok_or_else(|| Error::Backend(format!("credentials need {k}"))) |
| 290 |
}; |
| 291 |
match v.get("kind").and_then(|k| k.as_str()) { |
| 292 |
Some("aws") => Ok(Credentials::Aws { |
| 293 |
access_key_id: field("access_key_id")?, |
| 294 |
secret_access_key: field("secret_access_key")?, |
| 295 |
session_token: field("session_token").ok(), |
| 296 |
}), |
| 297 |
Some("gcp") => { |
| 298 |
let key = field("service_account_key")?; |
| 299 |
let parsed: serde_json::Value = serde_json::from_str(&key).map_err(|_| { |
| 300 |
Error::Backend("the service account key is not the JSON Google issues".into()) |
| 301 |
})?; |
| 302 |
if parsed.get("type").and_then(|t| t.as_str()) != Some("service_account") { |
| 303 |
return Err(Error::Backend( |
| 304 |
"the key is not a service account key (its type is not service_account)" |
| 305 |
.into(), |
| 306 |
)); |
| 307 |
} |
| 308 |
Ok(Credentials::Gcp { |
| 309 |
service_account_key: key, |
| 310 |
}) |
| 311 |
} |
| 312 |
Some("azure") => Ok(Credentials::Azure { |
| 313 |
tenant_id: field("tenant_id")?, |
| 314 |
client_id: field("client_id")?, |
| 315 |
client_secret: field("client_secret")?, |
| 316 |
}), |
| 317 |
_ => Err(Error::Backend( |
| 318 |
"credentials need a kind: aws, gcp or azure".into(), |
| 319 |
)), |
| 320 |
} |
| 321 |
} |
| 322 |
} |
| 323 |
|
| 324 |
|
| 325 |
pub type CredentialsLookup = fn(&str) -> Option<Credentials>; |
| 326 |
|
| 327 |
static CREDENTIALS: Mutex<Option<CredentialsLookup>> = Mutex::new(None); |
| 328 |
|
| 329 |
|
| 330 |
|
| 331 |
pub fn set_credentials_lookup(lookup: CredentialsLookup) { |
| 332 |
*CREDENTIALS.lock().unwrap_or_else(|p| p.into_inner()) = Some(lookup); |
| 333 |
} |
| 334 |
|
| 8d803563 Andy McCall ↶ |
335 |
|
| 336 |
#[derive(Clone, Debug, PartialEq, Eq)] |
| 337 |
pub struct StoreUrl { |
| 338 |
pub scheme: String, |
| 339 |
|
| 340 |
pub host: String, |
| 341 |
|
| 342 |
|
| 343 |
pub path: String, |
| 344 |
pub params: BTreeMap<String, String>, |
| 345 |
} |
| 346 |
|
| 347 |
impl StoreUrl { |
| 348 |
pub fn parse(url: &str) -> Result<StoreUrl> { |
| 349 |
let bad = |why: &str| Error::Backend(format!("{url}: {why}")); |
| 350 |
let (scheme, rest) = url.split_once("://").ok_or_else(|| bad("not a URL"))?; |
| 351 |
let (rest, query) = match rest.split_once('?') { |
| 352 |
Some((r, q)) => (r, Some(q)), |
| 353 |
None => (rest, None), |
| 354 |
}; |
| 355 |
let (host, path) = match rest.split_once('/') { |
| 356 |
Some((h, p)) => (h, p.trim_matches('/')), |
| 357 |
None => (rest, ""), |
| 358 |
}; |
| 359 |
if host.is_empty() { |
| 360 |
return Err(bad("names no bucket")); |
| 361 |
} |
| 362 |
let mut params = BTreeMap::new(); |
| 363 |
for pair in query.unwrap_or("").split('&').filter(|p| !p.is_empty()) { |
| 364 |
let (k, v) = pair.split_once('=').unwrap_or((pair, "")); |
| 365 |
params.insert(k.to_string(), percent_decode(v)); |
| 366 |
} |
| 367 |
Ok(StoreUrl { |
| 368 |
scheme: scheme.to_string(), |
| 369 |
host: host.to_string(), |
| 370 |
path: path.to_string(), |
| 371 |
params, |
| 372 |
}) |
| 373 |
} |
| 374 |
|
| 375 |
|
| 376 |
pub fn plain(&self) -> String { |
| 377 |
if self.path.is_empty() { |
| 378 |
format!("{}://{}", self.scheme, self.host) |
| 379 |
} else { |
| 380 |
format!("{}://{}/{}", self.scheme, self.host, self.path) |
| 381 |
} |
| 382 |
} |
| 383 |
|
| 384 |
pub fn param(&self, name: &str) -> Option<&str> { |
| 385 |
self.params.get(name).map(String::as_str) |
| 386 |
} |
| 387 |
} |
| 388 |
|
| 389 |
fn percent_decode(s: &str) -> String { |
| 390 |
let bytes = s.as_bytes(); |
| 391 |
let mut out = Vec::with_capacity(bytes.len()); |
| 392 |
let mut i = 0; |
| 393 |
while i < bytes.len() { |
| 394 |
if bytes[i] == b'%' && i + 2 < bytes.len() { |
| 395 |
if let Ok(v) = u8::from_str_radix(&s[i + 1..i + 3], 16) { |
| 396 |
out.push(v); |
| 397 |
i += 3; |
| 398 |
continue; |
| 399 |
} |
| 400 |
} |
| 401 |
out.push(bytes[i]); |
| 402 |
i += 1; |
| 403 |
} |
| 404 |
String::from_utf8_lossy(&out).into_owned() |
| 405 |
} |
| 406 |
|
| 407 |
|
| 408 |
|
| 409 |
pub struct CloudStore { |
| 410 |
inner: Arc<dyn ObjectStore>, |
| 411 |
prefix: String, |
| 412 |
name: String, |
| 413 |
hint: &'static str, |
| 2ddd4716 Andy McCall ↶ |
414 |
|
| 415 |
|
| c9369ad8 Andy McCall ↶ |
416 |
runtime: Option<tokio::runtime::Runtime>, |
| 8b53b935 Andy McCall ↶ |
417 |
|
| 418 |
misses: Misses, |
| 87c94872 Andy McCall ↶ |
419 |
|
| 420 |
listings: Listings, |
| 8d803563 Andy McCall ↶ |
421 |
} |
| 422 |
|
| 8b53b935 Andy McCall ↶ |
423 |
|
| 424 |
const MISS_FOR: std::time::Duration = std::time::Duration::from_secs(30); |
| 425 |
|
| 426 |
|
| 427 |
|
| 428 |
|
| 429 |
|
| 430 |
|
| 431 |
|
| 432 |
#[derive(Default)] |
| 433 |
struct Misses(Mutex<std::collections::HashMap<String, std::time::Instant>>); |
| 434 |
|
| 435 |
impl Misses { |
| 436 |
|
| 437 |
const MAX: usize = 100_000; |
| 438 |
|
| 439 |
fn known(&self, key: &str) -> bool { |
| 440 |
let map = self.0.lock().unwrap_or_else(|p| p.into_inner()); |
| 441 |
map.get(key).is_some_and(|t| t.elapsed() < MISS_FOR) |
| 442 |
} |
| 443 |
|
| 444 |
fn missed(&self, key: &str) { |
| 445 |
let mut map = self.0.lock().unwrap_or_else(|p| p.into_inner()); |
| 446 |
if map.len() >= Self::MAX { |
| 447 |
map.retain(|_, t| t.elapsed() < MISS_FOR / 2); |
| 448 |
if map.len() >= Self::MAX { |
| 449 |
map.clear(); |
| 450 |
} |
| 451 |
} |
| 452 |
map.insert(key.to_string(), std::time::Instant::now()); |
| 453 |
} |
| 454 |
|
| 455 |
fn written(&self, key: &str) { |
| 456 |
self.0.lock().unwrap_or_else(|p| p.into_inner()).remove(key); |
| 457 |
} |
| 458 |
} |
| 459 |
|
| 87c94872 Andy McCall ↶ |
460 |
|
| 461 |
|
| 462 |
|
| 463 |
|
| 464 |
|
| 465 |
|
| 466 |
#[derive(Default)] |
| 467 |
struct Listings(Mutex<std::collections::HashMap<String, (std::time::Instant, Vec<Entry>)>>); |
| 468 |
|
| 469 |
impl Listings { |
| 470 |
|
| 471 |
fn kept(prefix: &str) -> bool { |
| 472 |
prefix == "pack-" |
| 473 |
} |
| 474 |
|
| 475 |
fn get(&self, prefix: &str) -> Option<Vec<Entry>> { |
| 476 |
if !Self::kept(prefix) { |
| 477 |
return None; |
| 478 |
} |
| 479 |
let map = self.0.lock().unwrap_or_else(|p| p.into_inner()); |
| 480 |
map.get(prefix) |
| 481 |
.filter(|(t, _)| t.elapsed() < MISS_FOR) |
| 482 |
.map(|(_, e)| e.clone()) |
| 483 |
} |
| 484 |
|
| 485 |
fn put(&self, prefix: &str, entries: &[Entry]) { |
| 486 |
if Self::kept(prefix) { |
| 487 |
self.0.lock().unwrap_or_else(|p| p.into_inner()).insert( |
| 488 |
prefix.to_string(), |
| 489 |
(std::time::Instant::now(), entries.to_vec()), |
| 490 |
); |
| 491 |
} |
| 492 |
} |
| 493 |
|
| 494 |
fn written(&self, key: &str) { |
| 495 |
self.0 |
| 496 |
.lock() |
| 497 |
.unwrap_or_else(|p| p.into_inner()) |
| 498 |
.retain(|prefix, _| !key.starts_with(prefix.as_str())); |
| 499 |
} |
| 500 |
} |
| 501 |
|
| 767ac6cd Andy McCall |
502 |
|
| 503 |
|
| 504 |
|
| 505 |
|
| 506 |
|
| 507 |
fn writing<T>(misses: &Misses, listings: &Listings, key: &str, request: impl FnOnce() -> T) -> T { |
| 508 |
misses.written(key); |
| 509 |
listings.written(key); |
| 510 |
let out = request(); |
| 511 |
misses.written(key); |
| 512 |
listings.written(key); |
| 513 |
out |
| 514 |
} |
| 515 |
|
| c9369ad8 Andy McCall ↶ |
516 |
impl Drop for CloudStore { |
| 517 |
|
| 518 |
fn drop(&mut self) { |
| 519 |
if let Some(rt) = self.runtime.take() { |
| 520 |
rt.shutdown_background(); |
| 521 |
} |
| 522 |
} |
| 523 |
} |
| 524 |
|
| 8d803563 Andy McCall ↶ |
525 |
impl CloudStore { |
| 526 |
|
| 527 |
|
| 528 |
pub fn open(url: &str) -> Result<CloudStore> { |
| 7026bd7d Andy McCall ↶ |
529 |
Self::open_with(url, None) |
| 530 |
} |
| 531 |
|
| 532 |
|
| 533 |
|
| 534 |
pub fn open_with(url: &str, given: Option<&Credentials>) -> Result<CloudStore> { |
| 8d803563 Andy McCall ↶ |
535 |
let parsed = StoreUrl::parse(url)?; |
| 7026bd7d Andy McCall ↶ |
536 |
if let Some(c) = given.filter(|c| c.scheme() != parsed.scheme) { |
| 537 |
return Err(Error::Backend(format!( |
| 538 |
"{}: the credentials given are for {}:// stores", |
| 539 |
parsed.plain(), |
| 540 |
c.scheme() |
| 541 |
))); |
| 542 |
} |
| 543 |
let aws_keys = match given { |
| 544 |
Some(Credentials::Aws { |
| 545 |
access_key_id, |
| 546 |
secret_access_key, |
| 547 |
session_token, |
| 548 |
}) => Some(( |
| 549 |
access_key_id.as_str(), |
| 550 |
secret_access_key.as_str(), |
| 551 |
session_token.as_deref(), |
| 552 |
)), |
| 553 |
_ => None, |
| 554 |
}; |
| 555 |
let gcp_key = match given { |
| 556 |
Some(Credentials::Gcp { |
| 557 |
service_account_key, |
| 558 |
}) => Some(service_account_key.as_str()), |
| 559 |
_ => None, |
| 560 |
}; |
| 561 |
let principal = match given { |
| 562 |
Some(Credentials::Azure { |
| 563 |
tenant_id, |
| 564 |
client_id, |
| 565 |
client_secret, |
| 566 |
}) => Some(( |
| 567 |
tenant_id.as_str(), |
| 568 |
client_id.as_str(), |
| 569 |
client_secret.as_str(), |
| 570 |
)), |
| 571 |
_ => None, |
| 572 |
}; |
| 2ddd4716 Andy McCall ↶ |
573 |
OPENS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
| 574 |
let runtime = tokio::runtime::Builder::new_multi_thread() |
| 575 |
.worker_threads(2) |
| 576 |
.thread_name("oxy-strata") |
| 8d803563 Andy McCall ↶ |
577 |
.enable_all() |
| 578 |
.build() |
| 579 |
.map_err(|e| Error::Backend(format!("starting the HTTP runtime: {e}")))?; |
| 580 |
let (inner, prefix, hint): (Arc<dyn ObjectStore>, String, &'static str) = |
| 581 |
match parsed.scheme.as_str() { |
| 582 |
"s3" => ( |
| 7026bd7d Andy McCall ↶ |
583 |
Arc::new(aws::open(&parsed, aws_keys)?), |
| 8d803563 Andy McCall ↶ |
584 |
parsed.path.clone(), |
| 585 |
aws::HINT, |
| 586 |
), |
| 587 |
"gs" => ( |
| 7026bd7d Andy McCall ↶ |
588 |
Arc::new(gcp::open(&parsed, gcp_key)?), |
| 8d803563 Andy McCall ↶ |
589 |
parsed.path.clone(), |
| 590 |
gcp::HINT, |
| 591 |
), |
| 592 |
"az" => { |
| 7026bd7d Andy McCall ↶ |
593 |
let (store, prefix) = azure::open(&parsed, principal)?; |
| 8d803563 Andy McCall ↶ |
594 |
(Arc::new(store), prefix, azure::HINT) |
| 595 |
} |
| 596 |
other => { |
| 597 |
return Err(Error::Backend(format!( |
| 598 |
"{url}: {other}:// is not a store oxy knows (s3://, gs://, az://, dir://)" |
| 599 |
))) |
| 600 |
} |
| 601 |
}; |
| 602 |
Ok(CloudStore { |
| 603 |
inner, |
| 604 |
prefix, |
| 605 |
name: parsed.plain(), |
| 606 |
hint, |
| c9369ad8 Andy McCall ↶ |
607 |
runtime: Some(runtime), |
| 8b53b935 Andy McCall ↶ |
608 |
misses: Misses::default(), |
| 87c94872 Andy McCall ↶ |
609 |
listings: Listings::default(), |
| 8d803563 Andy McCall ↶ |
610 |
}) |
| 611 |
} |
| 612 |
|
| c9369ad8 Andy McCall ↶ |
613 |
|
| 614 |
|
| 615 |
|
| 616 |
fn block<F>(&self, f: F) -> F::Output |
| 617 |
where |
| 618 |
F: Future + Send, |
| 619 |
F::Output: Send, |
| 620 |
{ |
| 621 |
let runtime = self |
| 622 |
.runtime |
| 623 |
.as_ref() |
| 624 |
.expect("the runtime lives as long as the store"); |
| 2ddd4716 Andy McCall ↶ |
625 |
let started = std::time::Instant::now(); |
| 626 |
let out = if tokio::runtime::Handle::try_current().is_ok() { |
| 627 |
std::thread::scope(|s| { |
| c9369ad8 Andy McCall ↶ |
628 |
s.spawn(|| runtime.block_on(f)) |
| 629 |
.join() |
| 630 |
.unwrap_or_else(|p| std::panic::resume_unwind(p)) |
| 2ddd4716 Andy McCall ↶ |
631 |
}) |
| 632 |
} else { |
| 633 |
runtime.block_on(f) |
| 634 |
}; |
| 635 |
CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); |
| 636 |
MICROS.fetch_add( |
| 637 |
started.elapsed().as_micros() as u64, |
| 638 |
std::sync::atomic::Ordering::Relaxed, |
| 639 |
); |
| 640 |
out |
| 8d803563 Andy McCall ↶ |
641 |
} |
| 642 |
|
| 643 |
fn path(&self, key: &str) -> Result<ObjectPath> { |
| 644 |
check_key(key)?; |
| 645 |
let full = if self.prefix.is_empty() { |
| 646 |
key.to_string() |
| 647 |
} else { |
| 648 |
format!("{}/{key}", self.prefix) |
| 649 |
}; |
| 650 |
ObjectPath::parse(&full).map_err(|e| Error::Backend(format!("{full}: {e}"))) |
| 651 |
} |
| 652 |
|
| 653 |
|
| 654 |
fn map(&self, e: object_store::Error, key: &str) -> Error { |
| 655 |
use object_store::Error as E; |
| 656 |
match e { |
| 657 |
E::NotFound { .. } => Error::NotFound(key.to_string()), |
| 658 |
E::AlreadyExists { .. } => Error::Exists(key.to_string()), |
| 659 |
E::Precondition { .. } | E::NotModified { .. } => Error::Conflict(key.to_string()), |
| 660 |
other => { |
| 661 |
let text = other.to_string(); |
| 662 |
let lower = text.to_ascii_lowercase(); |
| 663 |
let advice = if lower.contains("403") |
| 664 |
|| lower.contains("accessdenied") |
| 665 |
|| lower.contains("forbidden") |
| 666 |
|| lower.contains("authorizationpermissionmismatch") |
| 667 |
{ |
| 668 |
format!( |
| 669 |
": access denied; the credentials in use may not do this on {}: {}", |
| 670 |
self.name, self.hint |
| 671 |
) |
| 672 |
} else if lower.contains("credential") || lower.contains("no valid credentials") { |
| 673 |
format!(": no usable credentials: {}", self.hint) |
| 674 |
} else if lower.contains("nosuchbucket") |
| 675 |
|| lower.contains("bucketnotfound") |
| 676 |
|| lower.contains("containernotfound") |
| 677 |
{ |
| 678 |
format!( |
| 679 |
": {} does not exist, or the credentials cannot see it", |
| 680 |
self.name |
| 681 |
) |
| 682 |
} else if lower.contains("dns") |
| 683 |
|| lower.contains("connect") |
| 684 |
|| lower.contains("timed out") |
| 685 |
{ |
| 686 |
": cannot reach the service; check the network, the endpoint and the region" |
| 687 |
.to_string() |
| 688 |
} else { |
| 689 |
String::new() |
| 690 |
}; |
| 691 |
Error::Backend(format!("{}: {text}{advice}", self.name)) |
| 692 |
} |
| 693 |
} |
| 694 |
} |
| 695 |
|
| 696 |
fn version_of(e_tag: Option<String>, version: Option<String>) -> Version { |
| 697 |
Version(format!( |
| 698 |
"{}\t{}", |
| 699 |
e_tag.unwrap_or_default(), |
| 700 |
version.unwrap_or_default() |
| 701 |
)) |
| 702 |
} |
| 703 |
|
| 704 |
fn update_version(v: &Version) -> UpdateVersion { |
| 705 |
let (e_tag, version) = v.0.split_once('\t').unwrap_or((v.0.as_str(), "")); |
| 706 |
UpdateVersion { |
| 707 |
e_tag: (!e_tag.is_empty()).then(|| e_tag.to_string()), |
| 708 |
version: (!version.is_empty()).then(|| version.to_string()), |
| 709 |
} |
| 710 |
} |
| 711 |
} |
| 712 |
|
| 713 |
impl Store for CloudStore { |
| 714 |
fn describe(&self) -> String { |
| 715 |
self.name.clone() |
| 716 |
} |
| 717 |
|
| 718 |
fn has(&self, key: &str) -> Result<bool> { |
| 719 |
let path = self.path(key)?; |
| 8b53b935 Andy McCall ↶ |
720 |
if self.misses.known(key) { |
| 721 |
return Ok(false); |
| 722 |
} |
| 8d803563 Andy McCall ↶ |
723 |
match self.block(self.inner.head(&path)) { |
| 1e0fbb47 Andy McCall ↶ |
724 |
Ok(_) => { |
| 725 |
counted(Op::Has); |
| 726 |
Ok(true) |
| 727 |
} |
| 728 |
Err(object_store::Error::NotFound { .. }) => { |
| 729 |
counted(Op::HasMissing); |
| 8b53b935 Andy McCall ↶ |
730 |
self.misses.missed(key); |
| 1e0fbb47 Andy McCall ↶ |
731 |
Ok(false) |
| 732 |
} |
| 8d803563 Andy McCall ↶ |
733 |
Err(e) => Err(self.map(e, key)), |
| 734 |
} |
| 735 |
} |
| 736 |
|
| 737 |
fn get(&self, key: &str) -> Result<Vec<u8>> { |
| 738 |
let path = self.path(key)?; |
| 1e0fbb47 Andy McCall ↶ |
739 |
counted(Op::Get); |
| 8d803563 Andy McCall ↶ |
740 |
self.block(async { |
| 741 |
let r = self.inner.get(&path).await?; |
| 742 |
r.bytes().await |
| 743 |
}) |
| 744 |
.map(|b| b.to_vec()) |
| 745 |
.map_err(|e| self.map(e, key)) |
| 746 |
} |
| 747 |
|
| 748 |
fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> { |
| 749 |
let path = self.path(key)?; |
| 750 |
if range.start > range.end { |
| 751 |
return Err(Error::Backend(format!( |
| 752 |
"{key}: bytes {}..{} is not a range", |
| 753 |
range.start, range.end |
| 754 |
))); |
| 755 |
} |
| 756 |
if range.is_empty() { |
| 757 |
return Ok(Vec::new()); |
| 758 |
} |
| 759 |
let want = (range.end - range.start) as usize; |
| 1e0fbb47 Andy McCall ↶ |
760 |
counted(Op::Range); |
| 8d803563 Andy McCall ↶ |
761 |
let bytes = self |
| 762 |
.block(self.inner.get_range(&path, range.clone())) |
| 763 |
.map_err(|e| self.map(e, key))?; |
| 764 |
if bytes.len() != want { |
| 765 |
return Err(Error::Backend(format!( |
| 766 |
"{key}: bytes {}..{} asked, {} returned: the object is shorter", |
| 767 |
range.start, |
| 768 |
range.end, |
| 769 |
bytes.len() |
| 770 |
))); |
| 771 |
} |
| 772 |
Ok(bytes.to_vec()) |
| 773 |
} |
| 774 |
|
| 775 |
fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> { |
| 776 |
let path = self.path(key)?; |
| 777 |
let opts = PutOptions { |
| 778 |
mode: PutMode::Create, |
| 779 |
..Default::default() |
| 780 |
}; |
| 1e0fbb47 Andy McCall ↶ |
781 |
counted(Op::Put); |
| 767ac6cd Andy McCall |
782 |
writing(&self.misses, &self.listings, key, || { |
| 783 |
self.block( |
| 784 |
self.inner |
| 785 |
.put_opts(&path, PutPayload::from(bytes.to_vec()), opts), |
| 786 |
) |
| 787 |
}) |
| 8d803563 Andy McCall ↶ |
788 |
.map(|_| ()) |
| 789 |
.map_err(|e| self.map(e, key)) |
| 790 |
} |
| 791 |
|
| 792 |
fn list(&self, prefix: &str) -> Result<Vec<Entry>> { |
| 87c94872 Andy McCall ↶ |
793 |
if let Some(kept) = self.listings.get(prefix) { |
| 30644f1e Andy McCall ↶ |
794 |
counted(Op::ListKept); |
| 87c94872 Andy McCall ↶ |
795 |
return Ok(kept); |
| 796 |
} |
| 8d803563 Andy McCall ↶ |
797 |
|
| 798 |
|
| 799 |
let dir = prefix.rsplit_once('/').map(|(d, _)| d).unwrap_or(""); |
| 800 |
let full_dir = match (self.prefix.as_str(), dir) { |
| 801 |
("", "") => None, |
| 802 |
("", d) => Some(d.to_string()), |
| 803 |
(p, "") => Some(p.to_string()), |
| 804 |
(p, d) => Some(format!("{p}/{d}")), |
| 805 |
}; |
| 806 |
let full_dir = full_dir |
| 807 |
.map(|d| ObjectPath::parse(&d).map_err(|e| Error::Backend(format!("{d}: {e}")))) |
| 808 |
.transpose()?; |
| 30644f1e Andy McCall ↶ |
809 |
counted(if Listings::kept(prefix) { |
| 810 |
Op::ListPacks |
| 811 |
} else { |
| 812 |
Op::List |
| 813 |
}); |
| 8d803563 Andy McCall ↶ |
814 |
let metas: Vec<object_store::ObjectMeta> = self |
| 815 |
.block(self.inner.list(full_dir.as_ref()).try_collect()) |
| 816 |
.map_err(|e| self.map(e, prefix))?; |
| 817 |
let strip = if self.prefix.is_empty() { |
| 818 |
String::new() |
| 819 |
} else { |
| 820 |
format!("{}/", self.prefix) |
| 821 |
}; |
| 822 |
let mut out: Vec<Entry> = metas |
| 823 |
.into_iter() |
| 824 |
.filter_map(|m| { |
| 825 |
let loc = m.location.to_string(); |
| 826 |
let key = loc.strip_prefix(&strip)?; |
| 827 |
key.starts_with(prefix).then(|| Entry { |
| 828 |
key: key.to_string(), |
| 829 |
size: m.size, |
| 64818938 Andy McCall ↶ |
830 |
modified_ms: u64::try_from(m.last_modified.timestamp_millis()).ok(), |
| 8d803563 Andy McCall ↶ |
831 |
}) |
| 832 |
}) |
| 833 |
.collect(); |
| 834 |
out.sort_by(|a, b| a.key.cmp(&b.key)); |
| 87c94872 Andy McCall ↶ |
835 |
self.listings.put(prefix, &out); |
| 8d803563 Andy McCall ↶ |
836 |
Ok(out) |
| 7e0e8e5a Andy McCall ↶ |
837 |
} |
| 838 |
|
| 839 |
|
| 840 |
fn stat(&self, key: &str) -> Result<Option<Entry>> { |
| 841 |
let path = self.path(key)?; |
| 842 |
counted(Op::Has); |
| 843 |
match self.block(self.inner.head(&path)) { |
| 844 |
Ok(m) => Ok(Some(Entry { |
| 845 |
key: key.to_string(), |
| 846 |
size: m.size, |
| 847 |
modified_ms: u64::try_from(m.last_modified.timestamp_millis()).ok(), |
| 848 |
})), |
| 849 |
Err(object_store::Error::NotFound { .. }) => Ok(None), |
| 850 |
Err(e) => Err(self.map(e, key)), |
| 851 |
} |
| 8d803563 Andy McCall ↶ |
852 |
} |
| 853 |
|
| 854 |
fn delete(&self, key: &str) -> Result<()> { |
| 855 |
let path = self.path(key)?; |
| 1e0fbb47 Andy McCall ↶ |
856 |
counted(Op::Delete); |
| 767ac6cd Andy McCall |
857 |
match writing(&self.misses, &self.listings, key, || { |
| 858 |
self.block(self.inner.delete(&path)) |
| 859 |
}) { |
| 8d803563 Andy McCall ↶ |
860 |
Ok(()) | Err(object_store::Error::NotFound { .. }) => Ok(()), |
| 861 |
Err(e) => Err(self.map(e, key)), |
| 862 |
} |
| 863 |
} |
| 864 |
|
| 865 |
fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> { |
| 866 |
let path = self.path(key)?; |
| ef7d4ea6 Andy McCall ↶ |
867 |
counted(if key == oxy_core::store::JOURNAL_KEY { |
| 868 |
Op::ReadJournal |
| 869 |
} else { |
| 870 |
Op::ReadRef |
| 871 |
}); |
| 8d803563 Andy McCall ↶ |
872 |
let got = self.block(async { |
| 873 |
let r = self.inner.get_opts(&path, GetOptions::default()).await?; |
| 874 |
let meta = r.meta.clone(); |
| 875 |
let bytes = r.bytes().await?; |
| 876 |
Ok::<_, object_store::Error>((bytes, meta)) |
| 877 |
}); |
| 878 |
match got { |
| 879 |
Ok((bytes, meta)) => Ok(Some(( |
| 880 |
bytes.to_vec(), |
| 881 |
Self::version_of(meta.e_tag, meta.version), |
| 882 |
))), |
| 883 |
Err(object_store::Error::NotFound { .. }) => Ok(None), |
| 884 |
Err(e) => Err(self.map(e, key)), |
| 885 |
} |
| 886 |
} |
| 887 |
|
| 888 |
fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> { |
| 889 |
let path = self.path(key)?; |
| 890 |
let mode = match expected { |
| 891 |
None => PutMode::Create, |
| 892 |
Some(v) => PutMode::Update(Self::update_version(v)), |
| 893 |
}; |
| 894 |
let opts = PutOptions { |
| 895 |
mode, |
| 896 |
..Default::default() |
| 897 |
}; |
| 1e0fbb47 Andy McCall ↶ |
898 |
counted(Op::WriteRef); |
| 767ac6cd Andy McCall |
899 |
let put = writing(&self.misses, &self.listings, key, || { |
| 900 |
self.block( |
| 8d803563 Andy McCall ↶ |
901 |
self.inner |
| 902 |
.put_opts(&path, PutPayload::from(bytes.to_vec()), opts), |
| 903 |
) |
| 767ac6cd Andy McCall |
904 |
}) |
| 905 |
.map_err(|e| match self.map(e, key) { |
| 906 |
|
| 907 |
|
| 908 |
Error::Exists(k) => Error::Conflict(k), |
| 909 |
other => other, |
| 910 |
})?; |
| 8d803563 Andy McCall ↶ |
911 |
Ok(Self::version_of(put.e_tag, put.version)) |
| 912 |
} |
| 913 |
} |
| 914 |
|
| 915 |
#[cfg(test)] |
| 916 |
mod tests { |
| 8b53b935 Andy McCall ↶ |
917 |
|
| 918 |
|
| 919 |
#[test] |
| 920 |
fn a_miss_is_remembered_until_written() { |
| 921 |
let m = super::Misses::default(); |
| 922 |
assert!(!m.known("loose/ab/cd")); |
| 923 |
m.missed("loose/ab/cd"); |
| 924 |
assert!(m.known("loose/ab/cd") && !m.known("loose/ab/ce")); |
| 925 |
m.written("loose/ab/cd"); |
| 926 |
assert!(!m.known("loose/ab/cd"), "a write forgets the miss"); |
| 927 |
m.missed("old"); |
| 928 |
m.0.lock() |
| 929 |
.unwrap() |
| 930 |
.insert("old".into(), std::time::Instant::now() - super::MISS_FOR); |
| 931 |
assert!(!m.known("old"), "a miss expires"); |
| 932 |
} |
| 1e0fbb47 Andy McCall ↶ |
933 |
|
| 934 |
|
| 87c94872 Andy McCall ↶ |
935 |
|
| 936 |
|
| 937 |
#[test] |
| 938 |
fn the_pack_listing_is_kept_until_a_pack_is_written() { |
| 939 |
let l = super::Listings::default(); |
| 940 |
let e = |k: &str| oxy_core::strata::Entry { |
| 941 |
key: k.into(), |
| 942 |
size: 1, |
| 943 |
modified_ms: None, |
| 944 |
}; |
| 945 |
l.put("pack-", &[e("pack-1.idx")]); |
| 946 |
assert_eq!(l.get("pack-").map(|v| v.len()), Some(1)); |
| 947 |
l.put("loose/", &[e("loose/ab")]); |
| 948 |
assert!(l.get("loose/").is_none(), "only the pack listing is kept"); |
| 949 |
l.written("loose/cd"); |
| 950 |
assert!(l.get("pack-").is_some(), "a loose object leaves it"); |
| 951 |
l.written("pack-2.pack"); |
| 952 |
assert!(l.get("pack-").is_none(), "a new pack forgets it"); |
| 953 |
l.put("pack-", &[e("pack-1.idx")]); |
| 954 |
l.0.lock().unwrap().get_mut("pack-").unwrap().0 -= super::MISS_FOR; |
| 955 |
assert!(l.get("pack-").is_none(), "and it ages out"); |
| 767ac6cd Andy McCall |
956 |
} |
| 957 |
|
| 958 |
|
| 959 |
|
| 960 |
|
| 961 |
#[test] |
| 962 |
fn a_listing_taken_during_a_write_is_forgotten_after_it() { |
| 963 |
let (m, l) = (super::Misses::default(), super::Listings::default()); |
| 964 |
let e = |k: &str| oxy_core::strata::Entry { |
| 965 |
key: k.into(), |
| 966 |
size: 1, |
| 967 |
modified_ms: None, |
| 968 |
}; |
| 969 |
l.put("pack-", &[e("pack-1.idx")]); |
| 970 |
let got = super::writing(&m, &l, "pack-2.idx", || { |
| 971 |
assert!(l.get("pack-").is_none(), "forgotten before it is sent"); |
| 972 |
|
| 973 |
l.put("pack-", &[e("pack-1.idx")]); |
| 974 |
m.missed("pack-2.idx"); |
| 975 |
7 |
| 976 |
}); |
| 977 |
assert_eq!(got, 7); |
| 978 |
assert!(l.get("pack-").is_none(), "the mid-request listing is gone"); |
| 979 |
assert!(!m.known("pack-2.idx"), "and the miss"); |
| 980 |
|
| 981 |
l.put("pack-", &[e("pack-1.idx")]); |
| 982 |
let r: std::result::Result<(), &str> = super::writing(&m, &l, "pack-1.idx", || { |
| 983 |
l.put("pack-", &[e("pack-1.idx")]); |
| 984 |
Err("network") |
| 985 |
}); |
| 986 |
assert!(r.is_err() && l.get("pack-").is_none()); |
| 987 |
|
| 988 |
l.put("pack-", &[e("pack-1.idx")]); |
| 989 |
super::writing(&m, &l, "loose/ab/cd", || ()); |
| 990 |
assert!(l.get("pack-").is_some()); |
| 87c94872 Andy McCall ↶ |
991 |
} |
| 992 |
|
| 1e0fbb47 Andy McCall ↶ |
993 |
#[test] |
| 994 |
fn stats_name_every_kind_of_request() { |
| 995 |
let names: Vec<&str> = super::stats().ops().map(|(n, _)| n).collect(); |
| 996 |
assert_eq!(names.len(), super::Op::ALL.len()); |
| 997 |
let mut unique = names.clone(); |
| 998 |
unique.sort(); |
| 999 |
unique.dedup(); |
| 1000 |
assert_eq!(unique.len(), names.len()); |
| 1001 |
assert!(names.contains(&"has_missing") && names.contains(&"range")); |
| 1002 |
} |
| 2ddd4716 Andy McCall ↶ |
1003 |
|
| 1004 |
|
| 1005 |
|
| 1006 |
#[test] |
| 1007 |
fn a_store_opened_again_shares_its_client() { |
| 1008 |
let creds = |k: &str| Credentials::Aws { |
| 1009 |
access_key_id: k.into(), |
| 1010 |
secret_access_key: "s".into(), |
| 1011 |
session_token: None, |
| 1012 |
}; |
| 1013 |
let url = "s3://oxy-shared-test/repos/a?region=eu-west-2"; |
| 1014 |
let before = super::stats().opens; |
| 1015 |
let a = super::shared(url, Some(creds("one"))).unwrap(); |
| 1016 |
let b = super::shared(url, Some(creds("one"))).unwrap(); |
| 1017 |
assert!(std::sync::Arc::ptr_eq(&a, &b)); |
| 1018 |
let other_keys = super::shared(url, Some(creds("two"))).unwrap(); |
| 1019 |
assert!( |
| 1020 |
!std::sync::Arc::ptr_eq(&a, &other_keys), |
| 1021 |
"other credentials, another client" |
| 1022 |
); |
| 1023 |
assert!( |
| 1024 |
super::stats().opens - before >= 2, |
| 1025 |
"two clients opened (other tests may open more)" |
| 1026 |
); |
| 1027 |
} |
| 1028 |
|
| c9369ad8 Andy McCall ↶ |
1029 |
#[tokio::test] |
| 1030 |
async fn a_store_can_be_used_from_inside_a_runtime() { |
| 1031 |
|
| 1032 |
|
| 1033 |
let store = CloudStore::open( |
| 1034 |
"s3://oxy-no-such-bucket/p?region=eu-west-2&endpoint=http://127.0.0.1:9", |
| 1035 |
) |
| 1036 |
.unwrap(); |
| 1037 |
assert_eq!(store.block(async { 7 }), 7); |
| 1038 |
drop(store); |
| 1039 |
} |
| 1040 |
|
| 7026bd7d Andy McCall ↶ |
1041 |
#[test] |
| 1042 |
fn credentials_round_trip_and_never_show_their_secret() { |
| 1043 |
let aws = Credentials::Aws { |
| 1044 |
access_key_id: "AKIDEXAMPLE".into(), |
| 1045 |
secret_access_key: "wJalrXUtnFEMI".into(), |
| 1046 |
session_token: None, |
| 1047 |
}; |
| 1048 |
assert_eq!(Credentials::from_json(&aws.to_json()).unwrap(), aws); |
| 1049 |
assert_eq!(format!("{aws:?}"), "Aws(AKIDEXAMPLE, secret hidden)"); |
| 1050 |
let az = Credentials::Azure { |
| 1051 |
tenant_id: "t".into(), |
| 1052 |
client_id: "c".into(), |
| 1053 |
client_secret: "s3cr3t".into(), |
| 1054 |
}; |
| 1055 |
assert_eq!(Credentials::from_json(&az.to_json()).unwrap(), az); |
| 1056 |
assert!(!format!("{az:?}").contains("s3cr3t")); |
| 1057 |
let key = r#"{"type":"service_account","client_email":"x@y","private_key":"k"}"#; |
| 1058 |
let gcp = Credentials::Gcp { |
| 1059 |
service_account_key: key.into(), |
| 1060 |
}; |
| 1061 |
assert_eq!(Credentials::from_json(&gcp.to_json()).unwrap(), gcp); |
| 1062 |
assert!(!format!("{gcp:?}").contains("private_key")); |
| 1063 |
for (bad, why) in [ |
| 1064 |
(r#"{"kind":"aws","access_key_id":"a"}"#, "secret_access_key"), |
| 1065 |
( |
| 1066 |
r#"{"kind":"gcp","service_account_key":"{\"type\":\"authorized_user\"}"}"#, |
| 1067 |
"not a service account key", |
| 1068 |
), |
| 1069 |
(r#"{"kind":"ftp"}"#, "aws, gcp or azure"), |
| 1070 |
("nope", "not JSON"), |
| 1071 |
] { |
| 1072 |
let e = Credentials::from_json(bad).unwrap_err().to_string(); |
| 1073 |
assert!(e.contains(why), "{bad}: {e}"); |
| 1074 |
} |
| 1075 |
let e = CloudStore::open_with("gs://bucket/p", Some(&aws)) |
| 1076 |
.err() |
| 1077 |
.unwrap() |
| 1078 |
.to_string(); |
| 1079 |
assert!(e.contains("are for s3:// stores"), "{e}"); |
| 1080 |
} |
| 1081 |
|
| 8d803563 Andy McCall ↶ |
1082 |
use super::*; |
| 1083 |
|
| 1084 |
#[test] |
| 1085 |
fn urls_parse_with_their_parameters() { |
| 1086 |
let u = |
| 1087 |
StoreUrl::parse("s3://my-bucket/repos/oxygen?profile=work®ion=eu-west-2").unwrap(); |
| 1088 |
assert_eq!(u.scheme, "s3"); |
| 1089 |
assert_eq!(u.host, "my-bucket"); |
| 1090 |
assert_eq!(u.path, "repos/oxygen"); |
| 1091 |
assert_eq!(u.param("profile"), Some("work")); |
| 1092 |
assert_eq!(u.param("region"), Some("eu-west-2")); |
| 1093 |
assert_eq!(u.plain(), "s3://my-bucket/repos/oxygen"); |
| 1094 |
let u = StoreUrl::parse("gs://bucket").unwrap(); |
| 1095 |
assert_eq!((u.host.as_str(), u.path.as_str()), ("bucket", "")); |
| 1096 |
assert_eq!(u.plain(), "gs://bucket"); |
| 1097 |
let u = StoreUrl::parse("az://acct/container/a/b/?endpoint=http%3A%2F%2F127.0.0.1%3A10000") |
| 1098 |
.unwrap(); |
| 1099 |
assert_eq!(u.path, "container/a/b"); |
| 1100 |
assert_eq!(u.param("endpoint"), Some("http://127.0.0.1:10000")); |
| 1101 |
assert!(StoreUrl::parse("s3://").is_err()); |
| 1102 |
assert!(StoreUrl::parse("nonsense").is_err()); |
| 1103 |
} |
| 1104 |
|
| 1105 |
#[test] |
| 1106 |
fn versions_carry_both_an_etag_and_a_generation() { |
| 1107 |
let v = CloudStore::version_of(Some("\"abc\"".into()), Some("17".into())); |
| 1108 |
let u = CloudStore::update_version(&v); |
| 1109 |
assert_eq!(u.e_tag.as_deref(), Some("\"abc\"")); |
| 1110 |
assert_eq!(u.version.as_deref(), Some("17")); |
| 1111 |
let u = CloudStore::update_version(&CloudStore::version_of(None, Some("3".into()))); |
| 1112 |
assert_eq!((u.e_tag, u.version.as_deref()), (None, Some("3"))); |
| 1113 |
} |
| 1114 |
} |