ambient-ci 0.14.0

A continuous integration engine
Documentation
//! Utility functions.

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};

/// Create an empty file.
pub fn create_file(filename: &Path) -> Result<PathBuf, UtilError> {
    File::create(filename).map_err(|e| UtilError::CreateFile(filename.into(), e))?;
    Ok(filename.into())
}

/// Create a directory.
pub fn mkdir(dirname: &Path) -> Result<(), UtilError> {
    create_dir_all(dirname).map_err(|e| UtilError::CreateDir(dirname.into(), e))?;
    Ok(())
}

/// Create a subdirectory.
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)
}

/// Make sure a directory exists and is empty.
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(())
}

/// Read a text file.
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)
}

/// Write a file.
pub fn write_file(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
    write(filename, data).map_err(|err| UtilError::WriteFile(filename.into(), err))
}

/// Copy a file.
pub fn copy_file(src: &Path, dst: &Path) -> Result<(), UtilError> {
    copy(src, dst).map_err(|err| UtilError::Copy(src.into(), dst.into(), err))?;
    Ok(())
}

/// Copy a file, make sure user has read and write permission to the copy.
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(())
}

/// Write a file, make it executable.
pub fn write_executable(filename: &Path, data: &[u8]) -> Result<(), UtilError> {
    // Unix mode bits for an executable file: read/write/exec for
    // owner, read/exec for group and others
    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(())
}

/// Current time as a string.
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)
}

/// Format a time stamp.
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)
}

/// Download a file over HTTP if local file is missing or out of date.
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)),
    }
}

/// Errors from utility functions.
#[derive(Debug, thiserror::Error)]
pub enum UtilError {
    /// Can't create directory.
    #[error("failed to create directory {0}")]
    CreateDir(PathBuf, #[source] std::io::Error),

    /// Can't remove directory.
    #[error("failed to remove directory {0}")]
    RemoveDir(PathBuf, #[source] std::io::Error),

    /// Can't write file.
    #[error("failed to write file {0}")]
    WriteFile(PathBuf, #[source] std::io::Error),

    /// Can't get file metadata.
    #[error("failed to get metadata for file: {0}")]
    GetMetadata(PathBuf, #[source] std::io::Error),

    /// Can't make executable.
    #[error("failed to make a file executable: {0}")]
    MakeExec(PathBuf, #[source] std::io::Error),

    /// Can't copy file.
    #[error("failed to copy file {0} to {1}")]
    Copy(PathBuf, PathBuf, #[source] std::io::Error),

    /// Can't set file permissions.
    #[error("failed to set permissions for file {0}")]
    SetPermissions(PathBuf, #[source] std::io::Error),

    /// Can't read file.
    #[error("failed to read file {0}")]
    Read(PathBuf, #[source] std::io::Error),

    /// Can't read file as UTF-8.
    #[error("failed to understand file {0} into UTF8")]
    Utf8(PathBuf, #[source] std::string::FromUtf8Error),

    /// Can't format time as string.
    #[error("failed to format time stamp")]
    TimeFormat(#[source] time::error::Format),

    /// Can't create file.
    #[error("failed to create file {0}")]
    CreateFile(PathBuf, #[source] std::io::Error),

    /// Can't run program.
    #[error("failed to run program {0}")]
    Execute(&'static str, #[source] CommandError),

    /// Can't build an HTTP client.
    #[error("failed to create HTTP client")]
    ClientBuild(#[source] reqwest::Error),

    /// Can't create an HTTP client.
    #[error("failed to build a reqwest client")]
    Client(#[source] reqwest::Error),

    /// Can't build an HTTP request.
    #[error("failed to build a reqwest request")]
    BuildRequest(#[source] reqwest::Error),

    /// Can't get file with GET.
    #[error("failed to GET URL {0:?}")]
    Get(String, reqwest::Error),

    /// Can't get GET response body.
    #[error("failed to get body of response from {0:?}")]
    GetBody(String, reqwest::Error),

    /// HTTP GET returned weird status code.
    #[error("failure getting file with HTTP GET: status code {0}")]
    UnwantedStatus(StatusCode),
}