@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.
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +36 -2
- package/desktop/Cargo.toml +29 -0
- package/desktop/build.rs +3 -0
- package/desktop/src/dialogs.rs +394 -0
- package/desktop/src/dirs.rs +363 -0
- package/desktop/src/download.rs +329 -0
- package/desktop/src/errors.rs +149 -0
- package/desktop/src/files.rs +640 -0
- package/desktop/src/handles.rs +697 -0
- package/desktop/src/lib.rs +286 -0
- package/desktop/src/links.rs +336 -0
- package/desktop/src/module.rs +192 -0
- package/desktop/src/paths.rs +231 -0
- package/desktop/src/upload.rs +237 -0
- package/desktop/src/watching.rs +392 -0
- package/package.json +38 -4
- package/react-native.config.js +9 -0
- package/src/NativeFs.ts +235 -0
- package/src/constants.ts +43 -0
- package/src/download.ts +110 -0
- package/src/index.ts +788 -0
- package/src/promises.ts +771 -0
- package/src/shared.ts +539 -0
- package/src/stats.ts +196 -0
- package/src/upload.ts +97 -0
- package/src/watch.ts +141 -0
|
@@ -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
|
+
}
|