oxigdal_streaming/io/
writer.rs1use super::buffer::{ChunkDescriptor, ChunkedBuffer};
4use super::chunked::{ChunkStrategy, ChunkedIO, FileChunkedIO};
5use crate::error::{Result, StreamingError};
6use bytes::Bytes;
7use std::path::Path;
8use std::sync::Arc;
9use tokio::sync::Semaphore;
10use tracing::{debug, info, warn};
11
12pub struct ChunkedWriter {
14 io: Box<dyn ChunkedIO>,
16
17 #[allow(dead_code)]
19 buffer: ChunkedBuffer,
20
21 current_index: usize,
23
24 bytes_written: u64,
26
27 write_semaphore: Arc<Semaphore>,
29
30 #[allow(dead_code)]
32 strategy: ChunkStrategy,
33
34 preset_total_chunks: Option<usize>,
39}
40
41impl ChunkedWriter {
42 pub async fn from_file<P: AsRef<Path>>(
44 path: P,
45 strategy: ChunkStrategy,
46 buffer_size: usize,
47 max_concurrent_writes: usize,
48 ) -> Result<Self> {
49 let mut io = FileChunkedIO::new(path, strategy).await?;
50 io.open_write().await?;
51
52 let chunk_size = strategy.chunk_size_for_index(0, 0);
53 let buffer = ChunkedBuffer::new(chunk_size, buffer_size);
54
55 info!("Created chunked writer with chunk size {}", chunk_size);
56
57 Ok(Self {
58 io: Box::new(io),
59 buffer,
60 current_index: 0,
61 bytes_written: 0,
62 write_semaphore: Arc::new(Semaphore::new(max_concurrent_writes)),
63 strategy,
64 preset_total_chunks: None,
65 })
66 }
67
68 pub fn set_total_chunks(mut self, n: usize) -> Self {
73 self.preset_total_chunks = Some(n);
74 self
75 }
76
77 pub async fn write_chunk(&mut self, data: Bytes) -> Result<()> {
79 let _permit = self
80 .write_semaphore
81 .acquire()
82 .await
83 .map_err(|e| StreamingError::Other(e.to_string()))?;
84
85 let offset = self.bytes_written;
86 let length = data.len();
87
88 let descriptor = ChunkDescriptor::new(
92 offset,
93 length,
94 self.current_index,
95 0, );
97
98 self.io.write_chunk(&descriptor, data).await?;
99
100 self.current_index += 1;
101 self.bytes_written += length as u64;
102
103 debug!(
104 "Wrote chunk {} ({} bytes), total: {} bytes",
105 descriptor.index, length, self.bytes_written
106 );
107
108 Ok(())
109 }
110
111 pub async fn write_chunks(&mut self, chunks: Vec<Bytes>) -> Result<()> {
113 for chunk in chunks {
114 self.write_chunk(chunk).await?;
115 }
116 Ok(())
117 }
118
119 pub async fn flush(&mut self) -> Result<()> {
121 self.io.flush().await?;
122 info!(
123 "Flushed {} bytes in {} chunks",
124 self.bytes_written, self.current_index
125 );
126 Ok(())
127 }
128
129 pub async fn finalize(mut self) -> Result<()> {
142 self.flush().await?;
143
144 let total_chunks = self.current_index;
145
146 if let Some(expected) = self.preset_total_chunks
148 && total_chunks != expected
149 {
150 warn!(
151 "Finalize mismatch: expected {} chunks, wrote {}",
152 expected, total_chunks
153 );
154 return Err(StreamingError::IncompleteFinalize {
155 expected,
156 actual: total_chunks,
157 });
158 }
159
160 let mut footer = Vec::with_capacity(12);
167 footer.extend_from_slice(b"CHNK");
168 footer.extend_from_slice(&(total_chunks as u64).to_le_bytes());
169
170 let footer_descriptor = ChunkDescriptor::new(
171 self.bytes_written,
172 footer.len(),
173 total_chunks, total_chunks + 1, );
176
177 self.io
178 .write_chunk(&footer_descriptor, Bytes::from(footer))
179 .await?;
180 self.io.flush().await?;
181
182 info!(
183 "Finalized chunked writer: {} data chunks, {} total bytes",
184 total_chunks, self.bytes_written
185 );
186
187 Ok(())
188 }
189
190 pub fn chunks_written(&self) -> usize {
192 self.current_index
193 }
194
195 pub fn bytes_written(&self) -> u64 {
197 self.bytes_written
198 }
199}
200
201#[cfg(test)]
202#[allow(clippy::panic)]
203mod tests {
204 use super::*;
205 use std::env;
206
207 #[tokio::test]
208 async fn test_chunked_writer() {
209 let temp_dir = env::temp_dir();
210 let test_path = temp_dir.join("test_chunked_write.dat");
211
212 let result =
213 ChunkedWriter::from_file(&test_path, ChunkStrategy::FixedSize(1024), 10240, 4).await;
214
215 if let Ok(mut writer) = result {
216 let data = Bytes::from(vec![42u8; 1024]);
217 writer.write_chunk(data).await.ok();
218 writer.finalize().await.ok();
219 }
220
221 tokio::fs::remove_file(&test_path).await.ok();
223 }
224
225 #[tokio::test]
227 async fn test_chunked_writer_finalize_patches_total_chunks() {
228 let temp_dir = env::temp_dir();
229 let test_path = temp_dir.join("test_cw_finalize_total.dat");
230 tokio::fs::remove_file(&test_path).await.ok();
232
233 let mut writer =
234 ChunkedWriter::from_file(&test_path, ChunkStrategy::FixedSize(512), 10240, 2)
235 .await
236 .expect("writer should be created successfully");
237
238 for i in 0u8..5 {
239 let data = Bytes::from(vec![i; 512]);
240 writer
241 .write_chunk(data)
242 .await
243 .expect("write_chunk should succeed");
244 }
245
246 assert_eq!(writer.chunks_written(), 5);
247
248 writer.finalize().await.expect("finalize should succeed");
249
250 let meta = tokio::fs::metadata(&test_path)
252 .await
253 .expect("file metadata should be readable");
254 assert!(meta.len() > 5 * 512, "footer was not written");
255
256 tokio::fs::remove_file(&test_path).await.ok();
257 }
258
259 #[tokio::test]
261 async fn test_chunked_writer_with_preset_chunks_matches_on_finalize() {
262 let temp_dir = env::temp_dir();
263 let test_path = temp_dir.join("test_cw_preset_match.dat");
264 tokio::fs::remove_file(&test_path).await.ok();
265
266 let mut writer =
267 ChunkedWriter::from_file(&test_path, ChunkStrategy::FixedSize(256), 10240, 2)
268 .await
269 .expect("writer should be created successfully");
270 writer = writer.set_total_chunks(3);
272
273 for i in 0u8..3 {
274 writer
275 .write_chunk(Bytes::from(vec![i; 256]))
276 .await
277 .expect("write_chunk should succeed");
278 }
279
280 writer
281 .finalize()
282 .await
283 .expect("finalize should succeed when preset matches actual");
284
285 tokio::fs::remove_file(&test_path).await.ok();
286 }
287
288 #[tokio::test]
290 async fn test_chunked_writer_preset_mismatch_returns_error() {
291 let temp_dir = env::temp_dir();
292 let test_path = temp_dir.join("test_cw_preset_mismatch.dat");
293 tokio::fs::remove_file(&test_path).await.ok();
294
295 let mut writer =
296 ChunkedWriter::from_file(&test_path, ChunkStrategy::FixedSize(256), 10240, 2)
297 .await
298 .expect("writer should be created successfully");
299 writer = writer.set_total_chunks(5);
301
302 for i in 0u8..3 {
303 writer
304 .write_chunk(Bytes::from(vec![i; 256]))
305 .await
306 .expect("write_chunk should succeed");
307 }
308
309 let result = writer.finalize().await;
310 match result {
311 Err(StreamingError::IncompleteFinalize { expected, actual }) => {
312 assert_eq!(expected, 5, "expected preset value");
313 assert_eq!(actual, 3, "actual written count");
314 }
315 other => panic!("expected IncompleteFinalize, got {:?}", other),
316 }
317
318 tokio::fs::remove_file(&test_path).await.ok();
319 }
320}