Skip to content

Commit 6f2ecc5

Browse files
authored
feat: add download form (#920)
Adds a checkbox to download a DAG instead of attempting to display it in-browser. Implements the different format and car options from the gateway spec. Workaround for #574 though is also useful in it's own right.
1 parent 94077e9 commit 6f2ecc5

29 files changed

+1364
-126
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ tsconfig-doc-check.aegir.json
1414
service-worker-gateway
1515
test-results
1616
test-e2e/fixtures/data/gateway-conformance-fixtures
17+
bafkqaddimvwgy3zao5xxe3debi

img/logo.pxd

-33.9 KB
Binary file not shown.

package.json

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@
3838
"test:inbrowser-dev": "cross-env BASE_URL='https://inbrowser.dev' playwright test -c playwright.config.js --project deployed",
3939
"test:inbrowser-prod": "cross-env BASE_URL='https://inbrowser.link' playwright test -c playwright.config.js --project deployed",
4040
"test:node": "aegir test -t node -f dist-tsc/test/node.js --cov",
41-
"postinstall": "patch-package"
41+
"postinstall": "patch-package",
42+
"generate-fixtures": "node --experimental-strip-types ./test-e2e/fixtures/data/generate.ts"
4243
},
4344
"browser": "./dist/src/index.js",
4445
"browserslist": [
@@ -60,7 +61,7 @@
6061
"@helia/http": "^3.0.8",
6162
"@helia/interface": "^6.0.2",
6263
"@helia/routers": "^4.0.3",
63-
"@helia/verified-fetch": "^4.0.3",
64+
"@helia/verified-fetch": "^4.1.1",
6465
"@libp2p/crypto": "^5.1.13",
6566
"@libp2p/dcutr": "^3.0.7",
6667
"@libp2p/identify": "^4.0.7",
@@ -75,7 +76,7 @@
7576
"@multiformats/dns": "^1.0.10",
7677
"@noble/hashes": "^2.0.1",
7778
"execa": "^9.5.2",
78-
"helia": "^6.0.10",
79+
"helia": "^6.0.11",
7980
"ipfs-css": "^1.4.0",
8081
"ipfsd-ctl": "^16.0.0",
8182
"libp2p": "^3.1.0",
@@ -92,23 +93,36 @@
9293
"@babel/preset-env": "^7.28.0",
9394
"@babel/preset-react": "^7.27.1",
9495
"@babel/preset-typescript": "^7.27.1",
96+
"@helia/car": "^5.3.0",
97+
"@helia/dag-cbor": "^5.0.3",
98+
"@helia/dag-json": "^5.0.3",
99+
"@helia/json": "^5.0.3",
100+
"@helia/unixfs": "^6.0.3",
101+
"@ipld/car": "^5.4.2",
102+
"@ipld/dag-cbor": "^9.2.5",
103+
"@ipld/dag-json": "^10.2.5",
95104
"@playwright/test": "^1.53.2",
96105
"@types/react": "^19.1.6",
97106
"@types/react-dom": "^19.1.9",
98107
"@types/wait-on": "^5.3.4",
99108
"aegir": "^47.0.24",
109+
"cborg": "^4.3.0",
100110
"concurrently": "^9.1.2",
101111
"cross-env": "^10.1.0",
102112
"esbuild": "^0.27.0",
103113
"glob": "^13.0.0",
104114
"http-server": "^14.1.1",
115+
"ipfs-unixfs-importer": "^16.0.1",
116+
"ipns": "^10.1.3",
105117
"it-all": "^3.0.7",
118+
"it-to-buffer": "^4.0.10",
106119
"kubo": "^0.38.1",
107120
"kubo-rpc-client": "^6.0.2",
108121
"npm-run-all": "^4.1.5",
109122
"patch-package": "^8.0.0",
110123
"playwright": "^1.45.3",
111124
"rimraf": "^6.0.1",
125+
"tar": "^7.5.2",
112126
"wait-on": "^9.0.1"
113127
},
114128
"sideEffects": [

public/ipfs-sw-icon-512.png

13.5 KB
Loading

src/sw/handlers/content-request-handler.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -249,20 +249,18 @@ async function fetchHandler ({ url, event, logs, subdomainGatewayRequest, pathGa
249249
const response = await verifiedFetch(resource, init)
250250
response.headers.set('server', `${APP_NAME}/${APP_VERSION}#${GIT_REVISION}`)
251251

252-
log('GET %s %d', resource, response.status, response.statusText)
253-
254-
/**
255-
* Now that we've got a response back from Helia, don't abort the promise
256-
* since any additional networking calls that may performed by Helia would
257-
* be dropped.
258-
*
259-
* If `event.request.signal` is aborted, that would cancel any underlying
260-
* network requests.
261-
*
262-
* Note: we haven't awaited the arrayBuffer, blob, json, etc.
263-
* `await verifiedFetch` only awaits the construction of the response
264-
* object, regardless of it's inner content
265-
*/
252+
log('%s %s %d %s', init.method ?? 'GET', resource, response.status, response.statusText)
253+
254+
// Now that we've got a response back from Helia, don't abort the promise
255+
// since any additional networking calls that may performed by Helia would
256+
// be dropped.
257+
//
258+
// If `event.request.signal` is aborted, that would cancel any underlying
259+
// network requests.
260+
//
261+
// Note: we haven't awaited the arrayBuffer, blob, json, etc.
262+
// `await verifiedFetch` only awaits the construction of the response
263+
// object, regardless of it's inner content
266264
if (!response.ok) {
267265
return fetchErrorPageResponse(resource, init, response, await response.text(), providers, config, firstInstallTime, logs)
268266
}

src/sw/pages/fetch-error-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export function fetchErrorPageResponse (resource: string, request: RequestInit,
125125
}
126126

127127
const page = htmlPage(props.title, 'fetchError', props)
128+
mergedHeaders.set('content-length', `${page.length}`)
128129

129130
return new Response(page, {
130131
status: fetchResponse.status,
Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { CID } from 'multiformats/cid'
2-
import React from 'react'
2+
import React, { useState } from 'react'
33
import { nativeProtocolRegex, pathRegex, subdomainRegex } from '../../lib/regex.js'
4-
import { getGatewayRoot } from '../../lib/to-gateway-root.js'
54
import { Link } from './link.jsx'
65
import type { IpfsUriParts } from '../../lib/regex.js'
76
import type { ReactElement } from 'react'
@@ -30,14 +29,14 @@ function FormatHelp (): ReactElement {
3029
</>
3130
)
3231
}
32+
3333
interface ValidationMessageProps {
34-
cidOrPeerIdOrDnslink?: IpfsUriParts['cidOrPeerIdOrDnslink'],
34+
cidOrPeerIdOrDnslink: string,
3535
requestPath: string,
36-
protocol?: IpfsUriParts['protocol'],
37-
children: React.ReactNode
36+
protocol: string
3837
}
3938

40-
const ValidationMessage: React.FC<ValidationMessageProps> = ({ cidOrPeerIdOrDnslink, requestPath, protocol, children }) => {
39+
const ValidationMessage: React.FC<ValidationMessageProps> = ({ cidOrPeerIdOrDnslink, requestPath, protocol }) => {
4140
let errorElement: ReactElement | null = null
4241

4342
if (requestPath == null || requestPath === '') {
@@ -62,9 +61,7 @@ const ValidationMessage: React.FC<ValidationMessageProps> = ({ cidOrPeerIdOrDnsl
6261

6362
if (errorElement == null) {
6463
return (
65-
<>
66-
{children}
67-
</>
64+
<></>
6865
)
6966
}
7067

@@ -77,41 +74,73 @@ const ValidationMessage: React.FC<ValidationMessageProps> = ({ cidOrPeerIdOrDnsl
7774
)
7875
}
7976

80-
const parseInput = (uri: string): Partial<IpfsUriParts> => {
81-
const uriMatch = uri.match(pathRegex) ?? uri.match(subdomainRegex) ?? uri.match(nativeProtocolRegex)
82-
if (uriMatch?.groups != null) {
83-
const { protocol, cidOrPeerIdOrDnslink, path } = uriMatch.groups as unknown as IpfsUriParts
84-
return { protocol, cidOrPeerIdOrDnslink, path: path?.trim() ?? undefined }
85-
}
77+
export interface CIDInputProps {
78+
requestPath: string
79+
setRequestPath(val: string): void
80+
setInvalid(invalid: boolean): void
81+
}
82+
83+
export function CIDInput ({ requestPath, setRequestPath, setInvalid }: CIDInputProps): ReactElement {
84+
let initialProtocol = ''
85+
let initialCid = ''
8686

87-
// it may be just a CID
8887
try {
89-
CID.parse(uri)
90-
return { protocol: 'ipfs', cidOrPeerIdOrDnslink: uri }
91-
} catch (_) {
92-
// ignore.
93-
}
88+
const uriMatch = requestPath.match(pathRegex) ?? requestPath.match(subdomainRegex) ?? requestPath.match(nativeProtocolRegex)
89+
const { protocol, cidOrPeerIdOrDnslink } = uriMatch?.groups as unknown as IpfsUriParts
9490

95-
return {}
96-
}
91+
initialProtocol = protocol
92+
initialCid = cidOrPeerIdOrDnslink
93+
} catch {}
94+
95+
const [protocol, setProtocol] = useState(initialProtocol)
96+
const [cidOrPeerIdOrDnslink, setCidOrPeerIdOrDnslink] = useState(initialCid)
9797

98-
export default function InputValidator ({ requestPath }: { requestPath: string }): ReactElement {
99-
const { protocol, cidOrPeerIdOrDnslink, path } = parseInput(requestPath)
100-
const swPath = `/${protocol}/${cidOrPeerIdOrDnslink}${path ?? ''}`
98+
function validate (val: string): void {
99+
setRequestPath(val)
101100

102-
function checkInput (): boolean | undefined {
103-
if (protocol == null || cidOrPeerIdOrDnslink == null) {
104-
return false
101+
const uriMatch = val.match(pathRegex) ?? val.match(subdomainRegex) ?? val.match(nativeProtocolRegex)
102+
if (uriMatch?.groups != null) {
103+
const { protocol, cidOrPeerIdOrDnslink } = uriMatch.groups as unknown as IpfsUriParts
104+
105+
setProtocol(protocol)
106+
setCidOrPeerIdOrDnslink(cidOrPeerIdOrDnslink)
107+
setInvalid(false)
108+
return
105109
}
106110

107-
window.location.href = getGatewayRoot() + swPath
111+
// it may be just a CID
112+
try {
113+
CID.parse(val)
114+
115+
setProtocol('ipfs')
116+
setCidOrPeerIdOrDnslink(val)
117+
setInvalid(false)
118+
return
119+
} catch {
120+
// ignore
121+
}
122+
123+
setInvalid(true)
108124
}
109125

110126
return (
111-
<div>
112-
<ValidationMessage protocol={protocol} cidOrPeerIdOrDnslink={cidOrPeerIdOrDnslink} requestPath={requestPath}>
113-
<button id='load-directly' className='button-reset pv3 tc bn bg-animate bg-teal-muted hover-bg-navy-muted white pointer f4 w-100' onClick={checkInput}>Load content</button>
114-
</ValidationMessage>
115-
</div>
127+
<>
128+
<label htmlFor='inputContent' className='f5 ma0 pb2 teal fw4 db'>CID, Content Path, or URL</label>
129+
<input
130+
className='input-reset bn black-80 bg-white pa3 w-100 mb3'
131+
id='inputContent'
132+
name='inputContent'
133+
type='text'
134+
placeholder='/ipfs/bafk.../path/to/file'
135+
required
136+
value={requestPath}
137+
onChange={(e) => validate(e.target.value)}
138+
/>
139+
<ValidationMessage
140+
protocol={protocol}
141+
cidOrPeerIdOrDnslink={cidOrPeerIdOrDnslink}
142+
requestPath={requestPath}
143+
/>
144+
</>
116145
)
117146
}

0 commit comments

Comments
 (0)