1use std::collections::HashSet;
2use std::future::Future;
3use std::path::{Path, PathBuf};
4
5use asset_container::{Asset, AssetFlags, AssetManager, Assets};
6use normpath::PathExt;
7use sha256::digest;
8use tokio::fs;
9use tracing::trace;
10use wick_config::config::RegistryConfig;
11use wick_config::{AssetReference, WickConfiguration};
12use wick_oci_utils::package::annotations::Annotations;
13use wick_oci_utils::package::{media_types, PackageFile};
14use wick_oci_utils::OciOptions;
15
16use crate::utils::{create_tar_gz, metadata_to_annotations};
17use crate::Error;
18
19type BoxFuture<'a, T> = std::pin::Pin<Box<dyn Future<Output = T> + Send + 'a>>;
20
21type ProcessResult = (HashSet<PathBuf>, Vec<PackageFile>);
22
23fn process_assets(
24 mut seen_assets: HashSet<PathBuf>,
25 assets: Assets<AssetReference>,
26 root_parent_dir: PathBuf,
27 parent_dir: PathBuf,
28) -> BoxFuture<Result<ProcessResult, Error>> {
29 let task = async move {
30 let mut wick_files: Vec<PackageFile> = Vec::new();
31 for asset in assets.iter() {
32 if asset.get_asset_flags() == AssetFlags::Lazy {
33 continue;
34 }
35 let pdir = parent_dir.clone();
36 asset.update_baseurl(&pdir);
37 if !asset.exists_outside_cache() {
38 continue;
39 }
40 let asset_path = asset.path()?; if seen_assets.contains(&asset_path) {
42 continue;
43 }
44 seen_assets.insert(asset_path.clone());
45
46 let relative_path = asset_path.strip_prefix(&root_parent_dir).unwrap_or(&asset_path);
47
48 let options = wick_config::FetchOptions::default();
49 let media_type: &str;
50
51 let new_parent_dir = asset_path
52 .parent()
53 .map_or_else(|| PathBuf::from("/"), |v| v.to_path_buf());
54
55 match relative_path.extension().and_then(|os_str| os_str.to_str()) {
56 Some("yaml" | "yml" | "wick") => {
57 let config = WickConfiguration::fetch((*asset).clone(), options.clone())
58 .await
59 .map(|b| b.into_inner());
60 match config {
61 Ok(WickConfiguration::App(config)) => {
62 media_type = media_types::APPLICATION;
63 let app_assets = config.assets();
64 let (newly_seen, app_files) =
65 process_assets(seen_assets, app_assets, root_parent_dir.clone(), new_parent_dir.clone()).await?;
66 seen_assets = newly_seen;
67 wick_files.extend(app_files);
68 }
69 Ok(WickConfiguration::Component(config)) => {
70 media_type = media_types::COMPONENT;
71 let component_assets = config.assets();
72 let (newly_seen, component_files) = process_assets(
73 seen_assets,
74 component_assets,
75 root_parent_dir.clone(),
76 new_parent_dir.clone(),
77 )
78 .await?;
79 seen_assets = newly_seen;
80 wick_files.extend(component_files);
81 }
82 Ok(WickConfiguration::Tests(_)) => {
83 media_type = media_types::TESTS;
84 }
85 Ok(WickConfiguration::Types(_)) => {
86 media_type = media_types::TYPES;
87 }
88 _ => {
89 media_type = media_types::OTHER;
90 }
91 }
92 }
93 Some("wasm") => {
94 media_type = media_types::WASM;
95 }
96 _ => {
97 media_type = media_types::OTHER;
98 }
99 }
100
101 if asset.exists_outside_cache() {
102 let file_bytes = asset.bytes(&options).await?;
103 let hash = format!("sha256:{}", digest(file_bytes.as_ref()));
104 let wick_file = PackageFile::new(relative_path.to_path_buf(), hash, media_type.to_owned(), file_bytes);
105 wick_files.push(wick_file);
106 }
107 }
108
109 Ok((seen_assets, wick_files))
110 };
111 Box::pin(task)
112}
113
114#[derive(Debug, Clone)]
116pub struct WickPackage {
117 kind: wick_config::config::ConfigurationKind,
118 name: String,
119 version: String,
120 files: Vec<PackageFile>,
121 annotations: Annotations,
122 absolute_path: PathBuf,
123 registry: Option<RegistryConfig>,
124 root: String,
125 basedir: Option<PathBuf>,
126}
127
128impl WickPackage {
129 #[allow(clippy::too_many_lines)]
134 pub async fn from_path(basedir: Option<PathBuf>, path: &Path) -> Result<Self, Error> {
135 let path = basedir
136 .as_ref()
137 .map_or_else(|| path.to_path_buf(), |basedir| basedir.join(path));
138 if path.is_dir() {
140 return Err(Error::Directory(path));
141 }
142
143 let options = wick_config::FetchOptions::default();
144 let config = WickConfiguration::fetch(&path, options).await?.into_inner();
145 if !matches!(
146 config,
147 WickConfiguration::App(_) | WickConfiguration::Component(_) | WickConfiguration::Types(_)
148 ) {
149 return Err(Error::InvalidWickConfig(path.to_string_lossy().to_string()));
150 }
151 let full_path = path
152 .normalize()
153 .map_err(|e| Error::ReadFile(path.clone(), e))?
154 .into_path_buf();
155
156 let parent_dir = full_path
157 .parent()
158 .map_or_else(|| PathBuf::from("/"), |v| v.to_path_buf());
159
160 if config.metadata().is_none() {
161 return Err(Error::NoMetadata(path.to_string_lossy().to_string()));
162 }
163
164 let annotations = metadata_to_annotations(config.metadata().unwrap());
165 let kind = config.kind();
166 let name = config.name().ok_or(Error::NoName)?;
167 let media_type = match kind {
168 wick_config::config::ConfigurationKind::App => media_types::APPLICATION,
169 wick_config::config::ConfigurationKind::Component => media_types::COMPONENT,
170 wick_config::config::ConfigurationKind::Types => media_types::TYPES,
171 wick_config::config::ConfigurationKind::Tests => unreachable!(),
172 wick_config::config::ConfigurationKind::Lockdown => unreachable!(),
173 };
174 let registry = config.package().and_then(|package| package.registry().cloned());
175
176 let (version, extra_files) = match &config {
177 WickConfiguration::App(config) => {
178 let version = config.version();
179
180 let extra_files = config.package_files().to_owned();
181
182 (version, extra_files)
183 }
184 WickConfiguration::Component(config) => {
185 let version = config.version();
186
187 let extra_files = config.package_files().map_or_else(Vec::new, |files| files.to_owned());
188
189 (version, extra_files)
190 }
191 WickConfiguration::Types(config) => {
192 let version = config.version();
193
194 let extra_files = config.package_files().map_or_else(Vec::new, |files| files.to_owned());
195
196 (version, extra_files)
197 }
198 _ => return Err(Error::InvalidWickConfig(path.to_string_lossy().to_string())),
199 };
200
201 let assets = config.assets();
202 let mut wick_files: Vec<PackageFile> = Vec::new();
203
204 let root_bytes = fs::read(&path).await.map_err(|e| Error::ReadFile(path.clone(), e))?;
205 let root_hash = format!("sha256:{}", digest(root_bytes.as_slice()));
206
207 let root_file = PackageFile::new(
208 PathBuf::from(path.file_name().unwrap()),
209 root_hash,
210 media_type.to_owned(),
211 root_bytes.into(),
212 );
213
214 let root_file_path = path.file_name().unwrap().to_string_lossy().to_string();
215 wick_files.push(root_file);
216
217 if !extra_files.is_empty() {
220 let gz_bytes = create_tar_gz(extra_files, &parent_dir).await?;
221
222 let tar_hash = format!("sha256:{}", digest(gz_bytes.as_slice()));
223 let tar_file = PackageFile::new(
224 PathBuf::from("extra_files.tar.gz"),
225 tar_hash,
226 media_types::TARGZ.to_owned(),
227 gz_bytes.into(),
228 );
229 wick_files.push(tar_file);
230 }
231
232 let (_, return_assets) = process_assets(Default::default(), assets, parent_dir.clone(), parent_dir.clone()).await?;
234 wick_files.extend(return_assets);
236 trace!(files = ?wick_files.iter().map(|f| f.package_path()).collect::<Vec<_>>(),
237 "package files"
238 );
239
240 Ok(Self {
241 kind,
242 name: name.to_owned(),
243 version: version.map(|v| v.to_owned()).ok_or(Error::NoVersion)?,
244 files: wick_files,
245 annotations,
246 absolute_path: full_path,
247 registry,
248 root: root_file_path,
249 basedir,
250 })
251 }
252
253 #[must_use]
254 pub fn list_files(&self) -> Vec<&PackageFile> {
256 self.files.iter().collect()
257 }
258
259 #[must_use]
260 pub const fn basedir(&self) -> Option<&PathBuf> {
262 self.basedir.as_ref()
263 }
264
265 #[must_use]
266 pub const fn path(&self) -> &PathBuf {
268 &self.absolute_path
269 }
270
271 #[must_use]
272 pub fn registry_reference(&self) -> Option<String> {
274 self
275 .registry
276 .as_ref()
277 .map(|r| format!("{}/{}/{}:{}", r.host(), r.namespace(), self.name, self.version))
278 }
279
280 #[must_use]
281 pub fn tagged_reference(&self, tag: &str) -> Option<String> {
283 self
284 .registry
285 .as_ref()
286 .map(|r| format!("{}/{}/{}:{}", r.host(), r.namespace(), self.name, tag))
287 }
288
289 #[must_use]
290 pub const fn registry(&self) -> Option<&RegistryConfig> {
292 self.registry.as_ref()
293 }
294
295 #[must_use]
296 pub fn registry_mut(&mut self) -> Option<&mut RegistryConfig> {
298 self.registry.as_mut()
299 }
300
301 pub async fn push(&mut self, reference: &str, options: &OciOptions) -> Result<String, Error> {
305 let kind = match self.kind {
306 wick_config::config::ConfigurationKind::App => wick_oci_utils::WickPackageKind::APPLICATION,
307 wick_config::config::ConfigurationKind::Component => wick_oci_utils::WickPackageKind::COMPONENT,
308 wick_config::config::ConfigurationKind::Types => wick_oci_utils::WickPackageKind::TYPES,
309 _ => {
310 return Err(Error::InvalidWickConfig(reference.to_owned()));
311 }
312 };
313 let config = wick_oci_utils::WickOciConfig::new(kind, self.root.clone());
314 let image_config_contents = serde_json::to_string(&config).unwrap();
315 let files = self.files.drain(..).collect();
316
317 let push_response = wick_oci_utils::package::push(
318 reference,
319 image_config_contents,
320 files,
321 self.annotations.clone(),
322 options,
323 )
324 .await?;
325
326 Ok(push_response.manifest_url)
327 }
328
329 pub async fn pull(reference: &str, options: &OciOptions) -> Result<Self, Error> {
331 let result = wick_oci_utils::package::pull(reference, options).await?;
332
333 let package = Self::from_path(Some(result.base_dir), &result.root_path).await;
334
335 match package {
336 Ok(package) => Ok(package),
337 Err(e) => Err(Error::PackageReadFailed(e.to_string())),
338 }
339 }
340}