1#![recursion_limit = "512"]
9mod config;
33mod download;
34mod error;
35mod http;
36mod js;
37mod plan;
38mod state;
39mod tune;
40mod upload;
41
42pub use config::{backoff_ms, ClientConfig};
43pub use error::LibfwError;
44pub use plan::FileEntry;
45pub use tune::{
46 CompressLevel, LocalStore, TuneCache, TuneEvent, TuneHandle, TuneParams, TunePhase,
47 TuneStats, TuneStore, TransferKind, tune_key,
48};
49
50use js_sys::Reflect;
51use wasm_bindgen::prelude::*;
52
53use crate::js::Callbacks;
54
55#[wasm_bindgen(start)]
56pub fn start() {
57 console_error_panic_hook::set_once();
58}
59use crate::state::{TaskControl, TaskState};
60use crate::tune::{TuningEngine, now_ms};
61
62#[wasm_bindgen]
64pub struct LibfwClient {
65 config: ClientConfig,
66 callbacks: Callbacks,
67 control: TaskControl,
68 tune: TuneHandle,
69}
70
71#[wasm_bindgen]
72impl LibfwClient {
73 #[wasm_bindgen(constructor)]
78 pub fn new(opts: JsValue) -> LibfwClient {
79 let config = ClientConfig::from_js(&opts);
80 LibfwClient {
81 config: config.clone(),
82 callbacks: Callbacks::new(),
83 control: TaskControl::with_max_parallel(config.concurrency.max(1)),
86 tune: std::rc::Rc::new(std::cell::RefCell::new(TuningEngine::new(
87 config.auto_tune,
88 config.tune_ttl_ms,
89 config.compress_level,
90 ))),
91 }
92 }
93
94 pub fn set_callbacks(&self, callbacks: JsValue) {
96 self.callbacks.set(callbacks);
97 }
98
99 pub fn set_tune_callback(&self, cb: JsValue) {
102 let f = cb.dyn_ref::<js_sys::Function>().cloned();
103 self.tune.borrow_mut().set_on_tuning(f);
104 }
105
106 pub fn tune_state(&self) -> JsValue {
108 self.tune.borrow().state_js()
109 }
110
111 pub fn download_folder(&self, base_url: &str, token: &str, dir_path: &str) -> js_sys::Promise {
115 let base_url = base_url.to_string();
116 let token = token.to_string();
117 let dir_path = dir_path.to_string();
118 let config = self.config.clone();
119 let callbacks = self.callbacks.clone();
120 let control = self.control.clone();
121 let tune = self.tune.clone();
122
123 wasm_bindgen_futures::future_to_promise(async move {
124 control.reset();
125 control.begin(TaskState::Downloading);
126 let level = match prepare_transfer(
127 &base_url, &config, &tune, &control, TransferKind::Download,
128 )
129 .await
130 {
131 Ok(l) => l,
132 Err(e) => {
133 control.fail();
134 return Err(e.to_js());
135 }
136 };
137 match download::download_folder(
138 &base_url,
139 &token,
140 &dir_path,
141 &callbacks,
142 &control,
143 &config,
144 &tune,
145 level,
146 )
147 .await
148 {
149 Ok(total) => {
150 tune.borrow_mut().transfer_end(&base_url, &LocalStore, now_ms(), true);
151 control.complete();
152 Ok(JsValue::from_f64(total as f64))
153 }
154 Err(e) => {
155 tune.borrow_mut().transfer_end(&base_url, &LocalStore, now_ms(), false);
156 control.fail();
157 Err(e.to_js())
158 }
159 }
160 })
161 }
162
163 pub fn download_file(&self, base_url: &str, token: &str, file_path: &str) -> js_sys::Promise {
167 let base_url = base_url.to_string();
168 let token = token.to_string();
169 let file_path = file_path.to_string();
170 let config = self.config.clone();
171 let callbacks = self.callbacks.clone();
172 let control = self.control.clone();
173 let tune = self.tune.clone();
174
175 wasm_bindgen_futures::future_to_promise(async move {
176 control.reset();
177 control.begin(TaskState::Downloading);
178 let level = match prepare_transfer(
179 &base_url, &config, &tune, &control, TransferKind::Download,
180 )
181 .await
182 {
183 Ok(l) => l,
184 Err(e) => {
185 control.fail();
186 return Err(e.to_js());
187 }
188 };
189 match download::download_single(
190 &base_url,
191 &token,
192 &file_path,
193 &callbacks,
194 &control,
195 &config,
196 &tune,
197 level,
198 )
199 .await
200 {
201 Ok(total) => {
202 tune.borrow_mut().transfer_end(&base_url, &LocalStore, now_ms(), true);
203 control.complete();
204 Ok(JsValue::from_f64(total as f64))
205 }
206 Err(e) => {
207 tune.borrow_mut().transfer_end(&base_url, &LocalStore, now_ms(), false);
208 control.fail();
209 Err(e.to_js())
210 }
211 }
212 })
213 }
214
215 pub fn upload(&self, base_url: &str, token: &str) -> js_sys::Promise {
219 let base_url = base_url.to_string();
220 let token = token.to_string();
221 let config = self.config.clone();
222 let callbacks = self.callbacks.clone();
223 let control = self.control.clone();
224 let tune = self.tune.clone();
225
226 wasm_bindgen_futures::future_to_promise(async move {
227 control.reset();
228 control.begin(TaskState::Uploading);
229 let _level = match prepare_transfer(
230 &base_url, &config, &tune, &control, TransferKind::Upload,
231 )
232 .await
233 {
234 Ok(l) => l,
235 Err(e) => {
236 control.fail();
237 return Err(e.to_js());
238 }
239 };
240 match upload::upload(&base_url, &token, &callbacks, &control, &config, &tune).await {
241 Ok(total) => {
242 tune.borrow_mut().transfer_end(&base_url, &LocalStore, now_ms(), true);
243 control.complete();
244 Ok(JsValue::from_f64(total as f64))
245 }
246 Err(e) => {
247 tune.borrow_mut().transfer_end(&base_url, &LocalStore, now_ms(), false);
248 control.fail();
249 Err(e.to_js())
250 }
251 }
252 })
253 }
254
255 pub fn pause(&self) {
257 self.control.pause();
258 }
259
260 pub fn resume(&self) {
262 self.control.resume();
263 }
264
265 pub fn cancel(&self) {
267 self.control.cancel();
268 }
269
270 pub fn state(&self) -> String {
273 self.control.state().as_str().to_string()
274 }
275
276 pub fn progress(&self) -> f64 {
278 self.control.progress()
279 }
280
281 pub fn done_bytes(&self) -> f64 {
283 self.control.done_bytes() as f64
284 }
285
286 pub fn total_bytes(&self) -> f64 {
288 self.control.total_bytes() as f64
289 }
290
291 pub fn has_callbacks(&self) -> bool {
293 self.callbacks.is_set()
294 }
295}
296
297#[wasm_bindgen]
299pub fn js_option_string(obj: &JsValue, key: &str) -> Option<String> {
300 Reflect::get(obj, &JsValue::from_str(key))
301 .ok()
302 .and_then(|v| v.as_string())
303}
304
305async fn prepare_transfer(
315 base_url: &str,
316 config: &ClientConfig,
317 tune: &TuneHandle,
318 control: &TaskControl,
319 direction: TransferKind,
320) -> Result<i32, LibfwError> {
321 let caps = if config.auto_tune {
322 match http::fetch_capabilities(base_url, config.timeout_ms).await {
323 Ok(c) => c,
324 Err(LibfwError::Http { status: 404, .. }) => {
325 tune.borrow_mut().set_enabled(false);
327 libfw_core::Capabilities::default()
328 }
329 Err(e) => return Err(e),
330 }
331 } else {
332 libfw_core::Capabilities::default()
333 };
334
335 let level = tune::resolve_level(config.compress_level, &caps);
336 let static_params = TuneParams::from_config(
337 config.concurrency,
338 config.upload_window,
339 config.download_window,
340 config.chunk_size,
341 config.download_chunk_size,
342 level,
343 &caps,
344 );
345 let params = {
346 let mut t = tune.borrow_mut();
347 t.set_direction(direction);
348 t.begin_transfer(base_url, &caps, &LocalStore, now_ms(), &static_params)
349 };
350 control.set_max_parallel(params.concurrency);
351 Ok(params.compress_level)
352}
353
354#[cfg(test)]
355mod tests {
356 #[test]
357 #[cfg(target_arch = "wasm32")]
358 fn engine_default_state_is_idle() {
359 let engine = super::LibfwClient::new(wasm_bindgen::JsValue::NULL);
360 assert_eq!(engine.state(), "idle");
361 assert!(!engine.has_callbacks());
362 }
363
364 #[test]
365 #[cfg(target_arch = "wasm32")]
366 fn engine_options_parse() {
367 let opts = js_sys::Object::new();
368 js_sys::Reflect::set(&opts, &wasm_bindgen::JsValue::from_str("concurrency"), &wasm_bindgen::JsValue::from_f64(2.0))
369 .unwrap();
370 js_sys::Reflect::set(&opts, &wasm_bindgen::JsValue::from_str("compress"), &wasm_bindgen::JsValue::FALSE).unwrap();
371 let engine = super::LibfwClient::new(opts.into());
372 assert_eq!(engine.config.concurrency, 2);
373 assert!(!engine.config.compress);
374 }
375
376 #[test]
377 #[cfg(target_arch = "wasm32")]
378 fn state_transitions_via_public_api() {
379 use crate::state::TaskState;
380 let engine = super::LibfwClient::new(wasm_bindgen::JsValue::NULL);
381 engine.control.begin(TaskState::Downloading);
382 engine.pause();
383 assert_eq!(engine.state(), "paused");
384 engine.resume();
385 assert_eq!(engine.state(), "downloading");
386 engine.cancel();
387 assert_eq!(engine.state(), "failed");
388 }
389}