Skip to content

Commit 0ba0c5b

Browse files
committed
feat(core): add output module
1 parent 5f10c3d commit 0ba0c5b

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

src/core/output.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use anyhow::Result;
2+
use serde_json::Value;
3+
use std::path::{Path, PathBuf};
4+
use std::time::{SystemTime, UNIX_EPOCH};
5+
use tokio::fs;
6+
use tokio::io::AsyncWriteExt;
7+
8+
#[derive(Debug)]
9+
pub(crate) struct PreparedOutput {
10+
pub(crate) output_dir: PathBuf,
11+
pub(crate) figures_dir: PathBuf,
12+
pub(crate) markdown_path: PathBuf,
13+
pub(crate) log_path: PathBuf,
14+
}
15+
16+
pub(crate) fn prepare_output_paths(
17+
output_root: &Path,
18+
pdf_path: &Path,
19+
overwrite: bool,
20+
) -> Result<PreparedOutput> {
21+
let stem = pdf_path
22+
.file_stem()
23+
.and_then(|s| s.to_str())
24+
.ok_or_else(|| anyhow::anyhow!("Invalid PDF filename: {}", pdf_path.display()))?;
25+
26+
let output_dir = output_root.join(stem);
27+
std::fs::create_dir_all(&output_dir)?;
28+
29+
let markdown_path = output_dir.join("index.md");
30+
let figures_dir = output_dir.join("figures");
31+
let log_path = output_dir.join("log.jsonl");
32+
33+
if !overwrite {
34+
if markdown_path.exists() {
35+
return Err(anyhow::anyhow!(
36+
"Output already exists: {}. Re-run with --overwrite",
37+
markdown_path.display()
38+
));
39+
}
40+
if figures_dir.exists() {
41+
return Err(anyhow::anyhow!(
42+
"Output already exists: {}. Re-run with --overwrite",
43+
figures_dir.display()
44+
));
45+
}
46+
} else {
47+
if markdown_path.exists() {
48+
std::fs::remove_file(&markdown_path)?;
49+
}
50+
if figures_dir.exists() {
51+
if figures_dir.is_dir() {
52+
std::fs::remove_dir_all(&figures_dir)?;
53+
} else {
54+
std::fs::remove_file(&figures_dir)?;
55+
}
56+
}
57+
}
58+
59+
std::fs::create_dir_all(&figures_dir)?;
60+
61+
Ok(PreparedOutput {
62+
output_dir,
63+
figures_dir,
64+
markdown_path,
65+
log_path,
66+
})
67+
}
68+
69+
pub(crate) async fn append_log(log_path: &Path, entry: Value) -> Result<()> {
70+
if let Some(parent) = log_path.parent() {
71+
fs::create_dir_all(parent).await?;
72+
}
73+
let mut file = fs::OpenOptions::new()
74+
.create(true)
75+
.append(true)
76+
.open(log_path)
77+
.await?;
78+
let line = serde_json::to_string(&entry)?;
79+
file.write_all(line.as_bytes()).await?;
80+
file.write_all(b"\n").await?;
81+
Ok(())
82+
}
83+
84+
pub(crate) async fn atomic_write_text(path: &Path, content: &str) -> Result<()> {
85+
atomic_write_bytes(path, content.as_bytes()).await
86+
}
87+
88+
pub(crate) async fn atomic_write_bytes(path: &Path, content: &[u8]) -> Result<()> {
89+
if let Some(parent) = path.parent() {
90+
fs::create_dir_all(parent).await?;
91+
}
92+
93+
let seed = SystemTime::now()
94+
.duration_since(UNIX_EPOCH)
95+
.unwrap_or_default()
96+
.as_nanos();
97+
let temp_path = path.with_extension(format!("tmp-{}-{seed}", std::process::id()));
98+
let mut temp_file = fs::File::create(&temp_path).await?;
99+
temp_file.write_all(content).await?;
100+
temp_file.flush().await?;
101+
drop(temp_file);
102+
fs::rename(&temp_path, path).await?;
103+
Ok(())
104+
}

0 commit comments

Comments
 (0)