UNPKG

@helia/verified-fetch

Version:

A fetch-like API for obtaining verified & trustless IPFS content on the web

438 lines (386 loc) 14.2 kB
// see https://github.com/ipfs/boxo/blob/09b0013e1c3e09468009b02dfc9b2b9041199d5d/gateway/assets/templates.go#L19C1-L25C2 function iconFromExt(name) { // not implemented yet // TODO: optimize icons: https://github.com/ipfs-shipyard/ipfs-css/issues/71 return 'ipfs-_blank'; } /** * If they click on the short hash, it should link to the host-without-subdomain + /ipfs/ + hash + ?filename={filename} */ function itemShortHashCell(item, dirData) { const host = dirData.globalData.gatewayURLWithoutSubdomain.host; const protocol = dirData.globalData.gatewayURLWithoutSubdomain.protocol; return `<a class="ipfs-hash" translate="no" href="${protocol}//${host}/ipfs/${item.hash}?filename=${item.name}">${item.shortHash}</a>`; } /** * Returns a new host with the subdomain removed if it includes "ipfs" or "ipns". * * @example * subdomain.ipfs.dweb.link -> dweb.link * abc.ipns.localhost -> localhost * bafyfoo.ipfs.localhost:3441 -> localhost:3441 * bafyfoo.ipfs.foo.localhost:3441 -> foo.localhost:3441 */ function removeIpfsOrIpnsSubdomain(host) { const segments = host.split('.'); const keepSegments = []; // Walk from the right to the left for (let i = segments.length - 1; i >= 0; i--) { const seg = segments[i]; // If we hit "ipfs" or "ipns", stop (ignore everything to the left) if (seg === 'ipfs' || seg === 'ipns') { break; } keepSegments.push(seg); } // Reverse because we collected from right to left keepSegments.reverse(); // If keepSegments is empty, it means "ipfs" or "ipns" was at the TLD level // but typically that means the next domain is empty; just return empty string return keepSegments.join('.'); } function getGatewayURLWithoutSubdomain(gatewayURL) { let currentUrl; try { currentUrl = new URL(gatewayURL); } catch { // If the gatewayURL is invalid (ipfs:// or ipns:// or just a CID), use inbrowser.link as a fallback currentUrl = new URL('https://inbrowser.link'); } currentUrl.host = removeIpfsOrIpnsSubdomain(currentUrl.host); return currentUrl; } function dirListingTitle(dirData) { if (dirData.path != null) { const href = `${dirData.globalData.gatewayURL}/${dirData.path}`; return `Index of <a href="${href}">${dirData.name}</a>`; } return `Index of ${dirData.name} ${dirData.path}`; } function getAllDirListingRows(dirData) { return dirData.listing.map((item) => `<div class="type-icon"> <div class="${iconFromExt(item.name)}">&nbsp;</div> </div> <div> <a href="${item.path}">${item.name}</a> </div> <div class="nowrap"> ${itemShortHashCell(item, dirData)} </div> <div class="nowrap" title="Cumulative size of IPFS DAG (data + metadata)">${item.size}</div>`).join(' '); } function getItemPath(item) { const itemPathParts = item.path.split('/'); return itemPathParts.pop() ?? item.path; } /** * if <= 11, return the hash as is * if > 11, return the first 4 and last 4 characters of the hash, separated by '...' * * e.g. QmabcccHnzA * e.g. Qmab...HnzA */ function getShortHash(hash) { return hash.length <= 11 ? hash : `${hash.slice(0, 4)}...${hash.slice(-4)}`; } /** * todo: https://github.com/ipfs/boxo/blob/09b0013e1c3e09468009b02dfc9b2b9041199d5d/gateway/handler_unixfs_dir.go#L200-L208 * * @see https://github.com/ipfs/boxo/blob/09b0013e1c3e09468009b02dfc9b2b9041199d5d/gateway/assets/directory.html * @see https://github.com/ipfs/boxo/pull/298 * @see https://github.com/ipfs/kubo/pull/8555 */ export const dirIndexHtml = (dir, items, { gatewayURL, dnsLink, log }) => { log('loading directory html for %s', dir.path); const dirData = { globalData: { gatewayURL, gatewayURLWithoutSubdomain: getGatewayURLWithoutSubdomain(gatewayURL), dnsLink: dnsLink ?? false }, listing: items.map((item) => { return { size: item.size.toString(), name: item.name, path: getItemPath(item), hash: item.cid.toString(), shortHash: getShortHash(item.cid.toString()) }; }), name: dir.name, size: dir.size.toString(), path: dir.path, breadcrumbs: [], backLink: '', hash: dir.cid.toString() }; return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="description" content="A directory of content-addressed files hosted on IPFS."> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="shortcut icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAlo89/56ZQ/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACUjDu1lo89/6mhTP+zrVP/nplD/5+aRK8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHNiIS6Wjz3/ubFY/761W/+vp1D/urRZ/8vDZf/GvmH/nplD/1BNIm8AAAAAAAAAAAAAAAAAAAAAAAAAAJaPPf+knEj/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf+tpk7/nplD/wAAAAAAAAAAAAAAAJaPPf+2rVX/vrVb/761W/++tVv/vrVb/6+nUP+6tFn/y8Nl/8vDZf/Lw2X/y8Nl/8G6Xv+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/761W/+vp1D/urRZ/8vDZf/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/vrVb/761W/++tVv/r6dQ/7q0Wf/Lw2X/y8Nl/8vDZf/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf++tVv/vrVb/761W/++tVv/vbRa/5aPPf+emUP/y8Nl/8vDZf/Lw2X/y8Nl/8vDZf+emUP/AAAAAAAAAACWjz3/vrVb/761W/++tVv/vrVb/5qTQP+inkb/op5G/6KdRv/Lw2X/y8Nl/8vDZf/Lw2X/nplD/wAAAAAAAAAAlo89/761W/++tVv/sqlS/56ZQ//LxWb/0Mlp/9DJaf/Kw2X/oJtE/7+3XP/Lw2X/y8Nl/56ZQ/8AAAAAAAAAAJaPPf+9tFr/mJE+/7GsUv/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+xrFL/nplD/8vDZf+emUP/AAAAAAAAAACWjz3/op5G/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+inkb/nplD/wAAAAAAAAAAAAAAAKKeRv+3slb/0cpq/9HKav/Rymr/0cpq/9HKav/Rymr/0cpq/9HKav+1sFX/op5G/wAAAAAAAAAAAAAAAAAAAAAAAAAAop5GUKKeRv/Nxmf/0cpq/9HKav/Rymr/0cpq/83GZ/+inkb/op5GSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G16KeRv/LxWb/y8Vm/6KeRv+inkaPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAop5G/6KeRtcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/n8AAPgfAADwDwAAwAMAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAwAMAAPAPAAD4HwAA/n8AAA=="> <title>${dirData.path}</title> <style>${style}</style> </head> <body> <!-- # Some JSON content for debugging: ## dirData ${JSON.stringify(dirData, null, 2)} --> <header id="header"> <div class="ipfs-logo">&nbsp;</div> <!-- <nav> <a href="https://ipfs.tech" target="_blank" rel="noopener noreferrer">About<span class="dn-mobile"> IPFS</span></a> <a href="https://docs.ipfs.tech/install/" target="_blank" rel="noopener noreferrer">Install<span class="dn-mobile"> IPFS</span></a> </nav> --> </header> <main id="main"> <header class="flex flex-wrap"> <div> <strong>${dirListingTitle(dirData)}</strong> ${dirData.hash == null ? '' : `<div class="ipfs-hash" translate="no"> ${dirData.hash} </div>`} </div> ${dirData.size == null ? '' : `<div class="nowrap flex-shrink ml-auto"> <strong title="Cumulative size of IPFS DAG (data + metadata)">&nbsp;${dirData.size}</strong> </div>`} </header> <div> <div class="grid dir"> <!--{{ if .BackLink }} <div class="type-icon"> <div class="ipfs-_blank">&nbsp;</div> </div> <div> <a href="{{.BackLink | urlEscape}}">..</a> </div> <div></div> <div></div> </tr> {{ end }}--> ${getAllDirListingRows(dirData)} </div> </div> </main> </body> </html>`; }; const style = ` .ipfs-_blank { background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3ClinearGradient id='a' gradientUnits='userSpaceOnUse' x1='36' y1='1' x2='36' y2='99' gradientTransform='matrix(1 0 0 -1 0 100)'%3E%3Cstop offset='0' stop-color='%23c8d4db'/%3E%3Cstop offset='.139' stop-color='%23d8e1e6'/%3E%3Cstop offset='.359' stop-color='%23ebf0f3'/%3E%3Cstop offset='.617' stop-color='%23f9fafb'/%3E%3Cstop offset='1' stop-color='%23fff'/%3E%3C/linearGradient%3E%3Cpath d='M45 1l27 26.7V99H0V1h45z' fill='url(%23a)'/%3E%3Cpath d='M45 1l27 26.7V99H0V1h45z' fill-opacity='0' stroke='%237191a1' stroke-width='2'/%3E%3ClinearGradient id='b' gradientUnits='userSpaceOnUse' x1='45.068' y1='72.204' x2='58.568' y2='85.705' gradientTransform='matrix(1 0 0 -1 0 100)'%3E%3Cstop offset='0' stop-color='%23fff'/%3E%3Cstop offset='.35' stop-color='%23fafbfb'/%3E%3Cstop offset='.532' stop-color='%23edf1f4'/%3E%3Cstop offset='.675' stop-color='%23dde5e9'/%3E%3Cstop offset='.799' stop-color='%23c7d3da'/%3E%3Cstop offset='.908' stop-color='%23adbdc7'/%3E%3Cstop offset='1' stop-color='%2392a5b0'/%3E%3C/linearGradient%3E%3Cpath d='M45 1l27 26.7H45V1z' fill='url(%23b)'/%3E%3Cpath d='M45 1l27 26.7H45V1z' fill-opacity='0' stroke='%237191a1' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-size: contain } :root { --sans-serif: "Plex",system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif; --monospace: Consolas, monaco, monospace; --navy: #073a53; --teal: #6bc4ce; --turquoise: #47AFB4; --steel-gray: #3f5667; --dark-white: #d9dbe2; --light-white: #edf0f4; --near-white: #f7f8fa; --radius: 4px; } body { color: #34373f; font-family: var(--sans-serif); line-height: 1.43; margin: 0; word-break: break-all; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; -webkit-tap-highlight-color: transparent; } pre, code { font-family: var(--monospace); } a { color: #117eb3; text-decoration: none; } a:hover { color: #00b0e9; text-decoration: underline; } a:active,a:visited { color: #00b0e9; } .flex { display: flex; } .flex-wrap { flex-flow: wrap; } .flex-shrink { flex-shrink: 1; } .ml-auto { margin-left: auto; } .nowrap { white-space: nowrap } .ipfs-hash { color: #7f8491; font-family: var(--monospace); } #header { align-items: center; background: var(--navy); border-bottom: 4px solid var(--teal); color: #fff; display: flex; font-weight: 500; justify-content: space-between; padding: 0 1em; } #header a { color: var(--teal); } #header a:active { color: #9ad4db; } #header a:hover { color: #fff; } #header .ipfs-logo { height: 2.25em; margin: .7em .7em .7em 0; width: 7.15em } #header nav { align-items: center; display: flex; margin: .65em 0; } #header nav a { margin: 0 .6em; } #header nav a:last-child { margin: 0 0 0 .6em; } #header nav svg { fill: var(--teal); height: 1.8em; margin-top: .125em; } #header nav svg:hover { fill: #fff; } main { border: 1px solid var(--dark-white); border-radius: var(--radius); overflow: hidden; margin: 1em; font-size: .875em; } main header,main .container { padding-left: 1em; padding-right: 1em; } main header { padding-top: .7em; padding-bottom: .7em; background-color: var(--light-white); } main header,main section:not(:last-child) { border-bottom: 1px solid var(--dark-white); } main section header { background-color: var(--near-white); } .grid { display: grid; overflow-x: auto; } .grid .grid { overflow-x: visible; } .grid > div { padding: .7em; border-bottom: 1px solid var(--dark-white); } .grid.dir { grid-template-columns: min-content 1fr min-content min-content; } .grid.dir > div:nth-of-type(4n+1) { padding-left: 1em; } .grid.dir > div:nth-of-type(4n+4) { padding-right: 1em; } .grid.dir > div:nth-last-child(-n+4) { border-bottom: 0; } .grid.dir > div:nth-of-type(8n+5),.grid.dir > div:nth-of-type(8n+6),.grid.dir > div:nth-of-type(8n+7),.grid.dir > div:nth-of-type(8n+8) { background-color: var(--near-white); } .grid.dag { grid-template-columns: max-content 1fr; } .grid.dag pre { margin: 0; } .grid.dag .grid { padding: 0; } .grid.dag > div:nth-last-child(-n+2) { border-bottom: 0; } .grid.dag > div { background: white } .grid.dag > div:nth-child(4n),.grid.dag > div:nth-child(4n+3) { background-color: var(--near-white); } section > .grid.dag > div:nth-of-type(2n+1) { padding-left: 1em; } .type-icon,.type-icon > * { width: 1.15em } .terminal { background: var(--steel-gray); color: white; padding: .7em; border-radius: var(--radius); word-wrap: break-word; white-space: break-spaces; } @media print { #header { display: none; } #main header,.ipfs-hash,body { color: #000; } #main,#main header { border-color: #000; } a,a:visited { color: #000; text-decoration: underline; } a[href]:after { content: " (" attr(href) ")" } } @media only screen and (max-width: 500px) { .dn-mobile { display: none; } }`; //# sourceMappingURL=dir-index-html.js.map