1pub mod catalog_io;
13pub mod content_block_io;
14pub mod custom_attribute_io;
15pub mod email_template_io;
16pub mod frontmatter;
17pub mod tag_io;
18
19use crate::error::{Error, Result};
20use std::path::{Path, PathBuf};
21
22pub(crate) fn try_read_resource_dir(
28 root: &Path,
29 kind_label: &str,
30) -> Result<Option<std::fs::ReadDir>> {
31 match std::fs::read_dir(root) {
32 Ok(rd) => Ok(Some(rd)),
33 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
34 Err(e) => {
35 if root.is_file() {
36 return Err(Error::InvalidFormat {
37 path: root.to_path_buf(),
38 message: format!("expected a directory for the {kind_label} root"),
39 });
40 }
41 Err(e.into())
42 }
43 }
44}
45
46pub(crate) fn validate_resource_name(kind_label: &str, name: &str) -> Result<()> {
50 let bad = name.is_empty()
51 || name == "."
52 || name == ".."
53 || name.contains('/')
54 || name.contains('\\')
55 || name.contains('\0');
56 if bad {
57 return Err(Error::InvalidFormat {
58 path: PathBuf::from(name),
59 message: format!("{kind_label} name '{name}' contains invalid characters"),
60 });
61 }
62 Ok(())
63}
64
65pub(crate) fn write_atomic(path: &Path, contents: &[u8]) -> Result<()> {
74 use std::io::Write;
75
76 let parent = path.parent().unwrap_or_else(|| Path::new("."));
77 std::fs::create_dir_all(parent)?;
78 let file_name = path.file_name().ok_or_else(|| Error::InvalidFormat {
79 path: path.to_path_buf(),
80 message: "atomic write target has no file name".into(),
81 })?;
82
83 let mut tmp_name = file_name.to_os_string();
84 tmp_name.push(format!(".{}.tmp", std::process::id()));
85 let tmp_path = parent.join(tmp_name);
86
87 let mut file = std::fs::File::create(&tmp_path)?;
88 if let Err(e) = file.write_all(contents).and_then(|_| file.sync_all()) {
89 drop(file);
90 let _ = std::fs::remove_file(&tmp_path);
91 return Err(e.into());
92 }
93 drop(file);
94
95 if let Err(e) = std::fs::rename(&tmp_path, path) {
96 let _ = std::fs::remove_file(&tmp_path);
97 return Err(e.into());
98 }
99 Ok(())
100}