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}