@flystorage/file-storage 0.0.3 → 0.0.4
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/esm/file-storage.js +44 -21
- package/dist/types/file-storage.d.ts +9 -1
- package/package.json +6 -1
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);
|
package/dist/esm/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);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
/// <reference types="node" />
|
|
3
3
|
/// <reference types="node" />
|
|
4
|
+
/// <reference types="node" />
|
|
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,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flystorage/file-storage",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "File-storage abstraction: multiple filesystems, one API.",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"types": "./dist/types/index.d.ts",
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"watch": "tsc --watch"
|
|
25
25
|
},
|
|
26
26
|
"author": "Frank de Jonge (https://frankdejonge.nl)",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/flystorage/flystorage.git",
|
|
30
|
+
"directory": "packages/file-storage"
|
|
31
|
+
},
|
|
27
32
|
"keywords": ["fs", "file", "files", "filesystem", "filesystems", "storage"],
|
|
28
33
|
"license": "MIT"
|
|
29
34
|
}
|