Skip to main content

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}