1 | import { Bucket as GoogleBucket, File as GoogleFile, Storage as GoogleStorage } from '@google-cloud/storage';
|
2 | import { Readable, Writable } from "stream";
|
3 | import { Bucket, BucketFile, buildFullDestPath, commonBucketCopy, commonBucketDownload, getContentType, parsePrefixOrGlob, commonDeleteAll, BucketFileDeleted, commonBucketUpload } from "./bucket-base";
|
4 | import micromatch = require('micromatch');
|
5 |
|
6 | export async function getGcpBucket(cfg: GcpBucketCfg) {
|
7 |
|
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 |
|
20 | export interface GcpBucketCfg {
|
21 | bucketName: string;
|
22 | project_id: string;
|
23 | client_email: string;
|
24 | private_key: string;
|
25 | }
|
26 |
|
27 | class 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 |
|
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 |
|
59 | if (ex.code === 404) {
|
60 | return null;
|
61 | }
|
62 |
|
63 | else {
|
64 | throw ex;
|
65 | }
|
66 |
|
67 | }
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
216 |
|
217 | async listGoogleFiles(prefixOrGlob?: string): Promise<GoogleFile[]> {
|
218 |
|
219 | const { prefix, glob } = parsePrefixOrGlob(prefixOrGlob);
|
220 |
|
221 |
|
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 |
|
229 | let files: GoogleFile[] = (!glob) ? gfList : gfList.filter(gf => micromatch.isMatch(gf.name, glob));
|
230 |
|
231 | return files;
|
232 | }
|
233 |
|
234 | }
|
235 |
|
236 |
|
237 |
|
238 |
|