use std::path::{Path, PathBuf};
use anyhow::Context;
use crate::asset_backend::{
mime_for_path, store_from_config, AssetBackendConfig, AssetBackendKind, AssetStore,
};
use crate::assets::{
build_client_bundle_index, resolve_bundle_file_path, AssetBundleIndex, ClientBundleSources,
};
use crate::content_pack::{build_sim_pack_index, upload_sim_pack};
#[derive(Debug, Clone)]
pub struct PublishPacksResult {
pub rev: u64,
pub published_at: String,
pub summary: String,
pub client_files: usize,
pub sim_files: usize,
pub uploaded: bool,
pub backend_kind: String,
pub bucket: String,
pub dest: String,
}
pub async fn publish_content_packs(
repo_root: &Path,
summary: String,
publish_rev: u64,
published_at: String,
skip_upload: bool,
) -> anyhow::Result<PublishPacksResult> {
publish_content_packs_opts(
repo_root,
summary,
publish_rev,
published_at,
skip_upload,
true,
)
.await
}
pub async fn publish_content_packs_opts(
repo_root: &Path,
summary: String,
publish_rev: u64,
published_at: String,
skip_upload: bool,
upload_sim: bool,
) -> anyhow::Result<PublishPacksResult> {
let (client_index, sources) =
build_client_sources(repo_root, publish_rev, &published_at)?;
let sim_index = build_sim_pack_index(repo_root, publish_rev, &published_at)?;
let cfg = AssetBackendConfig::from_env();
let mut uploaded = false;
if !skip_upload {
let store = store_from_config(&cfg)?;
upload_client_bundle(store.as_ref(), &cfg, &sources, &client_index)
.await
.context("upload client asset pack")?;
if upload_sim {
upload_sim_pack(store.as_ref(), &cfg, repo_root, &sim_index)
.await
.context("upload sim content pack")?;
}
uploaded = true;
}
Ok(PublishPacksResult {
rev: publish_rev,
published_at,
summary,
client_files: client_index.files.len(),
sim_files: sim_index.files.len(),
uploaded,
backend_kind: backend_label(&cfg).to_string(),
bucket: cfg.bucket.clone(),
dest: describe_dest(&cfg, &cfg.client_prefix, publish_rev),
})
}
struct ClientBundleOwned {
sprites_dir: PathBuf,
paperdoll_dir: Option<PathBuf>,
player_presentation: Option<PathBuf>,
player_art_dir: Option<PathBuf>,
client_settings: Option<PathBuf>,
terrain_kinds: Option<PathBuf>,
}
impl ClientBundleOwned {
fn as_refs(&self) -> ClientBundleSources<'_> {
ClientBundleSources {
sprites_dir: &self.sprites_dir,
paperdoll_dir: self.paperdoll_dir.as_deref(),
player_presentation: self.player_presentation.as_deref(),
player_art_dir: self.player_art_dir.as_deref(),
client_settings: self.client_settings.as_deref(),
terrain_kinds: self.terrain_kinds.as_deref(),
}
}
}
fn build_client_sources(
repo_root: &Path,
publish_rev: u64,
published_at: &str,
) -> anyhow::Result<(AssetBundleIndex, ClientBundleOwned)> {
let sprites_dir = {
let repo_sprites = repo_root.join("assets/gfx/sprites");
if repo_sprites.join("manifest.yaml").is_file() {
repo_sprites
} else {
flatland_presentation::default_sprites_dir().ok_or_else(|| {
anyhow::anyhow!("gfx sprites dir not found under assets/gfx/sprites")
})?
}
};
let paperdoll_dir = repo_root
.join("assets/paperdoll")
.is_dir()
.then(|| repo_root.join("assets/paperdoll"));
let player_presentation = {
let p = repo_root.join("assets/gfx/player-presentation.yaml");
p.is_file().then_some(p)
};
let player_art_dir = {
let p = repo_root.join("assets/gfx/player");
p.is_dir().then_some(p)
};
let client_settings = {
let p = repo_root.join("assets/config/client-settings.yaml");
p.is_file().then_some(p)
};
let terrain_kinds = {
let p = repo_root.join("assets/world/terrain-kinds.yaml");
p.is_file().then_some(p)
};
let owned = ClientBundleOwned {
sprites_dir,
paperdoll_dir,
player_presentation,
player_art_dir,
client_settings,
terrain_kinds,
};
let index = build_client_bundle_index(&owned.as_refs(), publish_rev, published_at)?;
Ok((index, owned))
}
async fn upload_client_bundle(
store: &dyn AssetStore,
cfg: &AssetBackendConfig,
sources: &ClientBundleOwned,
index: &AssetBundleIndex,
) -> anyhow::Result<()> {
let refs = sources.as_refs();
for (rel, _) in &index.files {
let path = resolve_bundle_file_path(&refs, rel);
let bytes = std::fs::read(&path).with_context(|| {
format!(
"read client pack file {rel} from {}",
path.display()
)
})?;
let key = cfg.client_object_key(index.publish_rev, rel);
store
.put(&key, mime_for_path(&path), &bytes)
.await
.with_context(|| format!("upload client {key}"))?;
}
let latest = serde_json::to_vec(index)?;
store
.put(&cfg.client_index_object(), "application/json", &latest)
.await?;
Ok(())
}
fn backend_label(cfg: &AssetBackendConfig) -> &'static str {
match cfg.kind {
AssetBackendKind::Local => "local",
AssetBackendKind::Gcs => "gcs",
AssetBackendKind::S3 => "s3",
}
}
fn describe_dest(cfg: &AssetBackendConfig, prefix: &str, rev: u64) -> String {
match cfg.kind {
AssetBackendKind::Local => cfg
.local_root
.join(prefix)
.join(format!("rev-{rev}"))
.display()
.to_string(),
AssetBackendKind::Gcs => format!("gs://{}/{prefix}/rev-{rev}/", cfg.bucket),
AssetBackendKind::S3 => format!("s3://{}/{prefix}/rev-{rev}/", cfg.bucket),
}
}