Skip to main content

agent_first_http/sdk/fetch/
writer.rs

1//! Atomic artifact writes into `--out/<request_id>/`.
2//!
3//! `write_bytes` creates the destination directory lazily and writes to a
4//! `.tmp` sibling first, then renames into place so a crashed fetch never
5//! leaves a half-written artifact that a later run would mistake for
6//! complete.
7
8use std::path::{Path, PathBuf};
9
10use tokio::fs;
11use tokio::io::AsyncWriteExt;
12
13use crate::shared::error::{Error, ErrorCode};
14
15pub async fn ensure_dir(dir: &Path) -> Result<(), Error> {
16    fs::create_dir_all(dir).await.map_err(|e| {
17        Error::new(
18            ErrorCode::IoError,
19            format!("create_dir_all({}): {e}", dir.display()),
20        )
21    })
22}
23
24pub async fn write_bytes(target: &Path, bytes: &[u8]) -> Result<(), Error> {
25    if let Some(parent) = target.parent() {
26        ensure_dir(parent).await?;
27    }
28    let tmp = sibling_tmp(target);
29    let mut file = fs::File::create(&tmp).await.map_err(|e| {
30        Error::new(
31            ErrorCode::IoError,
32            format!("create({}): {e}", tmp.display()),
33        )
34    })?;
35    file.write_all(bytes)
36        .await
37        .map_err(|e| Error::new(ErrorCode::IoError, format!("write({}): {e}", tmp.display())))?;
38    file.flush()
39        .await
40        .map_err(|e| Error::new(ErrorCode::IoError, format!("flush({}): {e}", tmp.display())))?;
41    drop(file);
42    fs::rename(&tmp, target).await.map_err(|e| {
43        Error::new(
44            ErrorCode::IoError,
45            format!("rename({} -> {}): {e}", tmp.display(), target.display()),
46        )
47    })
48}
49
50fn sibling_tmp(target: &Path) -> PathBuf {
51    let mut name = target
52        .file_name()
53        .map(|s| s.to_os_string())
54        .unwrap_or_default();
55    name.push(".tmp");
56    target.with_file_name(name)
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[tokio::test]
64    async fn writes_and_renames_atomically() {
65        let dir = tempfile::tempdir().unwrap();
66        let target = dir.path().join("nested/body.txt");
67        write_bytes(&target, b"hello").await.unwrap();
68        let read = fs::read(&target).await.unwrap();
69        assert_eq!(read, b"hello");
70        // No leftover .tmp
71        let tmp = sibling_tmp(&target);
72        assert!(!tmp.exists());
73    }
74}