use std::io::Read;
use std::path::Path;
use std::sync::Arc;
use flate2::bufread::GzDecoder;
use opendal::layers::LoggingLayer;
use opendal::{ErrorKind, Operator};
use tar::Archive;
use crate::model::cargo::CrateMetadata;
use crate::model::config::{Configuration, StorageConfig};
use crate::utils::apierror::ApiError;
use crate::utils::FaillibleFuture;
pub trait Storage {
fn store_crate<'a>(&'a self, metadata: &'a CrateMetadata, content: Vec<u8>) -> FaillibleFuture<'a, ()>;
fn download_crate<'a>(&'a self, name: &'a str, version: &'a str) -> FaillibleFuture<'a, Vec<u8>>;
fn download_crate_metadata<'a>(&'a self, name: &'a str, version: &'a str) -> FaillibleFuture<'a, Option<CrateMetadata>>;
fn download_crate_readme<'a>(&'a self, name: &'a str, version: &'a str) -> FaillibleFuture<'a, Vec<u8>>;
fn store_doc_file<'a>(&'a self, path: &'a str, file: &'a Path) -> FaillibleFuture<'a, ()>;
fn store_doc_data<'a>(&'a self, path: &'a str, content: Vec<u8>) -> FaillibleFuture<'a, ()>;
fn download_doc_file<'a>(&'a self, path: &'a str) -> FaillibleFuture<'a, Vec<u8>>;
}
#[must_use]
pub fn get_service(config: &Configuration) -> Arc<dyn Storage + Send + Sync> {
Arc::new(StorageImpl::from(config))
}
pub struct StorageImpl {
opendal_operator: Operator,
}
impl From<&Configuration> for StorageImpl {
fn from(config: &Configuration) -> Self {
let opendal_operator = match &config.storage {
StorageConfig::FileSystem => {
let builder = opendal::services::Fs::default().root(&config.data_dir);
opendal::Operator::new(builder)
.unwrap()
.layer(LoggingLayer::default())
.finish()
}
StorageConfig::S3 { params, bucket } => {
let builder = opendal::services::S3::default()
.bucket(bucket)
.root(¶ms.root)
.region(¶ms.region)
.endpoint(¶ms.endpoint)
.access_key_id(¶ms.access_key)
.secret_access_key(¶ms.secret_key);
opendal::Operator::new(builder)
.unwrap()
.layer(LoggingLayer::default())
.finish()
}
};
StorageImpl { opendal_operator }
}
}
impl Storage for StorageImpl {
fn store_crate<'a>(&'a self, metadata: &'a CrateMetadata, content: Vec<u8>) -> FaillibleFuture<'a, ()> {
Box::pin(async move { self.store_crate(metadata, content).await })
}
fn download_crate<'a>(&'a self, name: &'a str, version: &'a str) -> FaillibleFuture<'a, Vec<u8>> {
Box::pin(async move { self.download_crate(name, version).await })
}
fn download_crate_metadata<'a>(&'a self, name: &'a str, version: &'a str) -> FaillibleFuture<'a, Option<CrateMetadata>> {
Box::pin(async move { self.download_crate_metadata(name, version).await })
}
fn download_crate_readme<'a>(&'a self, name: &'a str, version: &'a str) -> FaillibleFuture<'a, Vec<u8>> {
Box::pin(async move { self.download_crate_readme(name, version).await })
}
fn store_doc_file<'a>(&'a self, path: &'a str, file: &'a Path) -> FaillibleFuture<'a, ()> {
Box::pin(async move { self.store_doc_file(path, file).await })
}
fn store_doc_data<'a>(&'a self, path: &'a str, content: Vec<u8>) -> FaillibleFuture<'a, ()> {
Box::pin(async move { self.store_doc_data(path, content).await })
}
fn download_doc_file<'a>(&'a self, path: &'a str) -> FaillibleFuture<'a, Vec<u8>> {
Box::pin(async move { self.download_doc_file(path).await })
}
}
impl StorageImpl {
async fn store_crate(&self, metadata: &CrateMetadata, content: Vec<u8>) -> Result<(), ApiError> {
let readme = extract_readme(&content)?;
let metadata_json = serde_json::to_vec(metadata)?;
let name = &metadata.name;
let version = &metadata.vers;
self.write_to_file(&Self::data_path(name, version), content).await?;
self.write_to_file(&Self::metadata_path(name, version), metadata_json).await?;
self.write_to_file(&Self::readme_path(name, version), readme).await?;
Ok(())
}
async fn download_crate(&self, name: &str, version: &str) -> Result<Vec<u8>, ApiError> {
match self.read_from_file(&Self::data_path(name, version)).await {
Ok(data) => Ok(data),
Err(e) => {
if matches!(e.kind(), ErrorKind::NotFound | ErrorKind::Unexpected) {
self.read_from_file(&format!("crates/{name}/{version}"))
.await
.map_err(ApiError::from)
} else {
Err(e.into())
}
}
}
}
async fn download_crate_metadata(&self, name: &str, version: &str) -> Result<Option<CrateMetadata>, ApiError> {
if let Ok(data) = self.read_from_file(&Self::metadata_path(name, version)).await {
Ok(Some(serde_json::from_slice(&data)?))
} else {
Ok(None)
}
}
async fn download_crate_readme(&self, name: &str, version: &str) -> Result<Vec<u8>, ApiError> {
self.read_from_file(&Self::readme_path(name, version))
.await
.map_err(ApiError::from)
}
async fn store_doc_file(&self, path: &str, file: &Path) -> Result<(), ApiError> {
let content = tokio::fs::read(file).await?;
self.store_doc_data(path, content).await?;
Ok(())
}
async fn store_doc_data(&self, path: &str, content: Vec<u8>) -> Result<(), ApiError> {
self.write_to_file(&format!("docs/{path}"), content).await?;
Ok(())
}
async fn download_doc_file(&self, path: &str) -> Result<Vec<u8>, ApiError> {
self.read_from_file(&format!("docs/{path}")).await.map_err(ApiError::from)
}
async fn write_to_file(&self, path: &str, content: Vec<u8>) -> Result<(), ApiError> {
self.opendal_operator.write(path, content).await?;
Ok(())
}
async fn read_from_file(&self, path: &str) -> Result<Vec<u8>, opendal::Error> {
let buffer = self.opendal_operator.read(path).await?;
Ok(buffer.to_vec())
}
fn crate_file_key(name: &str, version: &str, filename: &str) -> String {
format!("crates/{name}/{version}/{filename}")
}
fn data_path(name: &str, version: &str) -> String {
Self::crate_file_key(name, version, "data")
}
fn metadata_path(name: &str, version: &str) -> String {
Self::crate_file_key(name, version, "metadata")
}
fn readme_path(name: &str, version: &str) -> String {
Self::crate_file_key(name, version, "readme")
}
}
pub fn extract_readme(crate_content: &[u8]) -> Result<Vec<u8>, ApiError> {
let decoder = GzDecoder::new(crate_content);
let mut archive = Archive::new(decoder);
let mut buffer = Vec::new();
archive
.entries()?
.find(|entry| {
entry.as_ref().is_ok_and(|entry| {
entry.header().path().is_ok_and(|path| {
path.file_name()
.is_some_and(|file_name| file_name.to_string_lossy().contains("README"))
})
})
})
.transpose()?
.map(|mut entry| entry.read_to_end(&mut buffer))
.transpose()?;
Ok(buffer)
}