1mod wire;
22
23#[doc(hidden)]
24pub use wire::fuzz as fuzz_wire;
25
26use crate::key::{check_prefix, encode_component};
27use crate::net::{backoff, is_allowed_endpoint, is_retriable_status, transport_error};
28use crate::token::TokenProvider;
29use crate::{Bytes, Cond, Error, Key, Meta, Object, ObjectStore, Result};
30use std::future::Future;
31use std::ops::Range;
32use std::time::Duration;
33
34#[derive(Debug, Clone)]
36pub struct GcsConfig {
37 pub endpoint: String,
39 pub timeout: Duration,
41 pub max_retries: u32,
43 pub max_object_bytes: u64,
45 pub resumable_threshold: usize,
47}
48
49impl Default for GcsConfig {
50 fn default() -> Self {
51 Self {
52 endpoint: "https://storage.googleapis.com".into(),
53 timeout: Duration::from_secs(60),
54 max_retries: 5,
55 max_object_bytes: 4 << 30,
56 resumable_threshold: 8 << 20,
57 }
58 }
59}
60
61#[derive(Debug)]
63pub struct Gcs<T: TokenProvider> {
64 http: reqwest::Client,
65 tokens: T,
66 cfg: GcsConfig,
67 api: String,
69 upload: String,
71}
72
73impl<T: TokenProvider> Gcs<T> {
74 pub fn new(bucket: &str, tokens: T, cfg: GcsConfig) -> Result<Self> {
76 if !is_valid_bucket(bucket) {
77 return Err(Error::InvalidKey { reason: "invalid bucket name" });
78 }
79 let endpoint = cfg.endpoint.trim_end_matches('/').to_owned();
80 if !is_allowed_endpoint(&endpoint) {
81 return Err(Error::protocol(
82 "the endpoint must be https:// (http:// only for a loopback emulator)",
83 ));
84 }
85 let http = reqwest::Client::builder()
86 .timeout(cfg.timeout)
87 .redirect(reqwest::redirect::Policy::none())
88 .pool_idle_timeout(Duration::from_secs(90))
89 .user_agent(concat!("objstore-r/", env!("CARGO_PKG_VERSION")))
90 .build()
91 .map_err(|e| Error::unavailable(format!("http client: {e}"), false))?;
92 Ok(Self {
93 http,
94 tokens,
95 api: format!("{endpoint}/storage/v1/b/{bucket}"),
96 upload: format!("{endpoint}/upload/storage/v1/b/{bucket}"),
97 cfg,
98 })
99 }
100
101 pub fn tokens(&self) -> &T {
103 &self.tokens
104 }
105
106 fn object_url(&self, key: &str) -> String {
107 let mut u = String::with_capacity(self.api.len() + key.len() * 3 + 4);
108 u.push_str(&self.api);
109 u.push_str("/o/");
110 encode_component(key, &mut u);
111 u
112 }
113
114 async fn authed(&self, rb: reqwest::RequestBuilder) -> Result<reqwest::RequestBuilder> {
115 let t = self.tokens.token().await?;
116 let mut rb = rb;
117 if !t.secret().is_empty() {
118 rb = rb.bearer_auth(t.secret());
119 }
120 if let Some(q) = self.tokens.quota_project() {
121 rb = rb.header("x-goog-user-project", q);
122 }
123 Ok(rb)
124 }
125
126 async fn send(&self, build: impl Fn() -> reqwest::RequestBuilder) -> Result<reqwest::Response> {
128 let mut attempt = 0;
129 loop {
130 let rb = self.authed(build()).await?;
131 let outcome = rb.send().await;
132 let retry = match &outcome {
133 Ok(r) => is_retriable_status(r.status().as_u16()),
134 Err(e) => e.is_timeout() || e.is_connect() || e.is_request(),
135 };
136 if !retry || attempt >= self.cfg.max_retries {
137 return outcome.map_err(|e| transport_error(&e));
138 }
139 attempt += 1;
140 tokio::time::sleep(backoff(attempt)).await;
141 }
142 }
143
144 async fn body(&self, resp: reqwest::Response) -> Result<Bytes> {
145 if resp.content_length().is_some_and(|n| n > self.cfg.max_object_bytes) {
146 return Err(Error::TooLarge { limit: self.cfg.max_object_bytes });
147 }
148 let mut resp = resp;
149 let mut buf = bytes::BytesMut::new();
150 while let Some(chunk) = resp.chunk().await.map_err(|e| transport_error(&e))? {
151 if (buf.len() + chunk.len()) as u64 > self.cfg.max_object_bytes {
152 return Err(Error::TooLarge { limit: self.cfg.max_object_bytes });
153 }
154 buf.extend_from_slice(&chunk);
155 }
156 Ok(buf.freeze())
157 }
158
159 async fn head_inner(&self, key: &str) -> Result<Option<Meta>> {
160 let url = format!("{}?fields=name,size,generation", self.object_url(key));
161 let resp = self.send(|| self.http.get(&url)).await?;
162 match resp.status().as_u16() {
163 404 => Ok(None),
164 200 => {
165 let body = self.body(resp).await?;
166 wire::parse_meta(&body).map(Some)
167 }
168 _ => Err(status_error(resp).await),
169 }
170 }
171
172 async fn get_inner(&self, key: &str) -> Result<Option<Object>> {
173 let url = format!("{}?alt=media", self.object_url(key));
174 let resp = self.send(|| self.http.get(&url)).await?;
175 match resp.status().as_u16() {
176 404 => Ok(None),
177 200 => {
178 let header_gen = generation_header(&resp);
179 let bytes = self.body(resp).await?;
180 let generation = match header_gen {
181 Some(g) => g,
182 None => self.head_inner(key).await?.map_or(1, |m| m.generation),
184 };
185 Ok(Some(Object { bytes, generation }))
186 }
187 _ => Err(status_error(resp).await),
188 }
189 }
190
191 async fn range_inner(&self, key: &str, range: Range<u64>) -> Result<Bytes> {
192 if range.start >= range.end {
193 return match self.head_inner(key).await? {
194 Some(_) => Ok(Bytes::new()),
195 None => Err(Error::NotFound),
196 };
197 }
198 let url = format!("{}?alt=media", self.object_url(key));
199 let header = format!("bytes={}-{}", range.start, range.end - 1);
200 let resp = self.send(|| self.http.get(&url).header("range", &header)).await?;
201 match resp.status().as_u16() {
202 404 => Err(Error::NotFound),
203 416 => Ok(Bytes::new()),
204 206 => self.body(resp).await,
205 200 => {
207 let all = self.body(resp).await?;
208 Ok(all.slice(crate::clip(&range, all.len() as u64)))
209 }
210 _ => Err(status_error(resp).await),
211 }
212 }
213
214 async fn put_inner(&self, key: &str, body: Bytes, cond: Cond) -> Result<u64> {
215 let mut query = String::from("name=");
216 encode_component(key, &mut query);
217 match cond {
218 Cond::None => {}
219 Cond::CreateOnly => query.push_str("&ifGenerationMatch=0"),
220 Cond::IfGeneration(g) => {
221 use std::fmt::Write;
222 let _ = write!(query, "&ifGenerationMatch={g}");
223 }
224 }
225 let (status, resp) = if body.len() >= self.cfg.resumable_threshold {
226 self.put_resumable(&query, &body).await?
227 } else {
228 let url = format!("{}/o?uploadType=media&{query}", self.upload);
229 let resp = self
230 .send(|| {
231 self.http
232 .post(&url)
233 .header("content-type", "application/octet-stream")
234 .body(body.clone())
235 })
236 .await?;
237 (resp.status().as_u16(), resp)
238 };
239 match status {
240 200 | 201 => {
241 let b = self.body(resp).await?;
242 wire::parse_meta(&b).map(|m| m.generation)
243 }
244 412 if cond != Cond::None => {
245 if let Some(o) = self.get_inner(key).await? {
248 if o.bytes == body
249 && !matches!(cond, Cond::IfGeneration(g) if g == o.generation)
250 {
251 return Ok(o.generation);
252 }
253 }
254 Err(if cond == Cond::CreateOnly {
255 Error::Conflict
256 } else {
257 Error::PreconditionFailed
258 })
259 }
260 _ => Err(status_error(resp).await),
261 }
262 }
263
264 async fn put_resumable(&self, query: &str, body: &Bytes) -> Result<(u16, reqwest::Response)> {
265 let init = format!("{}/o?uploadType=resumable&{query}", self.upload);
266 let resp = self
267 .send(|| {
268 self.http
269 .post(&init)
270 .header("content-type", "application/json")
271 .header("x-upload-content-type", "application/octet-stream")
272 .header("x-upload-content-length", body.len().to_string())
273 .body("{}")
274 })
275 .await?;
276 let status = resp.status().as_u16();
277 if status != 200 && status != 201 {
278 return Ok((status, resp));
279 }
280 let session = resp
281 .headers()
282 .get("location")
283 .and_then(|v| v.to_str().ok())
284 .map(str::to_owned)
285 .ok_or_else(|| Error::protocol("resumable upload without a session URL"))?;
286 if !(session.starts_with(&self.upload) || session.starts_with("https://")) {
287 return Err(Error::protocol("resumable session URL on an unexpected host"));
288 }
289 let resp = self.send(|| self.http.put(&session).body(body.clone())).await?;
290 Ok((resp.status().as_u16(), resp))
291 }
292
293 async fn list_inner(&self, prefix: &str) -> Result<Vec<Meta>> {
294 let mut out = Vec::new();
295 let mut page: Option<String> = None;
296 loop {
297 let mut url = format!(
298 "{}/o?fields=items(name,size,generation),nextPageToken&maxResults=1000",
299 self.api
300 );
301 if !prefix.is_empty() {
302 url.push_str("&prefix=");
303 encode_component(prefix, &mut url);
304 }
305 if let Some(p) = &page {
306 url.push_str("&pageToken=");
307 encode_component(p, &mut url);
308 }
309 let resp = self.send(|| self.http.get(&url)).await?;
310 if resp.status().as_u16() != 200 {
311 return Err(status_error(resp).await);
312 }
313 let body = self.body(resp).await?;
314 let (items, next) = wire::parse_list(&body)?;
315 out.extend(
317 items
318 .into_iter()
319 .filter(|m| Key::parse(&m.key).is_ok() && m.key.starts_with(prefix)),
320 );
321 match next {
322 Some(n) if !n.is_empty() => page = Some(n),
323 _ => break,
324 }
325 }
326 out.sort();
327 Ok(out)
328 }
329
330 async fn delete_inner(&self, key: &str) -> Result<()> {
331 let url = self.object_url(key);
332 let resp = self.send(|| self.http.delete(&url)).await?;
333 match resp.status().as_u16() {
334 200 | 204 | 404 => Ok(()),
335 _ => Err(status_error(resp).await),
336 }
337 }
338}
339
340impl<T: TokenProvider> ObjectStore for Gcs<T> {
341 type Get<'a>
342 = impl Future<Output = Result<Option<Object>>> + 'a
343 where
344 Self: 'a;
345 type GetRange<'a>
346 = impl Future<Output = Result<Bytes>> + 'a
347 where
348 Self: 'a;
349 type Head<'a>
350 = impl Future<Output = Result<Option<Meta>>> + 'a
351 where
352 Self: 'a;
353 type Put<'a>
354 = impl Future<Output = Result<u64>> + 'a
355 where
356 Self: 'a;
357 type List<'a>
358 = impl Future<Output = Result<Vec<Meta>>> + 'a
359 where
360 Self: 'a;
361 type Delete<'a>
362 = impl Future<Output = Result<()>> + 'a
363 where
364 Self: 'a;
365
366 fn get<'a>(&'a self, key: &'a str) -> Self::Get<'a> {
367 async move { self.get_inner(Key::parse(key)?.as_str()).await }
368 }
369
370 fn get_range<'a>(&'a self, key: &'a str, range: Range<u64>) -> Self::GetRange<'a> {
371 async move { self.range_inner(Key::parse(key)?.as_str(), range).await }
372 }
373
374 fn head<'a>(&'a self, key: &'a str) -> Self::Head<'a> {
375 async move { self.head_inner(Key::parse(key)?.as_str()).await }
376 }
377
378 fn put<'a>(&'a self, key: &'a str, body: Bytes, cond: Cond) -> Self::Put<'a> {
379 async move { self.put_inner(Key::parse(key)?.as_str(), body, cond).await }
380 }
381
382 fn list<'a>(&'a self, prefix: &'a str) -> Self::List<'a> {
383 async move {
384 check_prefix(prefix)?;
385 self.list_inner(prefix).await
386 }
387 }
388
389 fn delete<'a>(&'a self, key: &'a str) -> Self::Delete<'a> {
390 async move { self.delete_inner(Key::parse(key)?.as_str()).await }
391 }
392}
393
394pub(crate) fn is_valid_bucket(b: &str) -> bool {
397 (3..=222).contains(&b.len())
398 && b.bytes().all(|c| {
399 c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, b'.' | b'_' | b'-')
400 })
401 && b.bytes().next().is_some_and(|c| c.is_ascii_alphanumeric())
402 && b.bytes().last().is_some_and(|c| c.is_ascii_alphanumeric())
403}
404
405fn generation_header(resp: &reqwest::Response) -> Option<u64> {
406 resp.headers()
407 .get("x-goog-generation")
408 .and_then(|v| v.to_str().ok())
409 .and_then(|s| s.parse().ok())
410 .filter(|g| *g != 0)
411}
412
413async fn status_error(resp: reqwest::Response) -> Error {
414 let status = resp.status().as_u16();
415 let body = resp.bytes().await.unwrap_or_default();
416 crate::net::status_error(status, &body)
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422
423 #[test]
424 fn bucket_names_and_endpoints() {
425 assert!(is_valid_bucket("my-project-sayasaya-state") && is_valid_bucket("a.b_c"));
426 for bad in ["ab", "-ab", "ab-", "AB", "a/b", &"a".repeat(223)] {
427 assert!(!is_valid_bucket(bad), "{bad}");
428 }
429 let cfg = |e: &str| GcsConfig { endpoint: e.into(), ..GcsConfig::default() };
430 let t = crate::token::Static::none;
431 assert!(
432 Gcs::new("bkt", t(), cfg("http://example.com")).is_err(),
433 "plain http to a remote host is refused"
434 );
435 assert!(Gcs::new("bkt", t(), cfg("http://127.0.0.1:4443")).is_ok());
436 assert!(Gcs::new("bkt", t(), cfg("https://storage.googleapis.com/")).is_ok());
437 }
438}