@appinventiv/aws-s3 1.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/README.md +595 -0
- package/package.json +23 -0
- package/src/exception-handler.ts +283 -0
- package/src/index.ts +3 -0
- package/src/interface.ts +4 -0
- package/src/s3.ts +251 -0
- package/tsconfig.json +20 -0
package/README.md
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
# @appinventiv/aws-s3
|
|
2
|
+
|
|
3
|
+
A comprehensive AWS S3 client package for Node.js applications. Provides easy-to-use methods for uploading, reading, deleting files, and generating presigned URLs for both S3 and CloudFront.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @appinventiv/aws-s3
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Upload files to S3 using presigned URLs
|
|
14
|
+
- Read files from S3 buckets
|
|
15
|
+
- Delete files from S3 buckets
|
|
16
|
+
- Generate S3 presigned URLs for uploads
|
|
17
|
+
- Generate CloudFront signed URLs for secure file access
|
|
18
|
+
- Generate CloudFront signed cookies for folder access
|
|
19
|
+
- Get file content as Base64 encoded string
|
|
20
|
+
- Support for private key loading from local filesystem or S3
|
|
21
|
+
|
|
22
|
+
## Prerequisites
|
|
23
|
+
|
|
24
|
+
- AWS account with S3 access
|
|
25
|
+
- AWS credentials configured (via environment variables, IAM role, or AWS credentials file)
|
|
26
|
+
- `AWS_REGION` environment variable set
|
|
27
|
+
- (Optional) CloudFront distribution for signed URL generation
|
|
28
|
+
|
|
29
|
+
## AWS Setup
|
|
30
|
+
|
|
31
|
+
1. Create an S3 bucket in AWS
|
|
32
|
+
2. Ensure your AWS credentials have permissions to access S3
|
|
33
|
+
3. Set the `AWS_REGION` environment variable
|
|
34
|
+
4. (Optional) Configure CloudFront distribution for signed URLs
|
|
35
|
+
|
|
36
|
+
### Required IAM Permissions
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"Version": "2012-10-17",
|
|
41
|
+
"Statement": [
|
|
42
|
+
{
|
|
43
|
+
"Effect": "Allow",
|
|
44
|
+
"Action": [
|
|
45
|
+
"s3:GetObject",
|
|
46
|
+
"s3:PutObject",
|
|
47
|
+
"s3:DeleteObject",
|
|
48
|
+
"s3:ListBucket"
|
|
49
|
+
],
|
|
50
|
+
"Resource": [
|
|
51
|
+
"arn:aws:s3:::your-bucket-name",
|
|
52
|
+
"arn:aws:s3:::your-bucket-name/*"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
### Basic Setup
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
65
|
+
|
|
66
|
+
// Set AWS region (required)
|
|
67
|
+
process.env.AWS_REGION = 'us-east-1';
|
|
68
|
+
|
|
69
|
+
// Initialize S3 service
|
|
70
|
+
s3Service.initialiseS3Manager({
|
|
71
|
+
cloudfrontDomain: 'https://d1234567890.cloudfront.net', // Optional
|
|
72
|
+
cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE' // Optional
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Generate Presigned URL for Upload
|
|
77
|
+
|
|
78
|
+
Generate a presigned URL that allows clients to upload files directly to S3:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
82
|
+
|
|
83
|
+
// Initialize S3 service
|
|
84
|
+
s3Service.initialiseS3Manager();
|
|
85
|
+
|
|
86
|
+
// Generate presigned URL for file upload
|
|
87
|
+
const presignedUrl = await s3Service.getPreSignedUrl(
|
|
88
|
+
'my-bucket', // Bucket name
|
|
89
|
+
'uploads/images', // Base path/folder
|
|
90
|
+
'photo.jpg', // File name
|
|
91
|
+
3600 // Expiration in seconds (1 hour)
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
console.log('Presigned URL:', presignedUrl);
|
|
95
|
+
|
|
96
|
+
// Client can now upload file using this URL
|
|
97
|
+
// Example: PUT request to presignedUrl with file in body
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Read File from S3
|
|
101
|
+
|
|
102
|
+
Read a file directly from S3 bucket:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
106
|
+
|
|
107
|
+
// Initialize S3 service
|
|
108
|
+
s3Service.initialiseS3Manager();
|
|
109
|
+
|
|
110
|
+
// Read file content
|
|
111
|
+
const fileContent = await s3Service.readFile(
|
|
112
|
+
'uploads/images/photo.jpg', // S3 object key
|
|
113
|
+
'my-bucket', // Bucket name
|
|
114
|
+
'utf-8' // Optional encoding (default: UTF-8)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
console.log('File content:', fileContent);
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Delete File from S3
|
|
121
|
+
|
|
122
|
+
Delete a file from S3 bucket:
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
126
|
+
|
|
127
|
+
// Initialize S3 service
|
|
128
|
+
s3Service.initialiseS3Manager();
|
|
129
|
+
|
|
130
|
+
// Delete file
|
|
131
|
+
await s3Service.deleteFile(
|
|
132
|
+
'my-bucket', // Bucket name
|
|
133
|
+
'uploads/images/photo.jpg' // S3 object key
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
console.log('File deleted successfully');
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Get File as Base64
|
|
140
|
+
|
|
141
|
+
Get file content as Base64 encoded string:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
145
|
+
|
|
146
|
+
// Initialize S3 service
|
|
147
|
+
s3Service.initialiseS3Manager();
|
|
148
|
+
|
|
149
|
+
// Get file as Base64
|
|
150
|
+
const base64Data = await s3Service.getFileBase64Data({
|
|
151
|
+
bucket: 'my-bucket',
|
|
152
|
+
path: 'uploads/images/photo.jpg'
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
console.log('Base64 data:', base64Data);
|
|
156
|
+
// Use this for embedding in HTML, sending via API, etc.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### CloudFront Signed URLs
|
|
160
|
+
|
|
161
|
+
Generate CloudFront signed URLs for secure file access:
|
|
162
|
+
|
|
163
|
+
#### Setup CloudFront Private Key
|
|
164
|
+
|
|
165
|
+
First, load the CloudFront private key:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
169
|
+
|
|
170
|
+
// Load private key from local filesystem
|
|
171
|
+
await s3Service.loadConfigForReadablePresignedUrl(
|
|
172
|
+
'/path/to/private-key.pem', // Local file path
|
|
173
|
+
false // Not stored on S3
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Or load private key from S3
|
|
177
|
+
await s3Service.loadConfigForReadablePresignedUrl(
|
|
178
|
+
'keys/cloudfront-private-key.pem', // S3 key
|
|
179
|
+
true, // Stored on S3
|
|
180
|
+
'my-config-bucket' // S3 bucket name
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### Generate CloudFront Signed URL for File
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
188
|
+
|
|
189
|
+
// Initialize with CloudFront config
|
|
190
|
+
s3Service.initialiseS3Manager({
|
|
191
|
+
cloudfrontDomain: 'https://d1234567890.cloudfront.net',
|
|
192
|
+
cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE'
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Load private key
|
|
196
|
+
await s3Service.loadConfigForReadablePresignedUrl(
|
|
197
|
+
'/path/to/private-key.pem',
|
|
198
|
+
false
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Generate signed URL for a file
|
|
202
|
+
const signedUrl = await s3Service.getPreSignedUrlToReadFile(
|
|
203
|
+
'/uploads/images/photo.jpg', // File path (relative to CloudFront domain)
|
|
204
|
+
3600000 // Expiration in milliseconds (1 hour)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
console.log('Signed URL:', signedUrl);
|
|
208
|
+
// URL is valid for the specified expiration time
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
#### Generate CloudFront Signed Cookies for Folder
|
|
212
|
+
|
|
213
|
+
Generate signed cookies that allow access to all files in a folder:
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
217
|
+
|
|
218
|
+
// Initialize with CloudFront config
|
|
219
|
+
s3Service.initialiseS3Manager({
|
|
220
|
+
cloudfrontDomain: 'https://d1234567890.cloudfront.net',
|
|
221
|
+
cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE'
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Load private key
|
|
225
|
+
await s3Service.loadConfigForReadablePresignedUrl(
|
|
226
|
+
'/path/to/private-key.pem',
|
|
227
|
+
false
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Generate signed cookies for folder access
|
|
231
|
+
const cookies = await s3Service.getPreSignedUrlToReadFolder(
|
|
232
|
+
'/uploads/images/', // Folder path (relative to CloudFront domain)
|
|
233
|
+
3600000 // Expiration in milliseconds (1 hour)
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
console.log('Signed cookies:', cookies);
|
|
237
|
+
// Set these cookies in the browser to access all files in the folder
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Complete Example
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
244
|
+
|
|
245
|
+
async function setupS3Service() {
|
|
246
|
+
// Set AWS region
|
|
247
|
+
process.env.AWS_REGION = 'us-east-1';
|
|
248
|
+
|
|
249
|
+
// Initialize S3 service with CloudFront config
|
|
250
|
+
s3Service.initialiseS3Manager({
|
|
251
|
+
cloudfrontDomain: 'https://d1234567890.cloudfront.net',
|
|
252
|
+
cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE'
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Load CloudFront private key
|
|
256
|
+
await s3Service.loadConfigForReadablePresignedUrl(
|
|
257
|
+
'./keys/cloudfront-private-key.pem',
|
|
258
|
+
false
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
// Generate presigned URL for upload
|
|
262
|
+
const uploadUrl = await s3Service.getPreSignedUrl(
|
|
263
|
+
'my-bucket',
|
|
264
|
+
'uploads',
|
|
265
|
+
'document.pdf',
|
|
266
|
+
3600
|
|
267
|
+
);
|
|
268
|
+
console.log('Upload URL:', uploadUrl);
|
|
269
|
+
|
|
270
|
+
// Read file from S3
|
|
271
|
+
const content = await s3Service.readFile(
|
|
272
|
+
'uploads/document.pdf',
|
|
273
|
+
'my-bucket'
|
|
274
|
+
);
|
|
275
|
+
console.log('File content:', content);
|
|
276
|
+
|
|
277
|
+
// Get file as Base64
|
|
278
|
+
const base64 = await s3Service.getFileBase64Data({
|
|
279
|
+
bucket: 'my-bucket',
|
|
280
|
+
path: 'uploads/document.pdf'
|
|
281
|
+
});
|
|
282
|
+
console.log('Base64:', base64);
|
|
283
|
+
|
|
284
|
+
// Generate CloudFront signed URL
|
|
285
|
+
const signedUrl = await s3Service.getPreSignedUrlToReadFile(
|
|
286
|
+
'/uploads/document.pdf',
|
|
287
|
+
3600000
|
|
288
|
+
);
|
|
289
|
+
console.log('Signed URL:', signedUrl);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
setupS3Service().catch(console.error);
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Express.js Integration Example
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
import express from 'express';
|
|
299
|
+
import { s3Service } from '@appinventiv/aws-s3';
|
|
300
|
+
|
|
301
|
+
const app = express();
|
|
302
|
+
|
|
303
|
+
// Initialize S3 on startup
|
|
304
|
+
s3Service.initialiseS3Manager({
|
|
305
|
+
cloudfrontDomain: process.env.CLOUDFRONT_DOMAIN || '',
|
|
306
|
+
cloudfrontKeyPairId: process.env.CLOUDFRONT_KEY_PAIR_ID || ''
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Load CloudFront private key
|
|
310
|
+
if (process.env.CLOUDFRONT_PRIVATE_KEY_PATH) {
|
|
311
|
+
await s3Service.loadConfigForReadablePresignedUrl(
|
|
312
|
+
process.env.CLOUDFRONT_PRIVATE_KEY_PATH,
|
|
313
|
+
false
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Generate upload URL endpoint
|
|
318
|
+
app.post('/api/upload-url', async (req, res) => {
|
|
319
|
+
try {
|
|
320
|
+
const { fileName, folder } = req.body;
|
|
321
|
+
|
|
322
|
+
const presignedUrl = await s3Service.getPreSignedUrl(
|
|
323
|
+
process.env.S3_BUCKET || 'my-bucket',
|
|
324
|
+
folder || 'uploads',
|
|
325
|
+
fileName,
|
|
326
|
+
3600
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
res.json({ uploadUrl: presignedUrl });
|
|
330
|
+
} catch (error) {
|
|
331
|
+
res.status(500).json({ error: 'Failed to generate upload URL' });
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Get file endpoint
|
|
336
|
+
app.get('/api/file/:key', async (req, res) => {
|
|
337
|
+
try {
|
|
338
|
+
const content = await s3Service.readFile(
|
|
339
|
+
req.params.key,
|
|
340
|
+
process.env.S3_BUCKET || 'my-bucket'
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
res.send(content);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
res.status(404).json({ error: 'File not found' });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Generate signed URL endpoint
|
|
350
|
+
app.post('/api/signed-url', async (req, res) => {
|
|
351
|
+
try {
|
|
352
|
+
const { filePath, expiration } = req.body;
|
|
353
|
+
|
|
354
|
+
const signedUrl = await s3Service.getPreSignedUrlToReadFile(
|
|
355
|
+
filePath,
|
|
356
|
+
expiration || 3600000
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
res.json({ signedUrl });
|
|
360
|
+
} catch (error) {
|
|
361
|
+
res.status(500).json({ error: 'Failed to generate signed URL' });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
app.listen(3000, () => {
|
|
366
|
+
console.log('Server started on port 3000');
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
## API Reference
|
|
371
|
+
|
|
372
|
+
### `s3Service` (Singleton Instance)
|
|
373
|
+
|
|
374
|
+
Pre-configured S3 service instance ready to use.
|
|
375
|
+
|
|
376
|
+
### `AWSS3Provider` Class
|
|
377
|
+
|
|
378
|
+
Main S3 provider class.
|
|
379
|
+
|
|
380
|
+
#### `initialiseS3Manager(config?: IS3Config)`
|
|
381
|
+
|
|
382
|
+
Initializes the S3 service with optional CloudFront configuration.
|
|
383
|
+
|
|
384
|
+
**Parameters:**
|
|
385
|
+
- `config.cloudfrontDomain` (string, optional): CloudFront distribution domain
|
|
386
|
+
- `config.cloudfrontKeyPairId` (string, optional): CloudFront key pair ID
|
|
387
|
+
|
|
388
|
+
**Example:**
|
|
389
|
+
```typescript
|
|
390
|
+
s3Service.initialiseS3Manager({
|
|
391
|
+
cloudfrontDomain: 'https://d1234567890.cloudfront.net',
|
|
392
|
+
cloudfrontKeyPairId: 'APKAIOSFODNN7EXAMPLE'
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
#### `loadConfigForReadablePresignedUrl(privateKeyPath: string, isStoredOnS3: boolean, bucket?: string)`
|
|
397
|
+
|
|
398
|
+
Loads CloudFront private key from local filesystem or S3.
|
|
399
|
+
|
|
400
|
+
**Parameters:**
|
|
401
|
+
- `privateKeyPath` (string): Path to private key (local path or S3 key)
|
|
402
|
+
- `isStoredOnS3` (boolean): Whether key is stored in S3
|
|
403
|
+
- `bucket` (string, optional): S3 bucket name (required if `isStoredOnS3` is true)
|
|
404
|
+
|
|
405
|
+
**Example:**
|
|
406
|
+
```typescript
|
|
407
|
+
// From local filesystem
|
|
408
|
+
await s3Service.loadConfigForReadablePresignedUrl('/path/to/key.pem', false);
|
|
409
|
+
|
|
410
|
+
// From S3
|
|
411
|
+
await s3Service.loadConfigForReadablePresignedUrl('keys/key.pem', true, 'my-bucket');
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### `getPreSignedUrl(bucketName: string, basePath: string, fileName: string, expiresIn?: number)`
|
|
415
|
+
|
|
416
|
+
Generates a presigned URL for uploading files to S3.
|
|
417
|
+
|
|
418
|
+
**Parameters:**
|
|
419
|
+
- `bucketName` (string): S3 bucket name
|
|
420
|
+
- `basePath` (string): Base path/folder in bucket
|
|
421
|
+
- `fileName` (string): Name of the file
|
|
422
|
+
- `expiresIn` (number, optional): Expiration time in seconds (default: 120)
|
|
423
|
+
|
|
424
|
+
**Returns:**
|
|
425
|
+
- `Promise<string>`: Presigned URL for upload
|
|
426
|
+
|
|
427
|
+
**Example:**
|
|
428
|
+
```typescript
|
|
429
|
+
const url = await s3Service.getPreSignedUrl('my-bucket', 'uploads', 'file.jpg', 3600);
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### `readFile(key: string, bucket: string, encoding?: string)`
|
|
433
|
+
|
|
434
|
+
Reads a file from S3 bucket.
|
|
435
|
+
|
|
436
|
+
**Parameters:**
|
|
437
|
+
- `key` (string): S3 object key (file path)
|
|
438
|
+
- `bucket` (string): S3 bucket name
|
|
439
|
+
- `encoding` (string, optional): File encoding (default: UTF-8)
|
|
440
|
+
|
|
441
|
+
**Returns:**
|
|
442
|
+
- `Promise<string>`: File content as string
|
|
443
|
+
|
|
444
|
+
**Example:**
|
|
445
|
+
```typescript
|
|
446
|
+
const content = await s3Service.readFile('uploads/file.txt', 'my-bucket', 'utf-8');
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
#### `deleteFile(bucket: string, key: string)`
|
|
450
|
+
|
|
451
|
+
Deletes a file from S3 bucket.
|
|
452
|
+
|
|
453
|
+
**Parameters:**
|
|
454
|
+
- `bucket` (string): S3 bucket name
|
|
455
|
+
- `key` (string): S3 object key (file path)
|
|
456
|
+
|
|
457
|
+
**Returns:**
|
|
458
|
+
- `Promise<object>`: Delete operation result
|
|
459
|
+
|
|
460
|
+
**Example:**
|
|
461
|
+
```typescript
|
|
462
|
+
await s3Service.deleteFile('my-bucket', 'uploads/file.jpg');
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
#### `getFileBase64Data(fileData: { bucket: string; path: string })`
|
|
466
|
+
|
|
467
|
+
Gets file content as Base64 encoded string.
|
|
468
|
+
|
|
469
|
+
**Parameters:**
|
|
470
|
+
- `fileData.bucket` (string): S3 bucket name
|
|
471
|
+
- `fileData.path` (string): S3 object key (file path)
|
|
472
|
+
|
|
473
|
+
**Returns:**
|
|
474
|
+
- `Promise<string>`: Base64 encoded file content
|
|
475
|
+
|
|
476
|
+
**Example:**
|
|
477
|
+
```typescript
|
|
478
|
+
const base64 = await s3Service.getFileBase64Data({
|
|
479
|
+
bucket: 'my-bucket',
|
|
480
|
+
path: 'uploads/image.jpg'
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
#### `getPreSignedUrlToReadFile(filePath: string, expiration: number)`
|
|
485
|
+
|
|
486
|
+
Generates a CloudFront signed URL for reading a file.
|
|
487
|
+
|
|
488
|
+
**Parameters:**
|
|
489
|
+
- `filePath` (string): File path relative to CloudFront domain
|
|
490
|
+
- `expiration` (number): Expiration time in milliseconds
|
|
491
|
+
|
|
492
|
+
**Returns:**
|
|
493
|
+
- `Promise<string>`: CloudFront signed URL
|
|
494
|
+
|
|
495
|
+
**Example:**
|
|
496
|
+
```typescript
|
|
497
|
+
const signedUrl = await s3Service.getPreSignedUrlToReadFile('/uploads/file.jpg', 3600000);
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
#### `getPreSignedUrlToReadFolder(folderPath: string, expiration: number)`
|
|
501
|
+
|
|
502
|
+
Generates CloudFront signed cookies for folder access.
|
|
503
|
+
|
|
504
|
+
**Parameters:**
|
|
505
|
+
- `folderPath` (string): Folder path relative to CloudFront domain
|
|
506
|
+
- `expiration` (number): Expiration time in milliseconds
|
|
507
|
+
|
|
508
|
+
**Returns:**
|
|
509
|
+
- `Promise<object>`: CloudFront signed cookies object
|
|
510
|
+
|
|
511
|
+
**Example:**
|
|
512
|
+
```typescript
|
|
513
|
+
const cookies = await s3Service.getPreSignedUrlToReadFolder('/uploads/images/', 3600000);
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Environment Variables
|
|
517
|
+
|
|
518
|
+
- `AWS_REGION` (required): AWS region where your S3 bucket is located (e.g., `us-east-1`)
|
|
519
|
+
|
|
520
|
+
## CloudFront Setup
|
|
521
|
+
|
|
522
|
+
To use CloudFront signed URLs:
|
|
523
|
+
|
|
524
|
+
1. Create a CloudFront distribution pointing to your S3 bucket
|
|
525
|
+
2. Create a CloudFront key pair in AWS
|
|
526
|
+
3. Download the private key
|
|
527
|
+
4. Configure the package with CloudFront domain and key pair ID
|
|
528
|
+
5. Load the private key using `loadConfigForReadablePresignedUrl()`
|
|
529
|
+
|
|
530
|
+
## Error Handling
|
|
531
|
+
|
|
532
|
+
The package includes structured error handling with `S3Exception` class. All errors are automatically categorized and returned in a consistent format:
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
import { s3Service, S3Exception } from '@appinventiv/aws-s3';
|
|
536
|
+
|
|
537
|
+
try {
|
|
538
|
+
await s3Service.readFile('file.jpg', 'my-bucket');
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (error instanceof S3Exception) {
|
|
541
|
+
const errorResponse = error.getError();
|
|
542
|
+
// Returns: { status: 404, data: { message, type, originalError, context, ... } }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Error types include: Connection, Authentication, Not Found, Validation, Timeout, Server, and Operation errors.
|
|
548
|
+
|
|
549
|
+
## TypeScript Support
|
|
550
|
+
|
|
551
|
+
The package includes full TypeScript definitions and is written in TypeScript.
|
|
552
|
+
|
|
553
|
+
## Dependencies
|
|
554
|
+
|
|
555
|
+
- `@aws-sdk/client-s3`: ^3.975.0
|
|
556
|
+
- `@aws-sdk/cloudfront-signer`: ^3.975.0
|
|
557
|
+
- `@aws-sdk/s3-request-presigner`: ^3.975.0
|
|
558
|
+
|
|
559
|
+
## Security Best Practices
|
|
560
|
+
|
|
561
|
+
1. **Never commit AWS credentials** to version control
|
|
562
|
+
2. **Use IAM roles** when running on AWS infrastructure (EC2, ECS, Lambda)
|
|
563
|
+
3. **Set appropriate expiration times** for presigned URLs
|
|
564
|
+
4. **Use CloudFront signed URLs** for secure file access
|
|
565
|
+
5. **Store private keys securely** (use AWS Secrets Manager or environment variables)
|
|
566
|
+
6. **Use least privilege IAM policies** for S3 access
|
|
567
|
+
7. **Enable S3 bucket encryption** for sensitive files
|
|
568
|
+
|
|
569
|
+
## Troubleshooting
|
|
570
|
+
|
|
571
|
+
### Common Issues
|
|
572
|
+
|
|
573
|
+
1. **"Unable to Connect Error"**
|
|
574
|
+
- Verify AWS credentials are configured
|
|
575
|
+
- Check `AWS_REGION` environment variable is set
|
|
576
|
+
- Ensure IAM permissions are correct
|
|
577
|
+
|
|
578
|
+
2. **"File not found" errors**
|
|
579
|
+
- Verify bucket name is correct
|
|
580
|
+
- Check S3 object key (path) is correct
|
|
581
|
+
- Ensure file exists in the bucket
|
|
582
|
+
|
|
583
|
+
3. **CloudFront signed URL errors**
|
|
584
|
+
- Verify CloudFront domain and key pair ID are correct
|
|
585
|
+
- Ensure private key is loaded before generating signed URLs
|
|
586
|
+
- Check private key format is correct (PEM format)
|
|
587
|
+
|
|
588
|
+
4. **Presigned URL expiration**
|
|
589
|
+
- URLs expire after the specified time
|
|
590
|
+
- Generate new URLs if expired
|
|
591
|
+
- Consider longer expiration times for production use
|
|
592
|
+
|
|
593
|
+
## License
|
|
594
|
+
|
|
595
|
+
ISC
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@appinventiv/aws-s3",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [],
|
|
12
|
+
"author": "",
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^25.0.10",
|
|
16
|
+
"typescript": "^5.9.3"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@aws-sdk/client-s3": "^3.975.0",
|
|
20
|
+
"@aws-sdk/cloudfront-signer": "^3.975.0",
|
|
21
|
+
"@aws-sdk/s3-request-presigner": "^3.975.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description HTTP status messages enum
|
|
3
|
+
*/
|
|
4
|
+
export enum HTTP_STATUS_MESSAGE {
|
|
5
|
+
BAD_REQUEST = 400,
|
|
6
|
+
UNAUTHORIZED = 401,
|
|
7
|
+
FORBIDDEN = 403,
|
|
8
|
+
NOT_FOUND = 404,
|
|
9
|
+
CONFLICT = 409,
|
|
10
|
+
REQUEST_TIMEOUT = 408,
|
|
11
|
+
TOO_MANY_REQUESTS = 429,
|
|
12
|
+
INTERNAL_SERVER_ERROR = 500,
|
|
13
|
+
SERVICE_UNAVAILABLE = 503
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @description Exception message types for S3 operations
|
|
18
|
+
*/
|
|
19
|
+
export enum ExceptionMessage {
|
|
20
|
+
S3_CONNECTION_ERROR = 'S3_CONNECTION_ERROR',
|
|
21
|
+
S3_OPERATION_ERROR = 'S3_OPERATION_ERROR',
|
|
22
|
+
S3_AUTH_ERROR = 'S3_AUTH_ERROR',
|
|
23
|
+
S3_NOT_FOUND = 'S3_NOT_FOUND',
|
|
24
|
+
S3_SERVER_ERROR = 'S3_SERVER_ERROR',
|
|
25
|
+
S3_TIMEOUT_ERROR = 'S3_TIMEOUT_ERROR',
|
|
26
|
+
S3_VALIDATION_ERROR = 'S3_VALIDATION_ERROR',
|
|
27
|
+
S3_UNKNOWN_ERROR = 'S3_UNKNOWN_ERROR'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @description S3 connection error codes
|
|
32
|
+
*/
|
|
33
|
+
export const S3_CONNECTION_ERROR = {
|
|
34
|
+
ECONNREFUSED: 'ECONNREFUSED',
|
|
35
|
+
ENOTFOUND: 'ENOTFOUND',
|
|
36
|
+
ECONNRESET: 'ECONNRESET',
|
|
37
|
+
EHOSTUNREACH: 'EHOSTUNREACH',
|
|
38
|
+
EADDRNOTAVAIL: 'EADDRNOTAVAIL',
|
|
39
|
+
ETIMEDOUT: 'ETIMEDOUT',
|
|
40
|
+
NETWORK_ERROR: 'NetworkError',
|
|
41
|
+
TIMEOUT_ERROR: 'TimeoutError'
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @description S3 operation error codes
|
|
46
|
+
*/
|
|
47
|
+
export const S3_OPERATION_ERROR = {
|
|
48
|
+
NO_SUCH_KEY: 'NoSuchKey',
|
|
49
|
+
NO_SUCH_BUCKET: 'NoSuchBucket',
|
|
50
|
+
ACCESS_DENIED: 'AccessDenied',
|
|
51
|
+
INVALID_BUCKET_NAME: 'InvalidBucketName',
|
|
52
|
+
BUCKET_ALREADY_EXISTS: 'BucketAlreadyExists',
|
|
53
|
+
INVALID_OBJECT_STATE: 'InvalidObjectState',
|
|
54
|
+
KEY_TOO_LONG: 'KeyTooLongError',
|
|
55
|
+
INVALID_ARGUMENT: 'InvalidArgument'
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @description Base exception handler class
|
|
60
|
+
*/
|
|
61
|
+
export class ExceptionHandler {
|
|
62
|
+
protected code: number;
|
|
63
|
+
protected status: HTTP_STATUS_MESSAGE;
|
|
64
|
+
protected data: any;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @description Get error response object
|
|
68
|
+
* @returns {object} Error object with status and data
|
|
69
|
+
*/
|
|
70
|
+
getError() {
|
|
71
|
+
return {
|
|
72
|
+
status: this.status || HTTP_STATUS_MESSAGE.BAD_REQUEST,
|
|
73
|
+
data: this.data
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @description AWS S3 exception handler
|
|
80
|
+
* Categorizes and handles AWS S3 SDK errors
|
|
81
|
+
*/
|
|
82
|
+
export class S3Exception extends ExceptionHandler {
|
|
83
|
+
constructor(error: any, context?: { bucket?: string; key?: string; operation?: string }) {
|
|
84
|
+
super();
|
|
85
|
+
|
|
86
|
+
const { type, status, message, name } = this.categorizeS3Error(error, context);
|
|
87
|
+
|
|
88
|
+
this.data = {
|
|
89
|
+
message: message || type,
|
|
90
|
+
type: type,
|
|
91
|
+
originalError: name || 'S3Error',
|
|
92
|
+
stack: error?.stack,
|
|
93
|
+
requestId: error?.$metadata?.requestId,
|
|
94
|
+
context: context || null
|
|
95
|
+
};
|
|
96
|
+
this.status = status;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @description Categorize S3 error based on error type and code
|
|
101
|
+
* @param {any} error - AWS SDK error object
|
|
102
|
+
* @param {object} context - Additional context (bucket, key, operation)
|
|
103
|
+
* @returns {object} Categorized error information
|
|
104
|
+
*/
|
|
105
|
+
private categorizeS3Error(error: any, context?: { bucket?: string; key?: string; operation?: string }): {
|
|
106
|
+
type: ExceptionMessage;
|
|
107
|
+
message: string;
|
|
108
|
+
name: string;
|
|
109
|
+
status: HTTP_STATUS_MESSAGE;
|
|
110
|
+
} {
|
|
111
|
+
const message = (error?.message || '').toLowerCase();
|
|
112
|
+
const name = (error?.name || '').toLowerCase();
|
|
113
|
+
const code = error?.Code || error?.code || '';
|
|
114
|
+
const httpStatusCode = error?.$metadata?.httpStatusCode || 500;
|
|
115
|
+
|
|
116
|
+
// ------------------
|
|
117
|
+
// Connection Errors
|
|
118
|
+
// ------------------
|
|
119
|
+
const connectionMatches = [
|
|
120
|
+
S3_CONNECTION_ERROR.ECONNREFUSED,
|
|
121
|
+
S3_CONNECTION_ERROR.ENOTFOUND,
|
|
122
|
+
S3_CONNECTION_ERROR.ECONNRESET,
|
|
123
|
+
S3_CONNECTION_ERROR.EHOSTUNREACH,
|
|
124
|
+
S3_CONNECTION_ERROR.EADDRNOTAVAIL,
|
|
125
|
+
S3_CONNECTION_ERROR.ETIMEDOUT,
|
|
126
|
+
S3_CONNECTION_ERROR.NETWORK_ERROR,
|
|
127
|
+
S3_CONNECTION_ERROR.TIMEOUT_ERROR,
|
|
128
|
+
'networkerror',
|
|
129
|
+
'timeout',
|
|
130
|
+
'connection'
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
if (connectionMatches.some((m) => message.includes(m.toLowerCase()) || name.includes(m.toLowerCase()))) {
|
|
134
|
+
return {
|
|
135
|
+
type: ExceptionMessage.S3_CONNECTION_ERROR,
|
|
136
|
+
message: `S3 connection error: ${error?.message || 'Unable to connect to S3'}`,
|
|
137
|
+
name: name || 'S3ConnectionError',
|
|
138
|
+
status: HTTP_STATUS_MESSAGE.SERVICE_UNAVAILABLE
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ------------------
|
|
143
|
+
// Authentication Errors
|
|
144
|
+
// ------------------
|
|
145
|
+
const authMatches = [
|
|
146
|
+
S3_OPERATION_ERROR.ACCESS_DENIED,
|
|
147
|
+
'InvalidAccessKeyId',
|
|
148
|
+
'SignatureDoesNotMatch',
|
|
149
|
+
'InvalidSecurity',
|
|
150
|
+
'MissingSecurityHeader',
|
|
151
|
+
'TokenRefreshRequired',
|
|
152
|
+
'InvalidToken',
|
|
153
|
+
'ExpiredToken',
|
|
154
|
+
'accessdenied',
|
|
155
|
+
'unauthorized',
|
|
156
|
+
'forbidden'
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
authMatches.some((m) =>
|
|
161
|
+
code?.toLowerCase().includes(m.toLowerCase()) ||
|
|
162
|
+
message.includes(m.toLowerCase()) ||
|
|
163
|
+
name.includes(m.toLowerCase())
|
|
164
|
+
)
|
|
165
|
+
) {
|
|
166
|
+
return {
|
|
167
|
+
type: ExceptionMessage.S3_AUTH_ERROR,
|
|
168
|
+
message: `S3 authentication error: ${error?.message || 'Access denied. Check your IAM permissions'}`,
|
|
169
|
+
name: name || 'S3AuthError',
|
|
170
|
+
status: HTTP_STATUS_MESSAGE.UNAUTHORIZED
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ------------------
|
|
175
|
+
// Not Found Errors
|
|
176
|
+
// ------------------
|
|
177
|
+
const notFoundMatches = [
|
|
178
|
+
S3_OPERATION_ERROR.NO_SUCH_KEY,
|
|
179
|
+
S3_OPERATION_ERROR.NO_SUCH_BUCKET,
|
|
180
|
+
'nosuchkey',
|
|
181
|
+
'nosuchbucket',
|
|
182
|
+
'notfound'
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
if (
|
|
186
|
+
notFoundMatches.some((m) =>
|
|
187
|
+
code?.toLowerCase().includes(m.toLowerCase()) ||
|
|
188
|
+
message.includes(m.toLowerCase()) ||
|
|
189
|
+
name.includes(m.toLowerCase())
|
|
190
|
+
)
|
|
191
|
+
) {
|
|
192
|
+
const bucketInfo = context?.bucket ? ` in bucket '${context.bucket}'` : '';
|
|
193
|
+
const keyInfo = context?.key ? ` for key '${context.key}'` : '';
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
type: ExceptionMessage.S3_NOT_FOUND,
|
|
197
|
+
message: `S3 resource not found${keyInfo}${bucketInfo}: ${error?.message || 'Resource does not exist'}`,
|
|
198
|
+
name: name || 'S3NotFoundError',
|
|
199
|
+
status: HTTP_STATUS_MESSAGE.NOT_FOUND
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ------------------
|
|
204
|
+
// Validation Errors
|
|
205
|
+
// ------------------
|
|
206
|
+
const validationMatches = [
|
|
207
|
+
S3_OPERATION_ERROR.INVALID_BUCKET_NAME,
|
|
208
|
+
S3_OPERATION_ERROR.KEY_TOO_LONG,
|
|
209
|
+
S3_OPERATION_ERROR.INVALID_ARGUMENT,
|
|
210
|
+
'InvalidArgument',
|
|
211
|
+
'MalformedXML',
|
|
212
|
+
'KeyTooLongError',
|
|
213
|
+
'validation',
|
|
214
|
+
'invalid'
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
if (
|
|
218
|
+
validationMatches.some((m) =>
|
|
219
|
+
code?.toLowerCase().includes(m.toLowerCase()) ||
|
|
220
|
+
message.includes(m.toLowerCase()) ||
|
|
221
|
+
name.includes(m.toLowerCase())
|
|
222
|
+
)
|
|
223
|
+
) {
|
|
224
|
+
return {
|
|
225
|
+
type: ExceptionMessage.S3_VALIDATION_ERROR,
|
|
226
|
+
message: `S3 validation error: ${error?.message || 'Invalid request parameters'}`,
|
|
227
|
+
name: name || 'S3ValidationError',
|
|
228
|
+
status: HTTP_STATUS_MESSAGE.BAD_REQUEST
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ------------------
|
|
233
|
+
// Timeout Errors
|
|
234
|
+
// ------------------
|
|
235
|
+
if (
|
|
236
|
+
code === 'RequestTimeout' ||
|
|
237
|
+
message.includes('timeout') ||
|
|
238
|
+
name.includes('timeout') ||
|
|
239
|
+
httpStatusCode === 408
|
|
240
|
+
) {
|
|
241
|
+
return {
|
|
242
|
+
type: ExceptionMessage.S3_TIMEOUT_ERROR,
|
|
243
|
+
message: `S3 request timeout: ${error?.message || 'Request took too long'}`,
|
|
244
|
+
name: name || 'S3TimeoutError',
|
|
245
|
+
status: HTTP_STATUS_MESSAGE.REQUEST_TIMEOUT
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ------------------
|
|
250
|
+
// Server Errors (5xx)
|
|
251
|
+
// ------------------
|
|
252
|
+
if (httpStatusCode >= 500 && httpStatusCode < 600) {
|
|
253
|
+
return {
|
|
254
|
+
type: ExceptionMessage.S3_SERVER_ERROR,
|
|
255
|
+
message: `S3 server error: ${error?.message || 'Internal server error'}`,
|
|
256
|
+
name: name || 'S3ServerError',
|
|
257
|
+
status: HTTP_STATUS_MESSAGE.INTERNAL_SERVER_ERROR
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ------------------
|
|
262
|
+
// Operational Errors (4xx)
|
|
263
|
+
// ------------------
|
|
264
|
+
if (httpStatusCode >= 400 && httpStatusCode < 500) {
|
|
265
|
+
return {
|
|
266
|
+
type: ExceptionMessage.S3_OPERATION_ERROR,
|
|
267
|
+
message: `S3 operation error: ${error?.message || 'Operation failed'}`,
|
|
268
|
+
name: name || 'S3OperationError',
|
|
269
|
+
status: <HTTP_STATUS_MESSAGE>httpStatusCode || HTTP_STATUS_MESSAGE.BAD_REQUEST
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ------------------
|
|
274
|
+
// Default / Unknown
|
|
275
|
+
// ------------------
|
|
276
|
+
return {
|
|
277
|
+
type: ExceptionMessage.S3_UNKNOWN_ERROR,
|
|
278
|
+
message: `S3 error: ${error?.message || 'Unknown error occurred'}`,
|
|
279
|
+
name: name || 'S3UnknownError',
|
|
280
|
+
status: HTTP_STATUS_MESSAGE.INTERNAL_SERVER_ERROR
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
package/src/index.ts
ADDED
package/src/interface.ts
ADDED
package/src/s3.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { DeleteObjectCommandInput, GetObjectCommand, GetObjectCommandInput, PutObjectCommand, S3, S3ClientConfig } from '@aws-sdk/client-s3';
|
|
2
|
+
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
|
3
|
+
import { CloudfrontSignInput, getSignedUrl as CloudFrontSigner, getSignedCookies } from '@aws-sdk/cloudfront-signer';
|
|
4
|
+
import { IS3Config } from './interface';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { S3Exception } from './exception-handler';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @description AWS S3 provider class for managing S3 operations
|
|
10
|
+
* Provides functionality for uploading, reading, deleting files, and generating presigned URLs
|
|
11
|
+
* Supports both S3 presigned URLs and CloudFront signed URLs/cookies
|
|
12
|
+
*/
|
|
13
|
+
class AWSS3Provider {
|
|
14
|
+
private client: S3;
|
|
15
|
+
private privateKey: string | Buffer;
|
|
16
|
+
private cloudfrontDomain: string;
|
|
17
|
+
private cloudfrontKeyPairId: string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @description Initialize S3 Manager with optional CloudFront configuration
|
|
21
|
+
* @param {IS3Config} config - Optional configuration for CloudFront domain and key pair ID
|
|
22
|
+
*/
|
|
23
|
+
public initialiseS3Manager(config?: IS3Config) {
|
|
24
|
+
try {
|
|
25
|
+
this.cloudfrontDomain = config?.cloudfrontDomain ?? '';
|
|
26
|
+
this.cloudfrontKeyPairId = config?.cloudfrontKeyPairId ?? '';
|
|
27
|
+
this.client = new S3(this.getConfiguration());
|
|
28
|
+
console.info('Connected to S3 Manager');
|
|
29
|
+
} catch (error: unknown) {
|
|
30
|
+
console.error(`Error--initializeS3Manager-Unable to Connect--msg :: `, error);
|
|
31
|
+
throw new S3Exception(error, { operation: 'initialiseS3Manager' }).getError();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @description Get S3 client configuration from environment variables
|
|
37
|
+
* @returns {S3ClientConfig} S3 client configuration object
|
|
38
|
+
*/
|
|
39
|
+
private getConfiguration(): S3ClientConfig {
|
|
40
|
+
const creds: S3ClientConfig = {};
|
|
41
|
+
creds.region = <string>process.env.AWS_REGION;
|
|
42
|
+
return creds;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @description Load private key for CloudFront signed URL generation
|
|
47
|
+
* @param {string} privateKeyPath - Path to the private key file (local path or S3 key)
|
|
48
|
+
* @param {boolean} isStoredOnS3 - Whether the private key is stored in S3 or locally
|
|
49
|
+
* @param {string} bucket - S3 bucket name (required if isStoredOnS3 is true)
|
|
50
|
+
*/
|
|
51
|
+
public async loadConfigForReadablePresignedUrl(privateKeyPath: string, isStoredOnS3: boolean, bucket?: string ){
|
|
52
|
+
try {
|
|
53
|
+
if(isStoredOnS3 && bucket){
|
|
54
|
+
this.privateKey = <string>await this.readFile(privateKeyPath, bucket);
|
|
55
|
+
} else {
|
|
56
|
+
this.privateKey = readFileSync(privateKeyPath, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
console.info('Private key Loaded Successfully');
|
|
59
|
+
} catch (error: unknown) {
|
|
60
|
+
console.error(`Error--loadConfigForReadablePresignedUrl--msg :: `, error);
|
|
61
|
+
throw new S3Exception(error, {
|
|
62
|
+
operation: 'loadConfigForReadablePresignedUrl',
|
|
63
|
+
bucket
|
|
64
|
+
}).getError();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @description Generate a presigned URL for uploading files to S3
|
|
70
|
+
* @param {string} bucketName - Name of the S3 bucket
|
|
71
|
+
* @param {string} basePath - Base path/folder in the bucket
|
|
72
|
+
* @param {string} fileName - Name of the file to upload
|
|
73
|
+
* @param {number} expiresIn - Expiration time in seconds (default: 120 seconds / 2 minutes)
|
|
74
|
+
* @returns {Promise<string>} Presigned URL for uploading the file
|
|
75
|
+
*/
|
|
76
|
+
public async getPreSignedUrl(
|
|
77
|
+
bucketName: string,
|
|
78
|
+
basePath: string,
|
|
79
|
+
fileName: string,
|
|
80
|
+
expiresIn: number = 120 // Default expiry set to 2 minutes
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
try {
|
|
83
|
+
console.info(`Generating presigned URL for - ${basePath}/${fileName} in ${bucketName}`);
|
|
84
|
+
const command = new PutObjectCommand({
|
|
85
|
+
Bucket: bucketName,
|
|
86
|
+
Key: `${basePath}/${fileName}`,
|
|
87
|
+
ACL: 'private'
|
|
88
|
+
});
|
|
89
|
+
return await getSignedUrl(this.client, command, { expiresIn });
|
|
90
|
+
} catch (error: unknown) {
|
|
91
|
+
console.error(`Error--getPreSignedUrl--msg :: `, error);
|
|
92
|
+
throw new S3Exception(error, {
|
|
93
|
+
operation: 'getPreSignedUrl',
|
|
94
|
+
bucket: bucketName,
|
|
95
|
+
key: `${basePath}/${fileName}`
|
|
96
|
+
}).getError();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @description Generate a CloudFront signed URL for reading a file
|
|
102
|
+
* @param {string} filePath - Path to the file in CloudFront (relative to domain)
|
|
103
|
+
* @param {number} expiration - Expiration time in milliseconds
|
|
104
|
+
* @returns {Promise<string>} CloudFront signed URL for reading the file
|
|
105
|
+
*/
|
|
106
|
+
public async getPreSignedUrlToReadFile(filePath: string, expiration: number) {
|
|
107
|
+
try {
|
|
108
|
+
const cmd: CloudfrontSignInput = {
|
|
109
|
+
url: `${this.cloudfrontDomain}${filePath}`,
|
|
110
|
+
keyPairId: this.cloudfrontKeyPairId,
|
|
111
|
+
dateLessThan: new Date(Date.now() + expiration).toISOString(),
|
|
112
|
+
privateKey: this.privateKey
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return CloudFrontSigner(cmd);
|
|
116
|
+
} catch (error: unknown) {
|
|
117
|
+
console.error(`Error--getPreSignedUrlToReadFile--msg :: `, error);
|
|
118
|
+
throw new S3Exception(error, {
|
|
119
|
+
operation: 'getPreSignedUrlToReadFile',
|
|
120
|
+
key: filePath
|
|
121
|
+
}).getError();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @description Generate CloudFront signed cookies for reading files in a folder
|
|
127
|
+
* @param {string} folderPath - Path to the folder in CloudFront (relative to domain)
|
|
128
|
+
* @param {number} expiration - Expiration time in milliseconds
|
|
129
|
+
* @returns {Promise<object>} CloudFront signed cookies object
|
|
130
|
+
*/
|
|
131
|
+
public async getPreSignedUrlToReadFolder(folderPath: string, expiration: number) {
|
|
132
|
+
try {
|
|
133
|
+
const policy = {
|
|
134
|
+
Statement: [
|
|
135
|
+
{
|
|
136
|
+
Resource: `${this.cloudfrontDomain}${folderPath}*`,
|
|
137
|
+
Condition: {
|
|
138
|
+
DateLessThan: {
|
|
139
|
+
'AWS:EpochTime': Math.floor((Date.now() + expiration) / 1000) //new Date(Date.now() + expiration).valueOf()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
};
|
|
145
|
+
const cmd: CloudfrontSignInput = {
|
|
146
|
+
keyPairId: this.cloudfrontKeyPairId,
|
|
147
|
+
privateKey: this.privateKey,
|
|
148
|
+
policy: JSON.stringify(policy)
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return getSignedCookies(cmd);
|
|
152
|
+
} catch (error: unknown) {
|
|
153
|
+
console.error(`Error--getPreSignedUrlToReadFolder--msg :: `, error);
|
|
154
|
+
throw new S3Exception(error, {
|
|
155
|
+
operation: 'getPreSignedUrlToReadFolder',
|
|
156
|
+
key: folderPath
|
|
157
|
+
}).getError();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @description Read a file from S3 bucket
|
|
163
|
+
* @param {string} key - S3 object key (file path)
|
|
164
|
+
* @param {string} bucket - S3 bucket name
|
|
165
|
+
* @param {string} encoding - Optional encoding for the file content (default: UTF-8)
|
|
166
|
+
* @returns {Promise<string>} File content as string
|
|
167
|
+
*/
|
|
168
|
+
async readFile(key: string, bucket: string, encoding?: string) {
|
|
169
|
+
try {
|
|
170
|
+
const params: GetObjectCommandInput = {
|
|
171
|
+
Bucket: bucket,
|
|
172
|
+
Key: key
|
|
173
|
+
};
|
|
174
|
+
const file = await this.client.getObject(params);
|
|
175
|
+
return await file.Body?.transformToString(encoding);
|
|
176
|
+
} catch (error: unknown) {
|
|
177
|
+
console.error(`Error--readFile--msg :: `, error);
|
|
178
|
+
throw new S3Exception(error, {
|
|
179
|
+
operation: 'readFile',
|
|
180
|
+
bucket,
|
|
181
|
+
key
|
|
182
|
+
}).getError();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @description Delete a file from S3 bucket
|
|
188
|
+
* @param {string} bucket - S3 bucket name
|
|
189
|
+
* @param {string} key - S3 object key (file path)
|
|
190
|
+
* @returns {Promise<object>} Delete operation result
|
|
191
|
+
*/
|
|
192
|
+
async deleteFile(bucket: string,key: string) {
|
|
193
|
+
try {
|
|
194
|
+
const params: DeleteObjectCommandInput = {
|
|
195
|
+
Bucket: bucket,
|
|
196
|
+
Key: key
|
|
197
|
+
};
|
|
198
|
+
return await this.client.deleteObject(params);
|
|
199
|
+
} catch (error: unknown) {
|
|
200
|
+
console.error(`Error--deleteFile--msg :: `, error);
|
|
201
|
+
throw new S3Exception(error, {
|
|
202
|
+
operation: 'deleteFile',
|
|
203
|
+
bucket,
|
|
204
|
+
key
|
|
205
|
+
}).getError();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @description Get file content as Base64 encoded string from S3
|
|
211
|
+
* @param {object} fileData - File data object
|
|
212
|
+
* @param {string} fileData.bucket - S3 bucket name
|
|
213
|
+
* @param {string} fileData.path - S3 object key (file path)
|
|
214
|
+
* @returns {Promise<string>} Base64 encoded file content
|
|
215
|
+
* @throws {Error} If file body is empty or unreadable
|
|
216
|
+
*/
|
|
217
|
+
async getFileBase64Data(fileData: { bucket: string; path: string }) {
|
|
218
|
+
try {
|
|
219
|
+
const params: GetObjectCommandInput = {
|
|
220
|
+
Bucket: fileData.bucket,
|
|
221
|
+
Key: fileData.path
|
|
222
|
+
};
|
|
223
|
+
const file = await this.client.send(new GetObjectCommand(params));
|
|
224
|
+
|
|
225
|
+
const bytes = await file.Body?.transformToByteArray();
|
|
226
|
+
|
|
227
|
+
if (!bytes || bytes.length === 0) {
|
|
228
|
+
throw new S3Exception(
|
|
229
|
+
{ message: `File body is empty or unreadable from S3`, Code: 'EmptyFileBody' },
|
|
230
|
+
{
|
|
231
|
+
operation: 'getFileBase64Data',
|
|
232
|
+
bucket: fileData.bucket,
|
|
233
|
+
key: fileData.path
|
|
234
|
+
}
|
|
235
|
+
).getError();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Convert to Base64 string
|
|
239
|
+
return Buffer.from(bytes).toString('base64');
|
|
240
|
+
} catch (error: unknown) {
|
|
241
|
+
console.error(`Error--getFileBase64Data--msg :: `, error);
|
|
242
|
+
throw new S3Exception(error, {
|
|
243
|
+
operation: 'getFileBase64Data',
|
|
244
|
+
bucket: fileData.bucket,
|
|
245
|
+
key: fileData.path
|
|
246
|
+
}).getError();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export const s3Service = new AWSS3Provider();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"incremental": true,
|
|
7
|
+
"noEmit": false, /* Specify what module code is generated. */
|
|
8
|
+
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
|
9
|
+
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
|
10
|
+
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
|
11
|
+
"strict": true, /* Enable all strict type-checking options. */
|
|
12
|
+
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
|
13
|
+
"strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */
|
|
14
|
+
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"experimentalDecorators": true,
|
|
17
|
+
"emitDecoratorMetadata": true,
|
|
18
|
+
"baseUrl": ".",
|
|
19
|
+
}
|
|
20
|
+
}
|