1use anyhow::{Result, bail};
14use clap::ValueEnum;
15use futures_util::stream::{self, StreamExt};
16
17use crate::api::SmugMugClient;
18use crate::api::albums::{AlbumSettingsUpdate, ChildNode};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
22pub enum FolderSort {
23 Name,
24 DateAdded,
25 DateModified,
26 Manual,
28}
29
30impl FolderSort {
31 fn api_name(self) -> &'static str {
32 match self {
33 FolderSort::Name => "Name",
34 FolderSort::DateAdded => "DateAdded",
35 FolderSort::DateModified => "DateModified",
36 FolderSort::Manual => "SortIndex",
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
43pub enum AlbumSort {
44 Manual,
46 Caption,
47 Filename,
48 DateUploaded,
49 DateModified,
50 DateTaken,
51}
52
53impl AlbumSort {
54 fn api_name(self) -> &'static str {
55 match self {
56 AlbumSort::Manual => "Position",
57 AlbumSort::Caption => "Caption",
58 AlbumSort::Filename => "Filename",
59 AlbumSort::DateUploaded => "Date Uploaded",
60 AlbumSort::DateModified => "Date Modified",
61 AlbumSort::DateTaken => "Date Taken",
62 }
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
67pub enum Direction {
68 #[value(alias = "ascending")]
69 Asc,
70 #[value(alias = "descending")]
71 Desc,
72}
73
74impl Direction {
75 fn api_name(self) -> &'static str {
76 match self {
77 Direction::Asc => "Ascending",
78 Direction::Desc => "Descending",
79 }
80 }
81}
82
83pub struct SortOptions {
84 pub path: String,
87 pub folders: Option<FolderSort>,
88 pub albums: Option<AlbumSort>,
89 pub direction: Direction,
90 pub recursive: bool,
92 pub dry_run: bool,
94}
95
96#[derive(Debug, Default)]
98pub struct SortSummary {
99 pub folders_changed: usize,
100 pub folders_already: usize,
101 pub albums_changed: usize,
102 pub albums_already: usize,
103 pub examples: Vec<String>,
105 pub failed: Vec<String>,
106}
107
108const EXAMPLES: usize = 10;
109
110#[allow(async_fn_in_trait)]
114pub trait SortBackend {
115 async fn root(&self) -> Result<String>;
117 async fn children(&self, node_uri: &str) -> Result<Vec<ChildNode>>;
118 async fn album_sort(&self, album_key: &str) -> Result<(String, String)>;
120 async fn set_folder_sort(&self, node_uri: &str, method: &str, direction: &str) -> Result<()>;
121 async fn set_album_sort(&self, album_key: &str, method: &str, direction: &str) -> Result<()>;
122}
123
124impl SortBackend for SmugMugClient {
125 async fn root(&self) -> Result<String> {
126 Ok(self.auth_user().await?.root_node_uri)
127 }
128 async fn children(&self, node_uri: &str) -> Result<Vec<ChildNode>> {
129 self.list_children(node_uri).await
130 }
131 async fn album_sort(&self, album_key: &str) -> Result<(String, String)> {
132 self.get_album_sort(album_key).await
133 }
134 async fn set_folder_sort(&self, node_uri: &str, method: &str, direction: &str) -> Result<()> {
135 SmugMugClient::set_folder_sort(self, node_uri, method, direction).await
136 }
137 async fn set_album_sort(&self, album_key: &str, method: &str, direction: &str) -> Result<()> {
138 self.update_album_settings(
139 album_key,
140 AlbumSettingsUpdate {
141 privacy: None,
142 description: None,
143 keywords: None,
144 sort_method: Some(method.to_string()),
145 sort_direction: Some(direction.to_string()),
146 },
147 )
148 .await
149 }
150}
151
152enum Item {
154 Folder {
155 path: String,
156 uri: String,
157 current: (String, String),
158 },
159 Album {
160 path: String,
161 key: String,
162 },
163}
164
165async fn find<B: SortBackend>(backend: &B, path: &str) -> Result<(ChildNode, String)> {
167 let parts: Vec<&str> = path.split('/').filter(|p| !p.is_empty()).collect();
168 if parts.is_empty() {
169 bail!("Give the folder or album to sort, e.g. Backup or Backup/2014/07");
170 }
171 let mut uri = backend.root().await?;
172 let mut shown = String::new();
173 for (i, part) in parts.iter().enumerate() {
174 let last = i == parts.len() - 1;
175 let matches: Vec<ChildNode> = backend
176 .children(&uri)
177 .await?
178 .into_iter()
179 .filter(|c| {
180 c.name == *part && (c.node_type == "Folder" || (last && c.node_type == "Album"))
181 })
182 .collect();
183 if !shown.is_empty() {
184 shown.push('/');
185 }
186 shown.push_str(part);
187 match matches.len() {
188 1 => {
189 let node = matches.into_iter().next().unwrap();
190 if last {
191 return Ok((node, shown));
192 }
193 uri = node.uri;
194 }
195 0 => bail!(
196 "No folder{} named '{}' at '{}'",
197 if last { " or album" } else { "" },
198 part,
199 shown
200 ),
201 n => bail!(
202 "{} folders and albums are named '{}' at '{}'; rename one first",
203 n,
204 part,
205 shown
206 ),
207 }
208 }
209 unreachable!("the loop returns on its last part")
210}
211
212fn folder_item(node: &ChildNode, path: String) -> Item {
213 Item::Folder {
214 path,
215 uri: node.uri.clone(),
216 current: (
217 node.sort_method.clone().unwrap_or_default(),
218 node.sort_direction.clone().unwrap_or_default(),
219 ),
220 }
221}
222
223fn album_item(node: &ChildNode, path: String) -> Option<Item> {
224 node.album().map(|album| Item::Album {
225 path,
226 key: album.album_key,
227 })
228}
229
230async fn collect<B: SortBackend>(
232 backend: &B,
233 options: &SortOptions,
234) -> Result<(Vec<Item>, Vec<String>)> {
235 let (target, shown) = find(backend, &options.path).await?;
236 let is_folder = target.node_type == "Folder";
237
238 if is_folder && !options.recursive && options.folders.is_none() {
239 bail!(
240 "'{}' is a folder: give --folders-by to set how it lists its contents (or --recursive to reach the albums inside)",
241 shown
242 );
243 }
244 if !is_folder && options.albums.is_none() {
245 bail!(
246 "'{}' is an album: give --albums-by to set how it lists its photos",
247 shown
248 );
249 }
250
251 let mut items = Vec::new();
252 let mut failed = Vec::new();
253 if is_folder {
254 if options.folders.is_some() {
255 items.push(folder_item(&target, shown.clone()));
256 }
257 if options.recursive {
258 let mut pending = vec![(target.uri.clone(), shown)];
259 while let Some((uri, path)) = pending.pop() {
260 let children = match backend.children(&uri).await {
263 Ok(children) => children,
264 Err(e) => {
265 failed.push(format!("{}: couldn't list what's in it: {:#}", path, e));
266 continue;
267 }
268 };
269 for child in children {
270 let child_path = format!("{}/{}", path, child.name);
271 match child.node_type.as_str() {
272 "Folder" => {
273 if options.folders.is_some() {
274 items.push(folder_item(&child, child_path.clone()));
275 }
276 pending.push((child.uri.clone(), child_path));
277 }
278 "Album" if options.albums.is_some() => {
279 items.extend(album_item(&child, child_path));
280 }
281 _ => {}
282 }
283 }
284 }
285 }
286 } else if let Some(item) = album_item(&target, shown) {
287 items.push(item);
288 }
289 Ok((items, failed))
290}
291
292enum Outcome {
293 Changed(String),
294 Already,
295 Failed(String),
296}
297
298const CONCURRENCY: usize = 8;
300
301async fn apply<B: SortBackend>(backend: &B, item: Item, options: &SortOptions) -> (bool, Outcome) {
302 let direction = options.direction.api_name();
303 match item {
304 Item::Folder { path, uri, current } => {
305 let method = options
306 .folders
307 .expect("folders are only collected with --folders-by")
308 .api_name();
309 if current.0 == method && current.1 == direction {
310 return (true, Outcome::Already);
311 }
312 if options.dry_run {
313 return (true, Outcome::Changed(path));
314 }
315 match backend.set_folder_sort(&uri, method, direction).await {
316 Ok(()) => (true, Outcome::Changed(path)),
317 Err(e) => (true, Outcome::Failed(format!("{}: {:#}", path, e))),
318 }
319 }
320 Item::Album { path, key } => {
321 let method = options
322 .albums
323 .expect("albums are only collected with --albums-by")
324 .api_name();
325 let current = match backend.album_sort(&key).await {
326 Ok(current) => current,
327 Err(e) => return (false, Outcome::Failed(format!("{}: {:#}", path, e))),
328 };
329 if current.0 == method && current.1 == direction {
330 return (false, Outcome::Already);
331 }
332 if options.dry_run {
333 return (false, Outcome::Changed(path));
334 }
335 match backend.set_album_sort(&key, method, direction).await {
336 Ok(()) => (false, Outcome::Changed(path)),
337 Err(e) => (false, Outcome::Failed(format!("{}: {:#}", path, e))),
338 }
339 }
340 }
341}
342
343pub async fn run<B: SortBackend>(backend: &B, options: &SortOptions) -> Result<SortSummary> {
347 if options.folders.is_none() && options.albums.is_none() {
348 bail!("Say what to sort: --folders-by and/or --albums-by");
349 }
350 let (items, listing_failures) = collect(backend, options).await?;
351 let total = items.len();
352 let mut summary = SortSummary {
353 failed: listing_failures,
354 ..SortSummary::default()
355 };
356 let mut done = 0;
357
358 let mut results = stream::iter(items)
359 .map(|item| apply(backend, item, options))
360 .buffer_unordered(CONCURRENCY);
361 while let Some((is_folder, outcome)) = results.next().await {
362 done += 1;
363 match outcome {
364 Outcome::Changed(path) => {
365 if is_folder {
366 summary.folders_changed += 1;
367 } else {
368 summary.albums_changed += 1;
369 }
370 if summary.examples.len() < EXAMPLES {
371 summary.examples.push(path);
372 }
373 }
374 Outcome::Already if is_folder => summary.folders_already += 1,
375 Outcome::Already => summary.albums_already += 1,
376 Outcome::Failed(message) => summary.failed.push(message),
377 }
378 if !options.dry_run && done % 100 == 0 && done < total {
379 println!(" {}/{} done...", done, total);
380 }
381 }
382 Ok(summary)
383}
384
385pub fn print_summary(summary: &SortSummary, options: &SortOptions) {
387 let verb = if options.dry_run {
388 "Would change"
389 } else {
390 "Changed"
391 };
392 let mut parts = Vec::new();
393 if options.folders.is_some() {
394 parts.push(format!(
395 "{} {} folder{} ({} already right)",
396 verb,
397 summary.folders_changed,
398 if summary.folders_changed == 1 {
399 ""
400 } else {
401 "s"
402 },
403 summary.folders_already
404 ));
405 }
406 if options.albums.is_some() {
407 parts.push(format!(
408 "{} {} album{} ({} already right)",
409 verb,
410 summary.albums_changed,
411 if summary.albums_changed == 1 { "" } else { "s" },
412 summary.albums_already
413 ));
414 }
415 println!();
416 for part in parts {
417 println!("{}", part);
418 }
419 if options.dry_run && !summary.examples.is_empty() {
420 let shown = summary.examples.len();
421 let total = summary.folders_changed + summary.albums_changed;
422 println!("\nFor example:");
423 for path in &summary.examples {
424 println!(" {}", path);
425 }
426 if total > shown {
427 println!(" ... and {} more", total - shown);
428 }
429 }
430 if !summary.failed.is_empty() {
431 println!(
432 "\n{} couldn't be updated (run again to retry):",
433 summary.failed.len()
434 );
435 for failure in summary.failed.iter().take(20) {
436 println!(" {}", failure);
437 }
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::api::albums::{ChildNodeUris, UriRef};
445 use std::collections::HashMap;
446 use std::sync::Mutex;
447
448 #[derive(Default)]
450 struct Fake {
451 children: Mutex<HashMap<String, Vec<ChildNode>>>,
452 albums: Mutex<HashMap<String, (String, String)>>,
453 fail: Mutex<Vec<String>>,
455 fail_listing: Mutex<Vec<String>>,
457 sets: Mutex<Vec<String>>,
458 next: Mutex<u32>,
459 }
460
461 impl Fake {
462 fn new() -> Self {
463 let fake = Fake::default();
464 fake.children
465 .lock()
466 .unwrap()
467 .insert("/node/root".into(), Vec::new());
468 fake
469 }
470
471 fn id(&self) -> u32 {
472 let mut n = self.next.lock().unwrap();
473 *n += 1;
474 *n
475 }
476
477 fn folder(&self, parent: &str, name: &str, sort: (&str, &str)) -> String {
478 let uri = format!("/node/f{}", self.id());
479 self.children
480 .lock()
481 .unwrap()
482 .get_mut(parent)
483 .unwrap()
484 .push(ChildNode {
485 name: name.into(),
486 node_type: "Folder".into(),
487 uri: uri.clone(),
488 node_id: String::new(),
489 url_name: String::new(),
490 web_uri: None,
491 uris: None,
492 sort_method: Some(sort.0.into()),
493 sort_direction: Some(sort.1.into()),
494 });
495 self.children
496 .lock()
497 .unwrap()
498 .insert(uri.clone(), Vec::new());
499 uri
500 }
501
502 fn album(&self, parent: &str, name: &str, sort: (&str, &str)) -> String {
503 let key = format!("A{}", self.id());
504 self.children
505 .lock()
506 .unwrap()
507 .get_mut(parent)
508 .unwrap()
509 .push(ChildNode {
510 name: name.into(),
511 node_type: "Album".into(),
512 uri: format!("/node/{}", key),
513 node_id: String::new(),
514 url_name: String::new(),
515 web_uri: None,
516 uris: Some(ChildNodeUris {
517 album: Some(UriRef {
518 uri: format!("/api/v2/album/{}", key),
519 }),
520 }),
521 sort_method: Some("Name".into()),
523 sort_direction: Some("Ascending".into()),
524 });
525 self.albums
526 .lock()
527 .unwrap()
528 .insert(key.clone(), (sort.0.into(), sort.1.into()));
529 key
530 }
531
532 fn folder_sort(&self, uri: &str) -> (String, String) {
533 for list in self.children.lock().unwrap().values() {
534 if let Some(c) = list.iter().find(|c| c.uri == uri) {
535 return (
536 c.sort_method.clone().unwrap(),
537 c.sort_direction.clone().unwrap(),
538 );
539 }
540 }
541 panic!("no node {}", uri);
542 }
543
544 fn album_sort_of(&self, key: &str) -> (String, String) {
545 self.albums.lock().unwrap()[key].clone()
546 }
547
548 fn set_count(&self) -> usize {
549 self.sets.lock().unwrap().len()
550 }
551
552 fn fails_for(&self, id: &str) {
553 self.fail.lock().unwrap().push(id.into());
554 }
555 }
556
557 impl SortBackend for Fake {
558 async fn root(&self) -> Result<String> {
559 Ok("/node/root".into())
560 }
561 async fn children(&self, node_uri: &str) -> Result<Vec<ChildNode>> {
562 if self
563 .fail_listing
564 .lock()
565 .unwrap()
566 .iter()
567 .any(|f| f == node_uri)
568 {
569 bail!("503");
570 }
571 Ok(self.children.lock().unwrap()[node_uri].clone())
572 }
573 async fn album_sort(&self, key: &str) -> Result<(String, String)> {
574 Ok(self.album_sort_of(key))
575 }
576 async fn set_folder_sort(&self, uri: &str, method: &str, direction: &str) -> Result<()> {
577 if self.fail.lock().unwrap().iter().any(|f| f == uri) {
578 bail!("503");
579 }
580 self.sets.lock().unwrap().push(uri.into());
581 for list in self.children.lock().unwrap().values_mut() {
582 if let Some(c) = list.iter_mut().find(|c| c.uri == uri) {
583 c.sort_method = Some(method.into());
584 c.sort_direction = Some(direction.into());
585 }
586 }
587 Ok(())
588 }
589 async fn set_album_sort(&self, key: &str, method: &str, direction: &str) -> Result<()> {
590 if self.fail.lock().unwrap().iter().any(|f| f == key) {
591 bail!("503");
592 }
593 self.sets.lock().unwrap().push(key.into());
594 self.albums
595 .lock()
596 .unwrap()
597 .insert(key.into(), (method.into(), direction.into()));
598 Ok(())
599 }
600 }
601
602 const DEFAULT: (&str, &str) = ("DateModified", "Descending");
603 const NAME_ASC: (&str, &str) = ("Name", "Ascending");
604
605 fn options(path: &str) -> SortOptions {
606 SortOptions {
607 path: path.into(),
608 folders: None,
609 albums: None,
610 direction: Direction::Asc,
611 recursive: false,
612 dry_run: false,
613 }
614 }
615
616 fn account() -> (Fake, Tree) {
619 let fake = Fake::new();
620 let backup = fake.folder("/node/root", "Backup", NAME_ASC);
621 let y2014 = fake.folder(&backup, "2014", DEFAULT);
622 let m07 = fake.folder(&y2014, "07", DEFAULT);
623 let m08 = fake.folder(&y2014, "08", DEFAULT);
624 let y2015 = fake.folder(&backup, "2015", DEFAULT);
625 let day = fake.album(&m07, "2014-07-12", ("Date Taken", "Ascending"));
626 let other = fake.folder("/node/root", "Other", DEFAULT);
627 (
628 fake,
629 Tree {
630 backup,
631 y2014,
632 m07,
633 m08,
634 y2015,
635 day,
636 other,
637 },
638 )
639 }
640
641 struct Tree {
642 backup: String,
643 y2014: String,
644 m07: String,
645 m08: String,
646 y2015: String,
647 day: String,
648 other: String,
649 }
650
651 #[tokio::test]
652 async fn recursive_folder_sort_changes_folders_below_and_nothing_else() {
653 let (fake, t) = account();
654 let mut o = options("Backup");
655 o.folders = Some(FolderSort::Name);
656 o.recursive = true;
657 let summary = run(&fake, &o).await.unwrap();
658
659 assert_eq!((summary.folders_changed, summary.folders_already), (4, 1));
661 for uri in [&t.y2014, &t.m07, &t.m08, &t.y2015] {
662 assert_eq!(fake.folder_sort(uri), ("Name".into(), "Ascending".into()));
663 }
664 assert_eq!(
666 fake.folder_sort(&t.other),
667 (DEFAULT.0.into(), DEFAULT.1.into())
668 );
669 assert_eq!(
670 fake.album_sort_of(&t.day),
671 ("Date Taken".into(), "Ascending".into())
672 );
673 assert_eq!(summary.albums_changed + summary.albums_already, 0);
674 assert!(summary.failed.is_empty());
675 }
676
677 #[tokio::test]
678 async fn repeating_a_run_changes_nothing() {
679 let (fake, _) = account();
680 let mut o = options("Backup");
681 o.folders = Some(FolderSort::Name);
682 o.recursive = true;
683 run(&fake, &o).await.unwrap();
684 let sets = fake.set_count();
685
686 let again = run(&fake, &o).await.unwrap();
687 assert_eq!((again.folders_changed, again.folders_already), (0, 5));
688 assert_eq!(fake.set_count(), sets);
689 }
690
691 #[tokio::test]
692 async fn without_recursive_only_the_named_folder_changes() {
693 let (fake, t) = account();
694 let mut o = options("Backup/2014");
695 o.folders = Some(FolderSort::DateAdded);
696 o.direction = Direction::Desc;
697 let summary = run(&fake, &o).await.unwrap();
698 assert_eq!(summary.folders_changed, 1);
699 assert_eq!(
700 fake.folder_sort(&t.y2014),
701 ("DateAdded".into(), "Descending".into())
702 );
703 assert_eq!(
704 fake.folder_sort(&t.m07),
705 (DEFAULT.0.into(), DEFAULT.1.into())
706 );
707 }
708
709 #[tokio::test]
710 async fn albums_are_sorted_by_their_own_values() {
711 let (fake, t) = account();
712 let mut o = options("Backup/2014/07/2014-07-12");
713 o.albums = Some(AlbumSort::Filename);
714 let summary = run(&fake, &o).await.unwrap();
715 assert_eq!(summary.albums_changed, 1);
716 assert_eq!(
717 fake.album_sort_of(&t.day),
718 ("Filename".into(), "Ascending".into())
719 );
720
721 let mut o = options("Backup");
723 o.albums = Some(AlbumSort::DateTaken);
724 o.recursive = true;
725 let summary = run(&fake, &o).await.unwrap();
726 assert_eq!((summary.albums_changed, summary.folders_changed), (1, 0));
727 assert_eq!(
728 fake.album_sort_of(&t.day),
729 ("Date Taken".into(), "Ascending".into())
730 );
731 assert_eq!(
732 fake.folder_sort(&t.m07),
733 (DEFAULT.0.into(), DEFAULT.1.into())
734 );
735 }
736
737 #[tokio::test]
738 async fn folders_and_albums_can_be_sorted_in_one_run() {
739 let (fake, t) = account();
740 let mut o = options("Backup");
741 o.folders = Some(FolderSort::Name);
742 o.albums = Some(AlbumSort::Filename);
743 o.direction = Direction::Desc;
744 o.recursive = true;
745 let summary = run(&fake, &o).await.unwrap();
746 assert_eq!((summary.folders_changed, summary.albums_changed), (5, 1));
747 assert_eq!(
748 fake.folder_sort(&t.backup),
749 ("Name".into(), "Descending".into())
750 );
751 assert_eq!(
752 fake.album_sort_of(&t.day),
753 ("Filename".into(), "Descending".into())
754 );
755 }
756
757 #[tokio::test]
758 async fn dry_run_reports_without_changing() {
759 let (fake, t) = account();
760 let mut o = options("Backup");
761 o.folders = Some(FolderSort::Name);
762 o.recursive = true;
763 o.dry_run = true;
764 let summary = run(&fake, &o).await.unwrap();
765 assert_eq!(summary.folders_changed, 4);
766 assert_eq!(fake.set_count(), 0);
767 assert_eq!(
768 fake.folder_sort(&t.y2014),
769 (DEFAULT.0.into(), DEFAULT.1.into())
770 );
771 assert!(summary.examples.contains(&"Backup/2014/07".to_string()));
772 }
773
774 #[tokio::test]
775 async fn one_failure_is_reported_and_the_rest_still_change() {
776 let (fake, t) = account();
777 fake.fails_for(&t.m07);
778 let mut o = options("Backup");
779 o.folders = Some(FolderSort::Name);
780 o.recursive = true;
781 let summary = run(&fake, &o).await.unwrap();
782 assert_eq!(summary.folders_changed, 3);
783 assert_eq!(summary.failed.len(), 1);
784 assert!(
785 summary.failed[0].starts_with("Backup/2014/07:"),
786 "{:?}",
787 summary.failed
788 );
789 assert_eq!(
790 fake.folder_sort(&t.y2015),
791 ("Name".into(), "Ascending".into())
792 );
793 }
794
795 async fn error_for(options: SortOptions) -> String {
797 let (fake, _) = account();
798 run(&fake, &options).await.unwrap_err().to_string()
799 }
800
801 fn folders_by_name(path: &str) -> SortOptions {
802 let mut o = options(path);
803 o.folders = Some(FolderSort::Name);
804 o
805 }
806
807 #[tokio::test]
808 async fn a_folder_that_cant_be_listed_is_reported_and_the_rest_still_done() {
809 let (fake, t) = account();
810 fake.fail_listing.lock().unwrap().push(t.y2014.clone());
811 let mut o = options("Backup");
812 o.folders = Some(FolderSort::Name);
813 o.recursive = true;
814 let summary = run(&fake, &o).await.unwrap();
815
816 assert_eq!(summary.folders_changed, 2);
818 assert_eq!(
819 fake.folder_sort(&t.y2014),
820 ("Name".into(), "Ascending".into())
821 );
822 assert_eq!(
823 fake.folder_sort(&t.y2015),
824 ("Name".into(), "Ascending".into())
825 );
826 assert_eq!(
827 fake.folder_sort(&t.m07),
828 (DEFAULT.0.into(), DEFAULT.1.into())
829 );
830 assert_eq!(summary.failed.len(), 1);
831 assert!(
832 summary.failed[0].starts_with("Backup/2014: couldn't list"),
833 "{:?}",
834 summary.failed
835 );
836 }
837
838 #[tokio::test]
839 async fn asks_for_what_it_needs() {
840 assert!(error_for(options("Backup")).await.contains("--folders-by"));
842
843 let mut o = options("Backup");
845 o.albums = Some(AlbumSort::DateTaken);
846 assert!(error_for(o).await.contains("is a folder"));
847
848 let err = error_for(folders_by_name("Backup/2014/07/2014-07-12")).await;
850 assert!(err.contains("is an album"), "{}", err);
851
852 let err = error_for(folders_by_name("")).await;
854 assert!(err.contains("Give the folder or album"), "{}", err);
855 let err = error_for(folders_by_name("Backup/1999")).await;
856 assert!(err.contains("No folder or album named '1999'"), "{}", err);
857 let err = error_for(folders_by_name("Backup/2014/07/2014-07-12/x")).await;
858 assert!(err.contains("No folder named '2014-07-12'"), "{}", err);
859 }
860
861 #[tokio::test]
862 async fn two_nodes_with_one_name_are_not_guessed_between() {
863 let (fake, t) = account();
864 fake.folder(&t.backup, "2014", DEFAULT);
865 let mut o = options("Backup/2014");
866 o.folders = Some(FolderSort::Name);
867 let err = run(&fake, &o).await.unwrap_err().to_string();
868 assert!(
869 err.contains("2 folders and albums are named '2014'"),
870 "{}",
871 err
872 );
873 }
874
875 #[test]
876 fn api_names() {
877 assert_eq!(FolderSort::Manual.api_name(), "SortIndex");
878 assert_eq!(FolderSort::DateModified.api_name(), "DateModified");
879 assert_eq!(AlbumSort::Manual.api_name(), "Position");
880 assert_eq!(AlbumSort::DateTaken.api_name(), "Date Taken");
881 assert_eq!(Direction::Desc.api_name(), "Descending");
882 }
883}