Skip to content

Commit 7a83045

Browse files
authored
feat: api get from bucket and worker binding gateway read from gateway api (#140)
1 parent 990e4f6 commit 7a83045

File tree

16 files changed

+462
-411
lines changed

16 files changed

+462
-411
lines changed

packages/api/src/errors.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,19 @@ export class TokenNotFoundError extends HTTPError {
8989
}
9090
TokenNotFoundError.CODE = 'ERROR_TOKEN_NOT_FOUND'
9191

92+
export class UrlNotFoundError extends Error {
93+
/**
94+
* @param {string} message
95+
*/
96+
constructor(message = 'URL Not Found') {
97+
super(message)
98+
this.name = 'UrlNotFoundError'
99+
this.status = 404
100+
this.code = UrlNotFoundError.CODE
101+
}
102+
}
103+
UrlNotFoundError.CODE = 'ERROR_URL_NOT_FOUND'
104+
92105
export class UnrecognisedTokenError extends HTTPError {
93106
constructor(msg = 'Could not parse provided API token') {
94107
super(msg, 401)

packages/api/src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Router } from 'itty-router'
55
import { withAuth } from './auth.js'
66
import {
77
permaCachePost,
8+
permaCacheGet,
89
permaCacheListGet,
910
permaCacheAccountGet,
1011
permaCacheDelete,
@@ -32,6 +33,7 @@ router
3233
.get('/perma-cache/status', (request) => {
3334
return Response.redirect(request.url.replace('status', 'account'), 302)
3435
})
36+
.get('/perma-cache/:url', permaCacheGet)
3537
.delete('/perma-cache/:url', auth['🔒'](permaCacheDelete))
3638

3739
/**

packages/api/src/perma-cache/delete.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/* eslint-env serviceworker, browser */
22

3-
// TODO: Move to separate file
4-
import { getSourceUrl, getNormalizedUrl } from './post.js'
3+
import { getSourceUrl, getNormalizedUrl } from '../utils/url.js'
54
import { JSONResponse } from '../utils/json-response.js'
65
/**
76
* @typedef {import('../env').Env} Env

packages/api/src/perma-cache/get.js

Lines changed: 12 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,96 +1,28 @@
11
/* eslint-env serviceworker, browser */
22
/* global Response */
33

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

67
/**
78
* @typedef {import('../env').Env} Env
89
*/
910

1011
/**
11-
* Handle perma-cache put request
12+
* Handle perma-cache get request
1213
*
1314
* @param {Request} request
1415
* @param {Env} env
1516
*/
16-
export async function permaCacheListGet(request, env) {
17-
const requestUrl = new URL(request.url)
18-
const { searchParams } = requestUrl
19-
const { size, page, sort, order } = parseSearchParams(searchParams)
20-
21-
const entries = await env.db.listPermaCache(request.auth.user.id, {
22-
size,
23-
page,
24-
sort,
25-
order,
26-
})
27-
28-
// Get next page link
29-
const headers =
30-
entries.length === size
31-
? {
32-
Link: `<${requestUrl.pathname}?size=${size}&page=${
33-
page + 1
34-
}>; rel="next"`,
35-
}
36-
: undefined
37-
return new JSONResponse(entries, { headers })
38-
}
39-
40-
/**
41-
* @param {URLSearchParams} searchParams
42-
*/
43-
function parseSearchParams(searchParams) {
44-
// Parse size parameter
45-
let size = 25
46-
if (searchParams.has('size')) {
47-
const parsedSize = parseInt(searchParams.get('size'))
48-
if (isNaN(parsedSize) || parsedSize <= 0 || parsedSize > 1000) {
49-
throw Object.assign(new Error('invalid page size'), { status: 400 })
50-
}
51-
size = parsedSize
17+
export async function permaCacheGet(request, env) {
18+
const sourceUrl = getSourceUrl(request, env)
19+
const normalizedUrl = getNormalizedUrl(sourceUrl, env)
20+
const r2Key = normalizedUrl.toString()
21+
22+
const r2Object = await env.SUPERHOT.get(r2Key)
23+
if (r2Object) {
24+
return new Response(r2Object.body)
5225
}
5326

54-
// Parse cursor parameter
55-
let page = 0
56-
if (searchParams.has('page')) {
57-
const parsedPage = parseInt(searchParams.get('page'))
58-
if (isNaN(parsedPage) || parsedPage <= 0) {
59-
throw Object.assign(new Error('invalid page number'), { status: 400 })
60-
}
61-
page = parsedPage
62-
}
63-
64-
// Parse sort parameter
65-
let sort = 'date'
66-
if (searchParams.has('sort')) {
67-
const parsedSort = searchParams.get('sort')
68-
if (parsedSort !== 'date' && parsedSort !== 'size') {
69-
throw Object.assign(
70-
new Error('invalid list sort, either "date" or "size"'),
71-
{ status: 400 }
72-
)
73-
}
74-
sort = parsedSort
75-
}
76-
77-
// Parse order parameter
78-
let order = 'asc'
79-
if (searchParams.has('order')) {
80-
const parsedOrder = searchParams.get('order')
81-
if (parsedOrder !== 'asc' && parsedOrder !== 'desc') {
82-
throw Object.assign(
83-
new Error('invalid list sort order, either "asc" or "desc"'),
84-
{ status: 400 }
85-
)
86-
}
87-
sort = parsedOrder
88-
}
89-
90-
return {
91-
size,
92-
page,
93-
sort,
94-
order,
95-
}
27+
throw new UrlNotFoundError()
9628
}

packages/api/src/perma-cache/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { permaCachePost } from './post.js'
2-
export { permaCacheListGet } from './get.js'
2+
export { permaCacheGet } from './get.js'
33
export { permaCacheAccountGet } from './account.js'
4+
export { permaCacheListGet } from './list.js'
45
export { permaCacheDelete } from './delete.js'

packages/api/src/perma-cache/list.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/* eslint-env serviceworker, browser */
2+
/* global Response */
3+
4+
import { JSONResponse } from '../utils/json-response.js'
5+
6+
/**
7+
* @typedef {import('../env').Env} Env
8+
*/
9+
10+
/**
11+
* Handle perma-cache list get request
12+
*
13+
* @param {Request} request
14+
* @param {Env} env
15+
*/
16+
export async function permaCacheListGet(request, env) {
17+
const requestUrl = new URL(request.url)
18+
const { searchParams } = requestUrl
19+
const { size, page, sort, order } = parseSearchParams(searchParams)
20+
21+
const entries = await env.db.listPermaCache(request.auth.user.id, {
22+
size,
23+
page,
24+
sort,
25+
order,
26+
})
27+
28+
// Get next page link
29+
const headers =
30+
entries.length === size
31+
? {
32+
Link: `<${requestUrl.pathname}?size=${size}&page=${
33+
page + 1
34+
}>; rel="next"`,
35+
}
36+
: undefined
37+
return new JSONResponse(entries, { headers })
38+
}
39+
40+
/**
41+
* @param {URLSearchParams} searchParams
42+
*/
43+
function parseSearchParams(searchParams) {
44+
// Parse size parameter
45+
let size = 25
46+
if (searchParams.has('size')) {
47+
const parsedSize = parseInt(searchParams.get('size'))
48+
if (isNaN(parsedSize) || parsedSize <= 0 || parsedSize > 1000) {
49+
throw Object.assign(new Error('invalid page size'), { status: 400 })
50+
}
51+
size = parsedSize
52+
}
53+
54+
// Parse cursor parameter
55+
let page = 0
56+
if (searchParams.has('page')) {
57+
const parsedPage = parseInt(searchParams.get('page'))
58+
if (isNaN(parsedPage) || parsedPage <= 0) {
59+
throw Object.assign(new Error('invalid page number'), { status: 400 })
60+
}
61+
page = parsedPage
62+
}
63+
64+
// Parse sort parameter
65+
let sort = 'date'
66+
if (searchParams.has('sort')) {
67+
const parsedSort = searchParams.get('sort')
68+
if (parsedSort !== 'date' && parsedSort !== 'size') {
69+
throw Object.assign(
70+
new Error('invalid list sort, either "date" or "size"'),
71+
{ status: 400 }
72+
)
73+
}
74+
sort = parsedSort
75+
}
76+
77+
// Parse order parameter
78+
let order = 'asc'
79+
if (searchParams.has('order')) {
80+
const parsedOrder = searchParams.get('order')
81+
if (parsedOrder !== 'asc' && parsedOrder !== 'desc') {
82+
throw Object.assign(
83+
new Error('invalid list sort order, either "asc" or "desc"'),
84+
{ status: 400 }
85+
)
86+
}
87+
sort = parsedOrder
88+
}
89+
90+
return {
91+
size,
92+
page,
93+
sort,
94+
order,
95+
}
96+
}

packages/api/src/perma-cache/post.js

Lines changed: 3 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
11
/* eslint-env serviceworker, browser */
22
/* global Response */
33

4-
import {
5-
MAX_ALLOWED_URL_LENGTH,
6-
INVALID_PERMA_CACHE_CACHE_CONTROL_DIRECTIVES,
7-
} from '../constants.js'
8-
import {
9-
InvalidUrlError,
10-
TimeoutError,
11-
HTTPError,
12-
ExpectationFailedError,
13-
} from '../errors.js'
4+
import { INVALID_PERMA_CACHE_CACHE_CONTROL_DIRECTIVES } from '../constants.js'
5+
import { TimeoutError, HTTPError, ExpectationFailedError } from '../errors.js'
146
import { JSONResponse } from '../utils/json-response.js'
15-
import { normalizeCid } from '../utils/cid.js'
7+
import { getSourceUrl, getNormalizedUrl } from '../utils/url.js'
168

179
/**
1810
* @typedef {import('../env').Env} Env
@@ -114,85 +106,6 @@ async function getResponse(request, env, url) {
114106
return response
115107
}
116108

117-
/**
118-
* Verify if provided url is a valid nftstorage.link URL
119-
* Returns subdomain format.
120-
*
121-
* @param {Request} request
122-
* @param {Env} env
123-
*/
124-
export function getSourceUrl(request, env) {
125-
let candidateUrl
126-
try {
127-
candidateUrl = new URL(decodeURIComponent(request.params.url))
128-
} catch (err) {
129-
throw new InvalidUrlError(
130-
`invalid URL provided: ${request.params.url}: ${err.message}`
131-
)
132-
}
133-
134-
const urlString = candidateUrl.toString()
135-
if (urlString.length > MAX_ALLOWED_URL_LENGTH) {
136-
throw new InvalidUrlError(
137-
`invalid URL provided: ${request.params.url}: maximum allowed length or URL is ${MAX_ALLOWED_URL_LENGTH}`
138-
)
139-
}
140-
if (!urlString.includes(env.GATEWAY_DOMAIN)) {
141-
throw new InvalidUrlError(
142-
`invalid URL provided: ${urlString}: not nftstorage.link URL`
143-
)
144-
}
145-
146-
return candidateUrl
147-
}
148-
149-
/**
150-
* Verify if candidate url has IPFS path or IPFS subdomain, returning subdomain format.
151-
*
152-
* @param {URL} candidateUrl
153-
* @param {Env} env
154-
*/
155-
export function getNormalizedUrl(candidateUrl, env) {
156-
// Verify if IPFS path resolution URL
157-
const ipfsPathParts = candidateUrl.pathname.split('/ipfs/')
158-
if (ipfsPathParts.length > 1) {
159-
const pathParts = ipfsPathParts[1].split(/\/(.*)/s)
160-
const cid = getCid(pathParts[0])
161-
162-
// Parse path + query params
163-
const path = pathParts[1] ? `/${pathParts[1]}` : ''
164-
const queryParamsCandidate = candidateUrl.searchParams.toString()
165-
const queryParams = queryParamsCandidate.length
166-
? `?${queryParamsCandidate}`
167-
: ''
168-
169-
return new URL(
170-
`${candidateUrl.protocol}//${cid}.ipfs.${env.GATEWAY_DOMAIN}${path}${queryParams}`
171-
)
172-
}
173-
174-
// Verify if subdomain resolution URL
175-
const subdomainParts = candidateUrl.hostname.split('.ipfs.')
176-
if (subdomainParts.length <= 1) {
177-
throw new InvalidUrlError(
178-
`invalid URL provided: ${candidateUrl}: not subdomain nor ipfs path available`
179-
)
180-
}
181-
182-
return candidateUrl
183-
}
184-
185-
/**
186-
* @param {string} candidateCid
187-
*/
188-
function getCid(candidateCid) {
189-
try {
190-
return normalizeCid(candidateCid)
191-
} catch (err) {
192-
throw new InvalidUrlError(`invalid CID: ${candidateCid}: ${err.message}`)
193-
}
194-
}
195-
196109
/**
197110
* Validates cache control header to verify if we should perma cache the response.
198111
* Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control

0 commit comments

Comments
 (0)