@helia/verified-fetch 0.0.0-7c07e11 → 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 +33 -0
- package/dist/index.min.js +4 -4
- package/dist/src/index.d.ts +36 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +33 -0
- package/dist/src/index.js.map +1 -1
- 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/parse-url-string.d.ts +2 -0
- 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 -15
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +206 -130
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +16 -12
- package/src/index.ts +37 -0
- package/src/utils/get-content-disposition-filename.ts +18 -0
- package/src/utils/parse-url-string.ts +11 -1
- package/src/utils/responses.ts +22 -0
- package/src/utils/select-output-type.ts +166 -0
- package/src/verified-fetch.ts +230 -135
package/src/verified-fetch.ts
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { ipns as heliaIpns, type IPNS } from '@helia/ipns'
|
|
2
2
|
import { dnsJsonOverHttps } from '@helia/ipns/dns-resolvers'
|
|
3
3
|
import { unixfs as heliaUnixFs, type UnixFS as HeliaUnixFs, type UnixFSStats } from '@helia/unixfs'
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import * as ipldDagCbor from '@ipld/dag-cbor'
|
|
5
|
+
import * as ipldDagJson from '@ipld/dag-json'
|
|
6
6
|
import { code as dagPbCode } from '@ipld/dag-pb'
|
|
7
7
|
import { code as jsonCode } from 'multiformats/codecs/json'
|
|
8
8
|
import { code as rawCode } from 'multiformats/codecs/raw'
|
|
9
9
|
import { identity } from 'multiformats/hashes/identity'
|
|
10
10
|
import { CustomProgressEvent } from 'progress-events'
|
|
11
11
|
import { dagCborToSafeJSON } from './utils/dag-cbor-to-safe-json.js'
|
|
12
|
+
import { getContentDispositionFilename } from './utils/get-content-disposition-filename.js'
|
|
12
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'
|
|
17
20
|
import type { RequestFormatShorthand } from './types.js'
|
|
18
21
|
import type { Helia } from '@helia/interface'
|
|
@@ -24,7 +27,6 @@ interface VerifiedFetchComponents {
|
|
|
24
27
|
helia: Helia
|
|
25
28
|
ipns?: IPNS
|
|
26
29
|
unixfs?: HeliaUnixFs
|
|
27
|
-
pathWalker?: PathWalkerFn
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
@@ -37,8 +39,13 @@ interface VerifiedFetchInit {
|
|
|
37
39
|
interface FetchHandlerFunctionArg {
|
|
38
40
|
cid: CID
|
|
39
41
|
path: string
|
|
40
|
-
terminalElement?: UnixFSEntry
|
|
41
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
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
interface FetchHandlerFunction {
|
|
@@ -60,29 +67,48 @@ function convertOptions (options?: VerifiedFetchOptions): (Omit<VerifiedFetchOpt
|
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
]
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
+
}
|
|
75
102
|
}
|
|
76
103
|
|
|
77
104
|
export class VerifiedFetch {
|
|
78
105
|
private readonly helia: Helia
|
|
79
106
|
private readonly ipns: IPNS
|
|
80
107
|
private readonly unixfs: HeliaUnixFs
|
|
81
|
-
private readonly pathWalker: PathWalkerFn
|
|
82
108
|
private readonly log: Logger
|
|
83
109
|
private readonly contentTypeParser: ContentTypeParser | undefined
|
|
84
110
|
|
|
85
|
-
constructor ({ helia, ipns, unixfs
|
|
111
|
+
constructor ({ helia, ipns, unixfs }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
|
|
86
112
|
this.helia = helia
|
|
87
113
|
this.log = helia.logger.forComponent('helia:verified-fetch')
|
|
88
114
|
this.ipns = ipns ?? heliaIpns(helia, {
|
|
@@ -92,60 +118,122 @@ export class VerifiedFetch {
|
|
|
92
118
|
]
|
|
93
119
|
})
|
|
94
120
|
this.unixfs = unixfs ?? heliaUnixFs(helia)
|
|
95
|
-
this.pathWalker = pathWalker ?? walkPath
|
|
96
121
|
this.contentTypeParser = init?.contentTypeParser
|
|
97
122
|
this.log.trace('created VerifiedFetch instance')
|
|
98
123
|
}
|
|
99
124
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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')
|
|
105
131
|
}
|
|
106
132
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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')
|
|
139
|
+
}
|
|
140
|
+
|
|
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')
|
|
112
151
|
}
|
|
113
152
|
|
|
114
|
-
private async handleJson ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
153
|
+
private async handleJson ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
115
154
|
this.log.trace('fetching %c/%s', cid, path)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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')
|
|
124
176
|
return response
|
|
125
177
|
}
|
|
126
178
|
|
|
127
|
-
private async handleDagCbor ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
179
|
+
private async handleDagCbor ({ cid, path, accept, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
128
180
|
this.log.trace('fetching %c/%s', cid, path)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const block = await this.helia.blockstore.get(cid)
|
|
181
|
+
|
|
182
|
+
const block = await this.helia.blockstore.get(cid, options)
|
|
132
183
|
let body: string | Uint8Array
|
|
133
184
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
} catch (err) {
|
|
137
|
-
this.log('could not decode DAG-CBOR as JSON-safe, falling back to `application/octet-stream`', err)
|
|
185
|
+
if (accept === 'application/octet-stream' || accept === 'application/vnd.ipld.dag-cbor' || accept === 'application/cbor') {
|
|
186
|
+
// skip decoding
|
|
138
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
|
+
}
|
|
139
212
|
}
|
|
140
213
|
|
|
141
214
|
const response = okResponse(body)
|
|
142
|
-
|
|
143
|
-
|
|
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,
|
|
@@ -172,7 +259,6 @@ export class VerifiedFetch {
|
|
|
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
|
|
@@ -185,19 +271,27 @@ export class VerifiedFetch {
|
|
|
185
271
|
const response = okResponse(stream)
|
|
186
272
|
await this.setContentType(firstChunk, path, response)
|
|
187
273
|
|
|
188
|
-
|
|
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
|
+
}
|
|
189
277
|
|
|
190
278
|
return response
|
|
191
279
|
}
|
|
192
280
|
|
|
193
281
|
private async handleRaw ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
|
|
194
|
-
this.
|
|
195
|
-
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
|
|
196
|
-
const result = await this.helia.blockstore.get(cid)
|
|
282
|
+
const result = await this.helia.blockstore.get(cid, options)
|
|
197
283
|
const response = okResponse(result)
|
|
198
|
-
await this.setContentType(result, path, response)
|
|
199
284
|
|
|
200
|
-
|
|
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
|
+
|
|
201
295
|
return response
|
|
202
296
|
}
|
|
203
297
|
|
|
@@ -228,111 +322,112 @@ export class VerifiedFetch {
|
|
|
228
322
|
}
|
|
229
323
|
|
|
230
324
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* @see https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter
|
|
234
|
-
* @default 'raw'
|
|
235
|
-
*/
|
|
236
|
-
private getFormat ({ headerFormat, queryFormat }: { headerFormat: string | null, queryFormat: RequestFormatShorthand | null }): RequestFormatShorthand | null {
|
|
237
|
-
const formatMap: Record<string, RequestFormatShorthand> = {
|
|
238
|
-
'vnd.ipld.raw': 'raw',
|
|
239
|
-
'vnd.ipld.car': 'car',
|
|
240
|
-
'application/x-tar': 'tar',
|
|
241
|
-
'application/vnd.ipld.dag-json': 'dag-json',
|
|
242
|
-
'application/vnd.ipld.dag-cbor': 'dag-cbor',
|
|
243
|
-
'application/json': 'json',
|
|
244
|
-
'application/cbor': 'cbor',
|
|
245
|
-
'vnd.ipfs.ipns-record': 'ipns-record'
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (headerFormat != null) {
|
|
249
|
-
for (const format in formatMap) {
|
|
250
|
-
if (headerFormat.includes(format)) {
|
|
251
|
-
return formatMap[format]
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
} else if (queryFormat != null) {
|
|
255
|
-
return queryFormat
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return null
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Map of format to specific handlers for that format.
|
|
263
|
-
* 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.
|
|
264
327
|
*/
|
|
265
|
-
private readonly formatHandlers: Record<string, FetchHandlerFunction> = {
|
|
266
|
-
raw: async () => notSupportedResponse('application/vnd.ipld.raw support is not implemented'),
|
|
267
|
-
car: this.handleIPLDCar,
|
|
268
|
-
'ipns-record': this.handleIPNSRecord,
|
|
269
|
-
tar: async () => notSupportedResponse('application/x-tar support is not implemented'),
|
|
270
|
-
'dag-json': async () => notSupportedResponse('application/vnd.ipld.dag-json support is not implemented'),
|
|
271
|
-
'dag-cbor': async () => notSupportedResponse('application/vnd.ipld.dag-cbor support is not implemented'),
|
|
272
|
-
json: async () => notSupportedResponse('application/json support is not implemented'),
|
|
273
|
-
cbor: async () => notSupportedResponse('application/cbor support is not implemented')
|
|
274
|
-
}
|
|
275
|
-
|
|
276
328
|
private readonly codecHandlers: Record<number, FetchHandlerFunction> = {
|
|
277
329
|
[dagPbCode]: this.handleDagPb,
|
|
278
|
-
[
|
|
330
|
+
[ipldDagJson.code]: this.handleJson,
|
|
279
331
|
[jsonCode]: this.handleJson,
|
|
280
|
-
[
|
|
332
|
+
[ipldDagCbor.code]: this.handleDagCbor,
|
|
281
333
|
[rawCode]: this.handleRaw,
|
|
282
334
|
[identity.code]: this.handleRaw
|
|
283
335
|
}
|
|
284
336
|
|
|
285
337
|
async fetch (resource: Resource, opts?: VerifiedFetchOptions): Promise<Response> {
|
|
338
|
+
this.log('fetch %s', resource)
|
|
339
|
+
|
|
286
340
|
const options = convertOptions(opts)
|
|
287
|
-
const { path, query, ...rest } = await parseResource(resource, { ipns: this.ipns, logger: this.helia.logger }, options)
|
|
288
|
-
const cid = rest.cid
|
|
289
|
-
let response: Response | undefined
|
|
290
341
|
|
|
291
|
-
|
|
342
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
|
|
292
343
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
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)
|
|
296
346
|
|
|
297
|
-
|
|
298
|
-
response = await formatHandler.call(this, { cid, path, options })
|
|
347
|
+
options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
|
|
299
348
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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)
|
|
304
354
|
}
|
|
305
355
|
|
|
306
|
-
|
|
307
|
-
let ipfsRoots: CID[] | undefined
|
|
356
|
+
const queryFormatMapping = queryFormatToAcceptHeader(query.format)
|
|
308
357
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
358
|
+
if (query.format != null) {
|
|
359
|
+
this.log('incoming query format "%s", mapped to %s', query.format, queryFormatMapping)
|
|
360
|
+
}
|
|
361
|
+
|
|
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()
|
|
316
368
|
}
|
|
317
369
|
|
|
318
|
-
|
|
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
|
|
319
395
|
const codecHandler = this.codecHandlers[cid.code]
|
|
320
396
|
|
|
321
|
-
if (codecHandler
|
|
322
|
-
response = await codecHandler.call(this, { cid, path, options, terminalElement })
|
|
323
|
-
} else {
|
|
397
|
+
if (codecHandler == null) {
|
|
324
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`)
|
|
325
399
|
}
|
|
400
|
+
|
|
401
|
+
response = await codecHandler.call(this, { cid, path, accept, options })
|
|
326
402
|
}
|
|
327
403
|
|
|
328
|
-
response.headers.set('etag', getETag({ cid, reqFormat
|
|
404
|
+
response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
|
|
329
405
|
response.headers.set('cache-control', 'public, max-age=29030400, immutable')
|
|
330
|
-
|
|
406
|
+
// https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
|
|
407
|
+
response.headers.set('X-Ipfs-Path', resource.toString())
|
|
331
408
|
|
|
332
|
-
|
|
333
|
-
|
|
409
|
+
// set Content-Disposition header
|
|
410
|
+
let contentDisposition: string | undefined
|
|
411
|
+
|
|
412
|
+
// force download if requested
|
|
413
|
+
if (query.download === true) {
|
|
414
|
+
contentDisposition = 'attachment'
|
|
415
|
+
}
|
|
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)}`
|
|
334
424
|
}
|
|
335
|
-
|
|
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 }))
|
|
336
431
|
|
|
337
432
|
return response
|
|
338
433
|
}
|