@gjsify/fs 0.1.15 → 0.2.0
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/lib/esm/callback.js +22 -13
- package/lib/esm/cp.js +253 -0
- package/lib/esm/dir.js +160 -0
- package/lib/esm/fd-ops.js +189 -0
- package/lib/esm/file-handle.js +263 -84
- package/lib/esm/fs-watcher.js +88 -4
- package/lib/esm/glob.js +164 -0
- package/lib/esm/index.js +128 -2
- package/lib/esm/promises.js +90 -27
- package/lib/esm/read-stream.js +53 -43
- package/lib/esm/stat-watcher.js +121 -0
- package/lib/esm/statfs.js +57 -0
- package/lib/esm/sync.js +70 -52
- package/lib/esm/utils.js +7 -0
- package/lib/esm/utimes.js +62 -0
- package/lib/esm/write-stream.js +2 -5
- package/lib/types/cp.d.ts +18 -0
- package/lib/types/cp.spec.d.ts +2 -0
- package/lib/types/dir.d.ts +29 -0
- package/lib/types/dir.spec.d.ts +2 -0
- package/lib/types/fd-ops.d.ts +57 -0
- package/lib/types/fd-ops.spec.d.ts +2 -0
- package/lib/types/file-handle.d.ts +34 -4
- package/lib/types/fs-watcher.d.ts +9 -2
- package/lib/types/glob.d.ts +8 -0
- package/lib/types/glob.spec.d.ts +2 -0
- package/lib/types/index.d.ts +51 -1
- package/lib/types/promises.d.ts +31 -4
- package/lib/types/read-stream.d.ts +3 -1
- package/lib/types/stat-watcher.d.ts +21 -0
- package/lib/types/statfs.d.ts +35 -0
- package/lib/types/statfs.spec.d.ts +2 -0
- package/lib/types/sync.d.ts +4 -7
- package/lib/types/utils.d.ts +2 -0
- package/lib/types/utimes.d.ts +13 -0
- package/lib/types/utimes.spec.d.ts +2 -0
- package/lib/types/watch.spec.d.ts +2 -0
- package/lib/types/watchfile.spec.d.ts +2 -0
- package/lib/types/write-stream.d.ts +1 -2
- package/package.json +12 -12
- package/src/callback.ts +22 -13
- package/src/cp.spec.ts +181 -0
- package/src/cp.ts +328 -0
- package/src/dir.spec.ts +204 -0
- package/src/dir.ts +199 -0
- package/src/fd-ops.spec.ts +234 -0
- package/src/fd-ops.ts +251 -0
- package/src/file-handle.ts +264 -94
- package/src/fs-watcher.ts +101 -6
- package/src/glob.spec.ts +201 -0
- package/src/glob.ts +205 -0
- package/src/index.ts +74 -0
- package/src/promises.ts +94 -29
- package/src/read-stream.ts +49 -43
- package/src/stat-watcher.ts +116 -0
- package/src/statfs.spec.ts +67 -0
- package/src/statfs.ts +92 -0
- package/src/streams.spec.ts +58 -0
- package/src/sync.ts +75 -57
- package/src/test.mts +13 -2
- package/src/utils.ts +10 -0
- package/src/utimes.spec.ts +113 -0
- package/src/utimes.ts +97 -0
- package/src/watch.spec.ts +171 -0
- package/src/watchfile.spec.ts +185 -0
- package/src/write-stream.ts +5 -8
- package/tsconfig.tsbuildinfo +1 -1
package/src/file-handle.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Reference: Node.js lib/internal/fs/promises.js (FileHandle)
|
|
2
2
|
// Reimplemented for GJS using Gio.File
|
|
3
3
|
|
|
4
|
-
import { warnNotImplemented, notImplemented } from '@gjsify/utils';
|
|
4
|
+
import { warnNotImplemented, notImplemented, createGLibFileError } from '@gjsify/utils';
|
|
5
5
|
import { ReadStream } from "./read-stream.js";
|
|
6
6
|
import { WriteStream } from "./write-stream.js";
|
|
7
|
-
import { Stats } from "./stats.js";
|
|
7
|
+
import { Stats, BigIntStats, STAT_ATTRIBUTES } from "./stats.js";
|
|
8
8
|
import { getEncodingFromOptions, encodeUint8Array } from './encoding.js';
|
|
9
|
+
import { normalizePath } from './utils.js';
|
|
10
|
+
import { chmodSync, chownSync } from './sync.js';
|
|
9
11
|
import GLib from '@girs/glib-2.0';
|
|
10
12
|
import Gio from '@girs/gio-2.0';
|
|
13
|
+
import { createInterface } from 'node:readline';
|
|
11
14
|
// Type-only import for ReadableStream — the runtime constructor is resolved
|
|
12
15
|
// via globalThis inside readableWebStream() to avoid bundling the entire
|
|
13
16
|
// WHATWG streams implementation for apps that never call this method.
|
|
@@ -28,51 +31,69 @@ import type {
|
|
|
28
31
|
OpenMode,
|
|
29
32
|
PathLike,
|
|
30
33
|
StatOptions,
|
|
31
|
-
BigIntStats,
|
|
32
34
|
WriteVResult,
|
|
33
35
|
ReadVResult,
|
|
34
36
|
ReadPosition,
|
|
35
37
|
} from 'node:fs';
|
|
36
38
|
import type { Interface as ReadlineInterface } from 'node:readline';
|
|
37
39
|
|
|
40
|
+
// POSIX numeric open(2) flags (values on Linux x86-64).
|
|
41
|
+
const O_WRONLY = 1;
|
|
42
|
+
const O_RDWR = 2;
|
|
43
|
+
const O_CREAT = 64;
|
|
44
|
+
const O_TRUNC = 512;
|
|
45
|
+
const O_APPEND = 1024;
|
|
46
|
+
|
|
47
|
+
type IOMode = 'r' | 'r+' | 'w' | 'w+' | 'a' | 'a+';
|
|
48
|
+
|
|
38
49
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
50
|
+
* Convert open flags (Node.js string or POSIX numeric) to a GLib.IOChannel mode.
|
|
51
|
+
* IOChannel.new_file() takes fopen(3) modes: 'r', 'r+', 'w', 'w+', 'a', 'a+'.
|
|
41
52
|
*/
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
function resolveIOMode(flags: OpenFlags | number | undefined): IOMode {
|
|
54
|
+
if (flags === undefined || flags === null) return 'r';
|
|
55
|
+
if (typeof flags === 'number') {
|
|
56
|
+
const rdwr = (flags & O_RDWR) !== 0;
|
|
57
|
+
const wronly = (flags & O_WRONLY) !== 0;
|
|
58
|
+
const append = (flags & O_APPEND) !== 0;
|
|
59
|
+
const trunc = (flags & O_TRUNC) !== 0;
|
|
60
|
+
if (rdwr) return trunc ? 'w+' : 'r+';
|
|
61
|
+
if (wronly) return append ? 'a' : 'w';
|
|
62
|
+
return 'r';
|
|
63
|
+
}
|
|
64
|
+
// Node.js string flags — map extras to IOChannel equivalents.
|
|
65
|
+
switch (flags) {
|
|
66
|
+
case 'ax': case 'wx': return 'w';
|
|
67
|
+
case 'ax+': case 'wx+': return 'w+';
|
|
68
|
+
case 'as': case 'rs+': return 'r+';
|
|
69
|
+
case 'as+': return 'a+';
|
|
70
|
+
default: return flags as IOMode;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Open the file with the given IOChannel mode. When the flags request
|
|
76
|
+
* create-if-missing + read/write without truncation (numeric O_CREAT | O_RDWR,
|
|
77
|
+
* which maps to IOChannel 'r+' — a mode that requires the file to exist), we
|
|
78
|
+
* catch the ENOENT and create an empty file, then retry. This avoids a TOCTOU
|
|
79
|
+
* existence check and keeps the common "file exists" path to a single syscall.
|
|
80
|
+
*/
|
|
81
|
+
function openIOChannel(path: string, mode: IOMode, creat: boolean): GLib.IOChannel {
|
|
82
|
+
try {
|
|
83
|
+
return GLib.IOChannel.new_file(path, mode);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const gErr = err as { code?: number } | null | undefined;
|
|
86
|
+
if (creat && mode === 'r+' && gErr?.code === GLib.FileError.NOENT) {
|
|
87
|
+
GLib.file_set_contents(path, new Uint8Array(0));
|
|
88
|
+
return GLib.IOChannel.new_file(path, mode);
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
64
93
|
|
|
65
94
|
function mapOpenError(err: unknown, path: string): NodeJS.ErrnoException {
|
|
66
|
-
const gErr = err as { code?: number; message?: string } | null | undefined;
|
|
67
|
-
const msg = gErr?.message ?? '';
|
|
68
95
|
// GLib.IOChannel.new_file() always throws GLib.FileError (not Gio.IOErrorEnum).
|
|
69
|
-
|
|
70
|
-
const code = GLIB_FILE_ERROR_TO_NODE[gErr?.code ?? -1] ?? 'EIO';
|
|
71
|
-
const error = new Error(`${code}: ${msg || 'unknown error'}, open '${path}'`) as NodeJS.ErrnoException;
|
|
72
|
-
error.code = code;
|
|
73
|
-
error.syscall = 'open';
|
|
74
|
-
error.path = path;
|
|
75
|
-
return error;
|
|
96
|
+
return createGLibFileError(err, 'open', { path }) as NodeJS.ErrnoException;
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
export class FileHandle implements IFileHandle {
|
|
@@ -80,29 +101,104 @@ export class FileHandle implements IFileHandle {
|
|
|
80
101
|
/** Not part of the default implementation, used internal by gjsify */
|
|
81
102
|
private _file: GLib.IOChannel;
|
|
82
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Lazily-opened Gio streams for positional read() / write() so each call does
|
|
106
|
+
* not re-load the entire file via Gio.File.load_contents(). The IOStream is
|
|
107
|
+
* used when the handle was opened with write capability (r+, w, w+, a, a+) —
|
|
108
|
+
* it shares seek state between input and output so writes are visible to
|
|
109
|
+
* subsequent reads without a flush. For read-only handles we only open a
|
|
110
|
+
* FileInputStream; trying to open_readwrite on a read-only file can fall
|
|
111
|
+
* back to create_readwrite(REPLACE_DESTINATION) which would truncate it.
|
|
112
|
+
*/
|
|
113
|
+
private _ioStream: Gio.FileIOStream | null = null;
|
|
114
|
+
private _readStream: Gio.FileInputStream | null = null;
|
|
115
|
+
private readonly _gFile: Gio.File;
|
|
116
|
+
private readonly _ioMode: IOMode;
|
|
117
|
+
// Serialize async I/O on the shared FileIOStream. Concurrent write_bytes_async
|
|
118
|
+
// calls hit Gio.IOErrorEnum.PENDING ("Datenstrom hat noch einen ausstehenden
|
|
119
|
+
// Vorgang"); overlapping seek()s on the shared cursor also corrupt positions.
|
|
120
|
+
// random-access-file (used by fs-chunk-store / webtorrent) issues many
|
|
121
|
+
// concurrent positional writes, so every async op on this handle chains
|
|
122
|
+
// through _ioLock.
|
|
123
|
+
private _ioLock: Promise<unknown> = Promise.resolve();
|
|
124
|
+
|
|
83
125
|
/** Not part of the default implementation, used internal by gjsify */
|
|
84
126
|
private static instances: {[fd: number]: FileHandle} = {};
|
|
85
127
|
|
|
86
128
|
constructor(readonly options: {
|
|
87
129
|
path: PathLike,
|
|
88
|
-
flags?: OpenFlags,
|
|
130
|
+
flags?: OpenFlags | number,
|
|
89
131
|
mode?: Mode
|
|
90
132
|
}) {
|
|
91
133
|
this.options.flags ||= "r";
|
|
92
134
|
this.options.mode ||= 0o666;
|
|
135
|
+
const pathStr = normalizePath(options.path);
|
|
136
|
+
const creat = typeof options.flags === 'number' && (options.flags & O_CREAT) !== 0;
|
|
137
|
+
const ioMode = resolveIOMode(options.flags);
|
|
93
138
|
try {
|
|
94
|
-
this._file =
|
|
139
|
+
this._file = openIOChannel(pathStr, ioMode, creat);
|
|
95
140
|
} catch (err: unknown) {
|
|
96
|
-
throw mapOpenError(err,
|
|
141
|
+
throw mapOpenError(err, pathStr);
|
|
97
142
|
}
|
|
98
143
|
// Binary mode: prevent GLib from doing any character set conversion.
|
|
99
144
|
this._file.set_encoding(null as unknown as string);
|
|
100
145
|
this.fd = this._file.unix_get_fd();
|
|
146
|
+
this._gFile = Gio.File.new_for_path(pathStr);
|
|
147
|
+
this._ioMode = ioMode;
|
|
101
148
|
|
|
102
149
|
FileHandle.instances[this.fd] = this;
|
|
103
150
|
return FileHandle.getInstance(this.fd);
|
|
104
151
|
}
|
|
105
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Lazy-open the read-capable stream and return both the input stream and
|
|
155
|
+
* its seekable view. Both FileInputStream (read-only handle) and
|
|
156
|
+
* FileIOStream (read/write handle) implement Gio.Seekable, but we return
|
|
157
|
+
* both to avoid callers needing to know which concrete type they got.
|
|
158
|
+
*/
|
|
159
|
+
private _getReadStream(): { input: Gio.InputStream; seekable: Gio.Seekable } {
|
|
160
|
+
if (this._ioStream) {
|
|
161
|
+
return {
|
|
162
|
+
input: this._ioStream.get_input_stream(),
|
|
163
|
+
seekable: this._ioStream as unknown as Gio.Seekable,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (this._ioMode === 'r') {
|
|
167
|
+
if (!this._readStream) this._readStream = this._gFile.read(null);
|
|
168
|
+
return {
|
|
169
|
+
input: this._readStream,
|
|
170
|
+
seekable: this._readStream as unknown as Gio.Seekable,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// open_readwrite requires the file to exist. For modes that imply
|
|
174
|
+
// create-if-missing (w, w+, a, a+) the IOChannel already created it
|
|
175
|
+
// above; for 'r+' openIOChannel() catches ENOENT and pre-creates it.
|
|
176
|
+
this._ioStream = this._gFile.open_readwrite(null);
|
|
177
|
+
return {
|
|
178
|
+
input: this._ioStream.get_input_stream(),
|
|
179
|
+
seekable: this._ioStream as unknown as Gio.Seekable,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Lazy-open the write-capable stream (IOStream) for this handle. Only valid
|
|
184
|
+
* when the handle was opened with a write-capable mode. */
|
|
185
|
+
private _getWriteStream(): Gio.FileIOStream {
|
|
186
|
+
if (this._ioStream) return this._ioStream;
|
|
187
|
+
if (this._ioMode === 'r') {
|
|
188
|
+
throw new Error('FileHandle opened read-only; cannot write');
|
|
189
|
+
}
|
|
190
|
+
this._ioStream = this._gFile.open_readwrite(null);
|
|
191
|
+
return this._ioStream;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Serialize an async operation on the shared FileIOStream. */
|
|
195
|
+
private _serialize<T>(op: () => Promise<T>): Promise<T> {
|
|
196
|
+
const prev = this._ioLock;
|
|
197
|
+
const next = prev.catch(() => {}).then(op);
|
|
198
|
+
this._ioLock = next;
|
|
199
|
+
return next;
|
|
200
|
+
}
|
|
201
|
+
|
|
106
202
|
|
|
107
203
|
/**
|
|
108
204
|
* The numeric file descriptor managed by the {FileHandle} object.
|
|
@@ -150,7 +246,7 @@ export class FileHandle implements IFileHandle {
|
|
|
150
246
|
* @return Fulfills with `undefined` upon success.
|
|
151
247
|
*/
|
|
152
248
|
async chown(uid: number, gid: number): Promise<void> {
|
|
153
|
-
|
|
249
|
+
chownSync(normalizePath(this.options.path), uid, gid);
|
|
154
250
|
}
|
|
155
251
|
/**
|
|
156
252
|
* Modifies the permissions on the file. See [`chmod(2)`](http://man7.org/linux/man-pages/man2/chmod.2.html).
|
|
@@ -159,7 +255,7 @@ export class FileHandle implements IFileHandle {
|
|
|
159
255
|
* @return Fulfills with `undefined` upon success.
|
|
160
256
|
*/
|
|
161
257
|
async chmod(mode: Mode): Promise<void> {
|
|
162
|
-
|
|
258
|
+
chmodSync(normalizePath(this.options.path), mode);
|
|
163
259
|
}
|
|
164
260
|
/**
|
|
165
261
|
* Unlike the 16 kb default `highWaterMark` for a `stream.Readable`, the stream
|
|
@@ -245,7 +341,7 @@ export class FileHandle implements IFileHandle {
|
|
|
245
341
|
* @return Fulfills with `undefined` upon success.
|
|
246
342
|
*/
|
|
247
343
|
async datasync(): Promise<void> {
|
|
248
|
-
|
|
344
|
+
this._file.flush();
|
|
249
345
|
}
|
|
250
346
|
/**
|
|
251
347
|
* Request that all data for the open file descriptor is flushed to the storage
|
|
@@ -255,7 +351,7 @@ export class FileHandle implements IFileHandle {
|
|
|
255
351
|
* @return Fufills with `undefined` upon success.
|
|
256
352
|
*/
|
|
257
353
|
async sync(): Promise<void> {
|
|
258
|
-
|
|
354
|
+
this._file.flush();
|
|
259
355
|
}
|
|
260
356
|
/**
|
|
261
357
|
* Reads data from the file and stores that in the given buffer.
|
|
@@ -295,24 +391,25 @@ export class FileHandle implements IFileHandle {
|
|
|
295
391
|
const bufView = buffer as unknown as Uint8Array;
|
|
296
392
|
const bufOffset = offset ?? 0;
|
|
297
393
|
const readLength = length ?? bufView?.byteLength ?? 65536;
|
|
298
|
-
|
|
299
|
-
// Use Gio.File.load_contents for reliable position-based reads.
|
|
300
|
-
// GLib.IOChannel.read_chars() is not introspectable in GJS (caller-allocated buffer),
|
|
301
|
-
// and read_to_end() has stdio-buffer visibility issues after mixed write/read.
|
|
302
|
-
const gFile = Gio.File.new_for_path(this.options.path.toString());
|
|
303
|
-
const [, fileContents] = gFile.load_contents(null);
|
|
304
|
-
const fileData = fileContents as Uint8Array;
|
|
305
394
|
const startPos = (position as number | null) ?? 0;
|
|
306
|
-
const readData = fileData.slice(startPos, startPos + readLength);
|
|
307
|
-
const bytesRead = readData.length;
|
|
308
|
-
if (bufView && bytesRead > 0) {
|
|
309
|
-
bufView.set(readData, bufOffset);
|
|
310
|
-
}
|
|
311
395
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
396
|
+
// Positional read — seek + read_bytes on the appropriate Gio stream,
|
|
397
|
+
// touching only the requested region. Replaces the old load_contents()
|
|
398
|
+
// path that read the entire file on every call (O(N²) over streamed
|
|
399
|
+
// workloads like WebTorrent piece hashing or random-access-file).
|
|
400
|
+
// Serialized with writes: seek() on the shared FileIOStream cursor must
|
|
401
|
+
// not interleave with a pending write_bytes_async.
|
|
402
|
+
return this._serialize(async () => {
|
|
403
|
+
const { input, seekable } = this._getReadStream();
|
|
404
|
+
seekable.seek(BigInt(startPos), GLib.SeekType.SET, null);
|
|
405
|
+
const bytes = input.read_bytes(readLength, null);
|
|
406
|
+
const data = bytes.get_data() as Uint8Array | null;
|
|
407
|
+
const bytesRead = data?.length ?? 0;
|
|
408
|
+
if (bufView && data && bytesRead > 0) {
|
|
409
|
+
bufView.set(data, bufOffset);
|
|
410
|
+
}
|
|
411
|
+
return { bytesRead, buffer: buffer as T };
|
|
412
|
+
});
|
|
316
413
|
}
|
|
317
414
|
/**
|
|
318
415
|
* Returns a `ReadableStream` that may be used to read the files data.
|
|
@@ -426,7 +523,7 @@ export class FileHandle implements IFileHandle {
|
|
|
426
523
|
* @param options See `filehandle.createReadStream()` for the options.
|
|
427
524
|
*/
|
|
428
525
|
readLines(options?: CreateReadStreamOptions): ReadlineInterface {
|
|
429
|
-
|
|
526
|
+
return createInterface({ input: this.createReadStream(options), crlfDelay: Infinity });
|
|
430
527
|
}
|
|
431
528
|
/**
|
|
432
529
|
* @since v10.0.0
|
|
@@ -443,8 +540,15 @@ export class FileHandle implements IFileHandle {
|
|
|
443
540
|
}
|
|
444
541
|
): Promise<BigIntStats>
|
|
445
542
|
async stat(opts?: StatOptions): Promise<Stats | BigIntStats> {
|
|
446
|
-
|
|
447
|
-
|
|
543
|
+
const info = await new Promise<Gio.FileInfo>((resolve, reject) => {
|
|
544
|
+
this._gFile.query_info_async(STAT_ATTRIBUTES, Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null, (_s: unknown, res: Gio.AsyncResult) => {
|
|
545
|
+
try { resolve(this._gFile.query_info_finish(res)); } catch (e) { reject(e); }
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
const pathStr = normalizePath(this.options.path);
|
|
549
|
+
return opts?.bigint
|
|
550
|
+
? new BigIntStats(info, pathStr)
|
|
551
|
+
: new Stats(info, pathStr);
|
|
448
552
|
}
|
|
449
553
|
/**
|
|
450
554
|
* Truncates the file.
|
|
@@ -477,18 +581,18 @@ export class FileHandle implements IFileHandle {
|
|
|
477
581
|
async truncate(len: number = 0): Promise<void> {
|
|
478
582
|
const effectiveLen = Math.max(0, len);
|
|
479
583
|
this._file.flush();
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
gFile.replace_contents(newContent, null, false, Gio.FileCreateFlags.NONE, null);
|
|
584
|
+
// Gio.FileOutputStream implements Seekable.truncate — extends with zeros
|
|
585
|
+
// when growing, matches POSIX ftruncate(2).
|
|
586
|
+
const out = this._getWriteStream().get_output_stream() as Gio.FileOutputStream;
|
|
587
|
+
out.truncate(effectiveLen, null);
|
|
485
588
|
}
|
|
486
589
|
/**
|
|
487
590
|
* Change the file system timestamps of the object referenced by the `FileHandle` then resolves the promise with no arguments upon success.
|
|
488
591
|
* @since v10.0.0
|
|
489
592
|
*/
|
|
490
593
|
async utimes(atime: string | number | Date, mtime: string | number | Date): Promise<void> {
|
|
491
|
-
|
|
594
|
+
const { utimesSync } = await import('./utimes.js');
|
|
595
|
+
utimesSync(normalizePath(this.options.path), atime, mtime);
|
|
492
596
|
}
|
|
493
597
|
/**
|
|
494
598
|
* Asynchronously writes data to a file, replacing the file if it already exists.`data` can be a string, a buffer, an
|
|
@@ -600,25 +704,32 @@ export class FileHandle implements IFileHandle {
|
|
|
600
704
|
const writeSlice = writeBuf.slice(bufOffset, bufOffset + writeLength);
|
|
601
705
|
const writePos = position ?? 0;
|
|
602
706
|
|
|
603
|
-
//
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
707
|
+
// Positional write — seek + write_bytes_async on the IOStream, touches
|
|
708
|
+
// only the requested region. Uses async Gio I/O so the GLib main loop
|
|
709
|
+
// (and GTK events) are not blocked during the write. Serialized via
|
|
710
|
+
// _serialize() so concurrent callers (e.g. random-access-file) don't
|
|
711
|
+
// trigger GIO_ERROR_PENDING or corrupt the shared seek cursor.
|
|
712
|
+
const bytesWritten = await this._serialize(async () => {
|
|
713
|
+
const stream = this._getWriteStream();
|
|
714
|
+
stream.seek(BigInt(writePos), GLib.SeekType.SET, null);
|
|
715
|
+
const output = stream.get_output_stream();
|
|
716
|
+
const written = await new Promise<number>((resolve, reject) => {
|
|
717
|
+
output.write_bytes_async(new GLib.Bytes(writeSlice), GLib.PRIORITY_DEFAULT, null, (_source, asyncResult) => {
|
|
718
|
+
try { resolve(output.write_bytes_finish(asyncResult)); }
|
|
719
|
+
catch (err) { reject(err); }
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
await new Promise<void>((resolve, reject) => {
|
|
723
|
+
output.flush_async(GLib.PRIORITY_DEFAULT, null, (_source, asyncResult) => {
|
|
724
|
+
try { output.flush_finish(asyncResult); resolve(); }
|
|
725
|
+
catch (err) { reject(err); }
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
return written;
|
|
729
|
+
});
|
|
619
730
|
|
|
620
731
|
return {
|
|
621
|
-
bytesWritten
|
|
732
|
+
bytesWritten,
|
|
622
733
|
buffer: data
|
|
623
734
|
}
|
|
624
735
|
}
|
|
@@ -640,11 +751,16 @@ export class FileHandle implements IFileHandle {
|
|
|
640
751
|
* position.
|
|
641
752
|
*/
|
|
642
753
|
async writev<TBuffers extends readonly NodeJS.ArrayBufferView[]>(buffers: TBuffers, position?: number): Promise<WriteVResult<TBuffers>> {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
754
|
+
let bytesWritten = 0;
|
|
755
|
+
for (const buf of buffers) {
|
|
756
|
+
const b = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
757
|
+
const res = await this.write(
|
|
758
|
+
b, 0, b.byteLength,
|
|
759
|
+
position != null ? position + bytesWritten : null,
|
|
760
|
+
);
|
|
761
|
+
bytesWritten += res.bytesWritten;
|
|
647
762
|
}
|
|
763
|
+
return { bytesWritten, buffers: buffers as unknown as TBuffers };
|
|
648
764
|
}
|
|
649
765
|
/**
|
|
650
766
|
* Read from a file and write to an array of [ArrayBufferView](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView) s
|
|
@@ -653,12 +769,57 @@ export class FileHandle implements IFileHandle {
|
|
|
653
769
|
* @return Fulfills upon success an object containing two properties:
|
|
654
770
|
*/
|
|
655
771
|
async readv<TBuffers extends readonly NodeJS.ArrayBufferView[]>(buffers: TBuffers, position?: number): Promise<ReadVResult<TBuffers>> {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
772
|
+
let bytesRead = 0;
|
|
773
|
+
for (const buf of buffers) {
|
|
774
|
+
const res = await this.read({
|
|
775
|
+
buffer: Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength),
|
|
776
|
+
position: position != null ? position + bytesRead : null,
|
|
777
|
+
});
|
|
778
|
+
bytesRead += res.bytesRead;
|
|
779
|
+
if (res.bytesRead < buf.byteLength) break;
|
|
660
780
|
}
|
|
781
|
+
return { bytesRead, buffers: buffers as unknown as TBuffers };
|
|
661
782
|
}
|
|
783
|
+
/** @internal */ _flushSync(): void {
|
|
784
|
+
this._file.flush();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** @internal */ _closeSync(): void {
|
|
788
|
+
try { this._ioStream?.close(null); } catch { /* best-effort */ }
|
|
789
|
+
try { this._readStream?.close(null); } catch { /* best-effort */ }
|
|
790
|
+
this._ioStream = null;
|
|
791
|
+
this._readStream = null;
|
|
792
|
+
try { this._file.shutdown(true); } catch { /* best-effort */ }
|
|
793
|
+
delete (FileHandle as any).instances[this.fd];
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** @internal */ _readSync(buffer: NodeJS.ArrayBufferView, offset: number, length: number, position: number | null): number {
|
|
797
|
+
const stream = this._gFile.read(null);
|
|
798
|
+
try {
|
|
799
|
+
if (position !== null && position >= 0) {
|
|
800
|
+
(stream as unknown as Gio.Seekable).seek(position, GLib.SeekType.SET, null);
|
|
801
|
+
}
|
|
802
|
+
const bytes = stream.read_bytes(length, null);
|
|
803
|
+
const arr = bytes.get_data()!;
|
|
804
|
+
new Uint8Array((buffer as any).buffer, (buffer as any).byteOffset + offset).set(arr.subarray(0, arr.length));
|
|
805
|
+
return arr.length;
|
|
806
|
+
} finally {
|
|
807
|
+
stream.close(null);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/** @internal */ _writeSync(data: Uint8Array, position: number | null): number {
|
|
812
|
+
const stream = this._gFile.open_readwrite(null);
|
|
813
|
+
try {
|
|
814
|
+
if (position !== null && position >= 0) {
|
|
815
|
+
(stream as unknown as Gio.Seekable).seek(position, GLib.SeekType.SET, null);
|
|
816
|
+
}
|
|
817
|
+
return stream.get_output_stream().write_bytes(GLib.Bytes.new(data), null);
|
|
818
|
+
} finally {
|
|
819
|
+
stream.close(null);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
662
823
|
/**
|
|
663
824
|
* Closes the file handle after waiting for any pending operation on the handle to
|
|
664
825
|
* complete.
|
|
@@ -677,7 +838,16 @@ export class FileHandle implements IFileHandle {
|
|
|
677
838
|
* @return Fulfills with `undefined` upon success.
|
|
678
839
|
*/
|
|
679
840
|
async close(): Promise<void> {
|
|
680
|
-
|
|
841
|
+
// Close the Gio streams first; they own an fd wrapping the same file
|
|
842
|
+
// as the IOChannel. IOChannel.shutdown(true) flushes + closes its own
|
|
843
|
+
// fd — safe to call even if the Gio streams already released theirs,
|
|
844
|
+
// but guarded here so a throw from shutdown doesn't strand the stream
|
|
845
|
+
// references in a "closed but still pinned" state.
|
|
846
|
+
try { this._ioStream?.close(null); } catch { /* best-effort */ }
|
|
847
|
+
try { this._readStream?.close(null); } catch { /* best-effort */ }
|
|
848
|
+
this._ioStream = null;
|
|
849
|
+
this._readStream = null;
|
|
850
|
+
try { this._file.shutdown(true); } catch { /* best-effort */ }
|
|
681
851
|
}
|
|
682
852
|
|
|
683
853
|
async [Symbol.asyncDispose](): Promise<void> {
|
package/src/fs-watcher.ts
CHANGED
|
@@ -4,20 +4,22 @@
|
|
|
4
4
|
import GLib from '@girs/glib-2.0';
|
|
5
5
|
import Gio from '@girs/gio-2.0';
|
|
6
6
|
import { EventEmitter } from 'node:events';
|
|
7
|
+
import { normalizePath } from './utils.js';
|
|
7
8
|
const privates = new WeakMap;
|
|
8
9
|
|
|
9
|
-
import type { FSWatcher as IFSWatcher } from 'node:fs';
|
|
10
|
+
import type { FSWatcher as IFSWatcher, PathLike, WatchOptions } from 'node:fs';
|
|
10
11
|
|
|
11
12
|
export class FSWatcher extends EventEmitter implements IFSWatcher {
|
|
12
13
|
|
|
13
|
-
constructor(filename:
|
|
14
|
+
constructor(filename: PathLike, options, listener) {
|
|
14
15
|
super();
|
|
15
16
|
if (!options || typeof options !== 'object')
|
|
16
17
|
options = {persistent: true};
|
|
17
18
|
|
|
18
19
|
const persistent = options.persistent !== false;
|
|
19
20
|
const cancellable = Gio.Cancellable.new();
|
|
20
|
-
const
|
|
21
|
+
const pathStr = normalizePath(filename);
|
|
22
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
21
23
|
const watcher = file.monitor(Gio.FileMonitorFlags.NONE, cancellable);
|
|
22
24
|
watcher.connect('changed', changed.bind(this));
|
|
23
25
|
|
|
@@ -28,7 +30,11 @@ export class FSWatcher extends EventEmitter implements IFSWatcher {
|
|
|
28
30
|
if (persistent) {
|
|
29
31
|
// Add a never-firing timeout source to keep the mainloop alive.
|
|
30
32
|
// This is a lightweight way to hold a ref on the main context.
|
|
31
|
-
sourceId = GLib.timeout_add(
|
|
33
|
+
sourceId = (GLib.timeout_add as unknown as (priority: number, interval: number, fn: () => boolean) => number)(
|
|
34
|
+
GLib.PRIORITY_LOW,
|
|
35
|
+
2147483647,
|
|
36
|
+
() => GLib.SOURCE_CONTINUE,
|
|
37
|
+
);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
privates.set(this, {
|
|
@@ -62,7 +68,11 @@ export class FSWatcher extends EventEmitter implements IFSWatcher {
|
|
|
62
68
|
const priv = privates.get(this);
|
|
63
69
|
if (!priv.persistent && !priv.cancellable.is_cancelled()) {
|
|
64
70
|
priv.persistent = true;
|
|
65
|
-
priv.sourceId = GLib.timeout_add(
|
|
71
|
+
priv.sourceId = (GLib.timeout_add as unknown as (priority: number, interval: number, fn: () => boolean) => number)(
|
|
72
|
+
GLib.PRIORITY_LOW,
|
|
73
|
+
2147483647,
|
|
74
|
+
() => GLib.SOURCE_CONTINUE,
|
|
75
|
+
);
|
|
66
76
|
}
|
|
67
77
|
return this;
|
|
68
78
|
}
|
|
@@ -100,4 +110,89 @@ function changed(watcher, file, otherFile, eventType) {
|
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
export default FSWatcher;
|
|
113
|
+
export default FSWatcher;
|
|
114
|
+
|
|
115
|
+
// ─── fs.promises.watch ────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
type WatchEvent = { eventType: string; filename: string | null };
|
|
118
|
+
|
|
119
|
+
function gioEventToNodeType(eventType: Gio.FileMonitorEvent): string | null {
|
|
120
|
+
switch (eventType) {
|
|
121
|
+
case Gio.FileMonitorEvent.CHANGES_DONE_HINT: return 'change';
|
|
122
|
+
case Gio.FileMonitorEvent.DELETED:
|
|
123
|
+
case Gio.FileMonitorEvent.CREATED:
|
|
124
|
+
case Gio.FileMonitorEvent.RENAMED:
|
|
125
|
+
case Gio.FileMonitorEvent.MOVED_IN:
|
|
126
|
+
case Gio.FileMonitorEvent.MOVED_OUT: return 'rename';
|
|
127
|
+
default: return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function* watchAsync(
|
|
132
|
+
filename: PathLike,
|
|
133
|
+
options?: WatchOptions & { signal?: AbortSignal },
|
|
134
|
+
): AsyncIterableIterator<WatchEvent> {
|
|
135
|
+
const signal = (options as any)?.signal as AbortSignal | undefined;
|
|
136
|
+
|
|
137
|
+
if (signal?.aborted) return;
|
|
138
|
+
|
|
139
|
+
const pathStr = normalizePath(filename);
|
|
140
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
141
|
+
const cancellable = Gio.Cancellable.new();
|
|
142
|
+
|
|
143
|
+
let watcher: Gio.FileMonitor;
|
|
144
|
+
try {
|
|
145
|
+
watcher = file.monitor(Gio.FileMonitorFlags.NONE, cancellable);
|
|
146
|
+
} catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const eventQueue: WatchEvent[] = [];
|
|
151
|
+
const waiterQueue: Array<{ resolve: (r: IteratorResult<WatchEvent>) => void }> = [];
|
|
152
|
+
let finished = false;
|
|
153
|
+
|
|
154
|
+
function enqueue(event: WatchEvent): void {
|
|
155
|
+
if (finished) return;
|
|
156
|
+
if (waiterQueue.length > 0) {
|
|
157
|
+
waiterQueue.shift()!.resolve({ value: event, done: false });
|
|
158
|
+
} else {
|
|
159
|
+
eventQueue.push(event);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function terminate(): void {
|
|
164
|
+
if (finished) return;
|
|
165
|
+
finished = true;
|
|
166
|
+
if (!cancellable.is_cancelled()) cancellable.cancel();
|
|
167
|
+
while (waiterQueue.length > 0) {
|
|
168
|
+
waiterQueue.shift()!.resolve({ value: undefined as any, done: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const signalId = watcher.connect('changed', (_mon: Gio.FileMonitor, changedFile: Gio.File, _otherFile: Gio.File | null, eventType: Gio.FileMonitorEvent) => {
|
|
173
|
+
const type = gioEventToNodeType(eventType);
|
|
174
|
+
if (type === null) return;
|
|
175
|
+
enqueue({ eventType: type, filename: changedFile?.get_basename() ?? null });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const abortHandler = () => terminate();
|
|
179
|
+
signal?.addEventListener('abort', abortHandler);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
while (!finished) {
|
|
183
|
+
if (eventQueue.length > 0) {
|
|
184
|
+
yield eventQueue.shift()!;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const result = await new Promise<IteratorResult<WatchEvent>>(resolve => {
|
|
188
|
+
waiterQueue.push({ resolve });
|
|
189
|
+
});
|
|
190
|
+
if (result.done) break;
|
|
191
|
+
yield result.value;
|
|
192
|
+
}
|
|
193
|
+
} finally {
|
|
194
|
+
signal?.removeEventListener('abort', abortHandler);
|
|
195
|
+
try { watcher.disconnect(signalId); } catch {}
|
|
196
|
+
if (!cancellable.is_cancelled()) cancellable.cancel();
|
|
197
|
+
}
|
|
198
|
+
}
|