use crate::client::{OrdinaryApiClient, compress_zstd, traverse};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as b64};
use fs_err::read_to_string;
use ordinary_config::{AssetsConfig, OrdinaryConfig};
use parking_lot::Mutex;
use anyhow::bail;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
fn get_post_process_path(
proj_path: &str,
asset_path: &str,
assets: &AssetsConfig,
path: &Path,
) -> anyhow::Result<PathBuf> {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
let use_gen_path = match ext {
"css" => assets.minify_css == Some(true),
"html" => assets.minify_html == Some(true),
"js" => assets.minify_js == Some(true),
"png" | "jpg" | "jpeg" | "tif" | "tiff" | "webp" | "avif" => {
assets.preserve_exif != Some(true)
}
_ => false,
};
let Some(assets_dir_path) = &assets.dir_path else {
bail!("assets.dir_path cannot be unset");
};
if use_gen_path {
let gen_path = Path::new(proj_path)
.join(".ordinary")
.join("gen")
.join(assets_dir_path)
.join(asset_path);
if gen_path.exists() {
return Ok(gen_path);
}
}
}
Ok(path.into())
}
async fn make_write_req(
api_client: &OrdinaryApiClient<'_>,
config: &OrdinaryConfig,
correlation_id: Option<String>,
access_token: &[u8],
real_path: &PathBuf,
src_path: &Path,
) -> anyhow::Result<()> {
if let Some(assets_config) = &config.assets {
if src_path.ends_with(".DS_Store") {
tracing::warn!("ignoring .DS_Store");
return Ok(());
}
let Some(assets_dir_path) = &assets_config.dir_path else {
bail!("assets.dir_path cannot be unset");
};
let dir_path_components = Path::new(assets_dir_path)
.components()
.filter(|c| matches!(c, Component::Normal(_)))
.collect::<Vec<_>>();
let src_path_components = src_path
.components()
.filter(|c| matches!(c, Component::Normal(_)))
.collect::<Vec<_>>();
let mut path = String::new();
let last = src_path_components.len() - 1;
for (i, component) in src_path_components.iter().enumerate() {
if let Some(dir_path_component) = dir_path_components.get(i)
&& dir_path_component == component
{
continue;
}
if let Component::Normal(str) = component
&& let Some(str) = str.to_str()
{
let escaped: String =
url::form_urlencoded::byte_serialize(str.as_bytes()).collect();
if str != escaped {
bail!("path component '{str}' is not URL safe and has been skipped.");
}
if i == last {
path = format!("{path}{str}");
} else {
path = format!("{path}{str}/");
}
}
}
tracing::info!(path = %real_path.display(), "writing...");
let content = fs_err::read(real_path)?;
let no_compress = if let Some(ext) = real_path.extension()
&& let Some(ext) = ext.to_str()
{
matches!(
ext,
"otf"
| "ttf"
| "woff"
| "woff2"
| "png"
| "apng"
| "gif"
| "jpg"
| "jpeg"
| "bmp"
| "tif"
| "tiff"
| "webp"
| "avif"
| "ico"
| "pdf"
)
} else {
false
};
let mut request = api_client
.client
.put(format!("{}/v1/assets", api_client.addr))
.query(&[("d", config.domain.as_str()), ("p", &path)])
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
);
if let Some(correlation_id) = correlation_id {
request = request.header("x-correlation-id", correlation_id);
}
if no_compress {
request = request.body(content);
} else {
request = request
.body(compress_zstd(&content[..])?)
.header("Content-Encoding", "zstd");
}
let status = request.send().await?.status();
let route = if assets_config.base_route == "/" {
format!("/{path}")
} else {
format!("{}/{path}", assets_config.base_route)
};
if status.is_success() {
tracing::info!(%route, "write complete.");
} else {
tracing::error!(%status, %route, "failed to write.");
}
}
Ok(())
}
pub async fn write(
api_client: &OrdinaryApiClient<'_>,
proj_path: &str,
asset_path: &str,
) -> anyhow::Result<()> {
let config = OrdinaryConfig::get(proj_path)?;
if let Some(assets) = &config.assets {
let Some(assets_dir_path) = &assets.dir_path else {
bail!("assets.dir_path cannot be unset");
};
let path = Path::new(proj_path).join(assets_dir_path).join(asset_path);
if path.ends_with(".DS_Store") {
tracing::warn!("ignoring .DS_Store");
return Ok(());
}
let real_path = get_post_process_path(proj_path, asset_path, assets, &path)?;
let correlation_id = api_client.correlation_id.map(|id| id.to_string());
let access_token = api_client.get_access(None, correlation_id.clone()).await?;
if let Err(err) = make_write_req(
api_client,
&config,
correlation_id,
&access_token,
&real_path,
&path,
)
.await
{
tracing::error!(%err);
}
} else {
tracing::warn!("no \"assets\" field in ordinary.json");
}
Ok(())
}
pub async fn write_all(api_client: &OrdinaryApiClient<'_>, proj_path: &str) -> anyhow::Result<()> {
let config = OrdinaryConfig::get(proj_path)?;
let paths: Arc<Mutex<Vec<(PathBuf, PathBuf)>>> = Arc::new(Mutex::new(vec![]));
if let Some(assets) = &config.assets {
let Some(assets_dir_path) = &assets.dir_path else {
bail!("assets.dir_path cannot be unset");
};
tracing::info!("gathering asset paths from {}...", assets_dir_path);
let parent_path = Path::new(proj_path).join(assets_dir_path);
traverse(&parent_path, &|entry| {
if let Ok(path) = entry.path().strip_prefix(&parent_path) {
if path.extension().is_some()
&& let Some(path_str) = path.to_str()
{
let paths = Arc::clone(&paths);
let mut paths = paths.lock();
if let Ok(real_path) =
get_post_process_path(proj_path, path_str, assets, &entry.path())
{
paths.push((path.to_path_buf(), real_path));
}
}
}
})?;
let correlation_id = api_client
.correlation_id
.unwrap_or(uuid::Uuid::new_v4())
.to_string();
let access_token = api_client
.get_access(None, Some(correlation_id.clone()))
.await?;
let paths = Arc::clone(&paths);
let paths = paths.lock().clone();
for (src_path, real_path) in paths {
if let Err(err) = make_write_req(
api_client,
&config,
Some(correlation_id.clone()),
&access_token,
&real_path,
&src_path,
)
.await
{
tracing::error!(%err);
}
}
} else {
tracing::warn!("no \"assets\" field in ordinary.json");
}
Ok(())
}
#[allow(clippy::too_many_lines)]
pub async fn write_frontend(
api_client: &OrdinaryApiClient<'_>,
proj_path: &str,
config: &OrdinaryConfig,
access_token: &Vec<u8>,
correlation_id: &str,
) -> anyhow::Result<()> {
if config.auth.is_some() {
let core_js = read_to_string(Path::new(proj_path).join(".ordinary/gen/client/js/core.js"))?
.replace("{{ version }}", &config.version)
.as_bytes()
.to_vec();
tracing::info!("writing {}/js/core.js to assets...", &config.version);
api_client
.client
.put(format!("{}/v1/assets", api_client.addr))
.query(&[
("d", config.domain.clone()),
("p", format!("{}/js/core.js", &config.version)),
])
.body(compress_zstd(&core_js[..])?)
.header("x-correlation-id", correlation_id)
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
)
.send()
.await?
.bytes()
.await?;
tracing::info!("{}/js/core.js written to assets.", &config.version);
}
if config.auth.is_some() {
let js_client_js =
read_to_string(Path::new(proj_path).join(".ordinary/gen/client/js/client.js"))?
.replace("{{ version }}", &config.version)
.as_bytes()
.to_vec();
tracing::info!("writing {}/js/client.js to assets...", &config.version);
api_client
.client
.put(format!("{}/v1/assets", api_client.addr))
.query(&[
("d", config.domain.clone()),
("p", format!("{}/js/client.js", &config.version)),
])
.body(compress_zstd(&js_client_js[..])?)
.header("x-correlation-id", correlation_id)
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
)
.send()
.await?
.bytes()
.await?;
tracing::info!("{}/js/client.js written to assets.", &config.version);
}
if config.auth.is_some()
|| config.obfuscation == Some(true)
|| config.client_rendering == Some(true)
{
let client_js =
read_to_string(Path::new(proj_path).join(".ordinary/gen/client/wasm/client.js"))?
.replace("{{ version }}", &config.version)
.as_bytes()
.to_vec();
let client_wasm =
fs_err::read(Path::new(proj_path).join(".ordinary/gen/client/wasm/client_bg_opt.wasm"))
.or_else(|_| {
fs_err::read(
Path::new(proj_path).join(".ordinary/gen/client/wasm/client_bg.wasm"),
)
})?;
tracing::info!("writing {}/wasm/client.js to assets...", &config.version);
api_client
.client
.put(format!("{}/v1/assets", api_client.addr))
.query(&[
("d", config.domain.clone()),
("p", format!("{}/wasm/client.js", &config.version)),
])
.body(compress_zstd(&client_js[..])?)
.header("x-correlation-id", correlation_id)
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
)
.send()
.await?
.bytes()
.await?;
tracing::info!("{}/wasm/client.js written to assets.", &config.version);
tracing::info!(
"writing {}/wasm/client_bg.wasm to assets...",
&config.version
);
api_client
.client
.put(format!("{}/v1/assets", api_client.addr))
.query(&[
("d", config.domain.clone()),
("p", format!("{}/wasm/client_bg.wasm", &config.version)),
])
.body(compress_zstd(&client_wasm[..])?)
.header("x-correlation-id", correlation_id)
.header("Content-Encoding", "zstd")
.header(
"Authorization",
format!("Bearer {}", b64.encode(access_token)),
)
.send()
.await?
.bytes()
.await?;
tracing::info!("{}/wasm/client_bg.wasm written to assets.", &config.version);
}
Ok(())
}