Compare commits

...
2 changed files with 87 additions and 1 deletions

No files matched your search

+76 -1
View File
@@ -3,8 +3,83 @@ import os from 'node:os';
import path from 'node:path';
import fs from 'fs-extra';
import nock from 'nock';
import { computeFileMd5Base64Async } from '../uploads';
import { computeFileMd5Base64Async, uploadWithPresignedPostWithRetryAsync } from '../uploads';
describe(uploadWithPresignedPostWithRetryAsync, () => {
const presignedPost = {
url: 'https://storage.example.com/upload',
fields: { key: 'object-key' },
};
let tmpDir: string;
let file: string;
beforeAll(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-presigned-post-test-'));
file = path.join(tmpDir, 'bundle.js');
await fs.writeFile(file, 'abc');
});
afterAll(async () => {
await fs.remove(tmpDir);
});
afterEach(() => {
nock.cleanAll();
});
it('sends the policy fields and the file', async () => {
let body = '';
nock('https://storage.example.com')
.post('/upload', requestBody => {
body = requestBody;
return true;
})
.reply(204);
const response = await uploadWithPresignedPostWithRetryAsync(file, presignedPost, () => {});
expect(response.status).toBe(204);
expect(body).toContain('object-key');
expect(body).toContain('filename="bundle.js"');
expect(body).toContain('abc');
});
it('accepts the upload when Cloud Storage stored the same MD5', async () => {
nock('https://storage.example.com')
.post('/upload')
.reply(204, '', { 'x-goog-hash': 'crc32c=AAAAAA==,md5=kAFQmDzST7DWlj99KOF/cg==' });
const response = await uploadWithPresignedPostWithRetryAsync(file, presignedPost, () => {});
expect(response.status).toBe(204);
});
it('retries and then fails when Cloud Storage stored a different MD5', async () => {
jest.useFakeTimers({ doNotFake: ['nextTick', 'setImmediate'] });
try {
nock('https://storage.example.com')
.post('/upload')
.times(4)
.reply(204, '', { 'x-goog-hash': 'crc32c=AAAAAA==,md5=AAAAAAAAAAAAAAAAAAAAAA==' });
const onAssetUploadBegin = jest.fn();
const upload = uploadWithPresignedPostWithRetryAsync(file, presignedPost, onAssetUploadBegin);
let settled = false;
upload.catch(() => {}).finally(() => (settled = true));
while (!settled) {
await new Promise(resolve => setImmediate(resolve));
jest.advanceTimersByTime(500);
}
await expect(upload).rejects.toThrow('does not match the local file');
expect(onAssetUploadBegin).toHaveBeenCalledTimes(4);
} finally {
jest.useRealTimers();
}
});
});
describe(computeFileMd5Base64Async, () => {
let tmpDir: string;
+11
View File
@@ -90,6 +90,7 @@ export async function uploadWithPresignedPostWithRetryAsync(
presignedPost: PresignedPost,
onAssetUploadBegin: () => void
): Promise<Response> {
const localMd5 = await computeFileMd5Base64Async(file);
return await promiseRetry(
async retry => {
// retry fetch errors (usually connection or DNS errors)
@@ -115,6 +116,16 @@ export async function uploadWithPresignedPostWithRetryAsync(
throw new Error(`Presigned upload responded with a ${response.status} status`);
}
const storedMd5 = response.headers.get('x-goog-hash')?.match(/md5=([^,]+)/)?.[1];
if (storedMd5 && storedMd5 !== localMd5) {
return retry(
new Error(
`Uploaded file ${file} does not match the local file (Cloud Storage MD5 ${storedMd5}, local MD5 ${localMd5}). ` +
`The file changed between this machine and Cloud Storage. Try again from another network, and contact support if it keeps happening.`
)
);
}
return response;
},
// retry parameters match google suggested defaults: https://cloud.google.com/storage/docs/retry-strategy#node.js