use crate::{
util::{self, send_request_with_retry},
CloudId, HttpClient, Path,
};
use anyhow::{Context as _, Result};
use tame_gcs::{objects::Object, BucketName, ObjectName};
use tracing::debug;
async fn acquire_gcs_token(cred_path: &Path) -> Result<tame_oauth::Token> {
use tame_oauth::gcp::{self, TokenProvider};
#[cfg(feature = "gcs")]
debug!("using credentials in {cred_path}");
let svc_account_info =
gcp::ServiceAccountInfo::deserialize(std::fs::read_to_string(cred_path)?)
.context("failed to deserilize service account")?;
let svc_account_access = gcp::ServiceAccountProvider::new(svc_account_info)?;
let token = match svc_account_access.get_token(&[tame_gcs::Scopes::ReadWrite])? {
gcp::TokenOrRequest::Request {
request,
scope_hash,
..
} => {
let client = HttpClient::new();
let res = client.execute(request.try_into().unwrap()).await?;
let mut builder = http::Response::builder()
.status(res.status())
.version(res.version());
let headers = builder
.headers_mut()
.context("failed to convert response headers")?;
headers.extend(
res.headers()
.into_iter()
.map(|(k, v)| (k.clone(), v.clone())),
);
let body = res.bytes().await?;
let response = builder.body(body)?;
svc_account_access.parse_token_response(scope_hash, response)?
}
gcp::TokenOrRequest::Token(_) => unreachable!(),
};
Ok(token)
}
pub struct GcsBackend {
client: HttpClient,
bucket: BucketName<'static>,
prefix: String,
obj: Object,
}
impl GcsBackend {
pub async fn new(
loc: crate::GcsLocation<'_>,
credentials: &Path,
timeout: std::time::Duration,
) -> Result<Self> {
let bucket = BucketName::try_from(loc.bucket.to_owned())?;
let token = acquire_gcs_token(credentials).await?;
use reqwest::header;
let hm = {
let mut hm = header::HeaderMap::new();
hm.insert(
header::AUTHORIZATION,
<tame_oauth::Token as std::convert::TryInto<header::HeaderValue>>::try_into(token)?,
);
hm
};
let client = HttpClient::builder()
.default_headers(hm)
.use_rustls_tls()
.timeout(timeout)
.build()?;
Ok(Self {
bucket,
client,
prefix: loc.prefix.to_owned(),
obj: Object::default(),
})
}
#[inline]
fn obj_name(&self, id: CloudId<'_>) -> Result<ObjectName<'static>> {
Ok(ObjectName::try_from(format!("{}{id}", self.prefix))?)
}
}
use std::fmt;
impl fmt::Debug for GcsBackend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("gcs")
.field("bucket", &self.bucket)
.field("prefix", &self.prefix)
.finish()
}
}
#[async_trait::async_trait]
impl crate::Backend for GcsBackend {
async fn fetch(&self, id: CloudId<'_>) -> Result<bytes::Bytes> {
let dl_req = self
.obj
.download(&(&self.bucket, &self.obj_name(id)?), None)?;
let content = send_request_with_retry(&self.client, util::convert_request(dl_req))
.await?
.error_for_status()?
.bytes()
.await?;
Ok(content)
}
async fn upload(&self, source: bytes::Bytes, id: CloudId<'_>) -> Result<usize> {
use tame_gcs::objects::InsertObjectOptional;
let content_len = source.len() as u64;
let insert_req = self.obj.insert_simple(
&(&self.bucket, &self.obj_name(id)?),
source,
content_len,
Some(InsertObjectOptional {
content_type: Some("application/x-tar"),
..Default::default()
}),
)?;
send_request_with_retry(&self.client, insert_req.try_into()?)
.await?
.error_for_status()?;
Ok(content_len as usize)
}
async fn list(&self) -> Result<Vec<String>> {
use tame_gcs::objects::{ListOptional, ListResponse};
let mut names = Vec::new();
let mut page_token: Option<String> = None;
loop {
let ls_req = self.obj.list(
&self.bucket,
Some(ListOptional {
delimiter: Some("/"),
prefix: Some(&self.prefix),
page_token: page_token.as_ref().map(|s| s.as_ref()),
..Default::default()
}),
)?;
let response = util::convert_response(
send_request_with_retry(&self.client, util::convert_request(ls_req)).await?,
)
.await?;
let list_response = ListResponse::try_from(response)?;
let name_block: Vec<_> = list_response
.objects
.into_iter()
.filter_map(|obj| obj.name)
.collect();
names.push(name_block);
page_token = list_response.page_token;
if page_token.is_none() {
break;
}
}
let len = self.prefix.len();
Ok(names
.into_iter()
.flat_map(|v| v.into_iter().map(|p| p[len..].to_owned()))
.collect())
}
async fn updated(&self, id: CloudId<'_>) -> Result<Option<crate::Timestamp>> {
use tame_gcs::objects::{GetObjectOptional, GetObjectResponse};
let get_req = self.obj.get(
&(&self.bucket, &self.obj_name(id)?),
Some(GetObjectOptional {
standard_params: tame_gcs::common::StandardQueryParameters {
fields: Some("updated"),
..Default::default()
},
..Default::default()
}),
)?;
let response = util::convert_response(
send_request_with_retry(&self.client, util::convert_request(get_req)).await?,
)
.await?;
let get_response = GetObjectResponse::try_from(response)?;
Ok(get_response.metadata.updated)
}
}