Strata: S3, Google Cloud Storage and Azure Blob Storage stores 8d80356371c5
A new crate, oxy-strata, with the stores that need an HTTP client and the providers' signing: s3://BUCKET/PREFIX, gs://BUCKET/PREFIX and az://ACCOUNT/CONTAINER/PREFIX, each a Store over the object_store crate (signing with ring, as the rest of oxy does, not aws-lc-rs). A URL may carry settings as query parameters (profile, region, endpoint, credentials), which is where oxy remote add will keep them. Credentials come from each provider's standard places. AWS: the environment, the profile's keys in ~/.aws/credentials, then what the AWS CLI can produce for the profile (SSO, login sessions, roles), then the instance's role; the region is taken from the URL, the environment or the profile, else asked of S3. Google: a key file named by GOOGLE_APPLICATION_CREDENTIALS or the URL, the application default credentials, then the instance. Azure: a service principal, an account key or a SAS token, then az login, then the managed identity. Errors say what to do next. oxy registers the stores at start. A version carries both an ETag and a generation, so one compare-and-swap serves all three providers. The tests run the conformance check and a repository round trip against each service that the environment names, and delete what they wrote; checked on the real S3, GCS and Azure. stored_url now keeps any URL with a scheme as written: it turned a bucket URL into a path. refs #63
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-03 22:04 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
8d80356371c5573ae038724d4adec6f3835e1d265c75ac1d9c7a0c82c49f2724- Depends on
- 135212de5370 2b5ee371060e 37c299cec358 5b9c6ad45f6e 750251b21a96 827281eed2ed 982864068a59 9f9e72b93e4e a70dce536476 c6aa5bd9d1c8 d02e545dc265 e993a086c13f fce44ae9ad29 ff0af1ee299d
- Depended on by
- 835ceb9b3ea7 135bdcadec4a ed931d1d1102 642da2636de2 4275ecb110f5 e02edccce98c b260109d27c5 82202a4d0025 6f06d9f912b4 ef7d4ea6f33b 767ac6cd0545 7e0e8e5ad882 30644f1ecb7b 87c948720343 e5f8eb2a2006 68dcf21fe055 f05b84d1ad2d efad40f9ac8e 8b53b935dd03 1e0fbb47ab90 2ddd471651cd e2a5edbf1395 d0bf2da15094 579afd107b9b c9369ad8b1b1 7026bd7dc5b8 d069701a4997 c71a5777509a 648189388f1a 2cd40ea1c04c 6cc9dea93092 f874dc8659c2
- In bookmarks
- agents-in-readme bare-hashes carried-not-removed carry-identical doctor-trust identical-sides keyless-tags main merge-remote-bookmark only-help-103 paths-for-resolutions-and-amend push-notes show-stat subrepo-pin-state switch-says
- In tags
- v0.10.0 v0.11.0 v0.4.0 v0.4.1 v0.4.2 v0.4.3 v0.4.4 v0.4.5 v0.4.6 v0.5.0 v0.5.1 v0.6.0 v0.7.0 v0.8.0 v0.9.0
- Edits
- 82 atoms, 10 tree operations, 13 files
Browse the tree at this change
added crates/oxy-strata/Cargo.toml +23 −0
| 1 | + | [package] |
|
| 2 | + | name = "oxy-strata" |
|
| 3 | + | version.workspace = true |
|
| 4 | + | edition.workspace = true |
|
| 5 | + | license.workspace = true |
|
| 6 | + | description = "Oxygen Strata: repositories kept on S3, Google Cloud Storage and Azure Blob Storage" |
|
| 7 | + | ||
| 8 | + | [dependencies] |
|
| 9 | + | oxy-core = { path = "../oxy-core" } |
|
| 10 | + | # The cloud stores, signing with ring (the crypto stack oxy already |
|
| 11 | + | # links), not aws-lc-rs, which needs a C toolchain and cmake to build. |
|
| 12 | + | object_store = { version = "0.14", default-features = false, features = ["aws-base", "gcp-base", "azure-base", "reqwest", "ring"] } |
|
| 13 | + | # object_store's reqwest needs a TLS stack: rustls, with its provider |
|
| 14 | + | # installed at run time (`rustls::crypto::ring`). A dependency's feature |
|
| 15 | + | # list cannot name another crate's feature, so reqwest is named here. |
|
| 16 | + | reqwest = { version = "0.13", default-features = false, features = ["rustls-no-provider"] } |
|
| 17 | + | rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } |
|
| 18 | + | tokio = { version = "1", default-features = false, features = ["rt"] } |
|
| 19 | + | futures = "0.3" |
|
| 20 | + | serde_json = "1" |
|
| 21 | + | ||
| 22 | + | [dev-dependencies] |
|
| 23 | + | tempfile = "3" |
added crates/oxy-strata/src/aws.rs +211 −0
| 1 | + | //! Amazon S3: `s3://BUCKET/PREFIX`. |
|
| 2 | + | //! |
|
| 3 | + | //! Credentials, in the order the AWS tools use them: |
|
| 4 | + | //! 1. `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (and |
|
| 5 | + | //! `AWS_SESSION_TOKEN`) in the environment, unless the URL names a |
|
| 6 | + | //! profile; |
|
| 7 | + | //! 2. the profile (`?profile=NAME`, else `AWS_PROFILE`, else `default`): |
|
| 8 | + | //! its keys from `~/.aws/credentials`; |
|
| 9 | + | //! 3. what the AWS CLI can produce for that profile |
|
| 10 | + | //! (`aws configure export-credentials`): SSO, login sessions, assumed |
|
| 11 | + | //! roles and credential processes, so anything that works for `aws` |
|
| 12 | + | //! works for `oxy`; |
|
| 13 | + | //! 4. the instance's or container's role, through the provider's own |
|
| 14 | + | //! lookup. |
|
| 15 | + | //! |
|
| 16 | + | //! The region comes from `?region=`, the environment, the profile, or is |
|
| 17 | + | //! asked of S3 itself; the endpoint (`?endpoint=`, for MinIO and other |
|
| 18 | + | //! S3-alikes) from the URL, the environment or the profile. |
|
| 19 | + | ||
| 20 | + | use crate::ini; |
|
| 21 | + | use crate::StoreUrl; |
|
| 22 | + | use object_store::aws::{AmazonS3, AmazonS3Builder, S3ConditionalPut}; |
|
| 23 | + | use object_store::ClientOptions; |
|
| 24 | + | use oxy_core::strata::{Error, Result}; |
|
| 25 | + | use std::path::PathBuf; |
|
| 26 | + | ||
| 27 | + | pub const HINT: &str = "set AWS_PROFILE (or add ?profile=NAME to the remote URL) to a profile in ~/.aws/credentials, run `aws sso login` or `aws login` for it, or set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY"; |
|
| 28 | + | ||
| 29 | + | struct Found { |
|
| 30 | + | key_id: String, |
|
| 31 | + | secret: String, |
|
| 32 | + | token: Option<String>, |
|
| 33 | + | } |
|
| 34 | + | ||
| 35 | + | fn env(name: &str) -> Option<String> { |
|
| 36 | + | std::env::var(name).ok().filter(|v| !v.is_empty()) |
|
| 37 | + | } |
|
| 38 | + | ||
| 39 | + | fn home() -> Option<PathBuf> { |
|
| 40 | + | env("HOME") |
|
| 41 | + | .or_else(|| env("USERPROFILE")) |
|
| 42 | + | .map(PathBuf::from) |
|
| 43 | + | } |
|
| 44 | + | ||
| 45 | + | fn read_ini(env_override: &str, default_rel: &str) -> ini::Sections { |
|
| 46 | + | let path = env(env_override) |
|
| 47 | + | .map(PathBuf::from) |
|
| 48 | + | .or_else(|| home().map(|h| h.join(default_rel))); |
|
| 49 | + | path.and_then(|p| std::fs::read_to_string(p).ok()) |
|
| 50 | + | .map(|t| ini::parse(&t)) |
|
| 51 | + | .unwrap_or_default() |
|
| 52 | + | } |
|
| 53 | + | ||
| 54 | + | /// A profile's section in `~/.aws/config`: `[profile NAME]`, or |
|
| 55 | + | /// `[default]`; `~/.aws/credentials` uses `[NAME]` for both. |
|
| 56 | + | fn config_section<'a>( |
|
| 57 | + | config: &'a ini::Sections, |
|
| 58 | + | profile: &str, |
|
| 59 | + | ) -> Option<&'a std::collections::BTreeMap<String, String>> { |
|
| 60 | + | if profile == "default" { |
|
| 61 | + | config.get("default") |
|
| 62 | + | } else { |
|
| 63 | + | config |
|
| 64 | + | .get(&format!("profile {profile}")) |
|
| 65 | + | .or_else(|| config.get(profile)) |
|
| 66 | + | } |
|
| 67 | + | } |
|
| 68 | + | ||
| 69 | + | /// Ask the AWS CLI for the profile's credentials, if it is installed and |
|
| 70 | + | /// knows the profile. |
|
| 71 | + | fn from_cli(profile: &str) -> Option<Found> { |
|
| 72 | + | let out = std::process::Command::new("aws") |
|
| 73 | + | .args([ |
|
| 74 | + | "configure", |
|
| 75 | + | "export-credentials", |
|
| 76 | + | "--profile", |
|
| 77 | + | profile, |
|
| 78 | + | "--format", |
|
| 79 | + | "process", |
|
| 80 | + | ]) |
|
| 81 | + | .stdin(std::process::Stdio::null()) |
|
| 82 | + | .stderr(std::process::Stdio::null()) |
|
| 83 | + | .output() |
|
| 84 | + | .ok()?; |
|
| 85 | + | if !out.status.success() { |
|
| 86 | + | return None; |
|
| 87 | + | } |
|
| 88 | + | let v: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?; |
|
| 89 | + | Some(Found { |
|
| 90 | + | key_id: v.get("AccessKeyId")?.as_str()?.to_string(), |
|
| 91 | + | secret: v.get("SecretAccessKey")?.as_str()?.to_string(), |
|
| 92 | + | token: v |
|
| 93 | + | .get("SessionToken") |
|
| 94 | + | .and_then(|t| t.as_str()) |
|
| 95 | + | .map(str::to_string), |
|
| 96 | + | }) |
|
| 97 | + | } |
|
| 98 | + | ||
| 99 | + | pub fn open(url: &StoreUrl, runtime: &tokio::runtime::Runtime) -> Result<AmazonS3> { |
|
| 100 | + | let bucket = url.host.clone(); |
|
| 101 | + | let explicit_profile = url.param("profile").map(str::to_string); |
|
| 102 | + | let profile = explicit_profile |
|
| 103 | + | .clone() |
|
| 104 | + | .or_else(|| env("AWS_PROFILE")) |
|
| 105 | + | .unwrap_or_else(|| "default".into()); |
|
| 106 | + | let config = read_ini("AWS_CONFIG_FILE", ".aws/config"); |
|
| 107 | + | let creds = read_ini("AWS_SHARED_CREDENTIALS_FILE", ".aws/credentials"); |
|
| 108 | + | let section = config_section(&config, &profile); |
|
| 109 | + | let known_profile = section.is_some() || creds.contains_key(&profile); |
|
| 110 | + | ||
| 111 | + | // from_env picks up the environment's keys, region, endpoint, and the |
|
| 112 | + | // container and instance roles; what follows fills in or overrides. |
|
| 113 | + | let mut b = AmazonS3Builder::from_env() |
|
| 114 | + | .with_bucket_name(&bucket) |
|
| 115 | + | .with_conditional_put(S3ConditionalPut::ETagMatch); |
|
| 116 | + | ||
| 117 | + | let env_keys = match (env("AWS_ACCESS_KEY_ID"), env("AWS_SECRET_ACCESS_KEY")) { |
|
| 118 | + | (Some(k), Some(s)) if explicit_profile.is_none() => Some(Found { |
|
| 119 | + | key_id: k, |
|
| 120 | + | secret: s, |
|
| 121 | + | token: env("AWS_SESSION_TOKEN"), |
|
| 122 | + | }), |
|
| 123 | + | _ => None, |
|
| 124 | + | }; |
|
| 125 | + | let found = env_keys |
|
| 126 | + | .or_else(|| { |
|
| 127 | + | let c = creds.get(&profile)?; |
|
| 128 | + | Some(Found { |
|
| 129 | + | key_id: c.get("aws_access_key_id")?.clone(), |
|
| 130 | + | secret: c.get("aws_secret_access_key")?.clone(), |
|
| 131 | + | token: c.get("aws_session_token").cloned(), |
|
| 132 | + | }) |
|
| 133 | + | }) |
|
| 134 | + | .or_else(|| { |
|
| 135 | + | if known_profile { |
|
| 136 | + | from_cli(&profile) |
|
| 137 | + | } else { |
|
| 138 | + | None |
|
| 139 | + | } |
|
| 140 | + | }); |
|
| 141 | + | match found { |
|
| 142 | + | Some(f) => { |
|
| 143 | + | b = b |
|
| 144 | + | .with_access_key_id(f.key_id) |
|
| 145 | + | .with_secret_access_key(f.secret); |
|
| 146 | + | if let Some(t) = f.token { |
|
| 147 | + | b = b.with_token(t); |
|
| 148 | + | } |
|
| 149 | + | } |
|
| 150 | + | None if explicit_profile.is_some() && !known_profile => { |
|
| 151 | + | return Err(Error::Backend(format!( |
|
| 152 | + | "{}: profile {profile:?} is not in ~/.aws/config or ~/.aws/credentials: {HINT}", |
|
| 153 | + | url.plain() |
|
| 154 | + | ))); |
|
| 155 | + | } |
|
| 156 | + | None => {} |
|
| 157 | + | } |
|
| 158 | + | ||
| 159 | + | let endpoint = url |
|
| 160 | + | .param("endpoint") |
|
| 161 | + | .map(str::to_string) |
|
| 162 | + | .or_else(|| env("AWS_ENDPOINT_URL_S3")) |
|
| 163 | + | .or_else(|| env("AWS_ENDPOINT_URL")) |
|
| 164 | + | .or_else(|| env("AWS_ENDPOINT")) |
|
| 165 | + | .or_else(|| section.and_then(|s| s.get("endpoint_url").cloned())); |
|
| 166 | + | if let Some(e) = &endpoint { |
|
| 167 | + | b = b |
|
| 168 | + | .with_endpoint(e.clone()) |
|
| 169 | + | .with_allow_http(e.starts_with("http://")); |
|
| 170 | + | // S3-alikes serve buckets by path, not by host name. |
|
| 171 | + | b = b.with_virtual_hosted_style_request(false); |
|
| 172 | + | } |
|
| 173 | + | ||
| 174 | + | let region = url |
|
| 175 | + | .param("region") |
|
| 176 | + | .map(str::to_string) |
|
| 177 | + | .or_else(|| env("AWS_REGION")) |
|
| 178 | + | .or_else(|| env("AWS_DEFAULT_REGION")) |
|
| 179 | + | .or_else(|| section.and_then(|s| s.get("region").cloned())); |
|
| 180 | + | let region = match region { |
|
| 181 | + | Some(r) => r, |
|
| 182 | + | None if endpoint.is_some() => "us-east-1".into(), |
|
| 183 | + | None => runtime |
|
| 184 | + | .block_on(object_store::aws::resolve_bucket_region(&bucket, &ClientOptions::new())) |
|
| 185 | + | .map_err(|e| { |
|
| 186 | + | Error::Backend(format!( |
|
| 187 | + | "{}: no region was given and S3 did not say which region holds the bucket ({e}): add ?region=NAME to the remote URL, set AWS_REGION, or put region in the profile", |
|
| 188 | + | url.plain() |
|
| 189 | + | )) |
|
| 190 | + | })?, |
|
| 191 | + | }; |
|
| 192 | + | b = b.with_region(region); |
|
| 193 | + | ||
| 194 | + | b.build() |
|
| 195 | + | .map_err(|e| Error::Backend(format!("{}: {e}: {HINT}", url.plain()))) |
|
| 196 | + | } |
|
| 197 | + | ||
| 198 | + | #[cfg(test)] |
|
| 199 | + | mod tests { |
|
| 200 | + | use super::*; |
|
| 201 | + | ||
| 202 | + | #[test] |
|
| 203 | + | fn profile_sections_are_found_under_either_heading() { |
|
| 204 | + | let config = |
|
| 205 | + | ini::parse("[default]\nregion = a\n[profile work]\nregion = b\n[plain]\nregion = c\n"); |
|
| 206 | + | assert_eq!(config_section(&config, "default").unwrap()["region"], "a"); |
|
| 207 | + | assert_eq!(config_section(&config, "work").unwrap()["region"], "b"); |
|
| 208 | + | assert_eq!(config_section(&config, "plain").unwrap()["region"], "c"); |
|
| 209 | + | assert!(config_section(&config, "none").is_none()); |
|
| 210 | + | } |
|
| 211 | + | } |
added crates/oxy-strata/src/azure.rs +65 −0
| 1 | + | //! Azure Blob Storage: `az://ACCOUNT/CONTAINER/PREFIX`. |
|
| 2 | + | //! |
|
| 3 | + | //! Credentials: a service principal (`AZURE_CLIENT_ID`, |
|
| 4 | + | //! `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID`, as the Azure SDKs name them, |
|
| 5 | + | //! or the `AZURE_STORAGE_*` spellings), an account key |
|
| 6 | + | //! (`AZURE_STORAGE_ACCOUNT_KEY`), a SAS token (`AZURE_STORAGE_SAS_TOKEN`), |
|
| 7 | + | //! else what `az login` left behind, else the machine's managed |
|
| 8 | + | //! identity. `?endpoint=URL` points at an emulator. |
|
| 9 | + | ||
| 10 | + | use crate::StoreUrl; |
|
| 11 | + | use object_store::azure::{MicrosoftAzure, MicrosoftAzureBuilder}; |
|
| 12 | + | use oxy_core::strata::{Error, Result}; |
|
| 13 | + | ||
| 14 | + | pub const HINT: &str = "run `az login`, or set AZURE_CLIENT_ID, AZURE_CLIENT_SECRET and AZURE_TENANT_ID for a service principal, or AZURE_STORAGE_ACCOUNT_KEY"; |
|
| 15 | + | ||
| 16 | + | fn env(name: &str) -> Option<String> { |
|
| 17 | + | std::env::var(name).ok().filter(|v| !v.is_empty()) |
|
| 18 | + | } |
|
| 19 | + | ||
| 20 | + | /// The store and the prefix under the container. |
|
| 21 | + | pub fn open(url: &StoreUrl) -> Result<(MicrosoftAzure, String)> { |
|
| 22 | + | let (container, prefix) = match url.path.split_once('/') { |
|
| 23 | + | Some((c, p)) => (c.to_string(), p.to_string()), |
|
| 24 | + | None if !url.path.is_empty() => (url.path.clone(), String::new()), |
|
| 25 | + | None => { |
|
| 26 | + | return Err(Error::Backend(format!( |
|
| 27 | + | "{}: an Azure store URL names the account and the container: az://ACCOUNT/CONTAINER/PREFIX", |
|
| 28 | + | url.plain() |
|
| 29 | + | ))) |
|
| 30 | + | } |
|
| 31 | + | }; |
|
| 32 | + | let mut b = MicrosoftAzureBuilder::from_env() |
|
| 33 | + | .with_account(&url.host) |
|
| 34 | + | .with_container_name(&container); |
|
| 35 | + | let principal = ( |
|
| 36 | + | env("AZURE_CLIENT_ID").or_else(|| env("AZURE_STORAGE_CLIENT_ID")), |
|
| 37 | + | env("AZURE_CLIENT_SECRET").or_else(|| env("AZURE_STORAGE_CLIENT_SECRET")), |
|
| 38 | + | env("AZURE_TENANT_ID").or_else(|| env("AZURE_STORAGE_TENANT_ID")), |
|
| 39 | + | ); |
|
| 40 | + | let mut explicit = false; |
|
| 41 | + | if let (Some(id), Some(secret), Some(tenant)) = principal { |
|
| 42 | + | b = b.with_client_secret_authorization(id, secret, tenant); |
|
| 43 | + | explicit = true; |
|
| 44 | + | } |
|
| 45 | + | if env("AZURE_STORAGE_ACCOUNT_KEY").is_some() |
|
| 46 | + | || env("AZURE_STORAGE_ACCESS_KEY").is_some() |
|
| 47 | + | || env("AZURE_STORAGE_SAS_TOKEN").is_some() |
|
| 48 | + | || env("AZURE_STORAGE_SAS_KEY").is_some() |
|
| 49 | + | { |
|
| 50 | + | explicit = true; |
|
| 51 | + | } |
|
| 52 | + | if let Some(endpoint) = url.param("endpoint") { |
|
| 53 | + | b = b |
|
| 54 | + | .with_endpoint(endpoint.to_string()) |
|
| 55 | + | .with_allow_http(endpoint.starts_with("http://")); |
|
| 56 | + | } |
|
| 57 | + | if !explicit { |
|
| 58 | + | // What `az login` left behind; the managed identity comes after. |
|
| 59 | + | b = b.with_use_azure_cli(true); |
|
| 60 | + | } |
|
| 61 | + | let store = b |
|
| 62 | + | .build() |
|
| 63 | + | .map_err(|e| Error::Backend(format!("{}: {e}: {HINT}", url.plain())))?; |
|
| 64 | + | Ok((store, prefix)) |
|
| 65 | + | } |
added crates/oxy-strata/src/gcp.rs +61 −0
| 1 | + | //! Google Cloud Storage: `gs://BUCKET/PREFIX`. |
|
| 2 | + | //! |
|
| 3 | + | //! Credentials, in Google's own order: a service account key file named |
|
| 4 | + | //! by `?credentials=PATH` or `GOOGLE_APPLICATION_CREDENTIALS` (which may |
|
| 5 | + | //! also hold application default credentials), the key or path in |
|
| 6 | + | //! `GOOGLE_SERVICE_ACCOUNT*`, then the application default credentials |
|
| 7 | + | //! `gcloud auth application-default login` wrote, then the instance's |
|
| 8 | + | //! service account. `?endpoint=URL` points at an emulator, and |
|
| 9 | + | //! `?anonymous=1` skips signing for one. |
|
| 10 | + | ||
| 11 | + | use crate::StoreUrl; |
|
| 12 | + | use object_store::gcp::{GoogleCloudStorage, GoogleCloudStorageBuilder}; |
|
| 13 | + | use oxy_core::strata::{Error, Result}; |
|
| 14 | + | ||
| 15 | + | pub const HINT: &str = "run `gcloud auth application-default login`, or set GOOGLE_APPLICATION_CREDENTIALS (or add ?credentials=PATH to the remote URL) to a service account key file"; |
|
| 16 | + | ||
| 17 | + | /// Whether a credentials file is a service account key, as opposed to |
|
| 18 | + | /// application default credentials. |
|
| 19 | + | fn is_service_account_key(path: &str) -> bool { |
|
| 20 | + | std::fs::read_to_string(path) |
|
| 21 | + | .ok() |
|
| 22 | + | .and_then(|t| serde_json::from_str::<serde_json::Value>(&t).ok()) |
|
| 23 | + | .and_then(|v| { |
|
| 24 | + | v.get("type") |
|
| 25 | + | .and_then(|t| t.as_str()) |
|
| 26 | + | .map(|t| t == "service_account") |
|
| 27 | + | }) |
|
| 28 | + | .unwrap_or(false) |
|
| 29 | + | } |
|
| 30 | + | ||
| 31 | + | pub fn open(url: &StoreUrl) -> Result<GoogleCloudStorage> { |
|
| 32 | + | let mut b = GoogleCloudStorageBuilder::from_env().with_bucket_name(&url.host); |
|
| 33 | + | let file = url.param("credentials").map(str::to_string).or_else(|| { |
|
| 34 | + | std::env::var("GOOGLE_APPLICATION_CREDENTIALS") |
|
| 35 | + | .ok() |
|
| 36 | + | .filter(|v| !v.is_empty()) |
|
| 37 | + | }); |
|
| 38 | + | if let Some(path) = file { |
|
| 39 | + | if !std::path::Path::new(&path).is_file() { |
|
| 40 | + | return Err(Error::Backend(format!( |
|
| 41 | + | "{}: the credentials file {path} does not exist: {HINT}", |
|
| 42 | + | url.plain() |
|
| 43 | + | ))); |
|
| 44 | + | } |
|
| 45 | + | b = if is_service_account_key(&path) { |
|
| 46 | + | b.with_service_account_path(path) |
|
| 47 | + | } else { |
|
| 48 | + | b.with_application_credentials(path) |
|
| 49 | + | }; |
|
| 50 | + | } |
|
| 51 | + | if let Some(endpoint) = url.param("endpoint") { |
|
| 52 | + | b = b.with_base_url(endpoint); |
|
| 53 | + | } |
|
| 54 | + | if url.param("anonymous").is_some_and(|v| v != "0") { |
|
| 55 | + | // An emulator checks no token, but the client would still ask the |
|
| 56 | + | // metadata server for one: give it a token to send. |
|
| 57 | + | b = b.with_bearer_token("anonymous").with_skip_signature(true); |
|
| 58 | + | } |
|
| 59 | + | b.build() |
|
| 60 | + | .map_err(|e| Error::Backend(format!("{}: {e}: {HINT}", url.plain()))) |
|
| 61 | + | } |
added crates/oxy-strata/src/ini.rs +42 −0
| 1 | + | //! The INI files the AWS tools keep (`~/.aws/credentials`, `~/.aws/config`): |
|
| 2 | + | //! `[section]` headings and `key = value` lines, nothing more. |
|
| 3 | + | ||
| 4 | + | use std::collections::BTreeMap; |
|
| 5 | + | ||
| 6 | + | pub type Sections = BTreeMap<String, BTreeMap<String, String>>; |
|
| 7 | + | ||
| 8 | + | pub fn parse(text: &str) -> Sections { |
|
| 9 | + | let mut out = Sections::new(); |
|
| 10 | + | let mut current: Option<String> = None; |
|
| 11 | + | for raw in text.lines() { |
|
| 12 | + | let line = raw.trim(); |
|
| 13 | + | if line.is_empty() || line.starts_with('#') || line.starts_with(';') { |
|
| 14 | + | continue; |
|
| 15 | + | } |
|
| 16 | + | if let Some(name) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) { |
|
| 17 | + | current = Some(name.trim().to_string()); |
|
| 18 | + | out.entry(name.trim().to_string()).or_default(); |
|
| 19 | + | continue; |
|
| 20 | + | } |
|
| 21 | + | if let (Some(section), Some((k, v))) = (¤t, line.split_once('=')) { |
|
| 22 | + | out.entry(section.clone()) |
|
| 23 | + | .or_default() |
|
| 24 | + | .insert(k.trim().to_string(), v.trim().to_string()); |
|
| 25 | + | } |
|
| 26 | + | } |
|
| 27 | + | out |
|
| 28 | + | } |
|
| 29 | + | ||
| 30 | + | #[cfg(test)] |
|
| 31 | + | mod tests { |
|
| 32 | + | use super::parse; |
|
| 33 | + | ||
| 34 | + | #[test] |
|
| 35 | + | fn sections_and_keys() { |
|
| 36 | + | let s = parse("[default]\nregion = eu-west-2\n\n# a comment\n[profile work]\nregion=us-east-1\nsso_start_url = https://x\n"); |
|
| 37 | + | assert_eq!(s["default"]["region"], "eu-west-2"); |
|
| 38 | + | assert_eq!(s["profile work"]["region"], "us-east-1"); |
|
| 39 | + | assert_eq!(s["profile work"]["sso_start_url"], "https://x"); |
|
| 40 | + | assert!(parse("").is_empty()); |
|
| 41 | + | } |
|
| 42 | + | } |
added crates/oxy-strata/src/lib.rs +436 −0
| 1 | + | //! Oxygen Strata: repositories kept on cloud object storage (docs/stores.md). |
|
| 2 | + | //! |
|
| 3 | + | //! This crate gives `oxy` the stores that need an HTTP client and the |
|
| 4 | + | //! providers' signing: Amazon S3 (`s3://BUCKET/PREFIX`), Google Cloud |
|
| 5 | + | //! Storage (`gs://BUCKET/PREFIX`) and Azure Blob Storage |
|
| 6 | + | //! (`az://ACCOUNT/CONTAINER/PREFIX`), each as a [`Store`] over the |
|
| 7 | + | //! `object_store` crate. Credentials come from each provider's standard |
|
| 8 | + | //! places, with nothing oxy-specific to set up; a URL may carry settings |
|
| 9 | + | //! as query parameters (`?profile=work®ion=eu-west-2`), which is where |
|
| 10 | + | //! `oxy remote add --profile` keeps them. |
|
| 11 | + | //! |
|
| 12 | + | //! [`register`] hands these stores to oxy-core's remote opener; a program |
|
| 13 | + | //! that links this crate calls it once at start. |
|
| 14 | + | ||
| 15 | + | mod aws; |
|
| 16 | + | mod azure; |
|
| 17 | + | mod gcp; |
|
| 18 | + | mod ini; |
|
| 19 | + | ||
| 20 | + | use futures::TryStreamExt; |
|
| 21 | + | use object_store::path::Path as ObjectPath; |
|
| 22 | + | use object_store::{ |
|
| 23 | + | GetOptions, ObjectStore, ObjectStoreExt, PutMode, PutOptions, PutPayload, UpdateVersion, |
|
| 24 | + | }; |
|
| 25 | + | use oxy_core::strata::{check_key, Entry, Error, Result, Store, Version}; |
|
| 26 | + | use std::collections::BTreeMap; |
|
| 27 | + | use std::future::Future; |
|
| 28 | + | use std::ops::Range; |
|
| 29 | + | use std::sync::{Arc, Mutex}; |
|
| 30 | + | ||
| 31 | + | /// Make the cloud stores available to `oxy_core::protocol::open_remote`. |
|
| 32 | + | /// Safe to call more than once. |
|
| 33 | + | pub fn register() { |
|
| 34 | + | // object_store's reqwest is built with rustls and no bundled crypto |
|
| 35 | + | // provider; oxy links ring already, so that is the one to install. |
|
| 36 | + | let _ = rustls::crypto::ring::default_provider().install_default(); |
|
| 37 | + | oxy_core::protocol::register_store_opener(open_url); |
|
| 38 | + | } |
|
| 39 | + | ||
| 40 | + | fn open_url( |
|
| 41 | + | url: &str, |
|
| 42 | + | _options: &oxy_core::protocol::RemoteOptions, |
|
| 43 | + | ) -> Option<Result<Box<dyn Store>>> { |
|
| 44 | + | let scheme = url.split_once("://")?.0; |
|
| 45 | + | if !matches!(scheme, "s3" | "gs" | "az") { |
|
| 46 | + | return None; |
|
| 47 | + | } |
|
| 48 | + | Some(CloudStore::open(url).map(|s| Box::new(s) as Box<dyn Store>)) |
|
| 49 | + | } |
|
| 50 | + | ||
| 51 | + | /// A parsed store URL: `SCHEME://HOST/PATH?PARAMS`. |
|
| 52 | + | #[derive(Clone, Debug, PartialEq, Eq)] |
|
| 53 | + | pub struct StoreUrl { |
|
| 54 | + | pub scheme: String, |
|
| 55 | + | /// The bucket (S3, GCS) or the storage account (Azure). |
|
| 56 | + | pub host: String, |
|
| 57 | + | /// The path under the host, without leading or trailing slashes; for |
|
| 58 | + | /// Azure the container is its first segment. |
|
| 59 | + | pub path: String, |
|
| 60 | + | pub params: BTreeMap<String, String>, |
|
| 61 | + | } |
|
| 62 | + | ||
| 63 | + | impl StoreUrl { |
|
| 64 | + | pub fn parse(url: &str) -> Result<StoreUrl> { |
|
| 65 | + | let bad = |why: &str| Error::Backend(format!("{url}: {why}")); |
|
| 66 | + | let (scheme, rest) = url.split_once("://").ok_or_else(|| bad("not a URL"))?; |
|
| 67 | + | let (rest, query) = match rest.split_once('?') { |
|
| 68 | + | Some((r, q)) => (r, Some(q)), |
|
| 69 | + | None => (rest, None), |
|
| 70 | + | }; |
|
| 71 | + | let (host, path) = match rest.split_once('/') { |
|
| 72 | + | Some((h, p)) => (h, p.trim_matches('/')), |
|
| 73 | + | None => (rest, ""), |
|
| 74 | + | }; |
|
| 75 | + | if host.is_empty() { |
|
| 76 | + | return Err(bad("names no bucket")); |
|
| 77 | + | } |
|
| 78 | + | let mut params = BTreeMap::new(); |
|
| 79 | + | for pair in query.unwrap_or("").split('&').filter(|p| !p.is_empty()) { |
|
| 80 | + | let (k, v) = pair.split_once('=').unwrap_or((pair, "")); |
|
| 81 | + | params.insert(k.to_string(), percent_decode(v)); |
|
| 82 | + | } |
|
| 83 | + | Ok(StoreUrl { |
|
| 84 | + | scheme: scheme.to_string(), |
|
| 85 | + | host: host.to_string(), |
|
| 86 | + | path: path.to_string(), |
|
| 87 | + | params, |
|
| 88 | + | }) |
|
| 89 | + | } |
|
| 90 | + | ||
| 91 | + | /// The URL without its parameters, for messages. |
|
| 92 | + | pub fn plain(&self) -> String { |
|
| 93 | + | if self.path.is_empty() { |
|
| 94 | + | format!("{}://{}", self.scheme, self.host) |
|
| 95 | + | } else { |
|
| 96 | + | format!("{}://{}/{}", self.scheme, self.host, self.path) |
|
| 97 | + | } |
|
| 98 | + | } |
|
| 99 | + | ||
| 100 | + | pub fn param(&self, name: &str) -> Option<&str> { |
|
| 101 | + | self.params.get(name).map(String::as_str) |
|
| 102 | + | } |
|
| 103 | + | } |
|
| 104 | + | ||
| 105 | + | fn percent_decode(s: &str) -> String { |
|
| 106 | + | let bytes = s.as_bytes(); |
|
| 107 | + | let mut out = Vec::with_capacity(bytes.len()); |
|
| 108 | + | let mut i = 0; |
|
| 109 | + | while i < bytes.len() { |
|
| 110 | + | if bytes[i] == b'%' && i + 2 < bytes.len() { |
|
| 111 | + | if let Ok(v) = u8::from_str_radix(&s[i + 1..i + 3], 16) { |
|
| 112 | + | out.push(v); |
|
| 113 | + | i += 3; |
|
| 114 | + | continue; |
|
| 115 | + | } |
|
| 116 | + | } |
|
| 117 | + | out.push(bytes[i]); |
|
| 118 | + | i += 1; |
|
| 119 | + | } |
|
| 120 | + | String::from_utf8_lossy(&out).into_owned() |
|
| 121 | + | } |
|
| 122 | + | ||
| 123 | + | /// A store on a cloud provider: the provider's client, the prefix every |
|
| 124 | + | /// key goes under, and a runtime to run the client on. |
|
| 125 | + | pub struct CloudStore { |
|
| 126 | + | inner: Arc<dyn ObjectStore>, |
|
| 127 | + | prefix: String, |
|
| 128 | + | name: String, |
|
| 129 | + | hint: &'static str, |
|
| 130 | + | runtime: tokio::runtime::Runtime, |
|
| 131 | + | /// One call at a time: the runtime runs on the calling thread. |
|
| 132 | + | turn: Mutex<()>, |
|
| 133 | + | } |
|
| 134 | + | ||
| 135 | + | impl CloudStore { |
|
| 136 | + | /// Open the store a URL names. Nothing is sent yet: credentials are |
|
| 137 | + | /// resolved here, but the bucket is first spoken to on the first call. |
|
| 138 | + | pub fn open(url: &str) -> Result<CloudStore> { |
|
| 139 | + | let parsed = StoreUrl::parse(url)?; |
|
| 140 | + | let runtime = tokio::runtime::Builder::new_current_thread() |
|
| 141 | + | .enable_all() |
|
| 142 | + | .build() |
|
| 143 | + | .map_err(|e| Error::Backend(format!("starting the HTTP runtime: {e}")))?; |
|
| 144 | + | let (inner, prefix, hint): (Arc<dyn ObjectStore>, String, &'static str) = |
|
| 145 | + | match parsed.scheme.as_str() { |
|
| 146 | + | "s3" => ( |
|
| 147 | + | Arc::new(aws::open(&parsed, &runtime)?), |
|
| 148 | + | parsed.path.clone(), |
|
| 149 | + | aws::HINT, |
|
| 150 | + | ), |
|
| 151 | + | "gs" => ( |
|
| 152 | + | Arc::new(gcp::open(&parsed)?), |
|
| 153 | + | parsed.path.clone(), |
|
| 154 | + | gcp::HINT, |
|
| 155 | + | ), |
|
| 156 | + | "az" => { |
|
| 157 | + | let (store, prefix) = azure::open(&parsed)?; |
|
| 158 | + | (Arc::new(store), prefix, azure::HINT) |
|
| 159 | + | } |
|
| 160 | + | other => { |
|
| 161 | + | return Err(Error::Backend(format!( |
|
| 162 | + | "{url}: {other}:// is not a store oxy knows (s3://, gs://, az://, dir://)" |
|
| 163 | + | ))) |
|
| 164 | + | } |
|
| 165 | + | }; |
|
| 166 | + | Ok(CloudStore { |
|
| 167 | + | inner, |
|
| 168 | + | prefix, |
|
| 169 | + | name: parsed.plain(), |
|
| 170 | + | hint, |
|
| 171 | + | runtime, |
|
| 172 | + | turn: Mutex::new(()), |
|
| 173 | + | }) |
|
| 174 | + | } |
|
| 175 | + | ||
| 176 | + | fn block<F: Future>(&self, f: F) -> F::Output { |
|
| 177 | + | let _turn = self.turn.lock().unwrap_or_else(|p| p.into_inner()); |
|
| 178 | + | self.runtime.block_on(f) |
|
| 179 | + | } |
|
| 180 | + | ||
| 181 | + | fn path(&self, key: &str) -> Result<ObjectPath> { |
|
| 182 | + | check_key(key)?; |
|
| 183 | + | let full = if self.prefix.is_empty() { |
|
| 184 | + | key.to_string() |
|
| 185 | + | } else { |
|
| 186 | + | format!("{}/{key}", self.prefix) |
|
| 187 | + | }; |
|
| 188 | + | ObjectPath::parse(&full).map_err(|e| Error::Backend(format!("{full}: {e}"))) |
|
| 189 | + | } |
|
| 190 | + | ||
| 191 | + | /// Our error for the provider's, with what to do next where it helps. |
|
| 192 | + | fn map(&self, e: object_store::Error, key: &str) -> Error { |
|
| 193 | + | use object_store::Error as E; |
|
| 194 | + | match e { |
|
| 195 | + | E::NotFound { .. } => Error::NotFound(key.to_string()), |
|
| 196 | + | E::AlreadyExists { .. } => Error::Exists(key.to_string()), |
|
| 197 | + | E::Precondition { .. } | E::NotModified { .. } => Error::Conflict(key.to_string()), |
|
| 198 | + | other => { |
|
| 199 | + | let text = other.to_string(); |
|
| 200 | + | let lower = text.to_ascii_lowercase(); |
|
| 201 | + | let advice = if lower.contains("403") |
|
| 202 | + | || lower.contains("accessdenied") |
|
| 203 | + | || lower.contains("forbidden") |
|
| 204 | + | || lower.contains("authorizationpermissionmismatch") |
|
| 205 | + | { |
|
| 206 | + | format!( |
|
| 207 | + | ": access denied; the credentials in use may not do this on {}: {}", |
|
| 208 | + | self.name, self.hint |
|
| 209 | + | ) |
|
| 210 | + | } else if lower.contains("credential") || lower.contains("no valid credentials") { |
|
| 211 | + | format!(": no usable credentials: {}", self.hint) |
|
| 212 | + | } else if lower.contains("nosuchbucket") |
|
| 213 | + | || lower.contains("bucketnotfound") |
|
| 214 | + | || lower.contains("containernotfound") |
|
| 215 | + | { |
|
| 216 | + | format!( |
|
| 217 | + | ": {} does not exist, or the credentials cannot see it", |
|
| 218 | + | self.name |
|
| 219 | + | ) |
|
| 220 | + | } else if lower.contains("dns") |
|
| 221 | + | || lower.contains("connect") |
|
| 222 | + | || lower.contains("timed out") |
|
| 223 | + | { |
|
| 224 | + | ": cannot reach the service; check the network, the endpoint and the region" |
|
| 225 | + | .to_string() |
|
| 226 | + | } else { |
|
| 227 | + | String::new() |
|
| 228 | + | }; |
|
| 229 | + | Error::Backend(format!("{}: {text}{advice}", self.name)) |
|
| 230 | + | } |
|
| 231 | + | } |
|
| 232 | + | } |
|
| 233 | + | ||
| 234 | + | fn version_of(e_tag: Option<String>, version: Option<String>) -> Version { |
|
| 235 | + | Version(format!( |
|
| 236 | + | "{}\t{}", |
|
| 237 | + | e_tag.unwrap_or_default(), |
|
| 238 | + | version.unwrap_or_default() |
|
| 239 | + | )) |
|
| 240 | + | } |
|
| 241 | + | ||
| 242 | + | fn update_version(v: &Version) -> UpdateVersion { |
|
| 243 | + | let (e_tag, version) = v.0.split_once('\t').unwrap_or((v.0.as_str(), "")); |
|
| 244 | + | UpdateVersion { |
|
| 245 | + | e_tag: (!e_tag.is_empty()).then(|| e_tag.to_string()), |
|
| 246 | + | version: (!version.is_empty()).then(|| version.to_string()), |
|
| 247 | + | } |
|
| 248 | + | } |
|
| 249 | + | } |
|
| 250 | + | ||
| 251 | + | impl Store for CloudStore { |
|
| 252 | + | fn describe(&self) -> String { |
|
| 253 | + | self.name.clone() |
|
| 254 | + | } |
|
| 255 | + | ||
| 256 | + | fn has(&self, key: &str) -> Result<bool> { |
|
| 257 | + | let path = self.path(key)?; |
|
| 258 | + | match self.block(self.inner.head(&path)) { |
|
| 259 | + | Ok(_) => Ok(true), |
|
| 260 | + | Err(object_store::Error::NotFound { .. }) => Ok(false), |
|
| 261 | + | Err(e) => Err(self.map(e, key)), |
|
| 262 | + | } |
|
| 263 | + | } |
|
| 264 | + | ||
| 265 | + | fn get(&self, key: &str) -> Result<Vec<u8>> { |
|
| 266 | + | let path = self.path(key)?; |
|
| 267 | + | self.block(async { |
|
| 268 | + | let r = self.inner.get(&path).await?; |
|
| 269 | + | r.bytes().await |
|
| 270 | + | }) |
|
| 271 | + | .map(|b| b.to_vec()) |
|
| 272 | + | .map_err(|e| self.map(e, key)) |
|
| 273 | + | } |
|
| 274 | + | ||
| 275 | + | fn get_range(&self, key: &str, range: Range<u64>) -> Result<Vec<u8>> { |
|
| 276 | + | let path = self.path(key)?; |
|
| 277 | + | if range.start > range.end { |
|
| 278 | + | return Err(Error::Backend(format!( |
|
| 279 | + | "{key}: bytes {}..{} is not a range", |
|
| 280 | + | range.start, range.end |
|
| 281 | + | ))); |
|
| 282 | + | } |
|
| 283 | + | if range.is_empty() { |
|
| 284 | + | return Ok(Vec::new()); |
|
| 285 | + | } |
|
| 286 | + | let want = (range.end - range.start) as usize; |
|
| 287 | + | let bytes = self |
|
| 288 | + | .block(self.inner.get_range(&path, range.clone())) |
|
| 289 | + | .map_err(|e| self.map(e, key))?; |
|
| 290 | + | if bytes.len() != want { |
|
| 291 | + | return Err(Error::Backend(format!( |
|
| 292 | + | "{key}: bytes {}..{} asked, {} returned: the object is shorter", |
|
| 293 | + | range.start, |
|
| 294 | + | range.end, |
|
| 295 | + | bytes.len() |
|
| 296 | + | ))); |
|
| 297 | + | } |
|
| 298 | + | Ok(bytes.to_vec()) |
|
| 299 | + | } |
|
| 300 | + | ||
| 301 | + | fn put_new(&self, key: &str, bytes: &[u8]) -> Result<()> { |
|
| 302 | + | let path = self.path(key)?; |
|
| 303 | + | let opts = PutOptions { |
|
| 304 | + | mode: PutMode::Create, |
|
| 305 | + | ..Default::default() |
|
| 306 | + | }; |
|
| 307 | + | self.block( |
|
| 308 | + | self.inner |
|
| 309 | + | .put_opts(&path, PutPayload::from(bytes.to_vec()), opts), |
|
| 310 | + | ) |
|
| 311 | + | .map(|_| ()) |
|
| 312 | + | .map_err(|e| self.map(e, key)) |
|
| 313 | + | } |
|
| 314 | + | ||
| 315 | + | fn list(&self, prefix: &str) -> Result<Vec<Entry>> { |
|
| 316 | + | // Providers list by whole path segments; the asked prefix may end |
|
| 317 | + | // mid-segment, so list its directory and filter here. |
|
| 318 | + | let dir = prefix.rsplit_once('/').map(|(d, _)| d).unwrap_or(""); |
|
| 319 | + | let full_dir = match (self.prefix.as_str(), dir) { |
|
| 320 | + | ("", "") => None, |
|
| 321 | + | ("", d) => Some(d.to_string()), |
|
| 322 | + | (p, "") => Some(p.to_string()), |
|
| 323 | + | (p, d) => Some(format!("{p}/{d}")), |
|
| 324 | + | }; |
|
| 325 | + | let full_dir = full_dir |
|
| 326 | + | .map(|d| ObjectPath::parse(&d).map_err(|e| Error::Backend(format!("{d}: {e}")))) |
|
| 327 | + | .transpose()?; |
|
| 328 | + | let metas: Vec<object_store::ObjectMeta> = self |
|
| 329 | + | .block(self.inner.list(full_dir.as_ref()).try_collect()) |
|
| 330 | + | .map_err(|e| self.map(e, prefix))?; |
|
| 331 | + | let strip = if self.prefix.is_empty() { |
|
| 332 | + | String::new() |
|
| 333 | + | } else { |
|
| 334 | + | format!("{}/", self.prefix) |
|
| 335 | + | }; |
|
| 336 | + | let mut out: Vec<Entry> = metas |
|
| 337 | + | .into_iter() |
|
| 338 | + | .filter_map(|m| { |
|
| 339 | + | let loc = m.location.to_string(); |
|
| 340 | + | let key = loc.strip_prefix(&strip)?; |
|
| 341 | + | key.starts_with(prefix).then(|| Entry { |
|
| 342 | + | key: key.to_string(), |
|
| 343 | + | size: m.size, |
|
| 344 | + | }) |
|
| 345 | + | }) |
|
| 346 | + | .collect(); |
|
| 347 | + | out.sort_by(|a, b| a.key.cmp(&b.key)); |
|
| 348 | + | Ok(out) |
|
| 349 | + | } |
|
| 350 | + | ||
| 351 | + | fn delete(&self, key: &str) -> Result<()> { |
|
| 352 | + | let path = self.path(key)?; |
|
| 353 | + | match self.block(self.inner.delete(&path)) { |
|
| 354 | + | Ok(()) | Err(object_store::Error::NotFound { .. }) => Ok(()), |
|
| 355 | + | Err(e) => Err(self.map(e, key)), |
|
| 356 | + | } |
|
| 357 | + | } |
|
| 358 | + | ||
| 359 | + | fn read_ref(&self, key: &str) -> Result<Option<(Vec<u8>, Version)>> { |
|
| 360 | + | let path = self.path(key)?; |
|
| 361 | + | let got = self.block(async { |
|
| 362 | + | let r = self.inner.get_opts(&path, GetOptions::default()).await?; |
|
| 363 | + | let meta = r.meta.clone(); |
|
| 364 | + | let bytes = r.bytes().await?; |
|
| 365 | + | Ok::<_, object_store::Error>((bytes, meta)) |
|
| 366 | + | }); |
|
| 367 | + | match got { |
|
| 368 | + | Ok((bytes, meta)) => Ok(Some(( |
|
| 369 | + | bytes.to_vec(), |
|
| 370 | + | Self::version_of(meta.e_tag, meta.version), |
|
| 371 | + | ))), |
|
| 372 | + | Err(object_store::Error::NotFound { .. }) => Ok(None), |
|
| 373 | + | Err(e) => Err(self.map(e, key)), |
|
| 374 | + | } |
|
| 375 | + | } |
|
| 376 | + | ||
| 377 | + | fn write_ref(&self, key: &str, bytes: &[u8], expected: Option<&Version>) -> Result<Version> { |
|
| 378 | + | let path = self.path(key)?; |
|
| 379 | + | let mode = match expected { |
|
| 380 | + | None => PutMode::Create, |
|
| 381 | + | Some(v) => PutMode::Update(Self::update_version(v)), |
|
| 382 | + | }; |
|
| 383 | + | let opts = PutOptions { |
|
| 384 | + | mode, |
|
| 385 | + | ..Default::default() |
|
| 386 | + | }; |
|
| 387 | + | let put = self |
|
| 388 | + | .block( |
|
| 389 | + | self.inner |
|
| 390 | + | .put_opts(&path, PutPayload::from(bytes.to_vec()), opts), |
|
| 391 | + | ) |
|
| 392 | + | .map_err(|e| match self.map(e, key) { |
|
| 393 | + | // A record that exists when none was expected is a lost |
|
| 394 | + | // race too, not a misuse of put_new. |
|
| 395 | + | Error::Exists(k) => Error::Conflict(k), |
|
| 396 | + | other => other, |
|
| 397 | + | })?; |
|
| 398 | + | Ok(Self::version_of(put.e_tag, put.version)) |
|
| 399 | + | } |
|
| 400 | + | } |
|
| 401 | + | ||
| 402 | + | #[cfg(test)] |
|
| 403 | + | mod tests { |
|
| 404 | + | use super::*; |
|
| 405 | + | ||
| 406 | + | #[test] |
|
| 407 | + | fn urls_parse_with_their_parameters() { |
|
| 408 | + | let u = |
|
| 409 | + | StoreUrl::parse("s3://my-bucket/repos/oxygen?profile=work®ion=eu-west-2").unwrap(); |
|
| 410 | + | assert_eq!(u.scheme, "s3"); |
|
| 411 | + | assert_eq!(u.host, "my-bucket"); |
|
| 412 | + | assert_eq!(u.path, "repos/oxygen"); |
|
| 413 | + | assert_eq!(u.param("profile"), Some("work")); |
|
| 414 | + | assert_eq!(u.param("region"), Some("eu-west-2")); |
|
| 415 | + | assert_eq!(u.plain(), "s3://my-bucket/repos/oxygen"); |
|
| 416 | + | let u = StoreUrl::parse("gs://bucket").unwrap(); |
|
| 417 | + | assert_eq!((u.host.as_str(), u.path.as_str()), ("bucket", "")); |
|
| 418 | + | assert_eq!(u.plain(), "gs://bucket"); |
|
| 419 | + | let u = StoreUrl::parse("az://acct/container/a/b/?endpoint=http%3A%2F%2F127.0.0.1%3A10000") |
|
| 420 | + | .unwrap(); |
|
| 421 | + | assert_eq!(u.path, "container/a/b"); |
|
| 422 | + | assert_eq!(u.param("endpoint"), Some("http://127.0.0.1:10000")); |
|
| 423 | + | assert!(StoreUrl::parse("s3://").is_err()); |
|
| 424 | + | assert!(StoreUrl::parse("nonsense").is_err()); |
|
| 425 | + | } |
|
| 426 | + | ||
| 427 | + | #[test] |
|
| 428 | + | fn versions_carry_both_an_etag_and_a_generation() { |
|
| 429 | + | let v = CloudStore::version_of(Some("\"abc\"".into()), Some("17".into())); |
|
| 430 | + | let u = CloudStore::update_version(&v); |
|
| 431 | + | assert_eq!(u.e_tag.as_deref(), Some("\"abc\"")); |
|
| 432 | + | assert_eq!(u.version.as_deref(), Some("17")); |
|
| 433 | + | let u = CloudStore::update_version(&CloudStore::version_of(None, Some("3".into()))); |
|
| 434 | + | assert_eq!((u.e_tag, u.version.as_deref()), (None, Some("3"))); |
|
| 435 | + | } |
|
| 436 | + | } |
added crates/oxy-strata/tests/cloud.rs +149 −0
| 1 | + | //! The cloud stores against real services or emulators, when the |
|
| 2 | + | //! environment names them; otherwise each test says so and passes. |
|
| 3 | + | //! |
|
| 4 | + | //! - `OXY_TEST_S3=s3://BUCKET/PREFIX[?params]`, with `OXY_TEST_S3_PROFILE` |
|
| 5 | + | //! added as `?profile=`; |
|
| 6 | + | //! - `OXY_TEST_GCS=gs://BUCKET/PREFIX[?params]`, with |
|
| 7 | + | //! `OXY_TEST_GCS_CREDENTIALS` added as `?credentials=`; |
|
| 8 | + | //! - `OXY_TEST_AZURE=az://ACCOUNT/CONTAINER/PREFIX[?params]`, with the |
|
| 9 | + | //! `AZURE_*` variables read from the file `OXY_TEST_AZURE_CREDENTIALS`. |
|
| 10 | + | //! |
|
| 11 | + | //! Every test works under its own prefix below the one given, and deletes |
|
| 12 | + | //! what it wrote. |
|
| 13 | + | ||
| 14 | + | use oxy_core::repo::Repo; |
|
| 15 | + | use oxy_core::strata::{conformance, Store}; |
|
| 16 | + | use oxy_strata::CloudStore; |
|
| 17 | + | use std::path::Path; |
|
| 18 | + | ||
| 19 | + | fn configured(scheme: &str) -> Option<String> { |
|
| 20 | + | let (var, extra, key) = match scheme { |
|
| 21 | + | "s3" => ("OXY_TEST_S3", "OXY_TEST_S3_PROFILE", "profile"), |
|
| 22 | + | "gs" => ("OXY_TEST_GCS", "OXY_TEST_GCS_CREDENTIALS", "credentials"), |
|
| 23 | + | "az" => ("OXY_TEST_AZURE", "OXY_TEST_AZURE_CREDENTIALS", ""), |
|
| 24 | + | _ => unreachable!(), |
|
| 25 | + | }; |
|
| 26 | + | let url = std::env::var(var).ok().filter(|u| !u.is_empty())?; |
|
| 27 | + | let extra_value = std::env::var(extra).ok().filter(|v| !v.is_empty()); |
|
| 28 | + | if scheme == "az" { |
|
| 29 | + | if let Some(file) = &extra_value { |
|
| 30 | + | for line in std::fs::read_to_string(file).unwrap_or_default().lines() { |
|
| 31 | + | if let Some((k, v)) = line.split_once('=') { |
|
| 32 | + | std::env::set_var(k.trim(), v.trim()); |
|
| 33 | + | } |
|
| 34 | + | } |
|
| 35 | + | } |
|
| 36 | + | return Some(url); |
|
| 37 | + | } |
|
| 38 | + | Some(match extra_value { |
|
| 39 | + | Some(v) if !url.contains(&format!("{key}=")) => { |
|
| 40 | + | let sep = if url.contains('?') { '&' } else { '?' }; |
|
| 41 | + | format!("{url}{sep}{key}={v}") |
|
| 42 | + | } |
|
| 43 | + | _ => url, |
|
| 44 | + | }) |
|
| 45 | + | } |
|
| 46 | + | ||
| 47 | + | fn each_store() -> Vec<(String, CloudStore)> { |
|
| 48 | + | oxy_strata::register(); |
|
| 49 | + | ["s3", "gs", "az"] |
|
| 50 | + | .into_iter() |
|
| 51 | + | .filter_map(|s| { |
|
| 52 | + | let Some(url) = configured(s) else { |
|
| 53 | + | eprintln!("{s}: not configured, skipped"); |
|
| 54 | + | return None; |
|
| 55 | + | }; |
|
| 56 | + | let store = CloudStore::open(&url).unwrap_or_else(|e| panic!("{url}: {e}")); |
|
| 57 | + | Some((url, store)) |
|
| 58 | + | }) |
|
| 59 | + | .collect() |
|
| 60 | + | } |
|
| 61 | + | ||
| 62 | + | fn remove_all(store: &dyn Store, prefix: &str) { |
|
| 63 | + | for e in store.list(prefix).unwrap() { |
|
| 64 | + | store.delete(&e.key).unwrap(); |
|
| 65 | + | } |
|
| 66 | + | } |
|
| 67 | + | ||
| 68 | + | /// What earlier runs that failed may have left under the test prefix: |
|
| 69 | + | /// other processes' `probe-*` and `repo-*` keys. |
|
| 70 | + | fn remove_stale(store: &dyn Store) { |
|
| 71 | + | let mine = format!("-{}", std::process::id()); |
|
| 72 | + | for e in store.list("").unwrap() { |
|
| 73 | + | let first = e.key.split('/').next().unwrap_or(""); |
|
| 74 | + | if (first.starts_with("probe-") || first.starts_with("repo-")) && !first.ends_with(&mine) { |
|
| 75 | + | store.delete(&e.key).unwrap(); |
|
| 76 | + | } |
|
| 77 | + | } |
|
| 78 | + | } |
|
| 79 | + | ||
| 80 | + | #[test] |
|
| 81 | + | fn every_configured_store_conforms() { |
|
| 82 | + | for (url, store) in each_store() { |
|
| 83 | + | remove_stale(&store); |
|
| 84 | + | let prefix = format!("probe-{}", std::process::id()); |
|
| 85 | + | let passed = conformance(&store, &prefix).unwrap_or_else(|e| { |
|
| 86 | + | remove_all(&store, &prefix); |
|
| 87 | + | panic!("{url}: {e}") |
|
| 88 | + | }); |
|
| 89 | + | eprintln!("{url}: {} checks passed", passed.len()); |
|
| 90 | + | assert_eq!(passed.len(), 8); |
|
| 91 | + | } |
|
| 92 | + | } |
|
| 93 | + | ||
| 94 | + | /// A small repository pushed to the store, cloned back, and pulled, by |
|
| 95 | + | /// URL: what a user does. Then every key the test wrote is removed. |
|
| 96 | + | #[test] |
|
| 97 | + | fn a_repository_round_trips_through_every_configured_store() { |
|
| 98 | + | for (url, base_store) in each_store() { |
|
| 99 | + | remove_stale(&base_store); |
|
| 100 | + | let dir = tempfile::tempdir().unwrap(); |
|
| 101 | + | let root = dir.path(); |
|
| 102 | + | std::fs::create_dir_all(root.join("home")).unwrap(); |
|
| 103 | + | std::env::set_var("OXY_HOME", root.join("home")); |
|
| 104 | + | let (base, query) = match url.split_once('?') { |
|
| 105 | + | Some((b, q)) => (b.to_string(), format!("?{q}")), |
|
| 106 | + | None => (url.clone(), String::new()), |
|
| 107 | + | }; |
|
| 108 | + | let remote_url = format!("{base}/repo-{}{query}", std::process::id()); |
|
| 109 | + | let plain = format!("{base}/repo-{}", std::process::id()); |
|
| 110 | + | let store = CloudStore::open(&remote_url).unwrap(); |
|
| 111 | + | ||
| 112 | + | let mut a = Repo::init(&root.join("a")).unwrap(); |
|
| 113 | + | a.config.set("remote.cloud.url", &remote_url).unwrap(); |
|
| 114 | + | a.save_config().unwrap(); |
|
| 115 | + | write(root, "a/f.txt", b"hello from a\n"); |
|
| 116 | + | let h1 = a.commit("first", &[]).unwrap(); |
|
| 117 | + | let r = a.push("cloud", false).unwrap(); |
|
| 118 | + | assert!(r.iter().all(|x| x.ok), "{plain}: {r:?}"); |
|
| 119 | + | assert!(store.has("oxygen.toml").unwrap()); |
|
| 120 | + | assert_eq!(store.list("bundles/").unwrap().len(), 1, "{plain}"); |
|
| 121 | + | ||
| 122 | + | let b = Repo::clone(&remote_url, &root.join("b")) |
|
| 123 | + | .unwrap_or_else(|e| panic!("{plain}: clone: {e}")); |
|
| 124 | + | assert_eq!( |
|
| 125 | + | std::fs::read(root.join("b/f.txt")).unwrap(), |
|
| 126 | + | b"hello from a\n" |
|
| 127 | + | ); |
|
| 128 | + | assert_eq!(b.bookmark_heads().unwrap(), vec![h1]); |
|
| 129 | + | write(root, "b/f.txt", b"hello from a\nand b\n"); |
|
| 130 | + | b.commit("second", &[]).unwrap(); |
|
| 131 | + | assert!(b.push("origin", false).unwrap()[0].ok); |
|
| 132 | + | let pr = a.pull("cloud").unwrap(); |
|
| 133 | + | assert!(pr.changed, "{plain}"); |
|
| 134 | + | assert_eq!( |
|
| 135 | + | std::fs::read(root.join("a/f.txt")).unwrap(), |
|
| 136 | + | b"hello from a\nand b\n" |
|
| 137 | + | ); |
|
| 138 | + | let total: u64 = store.list("").unwrap().iter().map(|e| e.size).sum(); |
|
| 139 | + | eprintln!("{plain}: round trip ok, {total} bytes in the store before cleanup"); |
|
| 140 | + | remove_all(&store, ""); |
|
| 141 | + | assert!(store.list("").unwrap().is_empty(), "{plain}: cleanup"); |
|
| 142 | + | } |
|
| 143 | + | } |
|
| 144 | + | ||
| 145 | + | fn write(root: &Path, rel: &str, content: &[u8]) { |
|
| 146 | + | let p = root.join(rel); |
|
| 147 | + | std::fs::create_dir_all(p.parent().unwrap()).unwrap(); |
|
| 148 | + | std::fs::write(p, content).unwrap(); |
|
| 149 | + | } |
modified Cargo.lock +702 −6
| … | 17 unchanged lines |
||
| 18 | 18 | ] |
|
| 19 | 19 | ||
| 20 | 20 | [[package]] |
|
| 21 | + | name = "android_system_properties" |
|
| 22 | + | version = "0.1.6" |
|
| 23 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 24 | + | checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" |
|
| 25 | + | dependencies = [ |
|
| 26 | + | "libc", |
|
| 27 | + | ] |
|
| 28 | + | ||
| 29 | + | [[package]] |
|
| 21 | 30 | name = "anstream" |
|
| 22 | 31 | version = "1.0.0" |
|
| 23 | 32 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 101 unchanged lines |
||
| 125 | 134 | "proc-macro2", |
|
| 126 | 135 | "quote", |
|
| 127 | 136 | "syn 2.0.119", |
|
| 137 | + | ] |
|
| 138 | + | ||
| 139 | + | [[package]] |
|
| 140 | + | name = "async-trait" |
|
| 141 | + | version = "0.1.92" |
|
| 142 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 143 | + | checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" |
|
| 144 | + | dependencies = [ |
|
| 145 | + | "proc-macro2", |
|
| 146 | + | "quote", |
|
| 147 | + | "syn 3.0.6", |
|
| 128 | 148 | ] |
|
| 129 | 149 | ||
| 130 | 150 | [[package]] |
|
| … | 194 unchanged lines |
||
| 325 | 345 | checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" |
|
| 326 | 346 | ||
| 327 | 347 | [[package]] |
|
| 348 | + | name = "chacha20" |
|
| 349 | + | version = "0.10.2" |
|
| 350 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 351 | + | checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" |
|
| 352 | + | dependencies = [ |
|
| 353 | + | "cfg-if", |
|
| 354 | + | "cpufeatures", |
|
| 355 | + | "rand_core 0.10.1", |
|
| 356 | + | ] |
|
| 357 | + | ||
| 358 | + | [[package]] |
|
| 359 | + | name = "chrono" |
|
| 360 | + | version = "0.4.45" |
|
| 361 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 362 | + | checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" |
|
| 363 | + | dependencies = [ |
|
| 364 | + | "iana-time-zone", |
|
| 365 | + | "num-traits", |
|
| 366 | + | "serde", |
|
| 367 | + | "windows-link", |
|
| 368 | + | ] |
|
| 369 | + | ||
| 370 | + | [[package]] |
|
| 328 | 371 | name = "clap" |
|
| 329 | 372 | version = "4.6.7" |
|
| 330 | 373 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 65 unchanged lines |
||
| 396 | 439 | checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" |
|
| 397 | 440 | ||
| 398 | 441 | [[package]] |
|
| 442 | + | name = "combine" |
|
| 443 | + | version = "4.6.8" |
|
| 444 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 445 | + | checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" |
|
| 446 | + | dependencies = [ |
|
| 447 | + | "bytes", |
|
| 448 | + | "memchr", |
|
| 449 | + | ] |
|
| 450 | + | ||
| 451 | + | [[package]] |
|
| 399 | 452 | name = "console" |
|
| 400 | 453 | version = "0.15.11" |
|
| 401 | 454 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 19 unchanged lines |
||
| 421 | 474 | checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" |
|
| 422 | 475 | ||
| 423 | 476 | [[package]] |
|
| 477 | + | name = "core-foundation" |
|
| 478 | + | version = "0.10.1" |
|
| 479 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 480 | + | checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" |
|
| 481 | + | dependencies = [ |
|
| 482 | + | "core-foundation-sys", |
|
| 483 | + | "libc", |
|
| 484 | + | ] |
|
| 485 | + | ||
| 486 | + | [[package]] |
|
| 487 | + | name = "core-foundation-sys" |
|
| 488 | + | version = "0.8.7" |
|
| 489 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 490 | + | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" |
|
| 491 | + | ||
| 492 | + | [[package]] |
|
| 424 | 493 | name = "cpufeatures" |
|
| 425 | 494 | version = "0.3.1" |
|
| 426 | 495 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 427 | 496 | checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" |
|
| 428 | 497 | dependencies = [ |
|
| 429 | 498 | "libc", |
|
| 499 | + | ] |
|
| 500 | + | ||
| 501 | + | [[package]] |
|
| 502 | + | name = "crc-fast" |
|
| 503 | + | version = "1.10.0" |
|
| 504 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 505 | + | checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" |
|
| 506 | + | dependencies = [ |
|
| 507 | + | "digest 0.10.7", |
|
| 508 | + | "spin", |
|
| 430 | 509 | ] |
|
| 431 | 510 | ||
| 432 | 511 | [[package]] |
|
| … | 29 unchanged lines |
||
| 462 | 541 | version = "0.8.23" |
|
| 463 | 542 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 464 | 543 | checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" |
|
| 544 | + | ||
| 545 | + | [[package]] |
|
| 546 | + | name = "crypto-common" |
|
| 547 | + | version = "0.1.7" |
|
| 548 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 549 | + | checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" |
|
| 550 | + | dependencies = [ |
|
| 551 | + | "generic-array", |
|
| 552 | + | "typenum", |
|
| 553 | + | ] |
|
| 465 | 554 | ||
| 466 | 555 | [[package]] |
|
| 467 | 556 | name = "crypto-common" |
|
| … | 24 unchanged lines |
||
| 492 | 581 | "cfg-if", |
|
| 493 | 582 | "cpufeatures", |
|
| 494 | 583 | "curve25519-dalek-derive", |
|
| 495 | - | "digest", |
|
| 584 | + | "digest 0.11.3", |
|
| 496 | 585 | "fiat-crypto", |
|
| 497 | 586 | "rustc_version", |
|
| 498 | 587 | "subtle", |
|
| … | 46 unchanged lines |
||
| 545 | 634 | version = "0.5.8" |
|
| 546 | 635 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 547 | 636 | checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" |
|
| 637 | + | ||
| 638 | + | [[package]] |
|
| 639 | + | name = "digest" |
|
| 640 | + | version = "0.10.7" |
|
| 641 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 642 | + | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" |
|
| 643 | + | dependencies = [ |
|
| 644 | + | "crypto-common 0.1.7", |
|
| 645 | + | ] |
|
| 548 | 646 | ||
| 549 | 647 | [[package]] |
|
| 550 | 648 | name = "digest" |
|
| … | 2 unchanged lines |
||
| 553 | 651 | checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" |
|
| 554 | 652 | dependencies = [ |
|
| 555 | 653 | "block-buffer", |
|
| 556 | - | "crypto-common", |
|
| 654 | + | "crypto-common 0.2.2", |
|
| 557 | 655 | ] |
|
| 558 | 656 | ||
| 559 | 657 | [[package]] |
|
| … | 109 unchanged lines |
||
| 669 | 767 | dependencies = [ |
|
| 670 | 768 | "autocfg", |
|
| 671 | 769 | "tokio", |
|
| 770 | + | ] |
|
| 771 | + | ||
| 772 | + | [[package]] |
|
| 773 | + | name = "futures" |
|
| 774 | + | version = "0.3.34" |
|
| 775 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 776 | + | checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" |
|
| 777 | + | dependencies = [ |
|
| 778 | + | "futures-channel", |
|
| 779 | + | "futures-core", |
|
| 780 | + | "futures-executor", |
|
| 781 | + | "futures-io", |
|
| 782 | + | "futures-sink", |
|
| 783 | + | "futures-task", |
|
| 784 | + | "futures-util", |
|
| 672 | 785 | ] |
|
| 673 | 786 | ||
| 674 | 787 | [[package]] |
|
| … | 3 unchanged lines |
||
| 678 | 791 | checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" |
|
| 679 | 792 | dependencies = [ |
|
| 680 | 793 | "futures-core", |
|
| 794 | + | "futures-sink", |
|
| 681 | 795 | ] |
|
| 682 | 796 | ||
| 683 | 797 | [[package]] |
|
| … | 3 unchanged lines |
||
| 687 | 801 | checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" |
|
| 688 | 802 | ||
| 689 | 803 | [[package]] |
|
| 804 | + | name = "futures-executor" |
|
| 805 | + | version = "0.3.34" |
|
| 806 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 807 | + | checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" |
|
| 808 | + | dependencies = [ |
|
| 809 | + | "futures-core", |
|
| 810 | + | "futures-task", |
|
| 811 | + | "futures-util", |
|
| 812 | + | ] |
|
| 813 | + | ||
| 814 | + | [[package]] |
|
| 815 | + | name = "futures-io" |
|
| 816 | + | version = "0.3.34" |
|
| 817 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 818 | + | checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" |
|
| 819 | + | ||
| 820 | + | [[package]] |
|
| 821 | + | name = "futures-macro" |
|
| 822 | + | version = "0.3.34" |
|
| 823 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 824 | + | checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" |
|
| 825 | + | dependencies = [ |
|
| 826 | + | "proc-macro2", |
|
| 827 | + | "quote", |
|
| 828 | + | "syn 3.0.6", |
|
| 829 | + | ] |
|
| 830 | + | ||
| 831 | + | [[package]] |
|
| 690 | 832 | name = "futures-sink" |
|
| 691 | 833 | version = "0.3.34" |
|
| 692 | 834 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 11 unchanged lines |
||
| 704 | 846 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 705 | 847 | checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" |
|
| 706 | 848 | dependencies = [ |
|
| 849 | + | "futures-channel", |
|
| 707 | 850 | "futures-core", |
|
| 851 | + | "futures-io", |
|
| 852 | + | "futures-macro", |
|
| 853 | + | "futures-sink", |
|
| 708 | 854 | "futures-task", |
|
| 855 | + | "memchr", |
|
| 709 | 856 | "pin-project-lite", |
|
| 710 | 857 | "slab", |
|
| 711 | 858 | ] |
|
| 712 | 859 | ||
| 713 | 860 | [[package]] |
|
| 861 | + | name = "generic-array" |
|
| 862 | + | version = "0.14.7" |
|
| 863 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 864 | + | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" |
|
| 865 | + | dependencies = [ |
|
| 866 | + | "typenum", |
|
| 867 | + | "version_check", |
|
| 868 | + | ] |
|
| 869 | + | ||
| 870 | + | [[package]] |
|
| 714 | 871 | name = "getrandom" |
|
| 715 | 872 | version = "0.2.17" |
|
| 716 | 873 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 122 unchanged lines |
||
| 839 | 996 | version = "1.0.3" |
|
| 840 | 997 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 841 | 998 | checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" |
|
| 999 | + | ||
| 1000 | + | [[package]] |
|
| 1001 | + | name = "humantime" |
|
| 1002 | + | version = "2.4.0" |
|
| 1003 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1004 | + | checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" |
|
| 842 | 1005 | ||
| 843 | 1006 | [[package]] |
|
| 844 | 1007 | name = "hybrid-array" |
|
| … | 24 unchanged lines |
||
| 869 | 1032 | "pin-project-lite", |
|
| 870 | 1033 | "smallvec", |
|
| 871 | 1034 | "tokio", |
|
| 1035 | + | "want", |
|
| 872 | 1036 | ] |
|
| 873 | 1037 | ||
| 874 | 1038 | [[package]] |
|
| 1039 | + | name = "hyper-rustls" |
|
| 1040 | + | version = "0.27.10" |
|
| 1041 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1042 | + | checksum = "dfa8e654703247911e29c23fbeaa261834bd9bb74efba2f9acddc37bfb127f53" |
|
| 1043 | + | dependencies = [ |
|
| 1044 | + | "http", |
|
| 1045 | + | "hyper", |
|
| 1046 | + | "hyper-util", |
|
| 1047 | + | "rustls", |
|
| 1048 | + | "tokio", |
|
| 1049 | + | "tokio-rustls", |
|
| 1050 | + | "tower-service", |
|
| 1051 | + | ] |
|
| 1052 | + | ||
| 1053 | + | [[package]] |
|
| 875 | 1054 | name = "hyper-util" |
|
| 876 | 1055 | version = "0.1.21" |
|
| 877 | 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 878 | 1057 | checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" |
|
| 879 | 1058 | dependencies = [ |
|
| 1059 | + | "base64 0.23.1", |
|
| 880 | 1060 | "bytes", |
|
| 1061 | + | "futures-channel", |
|
| 1062 | + | "futures-util", |
|
| 881 | 1063 | "http", |
|
| 882 | 1064 | "http-body", |
|
| 1065 | + | "httparse", |
|
| 883 | 1066 | "hyper", |
|
| 1067 | + | "ipnet", |
|
| 1068 | + | "libc", |
|
| 1069 | + | "percent-encoding", |
|
| 884 | 1070 | "pin-project-lite", |
|
| 1071 | + | "socket2", |
|
| 885 | 1072 | "tokio", |
|
| 886 | 1073 | "tower-service", |
|
| 1074 | + | "tracing", |
|
| 887 | 1075 | ] |
|
| 888 | 1076 | ||
| 889 | 1077 | [[package]] |
|
| 1078 | + | name = "iana-time-zone" |
|
| 1079 | + | version = "0.1.65" |
|
| 1080 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1081 | + | checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" |
|
| 1082 | + | dependencies = [ |
|
| 1083 | + | "android_system_properties", |
|
| 1084 | + | "core-foundation-sys", |
|
| 1085 | + | "iana-time-zone-haiku", |
|
| 1086 | + | "js-sys", |
|
| 1087 | + | "log", |
|
| 1088 | + | "wasm-bindgen", |
|
| 1089 | + | "windows-core", |
|
| 1090 | + | ] |
|
| 1091 | + | ||
| 1092 | + | [[package]] |
|
| 1093 | + | name = "iana-time-zone-haiku" |
|
| 1094 | + | version = "0.1.2" |
|
| 1095 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1096 | + | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" |
|
| 1097 | + | dependencies = [ |
|
| 1098 | + | "cc", |
|
| 1099 | + | ] |
|
| 1100 | + | ||
| 1101 | + | [[package]] |
|
| 890 | 1102 | name = "icu_collections" |
|
| 891 | 1103 | version = "2.3.0" |
|
| 892 | 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 137 unchanged lines |
||
| 1030 | 1242 | ] |
|
| 1031 | 1243 | ||
| 1032 | 1244 | [[package]] |
|
| 1245 | + | name = "ipnet" |
|
| 1246 | + | version = "2.12.2" |
|
| 1247 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1248 | + | checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" |
|
| 1249 | + | ||
| 1250 | + | [[package]] |
|
| 1033 | 1251 | name = "is_terminal_polyfill" |
|
| 1034 | 1252 | version = "1.70.2" |
|
| 1035 | 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1036 | 1254 | checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" |
|
| 1255 | + | ||
| 1256 | + | [[package]] |
|
| 1257 | + | name = "itertools" |
|
| 1258 | + | version = "0.15.0" |
|
| 1259 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1260 | + | checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" |
|
| 1261 | + | dependencies = [ |
|
| 1262 | + | "either", |
|
| 1263 | + | ] |
|
| 1037 | 1264 | ||
| 1038 | 1265 | [[package]] |
|
| 1039 | 1266 | name = "itoa" |
|
| … | 2 unchanged lines |
||
| 1042 | 1269 | checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" |
|
| 1043 | 1270 | ||
| 1044 | 1271 | [[package]] |
|
| 1272 | + | name = "jni" |
|
| 1273 | + | version = "0.22.4" |
|
| 1274 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1275 | + | checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" |
|
| 1276 | + | dependencies = [ |
|
| 1277 | + | "cfg-if", |
|
| 1278 | + | "combine", |
|
| 1279 | + | "jni-macros", |
|
| 1280 | + | "jni-sys", |
|
| 1281 | + | "log", |
|
| 1282 | + | "simd_cesu8", |
|
| 1283 | + | "thiserror 2.0.21", |
|
| 1284 | + | "walkdir", |
|
| 1285 | + | "windows-link", |
|
| 1286 | + | ] |
|
| 1287 | + | ||
| 1288 | + | [[package]] |
|
| 1289 | + | name = "jni-macros" |
|
| 1290 | + | version = "0.22.4" |
|
| 1291 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1292 | + | checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" |
|
| 1293 | + | dependencies = [ |
|
| 1294 | + | "proc-macro2", |
|
| 1295 | + | "quote", |
|
| 1296 | + | "rustc_version", |
|
| 1297 | + | "simd_cesu8", |
|
| 1298 | + | "syn 2.0.119", |
|
| 1299 | + | ] |
|
| 1300 | + | ||
| 1301 | + | [[package]] |
|
| 1302 | + | name = "jni-sys" |
|
| 1303 | + | version = "0.4.1" |
|
| 1304 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1305 | + | checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" |
|
| 1306 | + | dependencies = [ |
|
| 1307 | + | "jni-sys-macros", |
|
| 1308 | + | ] |
|
| 1309 | + | ||
| 1310 | + | [[package]] |
|
| 1311 | + | name = "jni-sys-macros" |
|
| 1312 | + | version = "0.4.1" |
|
| 1313 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1314 | + | checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" |
|
| 1315 | + | dependencies = [ |
|
| 1316 | + | "quote", |
|
| 1317 | + | "syn 2.0.119", |
|
| 1318 | + | ] |
|
| 1319 | + | ||
| 1320 | + | [[package]] |
|
| 1045 | 1321 | name = "jobserver" |
|
| 1046 | 1322 | version = "0.1.35" |
|
| 1047 | 1323 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 49 unchanged lines |
||
| 1097 | 1373 | checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" |
|
| 1098 | 1374 | ||
| 1099 | 1375 | [[package]] |
|
| 1376 | + | name = "lock_api" |
|
| 1377 | + | version = "0.4.14" |
|
| 1378 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1379 | + | checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" |
|
| 1380 | + | dependencies = [ |
|
| 1381 | + | "scopeguard", |
|
| 1382 | + | ] |
|
| 1383 | + | ||
| 1384 | + | [[package]] |
|
| 1100 | 1385 | name = "log" |
|
| 1101 | 1386 | version = "0.4.34" |
|
| 1102 | 1387 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 4 unchanged lines |
||
| 1107 | 1392 | version = "0.8.4" |
|
| 1108 | 1393 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1109 | 1394 | checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" |
|
| 1395 | + | ||
| 1396 | + | [[package]] |
|
| 1397 | + | name = "md-5" |
|
| 1398 | + | version = "0.11.0" |
|
| 1399 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1400 | + | checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" |
|
| 1401 | + | dependencies = [ |
|
| 1402 | + | "cfg-if", |
|
| 1403 | + | "digest 0.11.3", |
|
| 1404 | + | ] |
|
| 1110 | 1405 | ||
| 1111 | 1406 | [[package]] |
|
| 1112 | 1407 | name = "memchr" |
|
| … | 41 unchanged lines |
||
| 1154 | 1449 | checksum = "add6b9d92e496f16f4526d68ff29da1483aba4b119baeab8bed3b9e3544a6f3d" |
|
| 1155 | 1450 | dependencies = [ |
|
| 1156 | 1451 | "const-oid", |
|
| 1157 | - | "crypto-common", |
|
| 1452 | + | "crypto-common 0.2.2", |
|
| 1158 | 1453 | "ctutils", |
|
| 1159 | 1454 | "hybrid-array", |
|
| 1160 | 1455 | "module-lattice", |
|
| … | 62 unchanged lines |
||
| 1223 | 1518 | version = "0.4.0" |
|
| 1224 | 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1225 | 1520 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" |
|
| 1521 | + | ||
| 1522 | + | [[package]] |
|
| 1523 | + | name = "object_store" |
|
| 1524 | + | version = "0.14.2" |
|
| 1525 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1526 | + | checksum = "f1796bc93603f78c5760a69f2d58badc9618d22adade0a95385bb2adbae4eb94" |
|
| 1527 | + | dependencies = [ |
|
| 1528 | + | "async-trait", |
|
| 1529 | + | "base64 0.23.1", |
|
| 1530 | + | "bytes", |
|
| 1531 | + | "chrono", |
|
| 1532 | + | "crc-fast", |
|
| 1533 | + | "form_urlencoded", |
|
| 1534 | + | "futures-channel", |
|
| 1535 | + | "futures-core", |
|
| 1536 | + | "futures-util", |
|
| 1537 | + | "http", |
|
| 1538 | + | "http-body-util", |
|
| 1539 | + | "httparse", |
|
| 1540 | + | "humantime", |
|
| 1541 | + | "hyper", |
|
| 1542 | + | "itertools", |
|
| 1543 | + | "md-5", |
|
| 1544 | + | "parking_lot", |
|
| 1545 | + | "percent-encoding", |
|
| 1546 | + | "quick-xml", |
|
| 1547 | + | "rand 0.10.3", |
|
| 1548 | + | "reqwest", |
|
| 1549 | + | "ring", |
|
| 1550 | + | "rustls-pki-types", |
|
| 1551 | + | "serde", |
|
| 1552 | + | "serde_json", |
|
| 1553 | + | "serde_urlencoded", |
|
| 1554 | + | "thiserror 2.0.21", |
|
| 1555 | + | "tokio", |
|
| 1556 | + | "tracing", |
|
| 1557 | + | "url", |
|
| 1558 | + | "wasm-bindgen-futures", |
|
| 1559 | + | "web-time", |
|
| 1560 | + | ] |
|
| 1226 | 1561 | ||
| 1227 | 1562 | [[package]] |
|
| 1228 | 1563 | name = "oid-registry" |
|
| … | 15 unchanged lines |
||
| 1244 | 1579 | version = "1.70.2" |
|
| 1245 | 1580 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1246 | 1581 | checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" |
|
| 1582 | + | ||
| 1583 | + | [[package]] |
|
| 1584 | + | name = "openssl-probe" |
|
| 1585 | + | version = "0.2.1" |
|
| 1586 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1587 | + | checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" |
|
| 1247 | 1588 | ||
| 1248 | 1589 | [[package]] |
|
| 1249 | 1590 | name = "oxy" |
|
| … | 8 unchanged lines |
||
| 1258 | 1599 | "libc", |
|
| 1259 | 1600 | "oxy-core", |
|
| 1260 | 1601 | "oxy-server", |
|
| 1602 | + | "oxy-strata", |
|
| 1261 | 1603 | "serde_json", |
|
| 1262 | 1604 | "tempfile", |
|
| 1263 | 1605 | ] |
|
| … | 36 unchanged lines |
||
| 1300 | 1642 | ] |
|
| 1301 | 1643 | ||
| 1302 | 1644 | [[package]] |
|
| 1645 | + | name = "oxy-strata" |
|
| 1646 | + | version = "0.3.1" |
|
| 1647 | + | dependencies = [ |
|
| 1648 | + | "futures", |
|
| 1649 | + | "object_store", |
|
| 1650 | + | "oxy-core", |
|
| 1651 | + | "reqwest", |
|
| 1652 | + | "rustls", |
|
| 1653 | + | "serde_json", |
|
| 1654 | + | "tempfile", |
|
| 1655 | + | "tokio", |
|
| 1656 | + | ] |
|
| 1657 | + | ||
| 1658 | + | [[package]] |
|
| 1659 | + | name = "parking_lot" |
|
| 1660 | + | version = "0.12.5" |
|
| 1661 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1662 | + | checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" |
|
| 1663 | + | dependencies = [ |
|
| 1664 | + | "lock_api", |
|
| 1665 | + | "parking_lot_core", |
|
| 1666 | + | ] |
|
| 1667 | + | ||
| 1668 | + | [[package]] |
|
| 1669 | + | name = "parking_lot_core" |
|
| 1670 | + | version = "0.9.12" |
|
| 1671 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1672 | + | checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" |
|
| 1673 | + | dependencies = [ |
|
| 1674 | + | "cfg-if", |
|
| 1675 | + | "libc", |
|
| 1676 | + | "redox_syscall", |
|
| 1677 | + | "smallvec", |
|
| 1678 | + | "windows-link", |
|
| 1679 | + | ] |
|
| 1680 | + | ||
| 1681 | + | [[package]] |
|
| 1303 | 1682 | name = "pem" |
|
| 1304 | 1683 | version = "4.0.0" |
|
| 1305 | 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 96 unchanged lines |
||
| 1402 | 1781 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" |
|
| 1403 | 1782 | ||
| 1404 | 1783 | [[package]] |
|
| 1784 | + | name = "quick-xml" |
|
| 1785 | + | version = "0.41.0" |
|
| 1786 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1787 | + | checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" |
|
| 1788 | + | dependencies = [ |
|
| 1789 | + | "memchr", |
|
| 1790 | + | "serde", |
|
| 1791 | + | ] |
|
| 1792 | + | ||
| 1793 | + | [[package]] |
|
| 1405 | 1794 | name = "quote" |
|
| 1406 | 1795 | version = "1.0.47" |
|
| 1407 | 1796 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 36 unchanged lines |
||
| 1444 | 1833 | ] |
|
| 1445 | 1834 | ||
| 1446 | 1835 | [[package]] |
|
| 1836 | + | name = "rand" |
|
| 1837 | + | version = "0.10.3" |
|
| 1838 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1839 | + | checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" |
|
| 1840 | + | dependencies = [ |
|
| 1841 | + | "chacha20", |
|
| 1842 | + | "getrandom 0.4.3", |
|
| 1843 | + | "rand_core 0.10.1", |
|
| 1844 | + | ] |
|
| 1845 | + | ||
| 1846 | + | [[package]] |
|
| 1447 | 1847 | name = "rand_chacha" |
|
| 1448 | 1848 | version = "0.3.1" |
|
| 1449 | 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 81 unchanged lines |
||
| 1531 | 1931 | ] |
|
| 1532 | 1932 | ||
| 1533 | 1933 | [[package]] |
|
| 1934 | + | name = "redox_syscall" |
|
| 1935 | + | version = "0.5.18" |
|
| 1936 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1937 | + | checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" |
|
| 1938 | + | dependencies = [ |
|
| 1939 | + | "bitflags", |
|
| 1940 | + | ] |
|
| 1941 | + | ||
| 1942 | + | [[package]] |
|
| 1534 | 1943 | name = "regex-automata" |
|
| 1535 | 1944 | version = "0.4.18" |
|
| 1536 | 1945 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 11 unchanged lines |
||
| 1548 | 1957 | checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" |
|
| 1549 | 1958 | ||
| 1550 | 1959 | [[package]] |
|
| 1960 | + | name = "reqwest" |
|
| 1961 | + | version = "0.13.5" |
|
| 1962 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1963 | + | checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" |
|
| 1964 | + | dependencies = [ |
|
| 1965 | + | "base64 0.23.1", |
|
| 1966 | + | "bytes", |
|
| 1967 | + | "futures-core", |
|
| 1968 | + | "futures-util", |
|
| 1969 | + | "h2", |
|
| 1970 | + | "http", |
|
| 1971 | + | "http-body", |
|
| 1972 | + | "http-body-util", |
|
| 1973 | + | "hyper", |
|
| 1974 | + | "hyper-rustls", |
|
| 1975 | + | "hyper-util", |
|
| 1976 | + | "js-sys", |
|
| 1977 | + | "log", |
|
| 1978 | + | "percent-encoding", |
|
| 1979 | + | "pin-project-lite", |
|
| 1980 | + | "rustls", |
|
| 1981 | + | "rustls-pki-types", |
|
| 1982 | + | "rustls-platform-verifier", |
|
| 1983 | + | "sync_wrapper", |
|
| 1984 | + | "tokio", |
|
| 1985 | + | "tokio-rustls", |
|
| 1986 | + | "tokio-util", |
|
| 1987 | + | "tower", |
|
| 1988 | + | "tower-http", |
|
| 1989 | + | "tower-service", |
|
| 1990 | + | "url", |
|
| 1991 | + | "wasm-bindgen", |
|
| 1992 | + | "wasm-bindgen-futures", |
|
| 1993 | + | "wasm-streams", |
|
| 1994 | + | "web-sys", |
|
| 1995 | + | ] |
|
| 1996 | + | ||
| 1997 | + | [[package]] |
|
| 1551 | 1998 | name = "ring" |
|
| 1552 | 1999 | version = "0.17.14" |
|
| 1553 | 2000 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 60 unchanged lines |
||
| 1614 | 2061 | ] |
|
| 1615 | 2062 | ||
| 1616 | 2063 | [[package]] |
|
| 2064 | + | name = "rustls-native-certs" |
|
| 2065 | + | version = "0.8.4" |
|
| 2066 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2067 | + | checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" |
|
| 2068 | + | dependencies = [ |
|
| 2069 | + | "openssl-probe", |
|
| 2070 | + | "rustls-pki-types", |
|
| 2071 | + | "schannel", |
|
| 2072 | + | "security-framework", |
|
| 2073 | + | ] |
|
| 2074 | + | ||
| 2075 | + | [[package]] |
|
| 1617 | 2076 | name = "rustls-pemfile" |
|
| 1618 | 2077 | version = "2.2.0" |
|
| 1619 | 2078 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 9 unchanged lines |
||
| 1629 | 2088 | checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" |
|
| 1630 | 2089 | dependencies = [ |
|
| 1631 | 2090 | "zeroize", |
|
| 2091 | + | ] |
|
| 2092 | + | ||
| 2093 | + | [[package]] |
|
| 2094 | + | name = "rustls-platform-verifier" |
|
| 2095 | + | version = "0.7.0" |
|
| 2096 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2097 | + | checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" |
|
| 2098 | + | dependencies = [ |
|
| 2099 | + | "core-foundation", |
|
| 2100 | + | "core-foundation-sys", |
|
| 2101 | + | "jni", |
|
| 2102 | + | "log", |
|
| 2103 | + | "once_cell", |
|
| 2104 | + | "rustls", |
|
| 2105 | + | "rustls-native-certs", |
|
| 2106 | + | "rustls-platform-verifier-android", |
|
| 2107 | + | "rustls-webpki", |
|
| 2108 | + | "security-framework", |
|
| 2109 | + | "security-framework-sys", |
|
| 2110 | + | "webpki-root-certs", |
|
| 2111 | + | "windows-sys 0.52.0", |
|
| 1632 | 2112 | ] |
|
| 2113 | + | ||
| 2114 | + | [[package]] |
|
| 2115 | + | name = "rustls-platform-verifier-android" |
|
| 2116 | + | version = "0.1.1" |
|
| 2117 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2118 | + | checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" |
|
| 1633 | 2119 | ||
| 1634 | 2120 | [[package]] |
|
| 1635 | 2121 | name = "rustls-webpki" |
|
| … | 40 unchanged lines |
||
| 1676 | 2162 | ] |
|
| 1677 | 2163 | ||
| 1678 | 2164 | [[package]] |
|
| 2165 | + | name = "schannel" |
|
| 2166 | + | version = "0.1.29" |
|
| 2167 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2168 | + | checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" |
|
| 2169 | + | dependencies = [ |
|
| 2170 | + | "windows-sys 0.61.2", |
|
| 2171 | + | ] |
|
| 2172 | + | ||
| 2173 | + | [[package]] |
|
| 2174 | + | name = "scopeguard" |
|
| 2175 | + | version = "1.2.0" |
|
| 2176 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2177 | + | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" |
|
| 2178 | + | ||
| 2179 | + | [[package]] |
|
| 2180 | + | name = "security-framework" |
|
| 2181 | + | version = "3.7.0" |
|
| 2182 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2183 | + | checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" |
|
| 2184 | + | dependencies = [ |
|
| 2185 | + | "bitflags", |
|
| 2186 | + | "core-foundation", |
|
| 2187 | + | "core-foundation-sys", |
|
| 2188 | + | "libc", |
|
| 2189 | + | "security-framework-sys", |
|
| 2190 | + | ] |
|
| 2191 | + | ||
| 2192 | + | [[package]] |
|
| 2193 | + | name = "security-framework-sys" |
|
| 2194 | + | version = "2.17.0" |
|
| 2195 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2196 | + | checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" |
|
| 2197 | + | dependencies = [ |
|
| 2198 | + | "core-foundation-sys", |
|
| 2199 | + | "libc", |
|
| 2200 | + | ] |
|
| 2201 | + | ||
| 2202 | + | [[package]] |
|
| 1679 | 2203 | name = "semver" |
|
| 1680 | 2204 | version = "1.0.28" |
|
| 1681 | 2205 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 73 unchanged lines |
||
| 1755 | 2279 | dependencies = [ |
|
| 1756 | 2280 | "cfg-if", |
|
| 1757 | 2281 | "cpufeatures", |
|
| 1758 | - | "digest", |
|
| 2282 | + | "digest 0.11.3", |
|
| 1759 | 2283 | ] |
|
| 1760 | 2284 | ||
| 1761 | 2285 | [[package]] |
|
| … | 2 unchanged lines |
||
| 1764 | 2288 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1765 | 2289 | checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" |
|
| 1766 | 2290 | dependencies = [ |
|
| 1767 | - | "digest", |
|
| 2291 | + | "digest 0.11.3", |
|
| 1768 | 2292 | "keccak", |
|
| 1769 | 2293 | "sponge-cursor", |
|
| 1770 | 2294 | ] |
|
| … | 10 unchanged lines |
||
| 1781 | 2305 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1782 | 2306 | checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" |
|
| 1783 | 2307 | dependencies = [ |
|
| 1784 | - | "digest", |
|
| 2308 | + | "digest 0.11.3", |
|
| 1785 | 2309 | "rand_core 0.10.1", |
|
| 1786 | 2310 | ] |
|
| 1787 | 2311 | ||
| … | 2 unchanged lines |
||
| 1790 | 2314 | version = "0.3.10" |
|
| 1791 | 2315 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1792 | 2316 | checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" |
|
| 2317 | + | ||
| 2318 | + | [[package]] |
|
| 2319 | + | name = "simd_cesu8" |
|
| 2320 | + | version = "1.2.0" |
|
| 2321 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2322 | + | checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" |
|
| 2323 | + | dependencies = [ |
|
| 2324 | + | "rustc_version", |
|
| 2325 | + | "simdutf8", |
|
| 2326 | + | ] |
|
| 2327 | + | ||
| 2328 | + | [[package]] |
|
| 2329 | + | name = "simdutf8" |
|
| 2330 | + | version = "0.1.5" |
|
| 2331 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2332 | + | checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" |
|
| 1793 | 2333 | ||
| 1794 | 2334 | [[package]] |
|
| 1795 | 2335 | name = "similar" |
|
| … | 24 unchanged lines |
||
| 1820 | 2360 | ] |
|
| 1821 | 2361 | ||
| 1822 | 2362 | [[package]] |
|
| 2363 | + | name = "spin" |
|
| 2364 | + | version = "0.10.1" |
|
| 2365 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2366 | + | checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" |
|
| 2367 | + | ||
| 2368 | + | [[package]] |
|
| 1823 | 2369 | name = "spki" |
|
| 1824 | 2370 | version = "0.8.0" |
|
| 1825 | 2371 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 54 unchanged lines |
||
| 1880 | 2426 | version = "1.0.2" |
|
| 1881 | 2427 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 1882 | 2428 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" |
|
| 2429 | + | dependencies = [ |
|
| 2430 | + | "futures-core", |
|
| 2431 | + | ] |
|
| 1883 | 2432 | ||
| 1884 | 2433 | [[package]] |
|
| 1885 | 2434 | name = "synstructure" |
|
| … | 174 unchanged lines |
||
| 2060 | 2609 | "tower-layer", |
|
| 2061 | 2610 | "tower-service", |
|
| 2062 | 2611 | "tracing", |
|
| 2612 | + | ] |
|
| 2613 | + | ||
| 2614 | + | [[package]] |
|
| 2615 | + | name = "tower-http" |
|
| 2616 | + | version = "0.6.11" |
|
| 2617 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2618 | + | checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" |
|
| 2619 | + | dependencies = [ |
|
| 2620 | + | "bitflags", |
|
| 2621 | + | "bytes", |
|
| 2622 | + | "futures-util", |
|
| 2623 | + | "http", |
|
| 2624 | + | "http-body", |
|
| 2625 | + | "pin-project-lite", |
|
| 2626 | + | "tower", |
|
| 2627 | + | "tower-layer", |
|
| 2628 | + | "tower-service", |
|
| 2629 | + | "url", |
|
| 2063 | 2630 | ] |
|
| 2064 | 2631 | ||
| 2065 | 2632 | [[package]] |
|
| … | 16 unchanged lines |
||
| 2082 | 2649 | dependencies = [ |
|
| 2083 | 2650 | "log", |
|
| 2084 | 2651 | "pin-project-lite", |
|
| 2652 | + | "tracing-attributes", |
|
| 2085 | 2653 | "tracing-core", |
|
| 2086 | 2654 | ] |
|
| 2087 | 2655 | ||
| 2088 | 2656 | [[package]] |
|
| 2657 | + | name = "tracing-attributes" |
|
| 2658 | + | version = "0.1.31" |
|
| 2659 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2660 | + | checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" |
|
| 2661 | + | dependencies = [ |
|
| 2662 | + | "proc-macro2", |
|
| 2663 | + | "quote", |
|
| 2664 | + | "syn 2.0.119", |
|
| 2665 | + | ] |
|
| 2666 | + | ||
| 2667 | + | [[package]] |
|
| 2089 | 2668 | name = "tracing-core" |
|
| 2090 | 2669 | version = "0.1.36" |
|
| 2091 | 2670 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 3 unchanged lines |
||
| 2095 | 2674 | ] |
|
| 2096 | 2675 | ||
| 2097 | 2676 | [[package]] |
|
| 2677 | + | name = "try-lock" |
|
| 2678 | + | version = "0.2.5" |
|
| 2679 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2680 | + | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" |
|
| 2681 | + | ||
| 2682 | + | [[package]] |
|
| 2098 | 2683 | name = "typenum" |
|
| 2099 | 2684 | version = "1.20.1" |
|
| 2100 | 2685 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 64 unchanged lines |
||
| 2165 | 2750 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" |
|
| 2166 | 2751 | ||
| 2167 | 2752 | [[package]] |
|
| 2753 | + | name = "version_check" |
|
| 2754 | + | version = "0.9.5" |
|
| 2755 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2756 | + | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" |
|
| 2757 | + | ||
| 2758 | + | [[package]] |
|
| 2168 | 2759 | name = "wait-timeout" |
|
| 2169 | 2760 | version = "0.2.1" |
|
| 2170 | 2761 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 13 unchanged lines |
||
| 2184 | 2775 | ] |
|
| 2185 | 2776 | ||
| 2186 | 2777 | [[package]] |
|
| 2778 | + | name = "want" |
|
| 2779 | + | version = "0.3.1" |
|
| 2780 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2781 | + | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" |
|
| 2782 | + | dependencies = [ |
|
| 2783 | + | "try-lock", |
|
| 2784 | + | ] |
|
| 2785 | + | ||
| 2786 | + | [[package]] |
|
| 2187 | 2787 | name = "wasi" |
|
| 2188 | 2788 | version = "0.11.1+wasi-snapshot-preview1" |
|
| 2189 | 2789 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 19 unchanged lines |
||
| 2209 | 2809 | "rustversion", |
|
| 2210 | 2810 | "wasm-bindgen-macro", |
|
| 2211 | 2811 | "wasm-bindgen-shared", |
|
| 2812 | + | ] |
|
| 2813 | + | ||
| 2814 | + | [[package]] |
|
| 2815 | + | name = "wasm-bindgen-futures" |
|
| 2816 | + | version = "0.4.79" |
|
| 2817 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2818 | + | checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" |
|
| 2819 | + | dependencies = [ |
|
| 2820 | + | "js-sys", |
|
| 2821 | + | "tokio", |
|
| 2822 | + | "wasm-bindgen", |
|
| 2212 | 2823 | ] |
|
| 2213 | 2824 | ||
| 2214 | 2825 | [[package]] |
|
| … | 29 unchanged lines |
||
| 2244 | 2855 | ] |
|
| 2245 | 2856 | ||
| 2246 | 2857 | [[package]] |
|
| 2858 | + | name = "wasm-streams" |
|
| 2859 | + | version = "0.5.0" |
|
| 2860 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2861 | + | checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" |
|
| 2862 | + | dependencies = [ |
|
| 2863 | + | "futures-util", |
|
| 2864 | + | "js-sys", |
|
| 2865 | + | "wasm-bindgen", |
|
| 2866 | + | "wasm-bindgen-futures", |
|
| 2867 | + | "web-sys", |
|
| 2868 | + | ] |
|
| 2869 | + | ||
| 2870 | + | [[package]] |
|
| 2871 | + | name = "web-sys" |
|
| 2872 | + | version = "0.3.106" |
|
| 2873 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2874 | + | checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" |
|
| 2875 | + | dependencies = [ |
|
| 2876 | + | "js-sys", |
|
| 2877 | + | "wasm-bindgen", |
|
| 2878 | + | ] |
|
| 2879 | + | ||
| 2880 | + | [[package]] |
|
| 2247 | 2881 | name = "web-time" |
|
| 2248 | 2882 | version = "1.1.0" |
|
| 2249 | 2883 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| … | 1 unchanged lines |
||
| 2251 | 2885 | dependencies = [ |
|
| 2252 | 2886 | "js-sys", |
|
| 2253 | 2887 | "wasm-bindgen", |
|
| 2888 | + | ] |
|
| 2889 | + | ||
| 2890 | + | [[package]] |
|
| 2891 | + | name = "webpki-root-certs" |
|
| 2892 | + | version = "1.0.9" |
|
| 2893 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2894 | + | checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" |
|
| 2895 | + | dependencies = [ |
|
| 2896 | + | "rustls-pki-types", |
|
| 2254 | 2897 | ] |
|
| 2255 | 2898 | ||
| 2256 | 2899 | [[package]] |
|
| … | 21 unchanged lines |
||
| 2278 | 2921 | checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" |
|
| 2279 | 2922 | dependencies = [ |
|
| 2280 | 2923 | "windows-sys 0.61.2", |
|
| 2924 | + | ] |
|
| 2925 | + | ||
| 2926 | + | [[package]] |
|
| 2927 | + | name = "windows-core" |
|
| 2928 | + | version = "0.62.2" |
|
| 2929 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2930 | + | checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" |
|
| 2931 | + | dependencies = [ |
|
| 2932 | + | "windows-implement", |
|
| 2933 | + | "windows-interface", |
|
| 2934 | + | "windows-link", |
|
| 2935 | + | "windows-result", |
|
| 2936 | + | "windows-strings", |
|
| 2281 | 2937 | ] |
|
| 2282 | 2938 | ||
| 2283 | 2939 | [[package]] |
|
| 2940 | + | name = "windows-implement" |
|
| 2941 | + | version = "0.60.2" |
|
| 2942 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2943 | + | checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" |
|
| 2944 | + | dependencies = [ |
|
| 2945 | + | "proc-macro2", |
|
| 2946 | + | "quote", |
|
| 2947 | + | "syn 2.0.119", |
|
| 2948 | + | ] |
|
| 2949 | + | ||
| 2950 | + | [[package]] |
|
| 2951 | + | name = "windows-interface" |
|
| 2952 | + | version = "0.59.3" |
|
| 2953 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2954 | + | checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" |
|
| 2955 | + | dependencies = [ |
|
| 2956 | + | "proc-macro2", |
|
| 2957 | + | "quote", |
|
| 2958 | + | "syn 2.0.119", |
|
| 2959 | + | ] |
|
| 2960 | + | ||
| 2961 | + | [[package]] |
|
| 2284 | 2962 | name = "windows-link" |
|
| 2285 | 2963 | version = "0.2.1" |
|
| 2286 | 2964 | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2287 | 2965 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" |
|
| 2966 | + | ||
| 2967 | + | [[package]] |
|
| 2968 | + | name = "windows-result" |
|
| 2969 | + | version = "0.4.1" |
|
| 2970 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2971 | + | checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" |
|
| 2972 | + | dependencies = [ |
|
| 2973 | + | "windows-link", |
|
| 2974 | + | ] |
|
| 2975 | + | ||
| 2976 | + | [[package]] |
|
| 2977 | + | name = "windows-strings" |
|
| 2978 | + | version = "0.5.1" |
|
| 2979 | + | source = "registry+https://github.com/rust-lang/crates.io-index" |
|
| 2980 | + | checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" |
|
| 2981 | + | dependencies = [ |
|
| 2982 | + | "windows-link", |
|
| 2983 | + | ] |
|
| 2288 | 2984 | ||
| 2289 | 2985 | [[package]] |
|
| 2290 | 2986 | name = "windows-sys" |
|
| … | 271 unchanged lines |
modified Cargo.toml +2 −1
| 1 | 1 | [workspace] |
|
| 2 | - | members = ["crates/oxy-core", "crates/oxy-server", "."] |
|
| 2 | + | members = ["crates/oxy-core", "crates/oxy-server", "crates/oxy-strata", "."] |
|
| 3 | 3 | resolver = "2" |
|
| 4 | 4 | ||
| 5 | 5 | [workspace.package] |
|
| … | 16 unchanged lines |
||
| 22 | 22 | [dependencies] |
|
| 23 | 23 | oxy-core = { path = "crates/oxy-core" } |
|
| 24 | 24 | oxy-server = { path = "crates/oxy-server" } |
|
| 25 | + | oxy-strata = { path = "crates/oxy-strata" } |
|
| 25 | 26 | clap = { version = "4", features = ["derive"] } |
|
| 26 | 27 | anyhow = "1" |
|
| 27 | 28 | libc = "0.2" |
|
| … | 8 unchanged lines |
modified crates/oxy-core/src/protocol.rs +3 −3
| … | 531 unchanged lines |
||
| 532 | 532 | /// path made absolute (with its `file://` kept), so it still names the same |
|
| 533 | 533 | /// repository from inside the clone (oxygen#57). |
|
| 534 | 534 | pub fn stored_url(url: &str) -> String { |
|
| 535 | - | if url.starts_with("http://") || url.starts_with("https://") { |
|
| 536 | - | return url.to_string(); |
|
| 537 | - | } |
|
| 535 | + | // Only a path is made absolute: a plain one, or one behind file:// or |
|
| 536 | + | // dir://. Every other URL (a hub, a bucket) is kept as written. |
|
| 538 | 537 | let (prefix, path) = match (url.strip_prefix("file://"), url.strip_prefix("dir://")) { |
|
| 539 | 538 | (Some(p), _) => ("file://", p), |
|
| 540 | 539 | (_, Some(p)) => ("dir://", p), |
|
| 540 | + | _ if url.contains("://") => return url.to_string(), |
|
| 541 | 541 | _ => ("", url), |
|
| 542 | 542 | }; |
|
| 543 | 543 | match std::path::absolute(path) { |
|
| … | 701 unchanged lines |
modified crates/oxy-core/src/repo/tests.rs +6 −0
| … | 6933 unchanged lines |
||
| 6934 | 6934 | "https://hub.example/o/n" |
|
| 6935 | 6935 | ); |
|
| 6936 | 6936 | assert!(crate::protocol::stored_url("file://x").starts_with("file:///")); |
|
| 6937 | + | assert!(crate::protocol::stored_url("dir://x").starts_with("dir:///")); |
|
| 6938 | + | // A bucket is kept as written: it is not a path. |
|
| 6939 | + | assert_eq!( |
|
| 6940 | + | crate::protocol::stored_url("s3://bucket/prefix?profile=p"), |
|
| 6941 | + | "s3://bucket/prefix?profile=p" |
|
| 6942 | + | ); |
|
| 6937 | 6943 | let url = root.join("origin").to_string_lossy().to_string(); |
|
| 6938 | 6944 | let b = Repo::clone(&url, &root.join("b")).unwrap(); |
|
| 6939 | 6945 | assert_eq!(b.config.remotes["origin"], url); |
|
| … | 2 unchanged lines |
modified docs/stores.md +21 −10
| … | 140 unchanged lines |
||
| 141 | 141 | It then retries the write. Uploading packs needs no locking: two writers |
|
| 142 | 142 | uploading the same objects is harmless. |
|
| 143 | 143 | ||
| 144 | - | Before building on it, check each provider's current documentation for |
|
| 145 | - | conditional writes: S3 added `If-Match` on `PutObject` in 2024, and this |
|
| 146 | - | design depends on it. |
|
| 144 | + | **As built (2026-10-04):** the `object_store` crate 0.14.2 carries both |
|
| 145 | + | writes as `PutMode::Create` and `PutMode::Update(UpdateVersion { e_tag, |
|
| 146 | + | version })`; S3 needs `S3ConditionalPut::ETagMatch` switched on, which |
|
| 147 | + | sends `If-None-Match: *` and `If-Match: <ETag>`; GCS uses the generation |
|
| 148 | + | (`ifGenerationMatch`); Azure the ETag. A store's `Version` carries both |
|
| 149 | + | the ETag and the generation, so the same code serves all three. |
|
| 150 | + | Checked against the real services on 2026-10-03/04: every conditional |
|
| 151 | + | write refuses as required on S3, GCS and Azure. |
|
| 147 | 152 | ||
| 148 | 153 | ## 6. Safety |
|
| 149 | 154 | ||
| … | 22 unchanged lines |
||
| 172 | 177 | create-only refusals, compare-and-swap conflicts, concurrent pushers |
|
| 173 | 178 | merging, an interrupted push leaving the remote valid, gc with a |
|
| 174 | 179 | concurrent reader, and a repository-id mismatch being refused. |
|
| 175 | - | - **Emulators in CI** (Flow jobs have no network): |
|
| 176 | - | - MinIO for S3, if its conditional writes match S3's, otherwise |
|
| 177 | - | LocalStack; |
|
| 178 | - | - `fake-gcs-server` for GCS. |
|
| 179 | - | - **Real buckets** (Andy's AWS and GCP accounts) for a nightly or |
|
| 180 | - | manual test run. It needs network access, so it runs outside the |
|
| 181 | - | isolated tier, with credentials scoped to a test prefix. |
|
| 180 | + | - **In CI** (Flow jobs have no network): the in-memory and directory |
|
| 181 | + | stores run the conformance check and the whole push/clone/pull flow. |
|
| 182 | + | - **Real buckets** for the cloud stores, run by hand from a machine that |
|
| 183 | + | holds restricted credentials: `crates/oxy-strata/tests/cloud.rs` reads |
|
| 184 | + | `OXY_TEST_S3`, `OXY_TEST_GCS` and `OXY_TEST_AZURE` (each a store URL |
|
| 185 | + | under a test prefix, with `OXY_TEST_S3_PROFILE`, |
|
| 186 | + | `OXY_TEST_GCS_CREDENTIALS` and `OXY_TEST_AZURE_CREDENTIALS` naming the |
|
| 187 | + | identity), runs the conformance check and a repository round trip under |
|
| 188 | + | a prefix of its own, and deletes what it wrote. Unset, the tests say so |
|
| 189 | + | and pass. |
|
| 190 | + | - **Emulators** were tried and dropped: MinIO's images are no longer |
|
| 191 | + | published publicly, and `fake-gcs-server` rejects the client's XML |
|
| 192 | + | uploads. The real services are the test. |
|
| 182 | 193 | ||
| 183 | 194 | ## 8. Order of work |
|
| 184 | 195 | ||
| … | 31 unchanged lines |
modified src/cli/mod.rs +2 −0
| … | 997 unchanged lines |
||
| 998 | 998 | ||
| 999 | 999 | /// Parse arguments and run; returns the process exit code. |
|
| 1000 | 1000 | pub fn run() -> Result<i32> { |
|
| 1001 | + | // The cloud stores (s3://, gs://, az://) for remotes kept on them. |
|
| 1002 | + | oxy_strata::register(); |
|
| 1001 | 1003 | let cli = Cli::parse(); |
|
| 1002 | 1004 | ui::init(cli.plain); |
|
| 1003 | 1005 | AT_OP.set(cli.at_op.clone()).ok(); |
|
| … | 314 unchanged lines |