@helia/verified-fetch 0.0.0-7a7c0c1 → 0.0.0-7c3ce21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -6
- package/dist/index.min.js +4 -4
- package/dist/src/index.d.ts +222 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +219 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/types.d.ts +2 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/utils/dag-cbor-to-safe-json.d.ts +7 -0
- package/dist/src/utils/dag-cbor-to-safe-json.d.ts.map +1 -0
- package/dist/src/utils/dag-cbor-to-safe-json.js +37 -0
- package/dist/src/utils/dag-cbor-to-safe-json.js.map +1 -0
- package/dist/src/utils/get-content-disposition-filename.d.ts +6 -0
- package/dist/src/utils/get-content-disposition-filename.d.ts.map +1 -0
- package/dist/src/utils/get-content-disposition-filename.js +16 -0
- package/dist/src/utils/get-content-disposition-filename.js.map +1 -0
- package/dist/src/utils/get-e-tag.d.ts +28 -0
- package/dist/src/utils/get-e-tag.d.ts.map +1 -0
- package/dist/src/utils/get-e-tag.js +18 -0
- package/dist/src/utils/get-e-tag.js.map +1 -0
- package/dist/src/utils/parse-url-string.d.ts +7 -1
- package/dist/src/utils/parse-url-string.d.ts.map +1 -1
- package/dist/src/utils/parse-url-string.js +6 -0
- package/dist/src/utils/parse-url-string.js.map +1 -1
- package/dist/src/utils/responses.d.ts +4 -0
- package/dist/src/utils/responses.d.ts.map +1 -0
- package/dist/src/utils/responses.js +21 -0
- package/dist/src/utils/responses.js.map +1 -0
- package/dist/src/utils/select-output-type.d.ts +12 -0
- package/dist/src/utils/select-output-type.d.ts.map +1 -0
- package/dist/src/utils/select-output-type.js +147 -0
- package/dist/src/utils/select-output-type.js.map +1 -0
- package/dist/src/verified-fetch.d.ts +17 -25
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +220 -143
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +25 -15
- package/src/index.ts +223 -4
- package/src/types.ts +1 -0
- package/src/utils/dag-cbor-to-safe-json.ts +44 -0
- package/src/utils/get-content-disposition-filename.ts +18 -0
- package/src/utils/get-e-tag.ts +36 -0
- package/src/utils/parse-url-string.ts +17 -2
- package/src/utils/responses.ts +22 -0
- package/src/utils/select-output-type.ts +166 -0
- package/src/verified-fetch.ts +251 -153
package/src/verified-fetch.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
import { dagCbor as heliaDagCbor, type DAGCBOR } from '@helia/dag-cbor'
|
|
2
|
-
import { dagJson as heliaDagJson, type DAGJSON } from '@helia/dag-json'
|
|
3
1
|
import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
|
|
4
2
|
import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
|
|
5
|
-
import { json as heliaJson, type JSON } from '@helia/json'
|
|
6
3
|
import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
|
|
7
|
-
import
|
|
8
|
-
import
|
|
4
|
+
import * as ipldDagCbor from '@ipld/dag-cbor'
|
|
5
|
+
import * as ipldDagJson from '@ipld/dag-json'
|
|
9
6
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
10
7
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
11
|
-
import {
|
|
8
|
+
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
9
|
+
import { identity } from 'multiformats/hashes/identity'
|
|
12
10
|
import { CustomProgressEvent } from 'progress-events'
|
|
11
|
+
import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
|
|
12
|
+
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
13
|
+
import { getETag } from './utils/get-e-tag.js'
|
|
13
14
|
import { getStreamFromAsyncIterable } from './utils/get-stream-from-async-iterable.js'
|
|
14
15
|
import { parseResource } from './utils/parse-resource.js'
|
|
15
|
-
import {
|
|
16
|
+
import { notAcceptableResponse, notSupportedResponse, okResponse } from './utils/responses.js'
|
|
17
|
+
import { selectOutputType, queryFormatToAcceptHeader } from './utils/select-output-type.js'
|
|
18
|
+
import { walkPath } from './utils/walk-path.js'
|
|
16
19
|
import type { CIDDetail, ContentTypeParser, Resource, VerifiedFetchInit as VerifiedFetchOptions } from './index.js'
|
|
20
|
+
import type { RequestFormatShorthand } from './types.js'
|
|
17
21
|
import type { Helia } from '@helia/interface'
|
|
18
22
|
import type { AbortOptions, Logger } from '@libp2p/interface'
|
|
19
23
|
import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
|
|
@@ -23,10 +27,6 @@ interface VerifiedFetchComponents {
|
|
|
23
27
|
helia: Helia
|
|
24
28
|
ipns?: IPNS
|
|
25
29
|
unixfs?: HeliaUnixFs
|
|
26
|
-
dagJson?: DAGJSON
|
|
27
|
-
json?: JSON
|
|
28
|
-
dagCbor?: DAGCBOR
|
|
29
|
-
pathWalker?: PathWalkerFn
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -39,8 +39,13 @@ interface VerifiedFetchInit {
|
|
|
39
39
|
interface FetchHandlerFunctionArg {
|
|
40
40
|
cid: CID
|
|
41
41
|
path: string
|
|
42
|
-
terminalElement?: UnixFSEntry
|
|
43
42
|
options?: Omit<VerifiedFetchOptions, 'signal'> & AbortOptions
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* If present, the user has sent an accept header with this value - if the
|
|
46
|
+
* content cannot be represented in this format a 406 should be returned
|
|
47
|
+
*/
|
|
48
|
+
accept?: string
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
interface FetchHandlerFunction {
|
|
@@ -62,18 +67,48 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/**
|
|
71
|
+
* These are Accept header values that will cause content type sniffing to be
|
|
72
|
+
* skipped and set to these values.
|
|
73
|
+
*/
|
|
74
|
+
const RAW_HEADERS = [
|
|
75
|
+
'application/vnd.ipld.raw',
|
|
76
|
+
'application/octet-stream'
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* if the user has specified an `Accept` header, and it's in our list of
|
|
81
|
+
* allowable "raw" format headers, use that instead of detecting the content
|
|
82
|
+
* type. This avoids the user from receiving something different when they
|
|
83
|
+
* signal that they want to `Accept` a specific mime type.
|
|
84
|
+
*/
|
|
85
|
+
function getOverridenRawContentType (headers?: HeadersInit): string | undefined {
|
|
86
|
+
const acceptHeader = new Headers(headers).get('accept') ?? ''
|
|
87
|
+
|
|
88
|
+
// e.g. "Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8"
|
|
89
|
+
const acceptHeaders = acceptHeader.split(',')
|
|
90
|
+
.map(s => s.split(';')[0])
|
|
91
|
+
.map(s => s.trim())
|
|
92
|
+
|
|
93
|
+
for (const mimeType of acceptHeaders) {
|
|
94
|
+
if (mimeType === '*/*') {
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (RAW_HEADERS.includes(mimeType ?? '')) {
|
|
99
|
+
return mimeType
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
65
104
|
export class VerifiedFetch {
|
|
66
105
|
private readonly helia: Helia
|
|
67
106
|
private readonly ipns: IPNS
|
|
68
107
|
private readonly unixfs: HeliaUnixFs
|
|
69
|
-
private readonly dagJson: DAGJSON
|
|
70
|
-
private readonly dagCbor: DAGCBOR
|
|
71
|
-
private readonly json: JSON
|
|
72
|
-
private readonly pathWalker: PathWalkerFn
|
|
73
108
|
private readonly log: Logger
|
|
74
109
|
private readonly contentTypeParser: ContentTypeParser | undefined
|
|
75
110
|
|
|
76
|
-
constructor ({ helia, ipns, unixfs
|
|
111
|
+
constructor ({ helia, ipns, unixfs }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
|
|
77
112
|
this.helia = helia
|
|
78
113
|
this.log = helia.logger.forComponent('helia:verified-fetch')
|
|
79
114
|
this.ipns = ipns ?? heliaIpns(helia, {
|
|
@@ -83,69 +118,122 @@ export class VerifiedFetch {
|
|
|
83
118
|
]
|
|
84
119
|
})
|
|
85
120
|
this.unixfs = unixfs ?? heliaUnixFs(helia)
|
|
86
|
-
this.dagJson = dagJson ?? heliaDagJson(helia)
|
|
87
|
-
this.json = json ?? heliaJson(helia)
|
|
88
|
-
this.dagCbor = dagCbor ?? heliaDagCbor(helia)
|
|
89
|
-
this.pathWalker = pathWalker ?? walkPath
|
|
90
121
|
this.contentTypeParser = init?.contentTypeParser
|
|
91
122
|
this.log.trace('created VerifiedFetch instance')
|
|
92
123
|
}
|
|
93
124
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Accepts an `ipns://...` URL as a string and returns a `Response` containing
|
|
127
|
+
* a raw IPNS record.
|
|
128
|
+
*/
|
|
129
|
+
private async handleIPNSRecord (resource: string, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
130
|
+
return notSupportedResponse('vnd.ipfs.ipns-record support is not implemented')
|
|
99
131
|
}
|
|
100
132
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Accepts a `CID` and returns a `Response` with a body stream that is a CAR
|
|
135
|
+
* of the `DAG` referenced by the `CID`.
|
|
136
|
+
*/
|
|
137
|
+
private async handleCar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
138
|
+
return notSupportedResponse('vnd.ipld.car support is not implemented')
|
|
106
139
|
}
|
|
107
140
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return response
|
|
141
|
+
/**
|
|
142
|
+
* Accepts a UnixFS `CID` and returns a `.tar` file containing the file or
|
|
143
|
+
* directory structure referenced by the `CID`.
|
|
144
|
+
*/
|
|
145
|
+
private async handleTar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
146
|
+
if (cid.code !== dagPbCode) {
|
|
147
|
+
return notAcceptableResponse('only dag-pb CIDs can be returned in TAR files')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return notSupportedResponse('application/tar support is not implemented')
|
|
119
151
|
}
|
|
120
152
|
|
|
121
|
-
private async handleJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
153
|
+
private async handleJson ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
122
154
|
this.log.trace('fetching %c/%s', cid, path)
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
155
|
+
const block = await this.helia.blockstore.get(cid, options)
|
|
156
|
+
let body: string | Uint8Array
|
|
157
|
+
|
|
158
|
+
if (accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
159
|
+
try {
|
|
160
|
+
// if vnd.ipld.dag-cbor has been specified, convert to the format - note
|
|
161
|
+
// that this supports more data types than regular JSON, the content-type
|
|
162
|
+
// response header is set so the user knows to process it differently
|
|
163
|
+
const obj = ipldDagJson.decode(block)
|
|
164
|
+
body = ipldDagCbor.encode(obj)
|
|
165
|
+
} catch (err) {
|
|
166
|
+
this.log.error('could not transform %c to application/vnd.ipld.dag-cbor', err)
|
|
167
|
+
return notAcceptableResponse()
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
// skip decoding
|
|
171
|
+
body = block
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const response = okResponse(body)
|
|
175
|
+
response.headers.set('content-type', accept ?? 'application/json')
|
|
131
176
|
return response
|
|
132
177
|
}
|
|
133
178
|
|
|
134
|
-
private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
179
|
+
private async handleDagCbor ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
135
180
|
this.log.trace('fetching %c/%s', cid, path)
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
181
|
+
|
|
182
|
+
const block = await this.helia.blockstore.get(cid, options)
|
|
183
|
+
let body: string | Uint8Array
|
|
184
|
+
|
|
185
|
+
if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
186
|
+
// skip decoding
|
|
187
|
+
body = block
|
|
188
|
+
} else if (accept === 'application/vnd.ipld.dag-json') {
|
|
189
|
+
try {
|
|
190
|
+
// if vnd.ipld.dag-json has been specified, convert to the format - note
|
|
191
|
+
// that this supports more data types than regular JSON, the content-type
|
|
192
|
+
// response header is set so the user knows to process it differently
|
|
193
|
+
const obj = ipldDagCbor.decode(block)
|
|
194
|
+
body = ipldDagJson.encode(obj)
|
|
195
|
+
} catch (err) {
|
|
196
|
+
this.log.error('could not transform %c to application/vnd.ipld.dag-json', err)
|
|
197
|
+
return notAcceptableResponse()
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
try {
|
|
201
|
+
body = dagCborToSafeJSON(block)
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (accept === 'application/json') {
|
|
204
|
+
this.log('could not decode DAG-CBOR as JSON-safe, but the client sent "Accept: application/json"', err)
|
|
205
|
+
|
|
206
|
+
return notAcceptableResponse()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
|
|
210
|
+
body = block
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const response = okResponse(body)
|
|
215
|
+
|
|
216
|
+
if (accept == null) {
|
|
217
|
+
accept = body instanceof Uint8Array ? 'application/octet-stream' : 'application/json'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
response.headers.set('content-type', accept)
|
|
221
|
+
|
|
144
222
|
return response
|
|
145
223
|
}
|
|
146
224
|
|
|
147
|
-
private async handleDagPb ({ cid, path, options
|
|
148
|
-
|
|
225
|
+
private async handleDagPb ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
226
|
+
let terminalElement: UnixFSEntry | undefined
|
|
227
|
+
let ipfsRoots: CID[] | undefined
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const pathDetails = await walkPath(this.helia.blockstore, `${cid.toString()}/${path}`, options)
|
|
231
|
+
ipfsRoots = pathDetails.ipfsRoots
|
|
232
|
+
terminalElement = pathDetails.terminalElement
|
|
233
|
+
} catch (err) {
|
|
234
|
+
this.log.error('Error walking path %s', path, err)
|
|
235
|
+
}
|
|
236
|
+
|
|
149
237
|
let resolvedCID = terminalElement?.cid ?? cid
|
|
150
238
|
let stat: UnixFSStats
|
|
151
239
|
if (terminalElement?.type === 'directory') {
|
|
@@ -154,7 +242,6 @@ export class VerifiedFetch {
|
|
|
154
242
|
const rootFilePath = 'index.html'
|
|
155
243
|
try {
|
|
156
244
|
this.log.trace('found directory at %c/%s, looking for index.html', cid, path)
|
|
157
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: dirCid, path: rootFilePath }))
|
|
158
245
|
stat = await this.unixfs.stat(dirCid, {
|
|
159
246
|
path: rootFilePath,
|
|
160
247
|
signal: options?.signal,
|
|
@@ -166,36 +253,45 @@ export class VerifiedFetch {
|
|
|
166
253
|
// terminalElement = stat
|
|
167
254
|
} catch (err: any) {
|
|
168
255
|
this.log('error loading path %c/%s', dirCid, rootFilePath, err)
|
|
169
|
-
return
|
|
256
|
+
return notSupportedResponse('Unable to find index.html for directory at given path. Support for directories with implicit root is not implemented')
|
|
170
257
|
} finally {
|
|
171
258
|
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: dirCid, path: rootFilePath }))
|
|
172
259
|
}
|
|
173
260
|
}
|
|
174
261
|
|
|
175
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid: resolvedCID, path: '' }))
|
|
176
262
|
const asyncIter = this.unixfs.cat(resolvedCID, {
|
|
177
263
|
signal: options?.signal,
|
|
178
264
|
onProgress: options?.onProgress
|
|
179
265
|
})
|
|
180
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID, path: '' }))
|
|
181
266
|
this.log('got async iterator for %c/%s', cid, path)
|
|
182
267
|
|
|
183
268
|
const { stream, firstChunk } = await getStreamFromAsyncIterable(asyncIter, path ?? '', this.helia.logger, {
|
|
184
269
|
onProgress: options?.onProgress
|
|
185
270
|
})
|
|
186
|
-
const response =
|
|
271
|
+
const response = okResponse(stream)
|
|
187
272
|
await this.setContentType(firstChunk, path, response)
|
|
188
273
|
|
|
274
|
+
if (ipfsRoots != null) {
|
|
275
|
+
response.headers.set('X-Ipfs-Roots', ipfsRoots.map(cid => cid.toV1().toString()).join(',')) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-roots-response-header
|
|
276
|
+
}
|
|
277
|
+
|
|
189
278
|
return response
|
|
190
279
|
}
|
|
191
280
|
|
|
192
281
|
private async handleRaw ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
282
|
+
const result = await this.helia.blockstore.get(cid, options)
|
|
283
|
+
const response = okResponse(result)
|
|
284
|
+
|
|
285
|
+
// if the user has specified an `Accept` header that corresponds to a raw
|
|
286
|
+
// type, honour that header, so for example they don't request
|
|
287
|
+
// `application/vnd.ipld.raw` but get `application/octet-stream`
|
|
288
|
+
const overriddenContentType = getOverridenRawContentType(options?.headers)
|
|
289
|
+
if (overriddenContentType != null) {
|
|
290
|
+
response.headers.set('content-type', overriddenContentType)
|
|
291
|
+
} else {
|
|
292
|
+
await this.setContentType(result, path, response)
|
|
293
|
+
}
|
|
294
|
+
|
|
199
295
|
return response
|
|
200
296
|
}
|
|
201
297
|
|
|
@@ -226,110 +322,112 @@ export class VerifiedFetch {
|
|
|
226
322
|
}
|
|
227
323
|
|
|
228
324
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
|
|
232
|
-
* @default 'raw'
|
|
233
|
-
*/
|
|
234
|
-
private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat: string | null }): string | null {
|
|
235
|
-
const formatMap: Record<string, string> = {
|
|
236
|
-
'vnd.ipld.raw': 'raw',
|
|
237
|
-
'vnd.ipld.car': 'car',
|
|
238
|
-
'application/x-tar': 'tar',
|
|
239
|
-
'application/vnd.ipld.dag-json': 'dag-json',
|
|
240
|
-
'application/vnd.ipld.dag-cbor': 'dag-cbor',
|
|
241
|
-
'application/json': 'json',
|
|
242
|
-
'application/cbor': 'cbor',
|
|
243
|
-
'vnd.ipfs.ipns-record': 'ipns-record'
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (headerFormat != null) {
|
|
247
|
-
for (const format in formatMap) {
|
|
248
|
-
if (headerFormat.includes(format)) {
|
|
249
|
-
return formatMap[format]
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
} else if (queryFormat != null) {
|
|
253
|
-
return queryFormat
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return null
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Map of format to specific handlers for that format.
|
|
261
|
-
* These format handlers should adjust the response headers as specified in https://specs.ipfs.tech/http-gateways/path-gateway/#response-headers
|
|
325
|
+
* If the user has not specified an Accept header or format query string arg,
|
|
326
|
+
* use the CID codec to choose an appropriate handler for the block data.
|
|
262
327
|
*/
|
|
263
|
-
private readonly formatHandlers: Record<string, FetchHandlerFunction> = {
|
|
264
|
-
raw: async () => new Response('application/vnd.ipld.raw support is not implemented', { status: 501 }),
|
|
265
|
-
car: this.handleIPLDCar,
|
|
266
|
-
'ipns-record': this.handleIPNSRecord,
|
|
267
|
-
tar: async () => new Response('application/x-tar support is not implemented', { status: 501 }),
|
|
268
|
-
'dag-json': async () => new Response('application/vnd.ipld.dag-json support is not implemented', { status: 501 }),
|
|
269
|
-
'dag-cbor': async () => new Response('application/vnd.ipld.dag-cbor support is not implemented', { status: 501 }),
|
|
270
|
-
json: async () => new Response('application/json support is not implemented', { status: 501 }),
|
|
271
|
-
cbor: async () => new Response('application/cbor support is not implemented', { status: 501 })
|
|
272
|
-
}
|
|
273
|
-
|
|
274
328
|
private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
|
|
275
|
-
[dagJsonCode]: this.handleDagJson,
|
|
276
329
|
[dagPbCode]: this.handleDagPb,
|
|
330
|
+
[ipldDagJson.code]: this.handleJson,
|
|
277
331
|
[jsonCode]: this.handleJson,
|
|
278
|
-
[
|
|
279
|
-
[rawCode]: this.handleRaw
|
|
332
|
+
[ipldDagCbor.code]: this.handleDagCbor,
|
|
333
|
+
[rawCode]: this.handleRaw,
|
|
334
|
+
[identity.code]: this.handleRaw
|
|
280
335
|
}
|
|
281
336
|
|
|
282
337
|
async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
338
|
+
this.log('fetch %s', resource)
|
|
339
|
+
|
|
283
340
|
const options = convertOptions(opts)
|
|
284
|
-
const { path, query, ...rest } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
285
|
-
const cid = rest.cid
|
|
286
|
-
let response: Response | undefined
|
|
287
341
|
|
|
288
|
-
|
|
342
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
|
|
289
343
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const formatHandler = this.formatHandlers[format]
|
|
344
|
+
// resolve the CID/path from the requested resource
|
|
345
|
+
const { path, query, cid } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
293
346
|
|
|
294
|
-
|
|
295
|
-
response = await formatHandler.call(this, { cid, path, options })
|
|
347
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
|
|
296
348
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
349
|
+
const requestHeaders = new Headers(options?.headers)
|
|
350
|
+
const incomingAcceptHeader = requestHeaders.get('accept')
|
|
351
|
+
|
|
352
|
+
if (incomingAcceptHeader != null) {
|
|
353
|
+
this.log('incoming accept header "%s"', incomingAcceptHeader)
|
|
301
354
|
}
|
|
302
355
|
|
|
303
|
-
|
|
304
|
-
let ipfsRoots: CID[] | undefined
|
|
356
|
+
const queryFormatMapping = queryFormatToAcceptHeader(query.format)
|
|
305
357
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
ipfsRoots = pathDetails.ipfsRoots
|
|
309
|
-
terminalElement = pathDetails.terminalElement
|
|
310
|
-
} catch (err) {
|
|
311
|
-
this.log.error('Error walking path %s', path, err)
|
|
312
|
-
// return new Response(`Error walking path: ${(err as Error).message}`, { status: 500 })
|
|
358
|
+
if (query.format != null) {
|
|
359
|
+
this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping)
|
|
313
360
|
}
|
|
314
361
|
|
|
315
|
-
|
|
362
|
+
const acceptHeader = incomingAcceptHeader ?? queryFormatMapping
|
|
363
|
+
const accept = selectOutputType(cid, acceptHeader)
|
|
364
|
+
this.log('output type %s', accept)
|
|
365
|
+
|
|
366
|
+
if (acceptHeader != null && accept == null) {
|
|
367
|
+
return notAcceptableResponse()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
let response: Response
|
|
371
|
+
let reqFormat: RequestFormatShorthand | undefined
|
|
372
|
+
|
|
373
|
+
if (accept === 'application/vnd.ipfs.ipns-record') {
|
|
374
|
+
// the user requested a raw IPNS record
|
|
375
|
+
reqFormat = 'ipns-record'
|
|
376
|
+
response = await this.handleIPNSRecord(resource.toString(), options)
|
|
377
|
+
} else if (accept === 'application/vnd.ipld.car') {
|
|
378
|
+
// the user requested a CAR file
|
|
379
|
+
reqFormat = 'car'
|
|
380
|
+
query.download = true
|
|
381
|
+
query.filename = query.filename ?? `${cid.toString()}.car`
|
|
382
|
+
response = await this.handleCar({ cid, path, options })
|
|
383
|
+
} else if (accept === 'application/vnd.ipld.raw') {
|
|
384
|
+
// the user requested a raw block
|
|
385
|
+
reqFormat = 'raw'
|
|
386
|
+
query.download = true
|
|
387
|
+
query.filename = query.filename ?? `${cid.toString()}.bin`
|
|
388
|
+
response = await this.handleRaw({ cid, path, options })
|
|
389
|
+
} else if (accept === 'application/x-tar') {
|
|
390
|
+
// the user requested a TAR file
|
|
391
|
+
reqFormat = 'tar'
|
|
392
|
+
response = await this.handleTar({ cid, path, options })
|
|
393
|
+
} else {
|
|
394
|
+
// derive the handler from the CID type
|
|
316
395
|
const codecHandler = this.codecHandlers[cid.code]
|
|
317
396
|
|
|
318
|
-
if (codecHandler
|
|
319
|
-
|
|
320
|
-
} else {
|
|
321
|
-
return new Response(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`, { status: 501 })
|
|
397
|
+
if (codecHandler == null) {
|
|
398
|
+
return notSupportedResponse(`Support for codec with code ${cid.code} is not yet implemented. Please open an issue at https://github.com/ipfs/helia/issues/new`)
|
|
322
399
|
}
|
|
400
|
+
|
|
401
|
+
response = await codecHandler.call(this, { cid, path, accept, options })
|
|
323
402
|
}
|
|
324
403
|
|
|
325
|
-
response.headers.set('etag', cid
|
|
326
|
-
response.headers.set('cache-
|
|
327
|
-
|
|
404
|
+
response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
|
|
405
|
+
response.headers.set('cache-control', 'public, max-age=29030400, immutable')
|
|
406
|
+
// https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
|
|
407
|
+
response.headers.set('X-Ipfs-Path', resource.toString())
|
|
328
408
|
|
|
329
|
-
|
|
330
|
-
|
|
409
|
+
// set Content-Disposition header
|
|
410
|
+
let contentDisposition: string | undefined
|
|
411
|
+
|
|
412
|
+
// force download if requested
|
|
413
|
+
if (query.download === true) {
|
|
414
|
+
contentDisposition = 'attachment'
|
|
331
415
|
}
|
|
332
|
-
|
|
416
|
+
|
|
417
|
+
// override filename if requested
|
|
418
|
+
if (query.filename != null) {
|
|
419
|
+
if (contentDisposition == null) {
|
|
420
|
+
contentDisposition = 'inline'
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
contentDisposition = `${contentDisposition}; ${getContentDispositionFilename(query.filename)}`
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (contentDisposition != null) {
|
|
427
|
+
response.headers.set('Content-Disposition', contentDisposition)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
|
|
333
431
|
|
|
334
432
|
return response
|
|
335
433
|
}
|