use std::collections::HashSet;
use std::fs;
use std::path::{Path, PathBuf};
use super::format::InputFileType;
use crate::core::data::collect_files;
pub(crate) const PROCESSED_FILE_NAME: &str = ".processed";
pub(crate) struct PendingFile {
pub relative_path: String,
pub absolute_path: PathBuf,
}
pub(crate) fn load_checkpoint(staging_dir: &Path) -> Result<HashSet<String>, String> {
let path = staging_dir.join(PROCESSED_FILE_NAME);
let contents = match fs::read_to_string(&path) {
Ok(contents) => contents,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(HashSet::new()),
Err(err) => return Err(format!("failed to read {}: {err}", path.display())),
};
Ok(contents.lines().map(|line| line.to_string()).collect())
}
pub(crate) fn append_checkpoint(staging_dir: &Path, relative_path: &str) -> Result<(), String> {
use std::io::Write;
fs::create_dir_all(staging_dir)
.map_err(|err| format!("failed to create {}: {err}", staging_dir.display()))?;
let path = staging_dir.join(PROCESSED_FILE_NAME);
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|err| format!("failed to open {}: {err}", path.display()))?;
writeln!(file, "{relative_path}")
.map_err(|err| format!("failed to write {}: {err}", path.display()))
}
fn relative_path_string(base: &Path, path: &Path) -> Option<String> {
let relative = path.strip_prefix(base).ok()?;
let components: Vec<&str> = relative
.components()
.filter_map(|c| c.as_os_str().to_str())
.collect();
if components.is_empty() {
return None;
}
Some(components.join("/"))
}
pub(crate) fn gather_pending(
source_dir: &Path,
staging_dir: &Path,
input_file_type: InputFileType,
) -> Result<Vec<PendingFile>, String> {
let done = load_checkpoint(staging_dir)?;
let expected_extension = input_file_type.extension();
let mut pending = Vec::new();
for absolute_path in collect_files(source_dir)? {
let Some(relative_path) = relative_path_string(source_dir, &absolute_path) else {
continue;
};
let actual_extension = absolute_path
.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.to_ascii_lowercase());
if actual_extension.as_deref() != Some(expected_extension) {
tracing::warn!(
path = %absolute_path.display(),
expected = expected_extension,
"transform: found an unexpected extension under source/, skipping"
);
continue;
}
if done.contains(&relative_path) {
continue;
}
pending.push(PendingFile {
relative_path,
absolute_path,
});
}
Ok(pending)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_checkpoint_is_empty_for_a_missing_staging_dir() {
let dir = tempfile::tempdir().unwrap();
let staging_dir = dir.path().join("does-not-exist-yet");
assert!(load_checkpoint(&staging_dir).unwrap().is_empty());
}
#[test]
fn append_checkpoint_then_load_round_trips() {
let dir = tempfile::tempdir().unwrap();
let staging_dir = dir.path().join(".staging");
append_checkpoint(&staging_dir, "a.png").unwrap();
append_checkpoint(&staging_dir, "sub/b.png").unwrap();
let loaded = load_checkpoint(&staging_dir).unwrap();
assert!(loaded.contains("a.png"));
assert!(loaded.contains("sub/b.png"));
assert_eq!(loaded.len(), 2);
}
#[test]
fn gather_pending_excludes_checkpointed_files() {
let dir = tempfile::tempdir().unwrap();
let source_dir = dir.path().join("source");
let staging_dir = dir.path().join(".staging");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("a.png"), b"a").unwrap();
fs::write(source_dir.join("b.png"), b"b").unwrap();
append_checkpoint(&staging_dir, "a.png").unwrap();
let pending = gather_pending(&source_dir, &staging_dir, InputFileType::Png).unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].relative_path, "b.png");
}
#[test]
fn gather_pending_filters_by_extension() {
let dir = tempfile::tempdir().unwrap();
let source_dir = dir.path().join("source");
let staging_dir = dir.path().join(".staging");
fs::create_dir_all(&source_dir).unwrap();
fs::write(source_dir.join("a.png"), b"a").unwrap();
fs::write(source_dir.join("stray.txt"), b"not a png").unwrap();
let pending = gather_pending(&source_dir, &staging_dir, InputFileType::Png).unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].relative_path, "a.png");
}
#[test]
fn gather_pending_includes_a_nested_relative_path() {
let dir = tempfile::tempdir().unwrap();
let source_dir = dir.path().join("source");
let staging_dir = dir.path().join(".staging");
fs::create_dir_all(source_dir.join("screenshots")).unwrap();
fs::write(source_dir.join("screenshots/img.heic"), b"x").unwrap();
let pending = gather_pending(&source_dir, &staging_dir, InputFileType::Heic).unwrap();
assert_eq!(pending.len(), 1);
assert_eq!(pending[0].relative_path, "screenshots/img.heic");
}
}