1use std::collections::{BTreeMap, BTreeSet};
4use std::path::Path;
5
6use phoxal_model::AssetId;
7
8use crate::{
9 ASSETS_DIR, AssetIndex, BinaryReference, BinarySource, BundleError, BundlePath, BundleRoot,
10 DocumentError, RuntimeBundle, RuntimeDocument, copy_executable_source, create_staging_root,
11 ensure_staging_directory, prepare_publish_parent, publish_staging_root, reject_existing_target,
12 write_new_file,
13};
14
15pub struct BundleWriter;
17
18impl BundleWriter {
19 pub fn write(
25 root: impl AsRef<Path>,
26 document: &RuntimeDocument,
27 assets: &BTreeMap<AssetId, Vec<u8>>,
28 binaries: &BTreeMap<BundlePath, BinarySource>,
29 ) -> Result<RuntimeBundle, BundleError> {
30 write_bundle(root, document, assets, binaries)
31 }
32}
33
34fn write_bundle(
35 root: impl AsRef<Path>,
36 document: &RuntimeDocument,
37 assets: &BTreeMap<AssetId, Vec<u8>>,
38 binaries: &BTreeMap<BundlePath, BinarySource>,
39) -> Result<RuntimeBundle, BundleError> {
40 let expected_assets = document.runtime().assets();
41 let supplied_assets = AssetIndex::from_bytes(assets)?;
42 if supplied_assets.entries() != expected_assets.entries() {
43 return Err(BundleError::Document(DocumentError::AssetIndexMismatch));
44 }
45
46 let expected_binaries = document
47 .artifacts()
48 .values()
49 .map(|artifact| artifact.path().clone())
50 .collect::<BTreeSet<_>>();
51 let supplied_binaries = binaries.keys().cloned().collect::<BTreeSet<_>>();
52 if expected_binaries != supplied_binaries {
53 return Err(BundleError::Document(DocumentError::BinaryIndexMismatch));
54 }
55 let publish_target = prepare_publish_parent(root.as_ref())?;
56 reject_existing_target(&publish_target)?;
57 let staging_path = create_staging_root(&publish_target)?;
58 let root = BundleRoot::open(&staging_path)?;
59 let staged = (|| {
60 ensure_staging_directory(&root, ASSETS_DIR)?;
61 ensure_staging_directory(&root, crate::BIN_DIR)?;
62 for (id, bytes) in assets {
63 let path = BundlePath::new(format!("{ASSETS_DIR}/{}", id.as_str()))?;
64 write_new_file(&root, &path, bytes)?;
65 }
66 for (path, source) in binaries {
67 let artifact: &BinaryReference = document
68 .artifacts()
69 .values()
70 .find(|artifact| artifact.path() == path)
71 .ok_or_else(|| BundleError::MissingFile {
72 path: path.filesystem_path(root.path()),
73 })?;
74 copy_executable_source(
75 &root,
76 source,
77 path,
78 artifact.digest(),
79 artifact.size_bytes(),
80 )?;
81 }
82 let json = serde_json::to_vec_pretty(document)?;
83 write_new_file(&root, &BundlePath::new(crate::RUNTIME_FILE)?, &json)?;
84 RuntimeBundle::open_verified(root.path())
85 })();
86 let verified = match staged {
87 Ok(verified) => verified,
88 Err(error) => {
89 let _ = std::fs::remove_dir_all(&staging_path);
90 return Err(error);
91 }
92 };
93 if let Err(error) = publish_staging_root(root.path(), &publish_target) {
94 let _ = std::fs::remove_dir_all(&staging_path);
95 return Err(error);
96 }
97 Ok(verified.relocated(publish_target))
98}