1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
use std::path::{Path, PathBuf};

use asset_container::AssetManager;
use sha256::digest;
use tokio::fs;
use wick_config::WickConfiguration;
use wick_oci_utils::package::annotations::Annotations;
use wick_oci_utils::package::{media_types, PackageFile};
use wick_oci_utils::OciOptions;

use crate::utils::{get_relative_path, metadata_to_annotations};
use crate::{Error, WickPackageKind};

/// Represents a Wick package, including its files and metadata.
#[derive(Debug, Clone)]
pub struct WickPackage {
  #[allow(unused)]
  kind: WickPackageKind,
  #[allow(dead_code)]
  name: String,
  #[allow(dead_code)]
  version: String,
  files: Vec<PackageFile>,
  #[allow(unused)]
  annotations: Annotations,
}

impl WickPackage {
  /// Creates a new WickPackage from the provided path.
  ///
  /// The provided path can be a file or directory. If it is a directory, the WickPackage will be created
  /// based on the files within the directory.
  pub async fn from_path(path: &Path) -> Result<Self, Error> {
    //add check to see if its a path or directory and call appropriate api to find files based on that.
    if path.is_dir() {
      return Err(Error::Directory(path.to_path_buf()));
    }

    let options = wick_config::FetchOptions::default();
    let config = WickConfiguration::fetch(&path.to_string_lossy(), options).await?;
    if !matches!(config, WickConfiguration::App(_) | WickConfiguration::Component(_)) {
      return Err(Error::InvalidWickConfig(path.to_string_lossy().to_string()));
    }
    let full_path = tokio::fs::canonicalize(path)
      .await
      .map_err(|e| Error::ReadFile(path.to_path_buf(), e))?;
    let parent_dir = full_path
      .parent()
      .map_or_else(|| PathBuf::from("/"), |v| v.to_path_buf());

    let (kind, name, version, annotations, parent_dir, media_type) = match &config {
      WickConfiguration::App(app_config) => {
        let name = app_config.name();
        let version = app_config.version();
        let annotations = metadata_to_annotations(app_config.metadata());
        let media_type = media_types::APPLICATION;
        let kind = WickPackageKind::APPLICATION;
        (kind, name, version, annotations, parent_dir, media_type)
      }
      WickConfiguration::Component(component_config) => {
        let name = component_config.name().clone().ok_or(Error::NoName)?;
        let version = component_config.version();
        let annotations = metadata_to_annotations(component_config.metadata());
        let media_type = media_types::COMPONENT;
        let kind = WickPackageKind::COMPONENT;
        (kind, name, version, annotations, parent_dir, media_type)
      }
      _ => return Err(Error::InvalidWickConfig(path.to_string_lossy().to_string())),
    };

    let mut assets = config.assets();
    let mut wick_files: Vec<PackageFile> = Vec::new();

    let root_bytes = fs::read(path)
      .await
      .map_err(|e| Error::ReadFile(path.to_path_buf(), e))?;
    let root_hash = format!("sha256:{}", digest(root_bytes.as_slice()));

    let root_file = PackageFile::new(
      PathBuf::from(path.file_name().unwrap()),
      root_hash,
      media_type.to_owned(),
      root_bytes.into(),
    );

    wick_files.push(root_file);

    //populate wick_files
    for asset in assets.iter() {
      let location = asset.location(); // the path specified in the config
      let asset_path = asset.path()?; // the resolved, abolute path relative to the config location.

      let path = get_relative_path(&parent_dir, &asset_path)?;

      let options = wick_config::FetchOptions::default();
      let media_type: &str;

      match path.extension().and_then(|os_str| os_str.to_str()) {
        Some("yaml" | "yml" | "wick") => {
          let config = WickConfiguration::fetch(asset_path, options.clone()).await;
          match config {
            Ok(WickConfiguration::App(_)) => {
              media_type = media_types::APPLICATION;
            }
            Ok(WickConfiguration::Component(_)) => {
              media_type = media_types::COMPONENT;
            }
            Ok(WickConfiguration::Tests(_)) => {
              media_type = media_types::TESTS;
            }
            Ok(WickConfiguration::Types(_)) => {
              media_type = media_types::TYPES;
            }
            Err(_) => {
              media_type = media_types::OTHER;
            }
          }
        }
        Some("wasm") => {
          media_type = media_types::WASM;
        }
        _ => {
          media_type = media_types::OTHER;
        }
      }

      let file_bytes = asset.bytes(&options).await?;
      let hash = format!("sha256:{}", digest(file_bytes.as_ref()));
      let wick_file = PackageFile::new(PathBuf::from(location), hash, media_type.to_owned(), file_bytes);
      wick_files.push(wick_file);
    }

    Ok(Self {
      kind,
      name: name.clone(),
      version: version.clone(),
      files: wick_files,
      annotations,
    })
  }

  #[must_use]
  /// Returns a list of the files contained within the WickPackage.
  pub fn list_files(&self) -> Vec<&PackageFile> {
    self.files.iter().collect()
  }

  /// Pushes the WickPackage to a specified registry using the provided reference, username, and password.
  ///
  /// The username and password are optional. If not provided, the function falls back to anonymous authentication.
  pub async fn push(&mut self, reference: &str, options: &OciOptions) -> Result<String, Error> {
    let config = crate::WickConfig { kind: self.kind };
    let image_config_contents = serde_json::to_string(&config).unwrap();
    let files = self.files.drain(..).collect();

    let push_response = wick_oci_utils::package::push(
      reference,
      image_config_contents,
      files,
      self.annotations.clone(),
      options,
    )
    .await?;

    println!("Image successfully pushed to the registry.");
    println!("Config URL: {}", push_response.config_url);
    println!("Manifest URL: {}", push_response.manifest_url);
    Ok(push_response.manifest_url)
  }

  /// This function pulls a WickPackage from a specified registry using the provided reference, username, and password.
  pub async fn pull(reference: &str, options: &OciOptions) -> Result<Self, Error> {
    let result = wick_oci_utils::package::pull(reference, options).await?;

    let package = Self::from_path(&result.base_dir.join(Path::new(&result.root_path))).await;

    match package {
      Ok(package) => Ok(package),
      Err(e) => Err(Error::PackageReadFailed(e.to_string())),
    }
  }
}