1use std::path::{Path, PathBuf};
2use std::sync::Arc;
3
4use crate::error::ADResult;
5use crate::ndarray::NDArray;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum NDFileMode {
10 Single = 0,
11 Capture = 1,
12 Stream = 2,
13}
14
15impl NDFileMode {
16 pub fn from_i32(v: i32) -> Self {
17 match v {
18 0 => Self::Single,
19 1 => Self::Capture,
20 _ => Self::Stream,
21 }
22 }
23}
24
25pub trait NDFileWriter: Send + Sync {
27 fn open_file(&mut self, path: &Path, mode: NDFileMode, array: &NDArray) -> ADResult<()>;
28 fn write_file(&mut self, array: &NDArray) -> ADResult<()>;
29 fn read_file(&mut self) -> ADResult<NDArray>;
30 fn close_file(&mut self) -> ADResult<()>;
31 fn supports_multiple_arrays(&self) -> bool {
32 true
33 }
34}
35
36pub struct NDPluginFileBase {
38 pub file_path: String,
39 pub file_name: String,
40 pub file_number: i32,
41 pub file_template: String,
42 pub auto_increment: bool,
43 pub temp_suffix: String,
44 pub create_dir: i32,
45 pub lazy_open: bool,
46 pub delete_driver_file: bool,
47 capture_buffer: Vec<Arc<NDArray>>,
48 num_capture: usize,
49 num_captured: usize,
50 is_open: bool,
51 mode: NDFileMode,
52 last_written_name: String,
53}
54
55impl NDPluginFileBase {
56 pub fn new() -> Self {
57 Self {
58 file_path: String::new(),
59 file_name: String::new(),
60 file_number: 0,
61 file_template: String::new(),
62 auto_increment: false,
63 temp_suffix: String::new(),
64 create_dir: 0,
65 lazy_open: false,
66 delete_driver_file: false,
67 capture_buffer: Vec::new(),
68 num_capture: 1,
69 num_captured: 0,
70 is_open: false,
71 mode: NDFileMode::Single,
72 last_written_name: String::new(),
73 }
74 }
75
76 pub fn create_file_name(&self) -> String {
82 if self.file_template.is_empty() {
83 format!(
84 "{}{}{:04}",
85 self.file_path, self.file_name, self.file_number
86 )
87 } else {
88 let mut result = String::new();
89 let mut chars = self.file_template.chars().peekable();
90 let mut s_count = 0;
91 while let Some(c) = chars.next() {
92 if c == '%' {
93 let mut left_justify = false;
98 let mut zero_pad = false;
99 loop {
100 match chars.peek() {
101 Some('-') => {
102 left_justify = true;
103 chars.next();
104 }
105 Some('0') => {
106 zero_pad = true;
107 chars.next();
108 }
109 _ => break,
110 }
111 }
112 let mut spec = String::new();
113 while let Some(&nc) = chars.peek() {
114 if nc.is_ascii_digit() || nc == '.' {
115 spec.push(nc);
116 chars.next();
117 } else {
118 break;
119 }
120 }
121 match chars.next() {
122 Some('%') if spec.is_empty() && !left_justify => result.push('%'),
124 Some('s') => {
125 s_count += 1;
126 match s_count {
127 1 => result.push_str(&self.file_path),
128 2 => result.push_str(&self.file_name),
129 _ => {}
130 }
131 }
132 Some('d') => {
133 let (width, precision) = match spec.split_once('.') {
137 Some((w, p)) => (
138 w.parse::<usize>().unwrap_or(0),
139 p.parse::<usize>().unwrap_or(0),
140 ),
141 None => (spec.parse::<usize>().unwrap_or(0), 0),
142 };
143 let digits = format!("{:0>prec$}", self.file_number, prec = precision);
145 if digits.len() >= width {
149 result.push_str(&digits);
150 } else if zero_pad && !left_justify && precision == 0 {
151 result.push_str(&format!(
152 "{:0>width$}",
153 self.file_number,
154 width = width
155 ));
156 } else {
157 let pad = " ".repeat(width - digits.len());
158 if left_justify {
159 result.push_str(&digits);
160 result.push_str(&pad);
161 } else {
162 result.push_str(&pad);
163 result.push_str(&digits);
164 }
165 }
166 }
167 Some(other) => {
168 result.push('%');
169 if left_justify {
170 result.push('-');
171 }
172 if zero_pad {
173 result.push('0');
174 }
175 result.push_str(&spec);
176 result.push(other);
177 }
178 None => result.push('%'),
179 }
180 } else {
181 result.push(c);
182 }
183 }
184 result
185 }
186 }
187
188 pub fn temp_file_path(&self) -> Option<PathBuf> {
190 if self.temp_suffix.is_empty() {
191 None
192 } else {
193 let name = self.create_file_name();
194 Some(PathBuf::from(format!("{}{}", name, self.temp_suffix)))
195 }
196 }
197
198 pub fn last_written_name(&self) -> &str {
200 &self.last_written_name
201 }
202
203 pub fn ensure_directory(&self) -> ADResult<()> {
208 if self.create_dir != 0 && !self.file_path.is_empty() {
209 std::fs::create_dir_all(&self.file_path)?;
210 }
211 Ok(())
212 }
213
214 fn write_path(&self) -> (PathBuf, Option<PathBuf>) {
216 let final_path = PathBuf::from(self.create_file_name());
217 if self.temp_suffix.is_empty() {
218 (final_path, None)
219 } else {
220 let temp = PathBuf::from(format!("{}{}", final_path.display(), self.temp_suffix));
221 (temp, Some(final_path))
222 }
223 }
224
225 fn rename_temp(temp_path: &Path, final_path: &Path) -> ADResult<()> {
227 std::fs::rename(temp_path, final_path)?;
228 Ok(())
229 }
230
231 fn maybe_delete_driver_file(&self, array: &NDArray) {
235 if !self.delete_driver_file {
236 return;
237 }
238 if let Some(attr) = array.attributes.get("DriverFileName") {
239 let driver_file = attr.value.as_string();
240 if !driver_file.is_empty() {
241 let _ = std::fs::remove_file(&driver_file);
242 }
243 }
244 }
245
246 pub fn process_array(
248 &mut self,
249 array: Arc<NDArray>,
250 writer: &mut dyn NDFileWriter,
251 ) -> ADResult<()> {
252 match self.mode {
253 NDFileMode::Single => {
254 self.last_written_name = self.create_file_name();
255 let (write_path, final_path) = self.write_path();
256 writer.open_file(&write_path, NDFileMode::Single, &array)?;
257 writer.write_file(&array)?;
258 writer.close_file()?;
259 if let Some(final_path) = final_path {
260 Self::rename_temp(&write_path, &final_path)?;
261 }
262 self.maybe_delete_driver_file(&array);
263 if self.auto_increment {
264 self.file_number += 1;
265 }
266 }
267 NDFileMode::Capture => {
268 self.capture_buffer.push(array);
269 self.num_captured = self.capture_buffer.len();
270 if self.num_capture > 0 && self.num_captured >= self.num_capture {
272 self.flush_capture(writer)?;
273 }
274 }
275 NDFileMode::Stream => {
276 if !self.is_open && !self.lazy_open {
277 self.last_written_name = self.create_file_name();
278 let (write_path, _) = self.write_path();
279 writer.open_file(&write_path, NDFileMode::Stream, &array)?;
280 self.is_open = true;
281 }
282 if self.lazy_open && !self.is_open {
283 self.last_written_name = self.create_file_name();
284 let (write_path, _) = self.write_path();
285 writer.open_file(&write_path, NDFileMode::Stream, &array)?;
286 self.is_open = true;
287 }
288 writer.write_file(&array)?;
289 self.maybe_delete_driver_file(&array);
290 self.num_captured += 1;
291 }
292 }
293 Ok(())
294 }
295
296 pub fn flush_capture(&mut self, writer: &mut dyn NDFileWriter) -> ADResult<()> {
303 if self.capture_buffer.is_empty() {
304 return Ok(());
305 }
306
307 if writer.supports_multiple_arrays() {
308 self.last_written_name = self.create_file_name();
310 let (write_path, final_path) = self.write_path();
311 writer.open_file(&write_path, NDFileMode::Capture, &self.capture_buffer[0])?;
312 for arr in &self.capture_buffer {
313 writer.write_file(arr)?;
314 }
315 writer.close_file()?;
316 if let Some(final_path) = final_path {
317 Self::rename_temp(&write_path, &final_path)?;
318 }
319 let buffer = std::mem::take(&mut self.capture_buffer);
321 for arr in &buffer {
322 self.maybe_delete_driver_file(arr);
323 }
324 self.capture_buffer = buffer;
325 if self.auto_increment {
326 self.file_number += 1;
327 }
328 } else {
329 let buffer = std::mem::take(&mut self.capture_buffer);
331 for arr in &buffer {
332 self.last_written_name = self.create_file_name();
333 let (write_path, final_path) = self.write_path();
334 writer.open_file(&write_path, NDFileMode::Single, arr)?;
335 writer.write_file(arr)?;
336 writer.close_file()?;
337 if let Some(final_path) = final_path {
338 Self::rename_temp(&write_path, &final_path)?;
339 }
340 self.maybe_delete_driver_file(arr);
341 if self.auto_increment {
342 self.file_number += 1;
343 }
344 }
345 self.capture_buffer = buffer;
346 }
347
348 self.capture_buffer.clear();
349 self.num_captured = 0;
350 Ok(())
351 }
352
353 pub fn open_stream_eager(
360 &mut self,
361 writer: &mut dyn NDFileWriter,
362 array: &NDArray,
363 ) -> ADResult<()> {
364 if self.is_open {
365 return Ok(());
366 }
367 self.last_written_name = self.create_file_name();
368 let (write_path, _) = self.write_path();
369 writer.open_file(&write_path, NDFileMode::Stream, array)?;
370 self.is_open = true;
371 Ok(())
372 }
373
374 pub fn force_close(&mut self, writer: &mut dyn NDFileWriter) -> ADResult<()> {
377 if self.is_open {
378 self.close_stream(writer)?;
379 }
380 Ok(())
381 }
382
383 pub fn close_stream(&mut self, writer: &mut dyn NDFileWriter) -> ADResult<()> {
385 if self.is_open {
386 writer.close_file()?;
387 if !self.temp_suffix.is_empty() {
389 let final_name = self.create_file_name();
390 let temp_name = format!("{}{}", final_name, self.temp_suffix);
391 Self::rename_temp(Path::new(&temp_name), Path::new(&final_name))?;
392 }
393 self.is_open = false;
394 if self.auto_increment {
395 self.file_number += 1;
396 }
397 }
398 Ok(())
399 }
400
401 pub fn is_open(&self) -> bool {
402 self.is_open
403 }
404
405 pub fn set_mode(&mut self, mode: NDFileMode) {
406 self.mode = mode;
407 }
408
409 pub fn set_num_capture(&mut self, n: usize) {
410 self.num_capture = n;
411 }
412
413 pub fn num_captured(&self) -> usize {
414 self.num_captured
415 }
416
417 pub fn mode(&self) -> NDFileMode {
418 self.mode
419 }
420
421 pub fn num_capture_target(&self) -> usize {
422 self.num_capture
423 }
424
425 pub fn capture_array(&mut self, array: Arc<NDArray>) {
426 self.capture_buffer.push(array);
427 self.num_captured = self.capture_buffer.len();
428 }
429
430 pub fn clear_capture(&mut self) {
431 self.capture_buffer.clear();
432 self.num_captured = 0;
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::ndarray::{NDDataType, NDDimension};
440
441 struct MockWriter {
443 opens: Vec<PathBuf>,
444 writes: usize,
445 closes: usize,
446 multi: bool,
447 }
448
449 impl MockWriter {
450 fn new(multi: bool) -> Self {
451 Self {
452 opens: Vec::new(),
453 writes: 0,
454 closes: 0,
455 multi,
456 }
457 }
458 }
459
460 impl NDFileWriter for MockWriter {
461 fn open_file(&mut self, path: &Path, _mode: NDFileMode, _array: &NDArray) -> ADResult<()> {
462 self.opens.push(path.to_path_buf());
463 Ok(())
464 }
465 fn write_file(&mut self, _array: &NDArray) -> ADResult<()> {
466 self.writes += 1;
467 Ok(())
468 }
469 fn read_file(&mut self) -> ADResult<NDArray> {
470 Err(crate::error::ADError::UnsupportedConversion(
471 "not implemented".into(),
472 ))
473 }
474 fn close_file(&mut self) -> ADResult<()> {
475 self.closes += 1;
476 Ok(())
477 }
478 fn supports_multiple_arrays(&self) -> bool {
479 self.multi
480 }
481 }
482
483 fn make_array(id: i32) -> Arc<NDArray> {
484 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
485 arr.unique_id = id;
486 Arc::new(arr)
487 }
488
489 #[test]
490 fn test_single_mode() {
491 let mut fb = NDPluginFileBase::new();
492 fb.file_path = "/tmp/".into();
493 fb.file_name = "test_".into();
494 fb.file_number = 1;
495 fb.auto_increment = true;
496 fb.set_mode(NDFileMode::Single);
497
498 let mut writer = MockWriter::new(false);
499 fb.process_array(make_array(1), &mut writer).unwrap();
500
501 assert_eq!(writer.opens.len(), 1);
502 assert_eq!(writer.writes, 1);
503 assert_eq!(writer.closes, 1);
504 assert_eq!(fb.file_number, 2); }
506
507 #[test]
508 fn test_capture_mode() {
509 let mut fb = NDPluginFileBase::new();
510 fb.file_path = "/tmp/".into();
511 fb.file_name = "cap_".into();
512 fb.set_mode(NDFileMode::Capture);
513 fb.set_num_capture(3);
514
515 let mut writer = MockWriter::new(true);
516
517 fb.process_array(make_array(1), &mut writer).unwrap();
519 assert_eq!(writer.writes, 0); fb.process_array(make_array(2), &mut writer).unwrap();
521 assert_eq!(writer.writes, 0);
522 fb.process_array(make_array(3), &mut writer).unwrap();
523 assert_eq!(writer.opens.len(), 1);
525 assert_eq!(writer.writes, 3);
526 assert_eq!(writer.closes, 1);
527 }
528
529 #[test]
530 fn test_capture_mode_single_image_format() {
531 let mut fb = NDPluginFileBase::new();
532 fb.file_path = "/tmp/".into();
533 fb.file_name = "jpeg_".into();
534 fb.file_number = 0;
535 fb.auto_increment = true;
536 fb.set_mode(NDFileMode::Capture);
537 fb.set_num_capture(3);
538
539 let mut writer = MockWriter::new(false); fb.process_array(make_array(1), &mut writer).unwrap();
542 fb.process_array(make_array(2), &mut writer).unwrap();
543 fb.process_array(make_array(3), &mut writer).unwrap();
544 assert_eq!(writer.opens.len(), 3);
546 assert_eq!(writer.writes, 3);
547 assert_eq!(writer.closes, 3);
548 assert_eq!(fb.file_number, 3); }
550
551 #[test]
552 fn test_stream_mode() {
553 let mut fb = NDPluginFileBase::new();
554 fb.file_path = "/tmp/".into();
555 fb.file_name = "stream_".into();
556 fb.set_mode(NDFileMode::Stream);
557
558 let mut writer = MockWriter::new(true);
559
560 fb.process_array(make_array(1), &mut writer).unwrap();
561 fb.process_array(make_array(2), &mut writer).unwrap();
562 fb.process_array(make_array(3), &mut writer).unwrap();
563
564 assert_eq!(writer.opens.len(), 1); assert_eq!(writer.writes, 3);
566 assert_eq!(writer.closes, 0); fb.close_stream(&mut writer).unwrap();
569 assert_eq!(writer.closes, 1);
570 }
571
572 #[test]
573 fn test_create_file_name_default() {
574 let mut fb = NDPluginFileBase::new();
575 fb.file_path = "/data/".into();
576 fb.file_name = "img_".into();
577 fb.file_number = 42;
578 assert_eq!(fb.create_file_name(), "/data/img_0042");
579 }
580
581 #[test]
582 fn test_create_file_name_template() {
583 let mut fb = NDPluginFileBase::new();
584 fb.file_path = "/data/".into();
585 fb.file_name = "img_".into();
586 fb.file_number = 5;
587 fb.file_template = "%s%s%d.tif".into();
588 assert_eq!(fb.create_file_name(), "/data/img_5.tif");
589 }
590
591 #[test]
592 fn test_create_file_name_printf_specs() {
593 let mut fb = NDPluginFileBase::new();
595 fb.file_path = "/d/".into();
596 fb.file_name = "f".into();
597 fb.file_number = 7;
598
599 fb.file_template = "%s%s_%3.3d.dat".into();
600 assert_eq!(fb.create_file_name(), "/d/f_007.dat");
601
602 fb.file_template = "%s%s_%5d".into();
604 assert_eq!(fb.create_file_name(), "/d/f_ 7");
605
606 fb.file_template = "%s%s_%-5d".into();
608 assert_eq!(fb.create_file_name(), "/d/f_7 ");
609
610 fb.file_template = "%s%s_%05d".into();
612 assert_eq!(fb.create_file_name(), "/d/f_00007");
613
614 fb.file_template = "%s%s_%d%%".into();
616 assert_eq!(fb.create_file_name(), "/d/f_7%");
617 }
618
619 #[test]
620 fn test_auto_increment() {
621 let mut fb = NDPluginFileBase::new();
622 fb.file_path = "/tmp/".into();
623 fb.file_name = "t_".into();
624 fb.file_number = 0;
625 fb.auto_increment = true;
626 fb.set_mode(NDFileMode::Single);
627
628 let mut writer = MockWriter::new(false);
629 fb.process_array(make_array(1), &mut writer).unwrap();
630 assert_eq!(fb.file_number, 1);
631 fb.process_array(make_array(2), &mut writer).unwrap();
632 assert_eq!(fb.file_number, 2);
633 }
634
635 #[test]
636 fn test_temp_suffix() {
637 let mut fb = NDPluginFileBase::new();
638 fb.file_path = "/data/".into();
639 fb.file_name = "img_".into();
640 fb.file_number = 1;
641 fb.temp_suffix = ".tmp".into();
642
643 let temp = fb.temp_file_path().unwrap();
644 assert_eq!(temp.to_str().unwrap(), "/data/img_0001.tmp");
645 }
646
647 fn make_array_with_driver_file(id: i32, driver_file: &str) -> Arc<NDArray> {
648 use crate::attributes::{NDAttrSource, NDAttrValue, NDAttribute};
649 let mut arr = NDArray::new(vec![NDDimension::new(4)], NDDataType::UInt8);
650 arr.unique_id = id;
651 arr.attributes.add(NDAttribute::new_static(
652 "DriverFileName",
653 "",
654 NDAttrSource::Driver,
655 NDAttrValue::String(driver_file.to_string()),
656 ));
657 Arc::new(arr)
658 }
659
660 #[test]
661 fn test_delete_driver_file_in_capture_mode() {
662 let dir = std::env::temp_dir();
665 let f1 = dir.join("adcore_capture_driver_1.raw");
666 let f2 = dir.join("adcore_capture_driver_2.raw");
667 std::fs::write(&f1, b"x").unwrap();
668 std::fs::write(&f2, b"y").unwrap();
669
670 let mut fb = NDPluginFileBase::new();
671 fb.file_path = format!("{}/", dir.display());
672 fb.file_name = "capdel_".into();
673 fb.delete_driver_file = true;
674 fb.set_mode(NDFileMode::Capture);
675 fb.set_num_capture(2);
676
677 let mut writer = MockWriter::new(true);
678 fb.process_array(
679 make_array_with_driver_file(1, f1.to_str().unwrap()),
680 &mut writer,
681 )
682 .unwrap();
683 fb.process_array(
684 make_array_with_driver_file(2, f2.to_str().unwrap()),
685 &mut writer,
686 )
687 .unwrap();
688
689 assert!(
691 !f1.exists(),
692 "driver file 1 should be deleted in capture mode"
693 );
694 assert!(
695 !f2.exists(),
696 "driver file 2 should be deleted in capture mode"
697 );
698 }
699
700 #[test]
701 fn test_delete_driver_file_capture_single_image_format() {
702 let dir = std::env::temp_dir();
703 let f1 = dir.join("adcore_capture_si_driver_1.raw");
704 std::fs::write(&f1, b"x").unwrap();
705
706 let mut fb = NDPluginFileBase::new();
707 fb.file_path = format!("{}/", dir.display());
708 fb.file_name = "capdelsi_".into();
709 fb.delete_driver_file = true;
710 fb.set_mode(NDFileMode::Capture);
711 fb.set_num_capture(1);
712
713 let mut writer = MockWriter::new(false); fb.process_array(
715 make_array_with_driver_file(1, f1.to_str().unwrap()),
716 &mut writer,
717 )
718 .unwrap();
719
720 assert!(
721 !f1.exists(),
722 "driver file should be deleted in capture mode"
723 );
724 }
725
726 #[test]
727 fn test_ensure_directory() {
728 let fb = NDPluginFileBase::new();
729 fb.ensure_directory().unwrap();
731 }
732}