1use 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 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 while matches!((base.peek(), path.peek()), (Some(x), Some(y)) if x == y) {
50 base.next();
51 path.next();
52 }
53 let mut result = PathBuf::new();
55 while base.next().is_some() {
56 result.push(Component::ParentDir);
57 }
58 result.extend(path);
60 Ok(result)
61}
62
63pub 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}