lance_io/object_store/providers/
gcp.rs1use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
5
6use object_store::ObjectStore as OSObjectStore;
7use object_store_opendal::OpendalStore;
8use opendal::{Operator, services::Gcs};
9
10use object_store::{
11 RetryConfig, StaticCredentialProvider,
12 gcp::{GcpCredential, GoogleCloudStorageBuilder, GoogleConfigKey},
13};
14use url::Url;
15
16use crate::object_store::{
17 DEFAULT_CLOUD_BLOCK_SIZE, DEFAULT_CLOUD_IO_PARALLELISM, DEFAULT_MAX_IOP_SIZE, ObjectStore,
18 ObjectStoreParams, ObjectStoreProvider, StorageOptions, StorageOptionsAccessor,
19 dynamic_credentials::build_dynamic_credential_provider,
20 throttle::{AimdThrottleConfig, AimdThrottledStore},
21};
22use lance_core::error::{Error, Result};
23
24#[derive(Default, Debug)]
25pub struct GcsStoreProvider;
26
27impl GcsStoreProvider {
28 async fn build_opendal_gcs_store(
29 &self,
30 base_path: &Url,
31 storage_options: &StorageOptions,
32 ) -> Result<Arc<dyn OSObjectStore>> {
33 let bucket = base_path
34 .host_str()
35 .ok_or_else(|| Error::invalid_input("GCS URL must contain bucket name"))?
36 .to_string();
37
38 let prefix = base_path.path().trim_start_matches('/').to_string();
39
40 let mut config_map: HashMap<String, String> = storage_options.0.clone();
43
44 config_map.insert("bucket".to_string(), bucket);
46
47 if !prefix.is_empty() {
48 config_map.insert("root".to_string(), format!("/{}", prefix));
49 }
50
51 let operator = Operator::from_iter::<Gcs>(config_map)
52 .map_err(|e| Error::invalid_input(format!("Failed to create GCS operator: {:?}", e)))?
53 .finish();
54
55 Ok(Arc::new(OpendalStore::new(operator)) as Arc<dyn OSObjectStore>)
56 }
57
58 async fn build_google_cloud_store(
59 &self,
60 base_path: &Url,
61 storage_options: &StorageOptions,
62 accessor: Option<Arc<StorageOptionsAccessor>>,
63 ) -> Result<Arc<dyn OSObjectStore>> {
64 let retry_config = RetryConfig {
67 backoff: Default::default(),
68 max_retries: storage_options.client_max_retries(),
69 retry_timeout: Duration::from_secs(storage_options.client_retry_timeout()),
70 };
71
72 let mut builder = GoogleCloudStorageBuilder::new()
73 .with_url(base_path.as_ref())
74 .with_retry(retry_config)
75 .with_client_options(storage_options.client_options()?);
76 for (key, value) in storage_options.as_gcs_options() {
77 builder = builder.with_config(key, value);
78 }
79
80 if let Some(credentials) =
81 build_dynamic_credential_provider::<GcpCredential>(accessor).await?
82 {
83 builder = builder.with_credentials(credentials);
84 } else if let Some(storage_token) = storage_options.get("google_storage_token") {
85 let credential = GcpCredential {
86 bearer: storage_token.clone(),
87 };
88 let credential_provider = Arc::new(StaticCredentialProvider::new(credential)) as _;
89 builder = builder.with_credentials(credential_provider);
90 }
91
92 #[cfg(feature = "metrics")]
93 {
94 builder = builder.with_http_connector(
95 crate::object_store::metrics::MeteringHttpConnector::new(
96 self.calculate_object_store_prefix(base_path, Some(&storage_options.0))?,
97 ),
98 );
99 }
100
101 Ok(Arc::new(builder.build()?) as Arc<dyn OSObjectStore>)
102 }
103}
104
105#[async_trait::async_trait]
106impl ObjectStoreProvider for GcsStoreProvider {
107 async fn new_store(&self, base_path: Url, params: &ObjectStoreParams) -> Result<ObjectStore> {
108 let block_size = params.block_size.unwrap_or(DEFAULT_CLOUD_BLOCK_SIZE);
109 let mut storage_options =
110 StorageOptions::new(params.storage_options().cloned().unwrap_or_default());
111 storage_options.with_env_gcs();
112 let download_retry_count = storage_options.download_retry_count();
113
114 let use_opendal = storage_options
115 .0
116 .get("use_opendal")
117 .map(|v| v.as_str() == "true")
118 .unwrap_or(false);
119
120 let accessor = params.get_accessor();
121
122 let inner = if use_opendal {
123 self.build_opendal_gcs_store(&base_path, &storage_options)
126 .await?
127 } else {
128 self.build_google_cloud_store(&base_path, &storage_options, accessor)
129 .await?
130 };
131 let throttle_config = AimdThrottleConfig::from_storage_options(params.storage_options())?;
132 let inner = if throttle_config.is_disabled() {
133 inner
134 } else {
135 Arc::new(AimdThrottledStore::new(inner, throttle_config)?) as Arc<dyn OSObjectStore>
136 };
137
138 Ok(ObjectStore {
139 inner,
140 scheme: String::from("gs"),
141 block_size,
142 max_iop_size: *DEFAULT_MAX_IOP_SIZE,
143 use_constant_size_upload_parts: false,
144 list_is_lexically_ordered: true,
145 io_parallelism: DEFAULT_CLOUD_IO_PARALLELISM,
146 download_retry_count,
147 io_tracker: Default::default(),
148 store_prefix: self
149 .calculate_object_store_prefix(&base_path, params.storage_options())?,
150 })
151 }
152}
153
154impl StorageOptions {
155 pub fn with_env_gcs(&mut self) {
157 for (os_key, os_value) in std::env::vars_os() {
158 if let (Some(key), Some(value)) = (os_key.to_str(), os_value.to_str()) {
159 let lowercase_key = key.to_ascii_lowercase();
160 let token_key = "google_storage_token";
161
162 if let Ok(config_key) = GoogleConfigKey::from_str(&lowercase_key) {
163 if !self.0.contains_key(config_key.as_ref()) {
164 self.0
165 .insert(config_key.as_ref().to_string(), value.to_string());
166 }
167 }
168 else if lowercase_key == token_key && !self.0.contains_key(token_key) {
170 self.0.insert(token_key.to_string(), value.to_string());
171 }
172 }
173 }
174 }
175
176 pub fn as_gcs_options(&self) -> HashMap<GoogleConfigKey, String> {
178 self.0
179 .iter()
180 .filter_map(|(key, value)| {
181 let gcs_key = GoogleConfigKey::from_str(&key.to_ascii_lowercase()).ok()?;
182 Some((gcs_key, value.clone()))
183 })
184 .collect()
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191 use std::sync::Arc;
192
193 use crate::object_store::test_utils::StaticMockStorageOptionsProvider;
194 use crate::object_store::{ObjectStoreParams, StorageOptionsAccessor};
195 use std::collections::HashMap;
196
197 #[test]
198 fn test_gcs_store_path() {
199 let provider = GcsStoreProvider;
200
201 let url = Url::parse("gs://bucket/path/to/file").unwrap();
202 let path = provider.extract_path(&url).unwrap();
203 let expected_path = object_store::path::Path::from("path/to/file");
204 assert_eq!(path, expected_path);
205 }
206
207 #[tokio::test]
208 async fn test_use_opendal_flag() {
209 let provider = GcsStoreProvider;
210 let url = Url::parse("gs://test-bucket/path").unwrap();
211 let params_with_flag = ObjectStoreParams {
212 storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
213 HashMap::from([
214 ("use_opendal".to_string(), "true".to_string()),
215 (
216 "service_account".to_string(),
217 "test@example.iam.gserviceaccount.com".to_string(),
218 ),
219 ]),
220 ))),
221 ..Default::default()
222 };
223
224 let store = provider
225 .new_store(url.clone(), ¶ms_with_flag)
226 .await
227 .unwrap();
228 assert_eq!(store.scheme, "gs");
229 }
230
231 #[tokio::test]
232 async fn test_dynamic_gcp_credentials_provider() {
233 let accessor = Arc::new(StorageOptionsAccessor::with_provider(Arc::new(
234 StaticMockStorageOptionsProvider {
235 options: HashMap::from([(
236 "google_storage_token".to_string(),
237 "gcp-token".to_string(),
238 )]),
239 },
240 )));
241
242 let credentials = build_dynamic_credential_provider::<GcpCredential>(Some(accessor))
243 .await
244 .expect("dynamic gcp credentials should build")
245 .expect("expected credential provider")
246 .get_credential()
247 .await
248 .expect("expected gcp credential");
249
250 assert_eq!(credentials.bearer, "gcp-token");
251 }
252}