use mime_guess::from_path;
use rust_embed::RustEmbed;
use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(RustEmbed)]
#[folder = "vendor/frontend/dist/"]
pub struct FrontendDist;
pub struct FrontendAssets {
serve_dir: PathBuf,
owned: bool,
}
impl FrontendAssets {
pub fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
if let Ok(dist_path) = std::env::var("AGENTSIGHT_FRONTEND_DIST") {
let dir = PathBuf::from(&dist_path);
if !dir.join("index.html").exists() {
return Err(format!(
"AGENTSIGHT_FRONTEND_DIST={} does not contain index.html",
dist_path
)
.into());
}
log::info!("๐ Dev mode: serving frontend from disk: {}", dir.display());
return Ok(Self {
serve_dir: dir,
owned: false,
});
}
let temp_dir =
std::env::temp_dir().join(format!("agentsight-frontend-{}", uuid::Uuid::new_v4()));
fs::create_dir_all(&temp_dir)?;
for file_path in FrontendDist::iter() {
if let Some(content) = FrontendDist::get(&file_path) {
let full_path = temp_dir.join(&*file_path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&full_path, &content.data)?;
}
}
log::info!("๐ Extracted frontend assets to: {}", temp_dir.display());
Ok(Self {
serve_dir: temp_dir,
owned: true,
})
}
pub fn get(&self, path: &str) -> Option<Cow<'static, [u8]>> {
let file_path = if path == "/" || path == "/index.html" {
self.serve_dir.join("index.html")
} else {
let normalized_path = path.strip_prefix('/').unwrap_or(path);
self.serve_dir.join(normalized_path)
};
if let Ok(content) = fs::read(&file_path) {
Some(Cow::Owned(content))
} else {
None
}
}
pub fn get_content_type(&self, path: &str) -> String {
let file_path = if path == "/" || path == "/index.html" {
"index.html"
} else {
path.strip_prefix('/').unwrap_or(path)
};
from_path(file_path).first_or_octet_stream().to_string()
}
pub fn list_all_assets(&self) -> Vec<String> {
if self.owned {
FrontendDist::iter().map(|s| s.to_string()).collect()
} else {
let mut files = Vec::new();
if let Ok(entries) = walkdir(&self.serve_dir, &self.serve_dir) {
files = entries;
}
files
}
}
}
impl Drop for FrontendAssets {
fn drop(&mut self) {
if !self.owned {
return;
}
if self.serve_dir.exists() {
if let Err(e) = fs::remove_dir_all(&self.serve_dir) {
log::warn!(
"Failed to cleanup temp directory {}: {}",
self.serve_dir.display(),
e
);
} else {
log::info!("๐งน Cleaned up temp directory: {}", self.serve_dir.display());
}
}
}
}
fn walkdir(dir: &Path, root: &Path) -> std::io::Result<Vec<String>> {
let mut result = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
result.extend(walkdir(&path, root)?);
} else if let Ok(rel) = path.strip_prefix(root) {
result.push(rel.to_string_lossy().into_owned());
}
}
Ok(result)
}