@expo/build-tools 1.0.169 → 1.0.171
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/buildErrors/userErrorHandlers.js +3 -3
- package/dist/buildErrors/userErrorHandlers.js.map +1 -1
- package/dist/common/setup.js +2 -5
- package/dist/common/setup.js.map +1 -1
- package/dist/gcs/LoggerStream.d.ts +54 -0
- package/dist/gcs/LoggerStream.js +186 -0
- package/dist/gcs/LoggerStream.js.map +1 -0
- package/dist/gcs/__unit__/gcs.test.d.ts +1 -0
- package/dist/gcs/__unit__/gcs.test.js +338 -0
- package/dist/gcs/__unit__/gcs.test.js.map +1 -0
- package/dist/gcs/client.d.ts +59 -0
- package/dist/gcs/client.js +164 -0
- package/dist/gcs/client.js.map +1 -0
- package/dist/gcs/retry.d.ts +11 -0
- package/dist/gcs/retry.js +51 -0
- package/dist/gcs/retry.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/steps/utils/expoUpdates.js +1 -1
- package/dist/steps/utils/expoUpdates.js.map +1 -1
- package/package.json +5 -4
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const stream_1 = require("stream");
|
|
9
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
10
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
+
const client_1 = __importDefault(require("../client"));
|
|
12
|
+
jest.mock('node-fetch');
|
|
13
|
+
class ErrorWithCode extends Error {
|
|
14
|
+
constructor(message, code) {
|
|
15
|
+
super(message);
|
|
16
|
+
if (code) {
|
|
17
|
+
this._code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
get code() {
|
|
21
|
+
return this._code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
class DNSError extends ErrorWithCode {
|
|
25
|
+
constructor() {
|
|
26
|
+
super(...arguments);
|
|
27
|
+
this._code = 'ENOTFOUND';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const TEST_BUCKET = 'turtle-v2-test';
|
|
31
|
+
let googleApplicationCredentials;
|
|
32
|
+
beforeAll(() => {
|
|
33
|
+
googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
34
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS = path_1.default.join(__dirname, 'mock-credentials.json');
|
|
35
|
+
});
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
process.env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials;
|
|
38
|
+
});
|
|
39
|
+
describe('GCS client', () => {
|
|
40
|
+
describe('uploadWithPresignedURL function', () => {
|
|
41
|
+
it('should throw an error if upload fails', async () => {
|
|
42
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
43
|
+
const res = {
|
|
44
|
+
ok: false,
|
|
45
|
+
status: 500,
|
|
46
|
+
};
|
|
47
|
+
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
48
|
+
const key = 'cat.jpg';
|
|
49
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
50
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
51
|
+
key,
|
|
52
|
+
expirationTime: 30000,
|
|
53
|
+
contentType: 'image/jpeg',
|
|
54
|
+
});
|
|
55
|
+
fetchMock.mockImplementation(async () => res);
|
|
56
|
+
await expect(client_1.default.uploadWithSignedUrl({
|
|
57
|
+
signedUrl,
|
|
58
|
+
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
59
|
+
retryIntervalMs: 500,
|
|
60
|
+
})).rejects.toThrow();
|
|
61
|
+
});
|
|
62
|
+
it('should return stripped URL if successful', async () => {
|
|
63
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
64
|
+
const res = {
|
|
65
|
+
ok: true,
|
|
66
|
+
status: 200,
|
|
67
|
+
};
|
|
68
|
+
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
69
|
+
const key = 'cat.jpg';
|
|
70
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
71
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
72
|
+
key,
|
|
73
|
+
expirationTime: 30000,
|
|
74
|
+
contentType: 'image/jpeg',
|
|
75
|
+
});
|
|
76
|
+
fetchMock.mockImplementation(async () => res);
|
|
77
|
+
const result = await client_1.default.uploadWithSignedUrl({
|
|
78
|
+
signedUrl,
|
|
79
|
+
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
80
|
+
retryIntervalMs: 500,
|
|
81
|
+
});
|
|
82
|
+
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
|
83
|
+
});
|
|
84
|
+
it('should retry upload on DNS error up to 2 times', async () => {
|
|
85
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
86
|
+
const res = {
|
|
87
|
+
ok: true,
|
|
88
|
+
status: 200,
|
|
89
|
+
};
|
|
90
|
+
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
91
|
+
const key = 'cat.jpg';
|
|
92
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
93
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
94
|
+
key,
|
|
95
|
+
expirationTime: 30000,
|
|
96
|
+
contentType: 'image/jpeg',
|
|
97
|
+
});
|
|
98
|
+
fetchMock
|
|
99
|
+
.mockImplementationOnce(async () => {
|
|
100
|
+
throw new DNSError('failed once');
|
|
101
|
+
})
|
|
102
|
+
.mockImplementationOnce(async () => {
|
|
103
|
+
throw new DNSError('failed twice', 'EAI_AGAIN');
|
|
104
|
+
})
|
|
105
|
+
.mockImplementation(async () => res);
|
|
106
|
+
const result = await client_1.default.uploadWithSignedUrl({
|
|
107
|
+
signedUrl,
|
|
108
|
+
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
109
|
+
retryIntervalMs: 500,
|
|
110
|
+
});
|
|
111
|
+
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
|
112
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
113
|
+
});
|
|
114
|
+
it('should retry upload on retriable status codes error up to 2 times', async () => {
|
|
115
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
116
|
+
const res = {
|
|
117
|
+
ok: true,
|
|
118
|
+
status: 200,
|
|
119
|
+
};
|
|
120
|
+
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
121
|
+
const key = 'cat.jpg';
|
|
122
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
123
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
124
|
+
key,
|
|
125
|
+
expirationTime: 30000,
|
|
126
|
+
contentType: 'image/jpeg',
|
|
127
|
+
});
|
|
128
|
+
fetchMock
|
|
129
|
+
.mockImplementationOnce(async () => {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
status: 503,
|
|
133
|
+
};
|
|
134
|
+
})
|
|
135
|
+
.mockImplementationOnce(async () => {
|
|
136
|
+
return {
|
|
137
|
+
ok: false,
|
|
138
|
+
status: 408,
|
|
139
|
+
};
|
|
140
|
+
})
|
|
141
|
+
.mockImplementation(async () => res);
|
|
142
|
+
const result = await client_1.default.uploadWithSignedUrl({
|
|
143
|
+
signedUrl,
|
|
144
|
+
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
145
|
+
retryIntervalMs: 500,
|
|
146
|
+
});
|
|
147
|
+
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
|
148
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
149
|
+
});
|
|
150
|
+
it('should retry upload on retriable status codes and DNS errors error up to 2 times', async () => {
|
|
151
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
152
|
+
const res = {
|
|
153
|
+
ok: true,
|
|
154
|
+
status: 200,
|
|
155
|
+
};
|
|
156
|
+
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
157
|
+
const key = 'cat.jpg';
|
|
158
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
159
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
160
|
+
key,
|
|
161
|
+
expirationTime: 30000,
|
|
162
|
+
contentType: 'image/jpeg',
|
|
163
|
+
});
|
|
164
|
+
fetchMock
|
|
165
|
+
.mockImplementationOnce(async () => {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
status: 503,
|
|
169
|
+
};
|
|
170
|
+
})
|
|
171
|
+
.mockImplementationOnce(async () => {
|
|
172
|
+
throw new DNSError('failed once');
|
|
173
|
+
})
|
|
174
|
+
.mockImplementation(async () => res);
|
|
175
|
+
const result = await client_1.default.uploadWithSignedUrl({
|
|
176
|
+
signedUrl,
|
|
177
|
+
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
178
|
+
retryIntervalMs: 500,
|
|
179
|
+
});
|
|
180
|
+
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');
|
|
181
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
182
|
+
});
|
|
183
|
+
it('should not retry upload on DNS error more than 2 times', async () => {
|
|
184
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
185
|
+
const res = {
|
|
186
|
+
ok: true,
|
|
187
|
+
status: 200,
|
|
188
|
+
};
|
|
189
|
+
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
190
|
+
const key = 'cat.jpg';
|
|
191
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
192
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
193
|
+
key,
|
|
194
|
+
expirationTime: 30000,
|
|
195
|
+
contentType: 'image/jpeg',
|
|
196
|
+
});
|
|
197
|
+
const lastDNSError = new DNSError('failed thrice');
|
|
198
|
+
fetchMock
|
|
199
|
+
.mockImplementationOnce(async () => {
|
|
200
|
+
throw new DNSError('failed once');
|
|
201
|
+
})
|
|
202
|
+
.mockImplementationOnce(async () => {
|
|
203
|
+
throw new DNSError('failed twice', 'EAI_AGAIN');
|
|
204
|
+
})
|
|
205
|
+
.mockImplementationOnce(async () => {
|
|
206
|
+
throw lastDNSError;
|
|
207
|
+
})
|
|
208
|
+
.mockImplementation(async () => res);
|
|
209
|
+
await expect(client_1.default.uploadWithSignedUrl({
|
|
210
|
+
signedUrl,
|
|
211
|
+
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
212
|
+
retryIntervalMs: 500,
|
|
213
|
+
})).rejects.toThrow(lastDNSError);
|
|
214
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
215
|
+
});
|
|
216
|
+
it('should not retry upload on retriable status code more than 2 times', async () => {
|
|
217
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
218
|
+
const res = {
|
|
219
|
+
ok: true,
|
|
220
|
+
status: 200,
|
|
221
|
+
};
|
|
222
|
+
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
223
|
+
const key = 'cat.jpg';
|
|
224
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
225
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
226
|
+
key,
|
|
227
|
+
expirationTime: 30000,
|
|
228
|
+
contentType: 'image/jpeg',
|
|
229
|
+
});
|
|
230
|
+
fetchMock
|
|
231
|
+
.mockImplementationOnce(async () => {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
status: 503,
|
|
235
|
+
};
|
|
236
|
+
})
|
|
237
|
+
.mockImplementationOnce(async () => {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
status: 408,
|
|
241
|
+
};
|
|
242
|
+
})
|
|
243
|
+
.mockImplementationOnce(async () => {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
status: 504,
|
|
247
|
+
};
|
|
248
|
+
})
|
|
249
|
+
.mockImplementation(async () => res);
|
|
250
|
+
await expect(client_1.default.uploadWithSignedUrl({
|
|
251
|
+
signedUrl,
|
|
252
|
+
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
253
|
+
retryIntervalMs: 500,
|
|
254
|
+
})).rejects.toThrow();
|
|
255
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
256
|
+
});
|
|
257
|
+
it('should not retry upload on other error codes', async () => {
|
|
258
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
259
|
+
const res = {
|
|
260
|
+
ok: true,
|
|
261
|
+
status: 200,
|
|
262
|
+
};
|
|
263
|
+
const localImagePath = path_1.default.join(__dirname, 'cat.jpg');
|
|
264
|
+
const key = 'cat.jpg';
|
|
265
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
266
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
267
|
+
key,
|
|
268
|
+
expirationTime: 30000,
|
|
269
|
+
contentType: 'image/jpeg',
|
|
270
|
+
});
|
|
271
|
+
const nonDNSError = new ErrorWithCode('failed once', 'A_DIFFERENT_CODE');
|
|
272
|
+
fetchMock
|
|
273
|
+
.mockImplementationOnce(async () => {
|
|
274
|
+
throw nonDNSError;
|
|
275
|
+
})
|
|
276
|
+
.mockImplementation(async () => res);
|
|
277
|
+
await expect(client_1.default.uploadWithSignedUrl({
|
|
278
|
+
signedUrl,
|
|
279
|
+
srcGeneratorAsync: async () => fs_extra_1.default.createReadStream(localImagePath),
|
|
280
|
+
retryIntervalMs: 500,
|
|
281
|
+
})).rejects.toThrow(nonDNSError);
|
|
282
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
283
|
+
});
|
|
284
|
+
it('retries upload with full stream', async () => {
|
|
285
|
+
const fetchMock = jest.mocked(node_fetch_1.default);
|
|
286
|
+
const receivedBodies = [];
|
|
287
|
+
const recordBody = async (request) => {
|
|
288
|
+
let body = Buffer.from([]);
|
|
289
|
+
for await (const chunk of request.body) {
|
|
290
|
+
body = Buffer.concat([body, chunk]);
|
|
291
|
+
}
|
|
292
|
+
receivedBodies.push(body);
|
|
293
|
+
};
|
|
294
|
+
fetchMock
|
|
295
|
+
.mockImplementationOnce(async (_path, request) => {
|
|
296
|
+
await recordBody(request);
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
status: 503,
|
|
300
|
+
};
|
|
301
|
+
})
|
|
302
|
+
.mockImplementationOnce(async (_path, request) => {
|
|
303
|
+
await recordBody(request);
|
|
304
|
+
throw new DNSError('failed once');
|
|
305
|
+
})
|
|
306
|
+
.mockImplementation(async (_path, request) => {
|
|
307
|
+
await recordBody(request);
|
|
308
|
+
return {
|
|
309
|
+
ok: true,
|
|
310
|
+
status: 201,
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
const gcs = new client_1.default(TEST_BUCKET);
|
|
314
|
+
const signedUrl = await gcs.createSignedUploadUrl({
|
|
315
|
+
key: 'text.txt',
|
|
316
|
+
expirationTime: 30000,
|
|
317
|
+
contentType: 'text/plain',
|
|
318
|
+
});
|
|
319
|
+
const bufferToUpload = (0, crypto_1.randomBytes)(16);
|
|
320
|
+
const result = await client_1.default.uploadWithSignedUrl({
|
|
321
|
+
signedUrl,
|
|
322
|
+
srcGeneratorAsync: async () => stream_1.Readable.from(bufferToUpload),
|
|
323
|
+
retryIntervalMs: 500,
|
|
324
|
+
});
|
|
325
|
+
expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/text.txt');
|
|
326
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
327
|
+
expect(receivedBodies.length).toBe(3);
|
|
328
|
+
for (const body of receivedBodies) {
|
|
329
|
+
// Here we're testing that each body received in every retry is the same.
|
|
330
|
+
// If we passed the same body instance to each of the retries
|
|
331
|
+
// each retry would consume a chunk of the same stream,
|
|
332
|
+
// causing the uploaded file to be corrupted (missing first bytes).
|
|
333
|
+
expect(body).toStrictEqual(bufferToUpload);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
//# sourceMappingURL=gcs.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gcs.test.js","sourceRoot":"","sources":["../../../src/gcs/__unit__/gcs.test.ts"],"names":[],"mappings":"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","sourcesContent":["import path from 'path';\nimport { randomBytes } from 'crypto';\nimport { Readable } from 'stream';\n\nimport fetch, { RequestInit, Response } from 'node-fetch';\nimport fs from 'fs-extra';\n\nimport GCS from '../client';\n\njest.mock('node-fetch');\n\nclass ErrorWithCode extends Error {\n protected readonly _code: string | undefined;\n\n constructor(message: string, code?: string) {\n super(message);\n if (code) {\n this._code = code;\n }\n }\n\n public get code(): string | undefined {\n return this._code;\n }\n}\n\nclass DNSError extends ErrorWithCode {\n protected readonly _code: 'ENOTFOUND' | 'EAI_AGAIN' = 'ENOTFOUND';\n}\n\nconst TEST_BUCKET = 'turtle-v2-test';\n\nlet googleApplicationCredentials: string | undefined;\nbeforeAll(() => {\n googleApplicationCredentials = process.env.GOOGLE_APPLICATION_CREDENTIALS;\n process.env.GOOGLE_APPLICATION_CREDENTIALS = path.join(__dirname, 'mock-credentials.json');\n});\n\nafterAll(() => {\n process.env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials;\n});\n\ndescribe('GCS client', () => {\n describe('uploadWithPresignedURL function', () => {\n it('should throw an error if upload fails', async () => {\n const fetchMock = jest.mocked(fetch);\n const res = {\n ok: false,\n status: 500,\n } as Response;\n\n const localImagePath = path.join(__dirname, 'cat.jpg');\n const key = 'cat.jpg';\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key,\n expirationTime: 30000,\n contentType: 'image/jpeg',\n });\n\n fetchMock.mockImplementation(async () => res);\n await expect(\n GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => fs.createReadStream(localImagePath),\n retryIntervalMs: 500,\n })\n ).rejects.toThrow();\n });\n\n it('should return stripped URL if successful', async () => {\n const fetchMock = jest.mocked(fetch);\n const res = {\n ok: true,\n status: 200,\n } as Response;\n\n const localImagePath = path.join(__dirname, 'cat.jpg');\n const key = 'cat.jpg';\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key,\n expirationTime: 30000,\n contentType: 'image/jpeg',\n });\n\n fetchMock.mockImplementation(async () => res);\n const result = await GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => fs.createReadStream(localImagePath),\n retryIntervalMs: 500,\n });\n expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');\n });\n\n it('should retry upload on DNS error up to 2 times', async () => {\n const fetchMock = jest.mocked(fetch);\n const res = {\n ok: true,\n status: 200,\n } as Response;\n\n const localImagePath = path.join(__dirname, 'cat.jpg');\n const key = 'cat.jpg';\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key,\n expirationTime: 30000,\n contentType: 'image/jpeg',\n });\n\n fetchMock\n .mockImplementationOnce(async () => {\n throw new DNSError('failed once');\n })\n .mockImplementationOnce(async () => {\n throw new DNSError('failed twice', 'EAI_AGAIN');\n })\n .mockImplementation(async () => res);\n const result = await GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => fs.createReadStream(localImagePath),\n retryIntervalMs: 500,\n });\n expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');\n expect(fetchMock).toHaveBeenCalledTimes(3);\n });\n\n it('should retry upload on retriable status codes error up to 2 times', async () => {\n const fetchMock = jest.mocked(fetch);\n const res = {\n ok: true,\n status: 200,\n } as Response;\n\n const localImagePath = path.join(__dirname, 'cat.jpg');\n const key = 'cat.jpg';\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key,\n expirationTime: 30000,\n contentType: 'image/jpeg',\n });\n\n fetchMock\n .mockImplementationOnce(async () => {\n return {\n ok: false,\n status: 503,\n } as Response;\n })\n .mockImplementationOnce(async () => {\n return {\n ok: false,\n status: 408,\n } as Response;\n })\n .mockImplementation(async () => res);\n const result = await GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => fs.createReadStream(localImagePath),\n retryIntervalMs: 500,\n });\n expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');\n expect(fetchMock).toHaveBeenCalledTimes(3);\n });\n\n it('should retry upload on retriable status codes and DNS errors error up to 2 times', async () => {\n const fetchMock = jest.mocked(fetch);\n const res = {\n ok: true,\n status: 200,\n } as Response;\n\n const localImagePath = path.join(__dirname, 'cat.jpg');\n const key = 'cat.jpg';\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key,\n expirationTime: 30000,\n contentType: 'image/jpeg',\n });\n\n fetchMock\n .mockImplementationOnce(async () => {\n return {\n ok: false,\n status: 503,\n } as Response;\n })\n .mockImplementationOnce(async () => {\n throw new DNSError('failed once');\n })\n .mockImplementation(async () => res);\n const result = await GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => fs.createReadStream(localImagePath),\n retryIntervalMs: 500,\n });\n expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/cat.jpg');\n expect(fetchMock).toHaveBeenCalledTimes(3);\n });\n\n it('should not retry upload on DNS error more than 2 times', async () => {\n const fetchMock = jest.mocked(fetch);\n const res = {\n ok: true,\n status: 200,\n } as Response;\n\n const localImagePath = path.join(__dirname, 'cat.jpg');\n const key = 'cat.jpg';\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key,\n expirationTime: 30000,\n contentType: 'image/jpeg',\n });\n\n const lastDNSError = new DNSError('failed thrice');\n fetchMock\n .mockImplementationOnce(async () => {\n throw new DNSError('failed once');\n })\n .mockImplementationOnce(async () => {\n throw new DNSError('failed twice', 'EAI_AGAIN');\n })\n .mockImplementationOnce(async () => {\n throw lastDNSError;\n })\n .mockImplementation(async () => res);\n await expect(\n GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => fs.createReadStream(localImagePath),\n retryIntervalMs: 500,\n })\n ).rejects.toThrow(lastDNSError);\n expect(fetchMock).toHaveBeenCalledTimes(3);\n });\n\n it('should not retry upload on retriable status code more than 2 times', async () => {\n const fetchMock = jest.mocked(fetch);\n const res = {\n ok: true,\n status: 200,\n } as Response;\n\n const localImagePath = path.join(__dirname, 'cat.jpg');\n const key = 'cat.jpg';\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key,\n expirationTime: 30000,\n contentType: 'image/jpeg',\n });\n\n fetchMock\n .mockImplementationOnce(async () => {\n return {\n ok: false,\n status: 503,\n } as Response;\n })\n .mockImplementationOnce(async () => {\n return {\n ok: false,\n status: 408,\n } as Response;\n })\n .mockImplementationOnce(async () => {\n return {\n ok: false,\n status: 504,\n } as Response;\n })\n .mockImplementation(async () => res);\n await expect(\n GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => fs.createReadStream(localImagePath),\n retryIntervalMs: 500,\n })\n ).rejects.toThrow();\n expect(fetchMock).toHaveBeenCalledTimes(3);\n });\n\n it('should not retry upload on other error codes', async () => {\n const fetchMock = jest.mocked(fetch);\n const res = {\n ok: true,\n status: 200,\n } as Response;\n\n const localImagePath = path.join(__dirname, 'cat.jpg');\n const key = 'cat.jpg';\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key,\n expirationTime: 30000,\n contentType: 'image/jpeg',\n });\n\n const nonDNSError = new ErrorWithCode('failed once', 'A_DIFFERENT_CODE');\n fetchMock\n .mockImplementationOnce(async () => {\n throw nonDNSError;\n })\n .mockImplementation(async () => res);\n await expect(\n GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => fs.createReadStream(localImagePath),\n retryIntervalMs: 500,\n })\n ).rejects.toThrow(nonDNSError);\n expect(fetchMock).toHaveBeenCalledTimes(1);\n });\n\n it('retries upload with full stream', async () => {\n const fetchMock = jest.mocked(fetch);\n const receivedBodies: Buffer[] = [];\n const recordBody = async (request: RequestInit | undefined): Promise<void> => {\n let body = Buffer.from([]);\n for await (const chunk of request!.body as Readable) {\n body = Buffer.concat([body, chunk]);\n }\n receivedBodies.push(body);\n };\n fetchMock\n .mockImplementationOnce(async (_path, request) => {\n await recordBody(request);\n return {\n ok: false,\n status: 503,\n } as Response;\n })\n .mockImplementationOnce(async (_path, request) => {\n await recordBody(request);\n throw new DNSError('failed once');\n })\n .mockImplementation(async (_path, request) => {\n await recordBody(request);\n return {\n ok: true,\n status: 201,\n } as Response;\n });\n\n const gcs = new GCS(TEST_BUCKET);\n const signedUrl = await gcs.createSignedUploadUrl({\n key: 'text.txt',\n expirationTime: 30000,\n contentType: 'text/plain',\n });\n\n const bufferToUpload = randomBytes(16);\n\n const result = await GCS.uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync: async () => Readable.from(bufferToUpload),\n retryIntervalMs: 500,\n });\n expect(result).toEqual('https://storage.googleapis.com/turtle-v2-test/text.txt');\n expect(fetchMock).toHaveBeenCalledTimes(3);\n expect(receivedBodies.length).toBe(3);\n for (const body of receivedBodies) {\n // Here we're testing that each body received in every retry is the same.\n // If we passed the same body instance to each of the retries\n // each retry would consume a chunk of the same stream,\n // causing the uploaded file to be corrupted (missing first bytes).\n expect(body).toStrictEqual(bufferToUpload);\n }\n });\n });\n});\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
import { CreateWriteStreamOptions, File } from '@google-cloud/storage';
|
|
4
|
+
import { RetryOptions } from './retry';
|
|
5
|
+
interface SignedUrlParams {
|
|
6
|
+
key: string;
|
|
7
|
+
expirationTime: number;
|
|
8
|
+
contentType?: string;
|
|
9
|
+
extensionHeaders?: {
|
|
10
|
+
[key: string]: number | string | string[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface UploadWithSignedUrlParams {
|
|
14
|
+
signedUrl: GCS.SignedUrl;
|
|
15
|
+
srcGeneratorAsync: () => Promise<Readable>;
|
|
16
|
+
retryIntervalMs?: RetryOptions['retryIntervalMs'];
|
|
17
|
+
retries?: RetryOptions['retries'];
|
|
18
|
+
}
|
|
19
|
+
declare class GCS {
|
|
20
|
+
private readonly bucket;
|
|
21
|
+
private readonly client;
|
|
22
|
+
constructor(bucket: string);
|
|
23
|
+
static uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries, retryIntervalMs, }: UploadWithSignedUrlParams): Promise<string>;
|
|
24
|
+
formatHttpUrl(key: string): string;
|
|
25
|
+
uploadFile({ key, src, streamOptions, }: {
|
|
26
|
+
key: string;
|
|
27
|
+
src: Readable;
|
|
28
|
+
streamOptions?: CreateWriteStreamOptions;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
Location: string;
|
|
31
|
+
}>;
|
|
32
|
+
deleteFile(key: string): Promise<void>;
|
|
33
|
+
createSignedUploadUrl({ key, expirationTime, contentType, extensionHeaders, }: SignedUrlParams): Promise<GCS.SignedUrl>;
|
|
34
|
+
createSignedDownloadUrl({ key, expirationTime, }: {
|
|
35
|
+
key: string;
|
|
36
|
+
expirationTime: number;
|
|
37
|
+
}): Promise<string>;
|
|
38
|
+
checkIfFileExists(key: string, fileHash?: string): Promise<boolean>;
|
|
39
|
+
listDirectory(prefix: string): Promise<string[]>;
|
|
40
|
+
moveFile(src: string, dest: string): Promise<void>;
|
|
41
|
+
deleteFiles(keys: string[]): Promise<void>;
|
|
42
|
+
downloadFile(key: string, destinationPath: string): Promise<void>;
|
|
43
|
+
getFile(key: string): File;
|
|
44
|
+
}
|
|
45
|
+
declare namespace GCS {
|
|
46
|
+
interface SignedUrl {
|
|
47
|
+
url: string;
|
|
48
|
+
headers: {
|
|
49
|
+
[key: string]: string;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
interface Config {
|
|
53
|
+
accessKeyId: string;
|
|
54
|
+
secretAccessKey: string;
|
|
55
|
+
region: string;
|
|
56
|
+
bucket: string;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export default GCS;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
const node_util_1 = require("node:util");
|
|
30
|
+
const node_stream_1 = __importDefault(require("node:stream"));
|
|
31
|
+
const url_1 = require("url");
|
|
32
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
33
|
+
const storage_1 = require("@google-cloud/storage");
|
|
34
|
+
const node_fetch_1 = __importStar(require("node-fetch"));
|
|
35
|
+
const retry_1 = require("./retry");
|
|
36
|
+
const pipeline = (0, node_util_1.promisify)(node_stream_1.default.pipeline);
|
|
37
|
+
class GCS {
|
|
38
|
+
constructor(bucket) {
|
|
39
|
+
this.bucket = bucket;
|
|
40
|
+
this.client = new storage_1.Storage();
|
|
41
|
+
this.bucket = bucket;
|
|
42
|
+
}
|
|
43
|
+
static async uploadWithSignedUrl({ signedUrl, srcGeneratorAsync, retries = 2, retryIntervalMs = 30000, }) {
|
|
44
|
+
let resp;
|
|
45
|
+
try {
|
|
46
|
+
resp = await (0, retry_1.retryOnGCSUploadFailure)(async () => {
|
|
47
|
+
const src = await srcGeneratorAsync();
|
|
48
|
+
return await (0, node_fetch_1.default)(signedUrl.url, {
|
|
49
|
+
method: 'PUT',
|
|
50
|
+
headers: signedUrl.headers,
|
|
51
|
+
body: src,
|
|
52
|
+
});
|
|
53
|
+
}, {
|
|
54
|
+
retries,
|
|
55
|
+
retryIntervalMs,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (err instanceof node_fetch_1.FetchError) {
|
|
60
|
+
throw new Error(`Failed to upload the file, reason: ${err.code}`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!resp.ok) {
|
|
67
|
+
let body;
|
|
68
|
+
try {
|
|
69
|
+
body = await resp.text();
|
|
70
|
+
}
|
|
71
|
+
catch { }
|
|
72
|
+
throw new Error(`Failed to upload file: status: ${resp.status} status text: ${resp.statusText}, body: ${body}`);
|
|
73
|
+
}
|
|
74
|
+
const url = new url_1.URL(signedUrl.url);
|
|
75
|
+
return `${url.protocol}//${url.host}${url.pathname}`; // strip query string
|
|
76
|
+
}
|
|
77
|
+
formatHttpUrl(key) {
|
|
78
|
+
return this.client.bucket(this.bucket).file(key).publicUrl();
|
|
79
|
+
}
|
|
80
|
+
async uploadFile({ key, src, streamOptions, }) {
|
|
81
|
+
const file = this.client.bucket(this.bucket).file(key);
|
|
82
|
+
await new Promise((res, rej) => {
|
|
83
|
+
src.pipe(file
|
|
84
|
+
.createWriteStream(streamOptions)
|
|
85
|
+
.on('error', (err) => {
|
|
86
|
+
rej(err);
|
|
87
|
+
})
|
|
88
|
+
.on('finish', () => {
|
|
89
|
+
res();
|
|
90
|
+
}));
|
|
91
|
+
});
|
|
92
|
+
return { Location: file.publicUrl() };
|
|
93
|
+
}
|
|
94
|
+
async deleteFile(key) {
|
|
95
|
+
var _a;
|
|
96
|
+
try {
|
|
97
|
+
await this.client.bucket(this.bucket).file(key).delete();
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (((_a = err.response) === null || _a === void 0 ? void 0 : _a.statusCode) === 404) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async createSignedUploadUrl({ key, expirationTime, contentType, extensionHeaders = {}, }) {
|
|
107
|
+
const config = {
|
|
108
|
+
version: 'v4',
|
|
109
|
+
action: 'write',
|
|
110
|
+
expires: Date.now() + expirationTime,
|
|
111
|
+
contentType,
|
|
112
|
+
extensionHeaders,
|
|
113
|
+
};
|
|
114
|
+
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(config);
|
|
115
|
+
return {
|
|
116
|
+
url,
|
|
117
|
+
headers: {
|
|
118
|
+
...(contentType ? { 'content-type': contentType } : {}),
|
|
119
|
+
...extensionHeaders,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async createSignedDownloadUrl({ key, expirationTime, }) {
|
|
124
|
+
const options = {
|
|
125
|
+
version: 'v4',
|
|
126
|
+
action: 'read',
|
|
127
|
+
expires: Date.now() + expirationTime,
|
|
128
|
+
};
|
|
129
|
+
const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(options);
|
|
130
|
+
return url;
|
|
131
|
+
}
|
|
132
|
+
async checkIfFileExists(key, fileHash) {
|
|
133
|
+
let metadata;
|
|
134
|
+
try {
|
|
135
|
+
[metadata] = await this.client.bucket(this.bucket).file(key).getMetadata();
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error.code === 404) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
return fileHash ? metadata.etag === fileHash : true;
|
|
144
|
+
}
|
|
145
|
+
async listDirectory(prefix) {
|
|
146
|
+
const [files] = await this.client.bucket(this.bucket).getFiles({ prefix });
|
|
147
|
+
return files.map((x) => this.formatHttpUrl(x.name));
|
|
148
|
+
}
|
|
149
|
+
async moveFile(src, dest) {
|
|
150
|
+
await this.client.bucket(this.bucket).file(src).move(dest);
|
|
151
|
+
}
|
|
152
|
+
async deleteFiles(keys) {
|
|
153
|
+
await Promise.all(keys.map((key) => this.deleteFile(key)));
|
|
154
|
+
}
|
|
155
|
+
async downloadFile(key, destinationPath) {
|
|
156
|
+
const stream = this.client.bucket(this.bucket).file(key).createReadStream();
|
|
157
|
+
await pipeline(stream, fs_extra_1.default.createWriteStream(destinationPath));
|
|
158
|
+
}
|
|
159
|
+
getFile(key) {
|
|
160
|
+
return this.client.bucket(this.bucket).file(key);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
exports.default = GCS;
|
|
164
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/gcs/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { promisify } from 'node:util';\nimport stream from 'node:stream';\nimport { Readable } from 'stream';\nimport { URL } from 'url';\n\nimport fs from 'fs-extra';\nimport { CreateWriteStreamOptions, File, Storage } from '@google-cloud/storage';\nimport fetch, { FetchError } from 'node-fetch';\n\nimport { RetryOptions, retryOnGCSUploadFailure } from './retry';\n\nconst pipeline = promisify(stream.pipeline);\n\ninterface SignedUrlParams {\n key: string;\n expirationTime: number;\n contentType?: string;\n extensionHeaders?: { [key: string]: number | string | string[] };\n}\n\ninterface UploadWithSignedUrlParams {\n signedUrl: GCS.SignedUrl;\n srcGeneratorAsync: () => Promise<Readable>;\n retryIntervalMs?: RetryOptions['retryIntervalMs'];\n retries?: RetryOptions['retries'];\n}\n\nclass GCS {\n private readonly client = new Storage();\n\n constructor(private readonly bucket: string) {\n this.bucket = bucket;\n }\n\n public static async uploadWithSignedUrl({\n signedUrl,\n srcGeneratorAsync,\n retries = 2,\n retryIntervalMs = 30_000,\n }: UploadWithSignedUrlParams): Promise<string> {\n let resp;\n try {\n resp = await retryOnGCSUploadFailure(\n async () => {\n const src = await srcGeneratorAsync();\n return await fetch(signedUrl.url, {\n method: 'PUT',\n headers: signedUrl.headers,\n body: src,\n });\n },\n {\n retries,\n retryIntervalMs,\n }\n );\n } catch (err: any) {\n if (err instanceof FetchError) {\n throw new Error(`Failed to upload the file, reason: ${err.code}`);\n } else {\n throw err;\n }\n }\n if (!resp.ok) {\n let body: string | undefined;\n try {\n body = await resp.text();\n } catch {}\n throw new Error(\n `Failed to upload file: status: ${resp.status} status text: ${resp.statusText}, body: ${body}`\n );\n }\n const url = new URL(signedUrl.url);\n return `${url.protocol}//${url.host}${url.pathname}`; // strip query string\n }\n\n public formatHttpUrl(key: string): string {\n return this.client.bucket(this.bucket).file(key).publicUrl();\n }\n\n public async uploadFile({\n key,\n src,\n streamOptions,\n }: {\n key: string;\n src: Readable;\n streamOptions?: CreateWriteStreamOptions;\n }): Promise<{ Location: string }> {\n const file = this.client.bucket(this.bucket).file(key);\n await new Promise<void>((res, rej) => {\n src.pipe(\n file\n .createWriteStream(streamOptions)\n .on('error', (err) => {\n rej(err);\n })\n .on('finish', () => {\n res();\n })\n );\n });\n return { Location: file.publicUrl() };\n }\n\n public async deleteFile(key: string): Promise<void> {\n try {\n await this.client.bucket(this.bucket).file(key).delete();\n } catch (err: any) {\n if (err.response?.statusCode === 404) {\n return;\n }\n throw err;\n }\n }\n\n public async createSignedUploadUrl({\n key,\n expirationTime,\n contentType,\n extensionHeaders = {},\n }: SignedUrlParams): Promise<GCS.SignedUrl> {\n const config = {\n version: 'v4' as const,\n action: 'write' as const,\n expires: Date.now() + expirationTime,\n contentType,\n extensionHeaders,\n };\n\n const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(config);\n\n return {\n url,\n headers: {\n ...(contentType ? { 'content-type': contentType } : {}),\n ...extensionHeaders,\n },\n };\n }\n\n public async createSignedDownloadUrl({\n key,\n expirationTime,\n }: {\n key: string;\n expirationTime: number;\n }): Promise<string> {\n const options = {\n version: 'v4' as const,\n action: 'read' as const,\n expires: Date.now() + expirationTime,\n };\n\n const [url] = await this.client.bucket(this.bucket).file(key).getSignedUrl(options);\n\n return url;\n }\n\n public async checkIfFileExists(key: string, fileHash?: string): Promise<boolean> {\n let metadata;\n try {\n [metadata] = await this.client.bucket(this.bucket).file(key).getMetadata();\n } catch (error: any) {\n if (error.code === 404) {\n return false;\n }\n throw error;\n }\n\n return fileHash ? metadata.etag === fileHash : true;\n }\n\n public async listDirectory(prefix: string): Promise<string[]> {\n const [files] = await this.client.bucket(this.bucket).getFiles({ prefix });\n return files.map((x) => this.formatHttpUrl(x.name));\n }\n\n public async moveFile(src: string, dest: string): Promise<void> {\n await this.client.bucket(this.bucket).file(src).move(dest);\n }\n\n public async deleteFiles(keys: string[]): Promise<void> {\n await Promise.all(keys.map((key) => this.deleteFile(key)));\n }\n\n public async downloadFile(key: string, destinationPath: string): Promise<void> {\n const stream = this.client.bucket(this.bucket).file(key).createReadStream();\n await pipeline(stream, fs.createWriteStream(destinationPath));\n }\n\n public getFile(key: string): File {\n return this.client.bucket(this.bucket).file(key);\n }\n}\n\nnamespace GCS {\n export interface SignedUrl {\n url: string;\n headers: { [key: string]: string };\n }\n\n export interface Config {\n accessKeyId: string;\n secretAccessKey: string;\n region: string;\n bucket: string;\n }\n}\n\nexport default GCS;\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Response } from 'node-fetch';
|
|
2
|
+
export interface RetryOptions {
|
|
3
|
+
retries: number;
|
|
4
|
+
retryIntervalMs: number;
|
|
5
|
+
shouldRetryOnError: (error: any) => boolean;
|
|
6
|
+
shouldRetryOnResponse: (response: Response) => boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function retryOnGCSUploadFailure(fn: (attemptCount: number) => Promise<Response>, { retries, retryIntervalMs, }: {
|
|
9
|
+
retries: RetryOptions['retries'];
|
|
10
|
+
retryIntervalMs: RetryOptions['retryIntervalMs'];
|
|
11
|
+
}): Promise<Response>;
|