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: api get from bucket #140

Merged
merged 2 commits into from
Jul 6, 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
13 changes: 13 additions & 0 deletions packages/api/src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ export class TokenNotFoundError extends HTTPError {
}
TokenNotFoundError.CODE = 'ERROR_TOKEN_NOT_FOUND'

export class UrlNotFoundError extends Error {
/**
* @param {string} message
*/
constructor(message = 'URL Not Found') {
super(message)
this.name = 'UrlNotFoundError'
this.status = 404
this.code = UrlNotFoundError.CODE
}
}
UrlNotFoundError.CODE = 'ERROR_URL_NOT_FOUND'

export class UnrecognisedTokenError extends HTTPError {
constructor(msg = 'Could not parse provided API token') {
super(msg, 401)
Expand Down
2 changes: 2 additions & 0 deletions packages/api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Router } from 'itty-router'
import { withAuth } from './auth.js'
import {
permaCachePost,
permaCacheGet,
permaCacheListGet,
permaCacheAccountGet,
permaCacheDelete,
Expand Down Expand Up @@ -32,6 +33,7 @@ router
.get('/perma-cache/status', (request) => {
return Response.redirect(request.url.replace('status', 'account'), 302)
})
.get('/perma-cache/:url', permaCacheGet)
.delete('/perma-cache/:url', auth['🔒'](permaCacheDelete))

/**
Expand Down
3 changes: 1 addition & 2 deletions packages/api/src/perma-cache/delete.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-env serviceworker, browser */

// TODO: Move to separate file
import { getSourceUrl, getNormalizedUrl } from './post.js'
import { getSourceUrl, getNormalizedUrl } from '../utils/url.js'
import { JSONResponse } from '../utils/json-response.js'
/**
* @typedef {import('../env').Env} Env
Expand Down
92 changes: 12 additions & 80 deletions packages/api/src/perma-cache/get.js
Original file line number Diff line number Diff line change
@@ -1,96 +1,28 @@
/* eslint-env serviceworker, browser */
/* global Response */

import { JSONResponse } from '../utils/json-response.js'
import { getSourceUrl, getNormalizedUrl } from '../utils/url.js'
import { UrlNotFoundError } from '../errors.js'

/**
* @typedef {import('../env').Env} Env
*/

/**
* Handle perma-cache put request
* Handle perma-cache get request
*
* @param {Request} request
* @param {Env} env
*/
export async function permaCacheListGet(request, env) {
const requestUrl = new URL(request.url)
const { searchParams } = requestUrl
const { size, page, sort, order } = parseSearchParams(searchParams)

const entries = await env.db.listPermaCache(request.auth.user.id, {
size,
page,
sort,
order,
})

// Get next page link
const headers =
entries.length === size
? {
Link: `<${requestUrl.pathname}?size=${size}&page=${
page + 1
}>; rel="next"`,
}
: undefined
return new JSONResponse(entries, { headers })
}

/**
* @param {URLSearchParams} searchParams
*/
function parseSearchParams(searchParams) {
// Parse size parameter
let size = 25
if (searchParams.has('size')) {
const parsedSize = parseInt(searchParams.get('size'))
if (isNaN(parsedSize) || parsedSize <= 0 || parsedSize > 1000) {
throw Object.assign(new Error('invalid page size'), { status: 400 })
}
size = parsedSize
export async function permaCacheGet(request, env) {
const sourceUrl = getSourceUrl(request, env)
const normalizedUrl = getNormalizedUrl(sourceUrl, env)
const r2Key = normalizedUrl.toString()

const r2Object = await env.SUPERHOT.get(r2Key)
if (r2Object) {
return new Response(r2Object.body)
}

// Parse cursor parameter
let page = 0
if (searchParams.has('page')) {
const parsedPage = parseInt(searchParams.get('page'))
if (isNaN(parsedPage) || parsedPage <= 0) {
throw Object.assign(new Error('invalid page number'), { status: 400 })
}
page = parsedPage
}

// Parse sort parameter
let sort = 'date'
if (searchParams.has('sort')) {
const parsedSort = searchParams.get('sort')
if (parsedSort !== 'date' && parsedSort !== 'size') {
throw Object.assign(
new Error('invalid list sort, either "date" or "size"'),
{ status: 400 }
)
}
sort = parsedSort
}

// Parse order parameter
let order = 'asc'
if (searchParams.has('order')) {
const parsedOrder = searchParams.get('order')
if (parsedOrder !== 'asc' && parsedOrder !== 'desc') {
throw Object.assign(
new Error('invalid list sort order, either "asc" or "desc"'),
{ status: 400 }
)
}
sort = parsedOrder
}

return {
size,
page,
sort,
order,
}
throw new UrlNotFoundError()
}
3 changes: 2 additions & 1 deletion packages/api/src/perma-cache/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { permaCachePost } from './post.js'
export { permaCacheListGet } from './get.js'
export { permaCacheGet } from './get.js'
export { permaCacheAccountGet } from './account.js'
export { permaCacheListGet } from './list.js'
export { permaCacheDelete } from './delete.js'
96 changes: 96 additions & 0 deletions packages/api/src/perma-cache/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* eslint-env serviceworker, browser */
/* global Response */

import { JSONResponse } from '../utils/json-response.js'

/**
* @typedef {import('../env').Env} Env
*/

/**
* Handle perma-cache list get request
*
* @param {Request} request
* @param {Env} env
*/
export async function permaCacheListGet(request, env) {
const requestUrl = new URL(request.url)
const { searchParams } = requestUrl
const { size, page, sort, order } = parseSearchParams(searchParams)

const entries = await env.db.listPermaCache(request.auth.user.id, {
size,
page,
sort,
order,
})

// Get next page link
const headers =
entries.length === size
? {
Link: `<${requestUrl.pathname}?size=${size}&page=${
page + 1
}>; rel="next"`,
}
: undefined
return new JSONResponse(entries, { headers })
}

/**
* @param {URLSearchParams} searchParams
*/
function parseSearchParams(searchParams) {
// Parse size parameter
let size = 25
if (searchParams.has('size')) {
const parsedSize = parseInt(searchParams.get('size'))
if (isNaN(parsedSize) || parsedSize <= 0 || parsedSize > 1000) {
throw Object.assign(new Error('invalid page size'), { status: 400 })
}
size = parsedSize
}

// Parse cursor parameter
let page = 0
if (searchParams.has('page')) {
const parsedPage = parseInt(searchParams.get('page'))
if (isNaN(parsedPage) || parsedPage <= 0) {
throw Object.assign(new Error('invalid page number'), { status: 400 })
}
page = parsedPage
}

// Parse sort parameter
let sort = 'date'
if (searchParams.has('sort')) {
const parsedSort = searchParams.get('sort')
if (parsedSort !== 'date' && parsedSort !== 'size') {
throw Object.assign(
new Error('invalid list sort, either "date" or "size"'),
{ status: 400 }
)
}
sort = parsedSort
}

// Parse order parameter
let order = 'asc'
if (searchParams.has('order')) {
const parsedOrder = searchParams.get('order')
if (parsedOrder !== 'asc' && parsedOrder !== 'desc') {
throw Object.assign(
new Error('invalid list sort order, either "asc" or "desc"'),
{ status: 400 }
)
}
sort = parsedOrder
}

return {
size,
page,
sort,
order,
}
}
93 changes: 3 additions & 90 deletions packages/api/src/perma-cache/post.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
/* eslint-env serviceworker, browser */
/* global Response */

import {
MAX_ALLOWED_URL_LENGTH,
INVALID_PERMA_CACHE_CACHE_CONTROL_DIRECTIVES,
} from '../constants.js'
import {
InvalidUrlError,
TimeoutError,
HTTPError,
ExpectationFailedError,
} from '../errors.js'
import { INVALID_PERMA_CACHE_CACHE_CONTROL_DIRECTIVES } from '../constants.js'
import { TimeoutError, HTTPError, ExpectationFailedError } from '../errors.js'
import { JSONResponse } from '../utils/json-response.js'
import { normalizeCid } from '../utils/cid.js'
import { getSourceUrl, getNormalizedUrl } from '../utils/url.js'

/**
* @typedef {import('../env').Env} Env
Expand Down Expand Up @@ -114,85 +106,6 @@ async function getResponse(request, env, url) {
return response
}

/**
* Verify if provided url is a valid nftstorage.link URL
* Returns subdomain format.
*
* @param {Request} request
* @param {Env} env
*/
export function getSourceUrl(request, env) {
let candidateUrl
try {
candidateUrl = new URL(decodeURIComponent(request.params.url))
} catch (err) {
throw new InvalidUrlError(
`invalid URL provided: ${request.params.url}: ${err.message}`
)
}

const urlString = candidateUrl.toString()
if (urlString.length > MAX_ALLOWED_URL_LENGTH) {
throw new InvalidUrlError(
`invalid URL provided: ${request.params.url}: maximum allowed length or URL is ${MAX_ALLOWED_URL_LENGTH}`
)
}
if (!urlString.includes(env.GATEWAY_DOMAIN)) {
throw new InvalidUrlError(
`invalid URL provided: ${urlString}: not nftstorage.link URL`
)
}

return candidateUrl
}

/**
* Verify if candidate url has IPFS path or IPFS subdomain, returning subdomain format.
*
* @param {URL} candidateUrl
* @param {Env} env
*/
export function getNormalizedUrl(candidateUrl, env) {
// Verify if IPFS path resolution URL
const ipfsPathParts = candidateUrl.pathname.split('/ipfs/')
if (ipfsPathParts.length > 1) {
const pathParts = ipfsPathParts[1].split(/\/(.*)/s)
const cid = getCid(pathParts[0])

// Parse path + query params
const path = pathParts[1] ? `/${pathParts[1]}` : ''
const queryParamsCandidate = candidateUrl.searchParams.toString()
const queryParams = queryParamsCandidate.length
? `?${queryParamsCandidate}`
: ''

return new URL(
`${candidateUrl.protocol}//${cid}.ipfs.${env.GATEWAY_DOMAIN}${path}${queryParams}`
)
}

// Verify if subdomain resolution URL
const subdomainParts = candidateUrl.hostname.split('.ipfs.')
if (subdomainParts.length <= 1) {
throw new InvalidUrlError(
`invalid URL provided: ${candidateUrl}: not subdomain nor ipfs path available`
)
}

return candidateUrl
}

/**
* @param {string} candidateCid
*/
function getCid(candidateCid) {
try {
return normalizeCid(candidateCid)
} catch (err) {
throw new InvalidUrlError(`invalid CID: ${candidateCid}: ${err.message}`)
}
}

/**
* Validates cache control header to verify if we should perma cache the response.
* Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
Expand Down
Loading