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