1#![recursion_limit = "512"]
9mod config;
33mod download;
34mod error;
35mod http;
36mod js;
37mod plan;
38mod state;
39mod upload;
40
41pub use config::{backoff_ms, ClientConfig};
42pub use error::LibfwError;
43pub use plan::FileEntry;
44
45use js_sys::Reflect;
46use wasm_bindgen::prelude::*;
47
48use crate::js::Callbacks;
49use crate::state::{TaskControl, TaskState};
50
51#[wasm_bindgen]
53pub struct LibfwClient {
54 config: ClientConfig,
55 callbacks: Callbacks,
56 control: TaskControl,
57}
58
59#[wasm_bindgen]
60impl LibfwClient {
61 #[wasm_bindgen(constructor)]
66 pub fn new(opts: JsValue) -> LibfwClient {
67 let config = ClientConfig::from_js(&opts);
68 LibfwClient {
69 config: config.clone(),
70 callbacks: Callbacks::new(),
71 control: TaskControl::with_max_parallel(config.concurrency.max(1)),
74 }
75 }
76
77 pub fn set_callbacks(&self, callbacks: JsValue) {
79 self.callbacks.set(callbacks);
80 }
81
82 pub fn download_folder(&self, base_url: &str, token: &str, dir_path: &str) -> js_sys::Promise {
86 let base_url = base_url.to_string();
87 let token = token.to_string();
88 let dir_path = dir_path.to_string();
89 let config = self.config.clone();
90 let callbacks = self.callbacks.clone();
91 let control = self.control.clone();
92
93 wasm_bindgen_futures::future_to_promise(async move {
94 control.reset();
95 control.begin(TaskState::Downloading);
96 match download::download_folder(
97 &base_url,
98 &token,
99 &dir_path,
100 &callbacks,
101 &control,
102 &config,
103 )
104 .await
105 {
106 Ok(total) => {
107 control.complete();
108 Ok(JsValue::from_f64(total as f64))
109 }
110 Err(e) => {
111 control.fail();
112 Err(e.to_js())
113 }
114 }
115 })
116 }
117
118 pub fn download_file(&self, base_url: &str, token: &str, file_path: &str) -> js_sys::Promise {
122 let base_url = base_url.to_string();
123 let token = token.to_string();
124 let file_path = file_path.to_string();
125 let config = self.config.clone();
126 let callbacks = self.callbacks.clone();
127 let control = self.control.clone();
128
129 wasm_bindgen_futures::future_to_promise(async move {
130 control.reset();
131 control.begin(TaskState::Downloading);
132 match download::download_single(
133 &base_url,
134 &token,
135 &file_path,
136 &callbacks,
137 &control,
138 &config,
139 )
140 .await
141 {
142 Ok(total) => {
143 control.complete();
144 Ok(JsValue::from_f64(total as f64))
145 }
146 Err(e) => {
147 control.fail();
148 Err(e.to_js())
149 }
150 }
151 })
152 }
153
154 pub fn upload(&self, base_url: &str, token: &str) -> js_sys::Promise {
158 let base_url = base_url.to_string();
159 let token = token.to_string();
160 let config = self.config.clone();
161 let callbacks = self.callbacks.clone();
162 let control = self.control.clone();
163
164 wasm_bindgen_futures::future_to_promise(async move {
165 control.reset();
166 control.begin(TaskState::Uploading);
167 match upload::upload(&base_url, &token, &callbacks, &control, &config).await {
168 Ok(total) => {
169 control.complete();
170 Ok(JsValue::from_f64(total as f64))
171 }
172 Err(e) => {
173 control.fail();
174 Err(e.to_js())
175 }
176 }
177 })
178 }
179
180 pub fn pause(&self) {
182 self.control.pause();
183 }
184
185 pub fn resume(&self) {
187 self.control.resume();
188 }
189
190 pub fn cancel(&self) {
192 self.control.cancel();
193 }
194
195 pub fn state(&self) -> String {
198 self.control.state().as_str().to_string()
199 }
200
201 pub fn progress(&self) -> f64 {
203 self.control.progress()
204 }
205
206 pub fn done_bytes(&self) -> f64 {
208 self.control.done_bytes() as f64
209 }
210
211 pub fn total_bytes(&self) -> f64 {
213 self.control.total_bytes() as f64
214 }
215
216 pub fn has_callbacks(&self) -> bool {
218 self.callbacks.is_set()
219 }
220}
221
222#[wasm_bindgen]
224pub fn js_option_string(obj: &JsValue, key: &str) -> Option<String> {
225 Reflect::get(obj, &JsValue::from_str(key))
226 .ok()
227 .and_then(|v| v.as_string())
228}
229
230#[cfg(test)]
231mod tests {
232 #[test]
233 #[cfg(target_arch = "wasm32")]
234 fn engine_default_state_is_idle() {
235 let engine = super::LibfwClient::new(wasm_bindgen::JsValue::NULL);
236 assert_eq!(engine.state(), "idle");
237 assert!(!engine.has_callbacks());
238 }
239
240 #[test]
241 #[cfg(target_arch = "wasm32")]
242 fn engine_options_parse() {
243 let opts = js_sys::Object::new();
244 js_sys::Reflect::set(&opts, &wasm_bindgen::JsValue::from_str("concurrency"), &wasm_bindgen::JsValue::from_f64(2.0))
245 .unwrap();
246 js_sys::Reflect::set(&opts, &wasm_bindgen::JsValue::from_str("compress"), &wasm_bindgen::JsValue::FALSE).unwrap();
247 let engine = super::LibfwClient::new(opts.into());
248 assert_eq!(engine.config.concurrency, 2);
249 assert!(!engine.config.compress);
250 }
251
252 #[test]
253 #[cfg(target_arch = "wasm32")]
254 fn state_transitions_via_public_api() {
255 use crate::state::TaskState;
256 let engine = super::LibfwClient::new(wasm_bindgen::JsValue::NULL);
257 engine.control.begin(TaskState::Downloading);
258 engine.pause();
259 assert_eq!(engine.state(), "paused");
260 engine.resume();
261 assert_eq!(engine.state(), "downloading");
262 engine.cancel();
263 assert_eq!(engine.state(), "failed");
264 }
265}