@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.
Files changed (33) hide show
  1. package/README.md +33 -0
  2. package/dist/index.min.js +4 -4
  3. package/dist/src/index.d.ts +36 -0
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js +33 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/utils/get-content-disposition-filename.d.ts +6 -0
  8. package/dist/src/utils/get-content-disposition-filename.d.ts.map +1 -0
  9. package/dist/src/utils/get-content-disposition-filename.js +16 -0
  10. package/dist/src/utils/get-content-disposition-filename.js.map +1 -0
  11. package/dist/src/utils/parse-url-string.d.ts +2 -0
  12. package/dist/src/utils/parse-url-string.d.ts.map +1 -1
  13. package/dist/src/utils/parse-url-string.js +6 -0
  14. package/dist/src/utils/parse-url-string.js.map +1 -1
  15. package/dist/src/utils/responses.d.ts +4 -0
  16. package/dist/src/utils/responses.d.ts.map +1 -0
  17. package/dist/src/utils/responses.js +21 -0
  18. package/dist/src/utils/responses.js.map +1 -0
  19. package/dist/src/utils/select-output-type.d.ts +12 -0
  20. package/dist/src/utils/select-output-type.d.ts.map +1 -0
  21. package/dist/src/utils/select-output-type.js +147 -0
  22. package/dist/src/utils/select-output-type.js.map +1 -0
  23. package/dist/src/verified-fetch.d.ts +17 -15
  24. package/dist/src/verified-fetch.d.ts.map +1 -1
  25. package/dist/src/verified-fetch.js +206 -130
  26. package/dist/src/verified-fetch.js.map +1 -1
  27. package/package.json +16 -12
  28. package/src/index.ts +37 -0
  29. package/src/utils/get-content-disposition-filename.ts +18 -0
  30. package/src/utils/parse-url-string.ts +11 -1
  31. package/src/utils/responses.ts +22 -0
  32. package/src/utils/select-output-type.ts +166 -0
  33. package/src/verified-fetch.ts +230 -135
@@ -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 { code as dagCborCode } from '@ipld/dag-cbor'
5
- import { code as dagJsonCode } from '@ipld/dag-json'
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 { walkPath, type PathWalkerFn } from './utils/walk-path.js'
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
- function okResponse (body?: BodyInit | null): Response {
64
- return new Response(body, {
65
- status: 200,
66
- statusText: 'OK'
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
- function notSupportedResponse (body?: BodyInit | null): Response {
71
- return new Response(body, {
72
- status: 501,
73
- statusText: 'Not Implemented'
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, pathWalker }: VerifiedFetchComponents, init?: VerifiedFetchInit) {
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
- // handle vnd.ipfs.ipns-record
101
- private async handleIPNSRecord ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
102
- const response = notSupportedResponse('vnd.ipfs.ipns-record support is not implemented')
103
- response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
104
- return response
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
- // handle vnd.ipld.car
108
- private async handleIPLDCar ({ cid, path, options }: FetchHandlerFunctionArg): Promise<Response> {
109
- const response = notSupportedResponse('vnd.ipld.car support is not implemented')
110
- response.headers.set('X-Content-Type-Options', 'nosniff') // see https://specs.ipfs.tech/http-gateways/path-gateway/#x-content-type-options-response-header
111
- return response
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
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
117
- const result = await this.helia.blockstore.get(cid, {
118
- signal: options?.signal,
119
- onProgress: options?.onProgress
120
- })
121
- const response = okResponse(result)
122
- response.headers.set('content-type', 'application/json')
123
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
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
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { cid, path }))
130
- // return body as binary
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
- try {
135
- body = dagCborToSafeJSON(block)
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
- response.headers.set('content-type', body instanceof Uint8Array ? 'application/octet-stream' : 'application/json')
143
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
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, terminalElement }: FetchHandlerFunctionArg): Promise<Response> {
148
- this.log.trace('fetching %c/%s', cid, path)
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
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid: resolvedCID, path: '' }))
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.log.trace('fetching %c/%s', cid, path)
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
- options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:end', { cid, path }))
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
- * Determines the format requested by the client, defaults to `null` if no format is requested.
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
- [dagJsonCode]: this.handleJson,
330
+ [ipldDagJson.code]: this.handleJson,
279
331
  [jsonCode]: this.handleJson,
280
- [dagCborCode]: this.handleDagCbor,
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
- const format = this.getFormat({ headerFormat: new Headers(options?.headers).get('accept'), queryFormat: query.format ?? null })
342
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:start', { resource }))
292
343
 
293
- if (format != null) {
294
- // TODO: These should be handled last when they're returning something other than 501
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
- if (formatHandler != null) {
298
- response = await formatHandler.call(this, { cid, path, options })
347
+ options?.onProgress?.(new CustomProgressEvent<CIDDetail>('verified-fetch:request:resolve', { cid, path }))
299
348
 
300
- if (response.status === 501) {
301
- return response
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
- let terminalElement: UnixFSEntry | undefined
307
- let ipfsRoots: CID[] | undefined
356
+ const queryFormatMapping = queryFormatToAcceptHeader(query.format)
308
357
 
309
- try {
310
- const pathDetails = await this.pathWalker(this.helia.blockstore, `${cid.toString()}/${path}`, options)
311
- ipfsRoots = pathDetails.ipfsRoots
312
- terminalElement = pathDetails.terminalElement
313
- } catch (err) {
314
- this.log.error('Error walking path %s', path, err)
315
- // 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)
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
- if (response == null) {
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 != null) {
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: format ?? undefined, weak: false }))
404
+ response.headers.set('etag', getETag({ cid, reqFormat, weak: false }))
329
405
  response.headers.set('cache-control', 'public, max-age=29030400, immutable')
330
- response.headers.set('X-Ipfs-Path', resource.toString()) // https://specs.ipfs.tech/http-gateways/path-gateway/#x-ipfs-path-response-header
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
- if (ipfsRoots != null) {
333
- 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
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
- // response.headers.set('Content-Disposition', `TODO`) // https://specs.ipfs.tech/http-gateways/path-gateway/#content-disposition-response-header
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
  }