-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: rendered CID in browser with content-type recognition
- Loading branch information
Showing
6 changed files
with
292 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* eslint-disable @typescript-eslint/strict-boolean-expressions */ | ||
import React, { useEffect } from 'react' | ||
|
||
import { CID } from 'multiformats/cid' | ||
// import Video from './Video' | ||
|
||
/** | ||
* Test files: | ||
* bafkreienxxjqg3jomg5b75k7547dgf7qlbd3qpxy2kbg537ck3rol4mcve - text - https://bafkreienxxjqg3jomg5b75k7547dgf7qlbd3qpxy2kbg537ck3rol4mcve.ipfs.w3s.link/?filename=test.txt | ||
* bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra - image/jpeg - http://127.0.0.1:8080/ipfs/bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra?filename=bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra | ||
* bafkreif4ufrfpfcmqn5ltjvmeisgv4k7ykqz2mjygpngtwt4bijxosidqa - image/svg+xml - https://bafkreif4ufrfpfcmqn5ltjvmeisgv4k7ykqz2mjygpngtwt4bijxosidqa.ipfs.dweb.link/?filename=Web3.Storage-logo.svg | ||
* bafybeiekildl23opzqcsufewlbadhbabs6pyqg35tzpfavgtjyhchyikxa - video/quicktime - https://bafybeiekildl23opzqcsufewlbadhbabs6pyqg35tzpfavgtjyhchyikxa.ipfs.dweb.link | ||
* bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq - video/webm (147.78 KiB) - https://bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq.ipfs.dweb.link | ||
* bafybeierkpfmf4vhtdiujgahfptiyriykoetxf3rmd3vcxsdemincpjoyu - video/mp4 (2.80 MiB) - https://bafybeierkpfmf4vhtdiujgahfptiyriykoetxf3rmd3vcxsdemincpjoyu.ipfs.dweb.link | ||
* QmbGtJg23skhvFmu9mJiePVByhfzu5rwo74MEkVDYAmF5T - video (160MiB) | ||
*/ | ||
|
||
/** | ||
* | ||
* Test CIDs | ||
* QmbGtJg23skhvFmu9mJiePVByhfzu5rwo74MEkVDYAmF5T | ||
* | ||
*/ | ||
|
||
export default function CidRenderer ({ cid }: { cid: string }): JSX.Element { | ||
// const [isVideo, setIsVideo] = React.useState(false) | ||
// const [isImage, setIsImage] = React.useState(false) | ||
const [contentType, setContentType] = React.useState<string | null>(null) | ||
const [isLoading, setIsLoading] = React.useState(false) | ||
const [abortController, setAbortController] = React.useState<AbortController | null>(null) | ||
const [blob, setBlob] = React.useState<Blob | null>(null) | ||
const [text, setText] = React.useState('') | ||
|
||
useEffect(() => { | ||
if (cid === null || cid === '' || isLoading) { | ||
return | ||
} | ||
try { | ||
CID.parse(cid) | ||
} catch { | ||
return | ||
} | ||
// cancel previous fetchRequest when cid is changed | ||
abortController?.abort() | ||
// set loading to bloack this useEffect from running until done. | ||
setIsLoading(true) | ||
const newAbortController = new AbortController() | ||
setAbortController(newAbortController) | ||
// console.log('fetching from CidRenderer') | ||
const fetchContent = async (): Promise<void> => { | ||
const res = await fetch(`ipfs/${cid}`, { | ||
signal: newAbortController.signal, | ||
method: 'GET', | ||
headers: { | ||
// cache: 'no-cache', // uncomment when testing | ||
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8' | ||
} | ||
}) | ||
const contentType = res.headers.get('content-type') | ||
// console.log('res.headers: ', res.headers) | ||
// console.log('contentType: ', contentType) | ||
setContentType(contentType) | ||
console.log('contentType: ', contentType) | ||
// if ((contentType?.startsWith('video/')) === true) { | ||
// // setIsVideo(true) | ||
// } else if ((contentType?.startsWith('image/')) === true) { | ||
// // setIsImage(true) | ||
// } | ||
setBlob(await res.clone().blob()) | ||
setText(await res.text()) | ||
setIsLoading(false) | ||
} | ||
|
||
void fetchContent() | ||
}, [cid]) | ||
|
||
// console.log('cid: ', cid) | ||
if (cid == null || cid === '') { | ||
return <span>Nothing to render yet. Enter a CID</span> // bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq | ||
} | ||
try { | ||
CID.parse(cid) | ||
} catch { | ||
return <span>Invalid CID</span> | ||
} | ||
|
||
if (isLoading) { | ||
return <span>Loading...</span> | ||
} | ||
|
||
if (contentType?.startsWith('video/') && blob != null) { | ||
return ( | ||
<video controls autoPlay loop className="center" width="100%"> | ||
<source src={URL.createObjectURL(blob)} type={contentType} /> | ||
</video> | ||
) | ||
} | ||
if (contentType?.startsWith('image/') && blob != null) { | ||
return <img src={URL.createObjectURL(blob)} /> | ||
} | ||
if (contentType?.startsWith('text/') && blob != null) { | ||
return <pre>{text}</pre> | ||
} | ||
return <span>Not a supported content-type of <pre>{contentType}</pre></span> | ||
// return ( | ||
// <video controls autoPlay loop className="center"> | ||
// <source src={`/ipfs/${cid}`} type="video/mp4" /> | ||
// </video> | ||
// ) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import React from 'react' | ||
|
||
export default function Video ({ cid }: { cid: string }): JSX.Element { | ||
return ( | ||
<video controls autoPlay loop className="center"> | ||
<source src={`/ipfs/${cid}`} type="video/mp4" /> | ||
</video> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import type { Helia } from '@helia/interface' | ||
import { unixfs } from '@helia/unixfs' | ||
import { CID } from 'multiformats/cid' | ||
import FileType from 'file-type/core' | ||
|
||
// import { getHelia } from '../get-helia.ts' | ||
|
||
export interface HeliaFetchOptions { | ||
path: string | ||
helia: Helia | ||
} | ||
|
||
function mergeUint8Arrays (a: Uint8Array, b: Uint8Array): Uint8Array { | ||
const c = new Uint8Array(a.length + b.length) | ||
c.set(a, 0) | ||
c.set(b, a.length) | ||
return c | ||
} | ||
|
||
/** | ||
* Test files: | ||
* bafkreienxxjqg3jomg5b75k7547dgf7qlbd3qpxy2kbg537ck3rol4mcve - text - https://bafkreienxxjqg3jomg5b75k7547dgf7qlbd3qpxy2kbg537ck3rol4mcve.ipfs.w3s.link/?filename=test.txt | ||
* bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra - image/jpeg - http://127.0.0.1:8080/ipfs/bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra?filename=bafkreicafxt3zr4cshf7qteztjzl62ouxqrofu647e44wt7s2iaqjn7bra | ||
* QmY7fzZEpgDUqZ7BEePSS5JxxezDj3Zy36EEpWSmKmv5mo - image/jpeg - http://127.0.0.1:8080/ipfs/QmY7fzZEpgDUqZ7BEePSS5JxxezDj3Zy36EEpWSmKmv5mo?filename=QmY7fzZEpgDUqZ7BEePSS5JxxezDj3Zy36EEpWSmKmv5mo | ||
* bafkreif4ufrfpfcmqn5ltjvmeisgv4k7ykqz2mjygpngtwt4bijxosidqa - image/svg+xml - https://bafkreif4ufrfpfcmqn5ltjvmeisgv4k7ykqz2mjygpngtwt4bijxosidqa.ipfs.dweb.link/?filename=Web3.Storage-logo.svg | ||
* bafybeiekildl23opzqcsufewlbadhbabs6pyqg35tzpfavgtjyhchyikxa - video/quicktime - https://bafybeiekildl23opzqcsufewlbadhbabs6pyqg35tzpfavgtjyhchyikxa.ipfs.dweb.link | ||
* bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq - video/webm (147.78 KiB) - https://bafkreiezuss4xkt5gu256vjccx7vocoksxk77vwmdrpwoumfbbxcy2zowq.ipfs.dweb.link | ||
* bafybeierkpfmf4vhtdiujgahfptiyriykoetxf3rmd3vcxsdemincpjoyu - video/mp4 (2.80 MiB) - https://bafybeierkpfmf4vhtdiujgahfptiyriykoetxf3rmd3vcxsdemincpjoyu.ipfs.dweb.link | ||
*/ | ||
|
||
function isSvgText (bytes: Uint8Array): boolean { | ||
const svgText = new TextDecoder().decode(bytes.slice(0, 4)) | ||
return svgText.startsWith('<svg') | ||
} | ||
|
||
function handleVideoMimeTypes (videoMimeType: string): string { | ||
// console.log('videoMimeType: ', videoMimeType) | ||
// return 'video/webm;codecs=h264' | ||
switch (videoMimeType) { | ||
// case 'video/mp4': | ||
// return 'video/webm;codecs=h264' | ||
case 'video/quicktime': | ||
return 'video/mp4' | ||
default: | ||
return videoMimeType | ||
} | ||
} | ||
|
||
/** | ||
* TODO: support video files (v0=playable, v1=seekable and navigable) | ||
* TODO: support audio files | ||
* | ||
* @param param0 | ||
* | ||
* For inspiration | ||
* @see https://github.com/ipfs/js-ipfs/blob/master/packages/ipfs-http-response/src/utils/content-type.js | ||
* @see https://github.com/RangerMauve/js-ipfs-fetch | ||
* @returns | ||
*/ | ||
async function getContentType ({ cid, bytes }: { cid?: unknown, bytes: Uint8Array }): Promise<string> { | ||
// const fileType = magicBytesFiletype(bytes) | ||
// console.log('magicBytesFiletype(bytes): ', magicBytesFiletype(bytes)) | ||
const fileTypeDep = await FileType.fromBuffer(bytes) | ||
if (typeof fileTypeDep !== 'undefined') { | ||
// console.log('fileTypeDep.mime: ', fileTypeDep.mime) | ||
return handleVideoMimeTypes(fileTypeDep.mime) | ||
} | ||
|
||
if (isSvgText(bytes)) { | ||
return 'image/svg+xml' | ||
} | ||
|
||
return 'text/plain' | ||
} | ||
|
||
/** | ||
* * TODO: implement as much of the gateway spec as possible. | ||
* * TODO: why we would be better than ipfs.io/other-gateway | ||
* * TODO: have error handling that renders 404/500/other if the request is bad. | ||
* | ||
* @param event | ||
* @returns | ||
*/ | ||
export async function heliaFetch ({ path, helia }: HeliaFetchOptions): Promise<Response> { | ||
const pathParts = path.split('/') | ||
// console.log('pathParts: ', pathParts) | ||
// const scheme = pathParts[1] | ||
// console.log('scheme: ', scheme) | ||
const cidString = pathParts[2] | ||
// console.log('cidString: ', cidString) | ||
// const helia = await getHelia({ libp2pConfigType: 'ipni' }) | ||
|
||
const fs = unixfs(helia) | ||
const cid = CID.parse(cidString) | ||
|
||
const asyncIt = fs.cat(cid) | ||
|
||
const iter = asyncIt[Symbol.asyncIterator]() | ||
|
||
let next = iter.next | ||
next = next.bind(iter) | ||
|
||
// we have to get the first chunk before responding or else the response has the incorrect content type | ||
const firstChunk = await next() | ||
|
||
const fileType: string = await getContentType({ bytes: firstChunk.value }) | ||
// if (typeof chunkFileType !== 'undefined') { | ||
// fileType = handleVideoMimeTypes(chunkFileType.mime) | ||
// } | ||
|
||
// let bytes: Uint8Array = new Uint8Array() | ||
// let chunkFiletype: FileType.FileTypeResult | undefined | ||
const readableStream = new ReadableStream({ | ||
async start (controller) { | ||
controller.enqueue(firstChunk.value) | ||
// if (fileType == null) { | ||
// } | ||
|
||
if (firstChunk.done === true) { | ||
controller.close() | ||
return | ||
} | ||
for (let { value, done } = await next(); done === false; { value, done } = await next()) { | ||
// for await (const chunk of fs.cat(cid)) { | ||
const chunk = value | ||
// console.log('chunk: ', chunk) | ||
controller.enqueue(chunk) | ||
} | ||
// console.log('final fileType: ', fileType) | ||
controller.close() | ||
} | ||
}) | ||
|
||
// need to return byte stream | ||
return new Response(readableStream, { | ||
headers: { | ||
'Cache-Control': 'public, max-age=29030400, immutable', // same as ipfs.io gateway | ||
// 'Cache-Control': 'no-cache', // disable caching when debugging | ||
'Content-Type': fileType ?? 'text/plain' | ||
} | ||
|
||
}) | ||
} |
Oops, something went wrong.