1 | import { S3, Credentials } from 'aws-sdk';
|
2 | import { createWriteStream, readFile } from 'fs-extra-plus';
|
3 | import { PassThrough, Readable, Writable } from "stream";
|
4 | import { Bucket, BucketFile, buildFullDestPath, commonBucketDownload, getContentType, parsePrefixOrGlob, commonBucketCopy, commonDeleteAll, BucketFileDeleted, commonBucketUpload } from "./bucket-base";
|
5 | import micromatch = require('micromatch');
|
6 |
|
7 |
|
8 |
|
9 |
|
10 | type AwsFile = S3.Object;
|
11 |
|
12 | export interface AwsBucketCfg {
|
13 | bucketName: string;
|
14 | access_key_id: string;
|
15 | access_key_secret: string;
|
16 | }
|
17 |
|
18 | export async function getAwsBucket(cfg: AwsBucketCfg) {
|
19 | const credentials = new Credentials(cfg.access_key_id, cfg.access_key_secret);
|
20 |
|
21 | const s3 = new S3({ apiVersion: '2006-03-01', credentials });
|
22 | return new AwsBucket(s3, cfg.bucketName);
|
23 | }
|
24 |
|
25 | class AwsBucket implements Bucket<AwsFile> {
|
26 | private s3: S3;
|
27 | private baseParams: { Bucket: string };
|
28 |
|
29 | get type(): string {
|
30 | return 's3'
|
31 | }
|
32 |
|
33 | get name(): string {
|
34 | return this.baseParams.Bucket;
|
35 | }
|
36 |
|
37 | constructor(s3: S3, bucketName: string) {
|
38 | this.s3 = s3;
|
39 | this.baseParams = { Bucket: bucketName };
|
40 | }
|
41 |
|
42 | getPath(obj: AwsFile) {
|
43 | return obj.Key!;
|
44 | }
|
45 |
|
46 | async exists(path: string): Promise<boolean> {
|
47 | const file = await this.getFile(path);
|
48 | return (file) ? true : false;
|
49 | }
|
50 |
|
51 | async getFile(path: string): Promise<BucketFile | null> {
|
52 | try {
|
53 | const object = await this.s3.headObject({ ...this.baseParams, ...{ Key: path } }).promise();
|
54 | const updated = (object.LastModified) ? object.LastModified.toISOString() : undefined;
|
55 | return {
|
56 | bucket: this,
|
57 | path,
|
58 | updated,
|
59 | size: object.ContentLength,
|
60 | contentType: object.ContentType
|
61 | }
|
62 | } catch (ex) {
|
63 |
|
64 | if (ex.code === 'NotFound') {
|
65 | return null;
|
66 | }
|
67 |
|
68 | else {
|
69 | throw ex;
|
70 | }
|
71 | }
|
72 |
|
73 | }
|
74 |
|
75 | |
76 |
|
77 |
|
78 |
|
79 | async list(prefixOrGlob?: string): Promise<BucketFile[]> {
|
80 | const awsFiles = await this.listAwsFiles(prefixOrGlob);
|
81 |
|
82 | return awsFiles.map(gf => this.toFile(gf));
|
83 | }
|
84 |
|
85 | async copy(pathOrGlob: string, destDir: string | BucketFile): Promise<void> {
|
86 | const awsFiles = await this.listAwsFiles(pathOrGlob);
|
87 |
|
88 | const files = await commonBucketCopy(this, awsFiles, pathOrGlob, destDir,
|
89 | async (awsFile: AWS.S3.Object, dest: BucketFile) => {
|
90 | const destAwsBucket = (dest.bucket instanceof AwsBucket) ? dest.bucket as AwsBucket : null;
|
91 | if (!destAwsBucket) {
|
92 | throw new Error(`destBucket type ${dest.bucket.type} does not match source bucket type ${this.type}. For now, cross bucket type copy not supported.`)
|
93 | }
|
94 | const sourcePath = awsFile.Key!;
|
95 | const params = {
|
96 | CopySource: `${this.name}/${sourcePath}`,
|
97 | Bucket: destAwsBucket.name,
|
98 | Key: dest.path
|
99 | }
|
100 | await this.s3.copyObject(params).promise();
|
101 | }
|
102 | );
|
103 | }
|
104 |
|
105 | async download(pathOrGlob: string, localPath: string): Promise<BucketFile[]> {
|
106 | const awsFiles = await this.listAwsFiles(pathOrGlob);
|
107 |
|
108 | const files = await commonBucketDownload(this, awsFiles, pathOrGlob, localPath,
|
109 | async (object: AwsFile, localPath) => {
|
110 | const remotePath = object.Key!;
|
111 | const params = { ...this.baseParams, ...{ Key: remotePath } };
|
112 | const remoteReadStream = this.s3.getObject(params).createReadStream();
|
113 | const localWriteStream = createWriteStream(localPath);
|
114 | const writePromise = new Promise((resolve, reject) => {
|
115 | localWriteStream.once('close', () => {
|
116 | resolve();
|
117 | });
|
118 | localWriteStream.once('error', (ex) => {
|
119 | reject(ex);
|
120 | });
|
121 | remoteReadStream.pipe(localWriteStream);
|
122 | });
|
123 |
|
124 | await writePromise;
|
125 |
|
126 | });
|
127 |
|
128 | return files;
|
129 | }
|
130 |
|
131 | async downloadAsText(path: string): Promise<string> {
|
132 | const params = { ...this.baseParams, ...{ Key: path } };
|
133 |
|
134 | const obj = await this.s3.getObject(params).promise();
|
135 | const content = obj.Body!.toString();
|
136 | return content;
|
137 | }
|
138 |
|
139 | async upload(localFileOrDirOrGlob: string, destPath: string): Promise<BucketFile[]> {
|
140 | return commonBucketUpload(this, localFileOrDirOrGlob, destPath,
|
141 | async (localPath, fullDestPath, contentType) => {
|
142 | const localFileData = await readFile(localPath);
|
143 | const awsResult = await this.s3.putObject({ ...this.baseParams, ...{ Key: fullDestPath, Body: localFileData, ContentType: contentType } }).promise();
|
144 | return { bucket: this, path: fullDestPath, size: localFileData.length };
|
145 | });
|
146 | }
|
147 |
|
148 | async uploadOld(localPath: string, destPath: string): Promise<BucketFile> {
|
149 |
|
150 | const fullDestPath = buildFullDestPath(localPath, destPath);
|
151 |
|
152 | process.stdout.write(`Uploading file ${localPath} to s3://${this.name}/${fullDestPath}`);
|
153 |
|
154 | try {
|
155 | const localFileData = await readFile(localPath);
|
156 | const ContentType = getContentType(destPath);
|
157 | const awsResult = await this.s3.putObject({ ...this.baseParams, ...{ Key: fullDestPath, Body: localFileData, ContentType } }).promise();
|
158 | process.stdout.write(` - DONE\n`);
|
159 |
|
160 | return { bucket: this, path: fullDestPath, size: localFileData.length };
|
161 | } catch (ex) {
|
162 | process.stdout.write(` - FAIL - ABORT - Cause: ${ex}\n`);
|
163 | throw ex;
|
164 | }
|
165 |
|
166 | }
|
167 |
|
168 | async uploadContent(path: string, content: string): Promise<void> {
|
169 | const ContentType = getContentType(path);
|
170 | await this.s3.putObject({ ...this.baseParams, ...{ Key: path, Body: content, ContentType: ContentType } }).promise();
|
171 | }
|
172 |
|
173 | async createReadStream(path: string): Promise<Readable> {
|
174 | const params = { ...this.baseParams, ...{ Key: path } };
|
175 | const obj = this.s3.getObject(params);
|
176 |
|
177 | if (!obj) {
|
178 | throw new Error(`Object not found for ${path}`);
|
179 | }
|
180 | return obj.createReadStream();
|
181 | }
|
182 |
|
183 | async createWriteStream(path: string): Promise<Writable> {
|
184 | var pass = new PassThrough();
|
185 |
|
186 | const params = { ...this.baseParams, ...{ Key: path }, Body: pass };
|
187 | this.s3.upload(params);
|
188 |
|
189 | return pass;
|
190 | }
|
191 |
|
192 | async delete(path: string): Promise<boolean> {
|
193 | if (!path) {
|
194 | throw new Error(`AwsBucket - ERROR - Can't delete null or empty path`);
|
195 | }
|
196 |
|
197 | try {
|
198 | process.stdout.write(`Deleting s3://${this.baseParams.Bucket}/${path}`);
|
199 |
|
200 |
|
201 | const exists = await this.exists(path);
|
202 | if (exists) {
|
203 |
|
204 |
|
205 | await this.s3.deleteObject({ ...this.baseParams, ...{ Key: path } }).promise();
|
206 | process.stdout.write(` - DONE\n`);
|
207 | return true;
|
208 | } else {
|
209 | process.stdout.write(` - Skipped (object not found)\n`);
|
210 | return false;
|
211 | }
|
212 | } catch (ex) {
|
213 | process.stdout.write(` - FAILED - ABORT - Cause ${ex}\n`);
|
214 | throw ex;
|
215 | }
|
216 | }
|
217 |
|
218 |
|
219 | async deleteAll(files: BucketFile[]): Promise<BucketFileDeleted[]> {
|
220 | return await commonDeleteAll(this, files);
|
221 | }
|
222 |
|
223 |
|
224 |
|
225 | |
226 |
|
227 |
|
228 | async listAwsFiles(prefixOrGlob?: string): Promise<AwsFile[]> {
|
229 | const { prefix, glob } = parsePrefixOrGlob(prefixOrGlob);
|
230 |
|
231 |
|
232 | let listParams: { Prefix?: string } | undefined = undefined;
|
233 | if (prefix) {
|
234 | listParams = { Prefix: prefix };
|
235 | }
|
236 | const params = { ...this.baseParams, ...listParams };
|
237 |
|
238 |
|
239 | try {
|
240 | const awsResult = await this.s3.listObjects(params).promise();
|
241 | const awsFiles = awsResult.Contents as AwsFile[];
|
242 |
|
243 |
|
244 | let files: AwsFile[] = (!glob) ? awsFiles : awsFiles.filter(af => micromatch.isMatch(af.Key!, glob));
|
245 |
|
246 | return files;
|
247 | } catch (ex) {
|
248 | throw ex;
|
249 | }
|
250 |
|
251 | }
|
252 |
|
253 | private toFile(awsFile: AwsFile): BucketFile {
|
254 | if (!awsFile) {
|
255 | throw new Error(`No awsFile`);
|
256 | }
|
257 | const updated = (awsFile.LastModified) ? awsFile.LastModified.toISOString() : undefined;
|
258 |
|
259 | return {
|
260 | bucket: this,
|
261 | path: awsFile.Key!,
|
262 | size: awsFile.Size!,
|
263 | updated: updated
|
264 | }
|
265 | }
|
266 |
|
267 |
|
268 | }
|
269 |
|
270 |
|
271 |
|
272 |
|