@helia/verified-fetch 0.0.0-9b1ddf8 → 0.0.0-a04e041

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