wick_asset_reference/
asset_reference.rs

1use std::borrow::Cow;
2use std::future::Future;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5use std::sync::Arc;
6
7use asset_container::{self as assets, Asset, AssetManager, Progress};
8use bytes::Bytes;
9use normpath::PathExt;
10use parking_lot::RwLock;
11use tokio::io::AsyncReadExt;
12use tokio::sync::Mutex;
13use tokio_stream::Stream;
14use tracing::debug;
15use wick_oci_utils::OciOptions;
16
17use crate::{normalize_path, Error};
18
19#[derive(Debug, Clone)]
20pub struct FetchableAssetReference<'a>(&'a AssetReference, OciOptions);
21
22impl<'a> FetchableAssetReference<'a> {
23  pub async fn bytes(&self) -> Result<Bytes, Error> {
24    self.0.bytes(&self.1).await
25  }
26}
27
28impl<'a> std::ops::Deref for FetchableAssetReference<'a> {
29  type Target = AssetReference;
30
31  fn deref(&self) -> &Self::Target {
32    self.0
33  }
34}
35
36#[derive(Debug, Clone, serde::Serialize)]
37#[must_use]
38pub struct AssetReference {
39  pub(crate) location: String,
40  #[serde(skip)]
41  pub(crate) cache_location: Arc<RwLock<Option<PathBuf>>>,
42  #[serde(skip)]
43  pub(crate) baseurl: Arc<RwLock<Option<PathBuf>>>,
44  #[serde(skip)]
45  #[allow(unused)]
46  pub(crate) fetch_lock: Arc<Mutex<()>>,
47}
48
49impl<'a> From<assets::AssetRef<'a, AssetReference>> for AssetReference {
50  fn from(asset_ref: assets::AssetRef<'a, AssetReference>) -> Self {
51    std::ops::Deref::deref(&asset_ref).clone()
52  }
53}
54
55impl FromStr for AssetReference {
56  type Err = Error;
57
58  fn from_str(s: &str) -> Result<Self, Self::Err> {
59    Ok(Self::new(s))
60  }
61}
62
63impl From<&str> for AssetReference {
64  fn from(s: &str) -> Self {
65    Self::new(s)
66  }
67}
68
69impl From<&String> for AssetReference {
70  fn from(s: &String) -> Self {
71    Self::new(s)
72  }
73}
74
75impl From<&Path> for AssetReference {
76  fn from(s: &Path) -> Self {
77    Self::new(s.to_string_lossy())
78  }
79}
80
81impl From<&PathBuf> for AssetReference {
82  fn from(s: &PathBuf) -> Self {
83    Self::new(s.to_string_lossy())
84  }
85}
86
87impl std::fmt::Display for AssetReference {
88  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89    write!(f, "{}", self.location)
90  }
91}
92
93impl PartialEq for AssetReference {
94  fn eq(&self, other: &Self) -> bool {
95    self.location == other.location && *self.baseurl.read() == *other.baseurl.read()
96  }
97}
98
99impl Eq for AssetReference {}
100
101impl std::hash::Hash for AssetReference {
102  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
103    self.location.hash(state);
104    self.baseurl.read().hash(state);
105  }
106}
107
108impl AssetReference {
109  /// Create a new location reference.
110  pub fn new<T: Into<String>>(location: T) -> Self {
111    Self {
112      location: location.into(),
113      cache_location: Default::default(),
114      baseurl: Default::default(),
115      fetch_lock: Default::default(),
116    }
117  }
118
119  /// Embed [OciOptions] with an [AssetReference].
120  #[must_use]
121  pub const fn with_options(&self, options: OciOptions) -> FetchableAssetReference<'_> {
122    FetchableAssetReference(self, options)
123  }
124
125  /// Get the relative part of the path or return an error if the path does not exist within the base URL.
126  pub fn get_relative_part(&self) -> Result<PathBuf, Error> {
127    let path = self.path()?;
128    let base_dir = self.baseurl().unwrap(); // safe to unwrap because this is a developer error if it panics.
129
130    let base_dir = base_dir.normalize().map_err(|_| Error::NotFound(path.clone()))?;
131    let mut base_dir = base_dir.as_path().to_string_lossy().to_string();
132    if !base_dir.ends_with(std::path::MAIN_SEPARATOR_STR) {
133      base_dir.push_str(std::path::MAIN_SEPARATOR_STR);
134    }
135
136    path.strip_prefix(&base_dir).map_or_else(
137      |_e| Err(Error::FileEscapesRoot(path.clone(), base_dir)),
138      |s| Ok(s.to_owned()),
139    )
140  }
141
142  #[must_use]
143  pub fn baseurl(&self) -> Option<PathBuf> {
144    self.baseurl.read().clone()
145  }
146
147  pub fn path(&self) -> Result<PathBuf, Error> {
148    self.resolve_path(true)
149  }
150
151  #[allow(clippy::option_if_let_else)]
152  // Pass true to resolve path from cache, false to resolve from location irrespective of cache.
153  fn resolve_path(&self, use_cache: bool) -> Result<PathBuf, Error> {
154    if let Some(cache_loc) = self.cache_location.read().as_ref() {
155      if use_cache {
156        return Ok(cache_loc.clone());
157      }
158    }
159    if let Ok(path) = normalize_path(self.location.as_ref(), self.baseurl()) {
160      if path.exists() {
161        return Ok(path);
162      }
163    }
164    Err(Error::Unresolvable(self.location.clone()))
165  }
166
167  #[must_use]
168  pub fn location(&self) -> &str {
169    &self.location
170  }
171
172  #[must_use]
173  pub fn is_directory(&self) -> bool {
174    self.path().map_or(false, |path| path.is_dir())
175  }
176
177  pub async fn bytes(&self, options: &OciOptions) -> Result<Bytes, Error> {
178    match self.fetch(options.clone()).await {
179      Ok(bytes) => Ok(bytes.into()),
180      Err(_err) => Err(Error::LoadError(self.path()?)),
181    }
182  }
183
184  /// Check if the asset exists on disk and isn't in the cache.
185  #[must_use]
186  pub fn exists_outside_cache(&self) -> bool {
187    let path = self.resolve_path(false);
188    path.is_ok() && path.unwrap().exists()
189  }
190}
191
192impl Asset for AssetReference {
193  type Options = OciOptions;
194
195  #[allow(clippy::expect_used)]
196  fn update_baseurl(&self, baseurl: &Path) {
197    let baseurl = if baseurl.starts_with(".") {
198      let mut path = std::env::current_dir().expect("failed to get current dir");
199      path.push(baseurl);
200
201      path
202    } else {
203      baseurl.to_owned()
204    };
205
206    *self.baseurl.write() = Some(baseurl);
207  }
208
209  fn fetch_with_progress(&self, _options: OciOptions) -> std::pin::Pin<Box<dyn Stream<Item = Progress> + Send + '_>> {
210    unimplemented!()
211  }
212
213  fn fetch(
214    &self,
215    options: OciOptions,
216  ) -> std::pin::Pin<Box<dyn Future<Output = Result<Vec<u8>, assets::Error>> + Send + Sync>> {
217    let asset = self.clone();
218
219    Box::pin(async move {
220      let lock = asset.fetch_lock.lock().await;
221      let path = asset.path();
222
223      let exists = path.as_ref().map_or(false, |p| p.exists());
224      if exists {
225        // Drop the lock immediately. We can fetch from disk all day.
226        drop(lock);
227        let path = path.unwrap();
228        if path.is_dir() {
229          return Err(assets::Error::IsDirectory(path.clone()));
230        }
231
232        debug!(path = %path.display(), "fetching local asset");
233        let mut file = tokio::fs::File::open(&path)
234          .await
235          .map_err(|err| assets::Error::FileOpen(path.clone(), err.to_string()))?;
236        let mut bytes = Vec::new();
237
238        file.read_to_end(&mut bytes).await?;
239        Ok(bytes)
240      } else {
241        let path = asset.location();
242        debug!(%path, "fetching remote asset");
243        let (cache_loc, bytes) = retrieve_remote(path, options)
244          .await
245          .map_err(|err| assets::Error::RemoteFetch(path.to_owned(), err.to_string()))?;
246
247        *asset.cache_location.write() = Some(cache_loc);
248        Ok(bytes)
249      }
250    })
251  }
252
253  fn name(&self) -> &str {
254    self.location.as_str()
255  }
256}
257
258async fn retrieve_remote(location: &str, options: OciOptions) -> Result<(PathBuf, Vec<u8>), Error> {
259  let result = wick_oci_utils::package::pull(location, &options)
260    .await
261    .map_err(|e| Error::PullFailed(PathBuf::from(location), e.to_string()))?;
262  let cache_location = result.base_dir.join(result.root_path);
263  let bytes = tokio::fs::read(&cache_location)
264    .await
265    .map_err(|_| Error::LoadError(cache_location.clone()))?;
266  Ok((cache_location, bytes))
267}
268
269impl AssetManager for AssetReference {
270  type Asset = AssetReference;
271
272  fn set_baseurl(&self, baseurl: &Path) {
273    self.update_baseurl(baseurl);
274  }
275
276  fn assets(&self) -> assets::Assets<Self::Asset> {
277    assets::Assets::new(vec![Cow::Borrowed(self)], 0)
278  }
279}
280
281impl TryFrom<String> for AssetReference {
282  type Error = Error;
283  fn try_from(val: String) -> Result<Self, Error> {
284    Ok(Self::new(val))
285  }
286}
287
288#[cfg(test)]
289mod test {
290
291  use std::path::PathBuf;
292
293  use anyhow::Result;
294
295  use super::*;
296  use crate::Error;
297
298  #[test_logger::test]
299  fn test_no_baseurl() -> Result<()> {
300    let location = AssetReference::new("Cargo.toml");
301    println!("location: {:?}", location);
302    let mut expected = std::env::current_dir().unwrap();
303    expected.push("Cargo.toml");
304    assert_eq!(location.path()?, expected);
305    assert!((location.path()?).exists());
306    Ok(())
307  }
308
309  #[test_logger::test]
310  fn test_baseurl() -> Result<()> {
311    let location = AssetReference::new("Cargo.toml");
312    let mut root_project_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
313    root_project_dir.pop();
314    root_project_dir.pop();
315    root_project_dir.pop();
316
317    location.set_baseurl(&root_project_dir);
318    let mut expected = root_project_dir;
319    expected.push("Cargo.toml");
320    assert_eq!(location.path()?, expected);
321    assert!((location.path()?).exists());
322
323    Ok(())
324  }
325
326  #[test_logger::test]
327  fn test_relative_with_baseurl() -> Result<()> {
328    let location = AssetReference::new("../Cargo.toml");
329    let mut root_project_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
330
331    root_project_dir.pop();
332    root_project_dir.pop();
333
334    location.set_baseurl(&root_project_dir);
335    let mut expected = root_project_dir;
336    expected.pop();
337    expected.push("Cargo.toml");
338    assert_eq!(location.path()?, expected);
339
340    Ok(())
341  }
342
343  #[test_logger::test]
344  fn test_relative_with_baseurl2() -> Result<()> {
345    let location = AssetReference::new("../src/utils.rs");
346    let mut crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
347    crate_dir.push("src");
348    location.set_baseurl(&crate_dir);
349    println!("crate_dir: {}", crate_dir.to_string_lossy());
350    println!("actual: {:#?}", location);
351    let expected = PathBuf::from(&crate_dir);
352    let expected = expected.join("..").join("src").join("utils.rs");
353    println!("expected: {}", expected.to_string_lossy());
354    println!("actual: {}", location.path()?.to_string_lossy());
355
356    let expected = expected.normalize()?;
357    assert_eq!(location.path()?, expected);
358
359    Ok(())
360  }
361
362  #[rstest::rstest]
363  #[case("./files/assets/test.fake.wasm", Ok("files/assets/test.fake.wasm"))]
364  #[case("./files/./assets/test.fake.wasm", Ok("files/assets/test.fake.wasm"))]
365  #[case("./files/../files/assets/test.fake.wasm", Ok("files/assets/test.fake.wasm"))]
366  fn test_relativity(#[case] path: &str, #[case] expected: Result<&str, Error>) -> Result<()> {
367    let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
368    let testdata_dir = crate_dir.join("../../../integration-tests/testdata");
369
370    println!("base dir: {}", testdata_dir.display());
371    println!("asset location: {}", path);
372    let asset = AssetReference::new(path);
373    asset.update_baseurl(&testdata_dir);
374
375    let result = asset.get_relative_part();
376    let expected = expected.map(|s| PathBuf::from(s.to_owned()));
377    assert_eq!(result, expected);
378
379    Ok(())
380  }
381
382  #[rstest::rstest]
383  #[case("org/repo:0.6.0")]
384  #[case("registry.candle.dev/org/repo:0.6.0")]
385  fn test_unresolvable_paths(#[case] path: &str) -> Result<()> {
386    println!("asset location: {}", path);
387    let asset = AssetReference::new(path);
388
389    assert_eq!(asset.path(), Err(Error::Unresolvable(path.to_owned())));
390
391    Ok(())
392  }
393
394  #[rstest::rstest]
395  #[case(
396    "./integration-tests/testdata/files/assets/test.fake.wasm",
397    Ok("integration-tests/testdata/files/assets/test.fake.wasm")
398  )]
399  #[case(
400    "./integration-tests/../integration-tests/testdata/files/./assets/test.fake.wasm",
401    Ok("integration-tests/testdata/files/assets/test.fake.wasm")
402  )]
403  #[case(
404    "./integration-tests/./testdata/./files/../files/assets/test.fake.wasm",
405    Ok("integration-tests/testdata/files/assets/test.fake.wasm")
406  )]
407  fn test_baseurl_normalization(#[case] path: &str, #[case] expected: Result<&str, Error>) -> Result<()> {
408    let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
409    let ws_dir = crate_dir.join("../../../");
410    let base_dir = ws_dir.join("integration-tests/./testdata/../../integration-tests/./testdata/../..");
411
412    println!("base dir: {}", base_dir.display());
413    println!("asset location: {}", path);
414    let asset = AssetReference::new(path);
415    asset.update_baseurl(&base_dir);
416
417    let result = asset.get_relative_part();
418    let expected = expected.map(|s| PathBuf::from(s.to_owned()));
419    assert_eq!(result, expected);
420
421    Ok(())
422  }
423
424  #[rstest::rstest]
425  #[case(
426    "/integration-tests/testdata/files/assets/test.fake.wasm",
427    Ok("files/assets/test.fake.wasm")
428  )]
429  #[case(
430    "/integration-tests/../integration-tests/testdata/files/./assets/test.fake.wasm",
431    Ok("files/assets/test.fake.wasm")
432  )]
433  #[case(
434    "/integration-tests/./testdata/./files/../files/assets/test.fake.wasm",
435    Ok("files/assets/test.fake.wasm")
436  )]
437  fn test_path_normalization_absolute(#[case] path: &str, #[case] expected: Result<&str, Error>) -> Result<()> {
438    let crate_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
439    let ws_dir = crate_dir.join("../../../");
440    let testdata_dir = ws_dir.join("integration-tests/./testdata/../../integration-tests/./testdata");
441    let path = format!("{}/{}", ws_dir.display(), path);
442
443    println!("base dir: {}", testdata_dir.display());
444    println!("asset location: {}", path);
445    let asset = AssetReference::new(path);
446    asset.update_baseurl(&testdata_dir);
447
448    let result = asset.get_relative_part();
449    let expected = expected.map(|s| PathBuf::from(s.to_owned()));
450    assert_eq!(result, expected);
451
452    Ok(())
453  }
454}