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