canvas_lms_sync/sync.rs
1use futures::StreamExt;
2use log::{debug, error, warn};
3use std::{collections::HashMap, path::PathBuf};
4use tokio::sync::Mutex;
5
6use crate::{
7 canvas_api::Client,
8 download::Downloader,
9 path::{sanitize_file_name, write_url_file},
10 File,
11};
12
13pub struct IndentStack<T> {
14 stack: Vec<(i64, T)>,
15}
16
17impl<T> IndentStack<T> {
18 pub fn new() -> Self {
19 Self { stack: Vec::new() }
20 }
21 pub fn add(&mut self, indent: i64, item: T) {
22 self.stack.retain(|(i, _)| *i < indent);
23 self.stack.push((indent, item));
24 }
25 pub fn get(&self) -> Vec<&T> {
26 self.stack.iter().map(|(_, item)| item).collect()
27 }
28}
29
30pub struct SyncConfig {
31 pub courseid: i64,
32 pub path: PathBuf,
33}
34
35pub async fn download_modules(config: &SyncConfig, client: &Client, downloader: &Downloader) {
36 client
37 .list_modules(config.courseid)
38 .for_each(|module| async {
39 let indent = Mutex::new(IndentStack::new());
40 match module {
41 Ok(module) => {
42 client
43 .list_module_items(config.courseid, module.id)
44 .for_each(|item| async {
45 match item {
46 Ok(item) => {
47 debug!("Item: {:?}", item);
48 match item.type_.as_str() {
49 "SubHeader" => {
50 indent.lock().await.add(item.indent, item);
51 }
52 "File" => {
53 let mut file = client
54 .get_course_file(
55 config.courseid,
56 item.content_id.expect("No content id"),
57 )
58 .await
59 .unwrap();
60 if file.url == "" {
61 file.url = client.build_url(
62 format!(
63 "/files/{}/download?download_frd=1&verifier={}",
64 file.id, file.uuid
65 )
66 .as_str(),
67 );
68 warn!(
69 "No url for file: {:?}, trying to guess as {}",
70 file.display_name, file.url
71 );
72 }
73 debug!("File: {:?}", file);
74
75 let mut file = File::from(file);
76
77 file.folder_path =
78 vec!["Modules".to_string(), module.name.clone()];
79 file.folder_path.extend(
80 indent
81 .lock()
82 .await
83 .get()
84 .iter()
85 .map(|item| item.title.clone()),
86 );
87
88 if file.local_file_matches().unwrap_or(false) {
89 debug!("File already downloaded: {:?}", file);
90 return;
91 }
92
93 downloader.submit(file.into());
94 }
95 "ExternalUrl" | "ExternalTool" => {
96 if let Some(url) = item.url {
97 let folder_path = PathBuf::from(&config.path)
98 .join("Modules")
99 .join(sanitize_file_name(&module.name))
100 .join(sanitize_file_name(&item.title));
101 write_url_file(
102 &url,
103 &item.title,
104 folder_path.to_str().expect("Invalid path"),
105 )
106 .unwrap();
107 }
108 }
109 _ => {}
110 }
111 }
112 Err(e) => error!("Failed getting module items: {:?}", e),
113 }
114 })
115 .await;
116 }
117 Err(e) => error!("Failed getting modules: {:?}", e),
118 }
119 })
120 .await;
121}
122
123pub async fn download_files(config: &SyncConfig, client: &Client, downloader: &Downloader) {
124 let folders = Mutex::new(HashMap::new());
125
126 client
127 .get_all_folders(config.courseid)
128 .for_each(|folder| async {
129 match folder {
130 Ok(folder) => {
131 folders.lock().await.insert(folder.id, folder.clone());
132 }
133 Err(e) => error!("Failed getting folders: {:?}", e),
134 }
135 })
136 .await;
137
138 let folders = folders.into_inner();
139
140 client
141 .get_all_files(config.courseid)
142 .for_each(|file| async {
143 match file {
144 Ok(mut file) => {
145 if file.url == "" {
146 warn!(
147 "No url for file: {:?}, trying to guess as {}",
148 file.display_name, file.url
149 );
150 file.url = client.build_url(
151 format!(
152 "/files/{}/download?download_frd=1&verifier={}",
153 file.id, file.uuid
154 )
155 .as_str(),
156 );
157 }
158 let folder_id = file.folder_id;
159 let mut file = File::from(file);
160 file.set_folder_path(&folders, folder_id);
161
162 if file.local_file_matches().unwrap_or(false) {
163 debug!("File already exists: {:?}", file);
164 return;
165 }
166
167 downloader.submit(file.into());
168 }
169 Err(e) => error!("Failed getting files: {:?}", e),
170 }
171 })
172 .await;
173}