Skip to main content

ambient_ci/
util.rs

1//! Utility functions.
2
3use 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
14/// Create an empty file.
15pub 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
20/// Create a directory.
21pub fn mkdir(dirname: &Path) -> Result<(), UtilError> {
22    create_dir_all(dirname).map_err(|e| UtilError::CreateDir(dirname.into(), e))?;
23    Ok(())
24}
25
26/// Create a subdirectory.
27pub 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
33/// Make sure a directory exists and is empty.
34pub 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
42/// Read a text file.
43pub 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
49/// Write a file.
50pub fn write_file(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
51    write(filename, data).map_err(|err| UtilError::WriteFile(filename.into(), err))
52}
53
54/// Copy a file.
55pub 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
60/// Copy a file, make sure user has read and write permission to the copy.
61pub 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
72/// Write a file, make it executable.
73pub fn write_executable(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
74    // Unix mode bits for an executable file: read/write/exec for
75    // owner, read/exec for group and others
76    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
86/// Current time as a string.
87pub 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
94/// Format a time stamp.
95pub 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
102/// Download a file over HTTP if local file is missing or out of date.
103pub 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/// Errors from utility functions.
146#[derive(Debug, thiserror::Error)]
147pub enum UtilError {
148    /// Can't create directory.
149    #[error("failed to create directory {0}")]
150    CreateDir(PathBuf, #[source] std::io::Error),
151
152    /// Can't remove directory.
153    #[error("failed to remove directory {0}")]
154    RemoveDir(PathBuf, #[source] std::io::Error),
155
156    /// Can't write file.
157    #[error("failed to write file {0}")]
158    WriteFile(PathBuf, #[source] std::io::Error),
159
160    /// Can't get file metadata.
161    #[error("failed to get metadata for file: {0}")]
162    GetMetadata(PathBuf, #[source] std::io::Error),
163
164    /// Can't make executable.
165    #[error("failed to make a file executable: {0}")]
166    MakeExec(PathBuf, #[source] std::io::Error),
167
168    /// Can't copy file.
169    #[error("failed to copy file {0} to {1}")]
170    Copy(PathBuf, PathBuf, #[source] std::io::Error),
171
172    /// Can't set file permissions.
173    #[error("failed to set permissions for file {0}")]
174    SetPermissions(PathBuf, #[source] std::io::Error),
175
176    /// Can't read file.
177    #[error("failed to read file {0}")]
178    Read(PathBuf, #[source] std::io::Error),
179
180    /// Can't read file as UTF-8.
181    #[error("failed to understand file {0} into UTF8")]
182    Utf8(PathBuf, #[source] std::string::FromUtf8Error),
183
184    /// Can't format time as string.
185    #[error("failed to format time stamp")]
186    TimeFormat(#[source] time::error::Format),
187
188    /// Can't create file.
189    #[error("failed to create file {0}")]
190    CreateFile(PathBuf, #[source] std::io::Error),
191
192    /// Can't run program.
193    #[error("failed to run program {0}")]
194    Execute(&'static str, #[source] CommandError),
195
196    /// Can't build an HTTP client.
197    #[error("failed to create HTTP client")]
198    ClientBuild(#[source] reqwest::Error),
199
200    /// Can't create an HTTP client.
201    #[error("failed to build a reqwest client")]
202    Client(#[source] reqwest::Error),
203
204    /// Can't build an HTTP request.
205    #[error("failed to build a reqwest request")]
206    BuildRequest(#[source] reqwest::Error),
207
208    /// Can't get file with GET.
209    #[error("failed to GET URL {0:?}")]
210    Get(String, reqwest::Error),
211
212    /// Can't get GET response body.
213    #[error("failed to get body of response from {0:?}")]
214    GetBody(String, reqwest::Error),
215
216    /// HTTP GET returned weird status code.
217    #[error("failure getting file with HTTP GET: status code {0}")]
218    UnwantedStatus(StatusCode),
219}