@flystorage/file-storage 0.0.3 → 0.0.5
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.
- package/README.md +128 -0
- package/dist/cjs/file-storage.js +44 -21
- package/dist/cjs/package.json +1 -0
- package/dist/esm/checksum-from-stream.js +3 -7
- package/dist/esm/errors.js +20 -43
- package/dist/esm/file-storage.js +61 -50
- package/dist/esm/index.js +6 -22
- package/dist/esm/path-normalizer.js +5 -11
- package/dist/esm/path-prefixer.js +5 -9
- package/dist/esm/portable-visibility.js +2 -5
- package/dist/types/checksum-from-stream.d.ts +2 -2
- package/dist/types/file-storage.d.ts +12 -4
- package/package.json +8 -2
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<img src="https://avatars.githubusercontent.com/u/151840999" width="50px" height="50px" />
|
|
2
|
+
|
|
3
|
+
# Flystorage
|
|
4
|
+
Flystorage is a file storage abstraction for NodeJS and TypeScript. It is an 80/20 solution
|
|
5
|
+
that is built around a set of goals:
|
|
6
|
+
|
|
7
|
+
- Provide a straight-forward API that is easy to use.
|
|
8
|
+
- Allow application code to be unaware WHERE files are stored.
|
|
9
|
+
- Pragmatically smooth over underlying storage differences.
|
|
10
|
+
- Expose an async/await based API, promises all the way.
|
|
11
|
+
- Abstract over file permissions using "visibility".
|
|
12
|
+
- Actually tested using real integrations, _mocks are not welcome_.
|
|
13
|
+
- Stand on the shoulders of giants, use official vendor packages when possible.
|
|
14
|
+
|
|
15
|
+
### What is Flystorage NOT:
|
|
16
|
+
Flystorage is meant to be used in cases for generic file storage use-cases. It's not an API for
|
|
17
|
+
any specific filesystem. It's a generalised solution and will not implement feature only
|
|
18
|
+
specific to one particular storage implementation. There will be use-cases that are not catered
|
|
19
|
+
to, simply because they cannot be abstracted over in a reasonable manner.
|
|
20
|
+
|
|
21
|
+
## Capabilities
|
|
22
|
+
|
|
23
|
+
### Implemented
|
|
24
|
+
- [x] Write files using string | buffer | readable/stream
|
|
25
|
+
- [x] Read files as stream, string, or Uint8Array
|
|
26
|
+
- [x] Set permissions using abstracted visibility
|
|
27
|
+
- [x] List the contents of a directory/prefix, (shallow and deep).
|
|
28
|
+
- [x] Delete files without failing when they don't exist.
|
|
29
|
+
- [x] Delete directories (and any files it contains)
|
|
30
|
+
- [x] Generate public URLs.
|
|
31
|
+
- [x] Generate temporary (signed) URLs.
|
|
32
|
+
- [x] Expose or calculate checksums for files.
|
|
33
|
+
- [x] Mime-type resolving
|
|
34
|
+
- [x] Last modified fetching
|
|
35
|
+
- [x] File size
|
|
36
|
+
|
|
37
|
+
### Planned
|
|
38
|
+
- [ ] Moving files
|
|
39
|
+
- [ ] Copying files
|
|
40
|
+
|
|
41
|
+
## Implementations / Adapters
|
|
42
|
+
|
|
43
|
+
### Implemented
|
|
44
|
+
- [x] Local Filesystem
|
|
45
|
+
- [x] AWS S3 (using the V3 SDK)
|
|
46
|
+
|
|
47
|
+
### Planned
|
|
48
|
+
|
|
49
|
+
#### Prio 1
|
|
50
|
+
- [ ] Azure Blob Storage
|
|
51
|
+
- [ ] Test implementation (in-memory, with staged errors)
|
|
52
|
+
- [ ] Google Cloud Storage
|
|
53
|
+
|
|
54
|
+
### Prio 2
|
|
55
|
+
- [ ] FTP (using `basic-ftp`)
|
|
56
|
+
- [ ] SFTP (?)
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
Install the main package and any adapters you might need:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm i -S @flystorage/file-storage
|
|
63
|
+
|
|
64
|
+
# for using AWS S3
|
|
65
|
+
npm i -S @flystorage/aws-s3
|
|
66
|
+
|
|
67
|
+
# for using the local filesystem
|
|
68
|
+
npm i -S @flystorage/local-fs
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Local Usage
|
|
72
|
+
```typescript
|
|
73
|
+
import {resolve} from 'node:path';
|
|
74
|
+
import {createReadStream} from 'node:fs';
|
|
75
|
+
import {FileStorage, Visibility} from '@flystorage/file-storage';
|
|
76
|
+
import {LocalFileStorage} from '@flystorage/local-fs';
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* SETUP
|
|
80
|
+
**/
|
|
81
|
+
|
|
82
|
+
const rootDirectory = resolve(process.cwd(), 'my-files');
|
|
83
|
+
const storage = new FileStorage(new LocalFileStorage(rootDirectory));
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* USAGE
|
|
87
|
+
**/
|
|
88
|
+
|
|
89
|
+
// Write using a string
|
|
90
|
+
await storage.write('write-from-a-string.txt', 'file contents');
|
|
91
|
+
|
|
92
|
+
// Write using a stream
|
|
93
|
+
const stream = createReadStream(resolve(process.cwd(), 'test-files/picture.png'));
|
|
94
|
+
await storage.write('picture.png', stream);
|
|
95
|
+
|
|
96
|
+
// Write with visibility (permissions).
|
|
97
|
+
await storage.write('public.txt', 'debug', {
|
|
98
|
+
visibility: Visibility.PUBLIC, // mode: 0o644
|
|
99
|
+
});
|
|
100
|
+
await storage.write('private.txt', 'debug', {
|
|
101
|
+
visibility: Visibility.PRIVATE, // mode: 0o600
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// List directory contents
|
|
105
|
+
const contentsAsAsyncGenerator = storage.list('', {deep: true});
|
|
106
|
+
|
|
107
|
+
for await (const item of contentsAsAsyncGenerator) {
|
|
108
|
+
console.log(item.path);
|
|
109
|
+
|
|
110
|
+
if (item.isFile) {
|
|
111
|
+
// do something with the file
|
|
112
|
+
} else if (item.isDirectory) {
|
|
113
|
+
// do something with the directory
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Delete a file
|
|
118
|
+
await storage.deleteFile('some-file.txt');
|
|
119
|
+
|
|
120
|
+
// Delete a directory (with all contents)
|
|
121
|
+
await storage.deleteDirectory('some-directory');
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Author
|
|
125
|
+
Flystorage is built by the maintainer of [Flysystem](https://flysystem.thephpleague.com), a
|
|
126
|
+
filesystem abstraction for PHP. This brings along more than
|
|
127
|
+
a decade of smoothing over filesystem implementation differences
|
|
128
|
+
and weighing trade-offs to make a usable API.
|
package/dist/cjs/file-storage.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.readableToUint8Array = exports.readableToString = exports.closeReadable = exports.normalizeExpiryToMilliseconds = exports.normalizeExpiryToDate = exports.FileStorage = exports.isDirectory = exports.isFile = void 0;
|
|
3
|
+
exports.readableToUint8Array = exports.readableToString = exports.closeReadable = exports.normalizeExpiryToMilliseconds = exports.normalizeExpiryToDate = exports.FileStorage = exports.DirectoryListing = exports.isDirectory = exports.isFile = void 0;
|
|
4
4
|
const stream_1 = require("stream");
|
|
5
5
|
const checksum_from_stream_js_1 = require("./checksum-from-stream.js");
|
|
6
6
|
const errors = require("./errors.js");
|
|
@@ -14,6 +14,45 @@ function isDirectory(stat) {
|
|
|
14
14
|
return stat.isDirectory;
|
|
15
15
|
}
|
|
16
16
|
exports.isDirectory = isDirectory;
|
|
17
|
+
class DirectoryListing {
|
|
18
|
+
listing;
|
|
19
|
+
path;
|
|
20
|
+
deep;
|
|
21
|
+
constructor(listing, path, deep) {
|
|
22
|
+
this.listing = listing;
|
|
23
|
+
this.path = path;
|
|
24
|
+
this.deep = deep;
|
|
25
|
+
}
|
|
26
|
+
async toArray(sorted = true) {
|
|
27
|
+
const items = [];
|
|
28
|
+
for await (const item of this.listing) {
|
|
29
|
+
items.push(item);
|
|
30
|
+
}
|
|
31
|
+
return sorted ? items.sort((a, b) => naturalSorting.compare(a.path, b.path)) : items;
|
|
32
|
+
}
|
|
33
|
+
filter(filter) {
|
|
34
|
+
const listing = this.listing;
|
|
35
|
+
const filtered = (async function* () {
|
|
36
|
+
for await (const entry of listing) {
|
|
37
|
+
if (filter(entry)) {
|
|
38
|
+
yield entry;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
})();
|
|
42
|
+
return new DirectoryListing(filtered, this.path, this.deep);
|
|
43
|
+
}
|
|
44
|
+
async *[Symbol.asyncIterator]() {
|
|
45
|
+
try {
|
|
46
|
+
for await (const item of this.listing) {
|
|
47
|
+
yield item;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw errors.UnableToListDirectory.because(errors.errorToMessage(error), { cause: error, context: { path: this.path, deep: this.deep } });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.DirectoryListing = DirectoryListing;
|
|
17
56
|
function toReadable(contents) {
|
|
18
57
|
if (contents instanceof stream_1.Readable) {
|
|
19
58
|
return contents;
|
|
@@ -57,6 +96,9 @@ class FileStorage {
|
|
|
57
96
|
async readToUint8Array(path) {
|
|
58
97
|
return await readableToUint8Array(await this.read(path));
|
|
59
98
|
}
|
|
99
|
+
async readToBuffer(path) {
|
|
100
|
+
return Buffer.from(await this.readToUint8Array(path));
|
|
101
|
+
}
|
|
60
102
|
async deleteFile(path) {
|
|
61
103
|
try {
|
|
62
104
|
await this.adapter.deleteFile(this.pathNormalizer.normalizePath(path));
|
|
@@ -114,26 +156,7 @@ class FileStorage {
|
|
|
114
156
|
}
|
|
115
157
|
}
|
|
116
158
|
list(path, { deep = false } = {}) {
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
async toArray(sorted = true) {
|
|
120
|
-
const items = [];
|
|
121
|
-
for await (const item of listing) {
|
|
122
|
-
items.push(item);
|
|
123
|
-
}
|
|
124
|
-
return sorted ? items.sort((a, b) => naturalSorting.compare(a.path, b.path)) : items;
|
|
125
|
-
},
|
|
126
|
-
async *[Symbol.asyncIterator]() {
|
|
127
|
-
try {
|
|
128
|
-
for await (const item of listing) {
|
|
129
|
-
yield item;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
throw errors.UnableToListDirectory.because(errors.errorToMessage(error), { cause: error, context: { path, deep } });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
};
|
|
159
|
+
return new DirectoryListing(this.adapter.list(this.pathNormalizer.normalizePath(path), { deep }), path, deep);
|
|
137
160
|
}
|
|
138
161
|
async statFile(path) {
|
|
139
162
|
const stat = await this.stat(path);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type": "commonjs"}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.checksumFromStream = void 0;
|
|
4
|
-
const crypto_1 = require("crypto");
|
|
5
|
-
async function checksumFromStream(stream, options) {
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
export async function checksumFromStream(stream, options) {
|
|
6
3
|
return new Promise(async (resolve, reject) => {
|
|
7
|
-
const hash =
|
|
4
|
+
const hash = createHash(options.algo ?? 'md5');
|
|
8
5
|
stream.on('error', reject);
|
|
9
6
|
stream.pipe(hash, { end: false });
|
|
10
7
|
stream.on('end', () => {
|
|
@@ -13,4 +10,3 @@ async function checksumFromStream(stream, options) {
|
|
|
13
10
|
});
|
|
14
11
|
});
|
|
15
12
|
}
|
|
16
|
-
exports.checksumFromStream = checksumFromStream;
|
package/dist/esm/errors.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.UnableToListDirectory = exports.UnableToCheckDirectoryExistence = exports.UnableToCheckFileExistence = exports.UnableToDeleteFile = exports.UnableToDeleteDirectory = exports.UnableToCreateDirectory = exports.UnableToGetStat = exports.UnableToGetTemporaryUrl = exports.UnableToGetPublicUrl = exports.UnableToGetVisibility = exports.UnableToSetVisibility = exports.UnableToReadFile = exports.UnableToWriteFile = exports.UnableToGetFileSize = exports.UnableToGetLastModified = exports.UnableToGetMimeType = exports.UnableToGetChecksum = exports.ChecksumIsNotAvailable = exports.FlystorageError = exports.errorToMessage = void 0;
|
|
4
|
-
function errorToMessage(error) {
|
|
1
|
+
export function errorToMessage(error) {
|
|
5
2
|
return error instanceof Error ? error.message : String(error);
|
|
6
3
|
}
|
|
7
|
-
|
|
8
|
-
class FlystorageError extends Error {
|
|
4
|
+
export class FlystorageError extends Error {
|
|
9
5
|
context;
|
|
10
6
|
code = 'unknown_error';
|
|
11
7
|
constructor(message, context = {}, cause = undefined) {
|
|
@@ -15,12 +11,11 @@ class FlystorageError extends Error {
|
|
|
15
11
|
this.context = context;
|
|
16
12
|
}
|
|
17
13
|
}
|
|
18
|
-
exports.FlystorageError = FlystorageError;
|
|
19
14
|
/**
|
|
20
15
|
* Thrown when the checksum algo is not supported or not pre-computed. This error
|
|
21
16
|
* is thrown with the intention of falling back to computing it based on a file read.
|
|
22
17
|
*/
|
|
23
|
-
class ChecksumIsNotAvailable extends FlystorageError {
|
|
18
|
+
export class ChecksumIsNotAvailable extends FlystorageError {
|
|
24
19
|
algo;
|
|
25
20
|
code = 'flystorage.checksum_not_supported';
|
|
26
21
|
constructor(message, algo, context = {}, cause = undefined) {
|
|
@@ -29,90 +24,72 @@ class ChecksumIsNotAvailable extends FlystorageError {
|
|
|
29
24
|
}
|
|
30
25
|
static checksumNotSupported = (algo, { context = {}, cause = undefined } = {}) => new ChecksumIsNotAvailable(`Checksum algo "${algo}" is not supported`, algo, { ...context, algo }, cause);
|
|
31
26
|
}
|
|
32
|
-
|
|
33
|
-
class UnableToGetChecksum extends FlystorageError {
|
|
27
|
+
export class UnableToGetChecksum extends FlystorageError {
|
|
34
28
|
code = 'flystorage.unable_to_get_checksum';
|
|
35
29
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToGetChecksum(`Unable to write the file. Reason: ${reason}`, context, cause);
|
|
36
30
|
}
|
|
37
|
-
|
|
38
|
-
class UnableToGetMimeType extends FlystorageError {
|
|
31
|
+
export class UnableToGetMimeType extends FlystorageError {
|
|
39
32
|
code = 'flystorage.unable_to_get_mimetype';
|
|
40
33
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToGetMimeType(`Unable to get mime-type. Reason: ${reason}`, context, cause);
|
|
41
34
|
}
|
|
42
|
-
|
|
43
|
-
class UnableToGetLastModified extends FlystorageError {
|
|
35
|
+
export class UnableToGetLastModified extends FlystorageError {
|
|
44
36
|
code = 'flystorage.unable_to_get_last_modified';
|
|
45
37
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToGetLastModified(`Unable to get last modified. Reason: ${reason}`, context, cause);
|
|
46
38
|
}
|
|
47
|
-
|
|
48
|
-
class UnableToGetFileSize extends FlystorageError {
|
|
39
|
+
export class UnableToGetFileSize extends FlystorageError {
|
|
49
40
|
code = 'flystorage.unable_to_get_file_size';
|
|
50
41
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToGetFileSize(`Unable to get file size. Reason: ${reason}`, context, cause);
|
|
51
42
|
}
|
|
52
|
-
|
|
53
|
-
class UnableToWriteFile extends FlystorageError {
|
|
43
|
+
export class UnableToWriteFile extends FlystorageError {
|
|
54
44
|
code = 'flystorage.unable_to_write_file';
|
|
55
45
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToWriteFile(`Unable to write the file. Reason: ${reason}`, context, cause);
|
|
56
46
|
}
|
|
57
|
-
|
|
58
|
-
class UnableToReadFile extends FlystorageError {
|
|
47
|
+
export class UnableToReadFile extends FlystorageError {
|
|
59
48
|
code = 'flystorage.unable_to_read_file';
|
|
60
49
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToReadFile(`Unable to read the file. Reason: ${reason}`, context, cause);
|
|
61
50
|
}
|
|
62
|
-
|
|
63
|
-
class UnableToSetVisibility extends FlystorageError {
|
|
51
|
+
export class UnableToSetVisibility extends FlystorageError {
|
|
64
52
|
code = 'flystorage.unable_to_set_visibility';
|
|
65
53
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToSetVisibility(`Unable to set visibility. Reason: ${reason}`, context, cause);
|
|
66
54
|
}
|
|
67
|
-
|
|
68
|
-
class UnableToGetVisibility extends FlystorageError {
|
|
55
|
+
export class UnableToGetVisibility extends FlystorageError {
|
|
69
56
|
code = 'flystorage.unable_to_get_visibility';
|
|
70
57
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToGetVisibility(`Unable to get visibility. Reason: ${reason}`, context, cause);
|
|
71
58
|
}
|
|
72
|
-
|
|
73
|
-
class UnableToGetPublicUrl extends FlystorageError {
|
|
59
|
+
export class UnableToGetPublicUrl extends FlystorageError {
|
|
74
60
|
code = 'flystorage.unable_to_get_public_url';
|
|
75
61
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToGetPublicUrl(`Unable to get public URL. Reason: ${reason}`, context, cause);
|
|
76
62
|
}
|
|
77
|
-
|
|
78
|
-
class UnableToGetTemporaryUrl extends FlystorageError {
|
|
63
|
+
export class UnableToGetTemporaryUrl extends FlystorageError {
|
|
79
64
|
code = 'flystorage.unable_to_get_temporary_url';
|
|
80
65
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToGetTemporaryUrl(`Unable to get temporary URL. Reason: ${reason}`, context, cause);
|
|
81
66
|
}
|
|
82
|
-
|
|
83
|
-
class UnableToGetStat extends FlystorageError {
|
|
67
|
+
export class UnableToGetStat extends FlystorageError {
|
|
84
68
|
code = 'flystorage.unable_to_get_stat';
|
|
85
69
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToGetStat(`Unable to get stat. Reason: ${reason}`, context, cause);
|
|
86
70
|
static noFileStatResolved = ({ context = {}, cause = undefined }) => new UnableToGetStat(`Stat was not a file.`, context, cause);
|
|
87
71
|
}
|
|
88
|
-
|
|
89
|
-
class UnableToCreateDirectory extends FlystorageError {
|
|
72
|
+
export class UnableToCreateDirectory extends FlystorageError {
|
|
90
73
|
code = 'flystorage.unable_to_create_directory';
|
|
91
74
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToCreateDirectory(`Unable to create directory. Reason: ${reason}`, context, cause);
|
|
92
75
|
}
|
|
93
|
-
|
|
94
|
-
class UnableToDeleteDirectory extends FlystorageError {
|
|
76
|
+
export class UnableToDeleteDirectory extends FlystorageError {
|
|
95
77
|
code = 'flystorage.unable_to_delete_directory';
|
|
96
78
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToDeleteDirectory(`Unable to delete directory. Reason: ${reason}`, context, cause);
|
|
97
79
|
}
|
|
98
|
-
|
|
99
|
-
class UnableToDeleteFile extends FlystorageError {
|
|
80
|
+
export class UnableToDeleteFile extends FlystorageError {
|
|
100
81
|
code = 'flystorage.unable_to_delete_file';
|
|
101
82
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToDeleteFile(`Unable to delete file. Reason: ${reason}`, context, cause);
|
|
102
83
|
}
|
|
103
|
-
|
|
104
|
-
class UnableToCheckFileExistence extends FlystorageError {
|
|
84
|
+
export class UnableToCheckFileExistence extends FlystorageError {
|
|
105
85
|
code = 'flystorage.unable_to_check_file_existence';
|
|
106
86
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToCheckFileExistence(`Unable to check file existence. Reason: ${reason}`, context, cause);
|
|
107
87
|
}
|
|
108
|
-
|
|
109
|
-
class UnableToCheckDirectoryExistence extends FlystorageError {
|
|
88
|
+
export class UnableToCheckDirectoryExistence extends FlystorageError {
|
|
110
89
|
code = 'flystorage.unable_to_check_directory_existence';
|
|
111
90
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToCheckDirectoryExistence(`Unable to check directory existence. Reason: ${reason}`, context, cause);
|
|
112
91
|
}
|
|
113
|
-
|
|
114
|
-
class UnableToListDirectory extends FlystorageError {
|
|
92
|
+
export class UnableToListDirectory extends FlystorageError {
|
|
115
93
|
code = 'flystorage.unable_to_list_directory_contents';
|
|
116
94
|
static because = (reason, { context = {}, cause = undefined }) => new UnableToListDirectory(`Unable to list directory contents. Reason: ${reason}`, context, cause);
|
|
117
95
|
}
|
|
118
|
-
exports.UnableToListDirectory = UnableToListDirectory;
|
package/dist/esm/file-storage.js
CHANGED
|
@@ -1,34 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const errors_js_1 = require("./errors.js");
|
|
8
|
-
const path_normalizer_js_1 = require("./path-normalizer.js");
|
|
9
|
-
function isFile(stat) {
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
import { checksumFromStream } from './checksum-from-stream.js';
|
|
3
|
+
import * as errors from './errors.js';
|
|
4
|
+
import { ChecksumIsNotAvailable } from './errors.js';
|
|
5
|
+
import { PathNormalizerV1 } from './path-normalizer.js';
|
|
6
|
+
export function isFile(stat) {
|
|
10
7
|
return stat.isFile;
|
|
11
8
|
}
|
|
12
|
-
|
|
13
|
-
function isDirectory(stat) {
|
|
9
|
+
export function isDirectory(stat) {
|
|
14
10
|
return stat.isDirectory;
|
|
15
11
|
}
|
|
16
|
-
|
|
12
|
+
export class DirectoryListing {
|
|
13
|
+
listing;
|
|
14
|
+
path;
|
|
15
|
+
deep;
|
|
16
|
+
constructor(listing, path, deep) {
|
|
17
|
+
this.listing = listing;
|
|
18
|
+
this.path = path;
|
|
19
|
+
this.deep = deep;
|
|
20
|
+
}
|
|
21
|
+
async toArray(sorted = true) {
|
|
22
|
+
const items = [];
|
|
23
|
+
for await (const item of this.listing) {
|
|
24
|
+
items.push(item);
|
|
25
|
+
}
|
|
26
|
+
return sorted ? items.sort((a, b) => naturalSorting.compare(a.path, b.path)) : items;
|
|
27
|
+
}
|
|
28
|
+
filter(filter) {
|
|
29
|
+
const listing = this.listing;
|
|
30
|
+
const filtered = (async function* () {
|
|
31
|
+
for await (const entry of listing) {
|
|
32
|
+
if (filter(entry)) {
|
|
33
|
+
yield entry;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
return new DirectoryListing(filtered, this.path, this.deep);
|
|
38
|
+
}
|
|
39
|
+
async *[Symbol.asyncIterator]() {
|
|
40
|
+
try {
|
|
41
|
+
for await (const item of this.listing) {
|
|
42
|
+
yield item;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw errors.UnableToListDirectory.because(errors.errorToMessage(error), { cause: error, context: { path: this.path, deep: this.deep } });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
17
50
|
function toReadable(contents) {
|
|
18
|
-
if (contents instanceof
|
|
51
|
+
if (contents instanceof Readable) {
|
|
19
52
|
return contents;
|
|
20
53
|
}
|
|
21
|
-
return
|
|
54
|
+
return Readable.from(contents);
|
|
22
55
|
}
|
|
23
56
|
const naturalSorting = new Intl.Collator(undefined, {
|
|
24
57
|
numeric: true,
|
|
25
58
|
sensitivity: 'base'
|
|
26
59
|
});
|
|
27
|
-
class FileStorage {
|
|
60
|
+
export class FileStorage {
|
|
28
61
|
adapter;
|
|
29
62
|
pathNormalizer;
|
|
30
63
|
options;
|
|
31
|
-
constructor(adapter, pathNormalizer = new
|
|
64
|
+
constructor(adapter, pathNormalizer = new PathNormalizerV1(), options = {}) {
|
|
32
65
|
this.adapter = adapter;
|
|
33
66
|
this.pathNormalizer = pathNormalizer;
|
|
34
67
|
this.options = options;
|
|
@@ -45,7 +78,7 @@ class FileStorage {
|
|
|
45
78
|
}
|
|
46
79
|
async read(path) {
|
|
47
80
|
try {
|
|
48
|
-
return
|
|
81
|
+
return Readable.from(await this.adapter.read(this.pathNormalizer.normalizePath(path)));
|
|
49
82
|
}
|
|
50
83
|
catch (error) {
|
|
51
84
|
throw errors.UnableToReadFile.because(errors.errorToMessage(error), { cause: error, context: { path } });
|
|
@@ -57,6 +90,9 @@ class FileStorage {
|
|
|
57
90
|
async readToUint8Array(path) {
|
|
58
91
|
return await readableToUint8Array(await this.read(path));
|
|
59
92
|
}
|
|
93
|
+
async readToBuffer(path) {
|
|
94
|
+
return Buffer.from(await this.readToUint8Array(path));
|
|
95
|
+
}
|
|
60
96
|
async deleteFile(path) {
|
|
61
97
|
try {
|
|
62
98
|
await this.adapter.deleteFile(this.pathNormalizer.normalizePath(path));
|
|
@@ -114,26 +150,7 @@ class FileStorage {
|
|
|
114
150
|
}
|
|
115
151
|
}
|
|
116
152
|
list(path, { deep = false } = {}) {
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
async toArray(sorted = true) {
|
|
120
|
-
const items = [];
|
|
121
|
-
for await (const item of listing) {
|
|
122
|
-
items.push(item);
|
|
123
|
-
}
|
|
124
|
-
return sorted ? items.sort((a, b) => naturalSorting.compare(a.path, b.path)) : items;
|
|
125
|
-
},
|
|
126
|
-
async *[Symbol.asyncIterator]() {
|
|
127
|
-
try {
|
|
128
|
-
for await (const item of listing) {
|
|
129
|
-
yield item;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
throw errors.UnableToListDirectory.because(errors.errorToMessage(error), { cause: error, context: { path, deep } });
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
};
|
|
153
|
+
return new DirectoryListing(this.adapter.list(this.pathNormalizer.normalizePath(path), { deep }), path, deep);
|
|
137
154
|
}
|
|
138
155
|
async statFile(path) {
|
|
139
156
|
const stat = await this.stat(path);
|
|
@@ -171,7 +188,7 @@ class FileStorage {
|
|
|
171
188
|
return await this.adapter.checksum(this.pathNormalizer.normalizePath(path), options);
|
|
172
189
|
}
|
|
173
190
|
catch (error) {
|
|
174
|
-
if (error instanceof
|
|
191
|
+
if (error instanceof ChecksumIsNotAvailable) {
|
|
175
192
|
return this.calculateChecksum(path, options);
|
|
176
193
|
}
|
|
177
194
|
throw errors.UnableToGetChecksum.because(errors.errorToMessage(error), { cause: error, context: { path, options } });
|
|
@@ -203,23 +220,20 @@ class FileStorage {
|
|
|
203
220
|
}
|
|
204
221
|
async calculateChecksum(path, options) {
|
|
205
222
|
try {
|
|
206
|
-
return await
|
|
223
|
+
return await checksumFromStream(await this.read(path), options);
|
|
207
224
|
}
|
|
208
225
|
catch (error) {
|
|
209
226
|
throw errors.UnableToGetChecksum.because(errors.errorToMessage(error), { cause: error, context: { path, options } });
|
|
210
227
|
}
|
|
211
228
|
}
|
|
212
229
|
}
|
|
213
|
-
|
|
214
|
-
function normalizeExpiryToDate(expiresAt) {
|
|
230
|
+
export function normalizeExpiryToDate(expiresAt) {
|
|
215
231
|
return expiresAt instanceof Date ? expiresAt : new Date(expiresAt);
|
|
216
232
|
}
|
|
217
|
-
|
|
218
|
-
function normalizeExpiryToMilliseconds(expiresAt) {
|
|
233
|
+
export function normalizeExpiryToMilliseconds(expiresAt) {
|
|
219
234
|
return expiresAt instanceof Date ? expiresAt.getTime() : expiresAt;
|
|
220
235
|
}
|
|
221
|
-
|
|
222
|
-
async function closeReadable(body) {
|
|
236
|
+
export async function closeReadable(body) {
|
|
223
237
|
if (body.closed) {
|
|
224
238
|
return;
|
|
225
239
|
}
|
|
@@ -230,13 +244,11 @@ async function closeReadable(body) {
|
|
|
230
244
|
body.destroy();
|
|
231
245
|
});
|
|
232
246
|
}
|
|
233
|
-
exports.closeReadable = closeReadable;
|
|
234
247
|
const decoder = new TextDecoder();
|
|
235
|
-
async function readableToString(stream) {
|
|
248
|
+
export async function readableToString(stream) {
|
|
236
249
|
return decoder.decode(await readableToUint8Array(stream));
|
|
237
250
|
}
|
|
238
|
-
|
|
239
|
-
function readableToUint8Array(stream) {
|
|
251
|
+
export function readableToUint8Array(stream) {
|
|
240
252
|
return new Promise((resolve, reject) => {
|
|
241
253
|
const parts = [];
|
|
242
254
|
stream.on('data', (chunk) => {
|
|
@@ -246,7 +258,6 @@ function readableToUint8Array(stream) {
|
|
|
246
258
|
stream.on('end', () => resolve(concatUint8Arrays(parts)));
|
|
247
259
|
});
|
|
248
260
|
}
|
|
249
|
-
exports.readableToUint8Array = readableToUint8Array;
|
|
250
261
|
function concatUint8Arrays(input) {
|
|
251
262
|
const length = input.reduce((l, a) => l + a.byteLength, 0);
|
|
252
263
|
const output = new Uint8Array(length);
|
package/dist/esm/index.js
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
-
};
|
|
16
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
__exportStar(require("./checksum-from-stream.js"), exports);
|
|
18
|
-
__exportStar(require("./file-storage.js"), exports);
|
|
19
|
-
__exportStar(require("./errors.js"), exports);
|
|
20
|
-
__exportStar(require("./path-normalizer.js"), exports);
|
|
21
|
-
__exportStar(require("./path-prefixer.js"), exports);
|
|
22
|
-
__exportStar(require("./portable-visibility.js"), exports);
|
|
1
|
+
export * from './checksum-from-stream.js';
|
|
2
|
+
export * from './file-storage.js';
|
|
3
|
+
export * from './errors.js';
|
|
4
|
+
export * from './path-normalizer.js';
|
|
5
|
+
export * from './path-prefixer.js';
|
|
6
|
+
export * from './portable-visibility.js';
|
|
@@ -1,26 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PathTraversalDetected = exports.CorruptedPathDetected = exports.PathNormalizerV1 = void 0;
|
|
4
|
-
const node_path_1 = require("node:path");
|
|
1
|
+
import { join } from 'node:path';
|
|
5
2
|
const funkyWhiteSpaceRegex = new RegExp('\\p{C}+', 'u');
|
|
6
|
-
class PathNormalizerV1 {
|
|
3
|
+
export class PathNormalizerV1 {
|
|
7
4
|
normalizePath(path) {
|
|
8
5
|
if (funkyWhiteSpaceRegex.test(path)) {
|
|
9
6
|
throw CorruptedPathDetected.unexpectedWhitespace(path);
|
|
10
7
|
}
|
|
11
|
-
const normalized =
|
|
8
|
+
const normalized = join(...(path.split('/')));
|
|
12
9
|
if (normalized.indexOf('../') !== -1 || normalized == '..') {
|
|
13
10
|
throw PathTraversalDetected.forPath(path);
|
|
14
11
|
}
|
|
15
12
|
return normalized === '.' ? '' : normalized;
|
|
16
13
|
}
|
|
17
14
|
}
|
|
18
|
-
|
|
19
|
-
class CorruptedPathDetected extends Error {
|
|
15
|
+
export class CorruptedPathDetected extends Error {
|
|
20
16
|
static unexpectedWhitespace = (path) => new CorruptedPathDetected(`Corrupted path detected with unexpected whitespace: ${path}`);
|
|
21
17
|
}
|
|
22
|
-
|
|
23
|
-
class PathTraversalDetected extends Error {
|
|
18
|
+
export class PathTraversalDetected extends Error {
|
|
24
19
|
static forPath = (path) => new PathTraversalDetected(`Path traversal detected for: ${path}`);
|
|
25
20
|
}
|
|
26
|
-
exports.PathTraversalDetected = PathTraversalDetected;
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
exports.PathPrefixer = void 0;
|
|
4
|
-
const node_path_1 = require("node:path");
|
|
5
|
-
class PathPrefixer {
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
export class PathPrefixer {
|
|
6
3
|
prefix = '';
|
|
7
4
|
constructor(prefix = '') {
|
|
8
5
|
if (prefix.length > 0) {
|
|
9
|
-
this.prefix =
|
|
6
|
+
this.prefix = join(prefix, '/');
|
|
10
7
|
}
|
|
11
8
|
}
|
|
12
9
|
prefixFilePath(path) {
|
|
13
|
-
return this.prefix.length > 0 ?
|
|
10
|
+
return this.prefix.length > 0 ? join(this.prefix, path) : path;
|
|
14
11
|
}
|
|
15
12
|
prefixDirectoryPath(path) {
|
|
16
|
-
return this.prefix.length > 0 ?
|
|
13
|
+
return this.prefix.length > 0 ? join(this.prefix, path, '/') : join(path, '/');
|
|
17
14
|
}
|
|
18
15
|
stripFilePath(path) {
|
|
19
16
|
return path.substring(this.prefix.length);
|
|
@@ -22,4 +19,3 @@ class PathPrefixer {
|
|
|
22
19
|
return this.stripFilePath(path).replace(/\/+$/g, '');
|
|
23
20
|
}
|
|
24
21
|
}
|
|
25
|
-
exports.PathPrefixer = PathPrefixer;
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Visibility = void 0;
|
|
4
|
-
var Visibility;
|
|
1
|
+
export var Visibility;
|
|
5
2
|
(function (Visibility) {
|
|
6
3
|
Visibility["PUBLIC"] = "public";
|
|
7
4
|
Visibility["PRIVATE"] = "private";
|
|
8
|
-
})(Visibility || (
|
|
5
|
+
})(Visibility || (Visibility = {}));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
3
3
|
import { BinaryToTextEncoding } from 'crypto';
|
|
4
4
|
import { Readable } from 'node:stream';
|
|
5
5
|
export declare function checksumFromStream(stream: Readable, options: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
/// <reference types="node" />
|
|
3
|
-
/// <reference types="node" />
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
3
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
4
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
4
5
|
import { BinaryToTextEncoding } from 'crypto';
|
|
5
6
|
import { Readable } from 'stream';
|
|
6
7
|
import { PathNormalizer } from './path-normalizer.js';
|
|
@@ -45,8 +46,14 @@ export interface StorageAdapter {
|
|
|
45
46
|
lastModified(path: string): Promise<number>;
|
|
46
47
|
fileSize(path: string): Promise<number>;
|
|
47
48
|
}
|
|
48
|
-
export
|
|
49
|
+
export declare class DirectoryListing implements AsyncIterable<StatEntry> {
|
|
50
|
+
private readonly listing;
|
|
51
|
+
private readonly path;
|
|
52
|
+
private readonly deep;
|
|
53
|
+
constructor(listing: AsyncGenerator<StatEntry>, path: string, deep: boolean);
|
|
49
54
|
toArray(sorted?: boolean): Promise<StatEntry[]>;
|
|
55
|
+
filter(filter: (entry: StatEntry) => boolean): DirectoryListing;
|
|
56
|
+
[Symbol.asyncIterator](): AsyncGenerator<StatEntry, void, unknown>;
|
|
50
57
|
}
|
|
51
58
|
export type FileContents = Iterable<any> | AsyncIterable<any> | NodeJS.ReadableStream | Readable;
|
|
52
59
|
export type MiscellaneousOptions = {
|
|
@@ -86,6 +93,7 @@ export declare class FileStorage {
|
|
|
86
93
|
read(path: string): Promise<Readable>;
|
|
87
94
|
readToString(path: string): Promise<string>;
|
|
88
95
|
readToUint8Array(path: string): Promise<Uint8Array>;
|
|
96
|
+
readToBuffer(path: string): Promise<Buffer>;
|
|
89
97
|
deleteFile(path: string): Promise<void>;
|
|
90
98
|
createDirectory(path: string, options?: CreateDirectoryOptions): Promise<void>;
|
|
91
99
|
deleteDirectory(path: string): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flystorage/file-storage",
|
|
3
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.5",
|
|
4
5
|
"description": "File-storage abstraction: multiple filesystems, one API.",
|
|
5
6
|
"main": "./dist/cjs/index.js",
|
|
6
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -17,13 +18,18 @@
|
|
|
17
18
|
}
|
|
18
19
|
},
|
|
19
20
|
"scripts": {
|
|
20
|
-
"compile": "concurrently npm:compile:*",
|
|
21
|
+
"compile": "concurrently npm:compile:* && echo '{\"type\": \"commonjs\"}' > ./dist/cjs/package.json",
|
|
21
22
|
"compile:esm": "tsc --outDir ./dist/esm/ --declaration false",
|
|
22
23
|
"compile:cjs": "tsc --outDir ./dist/cjs/ --declaration false --module commonjs --moduleResolution node",
|
|
23
24
|
"compile:types": "tsc --outDir ./dist/types/ --declaration --emitDeclarationOnly",
|
|
24
25
|
"watch": "tsc --watch"
|
|
25
26
|
},
|
|
26
27
|
"author": "Frank de Jonge (https://frankdejonge.nl)",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/flystorage/flystorage.git",
|
|
31
|
+
"directory": "packages/file-storage"
|
|
32
|
+
},
|
|
27
33
|
"keywords": ["fs", "file", "files", "filesystem", "filesystems", "storage"],
|
|
28
34
|
"license": "MIT"
|
|
29
35
|
}
|