UNPKG

8.58 kBPlain TextView Raw
1import { S3, Credentials } from 'aws-sdk';
2import { createWriteStream, readFile } from 'fs-extra-plus';
3import { PassThrough, Readable, Writable } from "stream";
4import { Bucket, BucketFile, buildFullDestPath, commonBucketDownload, getContentType, parsePrefixOrGlob, commonBucketCopy, commonDeleteAll, BucketFileDeleted, commonBucketUpload } from "./bucket-base";
5import micromatch = require('micromatch');
6
7// import {Object as AwsFile} from 'aws-sdk';
8
9// type S3 = AWS.S3;
10type AwsFile = S3.Object;
11
12export interface AwsBucketCfg {
13 bucketName: string;
14 access_key_id: string;
15 access_key_secret: string;
16}
17
18export async function getAwsBucket(cfg: AwsBucketCfg) {
19 const credentials = new Credentials(cfg.access_key_id, cfg.access_key_secret);
20 // Create S3 service object
21 const s3 = new S3({ apiVersion: '2006-03-01', credentials });
22 return new AwsBucket(s3, cfg.bucketName);
23}
24
25class 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!; // TODO: need to investigate when Key is empty in S3.
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 // if NotFound, return false
64 if (ex.code === 'NotFound') {
65 return null;
66 }
67 // otherwise, propagate the exception
68 else {
69 throw ex;
70 }
71 }
72
73 }
74
75 /**
76 *
77 * @param path prefix path or glob (the string before the first '*' will be used as prefix)
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 //const remoteReadStream = this.s3.getObject(params).createReadStream();
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 // FIXME: Needs to make sure we cannot get the object from the putObject result, and that the size can be assumed to be localFileData.length
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 // NOTE: For aws API, the s3.deleteObject seems to return exactly the same if the object existed or not.
200 // Therefore, we need to do an additional ping to know if the file exist or not to return true/false
201 const exists = await this.exists(path);
202 if (exists) {
203 // NOTE: between the first test and this delete, the object might have been deleted, but since s3.deleteObjecct
204 // does not seems to tell if the object exits or not, this is the best can do.
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 //#region ---------- Private ----------
223
224
225 /**
226 * List the googleFiles for this bucket;
227 */
228 async listAwsFiles(prefixOrGlob?: string): Promise<AwsFile[]> {
229 const { prefix, glob } = parsePrefixOrGlob(prefixOrGlob);
230
231 // build the list params
232 let listParams: { Prefix?: string } | undefined = undefined;
233 if (prefix) {
234 listParams = { Prefix: prefix };
235 }
236 const params = { ...this.baseParams, ...listParams };
237
238 // perform the s3 list request
239 try {
240 const awsResult = await this.s3.listObjects(params).promise();
241 const awsFiles = awsResult.Contents as AwsFile[];
242
243 // if glob, filter again the result
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 // FIXME: Needs to handle when Key or Size is undefined.
259 return {
260 bucket: this,
261 path: awsFile.Key!,
262 size: awsFile.Size!,
263 updated: updated
264 }
265 }
266
267 //#endregion ---------- /Private ----------
268}
269
270
271
272