UNPKG

7.04 kBPlain TextView Raw
1import { Bucket as GoogleBucket, File as GoogleFile, Storage as GoogleStorage } from '@google-cloud/storage';
2import { Readable, Writable } from "stream";
3import { Bucket, BucketFile, buildFullDestPath, commonBucketCopy, commonBucketDownload, getContentType, parsePrefixOrGlob, commonDeleteAll, BucketFileDeleted, commonBucketUpload } from "./bucket-base";
4import micromatch = require('micromatch');
5
6export async function getGcpBucket(cfg: GcpBucketCfg) {
7 // TODO: valid cfg
8 const googleStorageConf = {
9 projectId: cfg.project_id,
10 credentials: {
11 client_email: cfg.client_email,
12 private_key: cfg.private_key
13 }
14 }
15 const storage = new GoogleStorage(googleStorageConf);
16 const googleBucket = storage.bucket(cfg.bucketName);
17 return new GcpBucket(googleBucket);
18}
19
20export interface GcpBucketCfg {
21 bucketName: string;
22 project_id: string;
23 client_email: string;
24 private_key: string;
25}
26
27class GcpBucket implements Bucket<GoogleFile> {
28 readonly googleBucket: GoogleBucket;
29
30 get type(): string {
31 return 'gs'
32 }
33
34 get name(): string {
35 return this.googleBucket.name
36 }
37
38 constructor(googleBucket: GoogleBucket) {
39 this.googleBucket = googleBucket;
40 }
41
42 getPath(obj: GoogleFile) {
43 return obj.name;
44 }
45
46 async exists(path: string): Promise<boolean> {
47 // Note: note gcp has specific file.exists method
48 const result = await this.googleBucket.file(path).exists();
49 return result[0];
50 }
51
52 async getFile(path: string): Promise<BucketFile | null> {
53 const googleFile = this.googleBucket.file(path);
54 try {
55 const f = (await googleFile.get())[0];
56 return this.toFile(f);
57 } catch (ex) {
58 // not found return null, as per getFile design.
59 if (ex.code === 404) {
60 return null;
61 }
62 // otherwise, propagate exception
63 else {
64 throw ex;
65 }
66
67 }
68 }
69
70 /**
71 *
72 * @param path prefix path or glob (the string before the first '*' will be used as prefix)
73 */
74 async list(prefixOrGlob?: string): Promise<BucketFile[]> {
75 const googleFiles = await this.listGoogleFiles(prefixOrGlob);
76
77 return googleFiles.map(gf => this.toFile(gf));
78 }
79
80 async copy(pathOrGlob: string, destDir: string | BucketFile): Promise<void> {
81 const gfiles = await this.listGoogleFiles(pathOrGlob);
82
83 const files = await commonBucketCopy(this, gfiles, pathOrGlob, destDir,
84 async (googleFile: GoogleFile, dest: BucketFile) => {
85 const destGcpBucket = (dest.bucket instanceof GcpBucket) ? dest.bucket as GcpBucket : null;
86 if (!destGcpBucket) {
87 throw new Error(`destBucket type ${dest.bucket.type} does not match source bucket type ${this.type}. For now, cross bucket type copy not supported.`)
88 }
89 const destFile = destGcpBucket.googleBucket.file(dest.path);
90 await googleFile.copy(destFile);
91 }
92 );
93
94 }
95
96 async download(pathOrGlob: string, localPath: string): Promise<BucketFile[]> {
97 const googleFiles = await this.listGoogleFiles(pathOrGlob);
98
99 const files = await commonBucketDownload(this, googleFiles, pathOrGlob, localPath,
100 async (gf: GoogleFile, localPath) => {
101 await gf.download({ destination: localPath });
102 });
103
104 return files;
105 }
106
107 async downloadAsText(path: string): Promise<string> {
108 const googleFile = this.googleBucket.file(path);
109 const buffer = await googleFile.download();
110 return buffer.toString();
111 }
112
113 async upload(localFileOrDirOrGlob: string, destPath: string): Promise<BucketFile[]> {
114 return commonBucketUpload(this, localFileOrDirOrGlob, destPath,
115 async (localPath, remoteFilePath, contentType) => {
116 const googleBucket = this.googleBucket;
117 const googleFile = (await googleBucket.upload(localPath, { destination: remoteFilePath, contentType }))[0];
118 return this.toFile(googleFile);
119 });
120 }
121
122 async uploadOld(localPath: string, destPath: string): Promise<BucketFile> {
123 const googleBucket = this.googleBucket;
124
125 const fullDestPath = buildFullDestPath(localPath, destPath);
126 const contentType = getContentType(destPath);
127
128 // TODO: Needs to do
129 process.stdout.write(`Uploading file ${localPath} to gs://${this.name}/${fullDestPath}`);
130 try {
131 const googleFile = (await googleBucket.upload(localPath, { destination: fullDestPath, contentType }))[0];
132 process.stdout.write(' - DONE\n');
133 return this.toFile(googleFile);
134 } catch (ex) {
135 process.stdout.write(` - FAIL - ABORT - Cause: ${ex}`);
136 throw ex;
137 }
138
139 }
140 async uploadContent(path: string, content: string): Promise<void> {
141 const googleFile = this.googleBucket.file(path);
142 const uploadReadable = new Readable();
143 const contentType = getContentType(path);
144 return new Promise(function (resolve, reject) {
145 uploadReadable
146 .pipe(googleFile.createWriteStream({ contentType }))
147 .on('error', function (err: any) {
148 reject(err);
149 })
150 .on('finish', function () {
151 resolve();
152 });
153 uploadReadable.push(content);
154 uploadReadable.push(null);
155 });
156 }
157 async createReadStream(path: string): Promise<Readable> {
158 const googleFile = this.googleBucket.file(path);
159 return googleFile.createReadStream();
160 }
161
162 async createWriteStream(path: string): Promise<Writable> {
163 const googleFile = this.googleBucket.file(path);
164 return googleFile.createWriteStream();
165 }
166
167
168 async delete(path: string): Promise<boolean> {
169 const googleFile = this.googleBucket.file(path);
170 process.stdout.write(`Deleting gs://${this.name}/${path}`);
171
172 if (googleFile) {
173 try {
174 await googleFile.delete();
175 process.stdout.write(` - DONE\n`);
176 } catch (ex) {
177 // if not found, just return false.
178 if (ex.code === 404) {
179 process.stdout.write(` - Skipped (object not found)\n`);
180 return false;
181 } else {
182 process.stdout.write(` - FAILED - ABORT - Cause ${ex}\n`);
183 throw ex;
184 }
185 }
186
187 // TODO: Probably needs to return true only if deleted.
188 return true;
189 } else {
190 return false;
191 }
192 }
193
194 async deleteAll(files: BucketFile[]): Promise<BucketFileDeleted[]> {
195 return await commonDeleteAll(this, files);
196 }
197
198 //#region ---------- Private ----------
199 toFile(this: GcpBucket, googleFile: GoogleFile): BucketFile {
200 if (!googleFile) {
201 throw new Error(`No googleFile`);
202 }
203 const size = (googleFile.metadata.size) ? Number(googleFile.metadata.size) : undefined;
204 return {
205 path: googleFile.name,
206 bucket: this,
207 size,
208 updated: googleFile.metadata.updated,
209 contentType: googleFile.metadata.contentType
210 }
211
212 }
213
214 /**
215 * List the googleFiles for this bucket;
216 */
217 async listGoogleFiles(prefixOrGlob?: string): Promise<GoogleFile[]> {
218 // extract the eventual prefix and glob from param
219 const { prefix, glob } = parsePrefixOrGlob(prefixOrGlob);
220
221 // build the query options and perform the request
222 let baseQuery = { autoPaginate: true };
223 let getListOpts = (prefix) ? { ...baseQuery, prefix } : baseQuery;
224 const result = await this.googleBucket.getFiles(getListOpts);
225 let gfList = result[0] || [];
226
227
228 // if glob, filter the data further
229 let files: GoogleFile[] = (!glob) ? gfList : gfList.filter(gf => micromatch.isMatch(gf.name, glob));
230
231 return files;
232 }
233 //#endregion ---------- /Private ----------
234}
235
236
237
238