Skip to content

Commit 8558d63

Browse files
Kazadeabhis3
andauthored
Implement resumable upload status checking in the storage emulator (#9047)
This commit introduces a few fixes: * Checking the status of a resumable upload by sending a PUT with a Content-Length of 0 no longer returns a 400 Bad Request and correctly returns a 308 status code if the upload is incomplete. * Resumable uploads are no longer finalized on the first chunk allowing multiple chunks to be uploaded. * Streaming uploads (uploads of unknown size) should now work correctly Co-authored-by: abhis3 <[email protected]>
1 parent 4b73b96 commit 8558d63

File tree

1 file changed

+42
-3
lines changed

1 file changed

+42
-3
lines changed

src/emulator/storage/apis/gcloud.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { EmulatorLogger } from "../../emulatorLogger";
1313
import { GetObjectResponse, ListObjectsResponse } from "../files";
1414
import type { Request, Response } from "express";
1515
import { parseObjectUploadMultipartRequest } from "../multipart";
16-
import { Upload, UploadNotActiveError } from "../upload";
16+
import { Upload, UploadNotActiveError, UploadStatus } from "../upload";
1717
import { ForbiddenError, NotFoundError } from "../errors";
1818
import { reqBodyToBuffer } from "../../shared/request";
1919
import type { Query } from "express-serve-static-core";
@@ -190,8 +190,47 @@ export function createCloudEndpoints(emulator: StorageEmulator): Router {
190190
const uploadId = req.query.upload_id.toString();
191191
let upload: Upload;
192192
try {
193-
uploadService.continueResumableUpload(uploadId, await reqBodyToBuffer(req));
194-
upload = uploadService.finalizeResumableUpload(uploadId);
193+
upload = uploadService.getResumableUpload(uploadId);
194+
195+
// It's OK to PUT an inactive resumable upload if the Content-Length is 0
196+
// see https://cloud.google.com/storage/docs/performing-resumable-uploads#status-check
197+
if (parseInt(req.headers["content-length"] ?? "0") === 0) {
198+
if (upload.size === 0 || upload.status === UploadStatus.ACTIVE) {
199+
if (upload.size > 0) {
200+
res.header("Range", `bytes=0-${upload.size - 1}`);
201+
}
202+
return res.sendStatus(308);
203+
} else {
204+
const getObjectResponse = await adminStorageLayer.getObject({
205+
bucketId: upload.bucketId,
206+
decodedObjectId: upload.objectId,
207+
});
208+
209+
return res.json(new CloudStorageObjectMetadata(getObjectResponse.metadata));
210+
}
211+
} else {
212+
upload = uploadService.continueResumableUpload(uploadId, await reqBodyToBuffer(req));
213+
214+
const rangeHeader = req.headers["content-range"] ?? "bytes 0-0/*";
215+
const rangeParts = rangeHeader.substring(rangeHeader.indexOf(" ")).split("/");
216+
const range = rangeParts[0];
217+
218+
let rangeTotal = "*";
219+
if (rangeParts.length === 2) {
220+
rangeTotal = rangeParts[1];
221+
}
222+
223+
const end = range.split("-")[1];
224+
225+
if (rangeTotal !== "*" && parseInt(end) >= parseInt(rangeTotal) - 1) {
226+
upload = uploadService.finalizeResumableUpload(uploadId);
227+
} else {
228+
if (upload.size > 0) {
229+
res.header("Range", `bytes=0-${upload.size - 1}`);
230+
}
231+
return res.sendStatus(308);
232+
}
233+
}
195234
} catch (err) {
196235
if (err instanceof NotFoundError) {
197236
return res.sendStatus(404);

0 commit comments

Comments
 (0)