agent_first_http/sdk/fetch/
writer.rs1use 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 let tmp = sibling_tmp(&target);
72 assert!(!tmp.exists());
73 }
74}