1 | import { Bucket as GoogleBucket, File as GoogleFile, GetFilesOptions, Storage as GoogleStorage } from '@google-cloud/storage';
|
2 | import { Readable, Writable } from "stream";
|
3 | import { Driver, ListCloudFilesOptions } from "./driver";
|
4 | import { BucketFile, BucketType } from './types';
|
5 | import micromatch = require('micromatch');
|
6 |
|
7 | export async function getGsDriver(cfg: GsDriverCfg) {
|
8 |
|
9 | const googleStorageConf = {
|
10 | projectId: cfg.project_id,
|
11 | credentials: {
|
12 | client_email: cfg.client_email,
|
13 | private_key: cfg.private_key
|
14 | }
|
15 | }
|
16 | const storage = new GoogleStorage(googleStorageConf);
|
17 | const googleBucket = storage.bucket(cfg.bucketName);
|
18 | return new GcpDriver(googleBucket);
|
19 | }
|
20 |
|
21 | export interface GsDriverCfg {
|
22 | bucketName: string;
|
23 | project_id: string;
|
24 | client_email: string;
|
25 | private_key: string;
|
26 | }
|
27 |
|
28 | class GcpDriver implements Driver<GoogleFile> {
|
29 | readonly googleBucket: GoogleBucket;
|
30 |
|
31 | get type(): BucketType {
|
32 | return 'gs'
|
33 | }
|
34 |
|
35 | get name(): string {
|
36 | return this.googleBucket.name
|
37 | }
|
38 |
|
39 | constructor(googleBucket: GoogleBucket) {
|
40 | this.googleBucket = googleBucket;
|
41 | }
|
42 |
|
43 | toFile(googleFile: GoogleFile): Omit<BucketFile, 'bucket'> {
|
44 | if (!googleFile) {
|
45 | throw new Error(`No googleFile`);
|
46 | }
|
47 | const size = (googleFile.metadata.size) ? Number(googleFile.metadata.size) : undefined;
|
48 | return {
|
49 | path: googleFile.name,
|
50 | size,
|
51 | updated: googleFile.metadata.updated,
|
52 | contentType: googleFile.metadata.contentType
|
53 | }
|
54 |
|
55 | }
|
56 |
|
57 | getPath(obj: GoogleFile) {
|
58 | return obj.name;
|
59 | }
|
60 |
|
61 | async exists(path: string): Promise<boolean> {
|
62 |
|
63 | const result = await this.googleBucket.file(path).exists();
|
64 | return result[0];
|
65 | }
|
66 |
|
67 | async getCloudFile(path: string): Promise<GoogleFile | null> {
|
68 | const googleFile = this.googleBucket.file(path);
|
69 | try {
|
70 | return (await googleFile.get())[0];
|
71 | } catch (ex) {
|
72 |
|
73 | if (ex.code === 404) {
|
74 | return null;
|
75 | }
|
76 |
|
77 | else {
|
78 | throw ex;
|
79 | }
|
80 | }
|
81 | }
|
82 |
|
83 | |
84 |
|
85 |
|
86 |
|
87 | async listCloudFiles(opts: ListCloudFilesOptions): Promise<GoogleFile[]> {
|
88 | const { prefix, glob, delimiter } = opts;
|
89 |
|
90 |
|
91 | let baseQuery: GetFilesOptions = { autoPaginate: true };
|
92 | if (delimiter === true) {
|
93 | baseQuery.delimiter = '/';
|
94 | }
|
95 | let getListOpts = (prefix) ? { ...baseQuery, prefix } : baseQuery;
|
96 | const result = await this.googleBucket.getFiles(getListOpts);
|
97 | let gfList = result[0] || [];
|
98 |
|
99 |
|
100 |
|
101 | let files: GoogleFile[] = (!glob) ? gfList : gfList.filter(gf => micromatch.isMatch(gf.name, glob));
|
102 |
|
103 | return files;
|
104 | }
|
105 |
|
106 | async downloadCloudFile(cf: GoogleFile, localPath: string): Promise<void> {
|
107 | await cf.download({ destination: localPath });
|
108 | }
|
109 |
|
110 | async uploadCloudFile(localFilePath: string, remoteFilePath: string, contentType?: string): Promise<GoogleFile> {
|
111 | const googleBucket = this.googleBucket;
|
112 | const googleFile = (await googleBucket.upload(localFilePath, { destination: remoteFilePath, contentType }))[0];
|
113 | return googleFile;
|
114 | }
|
115 |
|
116 | async copyCloudFile(cf: GoogleFile, dest: BucketFile): Promise<void> {
|
117 | if (dest.bucket.googleBucket == null) {
|
118 | throw new Error(`destBucket type ${dest.bucket.type} does not match source bucket type ${this.type}. For now, cross bucket type copy not supported.`)
|
119 | }
|
120 | const destGoogleBucket = dest.bucket.googleBucket;
|
121 |
|
122 | const destFile = destGoogleBucket.file(dest.path);
|
123 | await cf.copy(destFile);
|
124 | }
|
125 |
|
126 | async deleteCloudFile(path: string): Promise<boolean> {
|
127 | const googleFile = this.googleBucket.file(path);
|
128 |
|
129 | if (googleFile) {
|
130 | try {
|
131 | await googleFile.delete();
|
132 | } catch (ex) {
|
133 |
|
134 | if (ex.code === 404) {
|
135 | process.stdout.write(` - Skipped (object not found)\n`);
|
136 | return false;
|
137 | } else {
|
138 | process.stdout.write(` - FAILED - ABORT - Cause ${ex}\n`);
|
139 | throw ex;
|
140 | }
|
141 | }
|
142 |
|
143 |
|
144 | return true;
|
145 | } else {
|
146 | return false;
|
147 | }
|
148 | }
|
149 |
|
150 |
|
151 |
|
152 | async downloadAsText(path: string): Promise<string> {
|
153 | const googleFile = this.googleBucket.file(path);
|
154 | const buffer = await googleFile.download();
|
155 | return buffer.toString();
|
156 | }
|
157 |
|
158 |
|
159 | async uploadCloudContent(path: string, content: string, contentType?: string): Promise<void> {
|
160 | const googleFile = this.googleBucket.file(path);
|
161 | const uploadReadable = new Readable();
|
162 | return new Promise(function (resolve, reject) {
|
163 | uploadReadable
|
164 | .pipe(googleFile.createWriteStream({ contentType }))
|
165 | .on('error', function (err: any) {
|
166 | reject(err);
|
167 | })
|
168 | .on('finish', function () {
|
169 | resolve();
|
170 | });
|
171 | uploadReadable.push(content);
|
172 | uploadReadable.push(null);
|
173 | });
|
174 | }
|
175 | async createReadStream(path: string): Promise<Readable> {
|
176 | const googleFile = this.googleBucket.file(path);
|
177 | return googleFile.createReadStream();
|
178 | }
|
179 |
|
180 | async createWriteStream(path: string): Promise<Writable> {
|
181 | const googleFile = this.googleBucket.file(path);
|
182 | return googleFile.createWriteStream();
|
183 | }
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 | }
|
190 |
|
191 |
|
192 |
|
193 |
|