@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
package/src/stats.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Stats` and `Dirent`, built over the plain objects the native module returns.
|
|
3
|
+
*
|
|
4
|
+
* The module crosses the bridge with numbers rather than methods — a generated
|
|
5
|
+
* TurboModule result cannot carry predicates — so the classes live here and
|
|
6
|
+
* reproduce the shape Node's callers test for. The `kind` field is the coarse
|
|
7
|
+
* classification the native side can make without a platform syscall; `mode` is
|
|
8
|
+
* there for the finer predicates, and is zero on Windows where there are no
|
|
9
|
+
* Unix mode bits at all.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { constants } from './constants';
|
|
13
|
+
|
|
14
|
+
/** The native `FileKind` numbers; must match `module::kind` in Rust. */
|
|
15
|
+
export const FILE_KIND = {
|
|
16
|
+
file: 0,
|
|
17
|
+
directory: 1,
|
|
18
|
+
symlink: 2,
|
|
19
|
+
other: 3,
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
/** The shape `stat`, `lstat` and `fstat` return across the bridge. */
|
|
23
|
+
export interface StatResult {
|
|
24
|
+
size: number;
|
|
25
|
+
mode: number;
|
|
26
|
+
kind: number;
|
|
27
|
+
atimeMs: number;
|
|
28
|
+
mtimeMs: number;
|
|
29
|
+
ctimeMs: number;
|
|
30
|
+
birthtimeMs: number;
|
|
31
|
+
dev: number;
|
|
32
|
+
ino: number;
|
|
33
|
+
nlink: number;
|
|
34
|
+
uid: number;
|
|
35
|
+
gid: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The shape `readdir` returns across the bridge. */
|
|
39
|
+
export interface DirEntryResult {
|
|
40
|
+
name: string;
|
|
41
|
+
kind: number;
|
|
42
|
+
parentPath: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The type bits of a mode, or 0 when the platform does not report them. */
|
|
46
|
+
function fileTypeBits(mode: number): number {
|
|
47
|
+
return mode & constants.S_IFMT;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Node's `fs.Stats`.
|
|
52
|
+
*
|
|
53
|
+
* The fields are plain properties rather than getters: they are copied once from
|
|
54
|
+
* the native result, and a caller destructuring the object sees the same values
|
|
55
|
+
* Node's does.
|
|
56
|
+
*/
|
|
57
|
+
export class Stats {
|
|
58
|
+
readonly dev: number;
|
|
59
|
+
readonly ino: number;
|
|
60
|
+
readonly mode: number;
|
|
61
|
+
readonly nlink: number;
|
|
62
|
+
readonly uid: number;
|
|
63
|
+
readonly gid: number;
|
|
64
|
+
readonly rdev: number;
|
|
65
|
+
readonly size: number;
|
|
66
|
+
readonly blksize: number;
|
|
67
|
+
readonly blocks: number;
|
|
68
|
+
|
|
69
|
+
readonly atimeMs: number;
|
|
70
|
+
readonly mtimeMs: number;
|
|
71
|
+
readonly ctimeMs: number;
|
|
72
|
+
readonly birthtimeMs: number;
|
|
73
|
+
|
|
74
|
+
readonly atime: Date;
|
|
75
|
+
readonly mtime: Date;
|
|
76
|
+
readonly ctime: Date;
|
|
77
|
+
readonly birthtime: Date;
|
|
78
|
+
|
|
79
|
+
/** The native classification, kept because it answers predicates the mode
|
|
80
|
+
* cannot on platforms that report no type bits. */
|
|
81
|
+
private readonly kind: number;
|
|
82
|
+
|
|
83
|
+
constructor(result: StatResult) {
|
|
84
|
+
this.dev = result.dev;
|
|
85
|
+
this.ino = result.ino;
|
|
86
|
+
this.mode = result.mode;
|
|
87
|
+
this.nlink = result.nlink;
|
|
88
|
+
this.uid = result.uid;
|
|
89
|
+
this.gid = result.gid;
|
|
90
|
+
// The native result has no `rdev`, block size or block count; Node reports
|
|
91
|
+
// zero for them where the platform cannot say, so zero is the honest value.
|
|
92
|
+
this.rdev = 0;
|
|
93
|
+
this.size = result.size;
|
|
94
|
+
this.blksize = 0;
|
|
95
|
+
this.blocks = 0;
|
|
96
|
+
|
|
97
|
+
this.atimeMs = result.atimeMs;
|
|
98
|
+
this.mtimeMs = result.mtimeMs;
|
|
99
|
+
this.ctimeMs = result.ctimeMs;
|
|
100
|
+
this.birthtimeMs = result.birthtimeMs;
|
|
101
|
+
|
|
102
|
+
this.atime = new Date(result.atimeMs);
|
|
103
|
+
this.mtime = new Date(result.mtimeMs);
|
|
104
|
+
this.ctime = new Date(result.ctimeMs);
|
|
105
|
+
this.birthtime = new Date(result.birthtimeMs);
|
|
106
|
+
|
|
107
|
+
this.kind = result.kind;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isFile(): boolean {
|
|
111
|
+
const bits = fileTypeBits(this.mode);
|
|
112
|
+
return bits === 0
|
|
113
|
+
? this.kind === FILE_KIND.file
|
|
114
|
+
: bits === constants.S_IFREG;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
isDirectory(): boolean {
|
|
118
|
+
const bits = fileTypeBits(this.mode);
|
|
119
|
+
return bits === 0
|
|
120
|
+
? this.kind === FILE_KIND.directory
|
|
121
|
+
: bits === constants.S_IFDIR;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
isSymbolicLink(): boolean {
|
|
125
|
+
const bits = fileTypeBits(this.mode);
|
|
126
|
+
return bits === 0
|
|
127
|
+
? this.kind === FILE_KIND.symlink
|
|
128
|
+
: bits === constants.S_IFLNK;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
isBlockDevice(): boolean {
|
|
132
|
+
return fileTypeBits(this.mode) === constants.S_IFBLK;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
isCharacterDevice(): boolean {
|
|
136
|
+
return fileTypeBits(this.mode) === constants.S_IFCHR;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
isFIFO(): boolean {
|
|
140
|
+
return fileTypeBits(this.mode) === constants.S_IFIFO;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
isSocket(): boolean {
|
|
144
|
+
return fileTypeBits(this.mode) === constants.S_IFSOCK;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Node's `fs.Dirent`.
|
|
150
|
+
*
|
|
151
|
+
* `parentPath` is `''` when the caller already held the parent, matching what
|
|
152
|
+
* `readdir` with `{withFileTypes: true}` produces there.
|
|
153
|
+
*/
|
|
154
|
+
export class Dirent {
|
|
155
|
+
readonly name: string;
|
|
156
|
+
readonly parentPath: string;
|
|
157
|
+
readonly path: string;
|
|
158
|
+
|
|
159
|
+
private readonly kind: number;
|
|
160
|
+
|
|
161
|
+
constructor(result: DirEntryResult) {
|
|
162
|
+
this.name = result.name;
|
|
163
|
+
this.parentPath = result.parentPath;
|
|
164
|
+
// `path` is the older name for the same field, and libraries still read it.
|
|
165
|
+
this.path = result.parentPath;
|
|
166
|
+
this.kind = result.kind;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
isFile(): boolean {
|
|
170
|
+
return this.kind === FILE_KIND.file;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
isDirectory(): boolean {
|
|
174
|
+
return this.kind === FILE_KIND.directory;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
isSymbolicLink(): boolean {
|
|
178
|
+
return this.kind === FILE_KIND.symlink;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
isBlockDevice(): boolean {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
isCharacterDevice(): boolean {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
isFIFO(): boolean {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
isSocket(): boolean {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
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
|
+
}
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `watch`: the JS half of the typed watcher event.
|
|
3
|
+
*
|
|
4
|
+
* The native module returns a watcher id; every event it produces carries that
|
|
5
|
+
* id, so one shared emitter serves every open watcher and each listener keeps
|
|
6
|
+
* only its own. The subscription is taken through the generated typed emitter on
|
|
7
|
+
* the module (`NativeFs.onWatchEvent`), which is the same mechanism the fixture's
|
|
8
|
+
* `onProgress` uses in `RustTurboModuleFixture.tsx`, rather than through
|
|
9
|
+
* `NativeEventEmitter`: the spec declares `onWatchEvent` as a typed
|
|
10
|
+
* `CodegenTypes.EventEmitter`, and the bridge installs a listener for exactly
|
|
11
|
+
* that; `NativeEventEmitter` would subscribe on the global device-emitter channel
|
|
12
|
+
* that only `sendEvent` writes to, so it would never fire.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import NativeFs, { type WatchEvent } from './NativeFs';
|
|
16
|
+
import { toPath, withContext, type NodeError } from './shared';
|
|
17
|
+
|
|
18
|
+
/** The event names Node uses for the two things a watcher reports. */
|
|
19
|
+
export type WatchEventType = 'rename' | 'change';
|
|
20
|
+
|
|
21
|
+
export interface WatchOptions {
|
|
22
|
+
recursive?: boolean;
|
|
23
|
+
encoding?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FSWatcher {
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type WatchListener = (
|
|
31
|
+
eventType: WatchEventType,
|
|
32
|
+
filename: string | null,
|
|
33
|
+
) => void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Which of Node's two event types a native kind is.
|
|
37
|
+
*
|
|
38
|
+
* Node folds everything it sees into `rename` (the entry appeared, vanished or
|
|
39
|
+
* moved) and `change` (its contents or metadata changed). The caller's callback
|
|
40
|
+
* signature is Node's, so the mapping happens here while the native kind stays
|
|
41
|
+
* available to anyone who subscribes to the module directly.
|
|
42
|
+
*/
|
|
43
|
+
function eventTypeOf(kind: string): WatchEventType {
|
|
44
|
+
return kind === 'modify' ? 'change' : 'rename';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The file a path names, relative to the watched root where it is below it. */
|
|
48
|
+
/**
|
|
49
|
+
* The filename a listener is given.
|
|
50
|
+
*
|
|
51
|
+
* The native side already reported it relative to the watch root, and its own
|
|
52
|
+
* name for a single-file watch: it canonicalises the root, which is where macOS
|
|
53
|
+
* decides between `/var` and `/private/var`, and it strips whichever spelling the
|
|
54
|
+
* event actually carries. Stripping a prefix here as well would be a second,
|
|
55
|
+
* weaker guess at the same question — a relative root never matches, and Windows
|
|
56
|
+
* separates with `\` — so the event path is passed through unchanged.
|
|
57
|
+
*/
|
|
58
|
+
function fileNameOf(eventPath: string): string {
|
|
59
|
+
return eventPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Starts a watcher.
|
|
64
|
+
*
|
|
65
|
+
* The id arrives asynchronously, but the caller expects a handle it can close
|
|
66
|
+
* immediately, so the handle is created first and closing it before the id is
|
|
67
|
+
* known sets a flag the resolution honours. That is the one ordering hazard in
|
|
68
|
+
* this API, and it is invisible from the outside.
|
|
69
|
+
*
|
|
70
|
+
* Node reports a failure to start through an `error` event on the watcher. This
|
|
71
|
+
* handle has no event surface, so the failure is held and rejected from
|
|
72
|
+
* `close()` instead: it can then never become an unhandled rejection, and a
|
|
73
|
+
* caller that closes what it opened is guaranteed to see it.
|
|
74
|
+
*/
|
|
75
|
+
export function watch(path: string, listener: WatchListener): FSWatcher;
|
|
76
|
+
export function watch(
|
|
77
|
+
path: string,
|
|
78
|
+
options: WatchOptions | null,
|
|
79
|
+
listener: WatchListener,
|
|
80
|
+
): FSWatcher;
|
|
81
|
+
export function watch(
|
|
82
|
+
path: string,
|
|
83
|
+
optionsOrListener: WatchOptions | null | WatchListener,
|
|
84
|
+
maybeListener?: WatchListener,
|
|
85
|
+
): FSWatcher {
|
|
86
|
+
const options =
|
|
87
|
+
typeof optionsOrListener === 'function' ? {} : (optionsOrListener ?? {});
|
|
88
|
+
const listener =
|
|
89
|
+
typeof optionsOrListener === 'function' ? optionsOrListener : maybeListener;
|
|
90
|
+
if (typeof listener !== 'function') {
|
|
91
|
+
throw new TypeError('watch requires a listener function');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const target = toPath(path);
|
|
95
|
+
let id: number | null = null;
|
|
96
|
+
let closed = false;
|
|
97
|
+
let failure: NodeError | null = null;
|
|
98
|
+
let subscription: { remove(): void } | null = null;
|
|
99
|
+
|
|
100
|
+
const ready = NativeFs.watch(target, options.recursive ?? false)
|
|
101
|
+
.then(watcherId => {
|
|
102
|
+
id = watcherId;
|
|
103
|
+
if (closed) {
|
|
104
|
+
// `close()` ran while the id was in flight; stop the native watcher now
|
|
105
|
+
// that there is something to stop.
|
|
106
|
+
NativeFs.unwatch(watcherId);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
subscription = NativeFs.onWatchEvent((event: WatchEvent) => {
|
|
110
|
+
if (event.id !== id) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// The encoding option only affects how a non-ASCII name is decoded, and
|
|
114
|
+
// every name already arrives as a JS string.
|
|
115
|
+
listener(eventTypeOf(event.kind), fileNameOf(event.path));
|
|
116
|
+
});
|
|
117
|
+
})
|
|
118
|
+
.catch((error: unknown) => {
|
|
119
|
+
failure = withContext(error, 'watch', target);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
async close(): Promise<void> {
|
|
124
|
+
closed = true;
|
|
125
|
+
subscription?.remove();
|
|
126
|
+
subscription = null;
|
|
127
|
+
if (id !== null) {
|
|
128
|
+
NativeFs.unwatch(id);
|
|
129
|
+
id = null;
|
|
130
|
+
}
|
|
131
|
+
// Awaiting the start makes `close()` immediately after `watch()` still stop
|
|
132
|
+
// the native watcher rather than racing it.
|
|
133
|
+
await ready;
|
|
134
|
+
if (failure !== null) {
|
|
135
|
+
const error = failure;
|
|
136
|
+
failure = null;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|