@react-native-gpui/fs 0.0.1-alpha.0 → 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 +5 -0
- package/README.md +2 -2
- package/desktop/Cargo.toml +4 -0
- package/desktop/src/download.rs +329 -0
- package/desktop/src/lib.rs +28 -0
- package/desktop/src/module.rs +5 -0
- package/desktop/src/upload.rs +237 -0
- package/package.json +5 -4
- package/src/NativeFs.ts +73 -0
- package/src/download.ts +110 -0
- package/src/index.ts +20 -1
- package/src/upload.ts +97 -0
- package/desktop/Cargo.lock +0 -1989
package/src/NativeFs.ts
CHANGED
|
@@ -67,6 +67,61 @@ export type WatchEvent = {
|
|
|
67
67
|
path: string;
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
export type DownloadHeader = {
|
|
71
|
+
name: string;
|
|
72
|
+
value: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type DownloadOptions = {
|
|
76
|
+
id: number;
|
|
77
|
+
method: string;
|
|
78
|
+
headers: Array<DownloadHeader>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* What a download resolves with. A non-2xx answer is not an error here: it
|
|
83
|
+
* comes back with its `status` and a small `body`, and no file is written.
|
|
84
|
+
*/
|
|
85
|
+
export type DownloadResult = {
|
|
86
|
+
status: number;
|
|
87
|
+
headers: Array<DownloadHeader>;
|
|
88
|
+
bytes: number;
|
|
89
|
+
body?: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type DownloadProgress = {
|
|
93
|
+
id: number;
|
|
94
|
+
received: number;
|
|
95
|
+
// -1 when the server did not say how long the body is.
|
|
96
|
+
total: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type UploadOptions = {
|
|
100
|
+
id: number;
|
|
101
|
+
method: string;
|
|
102
|
+
headers: Array<DownloadHeader>;
|
|
103
|
+
// Where in the file the body starts.
|
|
104
|
+
offset: number;
|
|
105
|
+
// How many bytes to send; -1 sends everything from `offset`.
|
|
106
|
+
length: number;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* What an upload resolves with. Any status is a result; `body` is the first
|
|
111
|
+
* part of the response.
|
|
112
|
+
*/
|
|
113
|
+
export type UploadResult = {
|
|
114
|
+
status: number;
|
|
115
|
+
headers: Array<DownloadHeader>;
|
|
116
|
+
body: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type UploadProgress = {
|
|
120
|
+
id: number;
|
|
121
|
+
sent: number;
|
|
122
|
+
total: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
70
125
|
/**
|
|
71
126
|
* The file system module: one instance, on its own thread, so a slow file never
|
|
72
127
|
* blocks JS or the UI.
|
|
@@ -154,9 +209,27 @@ export interface Spec extends TurboModule {
|
|
|
154
209
|
renameSync(from: string, to: string): boolean;
|
|
155
210
|
copyFileSync(from: string, to: string): boolean;
|
|
156
211
|
|
|
212
|
+
download(
|
|
213
|
+
url: string,
|
|
214
|
+
path: string,
|
|
215
|
+
options: DownloadOptions,
|
|
216
|
+
): Promise<DownloadResult>;
|
|
217
|
+
// Returns whether a download with that id was running.
|
|
218
|
+
cancelDownload(id: number): boolean;
|
|
219
|
+
|
|
220
|
+
upload(
|
|
221
|
+
url: string,
|
|
222
|
+
path: string,
|
|
223
|
+
options: UploadOptions,
|
|
224
|
+
): Promise<UploadResult>;
|
|
225
|
+
// Returns whether an upload with that id was running.
|
|
226
|
+
cancelUpload(id: number): boolean;
|
|
227
|
+
|
|
157
228
|
getPaths(): KnownPaths;
|
|
158
229
|
|
|
159
230
|
readonly onWatchEvent: CodegenTypes.EventEmitter<WatchEvent>;
|
|
231
|
+
readonly onDownloadProgress: CodegenTypes.EventEmitter<DownloadProgress>;
|
|
232
|
+
readonly onUploadProgress: CodegenTypes.EventEmitter<UploadProgress>;
|
|
160
233
|
}
|
|
161
234
|
|
|
162
235
|
export default TurboModuleRegistry.getEnforcing<Spec>('RNFsModule');
|
package/src/download.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `download`: an HTTP response streamed into a file.
|
|
3
|
+
*
|
|
4
|
+
* The body goes to `<path>.part` as it arrives and is renamed onto `path` once it
|
|
5
|
+
* is complete, so `path` never holds half a file. The partial file is removed when
|
|
6
|
+
* the request fails or is aborted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import NativeFs from './NativeFs';
|
|
10
|
+
import { nodeError, toPath, withContext, type NodeError } from './shared';
|
|
11
|
+
|
|
12
|
+
export interface DownloadOptions {
|
|
13
|
+
/** `GET` unless set. No request body is sent. */
|
|
14
|
+
method?: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
/** Called while the body arrives; `total` is `-1` when the server did not say. */
|
|
17
|
+
onProgress?: (received: number, total: number) => void;
|
|
18
|
+
/** Aborting rejects with an `AbortError` and removes the partial file. */
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DownloadResult {
|
|
23
|
+
status: number;
|
|
24
|
+
/** Response headers, lower-cased; repeated headers are joined with `, `. */
|
|
25
|
+
headers: Record<string, string>;
|
|
26
|
+
/** The size of the file that was written. */
|
|
27
|
+
bytes: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** What a non-2xx response rejects with. */
|
|
31
|
+
export interface DownloadHttpError extends NodeError {
|
|
32
|
+
status: number;
|
|
33
|
+
headers: Record<string, string>;
|
|
34
|
+
/** The first part of the response body. */
|
|
35
|
+
body: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let nextId = 1;
|
|
39
|
+
|
|
40
|
+
function abortError(): NodeError {
|
|
41
|
+
const error = nodeError('ABORT_ERR', 'The download was aborted');
|
|
42
|
+
error.name = 'AbortError';
|
|
43
|
+
return error;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toRecord(list: Array<{ name: string; value: string }>) {
|
|
47
|
+
const record: Record<string, string> = {};
|
|
48
|
+
for (const { name, value } of list) {
|
|
49
|
+
record[name] = value;
|
|
50
|
+
}
|
|
51
|
+
return record;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function download(
|
|
55
|
+
url: string,
|
|
56
|
+
path: string,
|
|
57
|
+
options: DownloadOptions = {},
|
|
58
|
+
): Promise<DownloadResult> {
|
|
59
|
+
const { signal, onProgress } = options;
|
|
60
|
+
if (signal?.aborted) {
|
|
61
|
+
throw abortError();
|
|
62
|
+
}
|
|
63
|
+
const target = toPath(path);
|
|
64
|
+
const id = nextId++;
|
|
65
|
+
const subscription = onProgress
|
|
66
|
+
? NativeFs.onDownloadProgress(event => {
|
|
67
|
+
if (event.id === id) {
|
|
68
|
+
onProgress(event.received, event.total);
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
: null;
|
|
72
|
+
const onAbort = () => {
|
|
73
|
+
NativeFs.cancelDownload(id);
|
|
74
|
+
};
|
|
75
|
+
signal?.addEventListener('abort', onAbort);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const result = await NativeFs.download(url, target, {
|
|
79
|
+
id,
|
|
80
|
+
method: options.method ?? 'GET',
|
|
81
|
+
headers: Object.entries(options.headers ?? {}).map(([name, value]) => ({
|
|
82
|
+
name,
|
|
83
|
+
value,
|
|
84
|
+
})),
|
|
85
|
+
});
|
|
86
|
+
const headers = toRecord(result.headers);
|
|
87
|
+
if (result.status < 200 || result.status >= 300) {
|
|
88
|
+
const error = nodeError(
|
|
89
|
+
'ERR_HTTP_STATUS',
|
|
90
|
+
`HTTP ${result.status} downloading ${url}`,
|
|
91
|
+
) as DownloadHttpError;
|
|
92
|
+
error.status = result.status;
|
|
93
|
+
error.headers = headers;
|
|
94
|
+
error.body = result.body ?? '';
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
return { status: result.status, headers, bytes: result.bytes };
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (signal?.aborted) {
|
|
100
|
+
throw abortError();
|
|
101
|
+
}
|
|
102
|
+
if ((error as NodeError)?.code === 'ABORT_ERR') {
|
|
103
|
+
throw abortError();
|
|
104
|
+
}
|
|
105
|
+
throw withContext(error, 'download', target);
|
|
106
|
+
} finally {
|
|
107
|
+
signal?.removeEventListener('abort', onAbort);
|
|
108
|
+
subscription?.remove();
|
|
109
|
+
}
|
|
110
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
type Encoding,
|
|
39
39
|
type NodeError,
|
|
40
40
|
} from './shared';
|
|
41
|
+
import { download } from './download';
|
|
42
|
+
import { upload } from './upload';
|
|
41
43
|
import { watch } from './watch';
|
|
42
44
|
|
|
43
45
|
export type {
|
|
@@ -50,13 +52,28 @@ export type {
|
|
|
50
52
|
WriteOptions,
|
|
51
53
|
} from './promises';
|
|
52
54
|
export type { BinaryData, Encoding, NodeError } from './shared';
|
|
55
|
+
export type { UploadOptions, UploadResult } from './upload';
|
|
56
|
+
export type {
|
|
57
|
+
DownloadHttpError,
|
|
58
|
+
DownloadOptions,
|
|
59
|
+
DownloadResult,
|
|
60
|
+
} from './download';
|
|
53
61
|
export type {
|
|
54
62
|
FSWatcher,
|
|
55
63
|
WatchEventType,
|
|
56
64
|
WatchListener,
|
|
57
65
|
WatchOptions,
|
|
58
66
|
} from './watch';
|
|
59
|
-
export {
|
|
67
|
+
export {
|
|
68
|
+
Dirent,
|
|
69
|
+
Stats,
|
|
70
|
+
FileHandle,
|
|
71
|
+
promises,
|
|
72
|
+
constants,
|
|
73
|
+
watch,
|
|
74
|
+
download,
|
|
75
|
+
upload,
|
|
76
|
+
};
|
|
60
77
|
|
|
61
78
|
/* -------------------------------------------------------------------------- */
|
|
62
79
|
/* Dialogs */
|
|
@@ -748,6 +765,8 @@ const fs = {
|
|
|
748
765
|
open,
|
|
749
766
|
reveal,
|
|
750
767
|
openExternal,
|
|
768
|
+
download,
|
|
769
|
+
upload,
|
|
751
770
|
|
|
752
771
|
readFileSync,
|
|
753
772
|
writeFileSync,
|
package/src/upload.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `upload`: a file, or a range of one, streamed as an HTTP request body.
|
|
3
|
+
*
|
|
4
|
+
* The native side reads the file in small blocks while the connection takes
|
|
5
|
+
* them, so nothing larger than a block is ever held in memory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import NativeFs from './NativeFs';
|
|
9
|
+
import { nodeError, toPath, withContext, type NodeError } from './shared';
|
|
10
|
+
|
|
11
|
+
export interface UploadOptions {
|
|
12
|
+
/** `PUT` unless set. */
|
|
13
|
+
method?: string;
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
/** Where in the file the body starts. Zero unless set. */
|
|
16
|
+
offset?: number;
|
|
17
|
+
/** How many bytes to send. Everything from `offset` unless set. */
|
|
18
|
+
length?: number;
|
|
19
|
+
/** Called while the body is sent. */
|
|
20
|
+
onProgress?: (sent: number, total: number) => void;
|
|
21
|
+
/** Aborting rejects with an `AbortError`. */
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UploadResult {
|
|
26
|
+
status: number;
|
|
27
|
+
/** Response headers, lower-cased; repeated headers are joined with `, `. */
|
|
28
|
+
headers: Record<string, string>;
|
|
29
|
+
/** The first part of the response body. */
|
|
30
|
+
body: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let nextId = 1;
|
|
34
|
+
|
|
35
|
+
function abortError(): NodeError {
|
|
36
|
+
const error = nodeError('ABORT_ERR', 'The upload was aborted');
|
|
37
|
+
error.name = 'AbortError';
|
|
38
|
+
return error;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function toRecord(list: Array<{ name: string; value: string }>) {
|
|
42
|
+
const record: Record<string, string> = {};
|
|
43
|
+
for (const { name, value } of list) {
|
|
44
|
+
record[name] = value;
|
|
45
|
+
}
|
|
46
|
+
return record;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function upload(
|
|
50
|
+
url: string,
|
|
51
|
+
path: string,
|
|
52
|
+
options: UploadOptions = {},
|
|
53
|
+
): Promise<UploadResult> {
|
|
54
|
+
const { signal, onProgress } = options;
|
|
55
|
+
if (signal?.aborted) {
|
|
56
|
+
throw abortError();
|
|
57
|
+
}
|
|
58
|
+
const source = toPath(path);
|
|
59
|
+
const id = nextId++;
|
|
60
|
+
const subscription = onProgress
|
|
61
|
+
? NativeFs.onUploadProgress(event => {
|
|
62
|
+
if (event.id === id) {
|
|
63
|
+
onProgress(event.sent, event.total);
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
: null;
|
|
67
|
+
const onAbort = () => {
|
|
68
|
+
NativeFs.cancelUpload(id);
|
|
69
|
+
};
|
|
70
|
+
signal?.addEventListener('abort', onAbort);
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const result = await NativeFs.upload(url, source, {
|
|
74
|
+
id,
|
|
75
|
+
method: options.method ?? 'PUT',
|
|
76
|
+
headers: Object.entries(options.headers ?? {}).map(([name, value]) => ({
|
|
77
|
+
name,
|
|
78
|
+
value,
|
|
79
|
+
})),
|
|
80
|
+
offset: options.offset ?? 0,
|
|
81
|
+
length: options.length ?? -1,
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
status: result.status,
|
|
85
|
+
headers: toRecord(result.headers),
|
|
86
|
+
body: result.body,
|
|
87
|
+
};
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (signal?.aborted || (error as NodeError)?.code === 'ABORT_ERR') {
|
|
90
|
+
throw abortError();
|
|
91
|
+
}
|
|
92
|
+
throw withContext(error, 'upload', source);
|
|
93
|
+
} finally {
|
|
94
|
+
signal?.removeEventListener('abort', onAbort);
|
|
95
|
+
subscription?.remove();
|
|
96
|
+
}
|
|
97
|
+
}
|