1use crate::error::ServerError;
7use std::fs;
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, Mutex};
10use std::thread;
11
12#[derive(Clone, Debug)]
27pub struct BatchRequest {
28 pub relative_path: PathBuf,
30}
31
32#[derive(Debug)]
49pub struct BatchResult {
50 pub relative_path: PathBuf,
52 pub body: Result<Vec<u8>, ServerError>,
54}
55
56pub fn process_batch(
76 document_root: &Path,
77 requests: &[BatchRequest],
78 workers: usize,
79) -> Vec<BatchResult> {
80 if requests.is_empty() {
81 return Vec::new();
82 }
83
84 let workers = workers.max(1).min(requests.len());
85 let shared =
86 Arc::new(Mutex::new(Vec::with_capacity(requests.len())));
87
88 thread::scope(|scope| {
89 let chunk_size = requests.len().div_ceil(workers);
90 for chunk in requests.chunks(chunk_size) {
91 let root = document_root.to_path_buf();
92 let out = Arc::clone(&shared);
93 let _ = scope.spawn(move || {
94 for req in chunk {
95 let full_path = root.join(&req.relative_path);
96 let result =
97 fs::read(&full_path).map_err(ServerError::from);
98 let entry = BatchResult {
99 relative_path: req.relative_path.clone(),
100 body: result,
101 };
102 if let Ok(mut guard) = out.lock() {
103 guard.push(entry);
104 }
105 }
106 });
107 }
108 });
109
110 let mut guard = shared
111 .lock()
112 .unwrap_or_else(|poisoned| poisoned.into_inner());
113 let mut out = std::mem::take(&mut *guard);
114 out.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
115 out
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use tempfile::TempDir;
122
123 #[test]
124 fn batch_reads_multiple_files() {
125 let tmp = TempDir::new().expect("tmp");
126 fs::write(tmp.path().join("a.txt"), b"a").expect("write a");
127 fs::write(tmp.path().join("b.txt"), b"b").expect("write b");
128
129 let requests = vec![
130 BatchRequest {
131 relative_path: PathBuf::from("a.txt"),
132 },
133 BatchRequest {
134 relative_path: PathBuf::from("b.txt"),
135 },
136 ];
137
138 let results = process_batch(tmp.path(), &requests, 2);
139 assert_eq!(results.len(), 2);
140 assert!(results.iter().all(|r| r.body.is_ok()));
141 }
142
143 #[test]
144 fn batch_returns_empty_for_empty_requests() {
145 let tmp = TempDir::new().expect("tmp");
146 let requests: Vec<BatchRequest> = Vec::new();
147 let results = process_batch(tmp.path(), &requests, 4);
148 assert!(results.is_empty());
149 }
150
151 #[test]
152 fn batch_reports_missing_file_error() {
153 let tmp = TempDir::new().expect("tmp");
154 let requests = vec![BatchRequest {
155 relative_path: PathBuf::from("missing.txt"),
156 }];
157
158 let results = process_batch(tmp.path(), &requests, 1);
159 assert_eq!(results.len(), 1);
160 assert!(matches!(results[0].body, Err(ServerError::Io(_))));
161 }
162}