Skip to content

Commit a7f52a9

Browse files
committed
feat(fs): support read-only and read-write file systems
1 parent f907f11 commit a7f52a9

File tree

2 files changed

+63
-41
lines changed

2 files changed

+63
-41
lines changed

packages/fs/src/__test__/fs.abstraction.test.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,35 @@ o.spec('FileSystemAbstraction', () => {
1919
const fakeUnknown = new FakeSystem('unknown');
2020
fsa.register('', fakeUnknown);
2121

22-
o(fsa.get('fake://foo').protocol).equals('fake');
23-
o(fsa.get('fake:/foo').protocol).equals('unknown');
22+
o(fsa.get('fake://foo', 'r').protocol).equals('fake');
23+
o(fsa.get('fake:/foo', 'r').protocol).equals('unknown');
24+
});
25+
26+
o('should register filesystems as rw', () => {
27+
const fsa = new FileSystemAbstraction();
28+
29+
const fakeLocal = new FakeSystem('fake');
30+
fsa.register('fake://', fakeLocal);
31+
o(fsa.get('fake://foo', 'rw').protocol).equals('fake');
32+
});
33+
34+
o('should not return a read only filesystem when wanting to write', () => {
35+
const fsa = new FileSystemAbstraction();
36+
37+
const fakeLocal = new FakeSystem('fake');
38+
fsa.register('fake://', fakeLocal, 'r');
39+
o(() => fsa.get('fake://foo', 'rw')).throws(Error);
40+
});
41+
42+
o('should allow read and read-write file systems to be registered', () => {
43+
const fsa = new FileSystemAbstraction();
44+
45+
const fakeR = new FakeSystem('r');
46+
const fakeRw = new FakeSystem('rw');
47+
48+
fsa.register('fake://', fakeR, 'r');
49+
fsa.register('fake://', fakeRw, 'rw');
50+
o(fsa.get('fake://foo', 'rw').protocol).equals('rw');
2451
});
2552

2653
o('should find file systems in order they were registered', () => {
@@ -31,9 +58,9 @@ o.spec('FileSystemAbstraction', () => {
3158
fsa.register('fake://', fakeA);
3259
fsa.register('fake://some-prefix-string/', fakeB);
3360

34-
o(fsa.get('fake://foo').protocol).equals('fake');
35-
o(fsa.get('fake://some-prefix-string/').protocol).equals('fakeSpecific');
36-
o(fsa.get('fake://some-prefix-string/some-key').protocol).equals('fakeSpecific');
61+
o(fsa.get('fake://foo', 'r').protocol).equals('fake');
62+
o(fsa.get('fake://some-prefix-string/', 'r').protocol).equals('fakeSpecific');
63+
o(fsa.get('fake://some-prefix-string/some-key', 'r').protocol).equals('fakeSpecific');
3764
});
3865

3966
o('should order file systems by length', () => {
@@ -44,9 +71,9 @@ o.spec('FileSystemAbstraction', () => {
4471
fsa.register('fake://some-prefix-string/', fakeB);
4572
fsa.register('fake://', fakeA);
4673

47-
o(fsa.get('fake://foo').protocol).equals('fake');
48-
o(fsa.get('fake://some-prefix-string/').protocol).equals('fakeSpecific');
49-
o(fsa.get('fake://some-prefix-string/some-key').protocol).equals('fakeSpecific');
74+
o(fsa.get('fake://foo', 'r').protocol).equals('fake');
75+
o(fsa.get('fake://some-prefix-string/', 'r').protocol).equals('fakeSpecific');
76+
o(fsa.get('fake://some-prefix-string/some-key', 'r').protocol).equals('fakeSpecific');
5077
});
5178

5279
o('should replace file systems when registering duplicates', () => {

packages/fs/src/fs.abstraction.ts

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,37 @@ function isRecord(obj: unknown): obj is Record<string, unknown> {
1818
return obj.constructor === Object;
1919
}
2020

21+
export type Flag = FlagRead | FlagReadWrite;
22+
export type FlagRead = 'r';
23+
export type FlagReadWrite = 'rw';
24+
2125
export class FileSystemAbstraction implements FileSystem {
2226
protocol = 'abstract';
2327
/**
2428
* Is the systems array currently ordered
2529
* @see FileSystemAbstraction.sortSystems
2630
*/
2731
private isOrdered = true;
28-
systems: { path: string; system: FileSystem }[] = [];
32+
systems: { path: string; system: FileSystem; flag: Flag }[] = [];
2933

3034
/**
3135
* Register a file system to a specific path which can then be used with any `fsa` command
3236
*
3337
* @example
34-
* fsa.register('s3://', fsS3)
35-
* fsa.register('s3://bucket-a/key-a', specificS3)
38+
* fsa.register('s3://', fsS3, 'rw')
39+
* fsa.register('s3://bucket-a/key-a', specificS3, 'r')
3640
* fsa.register('http://', fsHttp)
3741
*
3842
*/
39-
register(path: string, system: FileSystem): void {
43+
register(path: string, system: FileSystem, flag: Flag = 'rw'): void {
4044
for (let i = 0; i < this.systems.length; i++) {
4145
const sys = this.systems[i];
42-
if (sys.path === path) {
43-
this.systems.splice(i, 1, { path, system });
46+
if (sys.path === path && sys.flag === flag) {
47+
this.systems.splice(i, 1, { path, system, flag });
4448
return;
4549
}
4650
}
47-
this.systems.push({ path, system });
51+
this.systems.push({ path, system, flag });
4852
this.isOrdered = false;
4953
}
5054

@@ -56,10 +60,11 @@ export class FileSystemAbstraction implements FileSystem {
5660
* @returns Content of the file
5761
*/
5862
read(filePath: string): Promise<Buffer> {
59-
return this.get(filePath).read(filePath);
63+
return this.get(filePath, 'r').read(filePath);
6064
}
6165

62-
/** Read a file as JSON
66+
/**
67+
* Read a file as JSON
6368
* @param filePath file to read
6469
* @returns JSON Content of the file
6570
*/
@@ -75,7 +80,7 @@ export class FileSystemAbstraction implements FileSystem {
7580
* @returns Stream of file contents
7681
*/
7782
stream(filePath: string): Readable {
78-
return this.get(filePath).stream(filePath);
83+
return this.get(filePath, 'r').stream(filePath);
7984
}
8085

8186
/**
@@ -89,9 +94,9 @@ export class FileSystemAbstraction implements FileSystem {
8994
write(filePath: string, buffer: FileWriteTypes, opts?: WriteOptions): Promise<void> {
9095
if (Array.isArray(buffer) || isRecord(buffer)) {
9196
const content = JSON.stringify(buffer, null, 2);
92-
return this.get(filePath).write(filePath, content, { contentType: 'application/json', ...opts });
97+
return this.get(filePath, 'rw').write(filePath, content, { contentType: 'application/json', ...opts });
9398
}
94-
return this.get(filePath).write(filePath, buffer, opts);
99+
return this.get(filePath, 'rw').write(filePath, buffer, opts);
95100
}
96101

97102
/**
@@ -100,7 +105,7 @@ export class FileSystemAbstraction implements FileSystem {
100105
* @returns list of files inside that path
101106
*/
102107
list(filePath: string, opts?: ListOptions): AsyncGenerator<string> {
103-
return this.get(filePath).list(filePath, opts);
108+
return this.get(filePath, 'r').list(filePath, opts);
104109
}
105110

106111
/**
@@ -111,7 +116,7 @@ export class FileSystemAbstraction implements FileSystem {
111116
* @returns list of files inside that path
112117
*/
113118
details(filePath: string, opts?: ListOptions): AsyncGenerator<FileInfo> {
114-
return this.get(filePath).details(filePath, opts);
119+
return this.get(filePath, 'r').details(filePath, opts);
115120
}
116121

117122
/**
@@ -121,7 +126,7 @@ export class FileSystemAbstraction implements FileSystem {
121126
* @returns true if file exists, false otherwise
122127
*/
123128
exists(filePath: string): Promise<boolean> {
124-
return this.get(filePath)
129+
return this.get(filePath, 'r')
125130
.head(filePath)
126131
.then((f) => f != null);
127132
}
@@ -133,7 +138,7 @@ export class FileSystemAbstraction implements FileSystem {
133138
* @returns basic information such as file size
134139
*/
135140
head(filePath: string): Promise<FileInfo | null> {
136-
return this.get(filePath).head(filePath);
141+
return this.get(filePath, 'r').head(filePath);
137142
}
138143

139144
join = joinUri;
@@ -145,7 +150,7 @@ export class FileSystemAbstraction implements FileSystem {
145150
* @returns
146151
*/
147152
source(filePath: string): ChunkSource {
148-
return this.get(filePath).source(filePath);
153+
return this.get(filePath, 'r').source(filePath);
149154
}
150155

151156
/**
@@ -160,28 +165,18 @@ export class FileSystemAbstraction implements FileSystem {
160165
}
161166

162167
/** Find the filesystem that would be used for a given path */
163-
get(filePath: string): FileSystem {
168+
get(filePath: string, flag: Flag): FileSystem {
164169
this.sortSystems();
165170
for (const cfg of this.systems) {
166-
if (filePath.startsWith(cfg.path)) return cfg.system;
171+
if (filePath.startsWith(cfg.path)) {
172+
// If we want to write to the system but only have read-only access
173+
if (flag === 'rw' && cfg.flag === 'r') continue;
174+
return cfg.system;
175+
}
167176
}
168177

169178
throw new Error(`Unable to find file system for path:${filePath}`);
170179
}
171180
}
172181

173182
export const fsa = new FileSystemAbstraction();
174-
175-
// async function main(): Promise<void> {
176-
// for await (const f of fsa.list('')) {
177-
// console.log(f);
178-
// }
179-
180-
// for await (const f of fsa.list('', { details: true })) {
181-
// console.log(f.path);
182-
// }
183-
184-
// for await (const f of fsa.list('', { details: false, recursive: false })) {
185-
// console.log(f.path);
186-
// }
187-
// }

0 commit comments

Comments
 (0)