Skip to content

Commit a143637

Browse files
authored
feat: add file system abstraction layer (#34)
BREAKING CHANGE: this renames `@chunkd/source-url` to `@chunkd/source-http`
1 parent 7afb91a commit a143637

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1325
-284
lines changed

README.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
[![Build Status](https://github.com/blacha/chunkd/workflows/Main/badge.svg)](https://github.com/blacha/chunkd/actions)
44

55

6-
File abstraction to read chunks of files from various sources
6+
File system abstraction to read files from various sources
77

88
## Usage
99

10-
Load a chunks from a URL using `fetch`
10+
Load a chunks of data from a URL using `fetch`
1111

1212
```typescript
13-
const source = new SourceUrl('https://example.com/foo')
13+
import {SourceHttp} from '@chunkd/source-http'
14+
const source = new SourceHttp('https://example.com/foo')
1415
// Read 1KB chunks
1516
source.chunkSize = 1024;
1617

@@ -28,6 +29,13 @@ bytes.getBigUint64(1024);
2829
```
2930

3031

32+
```typescript
33+
import {fsa} from '@chunkd/fs'
34+
35+
const source = fsa.source('https://example.com/foo.zip');
36+
source.chunkSize = 1024;
37+
```
38+
3139
# Building
3240

3341
This requires [NodeJs](https://nodejs.org/en/) > 12 & [Yarn](https://yarnpkg.com/en/)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
"devDependencies": {
1919
"@linzjs/style": "^3.1.1",
2020
"@types/ospec": "^4.0.2",
21+
"@types/sinon": "^10.0.2",
2122
"conventional-github-releaser": "^3.1.5",
2223
"lerna": "^4.0.0",
2324
"ospec": "^4.1.1",
2425
"rimraf": "^3.0.0",
26+
"sinon": "^11.1.2",
2527
"source-map-support": "^0.5.19"
2628
},
2729
"workspaces": {

packages/core/src/composite.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
export const enum ErrorCodes {
2+
PermissionDenied = 403,
3+
NotFound = 404,
4+
InternalError = 500,
5+
}
6+
/**
7+
* Utility error to wrap other errors to make them more understandable
8+
*/
9+
export class CompositeError extends Error {
10+
name = 'CompositeError';
11+
code: ErrorCodes;
12+
reason: unknown;
13+
14+
constructor(msg: string, code: ErrorCodes, reason: unknown) {
15+
super(msg);
16+
this.code = code;
17+
this.reason = reason;
18+
}
19+
20+
static isCompositeError(e: unknown): e is CompositeError {
21+
if (typeof e !== 'object' || e == null) return false;
22+
return (e as CompositeError).name === 'CompositeError';
23+
}
24+
}

packages/core/src/fs.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Readable } from 'node:stream';
2+
import { ChunkSource } from '.';
3+
4+
export interface FileInfo {
5+
/** file path */
6+
path: string;
7+
/**
8+
* Size of file in bytes
9+
* undefined if no size found
10+
*/
11+
size?: number;
12+
}
13+
14+
export interface FileSystem<T extends ChunkSource = ChunkSource> {
15+
/**
16+
* Protocol used for communication
17+
* @example
18+
* file
19+
* s3
20+
* http
21+
*/
22+
protocol: string;
23+
/** Read a file into a buffer */
24+
read(filePath: string): Promise<Buffer>;
25+
/** Create a read stream */
26+
stream(filePath: string): Readable;
27+
/** Write a file from either a buffer or stream */
28+
write(filePath: string, buffer: Buffer | Readable | string): Promise<void>;
29+
/** Recursively list all files in path */
30+
list(filePath: string): AsyncGenerator<string>;
31+
/** Recursively list all files in path with additional details */
32+
details(filePath: string): AsyncGenerator<FileInfo>;
33+
/** Does the path exists */
34+
exists(filePath: string): Promise<boolean>;
35+
/** Get information about the path */
36+
head(filePath: string): Promise<FileInfo | null>;
37+
/** Create a file source to read chunks out of */
38+
source(filePath: string): T | null;
39+
}

packages/core/src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@ export { ChunkSourceBase } from './chunk.source.js';
33
export { LogType } from './log.js';
44
export { SourceMemory } from './chunk.source.memory.js';
55
export { ChunkSource } from './source.js';
6+
export { ErrorCodes, CompositeError } from './composite.js';
7+
export { FileSystem, FileInfo } from './fs.js';
8+
9+
export function isRecord(value: unknown): value is Record<string, unknown> {
10+
return typeof value === 'object' && value !== null;
11+
}

packages/fs/CHANGELOG.md

Whitespace-only changes.

packages/fs/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# @chunkd/fs
2+
3+
Utility functions for working with files that could either reside on the local file system or other sources like AWS S3
4+
5+
## Usage
6+
7+
```typescript
8+
import { fsa } from '@chunkd/fs';
9+
10+
for await (const file of fsa.list('s3://foo/bar')) {
11+
// ['s3://foo/bar/baz.html', 's3://foo/bar/index.html']
12+
}
13+
14+
for await (const file of fsa.list('/home/blacha')) {
15+
// '/home/blacha/index.html'
16+
}
17+
18+
// Convert the generator to an array
19+
const files = await fsa.toArray(fsa.list('s3://foo/bar'));
20+
```
21+
22+
This is designed for use with multiple s3 credentials
23+
24+
```typescript
25+
import {fsa, FsAwsS3} from '@chunkd/fs'
26+
27+
const bucketA = new S3({ credentials: bucketACredentials })
28+
const bucketB = new S3({ credentials: bucketBCredentials })
29+
30+
fsa.register('s3://bucket-a', new FsAwsS3(bucketA))
31+
fsa.register('s3://bucket-b', new FsAwsS3(bucketB))
32+
33+
// Stream a file from bucketA to bucketB
34+
await fsa.write('s3://bucket-b/foo', fsa.stream('s3://bucket-a/foo'))
35+
```
36+
37+
Or even any s3 compatible api
38+
39+
```typescript
40+
import {fsa, FsAwsS3} from '@chunkd/fs'
41+
42+
const bucketA = new S3({ endpoint: 'http://10.0.0.1:8080' })
43+
const bucketB = new S3({ endpoint: 'http://10.0.0.99:8080' })
44+
45+
fsa.register('s3://bucket-a', new FsAwsS3(bucketA))
46+
fsa.register('s3://bucket-b', new FsAwsS3(bucketB))
47+
48+
// Stream a file from bucketA (10.0.0.1) to bucketB (10.0.0.99)
49+
await fsa.write('s3://bucket-b/foo', fsa.stream('s3://bucket-a/foo'))
50+
```

packages/fs/package.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@chunkd/fs",
3+
"version": "6.0.0",
4+
"type": "module",
5+
"engines": {
6+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
7+
},
8+
"repository": {
9+
"type": "git",
10+
"url": "https://github.com/blacha/chunkd.git",
11+
"directory": "packages/fs"
12+
},
13+
"author": "Blayne Chard",
14+
"main": "./build/index.node.js",
15+
"browser": "./build/index.js",
16+
"types": "./build/index.d.ts",
17+
"license": "MIT",
18+
"scripts": {},
19+
"dependencies": {
20+
"@chunkd/core": "^6.0.0",
21+
"@chunkd/source-file": "^6.0.0",
22+
"@chunkd/source-aws": "^6.0.0"
23+
},
24+
"publishConfig": {
25+
"access": "public"
26+
},
27+
"devDependencies": {
28+
"@types/node": "^14.14.31"
29+
}
30+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { FsFile } from '@chunkd/source-file';
2+
import o from 'ospec';
3+
import { FileSystemAbstraction } from '../fs.abstraction.js';
4+
5+
export class FakeSystem extends FsFile {
6+
constructor(protocol = 'fake') {
7+
super();
8+
this.protocol = protocol;
9+
}
10+
}
11+
12+
o.spec('FileSystemAbstraction', () => {
13+
o('should find file systems', () => {
14+
const fsa = new FileSystemAbstraction();
15+
16+
o(fsa.get('/foo').protocol).equals('file');
17+
o(fsa.get('/').protocol).equals('file');
18+
o(fsa.get('./').protocol).equals('file');
19+
});
20+
21+
o('should register new file systems', () => {
22+
const fsa = new FileSystemAbstraction();
23+
24+
const fakeLocal = new FakeSystem('fake');
25+
fsa.register('fake://', fakeLocal);
26+
27+
o(fsa.get('/').protocol).equals('file');
28+
o(fsa.get('//').protocol).equals('file');
29+
30+
o(fsa.get('fake://foo').protocol).equals('fake');
31+
o(fsa.get('fake:/foo').protocol).equals('file');
32+
o(fsa.get('fake//foo').protocol).equals('file');
33+
o(fsa.get('fake').protocol).equals('file');
34+
});
35+
36+
o('should find file systems in order they were registered', () => {
37+
const fakeA = new FakeSystem('fake');
38+
const fakeB = new FakeSystem('fakeSpecific');
39+
const fsa = new FileSystemAbstraction();
40+
41+
fsa.register('fake://', fakeA);
42+
fsa.register('fake://some-prefix-string/', fakeB);
43+
44+
o(fsa.get('fake://foo').protocol).equals('fake');
45+
o(fsa.get('fake://some-prefix-string/').protocol).equals('fakeSpecific');
46+
o(fsa.get('fake://some-prefix-string/some-key').protocol).equals('fakeSpecific');
47+
});
48+
49+
o('should order file systems by length', () => {
50+
const fakeA = new FakeSystem('fake');
51+
const fakeB = new FakeSystem('fakeSpecific');
52+
const fsa = new FileSystemAbstraction();
53+
54+
fsa.register('fake://some-prefix-string/', fakeB);
55+
fsa.register('fake://', fakeA);
56+
57+
o(fsa.get('fake://foo').protocol).equals('fake');
58+
o(fsa.get('fake://some-prefix-string/').protocol).equals('fakeSpecific');
59+
o(fsa.get('fake://some-prefix-string/some-key').protocol).equals('fakeSpecific');
60+
});
61+
});

0 commit comments

Comments
 (0)