Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ chrono = "0.4.39"
serde_with = { version = "3.9.0", features = ["json", "chrono"] }
uuid = { version = "1.11.0", features = ["v4", "serde"] }
hex = "0.4.3"
sha2 = "0.10"
serde_json = {version="1.0.138", features = ["preserve_order", "raw_value"]}
figment = { version = "0.10.19", features = ["env", "json"] }
dirs = "6"
Expand Down
21 changes: 21 additions & 0 deletions crates/app/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const LOCAL_DB_NAME: &str = "koi.db";
pub struct Configuration {
pub database_url: String,
pub abi_cache_dir: String,
pub image_cache_dir: String,
pub rpc_requests_per_second: u32,
pub rpc_max_in_flight_per_endpoint: usize,
pub rpc_rate_limit_retries: u32,
Expand All @@ -25,6 +26,7 @@ impl Default for Configuration {
Self {
database_url: "auto".to_string(),
abi_cache_dir: "cache/abis".to_string(),
image_cache_dir: "auto".to_string(),
rpc_requests_per_second: 12,
rpc_max_in_flight_per_endpoint: 8,
rpc_rate_limit_retries: 2,
Expand All @@ -44,11 +46,30 @@ impl Configuration {
if config.database_url == "auto" {
config.database_url = resolve_database_url()?;
}
if config.image_cache_dir == "auto" {
config.image_cache_dir = resolve_cache_dir("images")?;
}

Ok(config)
}
}

pub fn resolve_cache_dir(name: &str) -> Result<String, KoiError> {
let cwd = std::env::current_dir().map_err(|error| {
KoiError::Internal(format!("could not read current directory: {error}"))
})?;
Ok(resolve_cache_dir_in(&cwd, dirs::cache_dir(), name)
.display()
.to_string())
}

pub(crate) fn resolve_cache_dir_in(cwd: &Path, cache_dir: Option<PathBuf>, name: &str) -> PathBuf {
cache_dir
.unwrap_or_else(|| cwd.join(".cache"))
.join("koi")
.join(name)
}

pub fn resolve_database_url() -> Result<String, KoiError> {
let cwd = std::env::current_dir().map_err(|error| {
KoiError::Internal(format!("could not read current directory: {error}"))
Expand Down
16 changes: 14 additions & 2 deletions crates/app/src/http/asset/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ impl AssetApi {
payload: Json<Asset>,
) -> Result<Json<Asset>> {
let _auth_data = auth.unwrap()?;
let mut asset = payload.0;
if let Some(icon) = &asset.asset_icon_url {
asset.asset_icon_url = Some(state.images.store_reference(icon).await?);
}

Ok(Json(Asset::create(&state.database, payload.0).await?))
Ok(Json(Asset::create(&state.database, asset).await?))
}

/// Get an asset by ID
Expand Down Expand Up @@ -113,9 +117,17 @@ impl AssetApi {
payload: Json<AssetUpdate>,
) -> Result<Json<Asset>> {
let _auth_data = auth.unwrap()?;
let mut asset = payload.0;
if let Some(icon) = asset
.asset_icon_url
.clone()
.filter(|icon| !icon.trim().is_empty())
{
asset.asset_icon_url = Some(state.images.store_reference(&icon).await?);
}

Ok(Json(
Asset::update(&state.database, &asset_identity, payload.0).await?,
Asset::update(&state.database, &asset_identity, asset).await?,
))
}

Expand Down
72 changes: 72 additions & 0 deletions crates/app/src/http/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use poem::{
Error, Result,
http::{StatusCode, header},
web::Data,
};
use poem_openapi::{
ApiResponse, OpenApi,
param::Query,
payload::{Binary, Response},
};

use crate::{http::ApiTags, state::AppState};

pub struct CacheApi;

#[derive(ApiResponse)]
enum StoreImageResponse {
#[oai(status = 204)]
Stored(#[oai(header = "Location")] String),
}

pub fn api() -> impl OpenApi {
CacheApi
}

#[OpenApi]
impl CacheApi {
/// Get a cached image
///
/// GET /api/cache/image?id=...
#[oai(path = "/cache/image", method = "get", tag = "ApiTags::Cache")]
async fn cached_image(
&self,
state: Data<&AppState>,
id: Query<String>,
) -> Result<Response<Binary<Vec<u8>>>> {
let image = state
.images
.get(&id)
.await?
.ok_or_else(|| Error::from_status(StatusCode::NOT_FOUND))?;

Ok(image_response(image))
}

/// Fetch a remote image and store it in the cache
///
/// POST /api/cache/image?url=...
#[oai(path = "/cache/image", method = "post", tag = "ApiTags::Cache")]
async fn fetch_image(
&self,
state: Data<&AppState>,
url: Query<String>,
) -> Result<StoreImageResponse> {
let id = state.images.store(&url).await?;

Ok(StoreImageResponse::Stored(format!(
"/api/cache/image?id={id}"
)))
}
}

fn image_response(image: crate::models::image_cache::CachedImage) -> Response<Binary<Vec<u8>>> {
Response::new(Binary(image.bytes.to_vec()))
.header(header::CONTENT_TYPE, image.content_type)
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
.header(header::X_CONTENT_TYPE_OPTIONS, "nosniff")
.header(
header::CONTENT_SECURITY_POLICY,
"default-src 'none'; img-src 'self' data:",
)
}
4 changes: 4 additions & 0 deletions crates/app/src/http/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use crate::state::AppState;
mod account;
mod asset;
mod auth;
mod cache;
mod health;
mod net;
mod quoter;
Expand All @@ -35,6 +36,8 @@ pub enum ApiTags {
Task,
/// Health endpoints
Health,
/// Cache endpoints
Cache,
}

fn get_api() -> impl OpenApi {
Expand All @@ -45,6 +48,7 @@ fn get_api() -> impl OpenApi {
quoter::api(),
vendor::api(),
health::api(),
cache::api(),
)
}

Expand Down
16 changes: 14 additions & 2 deletions crates/app/src/http/net/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ impl NetworkApi {
payload: Json<Network>,
) -> Result<Json<Network>> {
let _auth_data = auth.unwrap()?;
let mut network = payload.0;
if let Some(icon) = &network.network_icon_url {
network.network_icon_url = Some(state.images.store_reference(icon).await?);
}

Ok(Json(Network::create(&state.database, payload.0).await?))
Ok(Json(Network::create(&state.database, network).await?))
}

/// Get network presets
Expand Down Expand Up @@ -151,9 +155,17 @@ impl NetworkApi {
payload: Json<NetworkUpdate>,
) -> Result<Json<Network>> {
let _auth_data = auth.unwrap()?;
let mut network = payload.0;
if let Some(icon) = network
.network_icon_url
.clone()
.filter(|icon| !icon.trim().is_empty())
{
network.network_icon_url = Some(state.images.store_reference(&icon).await?);
}

Ok(Json(
Network::update(&state.database, &network_identity, payload.0).await?,
Network::update(&state.database, &network_identity, network).await?,
))
}

Expand Down
Loading