Skip to content

Commit 137a8a0

Browse files
authored
[Storage] Fix fileExists() to return true for directories (#2968)
1 parent 1decd8a commit 137a8a0

File tree

2 files changed

+202
-6
lines changed

2 files changed

+202
-6
lines changed

packages/playground/storage/src/lib/filesystems.spec.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Filesystem } from './filesystems';
22
import {
33
InMemoryFilesystem,
4+
InMemoryFilesystemBackend,
45
ZipFilesystem,
56
OverlayFilesystem,
67
FetchFilesystem,
@@ -440,3 +441,191 @@ describe('NodeJsFilesystem', () => {
440441
);
441442
});
442443
});
444+
445+
describe('InMemoryFilesystemBackend', () => {
446+
let backend: InMemoryFilesystemBackend;
447+
448+
beforeEach(() => {
449+
backend = new InMemoryFilesystemBackend();
450+
});
451+
452+
describe('fileExists', () => {
453+
it('should return true for root directory', async () => {
454+
expect(await backend.fileExists('/')).toBe(true);
455+
});
456+
457+
it('should return true for existing files', async () => {
458+
await backend.writeFile('/test.txt', new Uint8Array([1, 2, 3]));
459+
expect(await backend.fileExists('/test.txt')).toBe(true);
460+
});
461+
462+
it('should return true for existing directories', async () => {
463+
await backend.mkdir('/mydir');
464+
expect(await backend.fileExists('/mydir')).toBe(true);
465+
});
466+
467+
it('should return false for non-existent paths', async () => {
468+
expect(await backend.fileExists('/nonexistent')).toBe(false);
469+
});
470+
471+
it('should return true for nested directories', async () => {
472+
await backend.mkdir('/parent/child');
473+
expect(await backend.fileExists('/parent')).toBe(true);
474+
expect(await backend.fileExists('/parent/child')).toBe(true);
475+
});
476+
});
477+
478+
describe('isDir', () => {
479+
it('should return true for root directory', async () => {
480+
expect(await backend.isDir('/')).toBe(true);
481+
});
482+
483+
it('should return true for directories', async () => {
484+
await backend.mkdir('/mydir');
485+
expect(await backend.isDir('/mydir')).toBe(true);
486+
});
487+
488+
it('should return false for files', async () => {
489+
await backend.writeFile('/test.txt', new Uint8Array([1, 2, 3]));
490+
expect(await backend.isDir('/test.txt')).toBe(false);
491+
});
492+
493+
it('should return false for non-existent paths', async () => {
494+
expect(await backend.isDir('/nonexistent')).toBe(false);
495+
});
496+
});
497+
498+
describe('mkdir', () => {
499+
it('should create a directory', async () => {
500+
await backend.mkdir('/newdir');
501+
expect(await backend.isDir('/newdir')).toBe(true);
502+
});
503+
504+
it('should create nested directories', async () => {
505+
await backend.mkdir('/parent/child/grandchild');
506+
expect(await backend.isDir('/parent')).toBe(true);
507+
expect(await backend.isDir('/parent/child')).toBe(true);
508+
expect(await backend.isDir('/parent/child/grandchild')).toBe(true);
509+
});
510+
511+
it('should not fail when directory already exists', async () => {
512+
await backend.mkdir('/mydir');
513+
await backend.mkdir('/mydir');
514+
expect(await backend.isDir('/mydir')).toBe(true);
515+
});
516+
});
517+
518+
describe('writeFile and read', () => {
519+
it('should write and read a file', async () => {
520+
const data = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
521+
await backend.writeFile('/test.txt', data);
522+
523+
const file = await backend.read('/test.txt');
524+
const content = new Uint8Array(await file.arrayBuffer());
525+
expect(content).toEqual(data);
526+
});
527+
528+
it('should create parent directories when writing a file', async () => {
529+
const data = new Uint8Array([1, 2, 3]);
530+
await backend.writeFile('/deep/nested/file.txt', data);
531+
532+
expect(await backend.isDir('/deep')).toBe(true);
533+
expect(await backend.isDir('/deep/nested')).toBe(true);
534+
expect(await backend.fileExists('/deep/nested/file.txt')).toBe(
535+
true
536+
);
537+
});
538+
});
539+
540+
describe('listFiles', () => {
541+
it('should list files in root directory', async () => {
542+
await backend.writeFile('/file1.txt', new Uint8Array([1]));
543+
await backend.writeFile('/file2.txt', new Uint8Array([2]));
544+
await backend.mkdir('/dir1');
545+
546+
const files = await backend.listFiles('/');
547+
expect(files).toContain('file1.txt');
548+
expect(files).toContain('file2.txt');
549+
expect(files).toContain('dir1');
550+
});
551+
552+
it('should list files in subdirectory', async () => {
553+
await backend.mkdir('/mydir');
554+
await backend.writeFile('/mydir/nested.txt', new Uint8Array([1]));
555+
556+
const files = await backend.listFiles('/mydir');
557+
expect(files).toContain('nested.txt');
558+
});
559+
});
560+
561+
describe('unlink', () => {
562+
it('should delete a file', async () => {
563+
await backend.writeFile('/test.txt', new Uint8Array([1]));
564+
expect(await backend.fileExists('/test.txt')).toBe(true);
565+
566+
await backend.unlink('/test.txt');
567+
expect(await backend.fileExists('/test.txt')).toBe(false);
568+
});
569+
});
570+
571+
describe('rmdir', () => {
572+
it('should delete an empty directory', async () => {
573+
await backend.mkdir('/emptydir');
574+
await backend.rmdir('/emptydir', false);
575+
expect(await backend.fileExists('/emptydir')).toBe(false);
576+
});
577+
578+
it('should delete a directory recursively', async () => {
579+
await backend.mkdir('/parent/child');
580+
await backend.writeFile(
581+
'/parent/child/file.txt',
582+
new Uint8Array([1])
583+
);
584+
585+
await backend.rmdir('/parent', true);
586+
expect(await backend.fileExists('/parent')).toBe(false);
587+
});
588+
589+
it('should throw when deleting non-empty directory without recursive flag', async () => {
590+
await backend.mkdir('/parent');
591+
await backend.writeFile('/parent/file.txt', new Uint8Array([1]));
592+
593+
await expect(backend.rmdir('/parent', false)).rejects.toThrow(
594+
'Directory not empty'
595+
);
596+
});
597+
});
598+
599+
describe('mv', () => {
600+
it('should move a file', async () => {
601+
await backend.writeFile('/source.txt', new Uint8Array([1, 2, 3]));
602+
await backend.mv('/source.txt', '/dest.txt');
603+
604+
expect(await backend.fileExists('/source.txt')).toBe(false);
605+
expect(await backend.fileExists('/dest.txt')).toBe(true);
606+
});
607+
608+
it('should move a directory', async () => {
609+
await backend.mkdir('/srcdir');
610+
await backend.writeFile('/srcdir/file.txt', new Uint8Array([1]));
611+
await backend.mv('/srcdir', '/dstdir');
612+
613+
expect(await backend.fileExists('/srcdir')).toBe(false);
614+
expect(await backend.fileExists('/dstdir')).toBe(true);
615+
expect(await backend.fileExists('/dstdir/file.txt')).toBe(true);
616+
});
617+
});
618+
619+
describe('clear', () => {
620+
it('should remove all files and directories', async () => {
621+
await backend.mkdir('/dir1');
622+
await backend.writeFile('/file1.txt', new Uint8Array([1]));
623+
await backend.writeFile('/dir1/nested.txt', new Uint8Array([2]));
624+
625+
await backend.clear();
626+
627+
const files = await backend.listFiles('/');
628+
expect(files).toHaveLength(0);
629+
});
630+
});
631+
});

packages/playground/storage/src/lib/filesystems.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -552,17 +552,24 @@ export class OpfsFilesystemBackend implements WritableFilesystemBackend {
552552

553553
async fileExists(absolutePath: string): Promise<boolean> {
554554
const segments = absolutePath.split('/').filter(Boolean);
555-
const fileName = segments.pop();
556-
if (!fileName) {
557-
return false;
555+
// Root always exists
556+
if (segments.length === 0) {
557+
return true;
558558
}
559+
const name = segments.pop()!;
559560
try {
560561
let dir = this.opfsRoot;
561562
for (const segment of segments) {
562563
dir = await dir.getDirectoryHandle(segment);
563564
}
564-
await dir.getFileHandle(fileName);
565-
return true;
565+
// Check if it's a file or directory
566+
try {
567+
await dir.getFileHandle(name);
568+
return true;
569+
} catch {
570+
await dir.getDirectoryHandle(name);
571+
return true;
572+
}
566573
} catch {
567574
return false;
568575
}
@@ -722,7 +729,7 @@ export class InMemoryFilesystemBackend implements WritableFilesystemBackend {
722729

723730
async fileExists(absolutePath: string): Promise<boolean> {
724731
const node = this.getNode(absolutePath);
725-
return !!node && node.type === 'file';
732+
return !!node;
726733
}
727734

728735
async listFiles(absolutePath: string): Promise<string[]> {

0 commit comments

Comments
 (0)