1use std::{
4 fs::{copy, create_dir_all, metadata, read, set_permissions, write, File},
5 os::unix::fs::PermissionsExt,
6 path::{Path, PathBuf},
7 time::{SystemTime, UNIX_EPOCH},
8};
9
10use clingwrap::runner::CommandError;
11use reqwest::{blocking::Client, header::IF_MODIFIED_SINCE, StatusCode};
12use time::{macros::format_description, OffsetDateTime};
13
14pub fn create_file(filename: &Path) -> Result<PathBuf, UtilError> {
16 File::create(filename).map_err(|e| UtilError::CreateFile(filename.into(), e))?;
17 Ok(filename.into())
18}
19
20pub fn mkdir(dirname: &Path) -> Result<(), UtilError> {
22 create_dir_all(dirname).map_err(|e| UtilError::CreateDir(dirname.into(), e))?;
23 Ok(())
24}
25
26pub fn mkdir_child(parent: &Path, subdir: &str) -> Result<PathBuf, UtilError> {
28 let pathname = parent.join(subdir);
29 create_dir_all(&pathname).map_err(|e| UtilError::CreateDir(pathname.clone(), e))?;
30 Ok(pathname)
31}
32
33pub fn recreate_dir(dirname: &Path) -> Result<(), UtilError> {
35 if dirname.exists() {
36 std::fs::remove_dir_all(dirname).map_err(|e| UtilError::RemoveDir(dirname.into(), e))?;
37 }
38 mkdir(dirname)?;
39 Ok(())
40}
41
42pub fn cat_text_file(filename: &Path) -> Result<String, UtilError> {
44 let data = read(filename).map_err(|err| UtilError::Read(filename.into(), err))?;
45 let text = String::from_utf8(data).map_err(|err| UtilError::Utf8(filename.into(), err))?;
46 Ok(text)
47}
48
49pub fn write_file(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
51 write(filename, data).map_err(|err| UtilError::WriteFile(filename.into(), err))
52}
53
54pub fn copy_file(src: &Path, dst: &Path) -> Result<(), UtilError> {
56 copy(src, dst).map_err(|err| UtilError::Copy(src.into(), dst.into(), err))?;
57 Ok(())
58}
59
60pub fn copy_file_rw(src: &Path, dst: &Path) -> Result<(), UtilError> {
62 copy_file(src, dst)?;
63 let mut perms = std::fs::metadata(dst)
64 .map_err(|err| UtilError::GetMetadata(dst.into(), err))?
65 .permissions();
66 perms.set_mode(0o644);
67 std::fs::set_permissions(dst, perms)
68 .map_err(|err| UtilError::SetPermissions(dst.into(), err))?;
69 Ok(())
70}
71
72pub fn write_executable(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
74 const EXECUTABLE: u32 = 0o755;
77
78 write(filename, data).map_err(|err| UtilError::WriteFile(filename.into(), err))?;
79 let meta = metadata(filename).map_err(|err| UtilError::GetMetadata(filename.into(), err))?;
80 let mut perm = meta.permissions();
81 perm.set_mode(EXECUTABLE);
82 set_permissions(filename, perm).map_err(|err| UtilError::MakeExec(filename.into(), err))?;
83 Ok(())
84}
85
86pub fn now() -> Result<String, UtilError> {
88 let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
89 OffsetDateTime::now_utc()
90 .format(fmt)
91 .map_err(UtilError::TimeFormat)
92}
93
94pub fn format_timestamp(time: SystemTime) -> Result<String, UtilError> {
96 let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
97 OffsetDateTime::from(time)
98 .format(fmt)
99 .map_err(UtilError::TimeFormat)
100}
101
102pub fn http_get_to_file(url: &str, filename: &Path) -> Result<Vec<u8>, UtilError> {
104 let timestamp = if let Ok(meta) = filename.metadata() {
105 meta.modified().unwrap_or(UNIX_EPOCH)
106 } else {
107 UNIX_EPOCH
108 };
109
110 let fmt = format_description!(
111 "[weekday repr:short], [day padding:zero] [month repr:short] [year] [hour]:[minute]:[second] GMT"
112 );
113 let ts = OffsetDateTime::from(timestamp)
114 .format(fmt)
115 .map_err(UtilError::TimeFormat)?;
116
117 let client = Client::builder().build().map_err(UtilError::ClientBuild)?;
118 let req = client
119 .get(url)
120 .header(IF_MODIFIED_SINCE, ts)
121 .build()
122 .map_err(UtilError::Client)?;
123
124 let resp = client
125 .execute(req)
126 .map_err(|err| UtilError::Get(url.into(), err))?;
127
128 match resp.status() {
129 StatusCode::NOT_MODIFIED => {
130 let data = std::fs::read(filename)
131 .map_err(|err| UtilError::Read(filename.to_path_buf(), err))?;
132 Ok(data)
133 }
134 StatusCode::OK => {
135 let body = resp
136 .bytes()
137 .map_err(|err| UtilError::GetBody(url.into(), err))?;
138 write_file(filename, &body)?;
139 Ok(body.to_vec())
140 }
141 x => Err(UtilError::UnwantedStatus(x)),
142 }
143}
144
145#[derive(Debug, thiserror::Error)]
147pub enum UtilError {
148 #[error("failed to create directory {0}")]
150 CreateDir(PathBuf, #[source] std::io::Error),
151
152 #[error("failed to remove directory {0}")]
154 RemoveDir(PathBuf, #[source] std::io::Error),
155
156 #[error("failed to write file {0}")]
158 WriteFile(PathBuf, #[source] std::io::Error),
159
160 #[error("failed to get metadata for file: {0}")]
162 GetMetadata(PathBuf, #[source] std::io::Error),
163
164 #[error("failed to make a file executable: {0}")]
166 MakeExec(PathBuf, #[source] std::io::Error),
167
168 #[error("failed to copy file {0} to {1}")]
170 Copy(PathBuf, PathBuf, #[source] std::io::Error),
171
172 #[error("failed to set permissions for file {0}")]
174 SetPermissions(PathBuf, #[source] std::io::Error),
175
176 #[error("failed to read file {0}")]
178 Read(PathBuf, #[source] std::io::Error),
179
180 #[error("failed to understand file {0} into UTF8")]
182 Utf8(PathBuf, #[source] std::string::FromUtf8Error),
183
184 #[error("failed to format time stamp")]
186 TimeFormat(#[source] time::error::Format),
187
188 #[error("failed to create file {0}")]
190 CreateFile(PathBuf, #[source] std::io::Error),
191
192 #[error("failed to run program {0}")]
194 Execute(&'static str, #[source] CommandError),
195
196 #[error("failed to create HTTP client")]
198 ClientBuild(#[source] reqwest::Error),
199
200 #[error("failed to build a reqwest client")]
202 Client(#[source] reqwest::Error),
203
204 #[error("failed to build a reqwest request")]
206 BuildRequest(#[source] reqwest::Error),
207
208 #[error("failed to GET URL {0:?}")]
210 Get(String, reqwest::Error),
211
212 #[error("failed to get body of response from {0:?}")]
214 GetBody(String, reqwest::Error),
215
216 #[error("failure getting file with HTTP GET: status code {0}")]
218 UnwantedStatus(StatusCode),
219}