use std::{
fs::{copy, create_dir_all, metadata, read, set_permissions, write, File},
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use clingwrap::runner::CommandError;
use reqwest::{blocking::Client, header::IF_MODIFIED_SINCE, StatusCode};
use time::{macros::format_description, OffsetDateTime};
pub fn create_file(filename: &Path) -> Result<PathBuf, UtilError> {
File::create(filename).map_err(|e| UtilError::CreateFile(filename.into(), e))?;
Ok(filename.into())
}
pub fn mkdir(dirname: &Path) -> Result<(), UtilError> {
create_dir_all(dirname).map_err(|e| UtilError::CreateDir(dirname.into(), e))?;
Ok(())
}
pub fn mkdir_child(parent: &Path, subdir: &str) -> Result<PathBuf, UtilError> {
let pathname = parent.join(subdir);
create_dir_all(&pathname).map_err(|e| UtilError::CreateDir(pathname.clone(), e))?;
Ok(pathname)
}
pub fn recreate_dir(dirname: &Path) -> Result<(), UtilError> {
if dirname.exists() {
std::fs::remove_dir_all(dirname).map_err(|e| UtilError::RemoveDir(dirname.into(), e))?;
}
mkdir(dirname)?;
Ok(())
}
pub fn cat_text_file(filename: &Path) -> Result<String, UtilError> {
let data = read(filename).map_err(|err| UtilError::Read(filename.into(), err))?;
let text = String::from_utf8(data).map_err(|err| UtilError::Utf8(filename.into(), err))?;
Ok(text)
}
pub fn write_file(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
write(filename, data).map_err(|err| UtilError::WriteFile(filename.into(), err))
}
pub fn copy_file(src: &Path, dst: &Path) -> Result<(), UtilError> {
copy(src, dst).map_err(|err| UtilError::Copy(src.into(), dst.into(), err))?;
Ok(())
}
pub fn copy_file_rw(src: &Path, dst: &Path) -> Result<(), UtilError> {
copy_file(src, dst)?;
let mut perms = std::fs::metadata(dst)
.map_err(|err| UtilError::GetMetadata(dst.into(), err))?
.permissions();
perms.set_mode(0o644);
std::fs::set_permissions(dst, perms)
.map_err(|err| UtilError::SetPermissions(dst.into(), err))?;
Ok(())
}
pub fn write_executable(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
const EXECUTABLE: u32 = 0o755;
write(filename, data).map_err(|err| UtilError::WriteFile(filename.into(), err))?;
let meta = metadata(filename).map_err(|err| UtilError::GetMetadata(filename.into(), err))?;
let mut perm = meta.permissions();
perm.set_mode(EXECUTABLE);
set_permissions(filename, perm).map_err(|err| UtilError::MakeExec(filename.into(), err))?;
Ok(())
}
pub fn now() -> Result<String, UtilError> {
let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
OffsetDateTime::now_utc()
.format(fmt)
.map_err(UtilError::TimeFormat)
}
pub fn format_timestamp(time: SystemTime) -> Result<String, UtilError> {
let fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second]Z");
OffsetDateTime::from(time)
.format(fmt)
.map_err(UtilError::TimeFormat)
}
pub fn http_get_to_file(url: &str, filename: &Path) -> Result<Vec<u8>, UtilError> {
let timestamp = if let Ok(meta) = filename.metadata() {
meta.modified().unwrap_or(UNIX_EPOCH)
} else {
UNIX_EPOCH
};
let fmt = format_description!(
"[weekday repr:short], [day padding:zero] [month repr:short] [year] [hour]:[minute]:[second] GMT"
);
let ts = OffsetDateTime::from(timestamp)
.format(fmt)
.map_err(UtilError::TimeFormat)?;
let client = Client::builder().build().map_err(UtilError::ClientBuild)?;
let req = client
.get(url)
.header(IF_MODIFIED_SINCE, ts)
.build()
.map_err(UtilError::Client)?;
let resp = client
.execute(req)
.map_err(|err| UtilError::Get(url.into(), err))?;
match resp.status() {
StatusCode::NOT_MODIFIED => {
let data = std::fs::read(filename)
.map_err(|err| UtilError::Read(filename.to_path_buf(), err))?;
Ok(data)
}
StatusCode::OK => {
let body = resp
.bytes()
.map_err(|err| UtilError::GetBody(url.into(), err))?;
write_file(filename, &body)?;
Ok(body.to_vec())
}
x => Err(UtilError::UnwantedStatus(x)),
}
}
#[derive(Debug, thiserror::Error)]
pub enum UtilError {
#[error("failed to create directory {0}")]
CreateDir(PathBuf, #[source] std::io::Error),
#[error("failed to remove directory {0}")]
RemoveDir(PathBuf, #[source] std::io::Error),
#[error("failed to write file {0}")]
WriteFile(PathBuf, #[source] std::io::Error),
#[error("failed to get metadata for file: {0}")]
GetMetadata(PathBuf, #[source] std::io::Error),
#[error("failed to make a file executable: {0}")]
MakeExec(PathBuf, #[source] std::io::Error),
#[error("failed to copy file {0} to {1}")]
Copy(PathBuf, PathBuf, #[source] std::io::Error),
#[error("failed to set permissions for file {0}")]
SetPermissions(PathBuf, #[source] std::io::Error),
#[error("failed to read file {0}")]
Read(PathBuf, #[source] std::io::Error),
#[error("failed to understand file {0} into UTF8")]
Utf8(PathBuf, #[source] std::string::FromUtf8Error),
#[error("failed to format time stamp")]
TimeFormat(#[source] time::error::Format),
#[error("failed to create file {0}")]
CreateFile(PathBuf, #[source] std::io::Error),
#[error("failed to run program {0}")]
Execute(&'static str, #[source] CommandError),
#[error("failed to create HTTP client")]
ClientBuild(#[source] reqwest::Error),
#[error("failed to build a reqwest client")]
Client(#[source] reqwest::Error),
#[error("failed to build a reqwest request")]
BuildRequest(#[source] reqwest::Error),
#[error("failed to GET URL {0:?}")]
Get(String, reqwest::Error),
#[error("failed to get body of response from {0:?}")]
GetBody(String, reqwest::Error),
#[error("failure getting file with HTTP GET: status code {0}")]
UnwantedStatus(StatusCode),
}