@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.
Files changed (67) hide show
  1. package/lib/esm/callback.js +22 -13
  2. package/lib/esm/cp.js +253 -0
  3. package/lib/esm/dir.js +160 -0
  4. package/lib/esm/fd-ops.js +189 -0
  5. package/lib/esm/file-handle.js +263 -84
  6. package/lib/esm/fs-watcher.js +88 -4
  7. package/lib/esm/glob.js +164 -0
  8. package/lib/esm/index.js +128 -2
  9. package/lib/esm/promises.js +90 -27
  10. package/lib/esm/read-stream.js +53 -43
  11. package/lib/esm/stat-watcher.js +121 -0
  12. package/lib/esm/statfs.js +57 -0
  13. package/lib/esm/sync.js +70 -52
  14. package/lib/esm/utils.js +7 -0
  15. package/lib/esm/utimes.js +62 -0
  16. package/lib/esm/write-stream.js +2 -5
  17. package/lib/types/cp.d.ts +18 -0
  18. package/lib/types/cp.spec.d.ts +2 -0
  19. package/lib/types/dir.d.ts +29 -0
  20. package/lib/types/dir.spec.d.ts +2 -0
  21. package/lib/types/fd-ops.d.ts +57 -0
  22. package/lib/types/fd-ops.spec.d.ts +2 -0
  23. package/lib/types/file-handle.d.ts +34 -4
  24. package/lib/types/fs-watcher.d.ts +9 -2
  25. package/lib/types/glob.d.ts +8 -0
  26. package/lib/types/glob.spec.d.ts +2 -0
  27. package/lib/types/index.d.ts +51 -1
  28. package/lib/types/promises.d.ts +31 -4
  29. package/lib/types/read-stream.d.ts +3 -1
  30. package/lib/types/stat-watcher.d.ts +21 -0
  31. package/lib/types/statfs.d.ts +35 -0
  32. package/lib/types/statfs.spec.d.ts +2 -0
  33. package/lib/types/sync.d.ts +4 -7
  34. package/lib/types/utils.d.ts +2 -0
  35. package/lib/types/utimes.d.ts +13 -0
  36. package/lib/types/utimes.spec.d.ts +2 -0
  37. package/lib/types/watch.spec.d.ts +2 -0
  38. package/lib/types/watchfile.spec.d.ts +2 -0
  39. package/lib/types/write-stream.d.ts +1 -2
  40. package/package.json +12 -12
  41. package/src/callback.ts +22 -13
  42. package/src/cp.spec.ts +181 -0
  43. package/src/cp.ts +328 -0
  44. package/src/dir.spec.ts +204 -0
  45. package/src/dir.ts +199 -0
  46. package/src/fd-ops.spec.ts +234 -0
  47. package/src/fd-ops.ts +251 -0
  48. package/src/file-handle.ts +264 -94
  49. package/src/fs-watcher.ts +101 -6
  50. package/src/glob.spec.ts +201 -0
  51. package/src/glob.ts +205 -0
  52. package/src/index.ts +74 -0
  53. package/src/promises.ts +94 -29
  54. package/src/read-stream.ts +49 -43
  55. package/src/stat-watcher.ts +116 -0
  56. package/src/statfs.spec.ts +67 -0
  57. package/src/statfs.ts +92 -0
  58. package/src/streams.spec.ts +58 -0
  59. package/src/sync.ts +75 -57
  60. package/src/test.mts +13 -2
  61. package/src/utils.ts +10 -0
  62. package/src/utimes.spec.ts +113 -0
  63. package/src/utimes.ts +97 -0
  64. package/src/watch.spec.ts +171 -0
  65. package/src/watchfile.spec.ts +185 -0
  66. package/src/write-stream.ts +5 -8
  67. package/tsconfig.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/fs",
3
- "version": "0.1.15",
3
+ "version": "0.3.0",
4
4
  "description": "Node.js fs module for Gjs",
5
5
  "module": "lib/esm/index.js",
6
6
  "types": "lib/types/index.d.ts",
@@ -25,7 +25,7 @@
25
25
  "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
26
26
  "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
27
27
  "test": "yarn build:gjsify && yarn build:test && yarn test:node && yarn test:gjs",
28
- "test:gjs": "gjs -m test.gjs.mjs",
28
+ "test:gjs": "gjsify run test.gjs.mjs",
29
29
  "test:node": "node test.node.mjs"
30
30
  },
31
31
  "keywords": [
@@ -34,18 +34,18 @@
34
34
  "fs"
35
35
  ],
36
36
  "devDependencies": {
37
- "@gjsify/cli": "^0.1.15",
38
- "@gjsify/unit": "^0.1.15",
37
+ "@gjsify/cli": "^0.3.0",
38
+ "@gjsify/unit": "^0.3.0",
39
39
  "@types/node": "^25.6.0",
40
- "typescript": "^6.0.2"
40
+ "typescript": "^6.0.3"
41
41
  },
42
42
  "dependencies": {
43
- "@girs/gio-2.0": "^2.88.0-4.0.0-rc.3",
44
- "@girs/glib-2.0": "^2.88.0-4.0.0-rc.3",
45
- "@gjsify/buffer": "^0.1.15",
46
- "@gjsify/events": "^0.1.15",
47
- "@gjsify/stream": "^0.1.15",
48
- "@gjsify/url": "^0.1.15",
49
- "@gjsify/utils": "^0.1.15"
43
+ "@girs/gio-2.0": "^2.88.0-4.0.0-rc.9",
44
+ "@girs/glib-2.0": "^2.88.0-4.0.0-rc.9",
45
+ "@gjsify/buffer": "^0.3.0",
46
+ "@gjsify/events": "^0.3.0",
47
+ "@gjsify/stream": "^0.3.0",
48
+ "@gjsify/url": "^0.3.0",
49
+ "@gjsify/utils": "^0.3.0"
50
50
  }
51
51
  }
package/src/callback.ts CHANGED
@@ -10,6 +10,7 @@ import { Buffer } from 'node:buffer';
10
10
  import { Stats, BigIntStats, STAT_ATTRIBUTES } from './stats.js';
11
11
  import { createNodeError } from './errors.js';
12
12
  import { realpathSync, readdirSync, renameSync, copyFileSync, accessSync, appendFileSync, readlinkSync, truncateSync, chmodSync, chownSync, mkdirSync, rmdirSync, readFileSync, writeFileSync } from './sync.js';
13
+ import { normalizePath } from './utils.js';
13
14
  // encoding helpers available if needed in future
14
15
 
15
16
  import type { OpenFlags } from './types/index.js';
@@ -23,13 +24,14 @@ function parseOptsCb(optionsOrCallback: unknown, maybeCallback?: Function): { op
23
24
  }
24
25
 
25
26
  function statImpl(path: PathLike, flags: Gio.FileQueryInfoFlags, syscall: string, options: Record<string, unknown>, callback: Function): void {
26
- const file = Gio.File.new_for_path(path.toString());
27
+ const pathStr = normalizePath(path);
28
+ const file = Gio.File.new_for_path(pathStr);
27
29
  file.query_info_async(STAT_ATTRIBUTES, flags, GLib.PRIORITY_DEFAULT, null, (_s: Gio.File, res: Gio.AsyncResult) => {
28
30
  try {
29
31
  const info = file.query_info_finish(res);
30
- callback(null, options?.bigint ? new BigIntStats(info, path) : new Stats(info, path));
32
+ callback(null, options?.bigint ? new BigIntStats(info, pathStr) : new Stats(info, pathStr));
31
33
  } catch (err: unknown) {
32
- callback(createNodeError(err, syscall, path));
34
+ callback(createNodeError(err, syscall, pathStr));
33
35
  }
34
36
  });
35
37
  }
@@ -89,13 +91,15 @@ export function symlink(target: PathLike, path: PathLike, typeOrCallback: string
89
91
  if (typeof callback !== 'function') {
90
92
  throw new TypeError('Callback must be a function. Received ' + typeof callback);
91
93
  }
92
- const file = Gio.File.new_for_path(path.toString());
93
- file.make_symbolic_link_async(target.toString(), GLib.PRIORITY_DEFAULT, null, (_s: Gio.File, res: Gio.AsyncResult) => {
94
+ const pathStr = normalizePath(path);
95
+ const targetStr = normalizePath(target);
96
+ const file = Gio.File.new_for_path(pathStr);
97
+ file.make_symbolic_link_async(targetStr, GLib.PRIORITY_DEFAULT, null, (_s: Gio.File, res: Gio.AsyncResult) => {
94
98
  try {
95
99
  file.make_symbolic_link_finish(res);
96
100
  callback(null);
97
101
  } catch (err: unknown) {
98
- callback(createNodeError(err, 'symlink', target, path));
102
+ callback(createNodeError(err, 'symlink', targetStr, pathStr));
99
103
  }
100
104
  });
101
105
  }
@@ -159,7 +163,7 @@ export function open(path: PathLike, ...args: (OpenMode | Mode | OpenCallback |
159
163
  break;
160
164
  }
161
165
 
162
- openP(path, flags as OpenFlags | undefined, mode)
166
+ openP(path, flags as OpenFlags | number | undefined, mode)
163
167
  .then((fileHandle) => {
164
168
  callback(null, fileHandle.fd);
165
169
  })
@@ -616,10 +620,11 @@ export function readFile(path: PathLike, options: { encoding?: string; flag?: st
616
620
  export function readFile(path: PathLike, optsOrCb: { encoding?: string; flag?: string } | string | ((err: NodeJS.ErrnoException | null, data: Buffer) => void), maybeCb?: (err: NodeJS.ErrnoException | null, data: string | Buffer | null) => void): void {
617
621
  const callback = typeof optsOrCb === 'function' ? optsOrCb : maybeCb!;
618
622
  const options = typeof optsOrCb === 'function' ? undefined : optsOrCb;
623
+ const pathStr = normalizePath(path);
619
624
  Promise.resolve().then(() => {
620
625
  try {
621
626
  const readOpts = typeof options === 'string' ? { encoding: options as string | null, flag: 'r' } : { encoding: (options?.encoding ?? null) as string | null, flag: options?.flag ?? 'r' };
622
- callback(null, readFileSync(path.toString(), readOpts) as unknown as Buffer);
627
+ callback(null, readFileSync(pathStr, readOpts) as unknown as Buffer);
623
628
  } catch (err: unknown) {
624
629
  callback(err as NodeJS.ErrnoException, null as unknown as Buffer);
625
630
  }
@@ -632,9 +637,10 @@ export function writeFile(path: PathLike, data: string | Uint8Array, callback: N
632
637
  export function writeFile(path: PathLike, data: string | Uint8Array, options: { encoding?: string; mode?: number; flag?: string } | string, callback: NoParamCallback): void;
633
638
  export function writeFile(path: PathLike, data: string | Uint8Array, optsOrCb: { encoding?: string; mode?: number; flag?: string } | string | NoParamCallback, maybeCb?: NoParamCallback): void {
634
639
  const callback = typeof optsOrCb === 'function' ? optsOrCb : maybeCb!;
640
+ const pathStr = normalizePath(path);
635
641
  Promise.resolve().then(() => {
636
642
  try {
637
- writeFileSync(path.toString(), data);
643
+ writeFileSync(pathStr, data);
638
644
  callback(null);
639
645
  } catch (err: unknown) {
640
646
  callback(err as NodeJS.ErrnoException);
@@ -645,13 +651,15 @@ export function writeFile(path: PathLike, data: string | Uint8Array, optsOrCb: {
645
651
  // --- link (callback) ---
646
652
 
647
653
  export function link(existingPath: PathLike, newPath: PathLike, callback: NoParamCallback): void {
654
+ const existingStr = normalizePath(existingPath);
655
+ const newStr = normalizePath(newPath);
648
656
  Promise.resolve().then(() => {
649
657
  try {
650
- const result = GLib.spawn_command_line_sync(`ln ${existingPath.toString()} ${newPath.toString()}`);
658
+ const result = GLib.spawn_command_line_sync(`ln ${existingStr} ${newStr}`);
651
659
  if (!result[0]) {
652
- throw Object.assign(new Error(`EPERM: operation not permitted, link '${existingPath}' -> '${newPath}'`), {
660
+ throw Object.assign(new Error(`EPERM: operation not permitted, link '${existingStr}' -> '${newStr}'`), {
653
661
  code: 'EPERM', errno: -1, syscall: 'link',
654
- path: existingPath.toString(), dest: newPath.toString()
662
+ path: existingStr, dest: newStr
655
663
  });
656
664
  }
657
665
  callback(null);
@@ -664,9 +672,10 @@ export function link(existingPath: PathLike, newPath: PathLike, callback: NoPara
664
672
  // --- unlink (callback) ---
665
673
 
666
674
  export function unlink(path: PathLike, callback: NoParamCallback): void {
675
+ const pathStr = normalizePath(path);
667
676
  Promise.resolve().then(() => {
668
677
  try {
669
- GLib.unlink(path.toString());
678
+ GLib.unlink(pathStr);
670
679
  callback(null);
671
680
  } catch (err: unknown) {
672
681
  callback(err as NodeJS.ErrnoException);
package/src/cp.spec.ts ADDED
@@ -0,0 +1,181 @@
1
+ // Ported from refs/bun/test/js/node/fs/cp.test.ts and
2
+ // refs/node-test/parallel/test-fs-cp-sync-*.mjs
3
+ // Original: MIT, Oven & contributors / Node.js contributors.
4
+ // Rewritten for @gjsify/unit — behavior preserved, assertion dialect adapted.
5
+
6
+ import { describe, it, expect } from '@gjsify/unit';
7
+ import { cpSync, promises, existsSync, mkdirSync, writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { tmpdir } from 'node:os';
10
+
11
+ function makeTmp(): string {
12
+ return mkdtempSync(join(tmpdir(), 'gjsify-cp-'));
13
+ }
14
+
15
+ export default async () => {
16
+ await describe('fs.cpSync', async () => {
17
+ await it('copies a single file', async () => {
18
+ const tmp = makeTmp();
19
+ writeFileSync(join(tmp, 'a.txt'), 'hello');
20
+
21
+ cpSync(join(tmp, 'a.txt'), join(tmp, 'b.txt'));
22
+
23
+ expect(readFileSync(join(tmp, 'b.txt'), 'utf8')).toBe('hello');
24
+ rmSync(tmp, { recursive: true, force: true });
25
+ });
26
+
27
+ await it('throws EISDIR when src is directory and recursive is false', async () => {
28
+ const tmp = makeTmp();
29
+ mkdirSync(join(tmp, 'src'));
30
+
31
+ let threw = false;
32
+ try {
33
+ cpSync(join(tmp, 'src'), join(tmp, 'dest'));
34
+ } catch (e: any) {
35
+ threw = true;
36
+ expect(e.code).toBe('ERR_FS_EISDIR');
37
+ }
38
+ expect(threw).toBe(true);
39
+ rmSync(tmp, { recursive: true, force: true });
40
+ });
41
+
42
+ await it('recursively copies a directory tree', async () => {
43
+ const tmp = makeTmp();
44
+ mkdirSync(join(tmp, 'src', 'sub'), { recursive: true });
45
+ writeFileSync(join(tmp, 'src', 'a.txt'), 'a');
46
+ writeFileSync(join(tmp, 'src', 'sub', 'b.txt'), 'b');
47
+
48
+ cpSync(join(tmp, 'src'), join(tmp, 'dst'), { recursive: true });
49
+
50
+ expect(readFileSync(join(tmp, 'dst', 'a.txt'), 'utf8')).toBe('a');
51
+ expect(readFileSync(join(tmp, 'dst', 'sub', 'b.txt'), 'utf8')).toBe('b');
52
+ rmSync(tmp, { recursive: true, force: true });
53
+ });
54
+
55
+ await it('overwrites existing file by default (force=true)', async () => {
56
+ const tmp = makeTmp();
57
+ writeFileSync(join(tmp, 'src.txt'), 'new');
58
+ writeFileSync(join(tmp, 'dst.txt'), 'old');
59
+
60
+ cpSync(join(tmp, 'src.txt'), join(tmp, 'dst.txt'));
61
+
62
+ expect(readFileSync(join(tmp, 'dst.txt'), 'utf8')).toBe('new');
63
+ rmSync(tmp, { recursive: true, force: true });
64
+ });
65
+
66
+ await it('does not overwrite when force=false', async () => {
67
+ const tmp = makeTmp();
68
+ writeFileSync(join(tmp, 'src.txt'), 'new');
69
+ writeFileSync(join(tmp, 'dst.txt'), 'old');
70
+
71
+ cpSync(join(tmp, 'src.txt'), join(tmp, 'dst.txt'), { force: false });
72
+
73
+ expect(readFileSync(join(tmp, 'dst.txt'), 'utf8')).toBe('old');
74
+ rmSync(tmp, { recursive: true, force: true });
75
+ });
76
+
77
+ await it('throws EEXIST when force=false and errorOnExist=true', async () => {
78
+ const tmp = makeTmp();
79
+ writeFileSync(join(tmp, 'src.txt'), 'new');
80
+ writeFileSync(join(tmp, 'dst.txt'), 'old');
81
+
82
+ let threw = false;
83
+ try {
84
+ cpSync(join(tmp, 'src.txt'), join(tmp, 'dst.txt'), { force: false, errorOnExist: true });
85
+ } catch (e: any) {
86
+ threw = true;
87
+ expect(e.code).toBe('ERR_FS_CP_EEXIST');
88
+ }
89
+ expect(threw).toBe(true);
90
+ rmSync(tmp, { recursive: true, force: true });
91
+ });
92
+
93
+ await it('applies filter function — skips excluded entries', async () => {
94
+ const tmp = makeTmp();
95
+ mkdirSync(join(tmp, 'src'));
96
+ writeFileSync(join(tmp, 'src', 'keep.txt'), 'keep');
97
+ writeFileSync(join(tmp, 'src', 'skip.log'), 'skip');
98
+
99
+ cpSync(join(tmp, 'src'), join(tmp, 'dst'), {
100
+ recursive: true,
101
+ filter: (_src, _dst) => !_src.endsWith('.log'),
102
+ });
103
+
104
+ expect(existsSync(join(tmp, 'dst', 'keep.txt'))).toBe(true);
105
+ expect(existsSync(join(tmp, 'dst', 'skip.log'))).toBe(false);
106
+ rmSync(tmp, { recursive: true, force: true });
107
+ });
108
+
109
+ await it('throws ENOENT when src does not exist', async () => {
110
+ const tmp = makeTmp();
111
+
112
+ let threw = false;
113
+ try {
114
+ cpSync(join(tmp, 'nonexistent.txt'), join(tmp, 'dst.txt'));
115
+ } catch (e: any) {
116
+ threw = true;
117
+ expect(e.code).toBe('ENOENT');
118
+ }
119
+ expect(threw).toBe(true);
120
+ rmSync(tmp, { recursive: true, force: true });
121
+ });
122
+ });
123
+
124
+ await describe('fs.promises.cp', async () => {
125
+ await it('copies a single file', async () => {
126
+ const tmp = makeTmp();
127
+ writeFileSync(join(tmp, 'a.txt'), 'hello');
128
+
129
+ await promises.cp(join(tmp, 'a.txt'), join(tmp, 'b.txt'));
130
+
131
+ expect(readFileSync(join(tmp, 'b.txt'), 'utf8')).toBe('hello');
132
+ rmSync(tmp, { recursive: true, force: true });
133
+ });
134
+
135
+ await it('throws EISDIR when src is directory and recursive is false', async () => {
136
+ const tmp = makeTmp();
137
+ mkdirSync(join(tmp, 'src'));
138
+
139
+ let threw = false;
140
+ try {
141
+ await promises.cp(join(tmp, 'src'), join(tmp, 'dest'));
142
+ } catch (e: any) {
143
+ threw = true;
144
+ expect(e.code).toBe('ERR_FS_EISDIR');
145
+ }
146
+ expect(threw).toBe(true);
147
+ rmSync(tmp, { recursive: true, force: true });
148
+ });
149
+
150
+ await it('recursively copies a directory tree', async () => {
151
+ const tmp = makeTmp();
152
+ mkdirSync(join(tmp, 'src', 'sub'), { recursive: true });
153
+ writeFileSync(join(tmp, 'src', 'a.txt'), 'a');
154
+ writeFileSync(join(tmp, 'src', 'sub', 'b.txt'), 'b');
155
+
156
+ await promises.cp(join(tmp, 'src'), join(tmp, 'dst'), { recursive: true });
157
+
158
+ expect(readFileSync(join(tmp, 'dst', 'a.txt'), 'utf8')).toBe('a');
159
+ expect(readFileSync(join(tmp, 'dst', 'sub', 'b.txt'), 'utf8')).toBe('b');
160
+ rmSync(tmp, { recursive: true, force: true });
161
+ });
162
+
163
+ await it('applies async filter function', async () => {
164
+ const tmp = makeTmp();
165
+ mkdirSync(join(tmp, 'src'));
166
+ writeFileSync(join(tmp, 'src', 'keep.ts'), 'ts');
167
+ writeFileSync(join(tmp, 'src', 'skip.js'), 'js');
168
+
169
+ await promises.cp(join(tmp, 'src'), join(tmp, 'dst'), {
170
+ recursive: true,
171
+ filter: async (_src, _dst) => {
172
+ return !_src.endsWith('.js');
173
+ },
174
+ });
175
+
176
+ expect(existsSync(join(tmp, 'dst', 'keep.ts'))).toBe(true);
177
+ expect(existsSync(join(tmp, 'dst', 'skip.js'))).toBe(false);
178
+ rmSync(tmp, { recursive: true, force: true });
179
+ });
180
+ });
181
+ };
package/src/cp.ts ADDED
@@ -0,0 +1,328 @@
1
+ // fs.cp / fs.cpSync / fs.promises.cp — recursive copy
2
+ // Reference: Node.js lib/internal/fs/cpSync.js
3
+ // Reimplemented for GJS using Gio.File synchronous operations
4
+
5
+ import Gio from '@girs/gio-2.0';
6
+ import { join } from 'node:path';
7
+ import { normalizePath } from './utils.js';
8
+ import { createNodeError } from './errors.js';
9
+
10
+ import type { PathLike } from 'node:fs';
11
+
12
+ export interface CpSyncOptions {
13
+ dereference?: boolean;
14
+ errorOnExist?: boolean;
15
+ filter?: (src: string, dest: string) => boolean;
16
+ force?: boolean;
17
+ mode?: number;
18
+ preserveTimestamps?: boolean;
19
+ recursive?: boolean;
20
+ verbatimSymlinks?: boolean;
21
+ }
22
+
23
+ export interface CpOptions extends Omit<CpSyncOptions, 'filter'> {
24
+ filter?: (src: string, dest: string) => boolean | Promise<boolean>;
25
+ }
26
+
27
+ function makeEEXIST(destStr: string): NodeJS.ErrnoException {
28
+ const e: NodeJS.ErrnoException = new Error(`ERR_FS_CP_EEXIST: file already exists, copyfile '${destStr}'`);
29
+ e.code = 'ERR_FS_CP_EEXIST';
30
+ e.syscall = 'copyfile';
31
+ e.path = destStr;
32
+ return e;
33
+ }
34
+
35
+ function makeEISDIR(srcStr: string): NodeJS.ErrnoException {
36
+ const e: NodeJS.ErrnoException = new Error(`ERR_FS_EISDIR: illegal operation on a directory, copyfile '${srcStr}'`);
37
+ e.code = 'ERR_FS_EISDIR';
38
+ e.syscall = 'copyfile';
39
+ e.path = srcStr;
40
+ return e;
41
+ }
42
+
43
+ function makeENOTDIR(srcStr: string, destStr: string): NodeJS.ErrnoException {
44
+ const e: NodeJS.ErrnoException = new Error(`ENOTDIR: not a directory, copyfile '${srcStr}' -> '${destStr}'`);
45
+ e.code = 'ENOTDIR';
46
+ e.syscall = 'copyfile';
47
+ e.path = srcStr;
48
+ (e as any).dest = destStr;
49
+ return e;
50
+ }
51
+
52
+ function makeSYMLINKLOOP(srcStr: string, destStr: string): NodeJS.ErrnoException {
53
+ const e: NodeJS.ErrnoException = new Error(`ELOOP: too many levels of symbolic links, copyfile '${srcStr}' -> '${destStr}'`);
54
+ e.code = 'ELOOP';
55
+ e.syscall = 'copyfile';
56
+ e.path = srcStr;
57
+ (e as any).dest = destStr;
58
+ return e;
59
+ }
60
+
61
+ function queryCopyFlags(opts: CpSyncOptions | CpOptions): Gio.FileCopyFlags {
62
+ const force = opts.force ?? true;
63
+ let flags = Gio.FileCopyFlags.NONE;
64
+ if (force) flags |= Gio.FileCopyFlags.OVERWRITE;
65
+ if (opts.preserveTimestamps) flags |= Gio.FileCopyFlags.TARGET_DEFAULT_MODIFIED_TIME;
66
+ if (!opts.dereference) flags |= Gio.FileCopyFlags.NOFOLLOW_SYMLINKS;
67
+ return flags;
68
+ }
69
+
70
+ function copyOneSyncFile(srcFile: Gio.File, destFile: Gio.File, srcStr: string, destStr: string, opts: CpSyncOptions | CpOptions): void {
71
+ const force = opts.force ?? true;
72
+
73
+ // Check dest is not a directory
74
+ try {
75
+ const destInfo = destFile.query_info('standard::type', Gio.FileQueryInfoFlags.NONE, null);
76
+ if (destInfo.get_file_type() === Gio.FileType.DIRECTORY) {
77
+ throw makeENOTDIR(srcStr, destStr);
78
+ }
79
+ if (!force) {
80
+ if (opts.errorOnExist) throw makeEEXIST(destStr);
81
+ return; // silent skip when force=false and no errorOnExist
82
+ }
83
+ } catch (e: any) {
84
+ if (typeof e.code === 'string') throw e; // re-throw Node-style errors (string codes)
85
+ // Gio errors have numeric codes (e.g. NOT_FOUND=1) — dest doesn't exist, fine
86
+ }
87
+
88
+ const flags = queryCopyFlags(opts);
89
+ try {
90
+ srcFile.copy(destFile, flags, null, null);
91
+ } catch (err: unknown) {
92
+ throw createNodeError(err, 'copyfile', srcStr, destStr);
93
+ }
94
+ }
95
+
96
+ function cpOneDirSync(srcFile: Gio.File, destFile: Gio.File, srcStr: string, destStr: string, opts: CpSyncOptions | CpOptions): void {
97
+ // Detect src ⊂ dest cycle (if dest path starts with srcStr + separator)
98
+ const sep = srcStr.endsWith('/') ? '' : '/';
99
+ if (destStr.startsWith(srcStr + sep) && destStr !== srcStr) {
100
+ throw makeSYMLINKLOOP(srcStr, destStr);
101
+ }
102
+
103
+ // Create dest directory if it doesn't exist
104
+ if (!destFile.query_exists(null)) {
105
+ try {
106
+ destFile.make_directory_with_parents(null);
107
+ } catch (err: unknown) {
108
+ throw createNodeError(err, 'mkdir', destStr);
109
+ }
110
+ } else {
111
+ // dest exists — must be a directory
112
+ try {
113
+ const destInfo = destFile.query_info('standard::type', Gio.FileQueryInfoFlags.NONE, null);
114
+ if (destInfo.get_file_type() !== Gio.FileType.DIRECTORY) {
115
+ throw makeENOTDIR(destStr, srcStr);
116
+ }
117
+ } catch (e: any) {
118
+ if (typeof e.code === 'string') throw e;
119
+ }
120
+ }
121
+
122
+ // Enumerate children
123
+ let enumerator: Gio.FileEnumerator;
124
+ try {
125
+ enumerator = srcFile.enumerate_children(
126
+ 'standard::name,standard::type,standard::is-symlink',
127
+ opts.dereference ? Gio.FileQueryInfoFlags.NONE : Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
128
+ null,
129
+ );
130
+ } catch (err: unknown) {
131
+ throw createNodeError(err, 'scandir', srcStr);
132
+ }
133
+
134
+ let childInfo = enumerator.next_file(null);
135
+ while (childInfo !== null) {
136
+ const name = childInfo.get_name();
137
+ const childSrc = join(srcStr, name);
138
+ const childDest = join(destStr, name);
139
+
140
+ const filter = (opts as CpSyncOptions).filter;
141
+ if (filter && !filter(childSrc, childDest)) {
142
+ childInfo = enumerator.next_file(null);
143
+ continue;
144
+ }
145
+
146
+ cpSyncInternal(childSrc, childDest, opts);
147
+ childInfo = enumerator.next_file(null);
148
+ }
149
+ }
150
+
151
+ function cpSyncInternal(srcStr: string, destStr: string, opts: CpSyncOptions | CpOptions): void {
152
+ const srcFile = Gio.File.new_for_path(srcStr);
153
+ const destFile = Gio.File.new_for_path(destStr);
154
+
155
+ let info: Gio.FileInfo;
156
+ try {
157
+ info = srcFile.query_info(
158
+ 'standard::type,standard::is-symlink',
159
+ opts.dereference ? Gio.FileQueryInfoFlags.NONE : Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
160
+ null,
161
+ );
162
+ } catch (err: unknown) {
163
+ throw createNodeError(err, 'stat', srcStr);
164
+ }
165
+
166
+ const type = info.get_file_type();
167
+
168
+ if (type === Gio.FileType.DIRECTORY) {
169
+ if (!opts.recursive) throw makeEISDIR(srcStr);
170
+ cpOneDirSync(srcFile, destFile, srcStr, destStr, opts);
171
+ } else {
172
+ copyOneSyncFile(srcFile, destFile, srcStr, destStr, opts);
173
+ }
174
+ }
175
+
176
+ // ─── Public API ──────────────────────────────────────────────────────────────
177
+
178
+ export function cpSync(src: PathLike, dest: PathLike, options?: CpSyncOptions): void {
179
+ const srcStr = normalizePath(src);
180
+ const destStr = normalizePath(dest);
181
+ const opts: CpSyncOptions = options ?? {};
182
+
183
+ const srcFile = Gio.File.new_for_path(srcStr);
184
+
185
+ // Apply top-level filter before doing anything
186
+ const filter = opts.filter;
187
+ if (filter && !filter(srcStr, destStr)) return;
188
+
189
+ // Check src is a directory without recursive option
190
+ try {
191
+ const info = srcFile.query_info(
192
+ 'standard::type',
193
+ opts.dereference ? Gio.FileQueryInfoFlags.NONE : Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
194
+ null,
195
+ );
196
+ if (info.get_file_type() === Gio.FileType.DIRECTORY && !opts.recursive) {
197
+ throw makeEISDIR(srcStr);
198
+ }
199
+ } catch (e: any) {
200
+ if (typeof e.code === 'string') throw e;
201
+ throw createNodeError(e, 'stat', srcStr);
202
+ }
203
+
204
+ cpSyncInternal(srcStr, destStr, opts);
205
+ }
206
+
207
+ export function cp(
208
+ src: PathLike,
209
+ dest: PathLike,
210
+ options: CpOptions | ((err: NodeJS.ErrnoException | null) => void),
211
+ callback?: (err: NodeJS.ErrnoException | null) => void,
212
+ ): void {
213
+ let opts: CpOptions;
214
+ let cb: (err: NodeJS.ErrnoException | null) => void;
215
+
216
+ if (typeof options === 'function') {
217
+ cb = options;
218
+ opts = {};
219
+ } else {
220
+ cb = callback!;
221
+ opts = options;
222
+ }
223
+
224
+ const asyncFilter = opts.filter;
225
+ if (asyncFilter) {
226
+ // Wrap async filter through a sync-compatible adapter by running
227
+ // a mini async pipeline: resolve filter for each entry, then cpSync.
228
+ // For simplicity, resolve the top-level filter and then run cpSync
229
+ // with a synchronous shim. Full async recursive filtering uses the
230
+ // promise variant.
231
+ Promise.resolve(asyncFilter(normalizePath(src), normalizePath(dest))).then((include) => {
232
+ if (!include) { cb(null); return; }
233
+ try {
234
+ // For directories, we must run asynchronously through promises.cp
235
+ cpPromises(src, dest, opts).then(() => cb(null)).catch(cb);
236
+ } catch (e: any) {
237
+ cb(e);
238
+ }
239
+ }).catch(cb);
240
+ return;
241
+ }
242
+
243
+ // No async filter — use synchronous implementation on next tick
244
+ Promise.resolve().then(() => {
245
+ try {
246
+ cpSync(src, dest, opts as CpSyncOptions);
247
+ cb(null);
248
+ } catch (e: any) {
249
+ cb(e);
250
+ }
251
+ });
252
+ }
253
+
254
+ // ─── promises.cp ─────────────────────────────────────────────────────────────
255
+
256
+ async function cpPromisesDir(srcStr: string, destStr: string, opts: CpOptions): Promise<void> {
257
+ const sep = srcStr.endsWith('/') ? '' : '/';
258
+ if (destStr.startsWith(srcStr + sep) && destStr !== srcStr) {
259
+ throw makeSYMLINKLOOP(srcStr, destStr);
260
+ }
261
+
262
+ const destFile = Gio.File.new_for_path(destStr);
263
+ if (!destFile.query_exists(null)) {
264
+ try {
265
+ destFile.make_directory_with_parents(null);
266
+ } catch (err: unknown) {
267
+ throw createNodeError(err, 'mkdir', destStr);
268
+ }
269
+ }
270
+
271
+ const srcFile = Gio.File.new_for_path(srcStr);
272
+ let enumerator: Gio.FileEnumerator;
273
+ try {
274
+ enumerator = srcFile.enumerate_children(
275
+ 'standard::name,standard::type',
276
+ opts.dereference ? Gio.FileQueryInfoFlags.NONE : Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
277
+ null,
278
+ );
279
+ } catch (err: unknown) {
280
+ throw createNodeError(err, 'scandir', srcStr);
281
+ }
282
+
283
+ let childInfo = enumerator.next_file(null);
284
+ while (childInfo !== null) {
285
+ const name = childInfo.get_name();
286
+ const childSrc = join(srcStr, name);
287
+ const childDest = join(destStr, name);
288
+
289
+ const filter = opts.filter;
290
+ if (filter) {
291
+ const include = await Promise.resolve(filter(childSrc, childDest));
292
+ if (!include) {
293
+ childInfo = enumerator.next_file(null);
294
+ continue;
295
+ }
296
+ }
297
+
298
+ await cpPromises(childSrc, childDest, opts);
299
+ childInfo = enumerator.next_file(null);
300
+ }
301
+ }
302
+
303
+ async function cpPromises(src: PathLike, dest: PathLike, opts: CpOptions = {}): Promise<void> {
304
+ const srcStr = normalizePath(src);
305
+ const destStr = normalizePath(dest);
306
+
307
+ const srcFile = Gio.File.new_for_path(srcStr);
308
+ let info: Gio.FileInfo;
309
+ try {
310
+ info = srcFile.query_info(
311
+ 'standard::type',
312
+ opts.dereference ? Gio.FileQueryInfoFlags.NONE : Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
313
+ null,
314
+ );
315
+ } catch (err: unknown) {
316
+ throw createNodeError(err, 'stat', srcStr);
317
+ }
318
+
319
+ if (info.get_file_type() === Gio.FileType.DIRECTORY) {
320
+ if (!opts.recursive) throw makeEISDIR(srcStr);
321
+ await cpPromisesDir(srcStr, destStr, opts);
322
+ } else {
323
+ const destFile = Gio.File.new_for_path(destStr);
324
+ copyOneSyncFile(srcFile, destFile, srcStr, destStr, opts);
325
+ }
326
+ }
327
+
328
+ export { cpPromises as cpAsync };