@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/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');
@@ -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 { Dirent, Stats, FileHandle, promises, constants, watch };
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
+ }