use std::collections::HashSet;
use std::fs;
use std::path::PathBuf;
use anyhow::{Context as _, Result};
use clap::Args;
use openlogi_assets::{CORE_FILES, FetchOutcome, http};
const DEFAULT_BASE: &str = "https://assets.openlogi.org";
fn is_optional_asset(name: &str) -> bool {
if name == "side_core.png" {
return true;
}
let path = std::path::Path::new(name);
let ext_is_png = path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("png"));
if !ext_is_png {
return false;
}
name.starts_with("front_ext_") || name.starts_with("side_ext_")
}
#[derive(Debug, Args)]
pub struct SyncArgs {
#[arg(long, default_value = DEFAULT_BASE, env = "OPENLOGI_ASSETS")]
base: String,
#[arg(long, default_value = "crates/openlogi-gui/assets")]
out: PathBuf,
}
pub fn run(args: SyncArgs) -> Result<()> {
let SyncArgs { base, out } = args;
fs::create_dir_all(&out).with_context(|| format!("create {}", out.display()))?;
let client = http::AssetClient::new(&base);
let index = client.fetch_index_to_dir(&out)?;
println!("index.json: {} devices", index.devices.len());
let expected: HashSet<&str> = index.devices.keys().map(String::as_str).collect();
for entry in fs::read_dir(&out)? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let name = entry.file_name();
let name_str = name.to_string_lossy();
if !expected.contains(name_str.as_ref()) {
println!(" pruning {name_str}");
fs::remove_dir_all(entry.path())?;
}
}
let mut fetched = 0_u32;
let mut cache_hits = 0_u32;
let mut depots: Vec<&String> = index.devices.keys().collect();
depots.sort();
for depot in depots {
let entry = &index.devices[depot];
let dir = out.join(depot);
fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?;
let wanted: Vec<&openlogi_assets::FileEntry> = entry
.files
.iter()
.filter(|f| CORE_FILES.contains(&f.name.as_str()) || is_optional_asset(&f.name))
.collect();
for required in CORE_FILES {
if !wanted.iter().any(|f| f.name == required) {
eprintln!(" WARN {depot}: registry missing {required}");
}
}
for &file_entry in &wanted {
match client.fetch_entry_if_stale(&entry.asset_path, &dir, file_entry)? {
FetchOutcome::CacheHit => cache_hits += 1,
FetchOutcome::Fetched { .. } => {
fetched += 1;
println!(" {depot}/{} ({} B)", file_entry.name, file_entry.bytes);
}
}
}
}
let bundle_bytes: u64 = index
.devices
.values()
.flat_map(|d| d.files.iter())
.filter(|f| CORE_FILES.contains(&f.name.as_str()) || is_optional_asset(&f.name))
.map(|f| f.bytes)
.sum();
#[allow(
clippy::cast_precision_loss,
reason = "bundle sizes are well under 2^53 bytes; f64 precision is fine for a display string"
)]
let mb = bundle_bytes as f64 / 1024.0 / 1024.0;
println!(
"done: {fetched} fetched, {cache_hits} cache-hit, {mb:.1} MB total under {}",
out.display()
);
Ok(())
}