1pub mod date;
11pub mod index;
12pub mod plan;
13pub mod run;
14pub mod scan;
15
16use colored::Colorize;
17
18pub use run::{RunOptions, RunStats, StopFlag, human_bytes, run};
19
20pub fn print_summary(stats: &RunStats, options: &RunOptions) {
22 let dry = options.dry_run;
23 println!();
24 println!(
25 "{} {} files found ({} unchanged since last time{})",
26 "•".cyan(),
27 stats.scanned,
28 stats.unchanged,
29 if stats.previously_refused > 0 {
30 format!(
31 ", {} of them refused by SmugMug before",
32 stats.previously_refused
33 )
34 } else {
35 String::new()
36 }
37 );
38 if stats.unsupported > 0 {
39 println!(
40 "{} {} files of types SmugMug doesn't take were ignored",
41 "•".cyan(),
42 stats.unsupported
43 );
44 }
45 if stats.raw_skipped_with_sibling > 0 {
46 println!(
47 "{} {} RAW files skipped: a JPEG or HEIC of the same name is next to them",
48 "•".cyan(),
49 stats.raw_skipped_with_sibling
50 );
51 }
52 if stats.raw_skipped > 0 {
53 println!(
54 "{} {} RAW files skipped (raw_mode)",
55 "•".cyan(),
56 stats.raw_skipped
57 );
58 }
59 if stats.live_photo_videos_skipped > 0 {
60 println!(
61 "{} {} Live Photo videos skipped",
62 "•".cyan(),
63 stats.live_photo_videos_skipped
64 );
65 }
66 if !stats.scan_errors.is_empty() {
67 println!(
68 "{}",
69 format!("⚠ {} paths couldn't be read:", stats.scan_errors.len()).yellow()
70 );
71 for e in stats.scan_errors.iter().take(10) {
72 println!(" {}", e);
73 }
74 }
75
76 if stats.to_process == 0 {
77 println!("{} Nothing new to upload", "✓".green());
78 return;
79 }
80
81 if dry {
82 println!(
83 "\n{} new or changed files ({}) would go into {}/YYYY/MM/YYYY-MM-DD",
84 stats.to_process,
85 human_bytes(stats.bytes_to_process),
86 options.root_folder
87 );
88 print_dates(stats);
89 println!(
90 "\n{}",
91 "Dry run: nothing was read in full or uploaded. Copies of files already on SmugMug will be linked rather than uploaded."
92 .bright_black()
93 );
94 return;
95 }
96
97 println!();
98 println!(" {}: {}", "Uploaded".green(), stats.uploaded);
99 if stats.replaced > 0 {
100 println!(" {}: {}", "Replaced (edited)".cyan(), stats.replaced);
101 }
102 if stats.linked > 0 {
103 println!(" {}: {}", "Already there (copies)".cyan(), stats.linked);
104 }
105 if stats.collected > 0 {
106 println!(
107 " {}: {}",
108 "Added from other albums".cyan(),
109 stats.collected
110 );
111 }
112 if stats.touched > 0 {
113 println!(
114 " {}: {}",
115 "Touched, unchanged content".cyan(),
116 stats.touched
117 );
118 }
119 if stats.refused > 0 {
120 println!(" {}: {}", "Refused by SmugMug".red(), stats.refused);
121 }
122 println!(" {}: {}", "Failed (retried next run)".red(), stats.failed);
123 println!(" Data sent: {}", human_bytes(stats.bytes_uploaded));
124 if stats.albums_created > 0 {
125 println!(" Albums created: {}", stats.albums_created);
126 }
127 print_dates(stats);
128 if !stats.errors.is_empty() {
129 println!("\n{}", "Problems:".yellow());
130 for e in &stats.errors {
131 println!(" {}", e);
132 }
133 }
134 if stats.interrupted {
135 println!(
136 "{}",
137 "Stopped early; the rest will be done next run.".yellow()
138 );
139 }
140}
141
142fn print_dates(stats: &RunStats) {
143 if !stats.date_sources.is_empty() {
144 println!(" Dated by:");
145 for (source, count) in &stats.date_sources {
146 println!(" {:>8} {}", count, source);
147 }
148 }
149 let mut years: std::collections::BTreeMap<i32, usize> = Default::default();
150 for (date, count) in &stats.days {
151 *years.entry(chrono::Datelike::year(date)).or_default() += count;
152 }
153 if years.len() > 1 {
154 println!(" By year:");
155 for (year, count) in &years {
156 println!(" {:>8} {}", count, year);
157 }
158 }
159 let mut busiest: Vec<_> = stats.days.iter().collect();
160 busiest.sort_by(|a, b| b.1.cmp(a.1));
161 let big: Vec<_> = busiest.iter().take(5).filter(|(_, n)| **n > 500).collect();
162 if !big.is_empty() {
163 println!(" Busiest days:");
164 for (date, count) in big {
165 let note = if **count as u64 > crate::uploader::album_series::MAX_ALBUM_IMAGES {
166 " (more than one album)"
167 } else {
168 ""
169 };
170 println!(" {:>8} {}{}", count, date, note);
171 }
172 }
173}
174
175pub fn stop_on_signal() -> StopFlag {
179 let stop: StopFlag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
180 let flag = stop.clone();
181 tokio::spawn(async move {
182 loop {
183 wait_for_signal().await;
184 if flag.swap(true, std::sync::atomic::Ordering::Relaxed) {
185 eprintln!("Stopping now.");
186 std::process::exit(130);
187 }
188 eprintln!(
189 "{}",
190 "Stopping after the uploads in progress (Ctrl-C again to quit now)...".yellow()
191 );
192 }
193 });
194 stop
195}
196
197async fn wait_for_signal() {
198 #[cfg(unix)]
199 {
200 use tokio::signal::unix::{SignalKind, signal};
201 match signal(SignalKind::terminate()) {
202 Ok(mut term) => {
203 tokio::select! {
204 _ = tokio::signal::ctrl_c() => {}
205 _ = term.recv() => {}
206 }
207 }
208 Err(_) => {
209 let _ = tokio::signal::ctrl_c().await;
210 }
211 }
212 }
213 #[cfg(not(unix))]
214 {
215 let _ = tokio::signal::ctrl_c().await;
216 }
217}