Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: labeling uploads on files #1694

Merged
merged 8 commits into from
Apr 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/api/src/bindings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ export type NFT = {
/**
* Name of the JWT token used to create this NFT.
*/
name?: string
/**
* Optional name of the file(s) uploaded as NFT.
*/
scope: string
/**
* Date this NFT was created in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format: YYYY-MM-DDTHH:MM:SSZ.
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { tokensDelete } from './routes/tokens-delete.js'
import { tokensCreate } from './routes/tokens-create.js'
import { tokensList } from './routes/tokens-list.js'
import { login } from './routes/login.js'
import { nftUpload } from './routes/nfts-upload.js'
import { nftUpdateUpload, nftUpload } from './routes/nfts-upload.js'
import { nftCheck } from './routes/nfts-check.js'
import { nftGet } from './routes/nfts-get.js'
import { nftDelete } from './routes/nfts-delete.js'
Expand Down Expand Up @@ -98,6 +98,8 @@ r.add('get', '/check/:cid', withMode(nftCheck, RO), [postCors])
r.add('get', '', withMode(nftList, RO), [postCors])
r.add('get', '/:cid', withMode(nftGet, RO), [postCors])
r.add('post', '/upload', withMode(nftUpload, RW), [postCors])
r.add('patch', '/upload/:id', withMode(nftUpdateUpload, RW), [postCors])

r.add('post', '/store', withMode(nftStore, RW), [postCors])
r.add('delete', '/:cid', withMode(nftDelete, RW), [postCors])

Expand Down
6 changes: 5 additions & 1 deletion packages/api/src/routes/cors.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function cors(event) {
let respHeaders = {
'Content-Length': '0',
'Access-Control-Allow-Origin': headers.get('origin') || '*',
'Access-Control-Allow-Methods': 'GET,POST,DELETE,OPTIONS',
'Access-Control-Allow-Methods': 'GET,POST,DELETE,PATCH,OPTIONS',
'Access-Control-Max-Age': '86400',
// Allow all future content Request headers to go back to browser
// such as Authorization (Bearer) or X-Client-Name-Version
Expand Down Expand Up @@ -42,6 +42,10 @@ export function postCors(req, rsp) {
const origin = req.headers.get('origin')
if (origin) {
rsp.headers.set('Access-Control-Allow-Origin', origin)
rsp.headers.set(
'Access-Control-Allow-Methods',
'GET,POST,DELETE,PATCH,OPTIONS'
)
rsp.headers.set('Vary', 'Origin')
} else {
rsp.headers.set('Access-Control-Allow-Origin', '*')
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/routes/metaplex-upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export async function metaplexUpload(event, ctx) {

const upload = await uploadCarWithStat(
{
event,
ctx,
user,
key,
Expand Down
35 changes: 34 additions & 1 deletion packages/api/src/routes/nfts-upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as cluster from '../cluster.js'
import { JSONResponse } from '../utils/json-response.js'
import { validate } from '../utils/auth.js'
import { toNFTResponse } from '../utils/db-transforms.js'
import { parseCid } from '../utils/utils.js'

const MAX_BLOCK_SIZE = 1 << 20 // Maximum permitted block size in bytes (1MiB).
const decoders = [pb, raw, cbor]
Expand Down Expand Up @@ -45,6 +46,7 @@ export async function nftUpload(event, ctx) {
})

upload = await uploadCar({
event,
ctx,
user,
key,
Expand All @@ -68,6 +70,7 @@ export async function nftUpload(event, ctx) {
let structure
/** @type {Blob} */
let car
/** @type {string} */

if (isCar) {
car = blob
Expand All @@ -85,6 +88,7 @@ export async function nftUpload(event, ctx) {
}

upload = await uploadCar({
event,
ctx,
user,
key,
Expand All @@ -102,6 +106,7 @@ export async function nftUpload(event, ctx) {

/**
* @typedef {{
* event: FetchEvent,
* ctx: import('../bindings').RouteContext
* user: Pick<import('../utils/db-client-types').UserOutput, 'id'>
* key?: Pick<import('../utils/db-client-types').UserOutputKey, 'id'>
Expand All @@ -127,7 +132,7 @@ export async function uploadCar(params) {
* @param {CarStat} stat
*/
export async function uploadCarWithStat(
{ ctx, user, key, car, uploadType = 'Car', mimeType, files, meta },
{ event, ctx, user, key, car, uploadType = 'Car', mimeType, files, meta },
stat
) {
const [added, backupUrl] = await Promise.all([
Expand All @@ -139,6 +144,12 @@ export async function uploadCarWithStat(
: Promise.resolve(null),
])

const xName = event.request.headers.get('x-name')
let name = xName && decodeURIComponent(xName)
if (!name || typeof name !== 'string') {
name = `Upload at ${new Date().toISOString()}`
}

const upload = await ctx.db.createUpload({
mime_type: mimeType,
type: uploadType,
Expand All @@ -150,11 +161,33 @@ export async function uploadCarWithStat(
meta,
key_id: key?.id,
backup_urls: backupUrl ? [backupUrl] : [],
name,
})

return upload
}

/** @type {import('../bindings').Handler} */
export async function nftUpdateUpload(event, ctx) {
const { params, db } = ctx
try {
const { user } = await validate(event, ctx)
const id = params.id

// id is required for updating
if (!id) return new JSONResponse({ ok: false, value: 'ID is required' })

const body = await event.request.json()
const { name } = body

const updatedRecord = await db.updateUpload({ id, name, user_id: user.id })

return new JSONResponse({ ok: true, value: updatedRecord })
} catch (/** @type {any} */ err) {
return new JSONResponse({ ok: false, value: err.message })
}
}

/**
* Gets CAR file information. Throws if the CAR does not conform to our idea of
* a valid CAR i.e.
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/utils/db-client-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ export interface CreateUploadInput {
pins?: Pick<definitions['pin'], 'service' | 'status'>[]
}

export interface UpdateUploadInput {
id: string
name?: string
user_id: number
}

export type ContentOutput = definitions['content'] & {
pins: Array<definitions['pin']>
deals: Deal[]
Expand Down
40 changes: 40 additions & 0 deletions packages/api/src/utils/db-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,46 @@ export class DBClient {
return data
}

/**
* Create upload with content and pins
*
* @param {import('./db-client-types').UpdateUploadInput} data
* @returns
*/
async updateUpload(data) {
const now = new Date().toISOString()

const query = this.client.from('upload')

// we have to see if the upload exists with:
// the id requested and that the user is the uploads user
// and that the upload is not deleted
const { data: upload, status } = await query
.update({
name: data.name,
updated_at: now,
})
.select(this.uploadQuery)
.eq('id', data.id)
.eq('user_id', data.user_id)
.is('deleted_at', null)
.single()

if (status === 406) {
throw new Error(`Status 406, cannot update ${data.id}`)
}

if (!upload) {
throw new Error(
`Cannot update upload ${JSON.stringify(data.id)} ${JSON.stringify(
status
)}`
)
}

return upload
}

/**
* Create upload with content and pins
*
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/utils/db-transforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function toNFTResponse(upload, sourceCid) {
scope: upload.key ? upload.key.name : 'session',
files: upload.files,
size: upload.content.dag_size || 0,
name: upload.name,
pin: {
cid: sourceCid || upload.source_cid,
created: upload.content.pin[0].inserted_at,
Expand Down
58 changes: 58 additions & 0 deletions packages/api/test/nfts-upload.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,4 +362,62 @@ describe('NFT Upload ', () => {
// @ts-ignore
assert.equal(data.meta.ucan.token, opUcan)
})

it('should update a single file', async () => {
const file = new Blob(['hello world!'], { type: 'application/text' })
// expected CID for the above data
const cid = 'bafkreidvbhs33ighmljlvr7zbv2ywwzcmp5adtf4kqvlly67cy56bdtmve'

await fetch('upload', {
method: 'POST',
headers: { Authorization: `Bearer ${client.token}` },
body: file,
})

const { data } = await rawClient
.from('upload')
.select('*')
.match({ source_cid: cid, user_id: client.userId })
.single()

// update file we just created above

const name = 'test updated name'

const uploadRes = await fetch(`upload/${data.id}`, {
method: 'PATCH',
headers: {
Authorization: `Bearer ${client.token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
}),
})

const { ok: uploadOk, value: uploadValue } = await uploadRes.json()
assert(
uploadOk,
'Server response payload has `ok` property for upload endpoint'
)
assert.strictEqual(
uploadValue.cid,
data.cid,
'Server responded with expected CID'
)
assert.strictEqual(
uploadValue.name,
name,
'type should match blob mime-type'
)

const { data: uploadData } = await rawClient
.from('upload')
.select('*')
.match({ id: data.id })
.single()

// @ts-ignore
assert.equal(uploadData.name, name)
})
})
3 changes: 3 additions & 0 deletions packages/website/lib/mock_files.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const MOCK_FILES = [
created: '2021-12-17T17:27:24.605+00:00',
type: 'application/car',
scope: 'session',
name: 'super special name for mocks',
files: [],
size: 3328510,
pin: {
Expand All @@ -19,6 +20,7 @@ export const MOCK_FILES = [
created: '2021-12-11T14:26:23.205+00:00',
type: 'application/car',
scope: 'session',
name: 'super special name 2 for mocks',
files: [],
size: 8267,
pin: {
Expand Down Expand Up @@ -81,6 +83,7 @@ export const MOCK_FILES = [
type: 'application/car',
scope: 'session',
files: [],
name: 'super special name 2 for mocks',
size: 414193,
pin: {
cid: 'bafybeiefibctpwu7tm25nxgcn3b5ywwskowkgccl4cbwnvtrgv6rsqjpxa',
Expand Down
Loading