1use crate::writers::file_log_writer::InfixFilter;
2use crate::{DeferredNow, FlexiLoggerError};
3use std::{
4 ffi::{OsStr, OsString},
5 ops::Add,
6 path::{Path, PathBuf},
7};
8#[derive(Debug, Clone, Eq, PartialEq)]
47pub struct FileSpec {
48 pub(crate) directory: PathBuf,
49 pub(crate) basename: String,
50 pub(crate) o_discriminant: Option<String>,
51 timestamp_cfg: TimestampCfg,
52 o_suffix: Option<String>,
53 pub(crate) use_utc: bool,
54}
55impl Default for FileSpec {
56 #[must_use]
60 fn default() -> Self {
61 FileSpec {
62 directory: PathBuf::from("."),
63 basename: Self::default_basename(),
64 o_discriminant: None,
65 timestamp_cfg: TimestampCfg::Default,
66 o_suffix: Some(String::from("log")),
67 use_utc: false,
68 }
69 }
70}
71impl FileSpec {
72 fn default_basename() -> String {
73 let arg0 = std::env::args().next().unwrap_or_else(|| "rs".to_owned());
74 Path::new(&arg0).file_stem().map(OsStr::to_string_lossy).unwrap().to_string()
75 }
76
77 pub fn try_from<P: Into<PathBuf>>(p: P) -> Result<Self, FlexiLoggerError> {
100 let p: PathBuf = p.into();
101 if p.is_dir() {
102 Err(FlexiLoggerError::OutputBadFile)
103 } else {
104 Ok(FileSpec {
105 directory: p.parent().unwrap().to_path_buf(),
106 basename: p.file_stem().unwrap().to_string_lossy().to_string(),
107 o_discriminant: None,
108 o_suffix: p.extension().map(|s| s.to_string_lossy().to_string()),
109 timestamp_cfg: TimestampCfg::No,
110 use_utc: false,
111 })
112 }
113 }
114
115 #[must_use]
119 pub fn suppress_basename(self) -> Self {
120 self.basename("")
121 }
122
123 #[must_use]
126 pub fn basename<S: Into<String>>(mut self, basename: S) -> Self {
127 self.basename = basename.into();
128 self
129 }
130
131 #[must_use]
134 pub fn o_basename<S: Into<String>>(mut self, o_basename: Option<S>) -> Self {
135 self.basename = o_basename.map_or_else(Self::default_basename, Into::into);
136 self
137 }
138
139 #[must_use]
144 pub fn directory<P: Into<PathBuf>>(mut self, directory: P) -> Self {
145 self.directory = directory.into();
146 self
147 }
148
149 #[must_use]
154 pub fn o_directory<P: Into<PathBuf>>(mut self, directory: Option<P>) -> Self {
155 self.directory = directory.map_or_else(|| PathBuf::from("."), Into::into);
156 self
157 }
158
159 #[must_use]
161 pub fn discriminant<S: Into<String>>(self, discriminant: S) -> Self {
162 self.o_discriminant(Some(discriminant))
163 }
164
165 #[must_use]
167 pub fn o_discriminant<S: Into<String>>(mut self, o_discriminant: Option<S>) -> Self {
168 self.o_discriminant = o_discriminant.map(Into::into);
169 self
170 }
171 #[must_use]
175 pub fn suffix<S: Into<String>>(self, suffix: S) -> Self {
176 self.o_suffix(Some(suffix))
177 }
178
179 #[must_use]
183 pub fn o_suffix<S: Into<String>>(mut self, o_suffix: Option<S>) -> Self {
184 self.o_suffix = o_suffix.map(Into::into);
185 self
186 }
187
188 #[must_use]
192 pub fn suppress_timestamp(self) -> Self {
193 self.use_timestamp(false)
194 }
195
196 #[must_use]
202 pub fn use_timestamp(mut self, use_timestamp: bool) -> Self {
203 self.timestamp_cfg = if use_timestamp {
204 TimestampCfg::Yes
205 } else {
206 TimestampCfg::No
207 };
208 self
209 }
210
211 #[doc(hidden)]
212 #[must_use]
213 pub fn used_directory(&self) -> PathBuf {
214 self.directory.clone()
215 }
216 pub(crate) fn has_basename(&self) -> bool {
217 !self.basename.is_empty()
218 }
219 pub(crate) fn has_discriminant(&self) -> bool {
220 self.o_discriminant.is_some()
221 }
222 pub(crate) fn uses_timestamp(&self) -> bool {
223 matches!(self.timestamp_cfg, TimestampCfg::Yes)
224 }
225
226 pub(crate) fn if_default_use_timestamp(&mut self, use_timestamp: bool) {
229 if let TimestampCfg::Default = self.timestamp_cfg {
230 self.timestamp_cfg = if use_timestamp {
231 TimestampCfg::Yes
232 } else {
233 TimestampCfg::No
234 };
235 }
236 }
237
238 pub(crate) fn get_directory(&self) -> PathBuf {
239 self.directory.clone()
240 }
241
242 pub(crate) fn get_suffix(&self) -> Option<String> {
243 self.o_suffix.clone()
244 }
245
246 pub(crate) fn fixed_name_part(&self) -> String {
248 let mut fixed_name_part = self.basename.clone();
249 fixed_name_part.reserve(50);
250
251 if let Some(discriminant) = &self.o_discriminant {
252 append_underscore_if_not_empty(&mut fixed_name_part);
253 fixed_name_part.push_str(discriminant);
254 }
255 if let Some(timestamp) = &self.timestamp_cfg.get_timestamp() {
256 append_underscore_if_not_empty(&mut fixed_name_part);
257 fixed_name_part.push_str(timestamp);
258 }
259 fixed_name_part
260 }
261
262 #[must_use]
264 pub fn as_pathbuf(&self, o_infix: Option<&str>) -> PathBuf {
265 let mut filename = self.fixed_name_part();
266
267 if let Some(infix) = o_infix {
268 if !infix.is_empty() {
269 append_underscore_if_not_empty(&mut filename);
270 filename.push_str(infix);
271 }
272 };
273 if let Some(suffix) = &self.o_suffix {
274 filename.push('.');
275 filename.push_str(suffix);
276 }
277
278 let mut p_path = self.directory.clone();
279 p_path.push(filename);
280 p_path
281 }
282
283 pub(crate) fn collision_free_infix_for_rotated_file(&self, infix: &str) -> String {
285 let uncompressed_files = self.list_of_files(
286 &InfixFilter::Equls(infix.to_string()),
287 self.o_suffix.as_deref(),
288 );
289 let compressed_files =
290 self.list_of_files(&InfixFilter::Equls(infix.to_string()), Some("gz"));
291
292 let mut restart_siblings = uncompressed_files
293 .into_iter()
294 .chain(compressed_files)
295 .filter(|pb| {
296 let mut pb2 = PathBuf::from(pb);
298 if pb2.extension() == Some(OsString::from("gz").as_ref()) {
299 pb2.set_extension("");
300 };
301 match self.o_suffix {
303 Some(ref sfx) => pb2.extension() == Some(OsString::from(sfx).as_ref()),
304 None => true,
305 }
306 })
307 .filter(|pb| {
308 pb.file_name()
309 .unwrap()
310 .to_string_lossy()
311 .contains(".restart-")
312 })
313 .collect::<Vec<PathBuf>>();
314
315 let new_path = self.as_pathbuf(Some(infix));
316 let new_path_with_gz = {
317 let mut new_path_with_gz = new_path.clone();
318 new_path_with_gz
319 .set_extension([self.o_suffix.as_deref().unwrap_or(""), ".gz"].concat());
320 new_path_with_gz
321 };
322
323 if new_path.exists() || new_path_with_gz.exists() || !restart_siblings.is_empty() {
326 let next_number = if restart_siblings.is_empty() {
327 0
328 } else {
329 restart_siblings.sort_unstable();
330 let new_path = restart_siblings.pop().unwrap();
331 let file_stem_string = if self.o_suffix.is_some() {
332 new_path
333 .file_stem().unwrap()
334 .to_string_lossy().to_string()
335 } else {
336 new_path.to_string_lossy().to_string()
337 };
338 let index = file_stem_string.find(".restart-").unwrap();
339 file_stem_string[(index + 9)..(index + 13)].parse::<usize>().unwrap() + 1
340 };
341
342 infix.to_string().add(&format!(".restart-{next_number:04}"))
343 } else {
344 infix.to_string()
345 }
346 }
347
348 pub(crate) fn list_of_files(
349 &self,
350 infix_filter: &InfixFilter,
351 o_suffix: Option<&str>,
352 ) -> Vec<PathBuf> {
353 self.filter_files(&self.read_dir_related_files(), infix_filter, o_suffix)
354 }
355
356 pub(crate) fn read_dir_related_files(&self) -> Vec<PathBuf> {
358 let fixed_name_part = self.fixed_name_part();
359 let mut log_files = std::fs::read_dir(&self.directory)
360 .unwrap()
361 .flatten()
362 .filter(|entry| entry.path().is_file())
363 .map(|de| de.path())
364 .filter(|path| {
365 if let Some(fln) = path.file_name() {
367 fln.to_string_lossy().starts_with(&fixed_name_part)
368 } else {
369 false
370 }
371 })
372 .collect::<Vec<PathBuf>>();
373 log_files.sort_unstable();
374 log_files.reverse();
375 log_files
376 }
377
378 pub(crate) fn filter_files(
379 &self,
380 files: &[PathBuf],
381 infix_filter: &InfixFilter,
382 o_suffix: Option<&str>,
383 ) -> Vec<PathBuf> {
384 let fixed_name_part = self.fixed_name_part();
385 files
386 .iter()
387 .filter(|path| {
388 if let Some(suffix) = o_suffix {
390 path.extension().is_some_and(|ext| {
391 let s = ext.to_string_lossy();
392 s == suffix
393 })
394 } else {
395 true
396 }
397 })
398 .filter(|path| {
399 let stem = path.file_stem().unwrap().to_string_lossy();
401 let infix_start = if fixed_name_part.is_empty() {
402 0
403 } else {
404 fixed_name_part.len() + 1 };
406 if stem.len() <= infix_start {
407 return false;
408 }
409 let maybe_infix = &stem[infix_start..];
410 let end = maybe_infix.find('.').unwrap_or(maybe_infix.len());
411 infix_filter.filter_infix(&maybe_infix[..end])
412 })
413 .map(PathBuf::clone)
414 .collect::<Vec<PathBuf>>()
415 }
416
417 #[cfg(test)]
418 pub(crate) fn get_timestamp(&self) -> Option<String> {
419 self.timestamp_cfg.get_timestamp()
420 }
421}
422
423fn append_underscore_if_not_empty(filename: &mut String) {
424 if !filename.is_empty() {
425 filename.push('_');
426 }
427}
428
429const TS_USCORE_DASHES_USCORE_DASHES: &str = "%Y-%m-%d_%H-%M-%S";
430
431#[derive(Debug, Clone, Eq, PartialEq)]
432enum TimestampCfg {
433 Default,
434 Yes,
435 No,
436}
437impl TimestampCfg {
438 fn get_timestamp(&self) -> Option<String> {
439 match self {
440 Self::Default | Self::Yes => Some(
441 DeferredNow::new()
442 .format(TS_USCORE_DASHES_USCORE_DASHES)
443 .to_string(),
444 ),
445 Self::No => None,
446 }
447 }
448}
449
450#[cfg(test)]
451mod test {
452 use super::{FileSpec, TimestampCfg};
453 use crate::writers::file_log_writer::InfixFilter;
454 use std::{
455 fs::File,
456 path::{Path, PathBuf},
457 };
458
459 #[test]
460 fn test_timstamp_cfg() {
461 let ts = TimestampCfg::Yes;
462 let s = ts.get_timestamp().unwrap();
463 let bytes = s.into_bytes();
464 assert_eq!(bytes[4], b'-');
465 assert_eq!(bytes[7], b'-');
466 assert_eq!(bytes[10], b'_');
467 assert_eq!(bytes[13], b'-');
468 assert_eq!(bytes[16], b'-');
469 }
470
471 #[test]
472 fn test_default() {
473 let path = FileSpec::default().as_pathbuf(None);
474 assert_file_spec(&path, &PathBuf::from("."), true, "log");
475 }
476
477 fn assert_file_spec(path: &Path, folder: &Path, with_timestamp: bool, suffix: &str) {
479 assert_eq!(
481 path.parent().unwrap(), folder );
484 let progname = PathBuf::from(std::env::args().next().unwrap())
487 .file_stem()
488 .unwrap()
489 .to_string_lossy()
490 .clone()
491 .to_string();
492 let stem = path
493 .file_stem()
494 .unwrap()
495 .to_string_lossy()
496 .clone()
497 .to_string();
498 assert!(
499 stem.starts_with(&progname),
500 "stem: {stem:?}, progname: {progname:?}",
501 );
502 if with_timestamp {
503 assert_eq!(stem.as_bytes()[progname.len()], b'_');
505 let s_ts = &stem[progname.len() + 1..];
506 assert!(
507 chrono::NaiveDateTime::parse_from_str(s_ts, "%Y-%m-%d_%H-%M-%S").is_ok(),
508 "s_ts: \"{s_ts}\"",
509 );
510 } else {
511 assert_eq!(
512 stem.len(),
513 progname.len(),
514 "stem: {stem:?}, progname: {progname:?}",
515 );
516 }
517
518 assert_eq!(path.extension().unwrap(), suffix);
520 }
521
522 #[test]
523 fn test_if_default_use_timestamp() {
524 {
526 let mut fs = FileSpec::default();
527 fs.if_default_use_timestamp(false);
528 let path = fs.as_pathbuf(None);
529 assert_file_spec(&path, &PathBuf::from("."), false, "log");
530 }
531 {
533 let mut fs = FileSpec::default().use_timestamp(true);
534 fs.if_default_use_timestamp(false);
535 let path = fs.as_pathbuf(None);
536 assert_file_spec(&path, &PathBuf::from("."), true, "log");
537 }
538 {
540 let mut fs = FileSpec::default();
541 fs.if_default_use_timestamp(false);
542 let path = fs.use_timestamp(true).as_pathbuf(None);
543 assert_file_spec(&path, &PathBuf::from("."), true, "log");
544 }
545 {
547 let mut fs = FileSpec::default();
548 fs.if_default_use_timestamp(false);
549 let path = fs.use_timestamp(true).as_pathbuf(None);
550 assert_file_spec(&path, &PathBuf::from("."), true, "log");
551 }
552 }
553
554 #[test]
555 fn test_from_url() {
556 let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
557 .unwrap()
558 .as_pathbuf(None);
559 assert_eq!(path.parent().unwrap(), PathBuf::from("/a/b/c"));
561 let stem = path
564 .file_stem()
565 .unwrap()
566 .to_string_lossy()
567 .clone()
568 .to_string();
569 assert_eq!(stem, "d_foo_bar");
570
571 assert_eq!(path.extension().unwrap(), "trc");
573 }
574
575 #[test]
576 fn test_basename() {
577 {
578 let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
579 .unwrap()
580 .o_basename(Some("boo_far"))
581 .as_pathbuf(None);
582 assert_eq!(path.parent().unwrap(), PathBuf::from("/a/b/c"));
584
585 let stem = path
588 .file_stem()
589 .unwrap()
590 .to_string_lossy()
591 .clone()
592 .to_string();
593 assert_eq!(stem, "boo_far");
594
595 assert_eq!(path.extension().unwrap(), "trc");
597 }
598 {
599 let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
600 .unwrap()
601 .o_basename(Option::<String>::None)
602 .as_pathbuf(None);
603 assert_file_spec(&path, &PathBuf::from("/a/b/c"), false, "trc");
604 }
605 }
606
607 #[test]
608 fn test_directory_and_suffix() {
609 {
610 let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
611 .unwrap()
612 .directory("/x/y/z")
613 .o_suffix(Some("txt"))
614 .o_basename(Option::<String>::None)
615 .as_pathbuf(None);
616 assert_file_spec(&path, &PathBuf::from("/x/y/z"), false, "txt");
617 }
618 }
619
620 #[test]
621 fn test_discriminant() {
622 let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
623 .unwrap()
624 .directory("/x/y/z")
625 .o_suffix(Some("txt"))
626 .o_discriminant(Some("1234"))
627 .as_pathbuf(None);
628 assert_eq!(
629 path.file_name().unwrap().to_str().unwrap(),
630 "d_foo_bar_1234.txt"
631 );
632 }
633
634 #[test]
635 fn test_suppress_basename() {
636 let path = FileSpec::try_from("/a/b/c/d_foo_bar.trc")
637 .unwrap()
638 .suppress_basename()
639 .o_suffix(Some("txt"))
640 .o_discriminant(Some("1234"))
641 .as_pathbuf(None);
642 assert_eq!(path.file_name().unwrap().to_str().unwrap(), "1234.txt");
643 }
644
645 #[test]
646 fn test_empty_base_name() {
647 let path = FileSpec::default()
648 .suppress_basename()
649 .suppress_timestamp()
650 .o_discriminant(Option::<String>::None)
651 .as_pathbuf(None);
652 assert_eq!(path.file_name().unwrap(), ".log");
653 }
654
655 #[test]
656 fn test_empty_name() {
657 let path = FileSpec::default()
658 .suppress_basename()
659 .suppress_timestamp()
660 .o_suffix(Option::<String>::None)
661 .as_pathbuf(None);
662 assert!(path.file_name().is_none());
663 }
664
665 #[test]
666 fn issue_178() {
667 let path = FileSpec::default()
668 .basename("BASENAME")
669 .suppress_timestamp()
670 .as_pathbuf(Some(""));
671 assert_eq!(path.file_name().unwrap().to_string_lossy(), "BASENAME.log");
672
673 let path = FileSpec::default()
674 .basename("BASENAME")
675 .discriminant("1")
676 .suppress_timestamp()
677 .as_pathbuf(Some(""));
678 assert_eq!(
679 path.file_name().unwrap().to_string_lossy(),
680 "BASENAME_1.log"
681 );
682 }
683
684 #[test]
685 fn test_list_of_files() {
686 let dir = temp_dir::TempDir::new().unwrap();
687 let pd = dir.path();
688 let filespec: FileSpec = FileSpec::default()
689 .directory(pd)
690 .basename("Base")
691 .discriminant("Discr")
692 .use_timestamp(true);
693 println!("Filespec: {}", filespec.as_pathbuf(Some("Infix")).display());
694
695 let mut fn1 = String::new();
696 fn1.push_str("Base_Discr_");
697 fn1.push_str(&filespec.get_timestamp().unwrap());
698 fn1.push_str("_Infix");
699 fn1.push_str(".log");
700 assert_eq!(
701 filespec
702 .as_pathbuf(Some("Infix"))
703 .file_name()
704 .unwrap()
705 .to_string_lossy(),
706 fn1
707 );
708 create_file(pd, "test1.txt");
710 create_file(pd, &build_filename(&filespec, "Infix1"));
711 create_file(pd, &build_filename(&filespec, "Infix2"));
712
713 println!("\nFolder content:");
714 for entry in std::fs::read_dir(pd).unwrap() {
715 println!(" {}", entry.unwrap().path().display());
716 }
717
718 println!("\nRelevant subset:");
719 for pb in filespec.list_of_files(&InfixFilter::StartsWth("Infix".to_string()), Some("log"))
720 {
721 println!(" {}", pb.display());
722 }
723 }
724
725 fn build_filename(file_spec: &FileSpec, infix: &str) -> String {
726 let mut fn1 = String::new();
727 fn1.push_str("Base_Discr_");
728 fn1.push_str(&file_spec.get_timestamp().unwrap());
729 fn1.push('_');
730 fn1.push_str(infix);
731 fn1.push_str(".log");
732 fn1
733 }
734
735 fn create_file(dir: &Path, filename: &str) {
736 File::create(dir.join(filename)).unwrap();
737 }
738}