wasefire_cli_tools/
fs.rs

1// Copyright 2023 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Wrappers around `tokio::fs` with descriptive errors.
16
17use std::fs::Metadata;
18use std::future::Future;
19use std::io::ErrorKind;
20use std::path::{Component, Path, PathBuf};
21
22use anyhow::{Context, Result};
23use serde::Serialize;
24use serde::de::DeserializeOwned;
25use tokio::fs::{File, OpenOptions, ReadDir};
26use tokio::io::{AsyncReadExt, AsyncWriteExt};
27
28pub async fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf> {
29    let name = path.as_ref().display();
30    tokio::fs::canonicalize(path.as_ref()).await.with_context(|| format!("canonicalizing {name}"))
31}
32
33pub async fn try_canonicalize(path: impl AsRef<Path>) -> Result<PathBuf> {
34    match canonicalize(&path).await {
35        Err(x) if x.downcast_ref::<std::io::Error>().unwrap().kind() == ErrorKind::NotFound => {
36            // We could try to canonicalize the existing prefix.
37            Ok(path.as_ref().to_path_buf())
38        }
39        x => x,
40    }
41}
42
43pub async fn try_relative(base: impl AsRef<Path>, path: impl AsRef<Path>) -> Result<PathBuf> {
44    let base = try_canonicalize(&base).await?;
45    let path = try_canonicalize(&path).await?;
46    let mut base = base.components().peekable();
47    let mut path = path.components().peekable();
48    // Advance the common prefix.
49    while matches!((base.peek(), path.peek()), (Some(x), Some(y)) if x == y) {
50        base.next();
51        path.next();
52    }
53    // Add necessary parent directory.
54    let mut result = PathBuf::new();
55    while base.next().is_some() {
56        result.push(Component::ParentDir);
57    }
58    // Add path suffix.
59    result.extend(path);
60    Ok(result)
61}
62
63/// Returns whether a destination file depending on a source file needs update.
64///
65/// If it does and the destination file exists, it is deleted. A file named like the destination
66/// file and suffixed with `.orig` is created.
67pub async fn has_changed(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> Result<bool> {
68    let dst_orig = dst.as_ref().with_added_extension("orig");
69    let mut changed = !exists(&dst).await
70        || metadata(&dst).await?.modified()? < metadata(&src).await?.modified()?
71        || !exists(&dst_orig).await;
72    if !changed {
73        let src_data = tokio::fs::read(&src).await?;
74        let dst_data = tokio::fs::read(&dst_orig).await?;
75        changed = src_data != dst_data;
76    }
77    if changed {
78        if exists(&dst_orig).await {
79            tokio::fs::remove_file(&dst_orig).await?;
80        } else if let Some(parent) = dst.as_ref().parent() {
81            create_dir_all(parent).await?;
82        }
83        match tokio::fs::remove_file(&dst).await {
84            Err(x) if x.kind() == ErrorKind::NotFound => (),
85            x => x?,
86        }
87        tokio::fs::copy(&src, &dst_orig).await?;
88    }
89    Ok(changed)
90}
91
92pub async fn copy(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<u64> {
93    let src = from.as_ref().display();
94    let dst = to.as_ref().display();
95    create_parent(to.as_ref()).await?;
96    debug!("cp {src:?} {dst:?}");
97    tokio::fs::copy(from.as_ref(), to.as_ref())
98        .await
99        .with_context(|| format!("copying {src} to {dst}"))
100}
101
102pub async fn create_dir_all(path: impl AsRef<Path>) -> Result<()> {
103    let name = path.as_ref().display();
104    if exists(path.as_ref()).await {
105        return Ok(());
106    }
107    debug!("mkdir -p {name:?}");
108    tokio::fs::create_dir_all(path.as_ref()).await.with_context(|| format!("creating {name}"))
109}
110
111pub async fn create_parent(path: impl AsRef<Path>) -> Result<()> {
112    if let Some(parent) = path.as_ref().parent()
113        && !parent.as_os_str().is_empty()
114    {
115        create_dir_all(parent).await?;
116    }
117    Ok(())
118}
119
120pub async fn download(url: &str) -> Result<Vec<u8>> {
121    debug!("download {url:?}");
122    let content: reqwest::Result<_> = try { reqwest::get(url).await?.bytes().await?.to_vec() };
123    content.with_context(|| format!("downloading {url}"))
124}
125
126pub async fn exists(path: impl AsRef<Path>) -> bool {
127    tokio::fs::try_exists(path).await.ok() == Some(true)
128}
129
130pub async fn metadata(path: impl AsRef<Path>) -> Result<Metadata> {
131    let name = path.as_ref().display();
132    tokio::fs::metadata(path.as_ref()).await.with_context(|| format!("reading {name} metadata"))
133}
134
135pub async fn read(path: impl AsRef<Path>) -> Result<Vec<u8>> {
136    let name = path.as_ref().display();
137    debug!("read < {name:?}");
138    tokio::fs::read(path.as_ref()).await.with_context(|| format!("reading {name}"))
139}
140
141pub async fn read_dir(path: impl AsRef<Path>) -> Result<ReadDir> {
142    let dir = path.as_ref().display();
143    debug!("walk {dir:?}");
144    tokio::fs::read_dir(path.as_ref()).await.with_context(|| format!("walking {dir}"))
145}
146
147pub async fn read_toml<T: DeserializeOwned>(path: impl AsRef<Path>) -> Result<T> {
148    let name = path.as_ref().display();
149    let contents = read(path.as_ref()).await?;
150    let data = String::from_utf8(contents).with_context(|| format!("reading {name}"))?;
151    toml::from_str(&data).with_context(|| format!("parsing {name}"))
152}
153
154pub async fn read_stdin() -> Result<Vec<u8>> {
155    let mut data = Vec::new();
156    tokio::io::stdin().read_to_end(&mut data).await.context("reading from stdin")?;
157    Ok(data)
158}
159
160pub async fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
161    let name = path.as_ref().display();
162    debug!("rm -r {name:?}");
163    tokio::fs::remove_dir_all(path.as_ref()).await.with_context(|| format!("removing {name}"))
164}
165
166pub async fn remove_file(path: impl AsRef<Path>) -> Result<()> {
167    let name = path.as_ref().display();
168    debug!("rm {name:?}");
169    tokio::fs::remove_file(path.as_ref()).await.with_context(|| format!("removing {name}"))
170}
171
172pub async fn rename(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
173    let src = from.as_ref().display();
174    let dst = to.as_ref().display();
175    create_parent(to.as_ref()).await?;
176    debug!("mv {src:?} {dst:?}");
177    tokio::fs::rename(from.as_ref(), to.as_ref())
178        .await
179        .with_context(|| format!("renaming {src} to {dst}"))
180}
181
182pub fn targz_list(targz: &[u8]) -> Result<Vec<PathBuf>> {
183    let tar = flate2::bufread::GzDecoder::new(targz);
184    tar::Archive::new(tar).entries()?.map(|x| Ok(x?.path()?.to_path_buf())).collect()
185}
186
187pub async fn targz_extract(
188    targz: impl AsRef<[u8]> + Send + 'static, dir: impl AsRef<Path> + Send + 'static,
189) -> Result<()> {
190    tokio::task::spawn_blocking(move || {
191        let name = dir.as_ref().display();
192        debug!("tar xz > {name:?}");
193        let tar = flate2::bufread::GzDecoder::new(targz.as_ref());
194        tar::Archive::new(tar).unpack(dir.as_ref()).with_context(|| format!("extracting {name}"))
195    })
196    .await?
197}
198
199pub async fn touch(path: impl AsRef<Path>) -> Result<()> {
200    if exists(path.as_ref()).await {
201        return Ok(());
202    }
203    write(path, "").await
204}
205
206pub struct WriteParams<P: AsRef<Path>> {
207    path: P,
208    options: OpenOptions,
209}
210
211impl<P: AsRef<Path>> WriteParams<P> {
212    pub fn new(path: P) -> Self {
213        WriteParams { path, options: OpenOptions::new() }
214    }
215    pub fn options(&mut self) -> &mut OpenOptions {
216        &mut self.options
217    }
218}
219
220pub trait WriteFile {
221    fn path(&self) -> &Path;
222    fn open(self) -> impl Future<Output = Result<File>>;
223}
224
225impl<P: AsRef<Path>> WriteFile for WriteParams<P> {
226    fn path(&self) -> &Path {
227        self.path.as_ref()
228    }
229    async fn open(self) -> Result<File> {
230        (&self).open().await
231    }
232}
233
234impl<P: AsRef<Path>> WriteFile for &WriteParams<P> {
235    fn path(&self) -> &Path {
236        (*self).path()
237    }
238    async fn open(self) -> Result<File> {
239        Ok(self.options.open(&self.path).await?)
240    }
241}
242
243impl<P: AsRef<Path>> WriteFile for P {
244    fn path(&self) -> &Path {
245        self.as_ref()
246    }
247    async fn open(self) -> Result<File> {
248        let mut params = WriteParams::new(self);
249        params.options().write(true).create(true).truncate(true);
250        params.open().await
251    }
252}
253
254pub async fn write(file: impl WriteFile, contents: impl AsRef<[u8]>) -> Result<()> {
255    let name = format!("{}", file.path().display());
256    let contents = contents.as_ref();
257    create_parent(file.path()).await?;
258    debug!("write > {name:?}");
259    let mut file = file.open().await.with_context(|| format!("creating {name}"))?;
260    file.write_all(contents).await.with_context(|| format!("writing {name}"))?;
261    file.flush().await.with_context(|| format!("flushing {name}"))?;
262    Ok(())
263}
264
265pub async fn write_toml<T: Serialize>(path: impl AsRef<Path>, contents: &T) -> Result<()> {
266    let name = path.as_ref().display();
267    let contents = toml::to_string(contents).with_context(|| format!("displaying {name}"))?;
268    write(path.as_ref(), contents).await?;
269    Ok(())
270}
271
272pub async fn write_stdout(contents: impl AsRef<[u8]>) -> Result<()> {
273    let contents = contents.as_ref();
274    tokio::io::stdout().write_all(contents).await.context("writing to stdout")?;
275    Ok(())
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[tokio::test]
283    async fn try_relative_ok() {
284        #[track_caller]
285        async fn test(base: &str, path: &str, res: &str) {
286            assert_eq!(try_relative(base, path).await.ok(), Some(PathBuf::from(res)));
287        }
288        test("/foo/bar", "/foo", "..").await;
289        test("/foo/bar", "/foo/baz/qux", "../baz/qux").await;
290        test("/foo/bar", "/foo/bar/qux", "qux").await;
291    }
292}