@helia/verified-fetch 5.1.1 → 6.1.0

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 (72) hide show
  1. package/dist/index.min.js +61 -61
  2. package/dist/index.min.js.map +4 -4
  3. package/dist/src/index.d.ts +29 -6
  4. package/dist/src/index.d.ts.map +1 -1
  5. package/dist/src/index.js.map +1 -1
  6. package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
  7. package/dist/src/plugins/plugin-handle-car.js +7 -3
  8. package/dist/src/plugins/plugin-handle-car.js.map +1 -1
  9. package/dist/src/plugins/plugin-handle-ipld.d.ts.map +1 -1
  10. package/dist/src/plugins/plugin-handle-ipld.js +4 -15
  11. package/dist/src/plugins/plugin-handle-ipld.js.map +1 -1
  12. package/dist/src/plugins/plugin-handle-raw.d.ts +11 -0
  13. package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -0
  14. package/dist/src/plugins/plugin-handle-raw.js +41 -0
  15. package/dist/src/plugins/plugin-handle-raw.js.map +1 -0
  16. package/dist/src/plugins/plugin-handle-unixfs.d.ts.map +1 -1
  17. package/dist/src/plugins/plugin-handle-unixfs.js +36 -18
  18. package/dist/src/plugins/plugin-handle-unixfs.js.map +1 -1
  19. package/dist/src/url-resolver.d.ts +2 -3
  20. package/dist/src/url-resolver.d.ts.map +1 -1
  21. package/dist/src/url-resolver.js +20 -57
  22. package/dist/src/url-resolver.js.map +1 -1
  23. package/dist/src/utils/error-to-response.d.ts +1 -1
  24. package/dist/src/utils/error-to-response.d.ts.map +1 -1
  25. package/dist/src/utils/error-to-response.js +14 -12
  26. package/dist/src/utils/error-to-response.js.map +1 -1
  27. package/dist/src/utils/get-range-header.d.ts +2 -1
  28. package/dist/src/utils/get-range-header.d.ts.map +1 -1
  29. package/dist/src/utils/get-range-header.js.map +1 -1
  30. package/dist/src/utils/get-tar-stream.d.ts.map +1 -1
  31. package/dist/src/utils/get-tar-stream.js +22 -9
  32. package/dist/src/utils/get-tar-stream.js.map +1 -1
  33. package/dist/src/utils/parse-resource.d.ts +10 -0
  34. package/dist/src/utils/parse-resource.d.ts.map +1 -0
  35. package/dist/src/utils/parse-resource.js +52 -0
  36. package/dist/src/utils/parse-resource.js.map +1 -0
  37. package/dist/src/utils/responses.d.ts +14 -14
  38. package/dist/src/utils/responses.d.ts.map +1 -1
  39. package/dist/src/utils/responses.js +8 -3
  40. package/dist/src/utils/responses.js.map +1 -1
  41. package/dist/src/verified-fetch.d.ts +1 -0
  42. package/dist/src/verified-fetch.d.ts.map +1 -1
  43. package/dist/src/verified-fetch.js +117 -119
  44. package/dist/src/verified-fetch.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/index.ts +30 -6
  47. package/src/plugins/plugin-handle-car.ts +8 -3
  48. package/src/plugins/plugin-handle-ipld.ts +4 -14
  49. package/src/plugins/plugin-handle-raw.ts +52 -0
  50. package/src/plugins/plugin-handle-unixfs.ts +42 -19
  51. package/src/url-resolver.ts +22 -65
  52. package/src/utils/error-to-response.ts +15 -12
  53. package/src/utils/get-range-header.ts +2 -1
  54. package/src/utils/get-tar-stream.ts +26 -10
  55. package/src/utils/parse-resource.ts +62 -0
  56. package/src/utils/responses.ts +24 -19
  57. package/src/verified-fetch.ts +123 -122
  58. package/dist/src/utils/ipfs-path-to-url.d.ts +0 -16
  59. package/dist/src/utils/ipfs-path-to-url.d.ts.map +0 -1
  60. package/dist/src/utils/ipfs-path-to-url.js +0 -45
  61. package/dist/src/utils/ipfs-path-to-url.js.map +0 -1
  62. package/dist/src/utils/parse-url-string.d.ts +0 -23
  63. package/dist/src/utils/parse-url-string.d.ts.map +0 -1
  64. package/dist/src/utils/parse-url-string.js +0 -120
  65. package/dist/src/utils/parse-url-string.js.map +0 -1
  66. package/dist/src/utils/resource-to-cache-key.d.ts +0 -15
  67. package/dist/src/utils/resource-to-cache-key.d.ts.map +0 -1
  68. package/dist/src/utils/resource-to-cache-key.js +0 -27
  69. package/dist/src/utils/resource-to-cache-key.js.map +0 -1
  70. package/src/utils/ipfs-path-to-url.ts +0 -54
  71. package/src/utils/parse-url-string.ts +0 -165
  72. package/src/utils/resource-to-cache-key.ts +0 -30
@@ -1,7 +1,8 @@
1
1
  import { code as dagPbCode } from '@ipld/dag-pb'
2
2
  import { isPromise } from '@libp2p/utils'
3
- import { exporter } from 'ipfs-unixfs-exporter'
3
+ import { exporter, walkPath } from 'ipfs-unixfs-exporter'
4
4
  import first from 'it-first'
5
+ import last from 'it-last'
5
6
  import itToBrowserReadableStream from 'it-to-browser-readablestream'
6
7
  import toBuffer from 'it-to-buffer'
7
8
  import * as raw from 'multiformats/codecs/raw'
@@ -9,27 +10,28 @@ import { MEDIA_TYPE_OCTET_STREAM, MEDIA_TYPE_DAG_PB } from '../utils/content-typ
9
10
  import { getContentDispositionFilename } from '../utils/get-content-disposition-filename.ts'
10
11
  import { badGatewayResponse, movedPermanentlyResponse, partialContentResponse, okResponse } from '../utils/responses.js'
11
12
  import { BasePlugin } from './plugin-base.js'
12
- import type { PluginContext } from '../index.js'
13
+ import type { PluginContext, Resource } from '../index.js'
13
14
  import type { RangeHeader } from '../utils/get-range-header.ts'
14
15
  import type { AbortOptions } from '@libp2p/interface'
15
16
  import type { IdentityNode, RawNode, UnixFSEntry, UnixFSFile } from 'ipfs-unixfs-exporter'
17
+ import type { CID } from 'multiformats/cid'
16
18
 
17
19
  /**
18
20
  * @see https://specs.ipfs.tech/http-gateways/path-gateway/#use-in-directory-url-normalization
19
21
  */
20
- function getRedirectUrl (resource: string, url: URL, terminalElement: UnixFSEntry): string | undefined {
22
+ function getDirectoryRedirectUrl (resource: Resource, url: URL): string | undefined {
21
23
  let uri: URL
22
24
 
23
25
  try {
24
26
  // try the requested resource
25
- uri = new URL(resource)
27
+ uri = new URL(resource.toString())
26
28
  } catch {
27
29
  // fall back to the canonical URL
28
30
  uri = url
29
31
  }
30
32
 
31
33
  // directories must be requested with a trailing slash
32
- if (terminalElement?.type === 'directory' && !uri.pathname.endsWith('/')) {
34
+ if (!uri.pathname.endsWith('/')) {
33
35
  // make sure we append slash to end of the path
34
36
  uri.pathname += '/'
35
37
 
@@ -54,12 +56,27 @@ export class UnixFSPlugin extends BasePlugin {
54
56
  }
55
57
 
56
58
  async handle (context: PluginContext): Promise<Response> {
57
- let { url, resource, terminalElement, ipfsRoots } = context
59
+ let { url, resource, terminalElement, ipfsRoots, blockstore } = context
58
60
  let filename = url.searchParams.get('filename') ?? terminalElement.name
59
61
  let redirected: undefined | true
62
+ let entry: UnixFSEntry
60
63
 
61
- if (terminalElement.type === 'directory') {
62
- const redirectUrl = getRedirectUrl(resource, url, terminalElement)
64
+ try {
65
+ entry = await exporter(terminalElement.cid, blockstore, context.options)
66
+ } catch (err: any) {
67
+ // throw abort error if signal was aborted
68
+ context?.options?.signal?.throwIfAborted()
69
+
70
+ if (err.name === 'BlockNotFoundWhileOfflineError') {
71
+ throw err
72
+ }
73
+
74
+ this.log.error('could not export %c - $e', terminalElement.cid, err)
75
+ return badGatewayResponse(resource, 'Unable to stream content')
76
+ }
77
+
78
+ if (entry.type === 'directory') {
79
+ const redirectUrl = getDirectoryRedirectUrl(resource, url)
63
80
 
64
81
  if (redirectUrl != null) {
65
82
  this.log.trace('directory url normalization spec requires redirect')
@@ -86,20 +103,26 @@ export class UnixFSPlugin extends BasePlugin {
86
103
  const rootFilePath = 'index.html'
87
104
 
88
105
  try {
89
- this.log.trace('found directory at %c/%s, looking for index.html', dirCid, url.pathname)
106
+ this.log.trace('found directory at %c%s, looking for index.html', dirCid, url.pathname)
90
107
 
91
- const entry = await context.serverTiming.time('exporter-dir', '', exporter(`/ipfs/${dirCid}/${rootFilePath}`, context.blockstore, context.options))
108
+ const indexFile = await context.serverTiming.time('exporter-dir', '', last(walkPath(`/ipfs/${dirCid}/${rootFilePath}`, context.blockstore, context.options)))
92
109
 
93
- if (entry.type === 'directory' || entry.type === 'object') {
110
+ if (indexFile == null) {
111
+ return badGatewayResponse(resource, 'Unable to stream content')
112
+ }
113
+
114
+ const indexFileEntry = await context.serverTiming.time('exporter-dir', '', exporter(indexFile.cid, context.blockstore, context.options))
115
+
116
+ if (indexFileEntry.type !== 'file' && indexFileEntry.type !== 'raw' && indexFileEntry.type !== 'identity') {
94
117
  return badGatewayResponse(resource, 'Unable to stream content')
95
118
  }
96
119
 
97
120
  // use `index.html` as the file name to help with content types
98
121
  filename = rootFilePath
99
122
 
100
- this.log.trace('found directory index at %c/%s with cid %c', dirCid, rootFilePath, entry.cid)
123
+ this.log.trace('found directory index at %c%s with cid %c', dirCid, rootFilePath, entry.cid)
101
124
 
102
- return await this.streamFile(resource, entry, filename, redirected, context.range, context.options)
125
+ return await this.streamFile(resource, indexFileEntry, filename, ipfsRoots, redirected, context.range, context.options)
103
126
  } catch (err: any) {
104
127
  if (err.name !== 'NotFoundError') {
105
128
  this.log.error('error loading path %c/%s - %e', dirCid, rootFilePath, err)
@@ -123,16 +146,16 @@ export class UnixFSPlugin extends BasePlugin {
123
146
  },
124
147
  redirected
125
148
  })
126
- } else if (terminalElement.type === 'file' || terminalElement.type === 'raw' || terminalElement.type === 'identity') {
149
+ } else if (entry.type === 'file' || entry.type === 'raw' || entry.type === 'identity') {
127
150
  this.log('streaming file')
128
- return this.streamFile(resource, terminalElement, filename, redirected, context.range, context.options)
151
+ return this.streamFile(resource, entry, filename, ipfsRoots, redirected, context.range, context.options)
129
152
  } else {
130
- this.log.error('cannot stream terminal element type %s', terminalElement.type)
153
+ this.log.error('cannot stream terminal element type %s', entry.type)
131
154
  return badGatewayResponse(resource, 'Unable to stream content')
132
155
  }
133
156
  }
134
157
 
135
- private async streamFile (resource: string, entry: UnixFSFile | RawNode | IdentityNode, filename: string, redirected?: boolean, rangeHeader?: RangeHeader, options?: AbortOptions): Promise<Response> {
158
+ private async streamFile (resource: Resource, entry: UnixFSFile | RawNode | IdentityNode, filename: string, ipfsRoots: CID[], redirected?: boolean, rangeHeader?: RangeHeader, options?: AbortOptions): Promise<Response> {
136
159
  let contentType = MEDIA_TYPE_OCTET_STREAM
137
160
 
138
161
  // only detect content type for non-range requests to avoid loading blocks
@@ -154,7 +177,7 @@ export class UnixFSPlugin extends BasePlugin {
154
177
  'content-disposition': `inline; ${
155
178
  getContentDispositionFilename(filename)
156
179
  }`,
157
- 'x-ipfs-roots': entry.cid.toString(),
180
+ 'x-ipfs-roots': ipfsRoots.map(cid => cid.toV1()).join(','),
158
181
  'accept-ranges': 'bytes'
159
182
  },
160
183
  redirected
@@ -170,7 +193,7 @@ export class UnixFSPlugin extends BasePlugin {
170
193
  'content-disposition': `inline; ${
171
194
  getContentDispositionFilename(filename)
172
195
  }`,
173
- 'x-ipfs-roots': entry.cid.toString(),
196
+ 'x-ipfs-roots': ipfsRoots.map(cid => cid.toV1()).join(','),
174
197
  'accept-ranges': 'bytes'
175
198
  },
176
199
  redirected
@@ -1,16 +1,9 @@
1
1
  import { DoesNotExistError } from '@helia/unixfs/errors'
2
- import * as dagCbor from '@ipld/dag-cbor'
3
- import * as dagJson from '@ipld/dag-json'
4
- import * as dagPb from '@ipld/dag-pb'
5
2
  import { peerIdFromString } from '@libp2p/peer-id'
6
3
  import { InvalidParametersError, walkPath } from 'ipfs-unixfs-exporter'
7
- import toBuffer from 'it-to-buffer'
8
4
  import { CID } from 'multiformats/cid'
9
- import * as json from 'multiformats/codecs/json'
10
- import * as raw from 'multiformats/codecs/raw'
11
5
  import QuickLRU from 'quick-lru'
12
- import { SESSION_CACHE_MAX_SIZE, SESSION_CACHE_TTL_MS, CODEC_CBOR, CODEC_IDENTITY } from './constants.ts'
13
- import { resourceToSessionCacheKey } from './utils/resource-to-cache-key.ts'
6
+ import { SESSION_CACHE_MAX_SIZE, SESSION_CACHE_TTL_MS } from './constants.ts'
14
7
  import { ServerTiming } from './utils/server-timing.ts'
15
8
  import type { ResolveURLResult, URLResolver as URLResolverInterface } from './index.ts'
16
9
  import type { DNSLink } from '@helia/dnslink'
@@ -18,49 +11,30 @@ import type { IPNSResolver } from '@helia/ipns'
18
11
  import type { AbortOptions } from '@libp2p/interface'
19
12
  import type { Helia, SessionBlockstore } from 'helia'
20
13
  import type { Blockstore } from 'interface-blockstore'
21
- import type { UnixFSEntry } from 'ipfs-unixfs-exporter'
14
+ import type { PathEntry } from 'ipfs-unixfs-exporter'
22
15
 
23
16
  // 1 year in seconds for ipfs content
24
17
  const IPFS_CONTENT_TTL = 29030400
25
18
 
26
- const ENTITY_CODECS = [
27
- CODEC_CBOR,
28
- json.code,
29
- raw.code
30
- ]
31
-
32
- /**
33
- * These are supported by the UnixFS exporter
34
- */
35
- const EXPORTABLE_CODECS = [
36
- dagPb.code,
37
- dagCbor.code,
38
- dagJson.code,
39
- raw.code
40
- ]
41
-
42
19
  interface GetBlockstoreOptions extends AbortOptions {
43
20
  session?: boolean
44
21
  }
45
22
 
46
23
  export interface WalkPathResult {
47
24
  ipfsRoots: CID[]
48
- terminalElement: UnixFSEntry
25
+ terminalElement: PathEntry
49
26
  blockstore: Blockstore
50
27
  }
51
28
 
52
- function basicEntry (type: 'raw' | 'object', cid: CID, bytes: Uint8Array): UnixFSEntry {
29
+ function basicEntry (cid: CID): PathEntry {
53
30
  return {
31
+ cid,
54
32
  name: cid.toString(),
55
33
  path: cid.toString(),
56
- depth: 0,
57
- type,
58
- node: bytes,
59
- cid,
60
- size: BigInt(bytes.byteLength),
61
- content: async function * () {
62
- yield bytes
63
- }
34
+ roots: [
35
+ cid
36
+ ],
37
+ remainder: []
64
38
  }
65
39
  }
66
40
 
@@ -78,7 +52,6 @@ export interface URLResolverInit {
78
52
  export interface ResolveURLOptions extends AbortOptions {
79
53
  session?: boolean
80
54
  isRawBlockRequest?: boolean
81
- onlyIfCached?: boolean
82
55
  }
83
56
 
84
57
  export class URLResolver implements URLResolverInterface {
@@ -118,7 +91,7 @@ export class URLResolver implements URLResolverInterface {
118
91
  return this.components.helia.blockstore
119
92
  }
120
93
 
121
- const key = resourceToSessionCacheKey(root)
94
+ const key = `ipfs:${root}`
122
95
  let session = this.blockstoreSessions.get(key)
123
96
 
124
97
  if (session == null) {
@@ -196,16 +169,14 @@ export class URLResolver implements URLResolverInterface {
196
169
  const cid = CID.parse(url.hostname)
197
170
  const blockstore = this.getBlockstore(cid, options)
198
171
 
199
- if (EXPORTABLE_CODECS.includes(cid.code)) {
172
+ try {
200
173
  const ipfsRoots: CID[] = []
201
- let terminalElement: UnixFSEntry | undefined
174
+ let terminalElement: PathEntry | undefined
202
175
  const ipfsPath = toIPFSPath(url)
203
176
 
204
- // @ts-expect-error offline is a helia option
205
177
  for await (const entry of walkPath(ipfsPath, blockstore, {
206
178
  ...options,
207
- offline: options.onlyIfCached === true,
208
- extended: options.isRawBlockRequest !== true
179
+ yieldSubShards: true
209
180
  })) {
210
181
  ipfsRoots.push(entry.cid)
211
182
  terminalElement = entry
@@ -220,31 +191,17 @@ export class URLResolver implements URLResolverInterface {
220
191
  terminalElement,
221
192
  blockstore
222
193
  }
223
- }
224
-
225
- let bytes: Uint8Array
226
-
227
- if (cid.multihash.code === CODEC_IDENTITY) {
228
- bytes = cid.multihash.digest
229
- } else {
230
- bytes = await toBuffer(blockstore.get(cid, options))
231
- }
232
-
233
- // entity codecs contain all the bytes for an entity in one block and no
234
- // path walking outside of that block is possible
235
- if (ENTITY_CODECS.includes(cid.code)) {
236
- return {
237
- ipfsRoots: [cid],
238
- terminalElement: basicEntry('object', cid, bytes),
239
- blockstore
194
+ } catch (err: any) {
195
+ if (err.name === 'NoResolverError') {
196
+ // may be an unknown codec
197
+ return {
198
+ ipfsRoots: [cid],
199
+ terminalElement: basicEntry(cid),
200
+ blockstore
201
+ }
240
202
  }
241
- }
242
203
 
243
- // may be an unknown codec
244
- return {
245
- ipfsRoots: [cid],
246
- terminalElement: basicEntry('raw', cid, bytes),
247
- blockstore
204
+ throw err
248
205
  }
249
206
  }
250
207
  }
@@ -1,45 +1,48 @@
1
1
  import { badGatewayResponse, gatewayTimeoutResponse, internalServerErrorResponse, notFoundResponse, preconditionFailedResponse } from './responses.js'
2
2
  import type { Resource } from '../index.js'
3
3
 
4
- export function errorToResponse (resource: Resource | string, err: any): Response {
5
- // if a signal abort caused the error, throw the error
6
- if (err.name === 'AbortError') {
4
+ export function errorToResponse (resource: Resource | string, err: any, init?: RequestInit): Response {
5
+ // throw an AbortError if the passed signal has aborted
6
+ init?.signal?.throwIfAborted()
7
+
8
+ // rethrow these errors
9
+ if (['AbortError', 'InvalidParametersError'].includes(err.name)) {
7
10
  throw err
8
11
  }
9
12
 
10
13
  // could not reach an upstream server, bad connection or offline
11
14
  if (err.code === 'ECONNREFUSED' || err.code === 'ECANCELLED' || err.name === 'DNSQueryFailedError') {
12
- return gatewayTimeoutResponse(resource.toString(), err)
15
+ return gatewayTimeoutResponse(resource, err)
13
16
  }
14
17
 
15
18
  // data was not parseable, user may be able to request raw block
16
19
  if (['NotUnixFSError'].includes(err.name)) {
17
- return badGatewayResponse(resource.toString(), err)
20
+ return badGatewayResponse(resource, err)
18
21
  }
19
22
 
20
23
  // an upstream server didn't respond in time but inside the signal timeout
21
24
  if (err.code === 'ETIMEOUT' || err.name === 'TimeoutError') {
22
- return gatewayTimeoutResponse(resource.toString(), err)
25
+ return gatewayTimeoutResponse(resource, err)
23
26
  }
24
27
 
25
28
  // path was not under DAG root
26
- if (['ERR_NO_PROP', 'ERR_NO_TERMINAL_ELEMENT', 'ERR_NOT_FOUND'].includes(err.code)) {
27
- return notFoundResponse(resource.toString())
29
+ if (['ERR_BAD_PATH', 'ERR_NO_TERMINAL_ELEMENT', 'ERR_NOT_FOUND'].includes(err.code)) {
30
+ return notFoundResponse(resource)
28
31
  }
29
32
 
30
33
  // path was not under DAG root
31
34
  if (['DoesNotExistError'].includes(err.name)) {
32
- return notFoundResponse(resource.toString())
35
+ return notFoundResponse(resource)
33
36
  }
34
37
 
35
38
  if (['BlockNotFoundWhileOfflineError'].includes(err.name)) {
36
- return preconditionFailedResponse(resource.toString())
39
+ return preconditionFailedResponse(resource)
37
40
  }
38
41
 
39
42
  if (['RecordNotFoundError', 'LoadBlockFailedError'].includes(err.name)) {
40
- return gatewayTimeoutResponse(resource.toString(), err)
43
+ return gatewayTimeoutResponse(resource, err)
41
44
  }
42
45
 
43
46
  // can't tell what went wrong, return a generic error
44
- return internalServerErrorResponse(resource.toString(), err)
47
+ return internalServerErrorResponse(resource, err)
45
48
  }
@@ -1,5 +1,6 @@
1
1
  import { InvalidRangeError } from '../errors.ts'
2
2
  import { badRequestResponse, notSatisfiableResponse } from './responses.ts'
3
+ import type { Resource } from '../index.ts'
3
4
 
4
5
  /**
5
6
  * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Range
@@ -78,7 +79,7 @@ function getByteRangeFromHeader (rangeHeader?: string): Range[] {
78
79
  return ranges
79
80
  }
80
81
 
81
- export function getRangeHeader (resource: string, headers: Headers): RangeHeader | undefined | Response {
82
+ export function getRangeHeader (resource: Resource, headers: Headers): RangeHeader | undefined | Response {
82
83
  const header = headers.get('range')
83
84
 
84
85
  // not a range request
@@ -1,5 +1,7 @@
1
1
  import { NotUnixFSError } from '@helia/unixfs/errors'
2
- import { exporter, recursive } from 'ipfs-unixfs-exporter'
2
+ import { InvalidParametersError } from '@libp2p/interface'
3
+ import { exporter, recursive, walkPath } from 'ipfs-unixfs-exporter'
4
+ import last from 'it-last'
3
5
  import map from 'it-map'
4
6
  import { pipe } from 'it-pipe'
5
7
  import { pack } from 'it-tar'
@@ -10,9 +12,14 @@ import type { TarEntryHeader, TarImportCandidate } from 'it-tar'
10
12
 
11
13
  const EXPORTABLE = ['file', 'raw', 'directory']
12
14
 
13
- function toHeader (file: UnixFSEntry): Partial<TarEntryHeader> & { name: string } {
15
+ function toHeader (file: UnixFSEntry, path: string): Partial<TarEntryHeader> & { name: string } {
14
16
  let mode: number | undefined
15
17
  let mtime: Date | undefined
18
+ let size = 0n
19
+
20
+ if (file.type === 'file' || file.type === 'raw' || file.type === 'identity') {
21
+ size = file.size
22
+ }
16
23
 
17
24
  if (file.type === 'file' || file.type === 'directory') {
18
25
  mode = file.unixfs.mode
@@ -20,21 +27,21 @@ function toHeader (file: UnixFSEntry): Partial<TarEntryHeader> & { name: string
20
27
  }
21
28
 
22
29
  return {
23
- name: file.path,
30
+ name: path,
24
31
  mode,
25
32
  mtime,
26
- size: Number(file.size),
33
+ size: Number(size),
27
34
  type: file.type === 'directory' ? 'directory' : 'file'
28
35
  }
29
36
  }
30
37
 
31
- function toTarImportCandidate (entry: UnixFSEntry): TarImportCandidate {
38
+ function toTarImportCandidate (entry: UnixFSEntry, path: string): TarImportCandidate {
32
39
  if (!EXPORTABLE.includes(entry.type)) {
33
40
  throw new NotUnixFSError(`${entry.type} is not a UnixFS node`)
34
41
  }
35
42
 
36
43
  const candidate: TarImportCandidate = {
37
- header: toHeader(entry)
44
+ header: toHeader(entry, path)
38
45
  }
39
46
 
40
47
  if (entry.type === 'file' || entry.type === 'raw') {
@@ -45,11 +52,17 @@ function toTarImportCandidate (entry: UnixFSEntry): TarImportCandidate {
45
52
  }
46
53
 
47
54
  export async function * tarStream (ipfsPath: string, blockstore: Blockstore, options?: AbortOptions): AsyncGenerator<Uint8Array> {
48
- const file = await exporter(ipfsPath, blockstore, options)
55
+ const entry = await last(walkPath(ipfsPath, blockstore, options))
56
+
57
+ if (entry == null) {
58
+ throw new InvalidParametersError(`Could not walk path "${ipfsPath}"`)
59
+ }
60
+
61
+ const file = await exporter(entry.cid, blockstore, options)
49
62
 
50
63
  if (file.type === 'file' || file.type === 'raw') {
51
64
  yield * pipe(
52
- [toTarImportCandidate(file)],
65
+ [toTarImportCandidate(file, entry.path)],
53
66
  pack()
54
67
  )
55
68
 
@@ -58,8 +71,11 @@ export async function * tarStream (ipfsPath: string, blockstore: Blockstore, opt
58
71
 
59
72
  if (file.type === 'directory') {
60
73
  yield * pipe(
61
- recursive(ipfsPath, blockstore, options),
62
- (source) => map(source, (entry) => toTarImportCandidate(entry)),
74
+ recursive(file.cid, blockstore, options),
75
+ (source) => map(source, async (entry) => {
76
+ const file = await exporter(entry.cid, blockstore, options)
77
+ return toTarImportCandidate(file, entry.path)
78
+ }),
63
79
  pack()
64
80
  )
65
81
 
@@ -0,0 +1,62 @@
1
+ import { InvalidParametersError } from '@libp2p/interface'
2
+ import { peerIdFromString } from '@libp2p/peer-id'
3
+ import { CID } from 'multiformats/cid'
4
+
5
+ /**
6
+ * Turns an IPFS or IPNS path into a URL
7
+ *
8
+ * - `/ipfs/cid` -> `ipfs://cid`
9
+ * - `/ipns/name` -> `ipns://name`
10
+ */
11
+ function ipfsPathToIpfsUrl (path: string): string {
12
+ if (!path.startsWith('/ipfs/') && !path.startsWith('/ipns/')) {
13
+ throw new InvalidParametersError(`Path ${path} did not start with /ipfs/ or /ipns/`)
14
+ }
15
+
16
+ // use non-http protocol as otherwise an empty path will become "/"
17
+ const url = new URL(`not-http://example.com${path}`)
18
+ const [
19
+ ,
20
+ protocol,
21
+ name,
22
+ ...rest
23
+ ] = url.pathname.split('/')
24
+
25
+ return `${protocol}://${name}${rest.length > 0 ? `/${rest.join('/')}` : ''}${url.search}${url.hash}`
26
+ }
27
+
28
+ /**
29
+ * Accepts the following url strings:
30
+ *
31
+ * - /ipfs/Qmfoo/path
32
+ * - /ipns/Qmfoo/path
33
+ * - ipfs://cid/path
34
+ * - ipns://name/path
35
+ */
36
+ export function stringToIpfsUrl (urlString: string): URL {
37
+ if (urlString.startsWith('/ipfs/') || urlString.startsWith('/ipns/')) {
38
+ urlString = ipfsPathToIpfsUrl(urlString)
39
+ }
40
+
41
+ if (urlString.startsWith('ipfs://') || urlString.startsWith('ipns://') || urlString.startsWith('dnslink://')) {
42
+ const url = new URL(urlString)
43
+
44
+ // ensure IPNS name can be parsed as a CID or peer id, otherwise treat as
45
+ // dnslink
46
+ if (url.protocol === 'ipns:') {
47
+ try {
48
+ CID.parse(url.hostname)
49
+ } catch {
50
+ try {
51
+ peerIdFromString(url.hostname)
52
+ } catch {
53
+ url.protocol = 'dnslink'
54
+ }
55
+ }
56
+ }
57
+
58
+ return url
59
+ }
60
+
61
+ throw new InvalidParametersError(`URL did not start with ipfs:// or ipns:// - ${urlString}`)
62
+ }