1use anyhow::{Context, Result};
2use chrono::{DateTime, Datelike, Local, NaiveDate, Timelike};
3use std::collections::{HashMap, HashSet};
4use std::fmt;
5use std::fs;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum RetentionReason {
11 KeepLast,
13 Hourly,
15 Daily,
17 Weekly,
19 Monthly,
21 Yearly,
23}
24
25impl fmt::Display for RetentionReason {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::KeepLast => write!(f, "keep-last"),
29 Self::Hourly => write!(f, "hourly"),
30 Self::Daily => write!(f, "daily"),
31 Self::Weekly => write!(f, "weekly"),
32 Self::Monthly => write!(f, "monthly"),
33 Self::Yearly => write!(f, "yearly"),
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
39pub struct FileInfo {
40 pub path: PathBuf,
41 pub created: DateTime<Local>,
42}
43
44#[derive(Debug, Clone, PartialEq)]
45pub struct RetentionConfig {
46 pub keep_last: usize,
47 pub keep_hourly: u32,
48 pub keep_daily: u32,
49 pub keep_weekly: u32,
50 pub keep_monthly: u32,
51 pub keep_yearly: u32,
52}
53
54impl Default for RetentionConfig {
55 fn default() -> Self {
56 Self {
57 keep_last: 5,
58 keep_hourly: 24,
59 keep_daily: 7,
60 keep_weekly: 4,
61 keep_monthly: 12,
62 keep_yearly: 10,
63 }
64 }
65}
66
67pub fn get_file_creation_time(path: &Path) -> Result<DateTime<Local>> {
75 let metadata = fs::metadata(path).context("Failed to read file metadata")?;
76 let mtime = metadata
77 .modified()
78 .or_else(|_| metadata.created())
79 .context("Failed to get file modification/creation time")?;
80 Ok(DateTime::from(mtime))
81}
82
83pub fn scan_files(dir: &Path) -> Result<Vec<FileInfo>> {
88 let mut files = Vec::new();
89
90 for entry in fs::read_dir(dir).context("Failed to read directory")? {
91 let entry = entry.context("Failed to read directory entry")?;
92 let path = entry.path();
93
94 if path.is_dir()
96 || path
97 .file_name()
98 .is_some_and(|n| n.to_string_lossy().starts_with('.'))
99 {
100 continue;
101 }
102
103 match get_file_creation_time(&path) {
104 Ok(created) => files.push(FileInfo { path, created }),
105 Err(e) => eprintln!("Warning: Skipping {}: {e}", path.display()),
106 }
107 }
108
109 files.sort_by(|a, b| b.created.cmp(&a.created));
111 Ok(files)
112}
113
114fn get_hour_key(dt: DateTime<Local>) -> (i32, u32, u32, u32) {
116 (dt.year(), dt.month(), dt.day(), dt.hour())
117}
118
119fn get_week_key(date: NaiveDate) -> (i32, u32) {
121 (date.iso_week().year(), date.iso_week().week())
122}
123
124fn get_month_key(date: NaiveDate) -> (i32, u32) {
125 (date.year(), date.month())
126}
127
128fn get_year_key(date: NaiveDate) -> i32 {
129 date.year()
130}
131
132#[must_use]
134pub fn select_files_to_keep_with_reasons(
135 files: &[FileInfo],
136 config: &RetentionConfig,
137 now: DateTime<Local>,
138) -> HashMap<usize, RetentionReason> {
139 let mut keep_reasons: HashMap<usize, RetentionReason> = HashMap::new();
140 let today = now.date_naive();
141
142 for i in 0..config.keep_last.min(files.len()) {
144 keep_reasons.insert(i, RetentionReason::KeepLast);
145 }
146
147 if config.keep_hourly > 0 {
151 let hour_boundary = now - chrono::Duration::hours(i64::from(config.keep_hourly));
152 let mut covered_hours: HashSet<(i32, u32, u32, u32)> = HashSet::new();
153 for (i, file) in files.iter().enumerate().rev() {
154 if keep_reasons.contains_key(&i) {
155 continue; }
157 let file_datetime = file.created;
158 let hour_key = get_hour_key(file_datetime);
159 if file_datetime >= hour_boundary && !covered_hours.contains(&hour_key) {
160 covered_hours.insert(hour_key);
161 keep_reasons.insert(i, RetentionReason::Hourly);
162 }
163 }
164 }
165
166 if config.keep_daily > 0 {
170 let day_boundary = today - chrono::Duration::days(i64::from(config.keep_daily));
171 let mut covered_days: HashSet<NaiveDate> = HashSet::new();
172 for (i, file) in files.iter().enumerate().rev() {
173 if keep_reasons.contains_key(&i) {
174 continue; }
176 let file_date = file.created.date_naive();
177 if file_date >= day_boundary && !covered_days.contains(&file_date) {
178 covered_days.insert(file_date);
179 keep_reasons.insert(i, RetentionReason::Daily);
180 }
181 }
182 }
183
184 if config.keep_weekly > 0 {
188 let week_boundary = today - chrono::Duration::weeks(i64::from(config.keep_weekly));
189 let mut covered_weeks: HashSet<(i32, u32)> = HashSet::new();
190 for (i, file) in files.iter().enumerate().rev() {
191 if keep_reasons.contains_key(&i) {
192 continue; }
194 let file_date = file.created.date_naive();
195 let week_key = get_week_key(file_date);
196 if file_date >= week_boundary && !covered_weeks.contains(&week_key) {
197 covered_weeks.insert(week_key);
198 keep_reasons.insert(i, RetentionReason::Weekly);
199 }
200 }
201 }
202
203 if config.keep_monthly > 0 {
207 let month_boundary = today - chrono::Duration::days(i64::from(config.keep_monthly) * 30);
208 let mut covered_months: HashSet<(i32, u32)> = HashSet::new();
209 for (i, file) in files.iter().enumerate().rev() {
210 if keep_reasons.contains_key(&i) {
211 continue; }
213 let file_date = file.created.date_naive();
214 let month_key = get_month_key(file_date);
215 if file_date >= month_boundary && !covered_months.contains(&month_key) {
216 covered_months.insert(month_key);
217 keep_reasons.insert(i, RetentionReason::Monthly);
218 }
219 }
220 }
221
222 if config.keep_yearly > 0 {
226 let year_boundary = today - chrono::Duration::days(i64::from(config.keep_yearly) * 365);
227 let mut covered_years: HashSet<i32> = HashSet::new();
228 for (i, file) in files.iter().enumerate().rev() {
229 if keep_reasons.contains_key(&i) {
230 continue; }
232 let file_date = file.created.date_naive();
233 let year_key = get_year_key(file_date);
234 if file_date >= year_boundary && !covered_years.contains(&year_key) {
235 covered_years.insert(year_key);
236 keep_reasons.insert(i, RetentionReason::Yearly);
237 }
238 }
239 }
240
241 keep_reasons
242}
243
244#[must_use]
245pub fn select_files_to_keep_with_datetime(
246 files: &[FileInfo],
247 config: &RetentionConfig,
248 now: DateTime<Local>,
249) -> HashSet<usize> {
250 select_files_to_keep_with_reasons(files, config, now)
251 .into_keys()
252 .collect()
253}
254
255#[must_use]
256pub fn select_files_to_keep(files: &[FileInfo], config: &RetentionConfig) -> HashSet<usize> {
257 let now = Local::now();
258 select_files_to_keep_with_datetime(files, config, now)
259}
260
261pub fn move_to_trash(file: &Path, trash_dir: &Path, dry_run: bool) -> Result<()> {
266 let file_name = file.file_name().context("Failed to get file name")?;
267 let dest = trash_dir.join(file_name);
268
269 if dry_run {
270 println!("Would move: {} -> {}", file.display(), dest.display());
271 } else {
272 let mut final_dest = dest.clone();
274 let mut counter = 1;
275 while final_dest.exists() {
276 let stem = dest.file_stem().unwrap_or_default().to_string_lossy();
277 let ext = dest
278 .extension()
279 .map(|e| format!(".{}", e.to_string_lossy()))
280 .unwrap_or_default();
281 final_dest = trash_dir.join(format!("{stem}_{counter}{ext}"));
282 counter += 1;
283 }
284 fs::rename(file, &final_dest).context("Failed to move file to trash")?;
285 println!("Moved: {} -> {}", file.display(), final_dest.display());
286 }
287
288 Ok(())
289}
290
291pub fn rotate_files(dir: &Path, config: &RetentionConfig, dry_run: bool) -> Result<(usize, usize)> {
300 if config.keep_last == 0 {
301 anyhow::bail!("keep-last must be at least 1");
302 }
303
304 let trash_dir = dir.join(".trash");
306 if !dry_run && !trash_dir.exists() {
307 fs::create_dir(&trash_dir).context("Failed to create .trash directory")?;
308 }
309
310 let files = scan_files(dir)?;
312
313 if files.is_empty() {
314 return Ok((0, 0));
315 }
316
317 let now = Local::now();
319 let keep_reasons = select_files_to_keep_with_reasons(&files, config, now);
320
321 for (i, file) in files.iter().enumerate() {
323 if let Some(reason) = keep_reasons.get(&i) {
324 let prefix = if dry_run { "Would keep" } else { "Keeping" };
325 println!("{prefix}: {} ({reason})", file.path.display());
326 }
327 }
328
329 let mut moved_count = 0;
331 for (i, file) in files.iter().enumerate() {
332 if !keep_reasons.contains_key(&i) {
333 move_to_trash(&file.path, &trash_dir, dry_run)?;
334 moved_count += 1;
335 }
336 }
337
338 Ok((keep_reasons.len(), moved_count))
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use chrono::TimeZone;
345
346 fn make_file_info_with_time(name: &str, dt: DateTime<Local>) -> FileInfo {
347 FileInfo {
348 path: PathBuf::from(name),
349 created: dt,
350 }
351 }
352
353 fn make_file_info(name: &str, date: NaiveDate) -> FileInfo {
354 let datetime = Local
355 .from_local_datetime(&date.and_hms_opt(12, 0, 0).unwrap())
356 .single()
357 .unwrap();
358 FileInfo {
359 path: PathBuf::from(name),
360 created: datetime,
361 }
362 }
363
364 fn zero_config() -> RetentionConfig {
365 RetentionConfig {
366 keep_last: 0,
367 keep_hourly: 0,
368 keep_daily: 0,
369 keep_weekly: 0,
370 keep_monthly: 0,
371 keep_yearly: 0,
372 }
373 }
374
375 #[test]
376 fn test_default_config() {
377 let config = RetentionConfig::default();
378 assert_eq!(config.keep_last, 5);
379 assert_eq!(config.keep_hourly, 24);
380 assert_eq!(config.keep_daily, 7);
381 assert_eq!(config.keep_weekly, 4);
382 assert_eq!(config.keep_monthly, 12);
383 assert_eq!(config.keep_yearly, 10);
384 }
385
386 #[test]
387 fn test_get_hour_key() {
388 let dt = Local.with_ymd_and_hms(2024, 6, 15, 14, 30, 0).unwrap();
389 assert_eq!(get_hour_key(dt), (2024, 6, 15, 14));
390 }
391
392 #[test]
393 fn test_get_week_key() {
394 let date = NaiveDate::from_ymd_opt(2024, 1, 15).unwrap();
395 let (year, week) = get_week_key(date);
396 assert_eq!(year, 2024);
397 assert!(week >= 1 && week <= 53);
398 }
399
400 #[test]
401 fn test_get_month_key() {
402 let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
403 assert_eq!(get_month_key(date), (2024, 6));
404 }
405
406 #[test]
407 fn test_get_year_key() {
408 let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
409 assert_eq!(get_year_key(date), 2024);
410 }
411
412 #[test]
413 fn test_keep_last_n_files() {
414 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
415 let config = RetentionConfig {
416 keep_last: 3,
417 ..zero_config()
418 };
419
420 let files: Vec<FileInfo> = (0..5)
422 .map(|i| {
423 let dt = now - chrono::Duration::minutes(i as i64);
424 FileInfo {
425 path: PathBuf::from(format!("file{}.txt", i)),
426 created: dt,
427 }
428 })
429 .collect();
430
431 let keep = select_files_to_keep_with_datetime(&files, &config, now);
432 assert_eq!(keep.len(), 3);
433 assert!(keep.contains(&0));
434 assert!(keep.contains(&1));
435 assert!(keep.contains(&2));
436 }
437
438 #[test]
439 fn test_keep_one_per_hour() {
440 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
441 let config = RetentionConfig {
442 keep_hourly: 5,
443 ..zero_config()
444 };
445
446 let files = vec![
449 make_file_info_with_time("file1.txt", now),
450 make_file_info_with_time("file2.txt", now - chrono::Duration::hours(1)),
451 make_file_info_with_time(
452 "file4.txt",
453 now - chrono::Duration::hours(2) + chrono::Duration::minutes(30),
454 ), make_file_info_with_time("file3.txt", now - chrono::Duration::hours(2)), ];
457
458 let keep = select_files_to_keep_with_datetime(&files, &config, now);
459 assert_eq!(keep.len(), 3); assert!(keep.contains(&0)); assert!(keep.contains(&1)); assert!(keep.contains(&3)); assert!(!keep.contains(&2)); }
465
466 #[test]
467 fn test_keep_one_per_day() {
468 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
469 let today = now.date_naive();
470 let config = RetentionConfig {
471 keep_daily: 5,
472 ..zero_config()
473 };
474
475 let files = vec![
478 make_file_info("file1.txt", today),
479 make_file_info("file2.txt", today - chrono::Duration::days(1)),
480 make_file_info("file3.txt", today - chrono::Duration::days(2)), make_file_info("file4.txt", today - chrono::Duration::days(2)), ];
483
484 let keep = select_files_to_keep_with_datetime(&files, &config, now);
485 assert_eq!(keep.len(), 3);
486 assert!(keep.contains(&0)); assert!(keep.contains(&1)); assert!(keep.contains(&3)); assert!(!keep.contains(&2)); }
491
492 #[test]
493 fn test_keep_one_per_week() {
494 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap(); let today = now.date_naive();
496 let config = RetentionConfig {
497 keep_weekly: 4,
498 ..zero_config()
499 };
500
501 let files = vec![
503 make_file_info("file1.txt", today),
504 make_file_info("file2.txt", today - chrono::Duration::weeks(1)),
505 make_file_info("file3.txt", today - chrono::Duration::weeks(2)),
506 make_file_info(
507 "file4.txt",
508 today - chrono::Duration::weeks(2) + chrono::Duration::days(1),
509 ), ];
511
512 let keep = select_files_to_keep_with_datetime(&files, &config, now);
513 assert_eq!(keep.len(), 3);
514 }
515
516 #[test]
517 fn test_keep_one_per_month() {
518 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
519 let config = RetentionConfig {
520 keep_monthly: 6,
521 ..zero_config()
522 };
523
524 let files = vec![
527 make_file_info("file1.txt", NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()),
528 make_file_info("file2.txt", NaiveDate::from_ymd_opt(2024, 5, 10).unwrap()), make_file_info("file3.txt", NaiveDate::from_ymd_opt(2024, 5, 5).unwrap()), make_file_info("file4.txt", NaiveDate::from_ymd_opt(2024, 4, 20).unwrap()),
531 ];
532
533 let keep = select_files_to_keep_with_datetime(&files, &config, now);
534 assert_eq!(keep.len(), 3);
535 assert!(keep.contains(&0)); assert!(keep.contains(&2)); assert!(!keep.contains(&1)); assert!(keep.contains(&3)); }
540
541 #[test]
542 fn test_keep_one_per_year() {
543 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
544 let config = RetentionConfig {
545 keep_yearly: 5,
546 ..zero_config()
547 };
548
549 let files = vec![
552 make_file_info("file1.txt", NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()),
553 make_file_info("file2.txt", NaiveDate::from_ymd_opt(2023, 3, 10).unwrap()), make_file_info("file3.txt", NaiveDate::from_ymd_opt(2023, 1, 5).unwrap()), make_file_info("file4.txt", NaiveDate::from_ymd_opt(2022, 12, 20).unwrap()),
556 ];
557
558 let keep = select_files_to_keep_with_datetime(&files, &config, now);
559 assert_eq!(keep.len(), 3);
560 assert!(keep.contains(&0)); assert!(keep.contains(&2)); assert!(!keep.contains(&1)); assert!(keep.contains(&3)); }
565
566 #[test]
567 fn test_empty_files() {
568 let now = Local::now();
569 let config = RetentionConfig::default();
570 let files: Vec<FileInfo> = vec![];
571
572 let keep = select_files_to_keep_with_datetime(&files, &config, now);
573 assert!(keep.is_empty());
574 }
575
576 #[test]
577 fn test_files_outside_retention_window() {
578 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
579 let config = RetentionConfig {
580 keep_daily: 5,
581 ..zero_config()
582 };
583
584 let files = vec![make_file_info(
586 "old_file.txt",
587 now.date_naive() - chrono::Duration::days(10),
588 )];
589
590 let keep = select_files_to_keep_with_datetime(&files, &config, now);
591 assert!(keep.is_empty());
592 }
593
594 #[test]
595 fn test_combined_retention_policies() {
596 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
597 let config = RetentionConfig {
598 keep_last: 2,
599 keep_hourly: 3,
600 keep_daily: 3,
601 keep_weekly: 2,
602 keep_monthly: 2,
603 keep_yearly: 1,
604 };
605
606 let files = vec![
607 make_file_info_with_time("file1.txt", now),
608 make_file_info_with_time("file2.txt", now - chrono::Duration::hours(1)),
609 make_file_info_with_time("file3.txt", now - chrono::Duration::days(1)),
610 make_file_info_with_time("file4.txt", now - chrono::Duration::days(10)),
611 make_file_info_with_time("file5.txt", now - chrono::Duration::days(40)),
612 ];
613
614 let keep = select_files_to_keep_with_datetime(&files, &config, now);
615 assert_eq!(keep.len(), 5); }
617
618 #[test]
619 fn test_keep_last_more_than_files() {
620 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
621 let config = RetentionConfig {
622 keep_last: 100,
623 ..zero_config()
624 };
625
626 let files = vec![
627 make_file_info("file1.txt", now.date_naive()),
628 make_file_info("file2.txt", now.date_naive() - chrono::Duration::days(1)),
629 ];
630
631 let keep = select_files_to_keep_with_datetime(&files, &config, now);
632 assert_eq!(keep.len(), 2);
633 }
634
635 #[test]
636 fn test_iso_week_year_boundary() {
637 let date = NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
640 let (year, week) = get_week_key(date);
641 assert_eq!(year, 2025);
643 assert_eq!(week, 1);
644 }
645
646 #[test]
651 fn test_cascading_keep_last_excludes_from_hourly() {
652 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 30, 0).unwrap();
654 let config = RetentionConfig {
655 keep_last: 2,
656 keep_hourly: 2,
657 ..zero_config()
658 };
659
660 let files = vec![
662 make_file_info_with_time("file0.txt", now),
663 make_file_info_with_time("file1.txt", now - chrono::Duration::minutes(10)),
664 make_file_info_with_time("file2.txt", now - chrono::Duration::minutes(20)),
665 make_file_info_with_time("file3.txt", now - chrono::Duration::hours(1)),
666 ];
667
668 let keep = select_files_to_keep_with_datetime(&files, &config, now);
669
670 assert_eq!(keep.len(), 4);
671 assert!(keep.contains(&0)); assert!(keep.contains(&1)); assert!(keep.contains(&2)); assert!(keep.contains(&3)); }
676
677 #[test]
678 fn test_cascading_hourly_excludes_from_daily() {
679 let now = Local.with_ymd_and_hms(2024, 6, 15, 14, 0, 0).unwrap();
681 let config = RetentionConfig {
682 keep_hourly: 2,
683 keep_daily: 2,
684 ..zero_config()
685 };
686
687 let files = vec![
688 make_file_info_with_time("file0.txt", now),
689 make_file_info_with_time("file1.txt", now - chrono::Duration::hours(1)),
690 make_file_info_with_time("file2.txt", now - chrono::Duration::days(1)),
691 ];
692
693 let keep = select_files_to_keep_with_datetime(&files, &config, now);
694
695 assert_eq!(keep.len(), 3);
696 assert!(keep.contains(&0)); assert!(keep.contains(&1)); assert!(keep.contains(&2)); }
700
701 #[test]
702 fn test_cascading_daily_excludes_from_weekly() {
703 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap(); let config = RetentionConfig {
706 keep_daily: 3,
707 keep_weekly: 2,
708 ..zero_config()
709 };
710
711 let files = vec![
712 make_file_info("file0.txt", now.date_naive()),
713 make_file_info("file1.txt", now.date_naive() - chrono::Duration::days(1)),
714 make_file_info("file2.txt", now.date_naive() - chrono::Duration::days(2)),
715 make_file_info("file3.txt", now.date_naive() - chrono::Duration::weeks(1)),
716 ];
717
718 let keep = select_files_to_keep_with_datetime(&files, &config, now);
719
720 assert_eq!(keep.len(), 4);
721 assert!(keep.contains(&3)); }
723
724 #[test]
725 fn test_cascading_weekly_excludes_from_monthly() {
726 let now = Local.with_ymd_and_hms(2024, 6, 28, 12, 0, 0).unwrap();
728 let config = RetentionConfig {
729 keep_weekly: 2,
730 keep_monthly: 3,
731 ..zero_config()
732 };
733
734 let files = vec![
735 make_file_info("file0.txt", NaiveDate::from_ymd_opt(2024, 6, 28).unwrap()),
736 make_file_info("file1.txt", NaiveDate::from_ymd_opt(2024, 6, 21).unwrap()),
737 make_file_info("file2.txt", NaiveDate::from_ymd_opt(2024, 5, 15).unwrap()),
738 ];
739
740 let keep = select_files_to_keep_with_datetime(&files, &config, now);
741
742 assert_eq!(keep.len(), 3);
743 assert!(keep.contains(&2)); }
745
746 #[test]
747 fn test_cascading_monthly_excludes_from_yearly() {
748 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
750 let config = RetentionConfig {
751 keep_monthly: 2,
752 keep_yearly: 3,
753 ..zero_config()
754 };
755
756 let files = vec![
757 make_file_info("file0.txt", NaiveDate::from_ymd_opt(2024, 6, 15).unwrap()),
758 make_file_info("file1.txt", NaiveDate::from_ymd_opt(2024, 5, 15).unwrap()),
759 make_file_info("file2.txt", NaiveDate::from_ymd_opt(2023, 12, 1).unwrap()),
760 ];
761
762 let keep = select_files_to_keep_with_datetime(&files, &config, now);
763
764 assert_eq!(keep.len(), 3);
765 assert!(keep.contains(&2)); }
767
768 #[test]
769 fn test_cascading_full_chain() {
770 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
772 let config = RetentionConfig {
773 keep_last: 1,
774 keep_hourly: 1,
775 keep_daily: 1,
776 keep_weekly: 1,
777 keep_monthly: 1,
778 keep_yearly: 1,
779 };
780
781 let files = vec![
782 make_file_info_with_time("file0.txt", now),
783 make_file_info_with_time("file1.txt", now - chrono::Duration::minutes(30)),
784 make_file_info_with_time("file2.txt", now - chrono::Duration::hours(5)),
785 make_file_info_with_time("file3.txt", now - chrono::Duration::days(2)),
786 make_file_info_with_time("file4.txt", now - chrono::Duration::weeks(2)),
787 make_file_info("file5.txt", NaiveDate::from_ymd_opt(2023, 7, 15).unwrap()),
788 ];
789
790 let keep = select_files_to_keep_with_datetime(&files, &config, now);
791
792 assert_eq!(keep.len(), 6);
793 assert!(keep.contains(&0)); assert!(keep.contains(&1)); assert!(keep.contains(&2)); assert!(keep.contains(&3)); assert!(keep.contains(&4)); assert!(keep.contains(&5)); }
800
801 #[test]
802 fn test_cascading_same_period_multiple_files() {
803 let now = Local.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
805 let config = RetentionConfig {
806 keep_last: 1,
807 keep_daily: 2,
808 ..zero_config()
809 };
810
811 let files = vec![
812 make_file_info_with_time("file0.txt", now),
813 make_file_info_with_time("file1.txt", now - chrono::Duration::hours(2)),
814 make_file_info_with_time("file2.txt", now - chrono::Duration::hours(4)),
815 make_file_info_with_time("file3.txt", now - chrono::Duration::days(1)),
816 ];
817
818 let keep = select_files_to_keep_with_datetime(&files, &config, now);
819
820 assert_eq!(keep.len(), 3);
821 assert!(keep.contains(&0)); assert!(keep.contains(&2)); assert!(!keep.contains(&1)); assert!(keep.contains(&3)); }
826}