@react-native-gpui/fs 0.0.0-stage → 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,363 @@
1
+ //! Listing, creating and removing directories, and `mkdtemp`.
2
+ //!
3
+ //! `readdir` returns every entry as a plain `{name, kind, parentPath}` object,
4
+ //! which the JS layer turns into `Dirent` instances; there is no per-directory
5
+ //! cursor to hold open because the whole listing crosses the bridge at once.
6
+
7
+ use std::fs;
8
+ use std::path::{Path, PathBuf};
9
+
10
+ use rn_gpui_module::ModuleError;
11
+
12
+ use crate::errors;
13
+ use crate::module::kind;
14
+ use crate::DirEntry;
15
+
16
+ pub async fn readdir(path: String, recursive: bool) -> Result<Vec<DirEntry>, ModuleError> {
17
+ readdir_sync(path, recursive)
18
+ }
19
+
20
+ pub async fn mkdir(path: String, recursive: bool, mode: f64) -> Result<(), ModuleError> {
21
+ create_dir(&path, recursive, mode)
22
+ }
23
+
24
+ pub async fn rm(path: String, recursive: bool, force: bool) -> Result<(), ModuleError> {
25
+ remove(&path, recursive, force)
26
+ }
27
+
28
+ pub async fn mkdtemp(prefix: String) -> Result<String, ModuleError> {
29
+ let directory = tempfile::Builder::new()
30
+ .prefix(&prefix)
31
+ .tempdir_in(std::env::temp_dir())
32
+ .map_err(|error| errors::from_io(error, &prefix))?;
33
+ // The caller asked for a directory to keep, so it must outlive this frame;
34
+ // dropping the guard would delete it again.
35
+ Ok(path_string(directory.keep()))
36
+ }
37
+
38
+ pub fn readdir_sync(path: String, recursive: bool) -> Result<Vec<DirEntry>, ModuleError> {
39
+ let entries = list(Path::new(&path))?;
40
+ if !recursive {
41
+ return Ok(entries);
42
+ }
43
+ let mut collected = entries;
44
+ // Depth-first, appending each level as it is reached. Iterative rather than
45
+ // recursive: a deep tree should cost heap, not stack.
46
+ let mut pending: Vec<PathBuf> = collected
47
+ .iter()
48
+ .filter(|entry| entry.kind == kind::DIRECTORY)
49
+ .map(|entry| Path::new(&entry.parent_path).join(&entry.name))
50
+ .collect();
51
+ while let Some(directory) = pending.pop() {
52
+ let children = list(&directory)?;
53
+ pending.extend(
54
+ children
55
+ .iter()
56
+ .filter(|entry| entry.kind == kind::DIRECTORY)
57
+ .map(|entry| Path::new(&entry.parent_path).join(&entry.name)),
58
+ );
59
+ collected.extend(children);
60
+ }
61
+ Ok(collected)
62
+ }
63
+
64
+ pub fn mkdir_sync(path: String, recursive: bool, mode: f64) -> Result<bool, ModuleError> {
65
+ create_dir(&path, recursive, mode)?;
66
+ Ok(true)
67
+ }
68
+
69
+ pub fn rm_sync(path: String, recursive: bool, force: bool) -> Result<bool, ModuleError> {
70
+ remove(&path, recursive, force)?;
71
+ Ok(true)
72
+ }
73
+
74
+ /// The direct children of `path`, in the order the file system lists them.
75
+ ///
76
+ /// `DirEntry::file_type` does not follow symbolic links, so a link is reported
77
+ /// as a link rather than as whatever it points at.
78
+ fn list(path: &Path) -> Result<Vec<DirEntry>, ModuleError> {
79
+ let name = path_string(path.to_path_buf());
80
+ let iterator = fs::read_dir(path).map_err(|error| errors::from_io(error, &name))?;
81
+ let mut entries = Vec::new();
82
+ for entry in iterator {
83
+ let entry = entry.map_err(|error| errors::from_io(error, &name))?;
84
+ let file_type = entry
85
+ .file_type()
86
+ .map_err(|error| errors::from_io(error, &name))?;
87
+ entries.push(DirEntry {
88
+ name: entry.file_name().to_string_lossy().into_owned(),
89
+ kind: entry_kind(&file_type),
90
+ parent_path: name.clone(),
91
+ });
92
+ }
93
+ Ok(entries)
94
+ }
95
+
96
+ fn entry_kind(file_type: &fs::FileType) -> f64 {
97
+ if file_type.is_file() {
98
+ kind::FILE
99
+ } else if file_type.is_dir() {
100
+ kind::DIRECTORY
101
+ } else if file_type.is_symlink() {
102
+ kind::SYMLINK
103
+ } else {
104
+ kind::OTHER
105
+ }
106
+ }
107
+
108
+ fn create_dir(path: &str, recursive: bool, mode: f64) -> Result<(), ModuleError> {
109
+ if recursive {
110
+ // `create_dir_all` succeeds on an existing directory, which is what
111
+ // Node's `{recursive: true}` promises.
112
+ return fs::create_dir_all(path).map_err(|error| errors::from_io(error, path));
113
+ }
114
+ #[cfg(unix)]
115
+ {
116
+ use std::os::unix::fs::DirBuilderExt as _;
117
+ let mut builder = fs::DirBuilder::new();
118
+ builder.mode(crate::module::mode(mode));
119
+ builder
120
+ .create(path)
121
+ .map_err(|error| errors::from_io(error, path))
122
+ }
123
+ // Windows has no mode for a new directory, and `create_dir` would reject
124
+ // the `DirBuilder` extension that only exists on Unix.
125
+ #[cfg(not(unix))]
126
+ {
127
+ let _ = mode;
128
+ fs::create_dir(path).map_err(|error| errors::from_io(error, path))
129
+ }
130
+ }
131
+
132
+ fn remove(path: &str, recursive: bool, force: bool) -> Result<(), ModuleError> {
133
+ // `symlink_metadata`, so a link is removed as a link instead of acting on
134
+ // its target.
135
+ let metadata = match fs::symlink_metadata(path) {
136
+ Ok(metadata) => metadata,
137
+ Err(error) if force && errors::code_for(&error) == "ENOENT" => return Ok(()),
138
+ Err(error) => return Err(errors::from_io(error, path)),
139
+ };
140
+ if recursive && metadata.is_dir() {
141
+ return fs::remove_dir_all(path).map_err(|error| errors::from_io(error, path));
142
+ }
143
+ if metadata.is_dir() {
144
+ // `remove_dir` fails with ENOTEMPTY on Unix and a folder-not-empty error
145
+ // on Windows, which the mapping turns into the same code; removing a
146
+ // file with it reports what Node calls ENOTDIR.
147
+ fs::remove_dir(path).map_err(|error| errors::from_io(error, path))
148
+ } else {
149
+ fs::remove_file(path).map_err(|error| errors::from_io(error, path))
150
+ }
151
+ }
152
+
153
+ fn path_string(path: PathBuf) -> String {
154
+ path.to_string_lossy().into_owned()
155
+ }
156
+
157
+ #[cfg(test)]
158
+ mod tests {
159
+ use super::*;
160
+ use tempfile::TempDir;
161
+
162
+ /// The async wrappers never suspend, so polling once with a no-op waker is
163
+ /// enough and keeps the tests independent of a runtime.
164
+ fn block_on<F: std::future::Future>(future: F) -> F::Output {
165
+ let mut future = std::pin::pin!(future);
166
+ let waker = std::task::Waker::noop();
167
+ let mut context = std::task::Context::from_waker(waker);
168
+ // One poll, no loop: these helpers do their work inline, so a `Pending`
169
+ // would mean the helper is not synchronous after all, which the test
170
+ // should fail loudly on rather than spin.
171
+ match future.as_mut().poll(&mut context) {
172
+ std::task::Poll::Ready(value) => value,
173
+ std::task::Poll::Pending => panic!("the wrappers are synchronous"),
174
+ }
175
+ }
176
+
177
+ fn tempdir() -> TempDir {
178
+ TempDir::new().expect("a temporary directory")
179
+ }
180
+
181
+ fn text(path: &Path) -> String {
182
+ path.to_string_lossy().into_owned()
183
+ }
184
+
185
+ /// The listed paths, with separators normalised to `/`.
186
+ ///
187
+ /// The entries themselves carry the platform separator, which on Windows is
188
+ /// `\`; building the expected path with `/` and comparing would test the
189
+ /// separator rather than the listing.
190
+ fn names(entries: &[DirEntry]) -> Vec<String> {
191
+ let mut names: Vec<String> = entries
192
+ .iter()
193
+ .map(|entry| format!("{}/{}", entry.parent_path, entry.name).replace('\\', "/"))
194
+ .collect();
195
+ names.sort();
196
+ names
197
+ }
198
+
199
+ fn tree(root: &Path) {
200
+ fs::create_dir_all(root.join("nested").join("deep")).expect("nested");
201
+ fs::write(root.join("top.txt"), b"top").expect("seed");
202
+ fs::write(root.join("nested").join("middle.txt"), b"middle").expect("seed");
203
+ fs::write(root.join("nested").join("deep").join("leaf.txt"), b"leaf").expect("seed");
204
+ }
205
+
206
+ #[test]
207
+ fn readdir_lists_direct_children_only() {
208
+ let dir = tempdir();
209
+ tree(dir.path());
210
+
211
+ let entries = readdir_sync(text(dir.path()), false).expect("readdir");
212
+ // `names` normalises separators, so the expected paths are built from a
213
+ // normalised root rather than the platform one.
214
+ let root = text(dir.path()).replace('\\', "/");
215
+ assert_eq!(
216
+ names(&entries),
217
+ vec![format!("{root}/nested"), format!("{root}/top.txt")]
218
+ );
219
+
220
+ let top = entries
221
+ .iter()
222
+ .find(|entry| entry.name == "top.txt")
223
+ .expect("top.txt");
224
+ assert_eq!(top.kind, kind::FILE);
225
+ assert_eq!(top.parent_path, text(dir.path()));
226
+ let nested = entries
227
+ .iter()
228
+ .find(|entry| entry.name == "nested")
229
+ .expect("nested");
230
+ assert_eq!(nested.kind, kind::DIRECTORY);
231
+ }
232
+
233
+ #[test]
234
+ fn readdir_recursive_reaches_every_descendant() {
235
+ let dir = tempdir();
236
+ tree(dir.path());
237
+
238
+ let entries = block_on(readdir(text(dir.path()), true)).expect("readdir");
239
+ // Two entries per directory, three directories deep.
240
+ assert_eq!(entries.len(), 5);
241
+ let listed = names(&entries);
242
+ let root = text(dir.path()).replace('\\', "/");
243
+ assert!(listed.contains(&format!("{root}/nested/deep/leaf.txt")));
244
+ assert!(listed.contains(&format!("{root}/nested/deep")));
245
+
246
+ let deep = entries
247
+ .iter()
248
+ .find(|entry| entry.name == "deep")
249
+ .expect("deep");
250
+ assert_eq!(
251
+ deep.parent_path,
252
+ text(&dir.path().join("nested")),
253
+ "parentPath is the containing directory"
254
+ );
255
+ }
256
+
257
+ #[cfg(unix)]
258
+ #[test]
259
+ fn readdir_reports_a_link_as_a_link() {
260
+ let dir = tempdir();
261
+ let target = dir.path().join("target.txt");
262
+ fs::write(&target, b"x").expect("seed");
263
+ std::os::unix::fs::symlink(&target, dir.path().join("link.txt")).expect("symlink");
264
+
265
+ let entries = readdir_sync(text(dir.path()), false).expect("readdir");
266
+ let link = entries
267
+ .iter()
268
+ .find(|entry| entry.name == "link.txt")
269
+ .expect("link.txt");
270
+ assert_eq!(link.kind, kind::SYMLINK);
271
+ }
272
+
273
+ #[test]
274
+ fn mkdir_with_recursive_creates_the_chain_and_accepts_an_existing_dir() {
275
+ let dir = tempdir();
276
+ let nested = dir.path().join("a").join("b").join("c");
277
+
278
+ block_on(mkdir(text(&nested), true, f64::from(0o755u32))).expect("mkdir");
279
+ assert!(nested.is_dir());
280
+
281
+ let again = mkdir_sync(text(&nested), true, f64::from(0o755u32)).expect("mkdir again");
282
+ assert!(again, "recursive mkdir succeeds when the path exists");
283
+ }
284
+
285
+ #[test]
286
+ fn mkdir_without_recursive_fails_with_eexist() {
287
+ let dir = tempdir();
288
+ let path = dir.path().join("single");
289
+ assert!(mkdir_sync(text(&path), false, f64::from(0o755u32)).expect("mkdir"));
290
+ assert!(path.is_dir());
291
+
292
+ let error =
293
+ block_on(mkdir(text(&path), false, f64::from(0o755u32))).expect_err("mkdir twice");
294
+ assert_eq!(error.code.as_deref(), Some("EEXIST"));
295
+
296
+ let missing_parent = text(&dir.path().join("no").join("such").join("dir"));
297
+ let error =
298
+ mkdir_sync(missing_parent, false, f64::from(0o755u32)).expect_err("missing parent");
299
+ assert_eq!(error.code.as_deref(), Some("ENOENT"));
300
+ }
301
+
302
+ #[test]
303
+ fn rm_force_ignores_a_missing_path() {
304
+ let dir = tempdir();
305
+ let missing = text(&dir.path().join("absent"));
306
+
307
+ block_on(rm(missing.clone(), false, true)).expect("rm force");
308
+ assert!(rm_sync(missing.clone(), true, true).expect("rm sync force"));
309
+
310
+ let error = block_on(rm(missing, false, false)).expect_err("rm without force");
311
+ assert_eq!(error.code.as_deref(), Some("ENOENT"));
312
+ }
313
+
314
+ #[test]
315
+ fn rm_removes_a_file_and_a_tree() {
316
+ let dir = tempdir();
317
+ let file = dir.path().join("one.txt");
318
+ fs::write(&file, b"x").expect("seed");
319
+ block_on(rm(text(&file), false, false)).expect("rm file");
320
+ assert!(!file.exists());
321
+
322
+ let root = dir.path().join("tree");
323
+ tree(&root);
324
+ assert!(rm_sync(text(&root), true, false).expect("rm tree"));
325
+ assert!(!root.exists());
326
+ }
327
+
328
+ #[test]
329
+ fn rm_without_recursive_refuses_a_non_empty_directory() {
330
+ let dir = tempdir();
331
+ let root = dir.path().join("tree");
332
+ tree(&root);
333
+
334
+ let error = block_on(rm(text(&root), false, false)).expect_err("non-empty");
335
+ assert_eq!(error.code.as_deref(), Some("ENOTEMPTY"));
336
+ assert!(root.exists());
337
+ }
338
+
339
+ #[test]
340
+ fn mkdtemp_creates_a_random_directory_with_the_prefix() {
341
+ let created = block_on(mkdtemp("rn-gpui-fs-".to_string())).expect("mkdtemp");
342
+ let path = Path::new(&created);
343
+ let name = path
344
+ .file_name()
345
+ .expect("the returned path names a directory")
346
+ .to_string_lossy()
347
+ .into_owned();
348
+
349
+ assert!(name.starts_with("rn-gpui-fs-"), "name was {name}");
350
+ assert!(name.len() > "rn-gpui-fs-".len(), "the rest is random");
351
+ assert!(path.is_dir());
352
+ assert_eq!(path.parent(), Some(std::env::temp_dir().as_path()));
353
+
354
+ fs::remove_dir_all(path).expect("clean up");
355
+ }
356
+
357
+ #[test]
358
+ fn readdir_of_a_missing_directory_is_enoent() {
359
+ let dir = tempdir();
360
+ let error = readdir_sync(text(&dir.path().join("absent")), false).expect_err("readdir");
361
+ assert_eq!(error.code.as_deref(), Some("ENOENT"));
362
+ }
363
+ }
@@ -0,0 +1,329 @@
1
+ //! `download`: an HTTP body streamed to a file.
2
+ //!
3
+ //! A transfer is two threads. The network thread makes the request and reads the
4
+ //! body in chunks; the coordinator receives them, writes `<path>.part` and renames
5
+ //! it onto `path` when the body is complete. Splitting them is what makes a cancel
6
+ //! prompt: the coordinator polls the cancel flag between messages, so it does not
7
+ //! wait on a socket that has gone quiet, and the network thread simply stops the
8
+ //! next time it tries to hand over a chunk.
9
+ //!
10
+ //! The module's own thread is only waited on through a channel, so a slow download
11
+ //! never blocks other file calls.
12
+
13
+ use std::collections::HashMap;
14
+ use std::fs::File;
15
+ use std::io::{Read, Write};
16
+ use std::path::PathBuf;
17
+ use std::sync::atomic::{AtomicBool, Ordering};
18
+ use std::sync::mpsc::{self, RecvTimeoutError, SyncSender};
19
+ use std::sync::{Arc, Mutex, OnceLock};
20
+ use std::time::{Duration, Instant};
21
+
22
+ use rn_gpui_module::ModuleError;
23
+ use ureq::config::Config;
24
+ use ureq::tls::{RootCerts, TlsConfig};
25
+ use ureq::Agent;
26
+
27
+ use crate::{
28
+ errors, DownloadHeader, DownloadOptions, DownloadProgress, DownloadResult, RNFsModule,
29
+ };
30
+
31
+ const CHUNK: usize = 64 * 1024;
32
+ const PROGRESS_INTERVAL: Duration = Duration::from_millis(100);
33
+ const POLL: Duration = Duration::from_millis(25);
34
+ /// How much of a failed response's body is kept for the error.
35
+ const ERROR_BODY_LIMIT: u64 = 2048;
36
+
37
+ pub type Downloads = Mutex<HashMap<u32, Arc<AtomicBool>>>;
38
+
39
+ enum Message {
40
+ Head {
41
+ status: u16,
42
+ headers: Vec<DownloadHeader>,
43
+ total: Option<u64>,
44
+ },
45
+ Rejected {
46
+ status: u16,
47
+ headers: Vec<DownloadHeader>,
48
+ body: String,
49
+ },
50
+ Chunk(Vec<u8>),
51
+ End,
52
+ Failed(String),
53
+ }
54
+
55
+ fn agent() -> &'static Agent {
56
+ static AGENT: OnceLock<Agent> = OnceLock::new();
57
+ AGENT.get_or_init(|| {
58
+ let config = Config::builder()
59
+ .http_status_as_error(false)
60
+ .timeout_connect(Some(Duration::from_secs(30)))
61
+ .tls_config(
62
+ TlsConfig::builder()
63
+ .root_certs(RootCerts::PlatformVerifier)
64
+ .build(),
65
+ )
66
+ .build();
67
+ Agent::new_with_config(config)
68
+ })
69
+ }
70
+
71
+ fn aborted() -> ModuleError {
72
+ ModuleError::with_code("ABORT_ERR", "The download was aborted")
73
+ }
74
+
75
+ fn failed(message: impl Into<String>) -> ModuleError {
76
+ ModuleError::with_code("ERR_DOWNLOAD", message.into())
77
+ }
78
+
79
+ fn id_of(value: f64) -> u32 {
80
+ value as u32
81
+ }
82
+
83
+ pub fn cancel(module: &RNFsModule, id: f64) -> bool {
84
+ match module.downloads.lock().expect("downloads").get(&id_of(id)) {
85
+ Some(stop) => {
86
+ stop.store(true, Ordering::SeqCst);
87
+ true
88
+ }
89
+ None => false,
90
+ }
91
+ }
92
+
93
+ fn fetch(
94
+ url: &str,
95
+ method: &str,
96
+ headers: &[DownloadHeader],
97
+ sender: &SyncSender<Message>,
98
+ stop: &AtomicBool,
99
+ ) {
100
+ let result = (|| -> Result<(), String> {
101
+ let mut builder = ureq::http::Request::builder().method(method).uri(url);
102
+ for header in headers {
103
+ builder = builder.header(header.name.as_str(), header.value.as_str());
104
+ }
105
+ let request = builder.body(()).map_err(|e| e.to_string())?;
106
+ let mut response = agent().run(request).map_err(|e| e.to_string())?;
107
+ let status = response.status().as_u16();
108
+ let mut listed: Vec<DownloadHeader> = Vec::new();
109
+ for (name, value) in response.headers() {
110
+ let value = String::from_utf8_lossy(value.as_bytes()).into_owned();
111
+ match listed.iter_mut().find(|h| h.name == name.as_str()) {
112
+ Some(existing) => {
113
+ existing.value.push_str(", ");
114
+ existing.value.push_str(&value);
115
+ }
116
+ None => listed.push(DownloadHeader {
117
+ name: name.as_str().to_string(),
118
+ value,
119
+ }),
120
+ }
121
+ }
122
+ let total = response
123
+ .headers()
124
+ .get("content-length")
125
+ .and_then(|v| v.to_str().ok())
126
+ .and_then(|v| v.parse::<u64>().ok());
127
+ let mut reader = response.body_mut().as_reader();
128
+ if !(200..300).contains(&status) {
129
+ let mut body = Vec::new();
130
+ let _ = reader
131
+ .by_ref()
132
+ .take(ERROR_BODY_LIMIT)
133
+ .read_to_end(&mut body);
134
+ let _ = sender.send(Message::Rejected {
135
+ status,
136
+ headers: listed,
137
+ body: String::from_utf8_lossy(&body).into_owned(),
138
+ });
139
+ return Ok(());
140
+ }
141
+ if sender
142
+ .send(Message::Head {
143
+ status,
144
+ headers: listed,
145
+ total,
146
+ })
147
+ .is_err()
148
+ {
149
+ return Ok(());
150
+ }
151
+ loop {
152
+ if stop.load(Ordering::SeqCst) {
153
+ return Ok(());
154
+ }
155
+ let mut chunk = vec![0u8; CHUNK];
156
+ let read = reader.read(&mut chunk).map_err(|e| e.to_string())?;
157
+ if read == 0 {
158
+ let _ = sender.send(Message::End);
159
+ return Ok(());
160
+ }
161
+ chunk.truncate(read);
162
+ if sender.send(Message::Chunk(chunk)).is_err() {
163
+ return Ok(());
164
+ }
165
+ }
166
+ })();
167
+ if let Err(message) = result {
168
+ let _ = sender.send(Message::Failed(message));
169
+ }
170
+ }
171
+
172
+ fn transfer(
173
+ module_events: &crate::NativeFsEvents,
174
+ id: u32,
175
+ url: String,
176
+ dest: PathBuf,
177
+ method: String,
178
+ headers: Vec<DownloadHeader>,
179
+ stop: Arc<AtomicBool>,
180
+ ) -> Result<DownloadResult, ModuleError> {
181
+ let mut partial = dest.clone().into_os_string();
182
+ partial.push(".part");
183
+ let partial = PathBuf::from(partial);
184
+ let shown = dest.display().to_string();
185
+
186
+ let (sender, receiver) = mpsc::sync_channel::<Message>(8);
187
+ {
188
+ let stop = stop.clone();
189
+ std::thread::spawn(move || fetch(&url, &method, &headers, &sender, &stop));
190
+ }
191
+
192
+ let mut file: Option<File> = None;
193
+ let mut head: Option<(u16, Vec<DownloadHeader>, Option<u64>)> = None;
194
+ let mut received: u64 = 0;
195
+ let mut last_progress = Instant::now() - PROGRESS_INTERVAL;
196
+ let emit = |received: u64, total: Option<u64>| {
197
+ module_events.on_download_progress(DownloadProgress {
198
+ id: f64::from(id),
199
+ received: received as f64,
200
+ total: total.map_or(-1.0, |t| t as f64),
201
+ });
202
+ };
203
+
204
+ let outcome: Result<DownloadResult, ModuleError> = loop {
205
+ if stop.load(Ordering::SeqCst) {
206
+ break Err(aborted());
207
+ }
208
+ let message = match receiver.recv_timeout(POLL) {
209
+ Ok(message) => message,
210
+ Err(RecvTimeoutError::Timeout) => continue,
211
+ Err(RecvTimeoutError::Disconnected) => break Err(failed("the connection closed")),
212
+ };
213
+ match message {
214
+ Message::Rejected {
215
+ status,
216
+ headers,
217
+ body,
218
+ } => {
219
+ break Ok(DownloadResult {
220
+ status: f64::from(status),
221
+ headers,
222
+ bytes: 0.0,
223
+ body: Some(body),
224
+ });
225
+ }
226
+ Message::Head {
227
+ status,
228
+ headers,
229
+ total,
230
+ } => {
231
+ match File::create(&partial) {
232
+ Ok(created) => file = Some(created),
233
+ Err(e) => break Err(errors::from_io(e, &shown)),
234
+ }
235
+ head = Some((status, headers, total));
236
+ emit(0, total);
237
+ }
238
+ Message::Chunk(bytes) => {
239
+ let Some(target) = file.as_mut() else {
240
+ break Err(failed("data arrived before the response"));
241
+ };
242
+ if let Err(e) = target.write_all(&bytes) {
243
+ break Err(errors::from_io(e, &shown));
244
+ }
245
+ received += bytes.len() as u64;
246
+ if last_progress.elapsed() >= PROGRESS_INTERVAL {
247
+ last_progress = Instant::now();
248
+ emit(received, head.as_ref().and_then(|h| h.2));
249
+ }
250
+ }
251
+ Message::End => {
252
+ let Some((status, headers, total)) = head.take() else {
253
+ break Err(failed("the response had no head"));
254
+ };
255
+ if let Some(expected) = total {
256
+ if received != expected {
257
+ break Err(failed(format!(
258
+ "the connection closed after {received} of {expected} bytes"
259
+ )));
260
+ }
261
+ }
262
+ if let Some(mut target) = file.take() {
263
+ if let Err(e) = target.flush() {
264
+ break Err(errors::from_io(e, &shown));
265
+ }
266
+ }
267
+ if let Err(e) = std::fs::rename(&partial, &dest) {
268
+ break Err(errors::from_io(e, &shown));
269
+ }
270
+ emit(received, total);
271
+ break Ok(DownloadResult {
272
+ status: f64::from(status),
273
+ headers,
274
+ bytes: received as f64,
275
+ body: None,
276
+ });
277
+ }
278
+ Message::Failed(message) => break Err(failed(message)),
279
+ }
280
+ };
281
+
282
+ drop(receiver);
283
+ drop(file);
284
+ let kept = matches!(&outcome, Ok(result) if result.body.is_none());
285
+ if !kept {
286
+ let _ = std::fs::remove_file(&partial);
287
+ }
288
+ outcome
289
+ }
290
+
291
+ pub async fn download(
292
+ module: &RNFsModule,
293
+ url: String,
294
+ path: String,
295
+ options: DownloadOptions,
296
+ ) -> Result<DownloadResult, ModuleError> {
297
+ let id = id_of(options.id);
298
+ let stop = Arc::new(AtomicBool::new(false));
299
+ {
300
+ let mut running = module.downloads.lock().expect("downloads");
301
+ if running.contains_key(&id) {
302
+ return Err(failed(format!("download {id} is already running")));
303
+ }
304
+ running.insert(id, stop.clone());
305
+ }
306
+ let (sender, receiver) = futures::channel::oneshot::channel();
307
+ let events = module.events.clone();
308
+ let method = if options.method.is_empty() {
309
+ "GET".to_string()
310
+ } else {
311
+ options.method
312
+ };
313
+ let flag = stop.clone();
314
+ std::thread::spawn(move || {
315
+ let result = transfer(
316
+ &events,
317
+ id,
318
+ url,
319
+ PathBuf::from(path),
320
+ method,
321
+ options.headers,
322
+ flag,
323
+ );
324
+ let _ = sender.send(result);
325
+ });
326
+ let result = receiver.await;
327
+ module.downloads.lock().expect("downloads").remove(&id);
328
+ result.unwrap_or_else(|_| Err(failed("the download stopped unexpectedly")))
329
+ }