File
Blob: src/workerd/api/tests/r2-test.js
| 1 | // Copyright (c) 2023 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | import assert from 'node:assert'; |
| 6 | |
| 7 | const key = 'basicKey'; |
| 8 | const body = 'content'; |
| 9 | const httpMetaObj = { |
| 10 | contentType: 'text/plain', |
| 11 | contentLanguage: 'en-US', |
| 12 | contentDisposition: 'attachment; filename = "basicKey.txt"', |
| 13 | contentEncoding: 'utf-8', |
| 14 | cacheControl: 'no-store', |
| 15 | cacheExpiry: new Date(1e3), |
| 16 | }; |
| 17 | const httpFields = { |
| 18 | ...httpMetaObj, |
| 19 | cacheExpiry: '1000', |
| 20 | }; |
| 21 | const httpMetaHeaders = new Headers({ |
| 22 | 'content-type': httpMetaObj.contentType, |
| 23 | 'content-language': httpMetaObj.contentLanguage, |
| 24 | 'content-disposition': httpMetaObj.contentDisposition, |
| 25 | 'content-encoding': httpMetaObj.contentEncoding, |
| 26 | 'cache-control': httpMetaObj.cacheControl, |
| 27 | expires: httpMetaObj.cacheExpiry.toUTCString(), |
| 28 | }); |
| 29 | const customMetadata = { |
| 30 | foo: 'bar', |
| 31 | baz: 'qux', |
| 32 | }; |
| 33 | const customFields = Object.entries(customMetadata).map(([k, v]) => ({ k, v })); |
| 34 | const bufferKey = new Uint8Array([ |
| 35 | 185, 255, 145, 154, 120, 76, 122, 72, 191, 42, 8, 64, 86, 189, 185, 75, 105, |
| 36 | 37, 155, 123, 165, 158, 4, 42, 222, 13, 135, 52, 87, 154, 181, 227, |
| 37 | ]); |
| 38 | const hexKey = |
| 39 | 'b9ff919a784c7a48bf2a084056bdb94b69259b7ba59e042ade0d8734579ab5e3'; |
| 40 | const keyMd5 = 'WGR5pEm07DroP3hYRAh8Yw=='; |
| 41 | const conditionalDate = '946684800000'; |
| 42 | |
| 43 | // Test checksums - known values for testing |
| 44 | const md5Buffer = new Uint8Array([ |
| 45 | 0x9a, 0x03, 0x64, 0xb9, 0xe9, 0x9b, 0xb4, 0x80, 0xdd, 0x25, 0xe1, 0xf0, 0x28, |
| 46 | 0x4c, 0x85, 0x55, |
| 47 | ]); |
| 48 | // Test SHA1 checksum |
| 49 | const sha1Buffer = new Uint8Array([ |
| 50 | 0x2a, 0x03, 0x64, 0xb9, 0xe9, 0x9b, 0xb4, 0x80, 0xdd, 0x25, 0xe1, 0xf0, 0x28, |
| 51 | 0x4c, 0x85, 0x55, 0x11, 0x22, 0x33, 0x44, |
| 52 | ]); |
| 53 | // Test SHA256 checksum |
| 54 | const sha256Buffer = new Uint8Array([ |
| 55 | 0x3a, 0x03, 0x64, 0xb9, 0xe9, 0x9b, 0xb4, 0x80, 0xdd, 0x25, 0xe1, 0xf0, 0x28, |
| 56 | 0x4c, 0x85, 0x55, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, |
| 57 | 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, |
| 58 | ]); |
| 59 | const objResponse = { |
| 60 | name: key, |
| 61 | version: 'objectVersion', |
| 62 | size: '123', |
| 63 | etag: 'objectEtag', |
| 64 | uploaded: '1724767257918', |
| 65 | storageClass: 'Standard', |
| 66 | }; |
| 67 | const HeadObject = { |
| 68 | ssecKeyMd5: undefined, |
| 69 | storageClass: 'Standard', |
| 70 | range: undefined, |
| 71 | customMetadata: {}, |
| 72 | httpMetadata: {}, |
| 73 | uploaded: new Date(Number(objResponse.uploaded)), |
| 74 | checksums: { |
| 75 | sha512: undefined, |
| 76 | sha384: undefined, |
| 77 | sha256: undefined, |
| 78 | sha1: undefined, |
| 79 | md5: undefined, |
| 80 | }, |
| 81 | httpEtag: '"objectEtag"', |
| 82 | etag: 'objectEtag', |
| 83 | size: 123, |
| 84 | version: 'objectVersion', |
| 85 | key, |
| 86 | }; |
| 87 | |
| 88 | function buildGetResponse({ head, body, isList } = {}) { |
| 89 | const encoder = new TextEncoder(); |
| 90 | let meta; |
| 91 | if (!isList) { |
| 92 | meta = { |
| 93 | ...objResponse, |
| 94 | }; |
| 95 | } |
| 96 | meta = { |
| 97 | ...meta, |
| 98 | ...head, |
| 99 | }; |
| 100 | const metadata = encoder.encode(JSON.stringify(meta)); |
| 101 | const responseBody = body |
| 102 | ? new ReadableStream({ |
| 103 | start(controller) { |
| 104 | controller.enqueue(metadata); |
| 105 | controller.enqueue(encoder.encode(body)); |
| 106 | controller.close(); |
| 107 | }, |
| 108 | }) |
| 109 | : metadata; |
| 110 | return new Response(responseBody, { |
| 111 | headers: { |
| 112 | 'cf-r2-metadata-size': metadata.length.toString(), |
| 113 | 'content-length': metadata.length.toString(), |
| 114 | }, |
| 115 | }); |
| 116 | } |
| 117 | async function compareResponse(res, { head, body } = {}, bytes) { |
| 118 | // Destructuring syntax looks ugly, but gets around needing to construct HeadResponse objects(somehow?) |
| 119 | const { ...obj } = await res; |
| 120 | obj.checksums = { ...obj.checksums }; |
| 121 | assert.deepEqual(obj, { |
| 122 | ...HeadObject, |
| 123 | ...head, |
| 124 | }); |
| 125 | if (body) { |
| 126 | if (bytes) { |
| 127 | const expected = new Uint8Array(new TextEncoder().encode(body)); |
| 128 | const actual = await (await res).bytes(); |
| 129 | assert.equal(expected.byteLength, actual.byteLength); |
| 130 | for (let i = 0; i < expected.byteLength; i++) { |
| 131 | assert.equal(expected[i], actual[i]); |
| 132 | } |
| 133 | return; |
| 134 | } |
| 135 | assert.strictEqual(await (await res).text(), body); |
| 136 | } |
| 137 | } |
| 138 | |
| 139 | export default { |
| 140 | // Handler for HTTP request binding makes to R2 |
| 141 | async fetch(request, env, ctx) { |
| 142 | // We only expect PUT/Get |
| 143 | assert(['GET', 'PUT'].includes(request.method)); |
| 144 | |
| 145 | switch (request.method) { |
| 146 | case 'PUT': { |
| 147 | // Each request should have a metadata size header indicating how much |
| 148 | // we should read to understand what type of request this is |
| 149 | const metadataSizeString = request.headers.get('cf-r2-metadata-size'); |
| 150 | assert.notStrictEqual(metadataSizeString, null); |
| 151 | |
| 152 | const metadataSize = parseInt(metadataSizeString); |
| 153 | assert(!Number.isNaN(metadataSize)); |
| 154 | |
| 155 | const reader = request.body.getReader({ mode: 'byob' }); |
| 156 | const jsonArray = new Uint8Array(metadataSize); |
| 157 | const { value } = await reader.readAtLeast(metadataSize, jsonArray); |
| 158 | reader.releaseLock(); |
| 159 | |
| 160 | const jsonRequest = JSON.parse(new TextDecoder().decode(value)); |
| 161 | |
| 162 | // Currently not using the body in these test so I'm going to just discard |
| 163 | for await (const _ of request.body) { |
| 164 | // intentionally empty |
| 165 | } |
| 166 | |
| 167 | // Assert it's the correct version |
| 168 | assert((jsonRequest.version = 1)); |
| 169 | |
| 170 | if (jsonRequest.method === 'delete') { |
| 171 | if (jsonRequest.objects) { |
| 172 | assert.deepEqual(jsonRequest.objects, [key, key + '2']); |
| 173 | } else { |
| 174 | assert.deepEqual(jsonRequest.object, key); |
| 175 | } |
| 176 | return new Response(); |
| 177 | } |
| 178 | |
| 179 | switch (jsonRequest.object) { |
| 180 | case 'basicKey': { |
| 181 | switch (jsonRequest.method) { |
| 182 | case 'put': { |
| 183 | break; |
| 184 | } |
| 185 | case 'createMultipartUpload': { |
| 186 | return Response.json({ |
| 187 | uploadId: 'multipartId', |
| 188 | }); |
| 189 | } |
| 190 | case 'uploadPart': { |
| 191 | return Response.json({ |
| 192 | etag: 'partEtag', |
| 193 | }); |
| 194 | } |
| 195 | case 'abortMultipartUpload': { |
| 196 | return new Response(); |
| 197 | } |
| 198 | case 'completeMultipartUpload': { |
| 199 | return Response.json(objResponse); |
| 200 | } |
| 201 | } |
| 202 | break; |
| 203 | } |
| 204 | case 'onlyIfStrongEtag': { |
| 205 | assert.deepStrictEqual(jsonRequest.onlyIf, { |
| 206 | etagMatches: [ |
| 207 | { |
| 208 | value: 'strongEtag', |
| 209 | type: 'strong', |
| 210 | }, |
| 211 | ], |
| 212 | etagDoesNotMatch: [ |
| 213 | { |
| 214 | value: 'strongEtag', |
| 215 | type: 'strong', |
| 216 | }, |
| 217 | ], |
| 218 | uploadedBefore: conditionalDate, |
| 219 | }); |
| 220 | break; |
| 221 | } |
| 222 | case 'onlyIfWildcard': { |
| 223 | assert.deepStrictEqual(jsonRequest.onlyIf, { |
| 224 | etagMatches: [ |
| 225 | { |
| 226 | type: 'wildcard', |
| 227 | }, |
| 228 | ], |
| 229 | etagDoesNotMatch: [ |
| 230 | { |
| 231 | type: 'wildcard', |
| 232 | }, |
| 233 | ], |
| 234 | uploadedAfter: conditionalDate, |
| 235 | }); |
| 236 | break; |
| 237 | } |
| 238 | case 'httpMetadata': { |
| 239 | if (jsonRequest.method !== 'completeMultipartUpload') { |
| 240 | assert.deepEqual(jsonRequest.httpFields, httpFields); |
| 241 | } |
| 242 | const head = { |
| 243 | ...objResponse, |
| 244 | httpFields, |
| 245 | }; |
| 246 | switch (jsonRequest.method) { |
| 247 | case 'put': |
| 248 | return Response.json(head); |
| 249 | case 'createMultipartUpload': |
| 250 | return Response.json({ |
| 251 | uploadId: 'multipartId', |
| 252 | }); |
| 253 | case 'completeMultipartUpload': { |
| 254 | return Response.json(head); |
| 255 | } |
| 256 | } |
| 257 | } |
| 258 | // falls through |
| 259 | case 'customMetadata': { |
| 260 | if (jsonRequest.method !== 'completeMultipartUpload') { |
| 261 | assert.deepEqual(jsonRequest.customFields, customFields); |
| 262 | } |
| 263 | const head = { |
| 264 | ...objResponse, |
| 265 | customFields, |
| 266 | }; |
| 267 | switch (jsonRequest.method) { |
| 268 | case 'put': |
| 269 | return Response.json(head); |
| 270 | case 'createMultipartUpload': |
| 271 | return Response.json({ |
| 272 | uploadId: 'multipartId', |
| 273 | }); |
| 274 | case 'completeMultipartUpload': |
| 275 | return Response.json(head); |
| 276 | } |
| 277 | } |
| 278 | // falls through |
| 279 | case 'classDefault': { |
| 280 | if (jsonRequest.method !== 'completeMultipartUpload') { |
| 281 | assert.strictEqual(jsonRequest.storageClass, undefined); |
| 282 | } |
| 283 | const head = objResponse; |
| 284 | switch (jsonRequest.method) { |
| 285 | case 'put': |
| 286 | return Response.json(head); |
| 287 | case 'createMultipartUpload': |
| 288 | return Response.json({ |
| 289 | uploadId: 'multipartId', |
| 290 | }); |
| 291 | case 'completeMultipartUpload': |
| 292 | return Response.json(head); |
| 293 | } |
| 294 | } |
| 295 | // falls through |
| 296 | case 'classStandard': { |
| 297 | if (jsonRequest.method !== 'completeMultipartUpload') { |
| 298 | assert.deepEqual(jsonRequest.storageClass, 'Standard'); |
| 299 | } |
| 300 | const head = { |
| 301 | ...objResponse, |
| 302 | storageClass: 'Standard', |
| 303 | }; |
| 304 | switch (jsonRequest.method) { |
| 305 | case 'put': |
| 306 | return Response.json(head); |
| 307 | case 'createMultipartUpload': |
| 308 | return Response.json({ |
| 309 | uploadId: 'multipartId', |
| 310 | }); |
| 311 | case 'completeMultipartUpload': |
| 312 | return Response.json(head); |
| 313 | } |
| 314 | } |
| 315 | // falls through |
| 316 | case 'classInfrequentAccess': { |
| 317 | if (jsonRequest.method !== 'completeMultipartUpload') { |
| 318 | assert.deepEqual(jsonRequest.storageClass, 'InfrequentAccess'); |
| 319 | } |
| 320 | const head = { |
| 321 | ...objResponse, |
| 322 | storageClass: 'InfrequentAccess', |
| 323 | }; |
| 324 | switch (jsonRequest.method) { |
| 325 | case 'put': |
| 326 | return Response.json(head); |
| 327 | case 'createMultipartUpload': |
| 328 | return Response.json({ |
| 329 | uploadId: 'multipartId', |
| 330 | }); |
| 331 | case 'completeMultipartUpload': |
| 332 | return Response.json(head); |
| 333 | } |
| 334 | } |
| 335 | // falls through |
| 336 | case 'ssec': { |
| 337 | assert.deepStrictEqual(jsonRequest.ssec, { |
| 338 | key: hexKey, |
| 339 | }); |
| 340 | return Response.json({ |
| 341 | ...objResponse, |
| 342 | ssec: { |
| 343 | algorithm: 'aes256', |
| 344 | keyMd5, |
| 345 | }, |
| 346 | }); |
| 347 | } |
| 348 | case 'ssecMultipart': { |
| 349 | if (jsonRequest.method === 'createMultipartUpload') { |
| 350 | assert.deepStrictEqual(jsonRequest.ssec, { |
| 351 | key: hexKey, |
| 352 | }); |
| 353 | return Response.json({ |
| 354 | uploadId: 'multipartId', |
| 355 | }); |
| 356 | } |
| 357 | if (jsonRequest.method === 'uploadPart') { |
| 358 | assert.deepStrictEqual(jsonRequest.ssec, { |
| 359 | key: hexKey, |
| 360 | }); |
| 361 | return Response.json({ |
| 362 | etag: 'partEtag', |
| 363 | ssec: { |
| 364 | algorithm: 'aes256', |
| 365 | keyMd5, |
| 366 | }, |
| 367 | }); |
| 368 | } |
| 369 | if (jsonRequest.method === 'completeMultipartUpload') { |
| 370 | return Response.json({ |
| 371 | ...objResponse, |
| 372 | ssec: { |
| 373 | algorithm: 'aes256', |
| 374 | keyMd5, |
| 375 | }, |
| 376 | }); |
| 377 | } |
| 378 | } |
| 379 | // falls through |
| 380 | case 'multipleChecksums': { |
| 381 | const toHex = (buffer) => |
| 382 | Array.from(buffer, (b) => b.toString(16).padStart(2, '0')).join( |
| 383 | '' |
| 384 | ); |
| 385 | return Response.json({ |
| 386 | ...objResponse, |
| 387 | checksums: { |
| 388 | 0: toHex(md5Buffer), // md5 |
| 389 | 1: toHex(sha1Buffer), // sha1 |
| 390 | 2: toHex(sha256Buffer), // sha256 |
| 391 | }, |
| 392 | }); |
| 393 | } |
| 394 | } |
| 395 | return Response.json(objResponse); |
| 396 | } |
| 397 | case 'GET': { |
| 398 | const rawHeader = request.headers.get('cf-r2-request'); |
| 399 | const jsonRequest = JSON.parse(rawHeader); |
| 400 | assert((jsonRequest.version = 1)); |
| 401 | if (jsonRequest.method === 'list') { |
| 402 | switch (jsonRequest.prefix) { |
| 403 | case 'basic': { |
| 404 | assert.deepEqual(jsonRequest, { |
| 405 | cursor: 'ai', |
| 406 | delimiter: '/', |
| 407 | include: [0, 1], |
| 408 | limit: 1, |
| 409 | method: 'list', |
| 410 | newRuntime: true, |
| 411 | prefix: 'basic', |
| 412 | version: 1, |
| 413 | }); |
| 414 | return buildGetResponse({ |
| 415 | head: { |
| 416 | objects: [objResponse], |
| 417 | truncated: true, |
| 418 | cursor: 'ai', |
| 419 | deliminatedPrefixes: [], |
| 420 | }, |
| 421 | isList: true, |
| 422 | }); |
| 423 | } |
| 424 | case 'httpMeta': { |
| 425 | assert.deepEqual(jsonRequest, { |
| 426 | include: [0], |
| 427 | method: 'list', |
| 428 | newRuntime: true, |
| 429 | prefix: 'httpMeta', |
| 430 | version: 1, |
| 431 | }); |
| 432 | |
| 433 | return buildGetResponse({ |
| 434 | head: { |
| 435 | objects: [{ ...objResponse, httpFields, customFields: [] }], |
| 436 | truncated: false, |
| 437 | deliminatedPrefixes: [], |
| 438 | }, |
| 439 | isList: true, |
| 440 | }); |
| 441 | } |
| 442 | case 'customMeta': { |
| 443 | assert.deepEqual(jsonRequest, { |
| 444 | include: [1], |
| 445 | method: 'list', |
| 446 | newRuntime: true, |
| 447 | prefix: 'customMeta', |
| 448 | version: 1, |
| 449 | }); |
| 450 | |
| 451 | return buildGetResponse({ |
| 452 | head: { |
| 453 | objects: [{ ...objResponse, httpFields: {}, customFields }], |
| 454 | truncated: false, |
| 455 | deliminatedPrefixes: [], |
| 456 | }, |
| 457 | isList: true, |
| 458 | }); |
| 459 | } |
| 460 | } |
| 461 | } |
| 462 | assert(['get', 'head'].includes(jsonRequest.method)); |
| 463 | switch (jsonRequest.object) { |
| 464 | case 'basicKey': { |
| 465 | return buildGetResponse({ body }); |
| 466 | } |
| 467 | case 'rangeOffLen': { |
| 468 | assert.deepEqual(jsonRequest.range, { |
| 469 | offset: '1', |
| 470 | length: '3', |
| 471 | }); |
| 472 | return buildGetResponse({ |
| 473 | head: { |
| 474 | range: jsonRequest.range, |
| 475 | }, |
| 476 | body: 'ont', |
| 477 | }); |
| 478 | } |
| 479 | case 'rangeSuff': { |
| 480 | assert.deepEqual(jsonRequest.range, { |
| 481 | suffix: '2', |
| 482 | }); |
| 483 | return buildGetResponse({ |
| 484 | head: { |
| 485 | range: { |
| 486 | offset: '6', |
| 487 | length: '2', |
| 488 | }, |
| 489 | }, |
| 490 | body: 'nt', |
| 491 | }); |
| 492 | } |
| 493 | case 'onlyIfStrongEtag': { |
| 494 | assert.deepStrictEqual(jsonRequest.onlyIf, { |
| 495 | etagMatches: [ |
| 496 | { |
| 497 | value: 'strongEtag', |
| 498 | type: 'strong', |
| 499 | }, |
| 500 | ], |
| 501 | etagDoesNotMatch: [ |
| 502 | { |
| 503 | value: 'strongEtag', |
| 504 | type: 'strong', |
| 505 | }, |
| 506 | ], |
| 507 | uploadedBefore: conditionalDate, |
| 508 | }); |
| 509 | return buildGetResponse({ body }); |
| 510 | } |
| 511 | case 'onlyIfWildcard': { |
| 512 | assert.deepStrictEqual(jsonRequest.onlyIf, { |
| 513 | etagMatches: [ |
| 514 | { |
| 515 | type: 'wildcard', |
| 516 | }, |
| 517 | ], |
| 518 | etagDoesNotMatch: [ |
| 519 | { |
| 520 | type: 'wildcard', |
| 521 | }, |
| 522 | ], |
| 523 | uploadedAfter: conditionalDate, |
| 524 | }); |
| 525 | return buildGetResponse({ body }); |
| 526 | } |
| 527 | case 'httpMetadata': { |
| 528 | const head = { |
| 529 | httpFields, |
| 530 | }; |
| 531 | switch (jsonRequest.method) { |
| 532 | case 'head': |
| 533 | return buildGetResponse({ head }); |
| 534 | case 'get': |
| 535 | return buildGetResponse({ head, body }); |
| 536 | } |
| 537 | } |
| 538 | // falls through |
| 539 | case 'customMetadata': { |
| 540 | const head = { |
| 541 | customFields, |
| 542 | }; |
| 543 | switch (jsonRequest.method) { |
| 544 | case 'head': |
| 545 | return buildGetResponse({ head }); |
| 546 | case 'get': |
| 547 | return buildGetResponse({ head, body }); |
| 548 | } |
| 549 | } |
| 550 | // falls through |
| 551 | case 'classDefault': |
| 552 | // falls through |
| 553 | case 'classStandard': { |
| 554 | const head = { |
| 555 | storageClass: 'Standard', |
| 556 | }; |
| 557 | switch (jsonRequest.method) { |
| 558 | case 'head': |
| 559 | return buildGetResponse({ head }); |
| 560 | case 'get': |
| 561 | return buildGetResponse({ head, body }); |
| 562 | } |
| 563 | } |
| 564 | // falls through |
| 565 | case 'classInfrequentAccess': { |
| 566 | const head = { |
| 567 | storageClass: 'InfrequentAccess', |
| 568 | }; |
| 569 | switch (jsonRequest.method) { |
| 570 | case 'head': |
| 571 | return buildGetResponse({ head }); |
| 572 | case 'get': |
| 573 | return buildGetResponse({ head, body }); |
| 574 | } |
| 575 | } |
| 576 | // falls through |
| 577 | case 'ssec': { |
| 578 | return buildGetResponse({ |
| 579 | head: { |
| 580 | ssec: { |
| 581 | algorithm: 'aes256', |
| 582 | keyMd5, |
| 583 | }, |
| 584 | }, |
| 585 | body, |
| 586 | }); |
| 587 | } |
| 588 | case 'multipleChecksums': { |
| 589 | const toHex = (buffer) => |
| 590 | Array.from(buffer, (b) => b.toString(16).padStart(2, '0')).join( |
| 591 | '' |
| 592 | ); |
| 593 | return buildGetResponse({ |
| 594 | head: { |
| 595 | checksums: { |
| 596 | 0: toHex(md5Buffer), // md5 |
| 597 | 1: toHex(sha1Buffer), // sha1 |
| 598 | 2: toHex(sha256Buffer), // sha256 |
| 599 | }, |
| 600 | }, |
| 601 | body: jsonRequest.method === 'get' ? body : undefined, |
| 602 | }); |
| 603 | } |
| 604 | } |
| 605 | throw new Error('Unexpected GET'); |
| 606 | } |
| 607 | default: |
| 608 | throw new Error('Unexpected HTTP Method'); |
| 609 | } |
| 610 | }, |
| 611 | async test(ctrl, env, ctx) { |
| 612 | // Basic Operations |
| 613 | { |
| 614 | // PutObject |
| 615 | await compareResponse(env.BUCKET.put(key, body)); |
| 616 | // GetObject |
| 617 | await compareResponse(env.BUCKET.get(key), { |
| 618 | body, |
| 619 | }); |
| 620 | // GetObject(.bytes()) |
| 621 | await compareResponse(env.BUCKET.get(key), { body }, true); |
| 622 | // HeadObject |
| 623 | await compareResponse(env.BUCKET.head(key)); |
| 624 | // MultipartUploads |
| 625 | { |
| 626 | // CreateMultipartUpload |
| 627 | const multi = await env.BUCKET.createMultipartUpload(key); |
| 628 | assert.equal(multi.uploadId, 'multipartId'); |
| 629 | assert.equal(multi.key, key); |
| 630 | // UploadPart |
| 631 | const part = await multi.uploadPart(1, body); |
| 632 | assert.equal(part.etag, 'partEtag'); |
| 633 | // Abort(doesn't quite make sense to abort **and** complete, but shouldn't matter) |
| 634 | await multi.abort(); |
| 635 | // CompleteMultipartUpload |
| 636 | await compareResponse( |
| 637 | multi.complete([ |
| 638 | { |
| 639 | partNumber: 1, |
| 640 | etag: 'partEtag', |
| 641 | }, |
| 642 | ]) |
| 643 | ); |
| 644 | } |
| 645 | // ListObjects |
| 646 | { |
| 647 | const list = await env.BUCKET.list({ |
| 648 | limit: 1, |
| 649 | prefix: 'basic', |
| 650 | cursor: 'ai', |
| 651 | delimiter: '/', |
| 652 | include: ['httpMetadata', 'customMetadata'], |
| 653 | }); |
| 654 | list.objects[0] = { ...list.objects[0] }; |
| 655 | list.objects[0].checksums = { ...list.objects[0].checksums }; |
| 656 | assert.deepEqual(list, { |
| 657 | objects: [HeadObject], |
| 658 | truncated: true, |
| 659 | cursor: 'ai', |
| 660 | delimitedPrefixes: [], |
| 661 | }); |
| 662 | } |
| 663 | // DeleteObject |
| 664 | { |
| 665 | await env.BUCKET.delete(key); |
| 666 | await env.BUCKET.delete([key, 'basicKey2']); |
| 667 | } |
| 668 | } |
| 669 | // Ranged Reads |
| 670 | { |
| 671 | // Offset/Length |
| 672 | { |
| 673 | const range = { |
| 674 | offset: 1, |
| 675 | length: 3, |
| 676 | }; |
| 677 | await compareResponse( |
| 678 | env.BUCKET.get('rangeOffLen', { |
| 679 | range, |
| 680 | }), |
| 681 | { |
| 682 | head: { range }, |
| 683 | }, |
| 684 | 'ont' |
| 685 | ); |
| 686 | } |
| 687 | // Suffix |
| 688 | await compareResponse( |
| 689 | env.BUCKET.get('rangeSuff', { |
| 690 | range: { |
| 691 | suffix: 2, |
| 692 | }, |
| 693 | }), |
| 694 | { |
| 695 | head: { |
| 696 | range: { |
| 697 | offset: 6, |
| 698 | length: 2, |
| 699 | }, |
| 700 | }, |
| 701 | }, |
| 702 | 'nt' |
| 703 | ); |
| 704 | } |
| 705 | // Conditionals |
| 706 | { |
| 707 | try { |
| 708 | await env.BUCKET.put('throwOnInvalidEtag', body, { |
| 709 | onlyIf: new Headers({ |
| 710 | 'if-match': 'strongEtag', |
| 711 | }), |
| 712 | }); |
| 713 | throw new Error('This should have thrown'); |
| 714 | } catch { |
| 715 | // intentionally empty |
| 716 | } |
| 717 | try { |
| 718 | await env.BUCKET.put('throwOnInvalidEtag', body, { |
| 719 | onlyIf: new Headers({ |
| 720 | 'if-none-match': 'strongEtag', |
| 721 | }), |
| 722 | }); |
| 723 | throw new Error('This should have thrown'); |
| 724 | } catch { |
| 725 | // intentionally empty |
| 726 | } |
| 727 | await env.BUCKET.put('onlyIfStrongEtag', body, { |
| 728 | onlyIf: { |
| 729 | etagMatches: 'strongEtag', |
| 730 | etagDoesNotMatch: 'strongEtag', |
| 731 | uploadedBefore: new Date('0'), |
| 732 | }, |
| 733 | }); |
| 734 | await env.BUCKET.put('onlyIfWildcard', body, { |
| 735 | onlyIf: { |
| 736 | etagMatches: '*', |
| 737 | etagDoesNotMatch: '*', |
| 738 | uploadedAfter: new Date('0'), |
| 739 | }, |
| 740 | }); |
| 741 | await env.BUCKET.get('onlyIfStrongEtag', { |
| 742 | onlyIf: { |
| 743 | etagMatches: 'strongEtag', |
| 744 | etagDoesNotMatch: 'strongEtag', |
| 745 | uploadedBefore: new Date('0'), |
| 746 | }, |
| 747 | }); |
| 748 | await env.BUCKET.get('onlyIfWildcard', { |
| 749 | onlyIf: { |
| 750 | etagMatches: '*', |
| 751 | etagDoesNotMatch: '*', |
| 752 | uploadedAfter: new Date('0'), |
| 753 | }, |
| 754 | }); |
| 755 | } |
| 756 | // Metadata |
| 757 | { |
| 758 | // httpMetadata |
| 759 | for (const httpMetadata of [httpMetaObj, httpMetaHeaders]) { |
| 760 | const head = { |
| 761 | httpMetadata: httpMetaObj, |
| 762 | }; |
| 763 | // PutObject |
| 764 | await compareResponse( |
| 765 | env.BUCKET.put('httpMetadata', body, { |
| 766 | httpMetadata, |
| 767 | }), |
| 768 | { head } |
| 769 | ); |
| 770 | // HeadObject |
| 771 | await compareResponse(env.BUCKET.head('httpMetadata'), { head }); |
| 772 | // GetObject |
| 773 | { |
| 774 | const objWithBody = await env.BUCKET.get('httpMetadata'); |
| 775 | await compareResponse(objWithBody, { head, body }); |
| 776 | // Hijacking this test to test `writeHttpMetadata` too |
| 777 | const createdHeaders = new Headers(); |
| 778 | objWithBody.writeHttpMetadata(createdHeaders); |
| 779 | assert.deepEqual( |
| 780 | Object.fromEntries(createdHeaders.entries()), |
| 781 | Object.fromEntries(httpMetaHeaders.entries()) |
| 782 | ); |
| 783 | } |
| 784 | // ListObjects |
| 785 | { |
| 786 | const list = await env.BUCKET.list({ |
| 787 | prefix: 'httpMeta', |
| 788 | include: ['httpMetadata'], |
| 789 | }); |
| 790 | list.objects[0] = { ...list.objects[0] }; |
| 791 | list.objects[0].checksums = { ...list.objects[0].checksums }; |
| 792 | assert.deepEqual(list, { |
| 793 | delimitedPrefixes: [], |
| 794 | objects: [{ ...HeadObject, ...head }], |
| 795 | truncated: false, |
| 796 | }); |
| 797 | } |
| 798 | // Multipart Upload |
| 799 | await compareResponse( |
| 800 | ( |
| 801 | await env.BUCKET.createMultipartUpload('httpMetadata', { |
| 802 | httpMetadata, |
| 803 | }) |
| 804 | ).complete([]), |
| 805 | { head } |
| 806 | ); |
| 807 | } |
| 808 | // customMetadata |
| 809 | { |
| 810 | // PutObject |
| 811 | const head = { |
| 812 | customMetadata, |
| 813 | }; |
| 814 | await compareResponse( |
| 815 | env.BUCKET.put('customMetadata', body, { |
| 816 | customMetadata, |
| 817 | }), |
| 818 | { head } |
| 819 | ); |
| 820 | // HeadObject |
| 821 | await compareResponse(await env.BUCKET.head('customMetadata'), { |
| 822 | head, |
| 823 | }); |
| 824 | // GetObject |
| 825 | await compareResponse(await env.BUCKET.get('customMetadata'), { |
| 826 | head, |
| 827 | body, |
| 828 | }); |
| 829 | // ListObjects |
| 830 | { |
| 831 | const list = await env.BUCKET.list({ |
| 832 | prefix: 'customMeta', |
| 833 | include: ['customMetadata'], |
| 834 | }); |
| 835 | list.objects[0] = { ...list.objects[0] }; |
| 836 | list.objects[0].checksums = { ...list.objects[0].checksums }; |
| 837 | assert.deepEqual(list, { |
| 838 | delimitedPrefixes: [], |
| 839 | objects: [{ ...HeadObject, ...head }], |
| 840 | truncated: false, |
| 841 | }); |
| 842 | } |
| 843 | // Multipart Upload |
| 844 | await compareResponse( |
| 845 | ( |
| 846 | await env.BUCKET.createMultipartUpload('customMetadata', { |
| 847 | customMetadata, |
| 848 | }) |
| 849 | ).complete([]), |
| 850 | { head } |
| 851 | ); |
| 852 | } |
| 853 | } |
| 854 | // StorageClasses |
| 855 | { |
| 856 | for (const storageClassName of [ |
| 857 | 'Default', |
| 858 | 'Standard', |
| 859 | 'InfrequentAccess', |
| 860 | ]) { |
| 861 | const key = 'class' + storageClassName; |
| 862 | const storageClass = |
| 863 | storageClassName === 'Default' ? undefined : storageClassName; |
| 864 | const head = { |
| 865 | storageClass: |
| 866 | storageClassName === 'Default' ? 'Standard' : storageClassName, |
| 867 | }; |
| 868 | // PutObject |
| 869 | await compareResponse( |
| 870 | env.BUCKET.put(key, body, { |
| 871 | storageClass, |
| 872 | }), |
| 873 | { head } |
| 874 | ); |
| 875 | // HeadObject |
| 876 | await compareResponse(env.BUCKET.head(key), { head }); |
| 877 | // GetObject |
| 878 | await compareResponse(env.BUCKET.get(key), { head, body }); |
| 879 | // Multipart Upload |
| 880 | await compareResponse( |
| 881 | ( |
| 882 | await env.BUCKET.createMultipartUpload(key, { |
| 883 | storageClass, |
| 884 | }) |
| 885 | ).complete([]), |
| 886 | { head } |
| 887 | ); |
| 888 | } |
| 889 | } |
| 890 | // SSEC |
| 891 | { |
| 892 | const head = { |
| 893 | ssecKeyMd5: keyMd5, |
| 894 | }; |
| 895 | for (const ssecKey of [bufferKey, hexKey]) { |
| 896 | // PutObject |
| 897 | await compareResponse( |
| 898 | env.BUCKET.put('ssec', body, { |
| 899 | ssecKey, |
| 900 | }), |
| 901 | { head } |
| 902 | ); |
| 903 | // GetObject |
| 904 | await compareResponse( |
| 905 | env.BUCKET.get('ssec', { |
| 906 | ssecKey, |
| 907 | }), |
| 908 | { head, body } |
| 909 | ); |
| 910 | // HeadObject |
| 911 | await compareResponse( |
| 912 | env.BUCKET.head('ssec', { |
| 913 | ssecKey, |
| 914 | }), |
| 915 | { head } |
| 916 | ); |
| 917 | // MultipartUpload |
| 918 | { |
| 919 | // CreateMultipartUpload |
| 920 | const multi = await env.BUCKET.createMultipartUpload( |
| 921 | 'ssecMultipart', |
| 922 | { |
| 923 | ssecKey, |
| 924 | } |
| 925 | ); |
| 926 | assert.equal(multi.uploadId, 'multipartId'); |
| 927 | // UploadPart |
| 928 | const part = await multi.uploadPart(1, 'hey', { |
| 929 | ssecKey, |
| 930 | }); |
| 931 | assert.equal(part.etag, 'partEtag'); |
| 932 | // CompleteMultipartUpload |
| 933 | await compareResponse( |
| 934 | multi.complete([ |
| 935 | { |
| 936 | partNumber: 1, |
| 937 | etag: 'partEtag', |
| 938 | }, |
| 939 | ]), |
| 940 | { head } |
| 941 | ); |
| 942 | } |
| 943 | } |
| 944 | } |
| 945 | // Checksums |
| 946 | { |
| 947 | // This tests the instrumentation with multiple checksums to ensure proper tag handling |
| 948 | let resp = await env.BUCKET.put('multipleChecksums', body, { |
| 949 | md5: md5Buffer, |
| 950 | }); |
| 951 | assert.ok(resp); |
| 952 | |
| 953 | // Also test HEAD operation to verify checksum tags |
| 954 | const headResp = await env.BUCKET.head('multipleChecksums'); |
| 955 | assert.ok(headResp); |
| 956 | } |
| 957 | }, |
| 958 | }; |