From d07b52f22fba71bff19c9a730186acea002f0254 Mon Sep 17 00:00:00 2001 From: Yusef Napora <yusef@protocol.ai> Date: Tue, 24 May 2022 14:39:25 -0400 Subject: [PATCH] feat: use application/vnd.ipld.car as preferred content-type --- packages/api/src/routes/metaplex-upload.js | 2 +- packages/api/src/routes/nfts-upload.js | 4 +- packages/api/src/utils/car.js | 6 +- .../api/test/nfts-metaplex-upload.spec.js | 14 ++-- packages/api/test/nfts-upload.spec.js | 80 ++++++++++--------- packages/client/src/lib.js | 2 +- packages/client/test/lib.spec.js | 2 +- packages/client/test/service.js | 5 +- packages/website/lib/mock_files.js | 52 ++++++------ .../pages/docs/how-to/mint-custom-metadata.md | 2 +- packages/website/public/schema.yml | 9 ++- 11 files changed, 97 insertions(+), 81 deletions(-) diff --git a/packages/api/src/routes/metaplex-upload.js b/packages/api/src/routes/metaplex-upload.js index e59812c0fd..851944c607 100644 --- a/packages/api/src/routes/metaplex-upload.js +++ b/packages/api/src/routes/metaplex-upload.js @@ -52,7 +52,7 @@ export async function metaplexUpload(event, ctx) { user, key, car: blob, - mimeType: 'application/car', + mimeType: 'application/vnd.ipld.car', files: [], structure: 'Unknown', meta, diff --git a/packages/api/src/routes/nfts-upload.js b/packages/api/src/routes/nfts-upload.js index 85fb854782..b2f5f8937a 100644 --- a/packages/api/src/routes/nfts-upload.js +++ b/packages/api/src/routes/nfts-upload.js @@ -67,7 +67,9 @@ export async function nftUpload(event, ctx) { throw new HTTPError('empty payload', 400) } - const isCar = contentType.includes('application/car') + const isCar = + contentType.includes('application/car') || + contentType.includes('application/vnd.ipld.car') /** @type {'Car'|'Blob'} */ let uploadType /** @type {DagStructure} */ diff --git a/packages/api/src/utils/car.js b/packages/api/src/utils/car.js index ce1b8dc5fc..c7d331b673 100644 --- a/packages/api/src/utils/car.js +++ b/packages/api/src/utils/car.js @@ -8,7 +8,7 @@ import { CarWriter } from '@ipld/car' /** * @param {CID[]} roots * @param {AsyncIterable<Block>|Iterable<Block>} blocks - * @returns {Promise<Blob & { type: 'application/car' }>} + * @returns {Promise<Blob & { type: 'application/vnd.ipld.car' }>} */ export const encode = async (roots, blocks) => { const { out, writer } = CarWriter.create(roots) @@ -22,9 +22,9 @@ export const encode = async (roots, blocks) => { parts.push(part) } - return /** @type {Blob & {type: 'application/car'}} */ ( + return /** @type {Blob & {type: 'application/vnd.ipld.car'}} */ ( new Blob(parts, { - type: 'application/car', + type: 'application/vnd.ipld.car', }) ) } diff --git a/packages/api/test/nfts-metaplex-upload.spec.js b/packages/api/test/nfts-metaplex-upload.spec.js index f9432cab11..0943e1b02b 100644 --- a/packages/api/test/nfts-metaplex-upload.spec.js +++ b/packages/api/test/nfts-metaplex-upload.spec.js @@ -31,7 +31,7 @@ describe('Metaplex Upload', () => { method: 'POST', headers: { 'x-web3auth': `Metaplex ${fixture.token}`, - 'Content-Type': 'application/car', + 'Content-Type': 'application/vnd.ipld.car', }, body: car, }) @@ -43,7 +43,7 @@ describe('Metaplex Upload', () => { assert.strictEqual(value.cid, cid, 'Server responded with expected CID') assert.strictEqual( value.type, - 'application/car', + 'application/vnd.ipld.car', 'type should match blob mime-type' ) @@ -81,7 +81,7 @@ describe('Metaplex Upload', () => { method: 'POST', headers: { 'x-web3auth': `Metaplex ${fixture.token}`, - 'Content-Type': 'application/car', + 'Content-Type': 'application/vnd.ipld.car', }, body: car, }) @@ -93,7 +93,7 @@ describe('Metaplex Upload', () => { assert.strictEqual(value.cid, cid, 'Server responded with expected CID') assert.strictEqual( value.type, - 'application/car', + 'application/vnd.ipld.car', 'type should match blob mime-type' ) @@ -134,7 +134,7 @@ describe('Metaplex Upload', () => { method: 'POST', headers: { 'x-web3auth': `Metaplex ${alteredToken}`, - 'Content-Type': 'application/car', + 'Content-Type': 'application/vnd.ipld.car', }, body: car, }) @@ -160,7 +160,7 @@ describe('Metaplex Upload', () => { method: 'POST', headers: { 'x-web3auth': `Metaplex ${alteredToken}`, - 'Content-Type': 'application/car', + 'Content-Type': 'application/vnd.ipld.car', }, body: car, }) @@ -183,7 +183,7 @@ describe('Metaplex Upload', () => { method: 'POST', headers: { 'x-web3auth': `Metaplex ${fixture.token}`, - 'Content-Type': 'application/car', + 'Content-Type': 'application/vnd.ipld.car', }, body: car, }) diff --git a/packages/api/test/nfts-upload.spec.js b/packages/api/test/nfts-upload.spec.js index 59226b48e2..46dae4692a 100644 --- a/packages/api/test/nfts-upload.spec.js +++ b/packages/api/test/nfts-upload.spec.js @@ -152,40 +152,44 @@ describe('NFT Upload ', () => { }) it('should upload a single CAR file', async () => { - const { root, car } = await createCar('hello world car') - // expected CID for the above data - const cid = 'bafkreifeqjorwymdmh77ars6tbrtno74gntsdcvqvcycucidebiri2e7qy' - assert.strictEqual(root.toString(), cid, 'car file has correct root') - const res = await fetch('upload', { - method: 'POST', - headers: { - Authorization: `Bearer ${client.token}`, - 'Content-Type': 'application/car', - }, - body: car, - }) - - assert(res, 'Server responded') - assert(res.ok, 'Server response ok') - const { ok, value } = await res.json() - assert(ok, 'Server response payload has `ok` property') - assert.strictEqual(value.cid, cid, 'Server responded with expected CID') - assert.strictEqual( - value.type, - 'application/car', - 'type should match blob mime-type' - ) + // check both accepted content-types for CARs + const contentTypes = ['application/car', 'application/vnd.ipld.car'] + for (const contentType of contentTypes) { + const { root, car } = await createCar('hello world car') + // expected CID for the above data + const cid = 'bafkreifeqjorwymdmh77ars6tbrtno74gntsdcvqvcycucidebiri2e7qy' + assert.strictEqual(root.toString(), cid, 'car file has correct root') + const res = await fetch('upload', { + method: 'POST', + headers: { + Authorization: `Bearer ${client.token}`, + 'Content-Type': contentType, + }, + body: car, + }) + + assert(res, 'Server responded') + assert(res.ok, 'Server response ok') + const { ok, value } = await res.json() + assert(ok, 'Server response payload has `ok` property') + assert.strictEqual(value.cid, cid, 'Server responded with expected CID') + assert.strictEqual( + value.type, + contentType, + 'type should match request mime-type' + ) - const { data } = await rawClient - .from('upload') - .select('*, content(*)') - .match({ source_cid: cid, user_id: client.userId }) - .single() + const { data } = await rawClient + .from('upload') + .select('*, content(*)') + .match({ source_cid: cid, user_id: client.userId }) + .single() - // @ts-ignore - assert.equal(data.source_cid, cid) - assert.equal(data.deleted_at, null) - assert.equal(data.content.dag_size, 15, 'correct dag size') + // @ts-ignore + assert.equal(data.source_cid, cid) + assert.equal(data.deleted_at, null) + assert.equal(data.content.dag_size, 15, 'correct dag size') + } }) it('should allow a CAR with unsupported hash function', async () => { @@ -207,9 +211,9 @@ describe('NFT Upload ', () => { method: 'POST', headers: { Authorization: `Bearer ${client.token}`, - 'Content-Type': 'application/car', + 'Content-Type': 'application/vnd.ipld.car', }, - body: new Blob(carBytes, { type: 'application/car' }), + body: new Blob(carBytes, { type: 'application/vnd.ipld.car' }), }) assert(res, 'Server responded') @@ -223,7 +227,7 @@ describe('NFT Upload ', () => { ) assert.strictEqual( value.type, - 'application/car', + 'application/vnd.ipld.car', 'type should match blob mime-type' ) }) @@ -247,7 +251,7 @@ describe('NFT Upload ', () => { method: 'POST', headers: { Authorization: `Bearer ${client.token}`, - 'Content-Type': 'application/car', + 'Content-Type': 'application/vnd.ipld.car', }, body: new Blob(carBytes), }) @@ -280,13 +284,13 @@ describe('NFT Upload ', () => { method: 'POST', headers: { Authorization: `Bearer ${client.token}`, - 'Content-Type': 'application/car', + 'Content-Type': 'application/vnd.ipld.car', }, body: car, }) const data2 = await res2.json() assert.equal(data2.value.cid, cid) - assert.equal(data2.value.type, 'application/car', 'car') + assert.equal(data2.value.type, 'application/vnd.ipld.car', 'car') const { data } = await rawClient .from('upload') diff --git a/packages/client/src/lib.js b/packages/client/src/lib.js index 14c03ed755..d1448c10c1 100644 --- a/packages/client/src/lib.js +++ b/packages/client/src/lib.js @@ -172,7 +172,7 @@ class NFTStorage { for await (const part of car) { carParts.push(part) } - const carFile = new Blob(carParts, { type: 'application/car' }) + const carFile = new Blob(carParts, { type: 'application/vnd.ipld.car' }) const cid = await pRetry( async () => { await rateLimiter() diff --git a/packages/client/test/lib.spec.js b/packages/client/test/lib.spec.js index b1a6dc4260..2075d399c7 100644 --- a/packages/client/test/lib.spec.js +++ b/packages/client/test/lib.spec.js @@ -104,7 +104,7 @@ describe('client', () => { for await (const part of out) { carParts.push(part) } - const car = new Blob(carParts, { type: 'application/car' }) + const car = new Blob(carParts, { type: 'application/vnd.ipld.car' }) const cid = await client.storeCar(car) assert.equal(cid, expectedCid) }) diff --git a/packages/client/test/service.js b/packages/client/test/service.js index c5e4a59d05..0ec7da3665 100644 --- a/packages/client/test/service.js +++ b/packages/client/test/service.js @@ -20,7 +20,10 @@ const headers = ({ headers }) => ({ */ const importUpload = async (request) => { const contentType = request.headers.get('content-type') || '' - if (!contentType.includes('application/car')) { + const isCar = + contentType.includes('application/car') || + contentType.includes('application/vnd.ipld.car') + if (!isCar) { throw new Error(`unexpected content type: ${contentType}`) } const content = await request.arrayBuffer() diff --git a/packages/website/lib/mock_files.js b/packages/website/lib/mock_files.js index df7f685e46..3b6640cc62 100644 --- a/packages/website/lib/mock_files.js +++ b/packages/website/lib/mock_files.js @@ -2,7 +2,7 @@ export const MOCK_FILES = [ { cid: 'bafybeic3a6txgjsavv57lvfud67y7zvz2eq2acp3wzbk5tey4xy72zvuyu', created: '2021-12-17T17:27:24.605+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', name: 'super special name for mocks', files: [], @@ -18,7 +18,7 @@ export const MOCK_FILES = [ { cid: 'bafkreibe235afeycijr36lc5r3xjw7lrucbzilmx4eewv5666dlas2hxia', created: '2021-12-11T14:26:23.205+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', name: 'super special name 2 for mocks', files: [], @@ -80,7 +80,7 @@ export const MOCK_FILES = [ { cid: 'bafybeiefibctpwu7tm25nxgcn3b5ywwskowkgccl4cbwnvtrgv6rsqjpxa', created: '2021-12-02T17:20:06.685+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], name: 'super special name 2 for mocks', @@ -142,7 +142,7 @@ export const MOCK_FILES = [ { cid: 'bafkreidewg7t752npyue5wbhuwq53dfcxpms3yp7vfr7jn4l5noulyf2eu', created: '2021-11-11T19:53:37.954+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 215085, @@ -233,7 +233,7 @@ export const MOCK_FILES = [ { cid: 'bafkreie3tjgq2phyctf2fippi5hbvhfu74qvagwn5yakq5xnmyv77azw7u', created: '2021-11-11T19:50:16.825+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 25342, @@ -324,7 +324,7 @@ export const MOCK_FILES = [ { cid: 'bafkreifn43pu4gv45m46qopifih6khe3rxsnpgbnvpotb7julek7f2pcga', created: '2021-11-11T14:35:33.89+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 117704, @@ -429,7 +429,7 @@ export const MOCK_FILES = [ { cid: 'bafybeid4fkhuzacw26xzd5zsm5w5zoh5ich7ujug6mfap2jseeucu6iqia', created: '2021-11-11T14:34:56.112+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 1096377, @@ -534,7 +534,7 @@ export const MOCK_FILES = [ { cid: 'bafybeiagkmu65y4l6wvxolhgwrma3hplgl7aqm3rmyjgfpak6wuzclpgka', created: '2021-11-11T14:34:40.703+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 1095138, @@ -639,7 +639,7 @@ export const MOCK_FILES = [ { cid: 'bafybeigp3wixuxy5jyssmvmpvxsde6tfnbvz73zlbd75n3vw5fbstahd6e', created: '2021-11-11T14:34:27.176+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 1094733, @@ -744,7 +744,7 @@ export const MOCK_FILES = [ { cid: 'bafybeihxt2sua5pvhyfjoxwl6qkdk5s3yxo7flxtjemkuq7rhlnrobz63u', created: '2021-11-11T14:34:15.756+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 1089162, @@ -849,7 +849,7 @@ export const MOCK_FILES = [ { cid: 'bafybeicdabe3cd366hjpwvtzgu7bdpz2tnc5gjulil53ynkowzn2h6hxai', created: '2021-11-11T14:34:03.956+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 1094558, @@ -954,7 +954,7 @@ export const MOCK_FILES = [ { cid: 'bafybeidvorqswzkixzupzya4m37burj5oxswa3rf65nghybvfec2zubgya', created: '2021-11-11T14:33:49.415+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 1086294, @@ -1059,7 +1059,7 @@ export const MOCK_FILES = [ { cid: 'bafkreibocn3taii22mjzxwcyczu2xxqlxjxpbymwc45nkeawixfmmjf5qi', created: '2021-11-11T14:33:11.606+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 179330, @@ -1164,7 +1164,7 @@ export const MOCK_FILES = [ { cid: 'bafkreia4klzyfw4nqycdn3re7fggeimvnle3zylh5zba3aln3gtesje4u4', created: '2021-11-11T14:32:53.399+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 100721, @@ -1419,7 +1419,7 @@ export const MOCK_FILES = [ { cid: 'bafkreiawfi33sq52ssimkml2htuhl2poq3sym5jk6mz23nh46hlb3mycbq', created: '2021-11-11T14:32:36.434+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 205142, @@ -1524,7 +1524,7 @@ export const MOCK_FILES = [ { cid: 'bafybeihl3bkck7v56ta237bpkjymzcfc6gsge32nhh2f2tmttgegfn4x3q', created: '2021-11-11T14:32:16.619+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 776278, @@ -1629,7 +1629,7 @@ export const MOCK_FILES = [ { cid: 'bafybeiemnso4riinbghkenyw2xvrui5frxbmkcyvlulflk54jpbubnbwaq', created: '2021-11-11T14:31:50.759+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 793259, @@ -1734,7 +1734,7 @@ export const MOCK_FILES = [ { cid: 'bafybeidcikrfoerfbgbrkkf2etyjhaws6f2qf7hlvocwlrcygknq245uka', created: '2021-11-11T14:31:21.604+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 1015204, @@ -1839,7 +1839,7 @@ export const MOCK_FILES = [ { cid: 'bafybeig3wxdo2xeipundiozlmqjmtp3epvktcwbo53h276ferzcwtuacbi', created: '2021-11-11T14:30:52.816+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 5160525, @@ -1944,7 +1944,7 @@ export const MOCK_FILES = [ { cid: 'bafybeicsxx354iiuvpaypcdtpu2sct2wjonmh4y5kjipxls6ima7ju2g34', created: '2021-11-11T14:30:07.33+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 5751188, @@ -2049,7 +2049,7 @@ export const MOCK_FILES = [ { cid: 'bafybeigmgqsmwy3wprkmsbpt3mwtcfsyleymhhlyzu7qieu2tezszvv6dy', created: '2021-11-11T14:29:40.718+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 2005672, @@ -2259,7 +2259,7 @@ export const MOCK_FILES = [ { cid: 'bafybeifpsn6trnutbt434v2gph6zvlwicqssxrnrsbmu5n35ocg6u7s6ri', created: '2021-11-11T14:29:20.203+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 5622049, @@ -2364,7 +2364,7 @@ export const MOCK_FILES = [ { cid: 'bafkreihggdz4hwbeh5vrffq42eo5w36my2dz6y6bl2sdjjakfsc4zml6hu', created: '2021-11-11T14:28:56.466+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 93475, @@ -2574,7 +2574,7 @@ export const MOCK_FILES = [ { cid: 'bafybeif73pezyrz5ug5aacjnjxujctc2dyoem2g5injvr3y7mk45aatw4y', created: '2021-11-11T14:28:36.024+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 394675, @@ -2949,7 +2949,7 @@ export const MOCK_FILES = [ { cid: 'bafkreiccjdgsev2zkqlvr6t64sicz3eaj2xibyehvyp2ty6h55qfruf5qu', created: '2021-11-11T14:28:19.996+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 61536, @@ -3054,7 +3054,7 @@ export const MOCK_FILES = [ { cid: 'bafybeibvlmcr7o6hwe2htf7nr3xypmv5f6sxi2jnjclk3xjero3fbxhdj4', created: '2021-11-11T14:28:03.331+00:00', - type: 'application/car', + type: 'application/vnd.ipld.car', scope: 'session', files: [], size: 2800699, diff --git a/packages/website/pages/docs/how-to/mint-custom-metadata.md b/packages/website/pages/docs/how-to/mint-custom-metadata.md index 57b64b2886..975ca0af51 100644 --- a/packages/website/pages/docs/how-to/mint-custom-metadata.md +++ b/packages/website/pages/docs/how-to/mint-custom-metadata.md @@ -131,7 +131,7 @@ The `------WebKitFormBoundary5peilISl2YOOweQy` tokens in the example above set t The `/upload` endpoint has some special support for IPFS Content Archives (CARs) that can be used to upload files larger than the 100 MiB single-request limit. -Uploading CAR files works just like uploading a single file. Send a `POST` request to `/upload`, with the `Content-Type` header set to `application/car` and the binary file data as the request body. +Uploading CAR files works just like uploading a single file. Send a `POST` request to `/upload`, with the `Content-Type` header set to `application/vnd.ipld.car` and the binary file data as the request body. To upload files larger than 100 MiB, you can pack them into a CAR file and split the CAR into chunks, uploading each chunk in a separate HTTP request. See the [CAR file guide][guide-car-files] to learn more. diff --git a/packages/website/public/schema.yml b/packages/website/public/schema.yml index 5bf57b0c60..caa9e0ddbf 100644 --- a/packages/website/public/schema.yml +++ b/packages/website/public/schema.yml @@ -117,7 +117,10 @@ paths: ``` ### Content Addressed Archive (CAR) files - You can also upload a CAR file, by setting the request body as a single CAR Blob/File object and providing the request header `Content-Type: application/car` + You can also upload a CAR file, by setting the request body as a single CAR Blob/File object and providing the request header `Content-Type: application/vnd.ipld.car`. + The Content-Type `application/car` is also supported for compatibility with an earlier version of this API, however the IANA media type + `application/vnd.ipld.car` should be preferred. + Providing a CAR file allows you to pre-compute the root CID for 1 or more files, ensures that NFT.Storage will store and provide your assets with the same CID. ### Size limitations @@ -156,6 +159,10 @@ paths: schema: type: string format: binary + application/vnd.ipld.car: + schema: + type: string + format: binary multipart/form-data: schema: type: object