kowalski_core/tools/internal/
file_system.rs1use std::fs;
7use std::io::{Read, Write};
8use std::path::{Path, PathBuf};
9
10pub const DEFAULT_MAX_READ_BYTES: usize = 256 * 1024;
12
13#[derive(Debug, Clone, Copy, Default)]
15pub struct FileSystemInternalModule;
16
17pub fn path_exists(path: &Path) -> bool {
18 path.exists()
19}
20
21pub fn is_file(path: &Path) -> bool {
22 path.is_file()
23}
24
25pub fn is_dir(path: &Path) -> bool {
26 path.is_dir()
27}
28
29pub fn file_len(path: &Path) -> Result<u64, String> {
31 let m = fs::metadata(path).map_err(|e| e.to_string())?;
32 if !m.is_file() {
33 return Err("path is not a regular file".into());
34 }
35 Ok(m.len())
36}
37
38pub fn read_file_bounded(path: &Path, max_bytes: usize) -> Result<String, String> {
40 let len = file_len(path)?;
41 if len > max_bytes as u64 {
42 return Err(format!(
43 "file size {} exceeds max_bytes {}",
44 len, max_bytes
45 ));
46 }
47 fs::read_to_string(path).map_err(|e| e.to_string())
48}
49
50pub fn read_file_prefix(path: &Path, max_bytes: usize) -> Result<String, String> {
52 let mut f = fs::File::open(path).map_err(|e| e.to_string())?;
53 let mut buf = vec![0u8; max_bytes];
54 let n = f.read(&mut buf).map_err(|e| e.to_string())?;
55 buf.truncate(n);
56 String::from_utf8(buf).map_err(|e| format!("invalid UTF-8: {}", e))
57}
58
59pub fn write_file(path: &Path, contents: &str) -> Result<(), String> {
61 if let Some(parent) = path.parent() {
62 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
63 }
64 fs::write(path, contents.as_bytes()).map_err(|e| e.to_string())
65}
66
67pub fn append_file(path: &Path, contents: &str) -> Result<(), String> {
69 if let Some(parent) = path.parent() {
70 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
71 }
72 let mut f = fs::OpenOptions::new()
73 .create(true)
74 .append(true)
75 .open(path)
76 .map_err(|e| e.to_string())?;
77 f.write_all(contents.as_bytes()).map_err(|e| e.to_string())
78}
79
80pub fn mkdir_all(path: &Path) -> Result<(), String> {
82 fs::create_dir_all(path).map_err(|e| e.to_string())
83}
84
85pub fn remove_file(path: &Path) -> Result<(), String> {
87 fs::remove_file(path).map_err(|e| e.to_string())
88}
89
90pub fn copy_file(src: &Path, dst: &Path) -> Result<(), String> {
92 if let Some(parent) = dst.parent() {
93 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
94 }
95 fs::copy(src, dst).map_err(|e| e.to_string())?;
96 Ok(())
97}
98
99pub fn rename(from: &Path, to: &Path) -> Result<(), String> {
101 if let Some(parent) = to.parent() {
102 fs::create_dir_all(parent).map_err(|e| e.to_string())?;
103 }
104 fs::rename(from, to).map_err(|e| e.to_string())
105}
106
107pub fn list_dir_names(path: &Path, max_entries: usize) -> Result<Vec<String>, String> {
109 if !path.is_dir() {
110 return Err("path is not a directory".into());
111 }
112 let mut names = Vec::new();
113 for e in fs::read_dir(path).map_err(|e| e.to_string())? {
114 let e = e.map_err(|e| e.to_string())?;
115 names.push(e.file_name().to_string_lossy().into_owned());
116 }
117 names.sort();
118 if names.len() > max_entries {
119 names.truncate(max_entries);
120 }
121 Ok(names)
122}
123
124pub fn list_dir_entries(path: &Path, max_entries: usize) -> Result<Vec<(String, bool)>, String> {
126 if !path.is_dir() {
127 return Err("path is not a directory".into());
128 }
129 let mut out = Vec::new();
130 for e in fs::read_dir(path).map_err(|e| e.to_string())? {
131 let e = e.map_err(|e| e.to_string())?;
132 let p = e.path();
133 let is_d = p.is_dir();
134 out.push((e.file_name().to_string_lossy().into_owned(), is_d));
135 }
136 out.sort_by(|a, b| a.0.cmp(&b.0));
137 out.truncate(max_entries);
138 Ok(out)
139}
140
141pub fn try_canonicalize(path: &Path) -> PathBuf {
143 fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use std::io::Write;
150
151 #[test]
152 fn write_read_roundtrip() {
153 let dir = tempfile::tempdir().unwrap();
154 let p = dir.path().join("a.txt");
155 write_file(&p, "hello").unwrap();
156 assert_eq!(read_file_bounded(&p, 1024).unwrap(), "hello");
157 }
158
159 #[test]
160 fn read_too_large_errors() {
161 let dir = tempfile::tempdir().unwrap();
162 let p = dir.path().join("big.txt");
163 let mut f = fs::File::create(&p).unwrap();
164 f.write_all(&[b'x'; 100]).unwrap();
165 assert!(read_file_bounded(&p, 50).is_err());
166 }
167
168 #[test]
169 fn list_dir_names_sorted() {
170 let dir = tempfile::tempdir().unwrap();
171 fs::write(dir.path().join("b"), "").unwrap();
172 fs::write(dir.path().join("a"), "").unwrap();
173 let n = list_dir_names(dir.path(), 10).unwrap();
174 assert_eq!(n, vec!["a".to_string(), "b".to_string()]);
175 }
176}