use std::fmt::Write as _; use crate::server::absolute_display_path; use crate::server::server_stats; use crate::server::volume_server::VolumeServerState; use crate::storage::store::Store; pub struct EmbeddedAsset { pub content_type: &'static str, pub bytes: &'static [u8], } struct UiDiskRow { dir: String, disk_type: String, all: u64, free: u64, used: u64, } struct UiVolumeRow { id: u32, collection: String, disk_type: String, size: u64, file_count: i64, delete_count: i64, deleted_byte_count: u64, ttl: String, read_only: bool, version: u32, remote_storage_name: String, remote_storage_key: String, } struct UiEcShardRow { shard_id: u8, size: u64, } struct UiEcVolumeRow { volume_id: u32, collection: String, size: u64, shards: Vec, created_at: String, } pub fn favicon_asset() -> EmbeddedAsset { EmbeddedAsset { content_type: "image/x-icon", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/favicon.ico" )), } } pub fn lookup_static_asset(path: &str) -> Option { let path = path.trim_start_matches('/'); let asset = match path { "bootstrap/3.3.1/css/bootstrap.min.css" => EmbeddedAsset { content_type: "text/css; charset=utf-8", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/bootstrap/3.3.1/css/bootstrap.min.css" )), }, "bootstrap/3.3.1/fonts/glyphicons-halflings-regular.eot" => EmbeddedAsset { content_type: "application/vnd.ms-fontobject", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/bootstrap/3.3.1/fonts/glyphicons-halflings-regular.eot" )), }, "bootstrap/3.3.1/fonts/glyphicons-halflings-regular.svg" => EmbeddedAsset { content_type: "image/svg+xml", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/bootstrap/3.3.1/fonts/glyphicons-halflings-regular.svg" )), }, "bootstrap/3.3.1/fonts/glyphicons-halflings-regular.ttf" => EmbeddedAsset { content_type: "font/ttf", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/bootstrap/3.3.1/fonts/glyphicons-halflings-regular.ttf" )), }, "bootstrap/3.3.1/fonts/glyphicons-halflings-regular.woff" => EmbeddedAsset { content_type: "font/woff", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/bootstrap/3.3.1/fonts/glyphicons-halflings-regular.woff" )), }, "images/folder.gif" => EmbeddedAsset { content_type: "image/gif", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/images/folder.gif" )), }, "javascript/jquery-3.6.0.min.js" => EmbeddedAsset { content_type: "application/javascript; charset=utf-8", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/javascript/jquery-3.6.0.min.js" )), }, "javascript/jquery-sparklines/2.1.2/jquery.sparkline.min.js" => EmbeddedAsset { content_type: "application/javascript; charset=utf-8", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/javascript/jquery-sparklines/2.1.2/jquery.sparkline.min.js" )), }, "seaweed50x50.png" => EmbeddedAsset { content_type: "image/png", bytes: include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/../weed/static/seaweed50x50.png" )), }, _ => return None, }; Some(asset) } pub fn render_volume_server_html(state: &VolumeServerState) -> String { let counters = server_stats::snapshot(); let (disk_rows, volume_rows, remote_volume_rows, ec_volume_rows) = { let store = state.store.read().unwrap(); collect_ui_data(&store) }; let masters = if state.master_urls.is_empty() { "[]".to_string() } else { format!("[{}]", state.master_urls.join(" ")) }; let uptime = server_stats::uptime_string(); let read_week = join_i64(&counters.read_requests.week_counter.to_list()); let read_day = join_i64(&counters.read_requests.day_counter.to_list()); let read_hour = join_i64(&counters.read_requests.hour_counter.to_list()); let read_minute = join_i64(&counters.read_requests.minute_counter.to_list()); let mut disk_rows_html = String::new(); for disk in &disk_rows { let _ = write!( disk_rows_html, "{}{}{}{}{:.2}%", escape_html(&disk.dir), escape_html(&disk.disk_type), bytes_to_human_readable(disk.all), bytes_to_human_readable(disk.free), percent_from(disk.all, disk.used), ); } let mut volume_rows_html = String::new(); for volume in &volume_rows { let _ = write!( volume_rows_html, "{}{}{}{}{}{} / {}{}{}{}", volume.id, escape_html(&volume.collection), escape_html(&volume.disk_type), bytes_to_human_readable(volume.size), volume.file_count, volume.delete_count, bytes_to_human_readable(volume.deleted_byte_count), escape_html(&volume.ttl), volume.read_only, volume.version, ); } let remote_section = if remote_volume_rows.is_empty() { String::new() } else { let mut remote_rows_html = String::new(); for volume in &remote_volume_rows { let _ = write!( remote_rows_html, "{}{}{}{}{} / {}{}{}", volume.id, escape_html(&volume.collection), bytes_to_human_readable(volume.size), volume.file_count, volume.delete_count, bytes_to_human_readable(volume.deleted_byte_count), escape_html(&volume.remote_storage_name), escape_html(&volume.remote_storage_key), ); } format!( r#"

Remote Volumes

{}
Id Collection Size Files Trash Remote Key
"#, remote_rows_html ) }; let ec_section = if ec_volume_rows.is_empty() { String::new() } else { let mut ec_rows_html = String::new(); for ec in &ec_volume_rows { let mut shard_labels = String::new(); for shard in &ec.shards { let _ = write!( shard_labels, "{}: {}", shard.shard_id, bytes_to_human_readable(shard.size) ); } let _ = write!( ec_rows_html, "{}{}{}{}{}", ec.volume_id, escape_html(&ec.collection), bytes_to_human_readable(ec.size), shard_labels, escape_html(&ec.created_at), ); } format!( r#"

Erasure Coding Shards

{}
Id Collection Total Size Shard Details CreatedAt
"#, ec_rows_html ) }; format!( r#" SeaweedFS {version}

Disk Stats

{disk_rows_html}
Path Disk Total Free Usage

System Stats

Masters{masters}
Weekly # ReadRequests{read_week}
Daily # ReadRequests{read_day}
Hourly # ReadRequests{read_hour}
Last Minute # ReadRequests{read_minute}
Up Time{uptime}

Volumes

{volume_rows_html}
Id Collection Disk Data Size Files Trash TTL ReadOnly Version
{remote_section} {ec_section}
"#, version = escape_html(crate::version::version()), disk_rows_html = disk_rows_html, masters = escape_html(&masters), read_week = read_week, read_day = read_day, read_hour = read_hour, read_minute = read_minute, uptime = escape_html(&uptime), volume_rows_html = volume_rows_html, remote_section = remote_section, ec_section = ec_section, ) } fn collect_ui_data( store: &Store, ) -> ( Vec, Vec, Vec, Vec, ) { let mut disk_rows = Vec::new(); let mut volumes = Vec::new(); let mut remote_volumes = Vec::new(); let mut ec_volumes = Vec::new(); for loc in &store.locations { let dir = absolute_display_path(&loc.directory); let (all, free) = crate::storage::disk_location::get_disk_stats(&dir); disk_rows.push(UiDiskRow { dir, disk_type: loc.disk_type.to_string(), all, free, used: all.saturating_sub(free), }); for (_, volume) in loc.volumes() { let (remote_storage_name, remote_storage_key) = volume.remote_storage_name_key(); let row = UiVolumeRow { id: volume.id.0, collection: volume.collection.clone(), disk_type: loc.disk_type.to_string(), size: volume.content_size(), file_count: volume.file_count(), delete_count: volume.deleted_count(), deleted_byte_count: volume.deleted_size(), ttl: volume.super_block.ttl.to_string(), read_only: volume.is_read_only(), version: volume.version().0 as u32, remote_storage_name, remote_storage_key, }; if row.remote_storage_name.is_empty() { volumes.push(row); } else { remote_volumes.push(row); } } for (_, ec_volume) in loc.ec_volumes() { let mut shards = Vec::new(); let mut total_size = 0u64; let mut created_at = String::from("-"); for shard in ec_volume.shards.iter().flatten() { let shard_size = shard.file_size().max(0) as u64; total_size = total_size.saturating_add(shard_size); shards.push(UiEcShardRow { shard_id: shard.shard_id, size: shard_size, }); if created_at == "-" && let Ok(metadata) = std::fs::metadata(shard.file_name()) && let Ok(modified) = metadata.modified() { let ts: chrono::DateTime = modified.into(); created_at = ts.format("%Y-%m-%d %H:%M").to_string(); } } let preferred_size = ec_volume.dat_file_size.max(0) as u64; ec_volumes.push(UiEcVolumeRow { volume_id: ec_volume.volume_id.0, collection: ec_volume.collection.clone(), size: preferred_size.max(total_size), shards, created_at, }); } } disk_rows.sort_by(|left, right| left.dir.cmp(&right.dir)); volumes.sort_by_key(|row| row.id); remote_volumes.sort_by_key(|row| row.id); ec_volumes.sort_by_key(|row| row.volume_id); (disk_rows, volumes, remote_volumes, ec_volumes) } fn join_i64(values: &[i64]) -> String { values .iter() .map(std::string::ToString::to_string) .collect::>() .join(",") } fn percent_from(total: u64, part: u64) -> f64 { if total == 0 { return 0.0; } (part as f64 / total as f64) * 100.0 } fn bytes_to_human_readable(bytes: u64) -> String { const UNIT: u64 = 1024; if bytes < UNIT { return format!("{} B", bytes); } let mut div = UNIT; let mut exp = 0usize; let mut n = bytes / UNIT; while n >= UNIT { div *= UNIT; n /= UNIT; exp += 1; } format!( "{:.2} {}iB", bytes as f64 / div as f64, ["K", "M", "G", "T", "P", "E"][exp] ) } fn escape_html(input: &str) -> String { input .replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) }