use super::{ByteRange, ObjectBody, ObjectMetadata, ObjectStore, PutMode};
use crate::object_store::Result;
use crate::store_io_runtime::StoreIoRuntime;
use crate::{ObjectStoreError, ProviderObjectStore, ProviderObjectStoreConfig};
use async_trait::async_trait;
use bytes::Bytes;
use futures::stream::BoxStream;
use object_store::gcp::GoogleCloudStorageBuilder;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GcpGcsStoreConfig {
pub bucket: String,
pub service_account_key_path: String,
pub key_prefix: Option<String>,
}
#[derive(Debug)]
pub struct GcpGcsStore {
inner: ProviderObjectStore,
_io_runtime: StoreIoRuntime,
}
impl GcpGcsStore {
pub fn new(config: GcpGcsStoreConfig) -> Result<Self> {
if config.bucket.trim().is_empty() {
return Err(ObjectStoreError::Configuration(
"bucket must not be empty".to_owned(),
));
}
if config.service_account_key_path.trim().is_empty() {
return Err(ObjectStoreError::Configuration(
"service account key path must not be empty".to_owned(),
));
}
let io_runtime = StoreIoRuntime::new()?;
let builder = GoogleCloudStorageBuilder::new()
.with_http_connector(io_runtime.connector())
.with_client_options(crate::provider_object_store::provider_client_options())
.with_retry(crate::provider_object_store::provider_retry_config())
.with_bucket_name(config.bucket)
.with_service_account_path(config.service_account_key_path);
let provider = Arc::new(
builder
.build()
.map_err(|err| ObjectStoreError::Configuration(err.to_string()))?,
);
let inner = ProviderObjectStore::new(
Arc::clone(&provider) as Arc<dyn object_store::ObjectStore>,
Some(provider),
ProviderObjectStoreConfig {
key_prefix: config.key_prefix,
},
)?;
Ok(Self {
inner,
_io_runtime: io_runtime,
})
}
fn generation_as_compare_token(metadata: ObjectMetadata) -> ObjectMetadata {
ObjectMetadata {
etag: metadata.version.clone(),
..metadata
}
}
fn require_generation_compare_token(key: &str, expected_etag: &str) -> Result<()> {
expected_etag
.parse::<u64>()
.map(|_| ())
.map_err(|_| ObjectStoreError::PreconditionFailed {
object_key: key.to_owned(),
})
}
}
#[async_trait]
impl ObjectStore for GcpGcsStore {
async fn head(&self, key: &str) -> Result<Option<ObjectMetadata>> {
Ok(self
.inner
.head(key)
.await?
.map(Self::generation_as_compare_token))
}
async fn get_with_metadata(&self, key: &str) -> Result<Option<ObjectBody>> {
Ok(self
.inner
.get_with_metadata(key)
.await?
.map(|body| ObjectBody {
metadata: Self::generation_as_compare_token(body.metadata),
bytes: body.bytes,
}))
}
async fn get(&self, key: &str, range: Option<ByteRange>) -> Result<Option<Bytes>> {
self.inner.get(key, range).await
}
async fn put(&self, key: &str, bytes: Bytes, mode: PutMode) -> Result<ObjectMetadata> {
self.inner.validate_key(key)?;
if let PutMode::CompareAndSwap { expected_etag } = &mode {
Self::require_generation_compare_token(key, expected_etag)?;
}
Ok(Self::generation_as_compare_token(
self.inner.put(key, bytes, mode).await?,
))
}
async fn delete(&self, key: &str) -> Result<()> {
self.inner.delete(key).await
}
fn list_prefix_stream(&self, prefix: &str) -> BoxStream<'static, Result<String>> {
self.inner.list_prefix_stream(prefix)
}
}
#[cfg(test)]
mod tests {
use super::{GcpGcsStore, GcpGcsStoreConfig};
use crate::{ObjectStore, ObjectStoreError};
use bytes::Bytes;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
const FAKE_SERVICE_ACCOUNT_KEY: &str = r#"{"private_key":"private_key","private_key_id":"private_key_id","client_email":"client_email","disable_oauth":true}"#;
#[tokio::test]
async fn invalid_keys_are_rejected_before_generation_tokens() {
let service_account_key_path = fake_service_account_key_file("gcs-invalid-key");
let store = GcpGcsStore::new(GcpGcsStoreConfig {
bucket: "bucket".to_owned(),
service_account_key_path: service_account_key_path.display().to_string(),
key_prefix: None,
})
.expect("construct gcs store");
assert!(matches!(
store
.compare_and_swap("../escape", "not-a-generation", Bytes::from_static(b"oops"))
.await,
Err(ObjectStoreError::InvalidKey { .. })
));
}
#[test]
fn service_account_key_path_is_required() {
assert!(matches!(
GcpGcsStore::new(GcpGcsStoreConfig {
bucket: "bucket".to_owned(),
service_account_key_path: " ".to_owned(),
key_prefix: None,
}),
Err(ObjectStoreError::Configuration(_))
));
}
fn fake_service_account_key_file(label: &str) -> PathBuf {
let path = unique_temp_dir(label).join("service-account.json");
fs::write(&path, FAKE_SERVICE_ACCOUNT_KEY).expect("write fake service account key");
path
}
#[allow(clippy::disallowed_methods)]
fn unique_temp_dir(label: &str) -> PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock after epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("loonfs-objectstore-{label}-{stamp}"));
fs::create_dir_all(&path).expect("create temp dir");
path
}
}