1 | import { Credentials, S3 } from 'aws-sdk';
|
2 | import { createReadStream, createWriteStream } from 'fs-extra-plus';
|
3 | import { PassThrough, Readable, Writable } from "stream";
|
4 | import { Driver, ListCloudFilesOptions } from "./driver";
|
5 | import { BucketFile, BucketType } from './types';
|
6 |
|
7 | import micromatch = require('micromatch');
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | type AwsFile = S3.Object & { ContentType?: string };
|
13 |
|
14 | export interface S3DriverCfg {
|
15 | bucketName: string;
|
16 | access_key_id: string;
|
17 | access_key_secret: string;
|
18 | }
|
19 |
|
20 | export async function getS3Driver(cfg: S3DriverCfg) {
|
21 | const credentials = new Credentials(cfg.access_key_id, cfg.access_key_secret);
|
22 |
|
23 | const s3 = new S3({ apiVersion: '2006-03-01', credentials });
|
24 | return new S3Driver(s3, cfg.bucketName);
|
25 | }
|
26 |
|
27 | class S3Driver implements Driver<AwsFile> {
|
28 | private s3: S3;
|
29 | private baseParams: { Bucket: string };
|
30 |
|
31 | get type(): BucketType {
|
32 | return 's3'
|
33 | }
|
34 |
|
35 | get name(): string {
|
36 | return this.baseParams.Bucket;
|
37 | }
|
38 |
|
39 | constructor(s3: S3, bucketName: string) {
|
40 | this.s3 = s3;
|
41 | this.baseParams = { Bucket: bucketName };
|
42 | }
|
43 |
|
44 | toFile(awsFile: AwsFile): Omit<BucketFile, 'bucket'> {
|
45 | if (!awsFile) {
|
46 | throw new Error(`No awsFile`);
|
47 | }
|
48 | const updated = (awsFile.LastModified) ? awsFile.LastModified.toISOString() : undefined;
|
49 | return {
|
50 | path: awsFile.Key!,
|
51 | size: awsFile.Size,
|
52 | contentType: awsFile.ContentType,
|
53 | updated: updated
|
54 | }
|
55 | }
|
56 |
|
57 | getPath(obj: AwsFile) {
|
58 | return obj.Key!;
|
59 | }
|
60 |
|
61 |
|
62 | async exists(path: string): Promise<boolean> {
|
63 | const file = await this.getCloudFile(path);
|
64 | return (file) ? true : false;
|
65 | }
|
66 |
|
67 | async getCloudFile(path: string): Promise<AwsFile | null> {
|
68 | try {
|
69 | const object = await this.s3.headObject({ ...this.baseParams, ...{ Key: path } }).promise();
|
70 |
|
71 |
|
72 |
|
73 |
|
74 |
|
75 | const { ContentLength, ContentType, LastModified, ETag } = object;
|
76 | const Key = path;
|
77 | const Size = ContentLength;
|
78 |
|
79 | const awsFile: AwsFile = { Key, Size, LastModified, ETag, ContentType };
|
80 | return awsFile;
|
81 | } catch (ex) {
|
82 |
|
83 | if (ex.code === 'NotFound') {
|
84 | return null;
|
85 | }
|
86 |
|
87 | else {
|
88 | throw ex;
|
89 | }
|
90 | }
|
91 |
|
92 | }
|
93 |
|
94 | async listCloudFiles(opts: ListCloudFilesOptions): Promise<AwsFile[]> {
|
95 |
|
96 | const { prefix, glob, delimiter } = opts;
|
97 |
|
98 |
|
99 | let listParams: { Prefix?: string, Delimiter?: string } = {};
|
100 | if (prefix) {
|
101 | listParams.Prefix = prefix;
|
102 | }
|
103 | if (delimiter) {
|
104 | listParams!.Delimiter = '/';
|
105 | }
|
106 | const params = { ...this.baseParams, ...listParams };
|
107 |
|
108 |
|
109 | try {
|
110 | const awsResult = await this.s3.listObjects(params).promise();
|
111 | const awsFiles = awsResult.Contents as AwsFile[];
|
112 |
|
113 |
|
114 | let files: AwsFile[] = (!glob) ? awsFiles : awsFiles.filter(af => micromatch.isMatch(af.Key!, glob));
|
115 |
|
116 | return files;
|
117 | } catch (ex) {
|
118 | throw ex;
|
119 | }
|
120 |
|
121 | }
|
122 |
|
123 | async copyCloudFile(cf: AwsFile, dest: BucketFile): Promise<void> {
|
124 | if (dest.bucket.type !== this.type) {
|
125 | throw new Error(`destBucket type ${dest.bucket.type} does not match source bucket type ${this.type}. For now, cross bucket type copy not supported.`)
|
126 | }
|
127 | const sourcePath = cf.Key!;
|
128 | const params = {
|
129 | CopySource: `${this.name}/${sourcePath}`,
|
130 | Bucket: dest.bucket.name,
|
131 | Key: dest.path
|
132 | }
|
133 | await this.s3.copyObject(params).promise();
|
134 | }
|
135 |
|
136 |
|
137 | async downloadCloudFile(rawFile: AwsFile, localPath: string): Promise<void> {
|
138 | const remotePath = rawFile.Key!;
|
139 | const params = { ...this.baseParams, ...{ Key: remotePath } };
|
140 | const remoteReadStream = this.s3.getObject(params).createReadStream();
|
141 | const localWriteStream = createWriteStream(localPath);
|
142 | const writePromise = new Promise((resolve, reject) => {
|
143 | localWriteStream.once('close', () => {
|
144 | resolve();
|
145 | });
|
146 | localWriteStream.once('error', (ex) => {
|
147 | reject(ex);
|
148 | });
|
149 | remoteReadStream.pipe(localWriteStream);
|
150 | });
|
151 |
|
152 | await writePromise;
|
153 | }
|
154 |
|
155 | async uploadCloudFile(localPath: string, remoteFilePath: string, contentType?: string): Promise<AwsFile> {
|
156 | const readable = createReadStream(localPath)
|
157 | const awsResult = await this.s3.putObject({ ...this.baseParams, ...{ Key: remoteFilePath, Body: readable, ContentType: contentType } }).promise();
|
158 |
|
159 | return { Key: remoteFilePath };
|
160 |
|
161 | }
|
162 |
|
163 | async downloadAsText(path: string): Promise<string> {
|
164 | const params = { ...this.baseParams, ...{ Key: path } };
|
165 | const obj = await this.s3.getObject(params).promise();
|
166 | const content = obj.Body!.toString();
|
167 | return content;
|
168 | }
|
169 |
|
170 | async uploadCloudContent(path: string, content: string, contentType?: string): Promise<void> {
|
171 |
|
172 | await this.s3.putObject({ ...this.baseParams, ...{ Key: path, Body: content, ContentType: contentType } }).promise();
|
173 | }
|
174 |
|
175 | async createReadStream(path: string): Promise<Readable> {
|
176 | const params = { ...this.baseParams, ...{ Key: path } };
|
177 | const obj = this.s3.getObject(params);
|
178 |
|
179 | if (!obj) {
|
180 | throw new Error(`Object not found for ${path}`);
|
181 | }
|
182 | return obj.createReadStream();
|
183 | }
|
184 |
|
185 | async createWriteStream(path: string): Promise<Writable> {
|
186 | var pass = new PassThrough();
|
187 |
|
188 | const params = { ...this.baseParams, ...{ Key: path }, Body: pass };
|
189 | this.s3.upload(params);
|
190 |
|
191 | return pass;
|
192 | }
|
193 |
|
194 | async deleteCloudFile(path: string): Promise<boolean> {
|
195 |
|
196 |
|
197 | const exists = await this.exists(path);
|
198 | if (exists) {
|
199 |
|
200 |
|
201 | await this.s3.deleteObject({ ...this.baseParams, ...{ Key: path } }).promise();
|
202 | return true;
|
203 | } else {
|
204 | process.stdout.write(` - Skipped (object not found)\n`);
|
205 | return false;
|
206 | }
|
207 |
|
208 | }
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 | }
|
216 |
|
217 |
|
218 |
|
219 |
|