leyline-http 0.1.1

HTTP client that mimics browsers on the wire
Documentation
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Path, PathBuf};

use crate::core::{Error, Kind, Result};

pub(crate) fn write_atomic(path: &Path, bytes: &[u8], mode: FileMode) -> Result<()> {
    let temp = temp_path(path)?;
    let written = write_synced(&temp, path, bytes, mode);
    if let Err(err) = written {
        drop(std::fs::remove_file(&temp));
        return Err(err);
    }
    commit(&temp, path)
}

pub(crate) fn commit(temp: &Path, path: &Path) -> Result<()> {
    if let Err(err) = std::fs::rename(temp, path) {
        drop(std::fs::remove_file(temp));
        return Err(err.into());
    }
    sync_parent(path)
}

fn write_synced(temp: &Path, target: &Path, bytes: &[u8], mode: FileMode) -> Result<()> {
    let mut file = create_temp(temp, target, mode)?;
    file.write_all(bytes)?;
    file.flush()?;
    file.sync_all()?;
    Ok(())
}

pub(crate) fn temp_path(path: &Path) -> Result<PathBuf> {
    let Some(name) = path.file_name() else {
        return Err(Error::new(Kind::Request).with_message("file path has no file name"));
    };
    let mut temp_name = std::ffi::OsString::from(".");
    temp_name.push(name);
    temp_name.push(format!(".{:016x}.part", rand::random::<u64>()));
    Ok(path.with_file_name(temp_name))
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum FileMode {
    Private,
    KeepExisting,
}

#[cfg(unix)]
pub(crate) fn create_temp(temp: &Path, target: &Path, mode: FileMode) -> Result<std::fs::File> {
    use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
    let mut options = OpenOptions::new();
    options.write(true).create_new(true);
    let perms = match mode {
        FileMode::Private => Some(std::fs::Permissions::from_mode(0o600)),
        FileMode::KeepExisting => std::fs::metadata(target)
            .ok()
            .map(|meta| std::fs::Permissions::from_mode(meta.permissions().mode() & 0o777)),
    };
    if let Some(perms) = &perms {
        options.mode(perms.mode());
    }
    let file = options.open(temp)?;
    if let Some(perms) = perms {
        file.set_permissions(perms)?;
    }
    Ok(file)
}

#[cfg(not(unix))]
pub(crate) fn create_temp(temp: &Path, _target: &Path, _mode: FileMode) -> Result<std::fs::File> {
    let mut options = OpenOptions::new();
    options.write(true).create_new(true);
    Ok(options.open(temp)?)
}

#[cfg(unix)]
fn sync_parent(path: &Path) -> Result<()> {
    let parent = path
        .parent()
        .filter(|dir| !dir.as_os_str().is_empty())
        .unwrap_or_else(|| Path::new("."));
    std::fs::File::open(parent)?.sync_all()?;
    Ok(())
}

#[cfg(not(unix))]
fn sync_parent(_path: &Path) -> Result<()> {
    Ok(())
}