@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.
- package/dist/index.min.js +61 -61
- package/dist/index.min.js.map +4 -4
- package/dist/src/index.d.ts +29 -6
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/plugins/plugin-handle-car.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-car.js +7 -3
- package/dist/src/plugins/plugin-handle-car.js.map +1 -1
- package/dist/src/plugins/plugin-handle-ipld.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-ipld.js +4 -15
- package/dist/src/plugins/plugin-handle-ipld.js.map +1 -1
- package/dist/src/plugins/plugin-handle-raw.d.ts +11 -0
- package/dist/src/plugins/plugin-handle-raw.d.ts.map +1 -0
- package/dist/src/plugins/plugin-handle-raw.js +41 -0
- package/dist/src/plugins/plugin-handle-raw.js.map +1 -0
- package/dist/src/plugins/plugin-handle-unixfs.d.ts.map +1 -1
- package/dist/src/plugins/plugin-handle-unixfs.js +36 -18
- package/dist/src/plugins/plugin-handle-unixfs.js.map +1 -1
- package/dist/src/url-resolver.d.ts +2 -3
- package/dist/src/url-resolver.d.ts.map +1 -1
- package/dist/src/url-resolver.js +20 -57
- package/dist/src/url-resolver.js.map +1 -1
- package/dist/src/utils/error-to-response.d.ts +1 -1
- package/dist/src/utils/error-to-response.d.ts.map +1 -1
- package/dist/src/utils/error-to-response.js +14 -12
- package/dist/src/utils/error-to-response.js.map +1 -1
- package/dist/src/utils/get-range-header.d.ts +2 -1
- package/dist/src/utils/get-range-header.d.ts.map +1 -1
- package/dist/src/utils/get-range-header.js.map +1 -1
- package/dist/src/utils/get-tar-stream.d.ts.map +1 -1
- package/dist/src/utils/get-tar-stream.js +22 -9
- package/dist/src/utils/get-tar-stream.js.map +1 -1
- package/dist/src/utils/parse-resource.d.ts +10 -0
- package/dist/src/utils/parse-resource.d.ts.map +1 -0
- package/dist/src/utils/parse-resource.js +52 -0
- package/dist/src/utils/parse-resource.js.map +1 -0
- package/dist/src/utils/responses.d.ts +14 -14
- package/dist/src/utils/responses.d.ts.map +1 -1
- package/dist/src/utils/responses.js +8 -3
- package/dist/src/utils/responses.js.map +1 -1
- package/dist/src/verified-fetch.d.ts +1 -0
- package/dist/src/verified-fetch.d.ts.map +1 -1
- package/dist/src/verified-fetch.js +117 -119
- package/dist/src/verified-fetch.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +30 -6
- package/src/plugins/plugin-handle-car.ts +8 -3
- package/src/plugins/plugin-handle-ipld.ts +4 -14
- package/src/plugins/plugin-handle-raw.ts +52 -0
- package/src/plugins/plugin-handle-unixfs.ts +42 -19
- package/src/url-resolver.ts +22 -65
- package/src/utils/error-to-response.ts +15 -12
- package/src/utils/get-range-header.ts +2 -1
- package/src/utils/get-tar-stream.ts +26 -10
- package/src/utils/parse-resource.ts +62 -0
- package/src/utils/responses.ts +24 -19
- package/src/verified-fetch.ts +123 -122
- package/dist/src/utils/ipfs-path-to-url.d.ts +0 -16
- package/dist/src/utils/ipfs-path-to-url.d.ts.map +0 -1
- package/dist/src/utils/ipfs-path-to-url.js +0 -45
- package/dist/src/utils/ipfs-path-to-url.js.map +0 -1
- package/dist/src/utils/parse-url-string.d.ts +0 -23
- package/dist/src/utils/parse-url-string.d.ts.map +0 -1
- package/dist/src/utils/parse-url-string.js +0 -120
- package/dist/src/utils/parse-url-string.js.map +0 -1
- package/dist/src/utils/resource-to-cache-key.d.ts +0 -15
- package/dist/src/utils/resource-to-cache-key.d.ts.map +0 -1
- package/dist/src/utils/resource-to-cache-key.js +0 -27
- package/dist/src/utils/resource-to-cache-key.js.map +0 -1
- package/src/utils/ipfs-path-to-url.ts +0 -54
- package/src/utils/parse-url-string.ts +0 -165
- 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
|
|
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 (
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
106
|
+
this.log.trace('found directory at %c%s, looking for index.html', dirCid, url.pathname)
|
|
90
107
|
|
|
91
|
-
const
|
|
108
|
+
const indexFile = await context.serverTiming.time('exporter-dir', '', last(walkPath(`/ipfs/${dirCid}/${rootFilePath}`, context.blockstore, context.options)))
|
|
92
109
|
|
|
93
|
-
if (
|
|
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
|
|
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,
|
|
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 (
|
|
149
|
+
} else if (entry.type === 'file' || entry.type === 'raw' || entry.type === 'identity') {
|
|
127
150
|
this.log('streaming file')
|
|
128
|
-
return this.streamFile(resource,
|
|
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',
|
|
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:
|
|
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':
|
|
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':
|
|
196
|
+
'x-ipfs-roots': ipfsRoots.map(cid => cid.toV1()).join(','),
|
|
174
197
|
'accept-ranges': 'bytes'
|
|
175
198
|
},
|
|
176
199
|
redirected
|
package/src/url-resolver.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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:
|
|
25
|
+
terminalElement: PathEntry
|
|
49
26
|
blockstore: Blockstore
|
|
50
27
|
}
|
|
51
28
|
|
|
52
|
-
function basicEntry (
|
|
29
|
+
function basicEntry (cid: CID): PathEntry {
|
|
53
30
|
return {
|
|
31
|
+
cid,
|
|
54
32
|
name: cid.toString(),
|
|
55
33
|
path: cid.toString(),
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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 =
|
|
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
|
-
|
|
172
|
+
try {
|
|
200
173
|
const ipfsRoots: CID[] = []
|
|
201
|
-
let terminalElement:
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
6
|
-
|
|
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
|
|
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
|
|
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
|
|
25
|
+
return gatewayTimeoutResponse(resource, err)
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
// path was not under DAG root
|
|
26
|
-
if (['
|
|
27
|
-
return notFoundResponse(resource
|
|
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
|
|
35
|
+
return notFoundResponse(resource)
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
if (['BlockNotFoundWhileOfflineError'].includes(err.name)) {
|
|
36
|
-
return preconditionFailedResponse(resource
|
|
39
|
+
return preconditionFailedResponse(resource)
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
if (['RecordNotFoundError', 'LoadBlockFailedError'].includes(err.name)) {
|
|
40
|
-
return gatewayTimeoutResponse(resource
|
|
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
|
|
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:
|
|
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 {
|
|
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:
|
|
30
|
+
name: path,
|
|
24
31
|
mode,
|
|
25
32
|
mtime,
|
|
26
|
-
size: Number(
|
|
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
|
|
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(
|
|
62
|
-
(source) => map(source, (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
|
+
}
|