UNPKG

6.33 kBPlain TextView Raw
1import { Credentials, S3 } from 'aws-sdk';
2import { createReadStream, createWriteStream } from 'fs-extra-plus';
3import { PassThrough, Readable, Writable } from "stream";
4import { Driver, ListCloudFilesOptions } from "./driver";
5import { BucketFile, BucketType } from './types';
6
7import micromatch = require('micromatch');
8
9// import {Object as AwsFile} from 'aws-sdk';
10
11// type S3 = AWS.S3;
12type AwsFile = S3.Object & { ContentType?: string };
13
14export interface S3DriverCfg {
15 bucketName: string;
16 access_key_id: string;
17 access_key_secret: string;
18}
19
20export async function getS3Driver(cfg: S3DriverCfg) {
21 const credentials = new Credentials(cfg.access_key_id, cfg.access_key_secret);
22 // Create S3 service object
23 const s3 = new S3({ apiVersion: '2006-03-01', credentials });
24 return new S3Driver(s3, cfg.bucketName);
25}
26
27class 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!; // TODO: need to investigate when Key is empty in S3.
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 // bucket: this,
71 // path,
72 // updated,
73 // size: object.ContentLength,
74 // contentType: object.ContentType
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 // if NotFound, return false
83 if (ex.code === 'NotFound') {
84 return null;
85 }
86 // otherwise, propagate the exception
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 // build the list params
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 // perform the s3 list request
109 try {
110 const awsResult = await this.s3.listObjects(params).promise();
111 const awsFiles = awsResult.Contents as AwsFile[];
112
113 // if glob, filter again the result
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 // TODO: probably check the awsResult that match remoteFilePath
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 // NOTE: For aws API, the s3.deleteObject seems to return exactly the same if the object existed or not.
196 // Therefore, we need to do an additional ping to know if the file exist or not to return true/false
197 const exists = await this.exists(path);
198 if (exists) {
199 // NOTE: between the first test and this delete, the object might have been deleted, but since s3.deleteObjecct
200 // does not seems to tell if the object exits or not, this is the best can do.
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 //#region ---------- Private ----------
210
211
212
213
214 //#endregion ---------- /Private ----------
215}
216
217
218
219