Skip to content

Commit

Permalink
feat: remove custom multipart parser
Browse files Browse the repository at this point in the history
Adds tests to store endpoint and improves upload tests with new mocks for cluster

Tweak some types and deps to make ts happy
  • Loading branch information
hugomrdias committed Aug 25, 2021
1 parent 00eef65 commit 3cc2515
Show file tree
Hide file tree
Showing 19 changed files with 1,071 additions and 1,049 deletions.
15 changes: 7 additions & 8 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,19 @@
"test:ts": "tsc",
"test:e2e": "npm-run-all -p -r test:e2e:**",
"test:e2e:mock:cluster": "smoke -p 9094 test/mocks/cluster",
"test:e2e:playwright": "playwright-test \"test/**/*.spec.js\" --sw src/index.js -b webkit"
"test:e2e:playwright": "playwright-test \"test/**/*.spec.js\" --sw src/index.js"
},
"author": "Hugo Dias <hugomrdias@gmail.com> (hugodias.me)",
"license": "MIT",
"dependencies": {
"@ipld/car": "^3.1.4",
"@ipld/dag-cbor": "^6.0.5",
"@ipld/car": "^3.1.16",
"@ipld/dag-cbor": "^6.0.9",
"@magic-sdk/admin": "^1.3.0",
"@nftstorage/ipfs-cluster": "^3.3.1",
"@ssttevee/multipart-parser": "^0.1.9",
"debug": "^4.3.2",
"just-safe-set": "^2.2.1",
"merge-options": "^3.0.4",
"multiformats": "^9.2.0",
"multiformats": "^9.4.3",
"regexparam": "^2.0.0",
"toucan-js": "^2.4.1"
},
Expand All @@ -37,13 +36,13 @@
"@sentry/cli": "^1.67.1",
"@sentry/webpack-plugin": "^1.16.0",
"@types/debug": "^4.1.5",
"@types/mocha": "^8.2.2",
"@types/mocha": "^9.0.0",
"buffer": "^6.0.3",
"cf-workers-idbkv": "^0.1.1",
"dotenv": "^10.0.0",
"esbuild": "^0.12.15",
"esbuild": "^0.12.22",
"git-rev-sync": "^3.0.1",
"ipfs-only-hash": "^4.0.0",
"ipfs-unixfs-importer": "^9.0.3",
"npm-run-all": "^4.1.5",
"playwright-test": "^7.0.0",
"process": "^0.11.10",
Expand Down
13 changes: 6 additions & 7 deletions packages/api/src/routes/nfts-store.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { validate } from '../utils/auth.js'
import setIn from 'just-safe-set'
import { toFormData } from '../utils/form-data.js'
import * as nfts from '../models/nfts.js'
import * as pins from '../models/pins.js'
import * as pinataQueue from '../models/pinata-queue.js'
Expand All @@ -23,7 +22,7 @@ const log = debug('nft-store')
/** @type {import('../utils/router.js').Handler} */
export async function store(event, ctx) {
const { user, tokenName } = await validate(event, ctx)
const form = await toFormData(event.request)
const form = await event.request.formData()

const data = JSON.parse(/** @type {string} */ (form.get('meta')))
const dag = JSON.parse(JSON.stringify(data))
Expand All @@ -33,11 +32,11 @@ export async function store(event, ctx) {
for (const [name, content] of form.entries()) {
if (name !== 'meta') {
const file = /** @type {File} */ (content)
const cid = CID.parse(
await cluster.importAsset(file, {
local: file.size > constants.cluster.localAddThreshold,
})
)
const asset = await cluster.importAsset(file, {
local: file.size > constants.cluster.localAddThreshold,
})
const cid = CID.parse(asset)

const href = `ipfs://${cid}/${file.name}`
const path = name.split('.')
setIn(data, path, href)
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/routes/nfts-upload.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { HTTPError } from '../errors.js'
import { toFormData } from '../utils/form-data.js'
import * as cluster from '../cluster.js'
import * as nfts from '../models/nfts.js'
import * as pins from '../models/pins.js'
Expand Down Expand Up @@ -28,11 +27,12 @@ export async function upload(event, ctx) {
const created = new Date().toISOString()

if (contentType.includes('multipart/form-data')) {
const form = await toFormData(event.request)
const form = await event.request.formData()
// Our API schema requires that all file parts be named `file` and
// encoded as binary, which is why we can expect that each part here is
// a file (and not a stirng).
const files = /** @type {File[]} */ (form.getAll('file'))

const dirSize = files.reduce((total, f) => total + f.size, 0)
const dir = await cluster.addDirectory(files, {
local: dirSize > LOCAL_ADD_THRESHOLD,
Expand Down
6 changes: 5 additions & 1 deletion packages/api/src/utils/car.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { CID } from 'multiformats'
import { CarWriter } from '@ipld/car'

/**
* @typedef {import('multiformats/block').Block<unknown>} Block
*/

/**
* @param {CID[]} roots
* @param {AsyncIterable<import('@ipld/car/api').Block>|Iterable<import('@ipld/car/api').Block>} blocks
* @param {AsyncIterable<Block>|Iterable<Block>} blocks
* @returns {Promise<Blob & { type: 'application/car' }>}
*/
export const encode = async (roots, blocks) => {
Expand Down
24 changes: 0 additions & 24 deletions packages/api/src/utils/form-data.js

This file was deleted.

59 changes: 47 additions & 12 deletions packages/api/test/mocks/cluster/post_add.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,57 @@
// @ts-ignore not typed
const Hash = require('ipfs-only-hash')
const { importer } = require('ipfs-unixfs-importer')
const block = {
get: async (/** @type {any} */ cid) => {
throw new Error(`unexpected block API get for ${cid}`)
},
put: async () => {},
}
/**
* @param {MultrFile[]} content
* @param {import('ipfs-unixfs-importer').UserImporterOptions} options
*/
async function add(content, options) {
const opts = { ...options }
const source = content.map((c) => ({
content: c.buffer,
path: c.originalname,
}))

const out = []
// @ts-ignore
for await (const unixfs of importer(source, block, opts)) {
out.push({
name: unixfs.path,
cid: {
'/': unixfs.cid.toString(),
},
size: unixfs.size,
})
}

return out
}

/**
* https://github.com/sinedied/smoke#javascript-mocks
* @typedef {{ buffer: Buffer, originalname: string }} MultrFile
* @param {{ query: Record<string, string>, files: MultrFile[] }} request
*/
module.exports = async ({ query, files }) => {
const result = {
cid: {
'/': await Hash.of(files[0].buffer, { cidVersion: 1, rawLeaves: true }),
},
name: files[0].originalname,
size: files[0].buffer.length,
}
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: query['stream-channels'] === 'false' ? [result] : result,
const body = await add(files, {
cidVersion: 1,
rawLeaves: true,
onlyHash: true,
wrapWithDirectory: query['wrap-with-directory'] === 'true',
})

try {
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body,
}
} catch (err) {
console.log(err)
}
}
103 changes: 103 additions & 0 deletions packages/api/test/nfts-store.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import assert from 'assert'
import { CID } from 'multiformats'
import { clearStores } from './scripts/helpers.js'
import stores from './scripts/stores.js'
import { signJWT } from '../src/utils/jwt.js'
import { SALT } from './scripts/worker-globals.js'
import * as Token from '../../client/src/token.js'

/**
* @param {{publicAddress?: string, issuer?: string, name?: string}} userInfo
*/
async function createTestUser({
publicAddress = `0x73573r${Date.now()}`,
issuer = `did:eth:${publicAddress}`,
name = 'A Tester',
} = {}) {
const token = await signJWT(
{
sub: issuer,
iss: 'nft-storage',
iat: Date.now(),
name: 'test',
},
SALT
)
await stores.users.put(
issuer,
JSON.stringify({
sub: issuer,
nickname: 'testymctestface',
name,
email: 'a.tester@example.org',
picture: 'http://example.org/avatar.png',
issuer,
publicAddress,
tokens: { test: token },
})
)
return { token, issuer }
}

describe('/store', () => {
beforeEach(clearStores)

it('should store image', async () => {
const { token, issuer } = await createTestUser()

const trick =
'ipfs://bafyreiemweb3jxougg7vaovg7wyiohwqszmgwry5xwitw3heepucg6vyd4'
const metadata = {
name: 'name',
description: 'stuff',
image: new File(['fake image'], 'cat.png', { type: 'image/png' }),
properties: {
extra: 'meta',
trick,
src: [
new File(['hello'], 'hello.txt', { type: 'text/plain' }),
new Blob(['bye']),
],
},
}
const body = Token.encode(metadata)

const res = await fetch('/store', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body,
})
assert(res, 'Server responded')
assert(res.ok, 'Server response ok')
const { ok, value } = await res.json()
const result = value
const cid = CID.parse(result.ipnft)
assert.equal(cid.version, 1)

assert.ok(typeof result.url === 'string')
assert.ok(result.url.startsWith('ipfs:'))

assert.equal(result.data.name, 'name')
assert.equal(result.data.description, 'stuff')
assert.equal(
result.data.image,
'ipfs://bafybeieb43wq6bqbfmyaawfmq6zuycdq4bo77zph33zxx26wvquth3qxau/cat.png'
)
assert.equal(result.data.properties.extra, 'meta')
assert.equal(result.data.properties.trick, trick)
assert.ok(Array.isArray(result.data.properties.src))
assert.equal(result.data.properties.src.length, 2)

const nftData = await stores.nfts.get(`${issuer}:${result.ipnft}`)
assert(nftData, 'nft data was stored')

const pinData = await stores.pins.getWithMetadata(result.ipnft)
assert(pinData.metadata, 'pin metadata was stored')
assert.strictEqual(
// @ts-ignore
pinData.metadata.status,
'pinned',
'pin status is "pinned"'
)
})
})
30 changes: 18 additions & 12 deletions packages/api/test/nfts-upload.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,6 @@ describe('/upload', () => {
const file2 = new Blob(['hello world! 2'])
body.append('file', file1, 'name1')
body.append('file', file2, 'name2')
// expected CID for the above data
const cid = 'bafkreidsnixyep54glvcz2ocszbokylqalkio2eintcio5tix2vrbmaatu'
const res = await fetch('/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
Expand All @@ -94,8 +92,12 @@ describe('/upload', () => {
{ name: 'name2', type: 'application/octet-stream' },
])
assert.ok(value.type === 'directory', 'should be directory')
assert.ok(value.size === file1.size, 'should have correct size')
assert.strictEqual(value.cid, cid, 'Server responded with expected CID')
assert.equal(value.size, 130, 'should have correct size')
assert.strictEqual(
value.cid,
'bafybeifrkxqq5bbn4fkykfyggoltlb7vn3moyhr3pldzx3me6yiukcfsem',
'Server responded with expected CID'
)
})

it('should upload a multiple blobs without name', async () => {
Expand All @@ -106,8 +108,6 @@ describe('/upload', () => {
const file2 = new Blob(['hello world! 2'])
body.append('file', file1)
body.append('file', file2)
// expected CID for the above data
const cid = 'bafkreidsnixyep54glvcz2ocszbokylqalkio2eintcio5tix2vrbmaatu'
const res = await fetch('/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
Expand All @@ -121,8 +121,12 @@ describe('/upload', () => {
{ name: 'blob', type: 'application/octet-stream' },
])
assert.ok(value.type === 'directory', 'should be directory')
assert.ok(value.size === file1.size, 'should have correct size')
assert.strictEqual(value.cid, cid, 'Server responded with expected CID')
assert.equal(value.size, 66, 'should have correct size')
assert.strictEqual(
value.cid,
'bafybeiaowg4ssqzemwgdlisgphib54clq62arief7ssabov5r3pbfh7vje',
'Server responded with expected CID'
)
})

it('should upload a multiple files without name', async () => {
Expand All @@ -133,8 +137,6 @@ describe('/upload', () => {
const file2 = new Blob(['hello world! 2'])
body.append('file', new File([file1], 'name1.png'))
body.append('file', new File([file2], 'name1.png'))
// expected CID for the above data
const cid = 'bafkreidsnixyep54glvcz2ocszbokylqalkio2eintcio5tix2vrbmaatu'
const res = await fetch('/upload', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
Expand All @@ -148,8 +150,12 @@ describe('/upload', () => {
{ name: 'name1.png', type: 'application/octet-stream' },
])
assert.ok(value.type === 'directory', 'should be directory')
assert.ok(value.size === file1.size, 'should have correct size')
assert.strictEqual(value.cid, cid, 'Server responded with expected CID')
assert.equal(value.size, 71, 'should have correct size')
assert.strictEqual(
value.cid,
'bafybeibl5yizqtzdnhflscdmzjy7t6undnn7zhvhryhbfknneu364w62pe',
'Server responded with expected CID'
)
})

it('should upload a single CAR file', async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@
"test",
"scripts"
],
"exclude": ["node_modules/", "dist/", "src/utils/multipart/**/*.js"]
"exclude": ["node_modules/", "dist/"]
}
4 changes: 4 additions & 0 deletions packages/api/wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ account_id = "fffa4b4363a7e5250af8357087263b3a"
workers_dev = true
type = "javascript"
usage_model = "unbound"

# Compatibility flags https://github.com/cloudflare/wrangler/pull/2009
compatibility_date = "2021-08-23"
compatibility_flags = [ "formdata_parser_supports_files" ]
kv_namespaces = [
{ binding = "USERS", preview_id = "7e441603d1bc4d5a87f6cecb959018e4", id = "7e441603d1bc4d5a87f6cecb959018e4" },
{ binding = "NFTS", preview_id = "f1c35cd1601c452782db6056b2d35f25", id = "f1c35cd1601c452782db6056b2d35f25" },
Expand Down
Loading

0 comments on commit 3cc2515

Please sign in to comment.