crankshaft_engine/task/input/contents.rs
1//! Contents of an input.
2
3use std::borrow::Cow;
4use std::fs;
5use std::io::Write;
6use std::path::Path;
7use std::path::PathBuf;
8
9use anyhow::Context;
10use anyhow::anyhow;
11use anyhow::bail;
12use thiserror::Error;
13use url::Url;
14
15/// An error related to an input's [`Contents`].
16#[derive(Error, Debug)]
17pub enum Error {
18 /// An error parsing a [`Url`](url::Url).
19 #[error("invalid URL: {0}")]
20 ParseUrl(url::ParseError),
21}
22
23/// A [`Result`](std::result::Result) with an [`Error`].
24pub type Result<T> = std::result::Result<T, Error>;
25
26/// The source of an input.
27#[derive(Clone, Debug)]
28pub enum Contents {
29 /// Contents sourced from a URL.
30 Url(Url),
31
32 /// Contents provided as a literal array of bytes.
33 Literal(Vec<u8>),
34
35 /// Contents are provided as a path to a file or directory on the host
36 /// system.
37 Path(PathBuf),
38}
39
40impl Contents {
41 /// Attempts to create a URL contents from a string slice.
42 pub fn url_from_str(url: impl AsRef<str>) -> Result<Self> {
43 url.as_ref().parse().map(Self::Url).map_err(Error::ParseUrl)
44 }
45
46 /// Consumes `self` and one hot encodes the inner contents.
47 ///
48 /// * The first value is the [`Url`] if the type is [`Contents::Url`]. Else,
49 /// the value is [`None`].
50 /// * The second value is the literal contents as a [`Vec<u8>`] if the type
51 /// is [`Contents::Literal`] or [`Contents::Path`]. Else, the value is
52 /// [`None`].
53 ///
54 /// Returns an error if the contents are to a path and the file contents
55 /// could not be read.
56 pub fn one_hot(self) -> anyhow::Result<(Option<Url>, Option<Vec<u8>>)> {
57 match self {
58 Self::Url(url) => Ok((Some(url), None)),
59 Self::Literal(value) => Ok((None, Some(value))),
60 Self::Path(path) => Ok((
61 None,
62 Some(fs::read(&path).with_context(|| {
63 format!("failed to read file `{path}`", path = path.display())
64 })?),
65 )),
66 }
67 }
68
69 /// Fetches the contents locally.
70 ///
71 /// If the contents is a path, the path is returned.
72 ///
73 /// If the contents is a literal, they are written to a temporary file.
74 ///
75 /// If the contents is a URL, the file is downloaded to a temporary file.
76 ///
77 /// Returns the path to the contents.
78 pub async fn fetch(&self, temp_dir: &Path) -> anyhow::Result<Cow<'_, Path>> {
79 let contents: Cow<'_, [u8]> = match self {
80 Self::Url(url) => {
81 match url.scheme() {
82 "file" => {
83 // SAFETY: we just checked to ensure this is a file, so
84 // getting the file path should always unwrap.
85 let path = url.to_file_path().map_err(|_| {
86 anyhow!(
87 "URL `{url}` has a file scheme but cannot be represented as a \
88 file path"
89 )
90 })?;
91 return Ok(path.into());
92 }
93 // TODO: remotely fetched contents should be cached somewhere
94 "http" | "https" => bail!("support for HTTP URLs is not yet implemented"),
95 "s3" => bail!("support for S3 URLs is not yet implemented"),
96 "az" => bail!("support for Azure Storage URLs is not yet implemented"),
97 "gs" => bail!("support for Google Cloud Storage URLs is not yet implemented"),
98 scheme => bail!("URL has unsupported scheme `{scheme}`"),
99 }
100 }
101 Self::Literal(bytes) => bytes.into(),
102 Self::Path(path) => return Ok(path.into()),
103 };
104
105 // Write the contents to a temporary file within the given temporary directory
106 let mut file = tempfile::NamedTempFile::new_in(temp_dir).with_context(|| {
107 format!(
108 "failed to create temporary input file in `{temp_dir}`",
109 temp_dir = temp_dir.display()
110 )
111 })?;
112
113 file.write(&contents).with_context(|| {
114 format!(
115 "failed to write input file contents to `{path}`",
116 path = file.path().display()
117 )
118 })?;
119
120 // Keep the file as the temporary directory itself will clean up the mounts
121 let (_, path) = file.keep().context("failed to persist temporary file")?;
122
123 Ok(path.into())
124 }
125}