1use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::path::{Path, PathBuf};
6use tokio::fs;
7
8pub async fn ensure_dir_exists(path: &Path) -> Result<()> {
10 if !path.exists() {
11 fs::create_dir_all(path)
12 .await
13 .with_context(|| format!("Failed to create directory: {}", path.display()))?;
14 }
15 Ok(())
16}
17
18pub async fn read_file_with_context(path: &Path, context: &str) -> Result<String> {
20 fs::read_to_string(path)
21 .await
22 .with_context(|| format!("Failed to read {}: {}", context, path.display()))
23}
24
25pub async fn write_file_with_context(path: &Path, content: &str, context: &str) -> Result<()> {
27 if let Some(parent) = path.parent() {
28 ensure_dir_exists(parent).await?;
29 }
30 fs::write(path, content)
31 .await
32 .with_context(|| format!("Failed to write {}: {}", context, path.display()))
33}
34
35pub async fn write_json_file<T: Serialize>(path: &Path, data: &T) -> Result<()> {
37 let json = serde_json::to_string_pretty(data)
38 .with_context(|| format!("Failed to serialize data for {}", path.display()))?;
39
40 write_file_with_context(path, &json, "JSON data").await
41}
42
43pub async fn read_json_file<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T> {
45 let content = read_file_with_context(path, "JSON file").await?;
46
47 serde_json::from_str(&content)
48 .with_context(|| format!("Failed to parse JSON from {}", path.display()))
49}
50
51pub fn parse_json_with_context<T: for<'de> Deserialize<'de>>(
53 content: &str,
54 context: &str,
55) -> Result<T> {
56 serde_json::from_str(content).with_context(|| format!("Failed to parse JSON from {}", context))
57}
58
59pub fn serialize_json_with_context<T: Serialize>(data: &T, context: &str) -> Result<String> {
61 serde_json::to_string(data).with_context(|| format!("Failed to serialize JSON for {}", context))
62}
63
64pub fn serialize_json_pretty_with_context<T: Serialize>(data: &T, context: &str) -> Result<String> {
66 serde_json::to_string_pretty(data)
67 .with_context(|| format!("Failed to pretty-serialize JSON for {}", context))
68}
69
70#[must_use]
76#[inline]
77pub fn try_parse_json<T: for<'de> Deserialize<'de>>(input: &str) -> Option<T> {
78 serde_json::from_str(input).ok()
79}
80
81#[must_use]
86#[inline]
87pub fn try_parse_json_value(input: &str) -> Option<serde_json::Value> {
88 serde_json::from_str(input).ok()
89}
90
91#[inline]
96pub fn parse_json_or_default<T: for<'de> Deserialize<'de> + Default>(
97 input: &str,
98 label: &str,
99) -> T {
100 serde_json::from_str(input).unwrap_or_else(|err| {
101 tracing::debug!(label, %err, "JSON parse failed, using default");
102 T::default()
103 })
104}
105
106pub fn canonicalize_with_context(path: &Path, context: &str) -> Result<PathBuf> {
108 path.canonicalize().with_context(|| {
109 format!(
110 "Failed to canonicalize {} path: {}",
111 context,
112 path.display()
113 )
114 })
115}
116
117pub async fn canonicalize_with_context_async(path: &Path, context: &str) -> Result<PathBuf> {
119 fs::canonicalize(path).await.with_context(|| {
120 format!(
121 "Failed to canonicalize {} path: {}",
122 context,
123 path.display()
124 )
125 })
126}
127
128pub async fn read_to_string_async(path: &Path) -> Result<String> {
130 fs::read_to_string(path)
131 .await
132 .with_context(|| format!("Failed to read {}", path.display()))
133}
134
135pub async fn write_async(path: &Path, contents: impl AsRef<[u8]>) -> Result<()> {
137 fs::write(path, contents)
138 .await
139 .with_context(|| format!("Failed to write {}", path.display()))
140}
141
142pub async fn create_dir_all_async(path: &Path) -> Result<()> {
144 fs::create_dir_all(path)
145 .await
146 .with_context(|| format!("Failed to create {}", path.display()))
147}
148
149pub async fn remove_file_async(path: &Path) -> Result<()> {
151 fs::remove_file(path)
152 .await
153 .with_context(|| format!("Failed to remove {}", path.display()))
154}
155
156pub async fn rename_async(from: &Path, to: &Path) -> Result<()> {
158 fs::rename(from, to)
159 .await
160 .with_context(|| format!("Failed to rename {} to {}", from.display(), to.display()))
161}
162
163pub fn ensure_dir_exists_sync(path: &Path) -> Result<()> {
167 if !path.exists() {
168 std::fs::create_dir_all(path)
169 .with_context(|| format!("Failed to create directory: {}", path.display()))?;
170 }
171 Ok(())
172}
173
174pub fn read_file_with_context_sync(path: &Path, context: &str) -> Result<String> {
176 std::fs::read_to_string(path)
177 .with_context(|| format!("Failed to read {}: {}", context, path.display()))
178}
179
180pub fn write_file_with_context_sync(path: &Path, content: &str, context: &str) -> Result<()> {
182 if let Some(parent) = path.parent() {
183 ensure_dir_exists_sync(parent)?;
184 }
185 std::fs::write(path, content)
186 .with_context(|| format!("Failed to write {}: {}", context, path.display()))
187}
188
189pub fn write_json_file_sync<T: Serialize>(path: &Path, data: &T) -> Result<()> {
191 let json = serde_json::to_string_pretty(data)
192 .with_context(|| format!("Failed to serialize data for {}", path.display()))?;
193
194 write_file_with_context_sync(path, &json, "JSON data")
195}
196
197pub fn read_json_file_sync<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T> {
199 let content = read_file_with_context_sync(path, "JSON file")?;
200
201 serde_json::from_str(&content)
202 .with_context(|| format!("Failed to parse JSON from {}", path.display()))
203}
204
205pub fn is_image_path(path: &Path) -> bool {
207 let Some(extension) = path.extension().and_then(|ext| ext.to_str()) else {
208 return false;
209 };
210
211 matches!(
212 extension,
213 _ if extension.eq_ignore_ascii_case("png")
214 || extension.eq_ignore_ascii_case("jpg")
215 || extension.eq_ignore_ascii_case("jpeg")
216 || extension.eq_ignore_ascii_case("gif")
217 || extension.eq_ignore_ascii_case("bmp")
218 || extension.eq_ignore_ascii_case("webp")
219 || extension.eq_ignore_ascii_case("tiff")
220 || extension.eq_ignore_ascii_case("tif")
221 || extension.eq_ignore_ascii_case("svg")
222 )
223}