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)]
65 pub fn new(opts: JsValue) -> LibfwClient {
66 let config = ClientConfig::from_js(&opts);
67 LibfwClient {
68 config,
69 callbacks: Callbacks::new(),
70 control: TaskControl::new(),
71 }
72 }
73
74 pub fn set_callbacks(&self, callbacks: JsValue) {
76 self.callbacks.set(callbacks);
77 }
78
79 pub fn download_folder(&self, base_url: &str, token: &str, dir_path: &str) -> js_sys::Promise {
83 let base_url = base_url.to_string();
84 let token = token.to_string();
85 let dir_path = dir_path.to_string();
86 let config = self.config.clone();
87 let callbacks = self.callbacks.clone();
88 let control = self.control.clone();
89
90 wasm_bindgen_futures::future_to_promise(async move {
91 control.reset();
92 control.begin(TaskState::Downloading);
93 match download::download_folder(
94 &base_url,
95 &token,
96 &dir_path,
97 &callbacks,
98 &control,
99 &config,
100 )
101 .await
102 {
103 Ok(total) => {
104 control.complete();
105 Ok(JsValue::from_f64(total as f64))
106 }
107 Err(e) => {
108 control.fail();
109 Err(e.to_js())
110 }
111 }
112 })
113 }
114
115 pub fn download_file(&self, base_url: &str, token: &str, file_path: &str) -> js_sys::Promise {
119 let base_url = base_url.to_string();
120 let token = token.to_string();
121 let file_path = file_path.to_string();
122 let config = self.config.clone();
123 let callbacks = self.callbacks.clone();
124 let control = self.control.clone();
125
126 wasm_bindgen_futures::future_to_promise(async move {
127 control.reset();
128 control.begin(TaskState::Downloading);
129 match download::download_single(
130 &base_url,
131 &token,
132 &file_path,
133 &callbacks,
134 &control,
135 &config,
136 )
137 .await
138 {
139 Ok(total) => {
140 control.complete();
141 Ok(JsValue::from_f64(total as f64))
142 }
143 Err(e) => {
144 control.fail();
145 Err(e.to_js())
146 }
147 }
148 })
149 }
150
151 pub fn upload(&self, base_url: &str, token: &str) -> js_sys::Promise {
155 let base_url = base_url.to_string();
156 let token = token.to_string();
157 let config = self.config.clone();
158 let callbacks = self.callbacks.clone();
159 let control = self.control.clone();
160
161 wasm_bindgen_futures::future_to_promise(async move {
162 control.reset();
163 control.begin(TaskState::Uploading);
164 match upload::upload(&base_url, &token, &callbacks, &control, &config).await {
165 Ok(total) => {
166 control.complete();
167 Ok(JsValue::from_f64(total as f64))
168 }
169 Err(e) => {
170 control.fail();
171 Err(e.to_js())
172 }
173 }
174 })
175 }
176
177 pub fn pause(&self) {
179 self.control.pause();
180 }
181
182 pub fn resume(&self) {
184 self.control.resume();
185 }
186
187 pub fn cancel(&self) {
189 self.control.cancel();
190 }
191
192 pub fn state(&self) -> String {
195 self.control.state().as_str().to_string()
196 }
197
198 pub fn progress(&self) -> f64 {
200 self.control.progress()
201 }
202
203 pub fn done_bytes(&self) -> f64 {
205 self.control.done_bytes() as f64
206 }
207
208 pub fn total_bytes(&self) -> f64 {
210 self.control.total_bytes() as f64
211 }
212
213 pub fn has_callbacks(&self) -> bool {
215 self.callbacks.is_set()
216 }
217}
218
219#[wasm_bindgen]
221pub fn js_option_string(obj: &JsValue, key: &str) -> Option<String> {
222 Reflect::get(obj, &JsValue::from_str(key))
223 .ok()
224 .and_then(|v| v.as_string())
225}
226
227#[cfg(test)]
228mod tests {
229 #[test]
230 #[cfg(target_arch = "wasm32")]
231 fn engine_default_state_is_idle() {
232 let engine = super::LibfwClient::new(wasm_bindgen::JsValue::NULL);
233 assert_eq!(engine.state(), "idle");
234 assert!(!engine.has_callbacks());
235 }
236
237 #[test]
238 #[cfg(target_arch = "wasm32")]
239 fn engine_options_parse() {
240 let opts = js_sys::Object::new();
241 js_sys::Reflect::set(&opts, &wasm_bindgen::JsValue::from_str("concurrency"), &wasm_bindgen::JsValue::from_f64(2.0))
242 .unwrap();
243 js_sys::Reflect::set(&opts, &wasm_bindgen::JsValue::from_str("compress"), &wasm_bindgen::JsValue::FALSE).unwrap();
244 let engine = super::LibfwClient::new(opts.into());
245 assert_eq!(engine.config.concurrency, 2);
246 assert!(!engine.config.compress);
247 }
248
249 #[test]
250 #[cfg(target_arch = "wasm32")]
251 fn state_transitions_via_public_api() {
252 use crate::state::TaskState;
253 let engine = super::LibfwClient::new(wasm_bindgen::JsValue::NULL);
254 engine.control.begin(TaskState::Downloading);
255 engine.pause();
256 assert_eq!(engine.state(), "paused");
257 engine.resume();
258 assert_eq!(engine.state(), "downloading");
259 engine.cancel();
260 assert_eq!(engine.state(), "failed");
261 }
262}