use std::path::Path;
use std::path::PathBuf;
use async_trait::async_trait;
use http::HeaderMap;
use http::Request;
use http::Response;
use http::StatusCode;
use http_body_util::BodyExt;
use tower::util::ServiceExt;
use tower_http::services::ServeFile;
use super::super::super::ports::project_asset_store::ProjectAssetStore;
use super::super::super::ports::project_asset_store::ProjectAssetStoreError;
use crate::common::domain::StaticAsset;
#[derive(Debug, Clone)]
pub struct ServeDirProjectAssets {
base_dir: PathBuf,
}
impl ServeDirProjectAssets {
pub fn new<P: AsRef<Path>>(base_dir: P) -> Self {
Self {
base_dir: base_dir.as_ref().to_path_buf(),
}
}
#[tracing::instrument(name = "`project_asset_store`: getting asset", err)]
pub async fn get_asset(
&self,
path: &Path,
request_headers: Option<HeaderMap>,
) -> Result<StaticAsset, ProjectAssetStoreError> {
let mut request = Request::new(());
if let Some(request_headers) = request_headers {
*request.headers_mut() = request_headers;
}
let file_path = self.base_dir.join(path);
tracing::debug!(path = ?file_path, "reading from path");
let serve_file = ServeFile::new(file_path.to_string_lossy().as_ref());
let response = serve_file.oneshot(request).await.map_err(|_err| {
ProjectAssetStoreError::AssetNotFound {
path: path.to_owned(),
}
})?;
if response.status() != StatusCode::OK
&& response.status() != StatusCode::PARTIAL_CONTENT
&& response.status() != StatusCode::NOT_MODIFIED
{
return Err(ProjectAssetStoreError::AssetNotFound {
path: path.to_owned(),
});
}
let (parts, body) = response.into_parts();
let bytes = body
.collect()
.await
.map_err(|_err| ProjectAssetStoreError::Parsing {
path: path.to_owned(),
})?
.to_bytes();
let response = Response::from_parts(parts, bytes);
Ok(StaticAsset(response))
}
}
#[async_trait]
impl ProjectAssetStore for ServeDirProjectAssets {
async fn get_asset(
&self,
path: &Path,
request_headers: Option<HeaderMap>,
) -> Result<StaticAsset, ProjectAssetStoreError> {
Self::get_asset(self, path, request_headers).await
}
}
#[cfg(test)]
mod serve_dir_project_assets_tests {
use fake::Fake;
use fake::Faker;
use super::*;
const TEST_FILE_CONTENT: &str = "this is a test string";
fn create_asset_file<P: AsRef<Path>>(assets_dir: P) -> PathBuf {
let asset_path = PathBuf::new().join(Faker.fake::<String>());
let asset_name = "test.txt";
let absolute_path = PathBuf::new().join(&assets_dir).join(&asset_path);
std::fs::create_dir(&absolute_path).unwrap();
std::fs::write(absolute_path.join(asset_name), TEST_FILE_CONTENT).unwrap();
asset_path.join(asset_name)
}
mod get_asset {
use http::HeaderValue;
use http::header;
use super::*;
#[tokio::test]
async fn should_return_a_valid_asset_if_it_exists() {
let assets_dir = tempfile::tempdir().unwrap();
let asset_service = ServeDirProjectAssets::new(&assets_dir);
let asset_path = create_asset_file(&assets_dir);
let static_asset = asset_service
.get_asset(&asset_path, Some(HeaderMap::new()))
.await
.expect("unable to find asset");
assert_eq!(
static_asset.0.headers().get(header::CONTENT_TYPE).unwrap(),
mime::TEXT_PLAIN.as_ref()
);
assert_eq!(
String::from_utf8_lossy(&static_asset.data()),
TEST_FILE_CONTENT.to_owned()
)
}
#[tokio::test]
async fn should_return_a_valid_partial_asset_if_it_exists() {
let assets_dir = tempfile::tempdir().unwrap();
let asset_service = ServeDirProjectAssets::new(&assets_dir);
let asset_path = create_asset_file(&assets_dir);
let mut headers = HeaderMap::new();
headers.append(header::RANGE, HeaderValue::from_static("bytes=3-7"));
let static_asset = asset_service
.get_asset(&asset_path, Some(headers))
.await
.expect("unable to find asset");
assert_eq!(
static_asset.0.headers().get(header::CONTENT_TYPE).unwrap(),
mime::TEXT_PLAIN.as_ref()
);
assert_eq!(
String::from_utf8_lossy(&static_asset.data()),
TEST_FILE_CONTENT[3..=7].to_owned()
)
}
#[tokio::test]
async fn should_return_correct_error_if_asset_does_not_exist() {
let assets_dir = tempfile::tempdir().unwrap();
let asset_service = ServeDirProjectAssets::new(&assets_dir);
let non_existent_path = Path::new("build/non/existent.txt");
let res = asset_service
.get_asset(non_existent_path, Some(HeaderMap::new()))
.await;
assert!(
matches!(res, Err(ProjectAssetStoreError::AssetNotFound { path }) if path == non_existent_path)
)
}
}
}