@spinajs/fs-s3 2.0.179 → 2.0.181

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/lib/mjs/index.js CHANGED
@@ -1,339 +1,353 @@
1
- var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
- else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
- return c > 3 && r && Object.defineProperty(target, key, r), r;
6
- };
7
- var __metadata = (this && this.__metadata) || function (k, v) {
8
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
- };
10
- import { Injectable, PerInstanceCheck } from '@spinajs/di';
11
- import { Log, Logger } from '@spinajs/log-common';
12
- import { fs, FileSystem } from '@spinajs/fs';
13
- import { S3Client, HeadObjectCommand, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
14
- import { Upload } from '@aws-sdk/lib-storage';
15
- import { Config } from '@spinajs/configuration';
16
- import archiver from 'archiver';
17
- import { basename } from 'path';
18
- import { InvalidArgument, IOFail, MethodNotImplemented } from '@spinajs/exceptions';
19
- import { createReadStream, existsSync, readFileSync, ReadStream } from 'fs';
20
- import { DateTime } from 'luxon';
21
- import { Readable } from 'stream';
22
- /**
23
- * Abstract layer for file operations.
24
- * Basic implementation is just wrapper for native node fs functions
25
- *
26
- * It allows to wrap other libs eg. aws s3, ftp
27
- * and inject it into code without changing logic that use them.
28
- *
29
- * TODO: map errors to some kind of common errors shared with other implementations
30
- */
31
- let fsS3 = class fsS3 extends fs {
32
- /**
33
- * Name of provider. We can have multiple providers of the same type but with different options.
34
- * Also used in InjectService decorator for mapping
35
- */
36
- get Name() {
37
- return this.Options.name;
38
- }
39
- constructor(Options) {
40
- super();
41
- this.Options = Options;
42
- }
43
- async resolve() {
44
- this.S3 = new S3Client(this.AwsConfig);
45
- }
46
- /**
47
- *
48
- * Tries to download file to local filesystem, then returns local filesystem path.
49
- * Native implementation simply returns local path and does nothing.
50
- *
51
- * @param path - file to download
52
- */
53
- async download(path) {
54
- const tmpName = this.TempFs.tmppath();
55
- const wStream = await this.TempFs.writeStream(tmpName);
56
- const command = new GetObjectCommand({
57
- Bucket: this.Options.bucket,
58
- Key: path,
59
- });
60
- const result = await this.S3.send(command);
61
- return new Promise((resolve, reject) => {
62
- if (result.Body instanceof Readable) {
63
- result.Body
64
- .pipe(wStream)
65
- .on("error", (err) => reject(err))
66
- .on("close", () => resolve(tmpName));
67
- }
68
- else {
69
- reject(new IOFail(`Cannot download file ${path}, empty response`));
70
- }
71
- });
72
- }
73
- /**
74
- * read all content of file
75
- */
76
- async read(path, encoding) {
77
- const fLocal = await this.download(path);
78
- const content = await this.TempFs.read(fLocal, encoding);
79
- await this.TempFs.unlink(fLocal);
80
- return content;
81
- }
82
- async readStream(path, encoding) {
83
- const fLocal = await this.download(path);
84
- return this.TempFs.readStream(fLocal, encoding);
85
- }
86
- /**
87
- * Write to file string or buffer
88
- */
89
- async write(path, data, encoding) {
90
- const upload = new Upload({
91
- client: this.S3,
92
- params: {
93
- Bucket: this.Options.bucket,
94
- Key: path,
95
- Body: data,
96
- ContentEncoding: encoding
97
- },
98
- });
99
- await upload.done();
100
- }
101
- async append(path, data, encoding) {
102
- /**
103
- * We cannot append to file in s3 directly,
104
- * we have to download file first, append locally, then upload again new file
105
- */
106
- const fLocal = await this.download(path);
107
- await this.TempFs.append(fLocal, data, encoding);
108
- const wStream = await this.writeStream(path, encoding);
109
- const rStream = await this.TempFs.readStream(fLocal, encoding);
110
- return new Promise((resolve, reject) => {
111
- rStream
112
- .pipe(wStream)
113
- .on('end', () => {
114
- this.TempFs.rm(fLocal)
115
- .then(() => {
116
- return resolve();
117
- })
118
- .catch(() => {
119
- resolve();
120
- });
121
- })
122
- .on('error', (err) => {
123
- // eslint-disable-next-line promise/no-promise-in-callback
124
- this.TempFs.rm(fLocal)
125
- .then(() => {
126
- return reject(err);
127
- })
128
- .catch(() => {
129
- reject(err);
130
- });
131
- });
132
- });
133
- }
134
- async upload(srcPath, destPath) {
135
- if (!existsSync(srcPath)) {
136
- throw new IOFail(`file ${srcPath} does not exists`);
137
- }
138
- const dPath = this.resolvePath(destPath ?? basename(srcPath));
139
- const rStream = createReadStream(srcPath);
140
- await this.writeStream(dPath, rStream);
141
- }
142
- async writeStream(path, rStream, encoding) {
143
- if (!(rStream instanceof ReadStream)) {
144
- throw new InvalidArgument(`rStream should be readable stream`);
145
- }
146
- const result = new Upload({
147
- client: this.S3,
148
- params: {
149
- Bucket: this.Options.bucket,
150
- Key: path,
151
- Body: rStream,
152
- ContentEncoding: encoding,
153
- },
154
- });
155
- await result.done();
156
- }
157
- /**
158
- * Checks if file existst
159
- * @param path - path to check
160
- */
161
- async exists(path) {
162
- try {
163
- const command = new HeadObjectCommand({
164
- Bucket: this.Options.bucket,
165
- Key: path,
166
- });
167
- await this.S3.send(command);
168
- }
169
- catch (err) {
170
- if (err.name === 'NotFound')
171
- return false;
172
- }
173
- return true;
174
- }
175
- async dirExists() {
176
- // s3 does not have concept of folders
177
- // we assume that all exists
178
- return Promise.resolve(true);
179
- }
180
- /**
181
- * Copy file to another location
182
- * @param path - src path
183
- * @param dest - dest path
184
- */
185
- async copy(path, dest) {
186
- const command = new CopyObjectCommand({
187
- Bucket: this.Options.bucket,
188
- CopySource: this.Options.bucket + '/' + path,
189
- Key: dest,
190
- });
191
- await this.S3.send(command);
192
- }
193
- /**
194
- * Copy file to another location and deletes src file
195
- */
196
- async move(oldPath, newPath) {
197
- await this.copy(oldPath, newPath);
198
- await this.unlink(oldPath);
199
- }
200
- /**
201
- * Change name of a file
202
- */
203
- async rename(oldPath, newPath) {
204
- return this.move(oldPath, newPath);
205
- }
206
- /**
207
- * Deletes file permanently
208
- *
209
- * @param path - path to file that will be deleted
210
- * @param onlyTemp - remote filesystems need to download file before, if so, calling unlink with this flag removes only local temp file after we finished processing
211
- */
212
- async unlink(path, onlyTemp) {
213
- if (onlyTemp) {
214
- await this.TempFs.unlink(path);
215
- return;
216
- }
217
- const command = new DeleteObjectCommand({
218
- Key: path,
219
- Bucket: this.Options.bucket,
220
- });
221
- await this.S3.send(command);
222
- }
223
- /**
224
- *
225
- * Deletes dir recursively & all contents inside
226
- *
227
- * @param path - dir to remove
228
- */
229
- async rm(_path) {
230
- const command = new DeleteObjectCommand({
231
- Bucket: this.Options.bucket,
232
- Key: _path
233
- });
234
- await this.S3.send(command);
235
- }
236
- /**
237
- *
238
- * Creates directory, recursively
239
- *
240
- */
241
- async mkdir() {
242
- // s3 dont need to create folders
243
- }
244
- /**
245
- * Returns file statistics, not all fields may be accesible
246
- */
247
- async stat(path) {
248
- const command = new HeadObjectCommand({
249
- Bucket: this.Options.bucket,
250
- Key: path
251
- });
252
- const result = await this.S3.send(command);
253
- return {
254
- // no directories in s3
255
- IsDirectory: false,
256
- // only files can be stored in s3
257
- IsFile: true,
258
- // no creation time
259
- CreationTime: DateTime.min(),
260
- ModifiedTime: DateTime.fromJSDate(result.LastModified),
261
- // no access time in s3s
262
- AccessTime: DateTime.min(),
263
- Size: result.ContentLength,
264
- };
265
- }
266
- // protected async getSignedUrl(path: string) {
267
- // return this.S3.getSignedUrlPromise('getObject', {
268
- // Bucket: this.Options.bucket,
269
- // Key: path,
270
- // Expires: 24 * 60 * 60,
271
- // });
272
- // }
273
- tmppath() {
274
- throw new MethodNotImplemented('fs s3 does not support temporary paths');
275
- }
276
- /**
277
- * List content of directory
278
- *
279
- * @param path - path to directory
280
- */
281
- async list(path) {
282
- const command = new ListObjectsV2Command({
283
- Bucket: this.Options.bucket,
284
- Delimiter: '/',
285
- Prefix: path,
286
- });
287
- const result = await this.S3.send(command);
288
- return result.Contents.map((x) => x.Key);
289
- }
290
- async unzip(_path, _destPath) {
291
- throw new Error('not implemented');
292
- }
293
- async zip(path, zName) {
294
- const zTmpName = this.TempFs.tmppath();
295
- const output = await this.TempFs.writeStream(zTmpName);
296
- const archive = archiver('zip', {
297
- zlib: { level: 9 }, // Sets the compression level.
298
- });
299
- // pipe archive data to the file
300
- archive.pipe(output);
301
- const tFile = await this.download(path);
302
- archive.file(tFile, { name: zName ?? basename(path) });
303
- await archive.finalize();
304
- return {
305
- asFilePath: () => {
306
- return zTmpName;
307
- },
308
- asStream: (encoding) => {
309
- return createReadStream(`${zTmpName}`, encoding);
310
- },
311
- asBase64: () => {
312
- return readFileSync(`${zTmpName}`, 'base64');
313
- },
314
- };
315
- }
316
- // eslint-disable-next-line @typescript-eslint/no-empty-function
317
- resolvePath(_path) {
318
- throw new MethodNotImplemented('fs s3 does not support path resolving');
319
- }
320
- };
321
- __decorate([
322
- Logger('fs'),
323
- __metadata("design:type", Log)
324
- ], fsS3.prototype, "Logger", void 0);
325
- __decorate([
326
- Config('fs.s3.config'),
327
- __metadata("design:type", Object)
328
- ], fsS3.prototype, "AwsConfig", void 0);
329
- __decorate([
330
- FileSystem('fs-temp'),
331
- __metadata("design:type", fs)
332
- ], fsS3.prototype, "TempFs", void 0);
333
- fsS3 = __decorate([
334
- Injectable('fs'),
335
- PerInstanceCheck(),
336
- __metadata("design:paramtypes", [Object])
337
- ], fsS3);
338
- export { fsS3 };
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Autoinject, Injectable, PerInstanceCheck } from '@spinajs/di';
11
+ import { Log, Logger } from '@spinajs/log-common';
12
+ import { fs, FileSystem, FileInfoService } from '@spinajs/fs';
13
+ import { S3Client, HeadObjectCommand, CopyObjectCommand, DeleteObjectCommand, ListObjectsV2Command, GetObjectCommand, } from '@aws-sdk/client-s3';
14
+ import { Upload } from '@aws-sdk/lib-storage';
15
+ import { Config } from '@spinajs/configuration';
16
+ import path, { basename } from 'path';
17
+ import { IOFail, MethodNotImplemented } from '@spinajs/exceptions';
18
+ import { createReadStream, existsSync } from 'fs';
19
+ import { DateTime } from 'luxon';
20
+ import { Readable } from 'stream';
21
+ import iconv from 'iconv-lite';
22
+ /**
23
+ * Abstract layer for file operations.
24
+ * Basic implementation is just wrapper for native node fs functions
25
+ *
26
+ * It allows to wrap other libs eg. aws s3, ftp
27
+ * and inject it into code without changing logic that use them.
28
+ *
29
+ * TODO: map errors to some kind of common errors shared with other implementations
30
+ */
31
+ let fsS3 = class fsS3 extends fs {
32
+ /**
33
+ * Name of provider. We can have multiple providers of the same type but with different options.
34
+ * Also used in InjectService decorator for mapping
35
+ */
36
+ get Name() {
37
+ return this.Options.name;
38
+ }
39
+ constructor(Options) {
40
+ super();
41
+ this.Options = Options;
42
+ }
43
+ async resolve() {
44
+ this.S3 = new S3Client(Object.assign({}, this.AwsConfig, {
45
+ endpoint: this.AwsConfig.endpoint ?? undefined,
46
+ logger: {
47
+ trace: (msg) => this.Logger.trace(msg),
48
+ debug: (msg) => this.Logger.debug(msg),
49
+ info: (msg) => this.Logger.info(msg),
50
+ warn: (msg) => this.Logger.warn(msg),
51
+ error: (msg) => this.Logger.error(msg),
52
+ },
53
+ }));
54
+ }
55
+ /**
56
+ *
57
+ * Tries to download file to local filesystem, then returns local filesystem path.
58
+ * Native implementation simply returns local path and does nothing.
59
+ *
60
+ * @param path - file to download
61
+ */
62
+ async download(path) {
63
+ const tmpName = this.TempFs.tmppath();
64
+ const wStream = await this.TempFs.writeStream(tmpName);
65
+ const command = new GetObjectCommand({
66
+ Bucket: this.Options.bucket,
67
+ Key: path,
68
+ });
69
+ const result = await this.S3.send(command);
70
+ return new Promise((resolve, reject) => {
71
+ if (result.Body instanceof Readable) {
72
+ result.Body.pipe(wStream)
73
+ .on('error', (err) => reject(err))
74
+ .on('close', () => resolve(tmpName));
75
+ }
76
+ else {
77
+ reject(new IOFail(`Cannot download file ${path}, empty response`));
78
+ }
79
+ });
80
+ }
81
+ /**
82
+ * read all content of file
83
+ */
84
+ async read(path, encoding) {
85
+ const fLocal = await this.download(path);
86
+ const content = await this.TempFs.read(fLocal, encoding);
87
+ await this.TempFs.rm(fLocal);
88
+ return content;
89
+ }
90
+ /**
91
+ *
92
+ * @param path
93
+ * @param _encoding
94
+ * @returns
95
+ */
96
+ async readStream(path, encoding) {
97
+ const command = new GetObjectCommand({
98
+ Bucket: this.Options.bucket,
99
+ Key: path,
100
+ });
101
+ const result = await this.S3.send(command);
102
+ const rStream = result.Body;
103
+ if (encoding) {
104
+ const encodedStream = rStream.pipe(iconv.decodeStream(encoding));
105
+ return encodedStream;
106
+ }
107
+ return rStream;
108
+ }
109
+ /**
110
+ * Write to file string or buffer
111
+ */
112
+ async write(path, data, encoding) {
113
+ const upload = new Upload({
114
+ client: this.S3,
115
+ params: {
116
+ Bucket: this.Options.bucket,
117
+ Key: path,
118
+ Body: data,
119
+ ContentEncoding: encoding,
120
+ },
121
+ });
122
+ await upload.done();
123
+ }
124
+ /**
125
+ * NOTE: append on s3 downloads file, appends to it, then uploads it again
126
+ * so it can be slow on large files
127
+ *
128
+ * @param path
129
+ * @param data
130
+ * @param encoding
131
+ */
132
+ async append(path, data, encoding) {
133
+ /**
134
+ * We cannot append to file in s3 directly,
135
+ * we have to download file first, append locally, then upload again new file
136
+ */
137
+ const fLocal = await this.download(path);
138
+ await this.TempFs.append(fLocal, data, encoding);
139
+ await this.upload(fLocal, path);
140
+ }
141
+ async upload(srcPath, destPath) {
142
+ if (!existsSync(srcPath)) {
143
+ throw new IOFail(`file ${srcPath} does not exists`);
144
+ }
145
+ const dPath = destPath ?? basename(srcPath);
146
+ const rStream = createReadStream(srcPath);
147
+ const hash = await this.hash(srcPath, 'md5');
148
+ const fInfo = await this.FileInfo.getInfo(this.resolvePath(srcPath));
149
+ const upload = new Upload({
150
+ client: this.S3,
151
+ params: {
152
+ Bucket: this.Options.bucket,
153
+ Key: dPath,
154
+ Body: rStream,
155
+ // content md5 header is always base64 encoded
156
+ ContentMD5: Buffer.from(hash, 'hex').toString('base64'),
157
+ // convert all metadata values to string, and back to object with key-value pair of strings
158
+ Metadata: Object.fromEntries(Object.entries(fInfo).map(([key, value]) => [key, String(value)])),
159
+ },
160
+ });
161
+ await upload.done();
162
+ }
163
+ /**
164
+ *
165
+ * Gets metadata of file in s3 bucket
166
+ *
167
+ * @param path path to file
168
+ * @returns
169
+ */
170
+ async getMetadata(path) {
171
+ const command = new HeadObjectCommand({
172
+ Bucket: this.Options.bucket,
173
+ Key: path,
174
+ });
175
+ const result = await this.S3.send(command);
176
+ return result.Metadata;
177
+ }
178
+ /**
179
+ *
180
+ * Returns writable stream for given path
181
+ *
182
+ * @param path file path ( relative to base path of provider)
183
+ * @param rStream readable stream, must be provided beforehand
184
+ * @param encoding optional stream encoding
185
+ */
186
+ async writeStream(_path, _encoding) {
187
+ throw new IOFail('Method not implemented, s3 does not support writable streams');
188
+ }
189
+ /**
190
+ * Checks if file existst
191
+ * @param path - path to check
192
+ */
193
+ async exists(path) {
194
+ try {
195
+ const command = new HeadObjectCommand({
196
+ Bucket: this.Options.bucket,
197
+ Key: path,
198
+ });
199
+ await this.S3.send(command);
200
+ }
201
+ catch (err) {
202
+ if (err.name === 'NotFound')
203
+ return false;
204
+ throw err;
205
+ }
206
+ return true;
207
+ }
208
+ async dirExists() {
209
+ throw new IOFail('Method not implemented, s3 does not support directories');
210
+ }
211
+ /**
212
+ * Copy file to another location
213
+ * @param path - src path
214
+ * @param dest - dest path
215
+ */
216
+ async copy(path, dest, dstFs) {
217
+ // if dest fs is set
218
+ // copy using it
219
+ if (dstFs) {
220
+ const file = await this.download(path);
221
+ await dstFs.upload(file, dest);
222
+ }
223
+ else {
224
+ // we copy file in s3 by copying it to another location
225
+ const command = new CopyObjectCommand({
226
+ Bucket: this.Options.bucket,
227
+ CopySource: this.Options.bucket + '/' + path,
228
+ Key: dest,
229
+ });
230
+ await this.S3.send(command);
231
+ }
232
+ }
233
+ /**
234
+ * Copy file to another location and deletes src file
235
+ */
236
+ async move(oldPath, newPath, dstFs) {
237
+ await this.copy(oldPath, newPath, dstFs);
238
+ await this.rm(oldPath);
239
+ }
240
+ /**
241
+ * Change name of a file
242
+ */
243
+ async rename(oldPath, newPath) {
244
+ return this.move(oldPath, newPath);
245
+ }
246
+ /**
247
+ *
248
+ * Deletes dir recursively & all contents inside
249
+ *
250
+ * @param path - dir to remove
251
+ */
252
+ async rm(_path) {
253
+ const command = new DeleteObjectCommand({
254
+ Bucket: this.Options.bucket,
255
+ Key: _path,
256
+ });
257
+ await this.S3.send(command);
258
+ }
259
+ /**
260
+ *
261
+ * Creates directory, recursively
262
+ *
263
+ */
264
+ async mkdir() {
265
+ throw new IOFail('Method not implemented, s3 does not support directories');
266
+ }
267
+ async isDir(_path) {
268
+ throw new IOFail('Method not implemented, s3 does not support directories');
269
+ }
270
+ /**
271
+ * Returns file statistics, not all fields may be accesible
272
+ */
273
+ async stat(path) {
274
+ const command = new HeadObjectCommand({
275
+ Bucket: this.Options.bucket,
276
+ Key: path,
277
+ });
278
+ const result = await this.S3.send(command);
279
+ return {
280
+ // no directories in s3
281
+ IsDirectory: false,
282
+ // only files can be stored in s3
283
+ IsFile: true,
284
+ // no creation time
285
+ CreationTime: DateTime.min(),
286
+ ModifiedTime: DateTime.fromJSDate(result.LastModified),
287
+ // no access time in s3s
288
+ AccessTime: DateTime.min(),
289
+ Size: result.ContentLength,
290
+ };
291
+ }
292
+ // protected async getSignedUrl(path: string) {
293
+ // return this.S3.getSignedUrlPromise('getObject', {
294
+ // Bucket: this.Options.bucket,
295
+ // Key: path,
296
+ // Expires: 24 * 60 * 60,
297
+ // });
298
+ // }
299
+ tmppath() {
300
+ throw new MethodNotImplemented('fs s3 does not support temporary paths');
301
+ }
302
+ /**
303
+ * List content of directory
304
+ *
305
+ * @param path - path to directory
306
+ */
307
+ async list(path) {
308
+ const command = new ListObjectsV2Command({
309
+ Bucket: this.Options.bucket,
310
+ Delimiter: '/',
311
+ Prefix: path,
312
+ });
313
+ const result = await this.S3.send(command);
314
+ return result.Contents.map((x) => x.Key);
315
+ }
316
+ async unzip(_path, _destPath) {
317
+ throw new IOFail('Method not implemented, you should download zipped file first, then unzip it');
318
+ }
319
+ async zip(_path, _dstFs, _dstFile) {
320
+ throw new IOFail('Method not implemented, you should zip files locally, then upload it');
321
+ }
322
+ resolvePath(_path) {
323
+ // we checek if path is absolute
324
+ // for hash function
325
+ if (path.isAbsolute(_path)) {
326
+ return _path;
327
+ }
328
+ throw new MethodNotImplemented('fs s3 does not support path resolving');
329
+ }
330
+ };
331
+ __decorate([
332
+ Logger('fs'),
333
+ __metadata("design:type", Log)
334
+ ], fsS3.prototype, "Logger", void 0);
335
+ __decorate([
336
+ Config('fs.s3.config'),
337
+ __metadata("design:type", Object)
338
+ ], fsS3.prototype, "AwsConfig", void 0);
339
+ __decorate([
340
+ FileSystem('fs-temp-s3'),
341
+ __metadata("design:type", fs)
342
+ ], fsS3.prototype, "TempFs", void 0);
343
+ __decorate([
344
+ Autoinject(),
345
+ __metadata("design:type", FileInfoService)
346
+ ], fsS3.prototype, "FileInfo", void 0);
347
+ fsS3 = __decorate([
348
+ Injectable('fs'),
349
+ PerInstanceCheck(),
350
+ __metadata("design:paramtypes", [Object])
351
+ ], fsS3);
352
+ export { fsS3 };
339
353
  //# sourceMappingURL=index.js.map