Skip to content

Commit d52b3b4

Browse files
committed
Directory operations
1 parent 2ae32b1 commit d52b3b4

File tree

3 files changed

+274
-8
lines changed

3 files changed

+274
-8
lines changed

packages/php-wasm/node/src/test/mounting.spec.ts

Lines changed: 245 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { createNodeFsMountHandler, loadNodeRuntime } from '..';
2-
import { ErrnoError, PHP } from '@php-wasm/universal';
2+
import {
3+
__private__dont__use,
4+
ErrnoError,
5+
FSHelpers,
6+
PHP,
7+
} from '@php-wasm/universal';
38
import { RecommendedPHPVersion } from '@wp-playground/common';
49
import path, { dirname } from 'path';
510
import fs from 'fs';
@@ -18,7 +23,7 @@ describe('Mounting', () => {
1823
php.exit();
1924
});
2025

21-
describe('File operations', () => {
26+
describe('Test mounted file operations', () => {
2227
it('Should mount a file with exact content match', async () => {
2328
const testFilePath = path.join(
2429
__dirname,
@@ -193,7 +198,7 @@ describe('Mounting', () => {
193198
});
194199
});
195200

196-
describe('Directory operations', () => {
201+
describe('Test mounted directory operations', () => {
197202
it('Should mount nested directories with recursive structure matching', async () => {
198203
const testDataPath = path.join(__dirname, 'test-data');
199204
await php.mount(
@@ -243,6 +248,227 @@ describe('Mounting', () => {
243248
}
244249
});
245250

251+
it('Should throw an error when mounting to an existing directory', async () => {
252+
const testDataPath = path.join(__dirname, 'test-data');
253+
await php.mount(
254+
'/nested-test',
255+
createNodeFsMountHandler(testDataPath)
256+
);
257+
258+
try {
259+
await php.mount(
260+
'/nested-test',
261+
createNodeFsMountHandler(testDataPath)
262+
);
263+
} catch (e: any) {
264+
e = e as ErrnoError;
265+
expect(e.name).toBe('ErrnoError');
266+
expect(e.errno).toBe(10);
267+
}
268+
});
269+
270+
describe('Should be editable', async () => {
271+
it('Should add a new directory', async () => {
272+
const testDataPath = path.join(__dirname, 'test-data');
273+
await php.mount(
274+
'/nested-test',
275+
createNodeFsMountHandler(testDataPath)
276+
);
277+
278+
await php.mkdir('/nested-test/new-dir');
279+
expect(php.isDir('/nested-test/new-dir')).toBe(true);
280+
281+
await php.rmdir('/nested-test/new-dir');
282+
expect(php.isDir('/nested-test/new-dir')).toBe(false);
283+
});
284+
285+
it('Should move a directory', async () => {
286+
const testDataPath = path.join(__dirname, 'test-data');
287+
await php.mount(
288+
'/nested-test',
289+
createNodeFsMountHandler(testDataPath)
290+
);
291+
292+
await php.mv(
293+
'/nested-test/nested-symlinked-folder',
294+
'/nested-test/new-dir'
295+
);
296+
expect(php.isDir('/nested-test/new-dir')).toBe(true);
297+
expect(php.isDir('/nested-test/nested-symlinked-folder')).toBe(
298+
false
299+
);
300+
301+
await php.mv(
302+
'/nested-test/new-dir',
303+
'/nested-test/nested-symlinked-folder'
304+
);
305+
expect(php.isDir('/nested-test/new-dir')).toBe(false);
306+
expect(php.isDir('/nested-test/nested-symlinked-folder')).toBe(
307+
true
308+
);
309+
});
310+
311+
it('Should remove a directory', async () => {
312+
const testDataPath = path.join(__dirname, 'test-data');
313+
await php.mount(
314+
'/nested-test',
315+
createNodeFsMountHandler(testDataPath)
316+
);
317+
318+
const backupDir = path.join(
319+
__dirname,
320+
'test-data',
321+
'backup-nested-test'
322+
);
323+
await php.mkdir(backupDir);
324+
await FSHelpers.copyRecursive(
325+
php[__private__dont__use].FS,
326+
'/nested-test/nested-symlinked-folder',
327+
backupDir
328+
);
329+
330+
await php.rmdir('/nested-test/nested-symlinked-folder');
331+
expect(php.isDir('/nested-test/nested-symlinked-folder')).toBe(
332+
false
333+
);
334+
335+
await FSHelpers.copyRecursive(
336+
php[__private__dont__use].FS,
337+
backupDir,
338+
'/nested-test/nested-symlinked-folder'
339+
);
340+
expect(php.isDir('/nested-test/nested-symlinked-folder')).toBe(
341+
true
342+
);
343+
});
344+
345+
it('Should add a new file', async () => {
346+
const testDataPath = path.join(__dirname, 'test-data');
347+
await php.mount(
348+
'/nested-test',
349+
createNodeFsMountHandler(testDataPath)
350+
);
351+
352+
await php.writeFile(
353+
'/nested-test/nested-symlinked-folder/new-file.txt',
354+
'new file content'
355+
);
356+
357+
expect(
358+
await php.readFileAsText(
359+
'/nested-test/nested-symlinked-folder/new-file.txt'
360+
)
361+
).toBe('new file content');
362+
363+
await php.unlink(
364+
'/nested-test/nested-symlinked-folder/new-file.txt'
365+
);
366+
expect(
367+
php.isFile(
368+
'/nested-test/nested-symlinked-folder/new-file.txt'
369+
)
370+
).toBe(false);
371+
});
372+
373+
it('Should edit a file', async () => {
374+
const testDataPath = path.join(__dirname, 'test-data');
375+
await php.mount(
376+
'/nested-test',
377+
createNodeFsMountHandler(testDataPath)
378+
);
379+
380+
const fileContent = await php.readFileAsText(
381+
'/nested-test/nested-symlinked-folder/nested-document.txt'
382+
);
383+
384+
await php.writeFile(
385+
'/nested-test/nested-symlinked-folder/nested-document.txt',
386+
'new file content'
387+
);
388+
389+
expect(
390+
await php.readFileAsText(
391+
'/nested-test/nested-symlinked-folder/nested-document.txt'
392+
)
393+
).toBe('new file content');
394+
395+
await php.writeFile(
396+
'/nested-test/nested-symlinked-folder/nested-document.txt',
397+
fileContent
398+
);
399+
expect(
400+
await php.readFileAsText(
401+
'/nested-test/nested-symlinked-folder/nested-document.txt'
402+
)
403+
).toBe(fileContent);
404+
});
405+
406+
it('Should delete a file', async () => {
407+
const testDataPath = path.join(__dirname, 'test-data');
408+
await php.mount(
409+
'/nested-test',
410+
createNodeFsMountHandler(testDataPath)
411+
);
412+
413+
const fileContent = await php.readFileAsText(
414+
'/nested-test/nested-symlinked-folder/nested-document.txt'
415+
);
416+
417+
await php.unlink(
418+
'/nested-test/nested-symlinked-folder/nested-document.txt'
419+
);
420+
expect(
421+
php.isFile(
422+
'/nested-test/nested-symlinked-folder/nested-document.txt'
423+
)
424+
).toBe(false);
425+
426+
await php.writeFile(
427+
'/nested-test/nested-symlinked-folder/nested-document.txt',
428+
fileContent
429+
);
430+
expect(
431+
await php.readFileAsText(
432+
'/nested-test/nested-symlinked-folder/nested-document.txt'
433+
)
434+
).toBe(fileContent);
435+
});
436+
});
437+
438+
it('Should not be deletable', async () => {
439+
const testDataPath = path.join(__dirname, 'test-data');
440+
await php.mount(
441+
'/nested-test',
442+
createNodeFsMountHandler(testDataPath)
443+
);
444+
445+
try {
446+
await php.rmdir('/nested-test');
447+
} catch (e: any) {
448+
e = e as Error;
449+
expect(e.message).toContain(
450+
'Could not remove directory "/nested-test": Device or resource busy.'
451+
);
452+
}
453+
});
454+
455+
it('Should not be movable', async () => {
456+
const testDataPath = path.join(__dirname, 'test-data');
457+
await php.mount(
458+
'/nested-test',
459+
createNodeFsMountHandler(testDataPath)
460+
);
461+
462+
try {
463+
await php.mv('/nested-test', '/nested-test-moved');
464+
} catch (e: any) {
465+
e = e as Error;
466+
expect(e.message).toContain(
467+
'Could not move /nested-test to /nested-test-moved: Device or resource busy.'
468+
);
469+
}
470+
});
471+
246472
it('Should unmount a directory and remove created node from VFS', async () => {
247473
const testDataPath = path.join(__dirname, 'test-data');
248474
const unmount = await php.mount(
@@ -270,5 +496,21 @@ describe('Mounting', () => {
270496
await unmount();
271497
expect(php.isDir('/nested-test')).toBe(true);
272498
});
499+
500+
it('Should remount after unmounting', async () => {
501+
const testDataPath = path.join(__dirname, 'test-data');
502+
const unmount = await php.mount(
503+
'/nested-test',
504+
createNodeFsMountHandler(testDataPath)
505+
);
506+
507+
await unmount();
508+
await php.mount(
509+
'/nested-test',
510+
createNodeFsMountHandler(testDataPath)
511+
);
512+
513+
expect(php.isDir('/nested-test')).toBe(true);
514+
});
273515
});
274516
});

packages/php-wasm/universal/src/lib/fs-helpers.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Emscripten } from './emscripten-types';
22
import {
3+
ErrnoError,
34
getEmscriptenFsError,
45
rethrowFileSystemError,
56
} from './rethrow-file-system-error';
@@ -132,6 +133,18 @@ export class FSHelpers {
132133
path: string,
133134
options: RmDirOptions = { recursive: true }
134135
) {
136+
/**
137+
* Mount points cannot be removed and will throw a ErrnoError with
138+
* the code 10 (EBUSY).
139+
* To prevent the recursive option from removing internal files before
140+
* failing to remove the mount point, we need to check if the path is a
141+
* mount point and throw an error early.
142+
*/
143+
const mountPoint = FS.lookupPath(path).node.mount;
144+
if (mountPoint.mountpoint === path) {
145+
throw new ErrnoError(10);
146+
}
147+
135148
if (options?.recursive) {
136149
FSHelpers.listFiles(FS, path).forEach((file) => {
137150
const filePath = `${path}/${file}`;

packages/php-wasm/universal/src/lib/rethrow-file-system-error.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,17 @@
66
* @see https://github.com/emscripten-core/emscripten/blob/38eedc630f17094b3202fd48ac0c2c585dbea31e/system/include/wasi/api.h#L336
77
*/
88

9-
export interface ErrnoError extends Error {
9+
export class ErrnoError extends Error {
10+
constructor(errno: number, message?: string, options?: { cause?: any }) {
11+
super(message);
12+
this.name = 'ErrnoError';
13+
this.errno = errno;
14+
this.message = message ?? '';
15+
this.cause = options?.cause;
16+
}
17+
1018
node?: any;
1119
errno: number;
12-
message: string;
1320
}
1421
/**
1522
* @see https://github.com/emscripten-core/emscripten/blob/38eedc630f17094b3202fd48ac0c2c585dbea31e/system/include/wasi/api.h#L336
@@ -117,9 +124,13 @@ export function rethrowFileSystemError(messagePrefix = '') {
117124
path !== null
118125
? messagePrefix.replaceAll('{path}', path)
119126
: messagePrefix;
120-
throw new Error(`${formattedPrefix}: ${errmsg}`, {
121-
cause: e,
122-
});
127+
throw new ErrnoError(
128+
errno,
129+
`${formattedPrefix}: ${errmsg}`,
130+
{
131+
cause: e,
132+
}
133+
);
123134
}
124135

125136
throw e;

0 commit comments

Comments
 (0)