rssg 1.7.3

A rusty static-site generator leveraging shell commands.
use std::collections::HashMap;
use std::fs::{read_dir, read_to_string, remove_file, File, OpenOptions};
use std::io::Write;
use std::{io, path::Path, str::FromStr};

use log::warn;
use sha2::{Digest, Sha256};

use crate::filepath::FilePath;

// Caching is lower-priority, don't stop anything if it fails

fn visit_dirs(dir: &Path) -> io::Result<Vec<FilePath>> {
    let mut files = Vec::new();
    if dir.is_dir() {
        for entry in read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                files.append(&mut visit_dirs(&path)?);
            } else {
                files.push(FilePath::from_str(&entry.path().to_string_lossy()).unwrap());
            }
        }
    }

    Ok(files)
}

pub fn hash_file(file: &Path) -> Option<(FilePath, u128)> {
    let mut hasher = Sha256::new();
    let mut f = File::open(file).ok()?;

    _ = io::copy(&mut f, &mut hasher).ok()?;

    let hash = hasher.finalize();
    let mut data: u128 = 0;
    for (i, byte) in hash.iter().enumerate() {
        data |= (*byte as u128).wrapping_shr(i as u32 * 8);
    }

    let file = match FilePath::from_str(&file.display().to_string()) {
        Ok(f) => f,
        Err(_) => return None,
    };

    Some((file, data))
}

pub fn cache_file(file: &Path, cache: &mut HashMap<FilePath, u128>) {
    let (file, data) = match hash_file(file) {
        Some(d) => d,
        None => {
            warn!("Failed to cache file `{}`", file.display());
            return;
        }
    };

    cache.insert(file, data);
}

pub fn write_cache(path: &Path, cache: HashMap<FilePath, u128>) {
    let mut cache_data = String::new();
    for (file, data) in cache {
        cache_data.push_str(format!("{file}  {data}\n").as_str());
    }

    match OpenOptions::new()
        .write(true)
        .truncate(true)
        .create(true)
        .open(path)
    {
        Ok(mut file) => {
            if let Err(e) = file.write_all(cache_data.as_bytes()) {
                warn!("Failed to write cache file at {}: {}", path.display(), e);
                if let Err(e) = remove_file(path) {
                    warn!("Failed to remove `.rssg-cache` for safety, please remove it manually then fix this: {}", e);
                }
            }
        }

        Err(e) => {
            warn!("Failed to open cache file at {}: {}", path.display(), e);
            if let Err(e) = remove_file(path) {
                warn!("Failed to remove `.rssg-cache` for safety, please remove it manually then fix this: {}", e);
            }
        }
    }
}

pub fn read_cache(path: &Path) -> HashMap<FilePath, u128> {
    let mut cache = HashMap::new();
    let cache_data = if let Ok(d) = read_to_string(path) {
        d
    } else {
        return cache;
    };

    let cache_data = cache_data.split_terminator('\n').filter(|s| !s.is_empty());

    for line in cache_data {
        let mut line = line.split_terminator("  ");

        let file = if let Some(Ok(f)) = line.next().map(FilePath::from_str) {
            f
        } else {
            warn!("Invalid entry in .rssg-cache");
            continue;
        };

        let data = if let Some(Ok(d)) = line.next().map(|s| s.parse::<u128>()) {
            d
        } else {
            warn!("Invalid hash in .rssg-cache");
            continue;
        };

        cache.insert(file, data);
    }

    cache
}

pub fn modified(
    cache: &HashMap<FilePath, u128>,
    content: &String,
    public: &String,
    templates: &String,
) -> Option<Vec<FilePath>> {
    let mut files = visit_dirs(Path::new(content)).unwrap();

    files.append(&mut visit_dirs(Path::new(public)).unwrap());

    files.append(&mut visit_dirs(Path::new(templates)).unwrap());

    let mut modified = Vec::with_capacity(files.len() / 2 + 1);
    for path in files {
        if let Some((file, data)) = hash_file(Path::new(&path.full())) {
            if cache.get(&file) != Some(&data) {
                modified.push(path);
            }
        }
    }

    Some(modified)
}