@gjsify/fs 0.1.15 → 0.3.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/statfs.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Reference: Node.js lib/fs.js (fs.statfs / fs.promises.statfs)
|
|
2
|
+
// Reimplemented for GJS using Gio.File.query_filesystem_info
|
|
3
|
+
|
|
4
|
+
import Gio from '@girs/gio-2.0';
|
|
5
|
+
import GLib from '@girs/glib-2.0';
|
|
6
|
+
import { normalizePath } from './utils.js';
|
|
7
|
+
|
|
8
|
+
import type { PathLike } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
const FS_INFO_ATTRS = [
|
|
11
|
+
'filesystem::size',
|
|
12
|
+
'filesystem::free',
|
|
13
|
+
].join(',');
|
|
14
|
+
|
|
15
|
+
// Block size used to derive block counts from byte counts.
|
|
16
|
+
// Gio does not expose the real fs block size; 4096 is a safe default.
|
|
17
|
+
const BSIZE = 4096;
|
|
18
|
+
|
|
19
|
+
export interface StatFsResult {
|
|
20
|
+
type: number;
|
|
21
|
+
bsize: number;
|
|
22
|
+
blocks: number;
|
|
23
|
+
bfree: number;
|
|
24
|
+
bavail: number;
|
|
25
|
+
files: number;
|
|
26
|
+
ffree: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface BigIntStatFsResult {
|
|
30
|
+
type: bigint;
|
|
31
|
+
bsize: bigint;
|
|
32
|
+
blocks: bigint;
|
|
33
|
+
bfree: bigint;
|
|
34
|
+
bavail: bigint;
|
|
35
|
+
files: bigint;
|
|
36
|
+
ffree: bigint;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildStatFs(info: Gio.FileInfo): StatFsResult {
|
|
40
|
+
const totalBytes = Number(info.get_attribute_uint64('filesystem::size') ?? 0);
|
|
41
|
+
const freeBytes = Number(info.get_attribute_uint64('filesystem::free') ?? 0);
|
|
42
|
+
const blocks = Math.floor(totalBytes / BSIZE);
|
|
43
|
+
const bfree = Math.floor(freeBytes / BSIZE);
|
|
44
|
+
return { type: 0, bsize: BSIZE, blocks, bfree, bavail: bfree, files: 0, ffree: 0 };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildBigIntStatFs(info: Gio.FileInfo): BigIntStatFsResult {
|
|
48
|
+
const totalBytes = BigInt(info.get_attribute_uint64('filesystem::size') ?? 0);
|
|
49
|
+
const freeBytes = BigInt(info.get_attribute_uint64('filesystem::free') ?? 0);
|
|
50
|
+
const bsize = BigInt(BSIZE);
|
|
51
|
+
const blocks = totalBytes / bsize;
|
|
52
|
+
const bfree = freeBytes / bsize;
|
|
53
|
+
return { type: 0n, bsize, blocks, bfree, bavail: bfree, files: 0n, ffree: 0n };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function statfsSync(path: PathLike, options?: { bigint?: false }): StatFsResult;
|
|
57
|
+
export function statfsSync(path: PathLike, options: { bigint: true }): BigIntStatFsResult;
|
|
58
|
+
export function statfsSync(path: PathLike, options?: { bigint?: boolean }): StatFsResult | BigIntStatFsResult {
|
|
59
|
+
const file = Gio.File.new_for_path(normalizePath(path));
|
|
60
|
+
const info = file.query_filesystem_info(FS_INFO_ATTRS, null);
|
|
61
|
+
return options?.bigint === true ? buildBigIntStatFs(info) : buildStatFs(info);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function queryFsInfoAsync(path: PathLike, useBigInt: boolean): Promise<StatFsResult | BigIntStatFsResult> {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const file = Gio.File.new_for_path(normalizePath(path));
|
|
67
|
+
file.query_filesystem_info_async(FS_INFO_ATTRS, GLib.PRIORITY_DEFAULT, null, (_s: unknown, res: Gio.AsyncResult) => {
|
|
68
|
+
try {
|
|
69
|
+
const info = file.query_filesystem_info_finish(res);
|
|
70
|
+
resolve(useBigInt ? buildBigIntStatFs(info) : buildStatFs(info));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
reject(err);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function statfs(path: PathLike, callback: (err: NodeJS.ErrnoException | null, stats: StatFsResult) => void): void;
|
|
79
|
+
export function statfs(path: PathLike, options: { bigint?: false }, callback: (err: NodeJS.ErrnoException | null, stats: StatFsResult) => void): void;
|
|
80
|
+
export function statfs(path: PathLike, options: { bigint: true }, callback: (err: NodeJS.ErrnoException | null, stats: BigIntStatFsResult) => void): void;
|
|
81
|
+
export function statfs(path: PathLike, optionsOrCb: any, callback?: any): void {
|
|
82
|
+
if (typeof optionsOrCb === 'function') {
|
|
83
|
+
callback = optionsOrCb;
|
|
84
|
+
optionsOrCb = {};
|
|
85
|
+
}
|
|
86
|
+
const useBigInt = optionsOrCb?.bigint === true;
|
|
87
|
+
queryFsInfoAsync(path, useBigInt).then(result => callback(null, result), err => callback(err, null));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function statfsAsync(path: PathLike, options?: { bigint?: boolean }): Promise<StatFsResult | BigIntStatFsResult> {
|
|
91
|
+
return queryFsInfoAsync(path, options?.bigint === true);
|
|
92
|
+
}
|
package/src/streams.spec.ts
CHANGED
|
@@ -159,6 +159,64 @@ export default async () => {
|
|
|
159
159
|
cleanup();
|
|
160
160
|
}
|
|
161
161
|
});
|
|
162
|
+
|
|
163
|
+
// Regression: ReadStream uses _construct() for async file opening.
|
|
164
|
+
// Verifies that 'open' and 'ready' fire and that pending starts as true.
|
|
165
|
+
await it('should emit open and ready events before data, pending starts true', async () => {
|
|
166
|
+
setup();
|
|
167
|
+
try {
|
|
168
|
+
const filePath = join(tmpDir, 'open-ready.txt');
|
|
169
|
+
writeFileSync(filePath, 'hello');
|
|
170
|
+
const stream = createReadStream(filePath);
|
|
171
|
+
expect(stream.pending).toBeTruthy();
|
|
172
|
+
const events: string[] = [];
|
|
173
|
+
let openFd: number | undefined;
|
|
174
|
+
await new Promise<void>((resolve, reject) => {
|
|
175
|
+
stream.on('open', (fd: number) => { openFd = fd; events.push('open'); });
|
|
176
|
+
stream.on('ready', () => events.push('ready'));
|
|
177
|
+
stream.on('data', () => { if (!events.includes('data')) events.push('data'); });
|
|
178
|
+
stream.on('end', () => resolve());
|
|
179
|
+
stream.on('error', reject);
|
|
180
|
+
stream.resume();
|
|
181
|
+
});
|
|
182
|
+
expect(openFd).toBeDefined();
|
|
183
|
+
// 'open' and 'ready' must have fired (file was opened)
|
|
184
|
+
expect(events).toContain('open');
|
|
185
|
+
expect(events).toContain('ready');
|
|
186
|
+
// 'open' must fire before any data event
|
|
187
|
+
const openIdx = events.indexOf('open');
|
|
188
|
+
const dataIdx = events.indexOf('data');
|
|
189
|
+
if (dataIdx !== -1) expect(openIdx).toBeLessThan(dataIdx);
|
|
190
|
+
} finally {
|
|
191
|
+
cleanup();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Regression: piping a ReadStream before the file is open (while pending=true)
|
|
196
|
+
// must still deliver all data. Previously _pendingReadSize approach hung under
|
|
197
|
+
// backpressure on GJS 1.88; _construct() fixes this properly.
|
|
198
|
+
await it('should pipe correctly when pipe() is called before file opens', async () => {
|
|
199
|
+
setup();
|
|
200
|
+
try {
|
|
201
|
+
const srcPath = join(tmpDir, 'pre-pipe-src.txt');
|
|
202
|
+
const dstPath = join(tmpDir, 'pre-pipe-dst.txt');
|
|
203
|
+
const size = 3 * 64 * 1024; // 3 × 64KB — forces multiple read chunks + backpressure
|
|
204
|
+
writeFileSync(srcPath, 'X'.repeat(size));
|
|
205
|
+
await new Promise<void>((resolve, reject) => {
|
|
206
|
+
const src = createReadStream(srcPath);
|
|
207
|
+
const dst = createWriteStream(dstPath);
|
|
208
|
+
// pipe() is called immediately — src is still pending (file not open yet)
|
|
209
|
+
expect(src.pending).toBeTruthy();
|
|
210
|
+
src.on('error', reject);
|
|
211
|
+
dst.on('error', reject);
|
|
212
|
+
dst.on('finish', resolve);
|
|
213
|
+
src.pipe(dst);
|
|
214
|
+
});
|
|
215
|
+
expect(readFileSync(dstPath, 'utf8').length).toBe(size);
|
|
216
|
+
} finally {
|
|
217
|
+
cleanup();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
162
220
|
});
|
|
163
221
|
|
|
164
222
|
// ---- createWriteStream ----
|
package/src/sync.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { FileHandle } from './file-handle.js';
|
|
|
13
13
|
import { Dirent } from './dirent.js';
|
|
14
14
|
import { Stats, BigIntStats, STAT_ATTRIBUTES } from './stats.js';
|
|
15
15
|
import { createNodeError, isNotFoundError } from './errors.js';
|
|
16
|
-
import { tempDirPath } from './utils.js';
|
|
16
|
+
import { tempDirPath, normalizePath } from './utils.js';
|
|
17
17
|
|
|
18
18
|
import type { OpenFlags, EncodingOption } from './types/index.js';
|
|
19
19
|
import type {
|
|
@@ -31,24 +31,26 @@ export { existsSync }
|
|
|
31
31
|
// --- stat / lstat ---
|
|
32
32
|
|
|
33
33
|
export function statSync(path: PathLike, options?: StatSyncOptions): Stats | BigIntStats | undefined {
|
|
34
|
+
const pathStr = normalizePath(path);
|
|
34
35
|
try {
|
|
35
|
-
const file = Gio.File.new_for_path(
|
|
36
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
36
37
|
const info = file.query_info(STAT_ATTRIBUTES, Gio.FileQueryInfoFlags.NONE, null);
|
|
37
|
-
return options?.bigint ? new BigIntStats(info,
|
|
38
|
+
return options?.bigint ? new BigIntStats(info, pathStr) : new Stats(info, pathStr);
|
|
38
39
|
} catch (err: unknown) {
|
|
39
40
|
if (options?.throwIfNoEntry === false && isNotFoundError(err)) return undefined;
|
|
40
|
-
throw createNodeError(err, 'stat',
|
|
41
|
+
throw createNodeError(err, 'stat', pathStr);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export function lstatSync(path: PathLike, options?: StatSyncOptions): Stats | BigIntStats | undefined {
|
|
46
|
+
const pathStr = normalizePath(path);
|
|
45
47
|
try {
|
|
46
|
-
const file = Gio.File.new_for_path(
|
|
48
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
47
49
|
const info = file.query_info(STAT_ATTRIBUTES, Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
|
|
48
|
-
return options?.bigint ? new BigIntStats(info,
|
|
50
|
+
return options?.bigint ? new BigIntStats(info, pathStr) : new Stats(info, pathStr);
|
|
49
51
|
} catch (err: unknown) {
|
|
50
52
|
if (options?.throwIfNoEntry === false && isNotFoundError(err)) return undefined;
|
|
51
|
-
throw createNodeError(err, 'lstat',
|
|
53
|
+
throw createNodeError(err, 'lstat', pathStr);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
@@ -58,7 +60,7 @@ export function readdirSync(
|
|
|
58
60
|
path: PathLike,
|
|
59
61
|
options?: { withFileTypes?: boolean; encoding?: string; recursive?: boolean }
|
|
60
62
|
): string[] | Dirent[] {
|
|
61
|
-
const pathStr = path
|
|
63
|
+
const pathStr = normalizePath(path);
|
|
62
64
|
const file = Gio.File.new_for_path(pathStr);
|
|
63
65
|
const enumerator = file.enumerate_children(
|
|
64
66
|
'standard::name,standard::type',
|
|
@@ -101,7 +103,8 @@ export function readdirSync(
|
|
|
101
103
|
const MAX_SYMLINK_DEPTH = 40; // matches Linux MAXSYMLINKS
|
|
102
104
|
|
|
103
105
|
export function realpathSync(path: PathLike): string {
|
|
104
|
-
|
|
106
|
+
const pathStr = normalizePath(path);
|
|
107
|
+
let current = Gio.File.new_for_path(pathStr);
|
|
105
108
|
let depth = 0;
|
|
106
109
|
|
|
107
110
|
while (true) {
|
|
@@ -120,7 +123,7 @@ export function realpathSync(path: PathLike): string {
|
|
|
120
123
|
current = parent ? parent.resolve_relative_path(target) : Gio.File.new_for_path(target);
|
|
121
124
|
|
|
122
125
|
if (++depth > MAX_SYMLINK_DEPTH) {
|
|
123
|
-
throw new Error(`ELOOP: too many levels of symbolic links, realpath '${
|
|
126
|
+
throw new Error(`ELOOP: too many levels of symbolic links, realpath '${pathStr}'`);
|
|
124
127
|
}
|
|
125
128
|
}
|
|
126
129
|
}
|
|
@@ -129,24 +132,27 @@ export function realpathSync(path: PathLike): string {
|
|
|
129
132
|
// --- symlink ---
|
|
130
133
|
|
|
131
134
|
export function symlinkSync(target: PathLike, path: PathLike, _type?: 'file' | 'dir' | 'junction'): void {
|
|
132
|
-
const
|
|
133
|
-
|
|
135
|
+
const pathStr = normalizePath(path);
|
|
136
|
+
const targetStr = normalizePath(target);
|
|
137
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
138
|
+
file.make_symbolic_link(targetStr, null);
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
export function readFileSync(path:
|
|
137
|
-
const
|
|
141
|
+
export function readFileSync(path: PathLike, options: any = { encoding: null, flag: 'r' }) {
|
|
142
|
+
const pathStr = normalizePath(path);
|
|
143
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
138
144
|
|
|
139
145
|
try {
|
|
140
146
|
const [ok, data] = file.load_contents(null);
|
|
141
147
|
|
|
142
148
|
if (!ok) {
|
|
143
|
-
throw createNodeError(new Error('failed to read file'), 'read',
|
|
149
|
+
throw createNodeError(new Error('failed to read file'), 'read', pathStr);
|
|
144
150
|
}
|
|
145
151
|
|
|
146
152
|
return encodeUint8Array(getEncodingFromOptions(options, "buffer"), data);
|
|
147
153
|
} catch (err: unknown) {
|
|
148
154
|
if ((err as { code?: unknown }).code && typeof (err as { code?: unknown }).code === 'string') throw err; // Already a Node error
|
|
149
|
-
throw createNodeError(err, 'read',
|
|
155
|
+
throw createNodeError(err, 'read', pathStr);
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
|
|
@@ -203,9 +209,7 @@ export function mkdirSync(path: PathLike, options?: Mode | MakeDirectoryOptions
|
|
|
203
209
|
mode = options || 0o777;
|
|
204
210
|
}
|
|
205
211
|
|
|
206
|
-
|
|
207
|
-
path = path.toString();
|
|
208
|
-
}
|
|
212
|
+
path = normalizePath(path);
|
|
209
213
|
|
|
210
214
|
if(typeof mode === 'string') {
|
|
211
215
|
throw new TypeError("mode as string is currently not supported!");
|
|
@@ -275,57 +279,63 @@ function mkdirSyncRecursive(pathStr: string, mode: number): string | undefined {
|
|
|
275
279
|
* @since v0.1.21
|
|
276
280
|
*/
|
|
277
281
|
export function rmdirSync(path: PathLike, _options?: RmDirOptions): void {
|
|
278
|
-
const
|
|
282
|
+
const pathStr = normalizePath(path);
|
|
283
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
279
284
|
try {
|
|
280
285
|
// Check if it's a directory
|
|
281
286
|
const info = file.query_info('standard::type', Gio.FileQueryInfoFlags.NONE, null);
|
|
282
287
|
if (info.get_file_type() !== Gio.FileType.DIRECTORY) {
|
|
283
288
|
const err = Object.assign(new Error(), { code: 4 }); // Gio.IOErrorEnum.NOT_DIRECTORY
|
|
284
|
-
throw createNodeError(err, 'rmdir',
|
|
289
|
+
throw createNodeError(err, 'rmdir', pathStr);
|
|
285
290
|
}
|
|
286
291
|
// Check if empty — rmdir only removes empty directories (use rmSync for recursive)
|
|
287
292
|
const enumerator = file.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
|
|
288
293
|
if (enumerator.next_file(null) !== null) {
|
|
289
294
|
const err = Object.assign(new Error(), { code: 5 }); // Gio.IOErrorEnum.NOT_EMPTY
|
|
290
|
-
throw createNodeError(err, 'rmdir',
|
|
295
|
+
throw createNodeError(err, 'rmdir', pathStr);
|
|
291
296
|
}
|
|
292
297
|
file.delete(null);
|
|
293
298
|
} catch (err: unknown) {
|
|
294
299
|
if ((err as { code?: unknown }).code && typeof (err as { code?: unknown }).code === 'string') throw err; // Already a Node error
|
|
295
|
-
throw createNodeError(err, 'rmdir',
|
|
300
|
+
throw createNodeError(err, 'rmdir', pathStr);
|
|
296
301
|
}
|
|
297
302
|
}
|
|
298
303
|
|
|
299
304
|
export function unlinkSync(path: PathLike): void {
|
|
300
|
-
const
|
|
305
|
+
const pathStr = normalizePath(path);
|
|
306
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
301
307
|
try {
|
|
302
308
|
file.delete(null);
|
|
303
309
|
} catch (err: unknown) {
|
|
304
|
-
throw createNodeError(err, 'unlink',
|
|
310
|
+
throw createNodeError(err, 'unlink', pathStr);
|
|
305
311
|
}
|
|
306
312
|
}
|
|
307
313
|
|
|
308
|
-
export function writeFileSync(path:
|
|
309
|
-
GLib.file_set_contents(path, data);
|
|
314
|
+
export function writeFileSync(path: PathLike, data: string | Uint8Array) {
|
|
315
|
+
GLib.file_set_contents(normalizePath(path), data);
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
// --- rename ---
|
|
313
319
|
|
|
314
320
|
export function renameSync(oldPath: PathLike, newPath: PathLike): void {
|
|
315
|
-
const
|
|
316
|
-
const
|
|
321
|
+
const oldStr = normalizePath(oldPath);
|
|
322
|
+
const newStr = normalizePath(newPath);
|
|
323
|
+
const src = Gio.File.new_for_path(oldStr);
|
|
324
|
+
const dest = Gio.File.new_for_path(newStr);
|
|
317
325
|
try {
|
|
318
326
|
src.move(dest, Gio.FileCopyFlags.OVERWRITE, null, null);
|
|
319
327
|
} catch (err: unknown) {
|
|
320
|
-
throw createNodeError(err, 'rename',
|
|
328
|
+
throw createNodeError(err, 'rename', oldStr, newStr);
|
|
321
329
|
}
|
|
322
330
|
}
|
|
323
331
|
|
|
324
332
|
// --- copyFile ---
|
|
325
333
|
|
|
326
334
|
export function copyFileSync(src: PathLike, dest: PathLike, mode?: number): void {
|
|
327
|
-
const
|
|
328
|
-
const
|
|
335
|
+
const srcStr = normalizePath(src);
|
|
336
|
+
const destStr = normalizePath(dest);
|
|
337
|
+
const srcFile = Gio.File.new_for_path(srcStr);
|
|
338
|
+
const destFile = Gio.File.new_for_path(destStr);
|
|
329
339
|
let flags = Gio.FileCopyFlags.NONE;
|
|
330
340
|
// mode 0 = default (overwrite), COPYFILE_EXCL (1) = no overwrite
|
|
331
341
|
if (mode && (mode & 1) === 0) {
|
|
@@ -336,40 +346,42 @@ export function copyFileSync(src: PathLike, dest: PathLike, mode?: number): void
|
|
|
336
346
|
try {
|
|
337
347
|
srcFile.copy(destFile, flags, null, null);
|
|
338
348
|
} catch (err: unknown) {
|
|
339
|
-
throw createNodeError(err, 'copyfile',
|
|
349
|
+
throw createNodeError(err, 'copyfile', srcStr, destStr);
|
|
340
350
|
}
|
|
341
351
|
}
|
|
342
352
|
|
|
343
353
|
// --- access ---
|
|
344
354
|
|
|
345
355
|
export function accessSync(path: PathLike, mode?: number): void {
|
|
346
|
-
const
|
|
356
|
+
const pathStr = normalizePath(path);
|
|
357
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
347
358
|
try {
|
|
348
359
|
const info = file.query_info('access::*', Gio.FileQueryInfoFlags.NONE, null);
|
|
349
360
|
// mode: F_OK=0, R_OK=4, W_OK=2, X_OK=1
|
|
350
361
|
if (mode !== undefined && mode !== 0) {
|
|
351
362
|
// Gio.IOErrorEnum.PERMISSION_DENIED = 14 → maps to EACCES via createNodeError
|
|
352
|
-
const permErr = { code: 14, message: `permission denied, access '${
|
|
363
|
+
const permErr = { code: 14, message: `permission denied, access '${pathStr}'` };
|
|
353
364
|
if ((mode & 4) && !info.get_attribute_boolean('access::can-read')) {
|
|
354
|
-
throw createNodeError(permErr, 'access',
|
|
365
|
+
throw createNodeError(permErr, 'access', pathStr);
|
|
355
366
|
}
|
|
356
367
|
if ((mode & 2) && !info.get_attribute_boolean('access::can-write')) {
|
|
357
|
-
throw createNodeError(permErr, 'access',
|
|
368
|
+
throw createNodeError(permErr, 'access', pathStr);
|
|
358
369
|
}
|
|
359
370
|
if ((mode & 1) && !info.get_attribute_boolean('access::can-execute')) {
|
|
360
|
-
throw createNodeError(permErr, 'access',
|
|
371
|
+
throw createNodeError(permErr, 'access', pathStr);
|
|
361
372
|
}
|
|
362
373
|
}
|
|
363
374
|
} catch (err: unknown) {
|
|
364
375
|
if ((err as { code?: unknown }).code && typeof (err as { code?: unknown }).code === 'string') throw err; // Already a Node-style error
|
|
365
|
-
throw createNodeError(err, 'access',
|
|
376
|
+
throw createNodeError(err, 'access', pathStr);
|
|
366
377
|
}
|
|
367
378
|
}
|
|
368
379
|
|
|
369
380
|
// --- appendFile ---
|
|
370
381
|
|
|
371
382
|
export function appendFileSync(path: PathLike, data: string | Uint8Array, options?: { encoding?: string; mode?: number; flag?: string } | string): void {
|
|
372
|
-
const
|
|
383
|
+
const pathStr = normalizePath(path);
|
|
384
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
373
385
|
let bytes: Uint8Array;
|
|
374
386
|
if (typeof data === 'string') {
|
|
375
387
|
bytes = new TextEncoder().encode(data);
|
|
@@ -384,19 +396,20 @@ export function appendFileSync(path: PathLike, data: string | Uint8Array, option
|
|
|
384
396
|
}
|
|
385
397
|
stream.close(null);
|
|
386
398
|
} catch (err: unknown) {
|
|
387
|
-
throw createNodeError(err, 'appendfile',
|
|
399
|
+
throw createNodeError(err, 'appendfile', pathStr);
|
|
388
400
|
}
|
|
389
401
|
}
|
|
390
402
|
|
|
391
403
|
// --- readlink ---
|
|
392
404
|
|
|
393
405
|
export function readlinkSync(path: PathLike, options?: { encoding?: string } | string): string | Buffer {
|
|
394
|
-
const
|
|
406
|
+
const pathStr = normalizePath(path);
|
|
407
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
395
408
|
try {
|
|
396
409
|
const info = file.query_info('standard::symlink-target', Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null);
|
|
397
410
|
const target = info.get_symlink_target();
|
|
398
411
|
if (!target) {
|
|
399
|
-
throw Object.assign(new Error(`EINVAL: invalid argument, readlink '${
|
|
412
|
+
throw Object.assign(new Error(`EINVAL: invalid argument, readlink '${pathStr}'`), { code: 'EINVAL', errno: -22, syscall: 'readlink', path: pathStr });
|
|
400
413
|
}
|
|
401
414
|
const encoding = typeof options === 'string' ? options : options?.encoding;
|
|
402
415
|
if (encoding === 'buffer') {
|
|
@@ -405,24 +418,27 @@ export function readlinkSync(path: PathLike, options?: { encoding?: string } | s
|
|
|
405
418
|
return target;
|
|
406
419
|
} catch (err: unknown) {
|
|
407
420
|
if (typeof (err as { code?: unknown }).code === 'string') throw err;
|
|
408
|
-
throw createNodeError(err, 'readlink',
|
|
421
|
+
throw createNodeError(err, 'readlink', pathStr);
|
|
409
422
|
}
|
|
410
423
|
}
|
|
411
424
|
|
|
412
425
|
// --- link ---
|
|
413
426
|
|
|
414
427
|
export function linkSync(existingPath: PathLike, newPath: PathLike): void {
|
|
428
|
+
const existingStr = normalizePath(existingPath);
|
|
429
|
+
const newStr = normalizePath(newPath);
|
|
415
430
|
// Gio doesn't have a direct hard link API, use GLib
|
|
416
|
-
const result = GLib.spawn_command_line_sync(`ln ${
|
|
431
|
+
const result = GLib.spawn_command_line_sync(`ln ${existingStr} ${newStr}`);
|
|
417
432
|
if (!result[0]) {
|
|
418
|
-
throw Object.assign(new Error(`EPERM: operation not permitted, link '${
|
|
433
|
+
throw Object.assign(new Error(`EPERM: operation not permitted, link '${existingStr}' -> '${newStr}'`), { code: 'EPERM', errno: -1, syscall: 'link', path: existingStr, dest: newStr });
|
|
419
434
|
}
|
|
420
435
|
}
|
|
421
436
|
|
|
422
437
|
// --- truncate ---
|
|
423
438
|
|
|
424
439
|
export function truncateSync(path: PathLike, len?: number): void {
|
|
425
|
-
const
|
|
440
|
+
const pathStr = normalizePath(path);
|
|
441
|
+
const file = Gio.File.new_for_path(pathStr);
|
|
426
442
|
try {
|
|
427
443
|
const stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null);
|
|
428
444
|
if (len && len > 0) {
|
|
@@ -435,35 +451,37 @@ export function truncateSync(path: PathLike, len?: number): void {
|
|
|
435
451
|
}
|
|
436
452
|
stream.close(null);
|
|
437
453
|
} catch (err: unknown) {
|
|
438
|
-
throw createNodeError(err, 'truncate',
|
|
454
|
+
throw createNodeError(err, 'truncate', pathStr);
|
|
439
455
|
}
|
|
440
456
|
}
|
|
441
457
|
|
|
442
458
|
// --- chmodSync ---
|
|
443
459
|
|
|
444
460
|
export function chmodSync(path: PathLike, mode: Mode): void {
|
|
461
|
+
const pathStr = normalizePath(path);
|
|
445
462
|
const modeNum = typeof mode === 'string' ? parseInt(mode, 8) : mode;
|
|
446
|
-
const result = GLib.spawn_command_line_sync(`chmod ${modeNum.toString(8)} ${
|
|
463
|
+
const result = GLib.spawn_command_line_sync(`chmod ${modeNum.toString(8)} ${pathStr}`);
|
|
447
464
|
if (!result[0]) {
|
|
448
|
-
throw Object.assign(new Error(`EPERM: operation not permitted, chmod '${
|
|
465
|
+
throw Object.assign(new Error(`EPERM: operation not permitted, chmod '${pathStr}'`), { code: 'EPERM', errno: -1, syscall: 'chmod', path: pathStr });
|
|
449
466
|
}
|
|
450
467
|
}
|
|
451
468
|
|
|
452
469
|
// --- chownSync ---
|
|
453
470
|
|
|
454
471
|
export function chownSync(path: PathLike, uid: number, gid: number): void {
|
|
455
|
-
const
|
|
472
|
+
const pathStr = normalizePath(path);
|
|
473
|
+
const result = GLib.spawn_command_line_sync(`chown ${uid}:${gid} ${pathStr}`);
|
|
456
474
|
if (!result[0]) {
|
|
457
|
-
throw Object.assign(new Error(`EPERM: operation not permitted, chown '${
|
|
475
|
+
throw Object.assign(new Error(`EPERM: operation not permitted, chown '${pathStr}'`), { code: 'EPERM', errno: -1, syscall: 'chown', path: pathStr });
|
|
458
476
|
}
|
|
459
477
|
}
|
|
460
478
|
|
|
461
|
-
export function watch(filename:
|
|
462
|
-
return new FSWatcher(filename, options, listener);
|
|
479
|
+
export function watch(filename: PathLike, options: { persistent?: boolean; recursive?: boolean; encoding?: string } | undefined, listener: ((eventType: string, filename: string | null) => void) | undefined) {
|
|
480
|
+
return new FSWatcher(normalizePath(filename), options, listener);
|
|
463
481
|
}
|
|
464
482
|
|
|
465
|
-
export function openSync(path: PathLike, flags?: OpenFlags, mode?: Mode): FileHandle {
|
|
466
|
-
return new FileHandle({ path, flags, mode });
|
|
483
|
+
export function openSync(path: PathLike, flags?: OpenFlags | number, mode?: Mode): FileHandle {
|
|
484
|
+
return new FileHandle({ path, flags: flags as OpenFlags | undefined, mode });
|
|
467
485
|
}
|
|
468
486
|
|
|
469
487
|
/**
|
|
@@ -507,7 +525,7 @@ export function mkdtempSync(prefix: string, options?: EncodingOption | BufferEnc
|
|
|
507
525
|
* @since v14.14.0
|
|
508
526
|
*/
|
|
509
527
|
export function rmSync(path: PathLike, options?: RmOptions): void {
|
|
510
|
-
const pathStr = path
|
|
528
|
+
const pathStr = normalizePath(path);
|
|
511
529
|
const file = Gio.File.new_for_path(pathStr);
|
|
512
530
|
const recursive = options?.recursive || false;
|
|
513
531
|
const force = options?.force || false;
|
package/src/test.mts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import '@gjsify/node-globals/register';
|
|
1
|
+
import '@gjsify/node-globals/register/process';
|
|
2
|
+
import '@gjsify/node-globals/register/buffer';
|
|
3
|
+
import '@gjsify/node-globals/register/timers';
|
|
4
|
+
import '@gjsify/node-globals/register/url';
|
|
2
5
|
import { run } from '@gjsify/unit';
|
|
3
6
|
|
|
4
7
|
import testSuiteCallback from './callback.spec.js';
|
|
@@ -12,5 +15,13 @@ import testSuiteExtended from './extended.spec.js';
|
|
|
12
15
|
|
|
13
16
|
import testSuiteErrors from './errors.spec.js';
|
|
14
17
|
import testSuiteStreams from './streams.spec.js';
|
|
18
|
+
import testSuiteCp from './cp.spec.js';
|
|
19
|
+
import testSuiteDir from './dir.spec.js';
|
|
20
|
+
import testSuiteGlob from './glob.spec.js';
|
|
21
|
+
import testSuiteWatch from './watch.spec.js';
|
|
22
|
+
import testSuiteWatchFile from './watchfile.spec.js';
|
|
23
|
+
import testSuiteStatFs from './statfs.spec.js';
|
|
24
|
+
import testSuiteUtimes from './utimes.spec.js';
|
|
25
|
+
import testSuiteFdOps from './fd-ops.spec.js';
|
|
15
26
|
|
|
16
|
-
run({testSuiteCallback, testSuiteFileHandle, testSuitePromise, testSuiteSync, testSuiteSymlink, testSuiteStat, testSuiteNewApis, testSuiteExtended, testSuiteErrors, testSuiteStreams});
|
|
27
|
+
run({testSuiteCallback, testSuiteFileHandle, testSuitePromise, testSuiteSync, testSuiteSymlink, testSuiteStat, testSuiteNewApis, testSuiteExtended, testSuiteErrors, testSuiteStreams, testSuiteCp, testSuiteDir, testSuiteGlob, testSuiteWatch, testSuiteWatchFile, testSuiteStatFs, testSuiteUtimes, testSuiteFdOps});
|
package/src/utils.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
// Shared filesystem utilities for GJS — original implementation using Gio
|
|
2
2
|
|
|
3
3
|
import { existsSync } from './sync.js';
|
|
4
|
+
import { fileURLToPath, URL as NodeURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import type { PathLike } from 'node:fs';
|
|
7
|
+
|
|
8
|
+
// Gio.File.new_for_path only accepts strings; convert URL/Buffer accordingly.
|
|
9
|
+
export function normalizePath(path: PathLike): string {
|
|
10
|
+
if (path instanceof URL || path instanceof NodeURL) return fileURLToPath(path as URL);
|
|
11
|
+
if (typeof path === 'string') return path;
|
|
12
|
+
return (path as Buffer).toString();
|
|
13
|
+
}
|
|
4
14
|
|
|
5
15
|
const CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
6
16
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Ported from refs/node-test/parallel/test-fs-utimes.js (behavior)
|
|
2
|
+
// Original: MIT, Node.js contributors.
|
|
3
|
+
// Rewritten for @gjsify/unit — behavior preserved, assertion dialect adapted.
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
6
|
+
import { utimesSync, utimes, lutimesSync, lchownSync, promises } from 'node:fs';
|
|
7
|
+
import { statSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
const TMP = tmpdir();
|
|
12
|
+
|
|
13
|
+
function tmpFile(name: string): string {
|
|
14
|
+
const p = join(TMP, `gjsify-utimes-${name}-${process.pid}`);
|
|
15
|
+
writeFileSync(p, 'test');
|
|
16
|
+
return p;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default async () => {
|
|
20
|
+
await describe('fs.utimes / fs.lutimes / fs.lchown / fs.lchmod', async () => {
|
|
21
|
+
await it('utimesSync sets mtime', async () => {
|
|
22
|
+
const f = tmpFile('mtime');
|
|
23
|
+
const mtime = new Date('2020-01-01T00:00:00Z');
|
|
24
|
+
utimesSync(f, mtime, mtime);
|
|
25
|
+
const stat = statSync(f);
|
|
26
|
+
expect(stat.mtime.getFullYear()).toBe(2020);
|
|
27
|
+
unlinkSync(f);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await it('utimesSync sets atime', async () => {
|
|
31
|
+
const f = tmpFile('atime');
|
|
32
|
+
const atime = new Date('2021-06-15T12:00:00Z');
|
|
33
|
+
const mtime = new Date('2020-01-01T00:00:00Z');
|
|
34
|
+
utimesSync(f, atime, mtime);
|
|
35
|
+
const stat = statSync(f);
|
|
36
|
+
expect(stat.mtime.getFullYear()).toBe(2020);
|
|
37
|
+
unlinkSync(f);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await it('utimes callback sets timestamps', async () => {
|
|
41
|
+
const f = tmpFile('cb');
|
|
42
|
+
const mtime = new Date('2019-03-10T00:00:00Z');
|
|
43
|
+
await new Promise<void>((resolve, reject) => {
|
|
44
|
+
utimes(f, mtime, mtime, (err) => {
|
|
45
|
+
if (err) reject(err);
|
|
46
|
+
else resolve();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
const stat = statSync(f);
|
|
50
|
+
expect(stat.mtime.getFullYear()).toBe(2019);
|
|
51
|
+
unlinkSync(f);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await it('promises.utimes sets timestamps', async () => {
|
|
55
|
+
const f = tmpFile('promise');
|
|
56
|
+
const mtime = new Date('2018-08-01T00:00:00Z');
|
|
57
|
+
await promises.utimes(f, mtime, mtime);
|
|
58
|
+
const stat = statSync(f);
|
|
59
|
+
expect(stat.mtime.getFullYear()).toBe(2018);
|
|
60
|
+
unlinkSync(f);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await it('lutimesSync does not throw on a symlink', async () => {
|
|
64
|
+
const target = tmpFile('lutime-target');
|
|
65
|
+
const link = join(TMP, `gjsify-lutime-link-${process.pid}`);
|
|
66
|
+
try { unlinkSync(link); } catch {}
|
|
67
|
+
symlinkSync(target, link);
|
|
68
|
+
const mtime = new Date('2017-05-20T00:00:00Z');
|
|
69
|
+
// Just verify the call completes without throwing
|
|
70
|
+
expect(() => lutimesSync(link, mtime, mtime)).not.toThrow();
|
|
71
|
+
unlinkSync(link);
|
|
72
|
+
unlinkSync(target);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await it('lutimes callback completes without error', async () => {
|
|
76
|
+
const f = tmpFile('lutimes-cb');
|
|
77
|
+
const mtime = new Date('2016-01-01T00:00:00Z');
|
|
78
|
+
const { lutimes } = await import('node:fs') as any;
|
|
79
|
+
await new Promise<void>((resolve, reject) => {
|
|
80
|
+
lutimes(f, mtime, mtime, (err: any) => {
|
|
81
|
+
if (err) reject(err);
|
|
82
|
+
else resolve();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
unlinkSync(f);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await it('lchownSync does not throw (may need root to actually change)', async () => {
|
|
89
|
+
const f = tmpFile('lchown');
|
|
90
|
+
const link = join(TMP, `gjsify-lchown-link-${process.pid}`);
|
|
91
|
+
try { unlinkSync(link); } catch {}
|
|
92
|
+
symlinkSync(f, link);
|
|
93
|
+
// This will only actually change owner if running as root; just verify no throw
|
|
94
|
+
try {
|
|
95
|
+
lchownSync(link, process.getuid ? process.getuid() : 0, process.getgid ? process.getgid() : 0);
|
|
96
|
+
} catch {
|
|
97
|
+
// acceptable if kernel denies (EPERM) — just must not crash the process
|
|
98
|
+
}
|
|
99
|
+
unlinkSync(link);
|
|
100
|
+
unlinkSync(f);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await it('lchmod is a no-op (does not throw)', async () => {
|
|
104
|
+
const f = tmpFile('lchmod');
|
|
105
|
+
// Node.js removed lchmod on Linux; on GJS we export it as a no-op
|
|
106
|
+
const fsModule = await import('node:fs') as any;
|
|
107
|
+
if (typeof fsModule.lchmod === 'function') {
|
|
108
|
+
expect(() => fsModule.lchmod(f, 0o644, () => {})).not.toThrow();
|
|
109
|
+
}
|
|
110
|
+
unlinkSync(f);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
};
|