@engine9-io/input-tools 1.9.10 → 2.0.0

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/file/S3.js CHANGED
@@ -1,340 +1,308 @@
1
- const debug = require('debug')('@engine9-io/input/S3');
2
- const fs = require('node:fs');
3
- const { mimeType: mime } = require('mime-type/with-db');
4
- const {
5
- S3Client,
6
- CopyObjectCommand,
7
- DeleteObjectCommand,
8
- GetObjectCommand,
9
- HeadObjectCommand,
10
- GetObjectAttributesCommand,
11
- PutObjectCommand,
12
- ListObjectsV2Command
13
- } = require('@aws-sdk/client-s3');
14
- const { getTempFilename, relativeDate } = require('./tools');
15
-
1
+ import debug$0 from "debug";
2
+ import fs from "node:fs";
3
+ import withDb from "mime-type/with-db";
4
+ import clientS3 from "@aws-sdk/client-s3";
5
+ import { getTempFilename, relativeDate } from "./tools.js";
6
+ const debug = debug$0('@engine9-io/input/S3');
7
+ const { mimeType: mime } = withDb;
8
+ const { S3Client, CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, GetObjectAttributesCommand, PutObjectCommand, ListObjectsV2Command } = clientS3;
16
9
  function Worker() {
17
- this.prefix = 's3';
10
+ this.prefix = 's3';
18
11
  }
19
-
20
12
  function getParts(filename) {
21
- if (!filename) throw new Error(`Invalid filename: ${filename}`);
22
- if (!filename.startsWith('r2://') && !filename.startsWith('s3://')) {
23
- throw new Error(`Invalid filename, must start with r2:// or s3://: ${filename}`);
24
- }
25
- const parts = filename.split('/');
26
- const Bucket = parts[2];
27
- const Key = parts.slice(3).join('/');
28
- return { Bucket, Key };
13
+ if (!filename)
14
+ throw new Error(`Invalid filename: ${filename}`);
15
+ if (!filename.startsWith('r2://') && !filename.startsWith('s3://')) {
16
+ throw new Error(`Invalid filename, must start with r2:// or s3://: ${filename}`);
17
+ }
18
+ const parts = filename.split('/');
19
+ const Bucket = parts[2];
20
+ const Key = parts.slice(3).join('/');
21
+ return { Bucket, Key };
29
22
  }
30
23
  Worker.prototype.getClient = function () {
31
- if (!this.client) this.client = new S3Client({});
32
- return this.client;
24
+ if (!this.client)
25
+ this.client = new S3Client({});
26
+ return this.client;
33
27
  };
34
-
35
28
  Worker.prototype.getMetadata = async function ({ filename }) {
36
- const s3Client = this.getClient();
37
- const { Bucket, Key } = getParts(filename);
38
-
39
- const resp = await s3Client.send(
40
- new GetObjectAttributesCommand({
41
- Bucket,
42
- Key,
43
- ObjectAttributes: ['ETag', 'Checksum', 'ObjectParts', 'StorageClass', 'ObjectSize']
44
- })
45
- );
46
-
47
- return resp;
29
+ const s3Client = this.getClient();
30
+ const { Bucket, Key } = getParts(filename);
31
+ const resp = await s3Client.send(new GetObjectAttributesCommand({
32
+ Bucket,
33
+ Key,
34
+ ObjectAttributes: ['ETag', 'Checksum', 'ObjectParts', 'StorageClass', 'ObjectSize']
35
+ }));
36
+ return resp;
48
37
  };
49
38
  Worker.prototype.getMetadata.metadata = {
50
- options: {
51
- filename: {}
52
- }
39
+ options: {
40
+ filename: {}
41
+ }
53
42
  };
54
-
55
43
  Worker.prototype.stream = async function ({ filename }) {
56
- const s3Client = this.getClient();
57
- const { Bucket, Key } = getParts(filename);
58
- const command = new GetObjectCommand({ Bucket, Key });
59
- try {
60
- debug(`Streaming file ${Key}`);
61
- const response = await s3Client.send(command);
62
- return { stream: response.Body };
63
- } catch (e) {
64
- debug(`Could not stream filename:${filename}`);
65
- throw e;
66
- }
44
+ const s3Client = this.getClient();
45
+ const { Bucket, Key } = getParts(filename);
46
+ const command = new GetObjectCommand({ Bucket, Key });
47
+ try {
48
+ debug(`Streaming file ${Key}`);
49
+ const response = await s3Client.send(command);
50
+ return { stream: response.Body };
51
+ }
52
+ catch (e) {
53
+ debug(`Could not stream filename:${filename}`);
54
+ throw e;
55
+ }
67
56
  };
68
57
  Worker.prototype.stream.metadata = {
69
- options: {
70
- filename: {}
71
- }
58
+ options: {
59
+ filename: {}
60
+ }
72
61
  };
73
-
74
62
  Worker.prototype.copy = async function ({ filename, target }) {
75
- if (filename.startsWith('s3://') || filename.startsWith('r2://')) {
76
- //we're fine
77
- } else {
78
- throw new Error('Cowardly not copying a file not from s3 -- use put instead');
79
- }
80
- const s3Client = this.getClient();
81
- const { Bucket, Key } = getParts(target);
82
-
83
- debug(`Copying ${filename} to ${JSON.stringify({ Bucket, Key })}}`);
84
-
85
- const command = new CopyObjectCommand({
86
- CopySource: filename.slice(4), // remove the s3:/
87
- Bucket,
88
- Key
89
- });
90
-
91
- return s3Client.send(command);
63
+ if (filename.startsWith('s3://') || filename.startsWith('r2://')) {
64
+ //we're fine
65
+ }
66
+ else {
67
+ throw new Error('Cowardly not copying a file not from s3 -- use put instead');
68
+ }
69
+ const s3Client = this.getClient();
70
+ const { Bucket, Key } = getParts(target);
71
+ debug(`Copying ${filename} to ${JSON.stringify({ Bucket, Key })}}`);
72
+ const command = new CopyObjectCommand({
73
+ CopySource: filename.slice(4), // remove the s3:/
74
+ Bucket,
75
+ Key
76
+ });
77
+ return s3Client.send(command);
92
78
  };
93
-
94
79
  Worker.prototype.copy.metadata = {
95
- options: {
96
- filename: {},
97
- target: {}
98
- }
80
+ options: {
81
+ filename: {},
82
+ target: {}
83
+ }
99
84
  };
100
85
  Worker.prototype.move = async function ({ filename, target }) {
101
- await this.copy({ filename, target });
102
- await this.remove({ filename });
103
- return { filename: target };
86
+ await this.copy({ filename, target });
87
+ await this.remove({ filename });
88
+ return { filename: target };
104
89
  };
105
90
  Worker.prototype.move.metadata = {
106
- options: {
107
- filename: {},
108
- target: {}
109
- }
91
+ options: {
92
+ filename: {},
93
+ target: {}
94
+ }
110
95
  };
111
-
112
96
  Worker.prototype.remove = async function ({ filename }) {
113
- const s3Client = this.getClient();
114
- const { Bucket, Key } = getParts(filename);
115
- const command = new DeleteObjectCommand({ Bucket, Key });
116
- return s3Client.send(command);
97
+ const s3Client = this.getClient();
98
+ const { Bucket, Key } = getParts(filename);
99
+ const command = new DeleteObjectCommand({ Bucket, Key });
100
+ return s3Client.send(command);
117
101
  };
118
102
  Worker.prototype.remove.metadata = {
119
- options: {
120
- filename: {}
121
- }
103
+ options: {
104
+ filename: {}
105
+ }
122
106
  };
123
-
124
107
  Worker.prototype.download = async function ({ filename }) {
125
- const file = filename.split('/').pop();
126
- const localPath = await getTempFilename({ targetFilename: file });
127
- const s3Client = this.getClient();
128
- const { Bucket, Key } = getParts(filename);
129
- const command = new GetObjectCommand({ Bucket, Key });
130
- debug(`Downloading ${file} to ${localPath}`);
131
- const response = await s3Client.send(command);
132
- const fileStream = fs.createWriteStream(localPath);
133
-
134
- response.Body.pipe(fileStream);
135
-
136
- return new Promise((resolve, reject) => {
137
- fileStream.on('finish', async () => {
138
- const { size } = await fs.promises.stat(localPath);
139
- resolve({ size, filename: localPath });
108
+ const file = filename.split('/').pop();
109
+ const localPath = await getTempFilename({ targetFilename: file });
110
+ const s3Client = this.getClient();
111
+ const { Bucket, Key } = getParts(filename);
112
+ const command = new GetObjectCommand({ Bucket, Key });
113
+ debug(`Downloading ${file} to ${localPath}`);
114
+ const response = await s3Client.send(command);
115
+ const fileStream = fs.createWriteStream(localPath);
116
+ response.Body.pipe(fileStream);
117
+ return new Promise((resolve, reject) => {
118
+ fileStream.on('finish', async () => {
119
+ const { size } = await fs.promises.stat(localPath);
120
+ resolve({ size, filename: localPath });
121
+ });
122
+ fileStream.on('error', reject);
140
123
  });
141
- fileStream.on('error', reject);
142
- });
143
124
  };
144
125
  Worker.prototype.download.metadata = {
145
- options: {
146
- filename: {}
147
- }
126
+ options: {
127
+ filename: {}
128
+ }
148
129
  };
149
-
150
130
  Worker.prototype.put = async function (options) {
151
- const { filename, directory } = options;
152
- if (!filename) throw new Error('Local filename required');
153
- if (directory?.indexOf('s3://') !== 0 && directory?.indexOf('r2://') !== 0)
154
- throw new Error(`directory path must start with s3:// or r2://, is ${directory}`);
155
-
156
- const file = options.file || filename.split('/').pop();
157
- const parts = directory.split('/');
158
- const Bucket = parts[2];
159
- const Key = parts.slice(3).filter(Boolean).concat(file).join('/');
160
- const Body = fs.createReadStream(filename);
161
-
162
- const ContentType = mime.lookup(file);
163
-
164
- debug(`Putting ${filename} to ${JSON.stringify({ Bucket, Key, ContentType })}}`);
165
- const s3Client = this.getClient();
166
-
167
- const command = new PutObjectCommand({
168
- Bucket,
169
- Key,
170
- Body,
171
- ContentType
172
- });
173
-
174
- return s3Client.send(command);
131
+ const { filename, directory } = options;
132
+ if (!filename)
133
+ throw new Error('Local filename required');
134
+ if (directory?.indexOf('s3://') !== 0 && directory?.indexOf('r2://') !== 0)
135
+ throw new Error(`directory path must start with s3:// or r2://, is ${directory}`);
136
+ const file = options.file || filename.split('/').pop();
137
+ const parts = directory.split('/');
138
+ const Bucket = parts[2];
139
+ const Key = parts.slice(3).filter(Boolean).concat(file).join('/');
140
+ const Body = fs.createReadStream(filename);
141
+ const ContentType = mime.lookup(file);
142
+ debug(`Putting ${filename} to ${JSON.stringify({ Bucket, Key, ContentType })}}`);
143
+ const s3Client = this.getClient();
144
+ const command = new PutObjectCommand({
145
+ Bucket,
146
+ Key,
147
+ Body,
148
+ ContentType
149
+ });
150
+ return s3Client.send(command);
175
151
  };
176
152
  Worker.prototype.put.metadata = {
177
- options: {
178
- filename: {},
179
- directory: { description: 'Directory to put file, e.g. s3://foo-bar/dir/xyz' },
180
- file: { description: 'Name of file, defaults to the filename' }
181
- }
153
+ options: {
154
+ filename: {},
155
+ directory: { description: 'Directory to put file, e.g. s3://foo-bar/dir/xyz' },
156
+ file: { description: 'Name of file, defaults to the filename' }
157
+ }
182
158
  };
183
-
184
159
  Worker.prototype.write = async function (options) {
185
- const { directory, file, content } = options;
186
-
187
- if (!directory?.indexOf('s3://') === 0) throw new Error('directory must start with s3://');
188
- const parts = directory.split('/');
189
-
190
- const Bucket = parts[2];
191
- const Key = parts.slice(3).filter(Boolean).concat(file).join('/');
192
- const Body = content;
193
-
194
- debug(`Writing content of length ${content.length} to ${JSON.stringify({ Bucket, Key })}}`);
195
- const s3Client = this.getClient();
196
- const ContentType = mime.lookup(file);
197
-
198
- const command = new PutObjectCommand({
199
- Bucket,
200
- Key,
201
- Body,
202
- ContentType
203
- });
204
-
205
- return s3Client.send(command);
160
+ const { directory, file, content } = options;
161
+ if (!directory?.indexOf('s3://') === 0)
162
+ throw new Error('directory must start with s3://');
163
+ const parts = directory.split('/');
164
+ const Bucket = parts[2];
165
+ const Key = parts.slice(3).filter(Boolean).concat(file).join('/');
166
+ const Body = content;
167
+ debug(`Writing content of length ${content.length} to ${JSON.stringify({ Bucket, Key })}}`);
168
+ const s3Client = this.getClient();
169
+ const ContentType = mime.lookup(file);
170
+ const command = new PutObjectCommand({
171
+ Bucket,
172
+ Key,
173
+ Body,
174
+ ContentType
175
+ });
176
+ return s3Client.send(command);
206
177
  };
207
178
  Worker.prototype.write.metadata = {
208
- options: {
209
- directory: { description: 'Directory to put file, e.g. s3://foo-bar/dir/xyz' },
210
- file: { description: 'Name of file, defaults to the filename' },
211
- content: { description: 'Contents of file' }
212
- }
179
+ options: {
180
+ directory: { description: 'Directory to put file, e.g. s3://foo-bar/dir/xyz' },
181
+ file: { description: 'Name of file, defaults to the filename' },
182
+ content: { description: 'Contents of file' }
183
+ }
213
184
  };
214
-
215
185
  Worker.prototype.list = async function ({ directory, start, end, raw }) {
216
- if (!directory) throw new Error('directory is required');
217
- let dir = directory;
218
- while (dir.slice(-1) === '/') dir = dir.slice(0, -1);
219
- const { Bucket, Key: Prefix } = getParts(dir);
220
- const s3Client = this.getClient();
221
- const command = new ListObjectsV2Command({
222
- Bucket,
223
- Prefix: `${Prefix}/`,
224
- Delimiter: '/'
225
- });
226
-
227
- const { Contents: files, CommonPrefixes } = await s3Client.send(command);
228
- if (raw) return files;
229
- // debug('Prefixes:', { CommonPrefixes });
230
- const output = []
231
- .concat(
232
- (CommonPrefixes || []).map((f) => ({
186
+ if (!directory)
187
+ throw new Error('directory is required');
188
+ let dir = directory;
189
+ while (dir.slice(-1) === '/')
190
+ dir = dir.slice(0, -1);
191
+ const { Bucket, Key: Prefix } = getParts(dir);
192
+ const s3Client = this.getClient();
193
+ const command = new ListObjectsV2Command({
194
+ Bucket,
195
+ Prefix: `${Prefix}/`,
196
+ Delimiter: '/'
197
+ });
198
+ const { Contents: files, CommonPrefixes } = await s3Client.send(command);
199
+ if (raw)
200
+ return files;
201
+ // debug('Prefixes:', { CommonPrefixes });
202
+ const output = []
203
+ .concat((CommonPrefixes || []).map((f) => ({
233
204
  name: f.Prefix.slice(Prefix.length + 1, -1),
234
205
  type: 'directory'
235
- }))
236
- )
237
- .concat(
238
- (files || [])
206
+ })))
207
+ .concat((files || [])
239
208
  .filter(({ LastModified }) => {
240
- if (start && new Date(LastModified) < start) {
209
+ if (start && new Date(LastModified) < start) {
241
210
  return false;
242
- } else if (end && new Date(LastModified) > end) {
211
+ }
212
+ else if (end && new Date(LastModified) > end) {
243
213
  return false;
244
- } else {
214
+ }
215
+ else {
245
216
  return true;
246
- }
247
- })
217
+ }
218
+ })
248
219
  .map(({ Key, Size, LastModified }) => ({
249
- name: Key.slice(Prefix.length + 1),
250
- type: 'file',
251
- size: Size,
252
- modifiedAt: new Date(LastModified).toISOString()
253
- }))
254
- );
255
-
256
- return output;
220
+ name: Key.slice(Prefix.length + 1),
221
+ type: 'file',
222
+ size: Size,
223
+ modifiedAt: new Date(LastModified).toISOString()
224
+ })));
225
+ return output;
257
226
  };
258
227
  Worker.prototype.list.metadata = {
259
- options: {
260
- directory: { required: true }
261
- }
228
+ options: {
229
+ directory: { required: true }
230
+ }
262
231
  };
263
232
  /* List everything with the prefix */
264
233
  Worker.prototype.listAll = async function (options) {
265
- const { directory } = options;
266
- if (!directory) throw new Error('directory is required');
267
- let dir = directory;
268
- const start = options.start && relativeDate(options.start);
269
- const end = options.end && relativeDate(options.end);
270
- while (dir.slice(-1) === '/') dir = dir.slice(0, -1);
271
- const { Bucket, Key } = getParts(dir);
272
- const s3Client = this.getClient();
273
- const files = [];
274
- let ContinuationToken = null;
275
- let Prefix = null;
276
- if (Key) Prefix = `${Key}/`;
277
- do {
278
- const command = new ListObjectsV2Command({
279
- Bucket,
280
- Prefix,
281
- ContinuationToken
282
- // Delimiter: '/',
283
- });
284
- debug(`Sending List command with prefix ${Prefix} with ContinuationToken ${ContinuationToken}`);
285
-
286
- const result = await s3Client.send(command);
287
- const newFiles =
288
- result.Contents?.filter(({ LastModified }) => {
289
- if (start && new Date(LastModified) < start) {
290
- return false;
291
- } else if (end && new Date(LastModified) > end) {
292
- return false;
293
- } else {
294
- return true;
295
- }
296
- })?.map((d) => `${this.prefix}://${Bucket}/${d.Key}`) || [];
297
- debug(`Retrieved ${newFiles.length} new files, total ${files.length},sample ${newFiles.slice(0, 3).join(',')}`);
298
- files.push(...newFiles);
299
- ContinuationToken = result.NextContinuationToken;
300
- } while (ContinuationToken);
301
- return files;
234
+ const { directory } = options;
235
+ if (!directory)
236
+ throw new Error('directory is required');
237
+ let dir = directory;
238
+ const start = options.start && relativeDate(options.start);
239
+ const end = options.end && relativeDate(options.end);
240
+ while (dir.slice(-1) === '/')
241
+ dir = dir.slice(0, -1);
242
+ const { Bucket, Key } = getParts(dir);
243
+ const s3Client = this.getClient();
244
+ const files = [];
245
+ let ContinuationToken = null;
246
+ let Prefix = null;
247
+ if (Key)
248
+ Prefix = `${Key}/`;
249
+ do {
250
+ const command = new ListObjectsV2Command({
251
+ Bucket,
252
+ Prefix,
253
+ ContinuationToken
254
+ // Delimiter: '/',
255
+ });
256
+ debug(`Sending List command with prefix ${Prefix} with ContinuationToken ${ContinuationToken}`);
257
+ const result = await s3Client.send(command);
258
+ const newFiles = result.Contents?.filter(({ LastModified }) => {
259
+ if (start && new Date(LastModified) < start) {
260
+ return false;
261
+ }
262
+ else if (end && new Date(LastModified) > end) {
263
+ return false;
264
+ }
265
+ else {
266
+ return true;
267
+ }
268
+ })?.map((d) => `${this.prefix}://${Bucket}/${d.Key}`) || [];
269
+ debug(`Retrieved ${newFiles.length} new files, total ${files.length},sample ${newFiles.slice(0, 3).join(',')}`);
270
+ files.push(...newFiles);
271
+ ContinuationToken = result.NextContinuationToken;
272
+ } while (ContinuationToken);
273
+ return files;
302
274
  };
303
275
  Worker.prototype.listAll.metadata = {
304
- options: {
305
- directory: { required: true }
306
- }
276
+ options: {
277
+ directory: { required: true }
278
+ }
307
279
  };
308
-
309
280
  Worker.prototype.moveAll = async function ({ directory, targetDirectory }) {
310
- if (!directory || !targetDirectory) throw new Error('directory and targetDirectory required');
311
- const files = await this.listAll({ directory });
312
- const configs = files.map((d) => ({
313
- filename: d,
314
- target: d.replace(directory, targetDirectory)
315
- }));
316
-
317
- const pLimit = await import('p-limit');
318
- const limitedMethod = pLimit.default(10);
319
-
320
- return Promise.all(configs.map(({ filename, target }) => limitedMethod(async () => this.move({ filename, target }))));
281
+ if (!directory || !targetDirectory)
282
+ throw new Error('directory and targetDirectory required');
283
+ const files = await this.listAll({ directory });
284
+ const configs = files.map((d) => ({
285
+ filename: d,
286
+ target: d.replace(directory, targetDirectory)
287
+ }));
288
+ const pLimit = await import('p-limit');
289
+ const limitedMethod = pLimit.default(10);
290
+ return Promise.all(configs.map(({ filename, target }) => limitedMethod(async () => this.move({ filename, target }))));
321
291
  };
322
292
  Worker.prototype.moveAll.metadata = {
323
- options: {
324
- directory: { required: true },
325
- targetDirectory: { required: true }
326
- }
293
+ options: {
294
+ directory: { required: true },
295
+ targetDirectory: { required: true }
296
+ }
327
297
  };
328
-
329
298
  Worker.prototype.stat = async function ({ filename }) {
330
- if (!filename) throw new Error('filename is required');
331
-
332
- const s3Client = this.getClient();
333
- const { Bucket, Key } = getParts(filename);
334
- const command = new HeadObjectCommand({ Bucket, Key });
335
- const response = await s3Client.send(command);
336
-
337
- const {
299
+ if (!filename)
300
+ throw new Error('filename is required');
301
+ const s3Client = this.getClient();
302
+ const { Bucket, Key } = getParts(filename);
303
+ const command = new HeadObjectCommand({ Bucket, Key });
304
+ const response = await s3Client.send(command);
305
+ const {
338
306
  // "AcceptRanges": "bytes",
339
307
  ContentLength, // : "3191",
340
308
  ContentType, // : "image/jpeg",
@@ -342,22 +310,20 @@ Worker.prototype.stat = async function ({ filename }) {
342
310
  LastModified // : "2016-12-15T01:19:41.000Z",
343
311
  // Metadata": {},
344
312
  // VersionId": "null"
345
- } = response;
346
- const modifiedAt = new Date(LastModified);
347
- const createdAt = modifiedAt; // Same for S3
348
- const size = parseInt(ContentLength, 10);
349
-
350
- return {
351
- createdAt,
352
- modifiedAt,
353
- contentType: ContentType,
354
- size
355
- };
313
+ } = response;
314
+ const modifiedAt = new Date(LastModified);
315
+ const createdAt = modifiedAt; // Same for S3
316
+ const size = parseInt(ContentLength, 10);
317
+ return {
318
+ createdAt,
319
+ modifiedAt,
320
+ contentType: ContentType,
321
+ size
322
+ };
356
323
  };
357
324
  Worker.prototype.stat.metadata = {
358
- options: {
359
- filename: {}
360
- }
325
+ options: {
326
+ filename: {}
327
+ }
361
328
  };
362
-
363
- module.exports = Worker;
329
+ export default Worker;