1
0
mirror of https://github.com/kremalicious/ipfs.git synced 2024-11-21 17:27:06 +01:00

migrate to new APIs, fix adding files

This commit is contained in:
Matthias Kretschmann 2021-09-12 16:41:30 +02:00
parent b03d3df2fc
commit e04041c46b
Signed by: m
GPG Key ID: 606EEEF3C479A91F
11 changed files with 21878 additions and 750 deletions

View File

@ -22,7 +22,8 @@
"prettier/@typescript-eslint", "prettier/@typescript-eslint",
"prettier/react", "prettier/react",
"plugin:prettier/recommended", "plugin:prettier/recommended",
"plugin:react/recommended" "plugin:react/recommended",
"plugin:react-hooks/recommended"
], ],
"plugins": ["@typescript-eslint", "react"], "plugins": ["@typescript-eslint", "react"],
"rules": { "rules": {

22418
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.1",
"ipfs-http-client": "^48.1.3", "ipfs-http-client": "^52.0.3",
"next": "^10.0.7", "next": "^10.0.7",
"next-seo": "^4.20.0", "next-seo": "^4.20.0",
"next-svgr": "^0.0.2", "next-svgr": "^0.0.2",
@ -41,6 +41,7 @@
"eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0", "eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"jest": "^26.6.3", "jest": "^26.6.3",
"postcss-preset-env": "^6.7.0", "postcss-preset-env": "^6.7.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
@ -48,7 +49,7 @@
"typescript": "^4.2.2" "typescript": "^4.2.2"
}, },
"engines": { "engines": {
"node": "10.x" "node": "14.x"
}, },
"browserslist": [ "browserslist": [
">0.2%", ">0.2%",

View File

@ -11,5 +11,3 @@ declare module '*.svg' {
const src: string const src: string
export default src export default src
} }
declare module 'ipfs-http-client'

11
src/@types/ipfs.d.ts vendored
View File

@ -1,5 +1,8 @@
export interface IpfsConfig { import { CID } from 'ipfs-http-client'
protocol: string
host: string export interface FileIpfs {
port: string path: string
cid: CID
size: number
mode?: number
} }

View File

@ -4,40 +4,37 @@ import Dropzone, { FileDropzone } from './Dropzone'
import styles from './Add.module.css' import styles from './Add.module.css'
import Loader from './Loader' import Loader from './Loader'
import useIpfsApi from '../hooks/use-ipfs-api' import useIpfsApi from '../hooks/use-ipfs-api'
import { IpfsConfig } from '../@types/ipfs' import { FileIpfs } from '../@types/ipfs'
import { addToIpfs, FileIpfs } from '../ipfs'
const { hostname, port, protocol } = new URL(ipfsNodeUri) const { hostname, port, protocol } = new URL(ipfsNodeUri)
const ipfsConfig: IpfsConfig = { const ipfsConfig = {
protocol: protocol.replace(':', ''), protocol: protocol.replace(':', ''),
host: hostname, host: hostname,
port: port || '443' port: Number(port) || 443
} }
export default function Add(): ReactElement { export default function Add(): ReactElement {
const { ipfs, isIpfsReady, ipfsError } = useIpfsApi(ipfsConfig) const { ipfs, isIpfsReady, ipfsError, addFiles } = useIpfsApi(ipfsConfig)
const [files, setFiles] = useState([]) const [files, setFiles] = useState<FileIpfs[]>()
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [message] = useState() const [message] = useState()
const [error, setError] = useState('') const [error, setError] = useState<string>()
async function handleOnDrop(acceptedFiles: FileDropzone[]): Promise<any> { async function handleOnDrop(acceptedFiles: FileDropzone[]): Promise<void> {
if (!acceptedFiles) return if (!acceptedFiles || !ipfs || !isIpfsReady) return
setLoading(true) setLoading(true)
setError('') setError(undefined)
try { try {
const directoryCid = await addToIpfs(ipfs, acceptedFiles) const addedFiles = await addFiles(acceptedFiles)
if (!directoryCid) return if (!addedFiles) return
setFiles(addedFiles)
const fileList = await ipfs.ls(directoryCid)
setFiles(fileList)
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
setError(`Adding to IPFS failed: ${error.message}`) setError(`Adding to IPFS failed: ${(error as Error).message}`)
return null return
} }
} }
@ -45,14 +42,14 @@ export default function Add(): ReactElement {
<div className={styles.add}> <div className={styles.add}>
{loading ? ( {loading ? (
<Loader message={message} /> <Loader message={message} />
) : files.length ? ( ) : files?.length ? (
<ul style={{ textAlign: 'left' }}> <ul style={{ textAlign: 'left' }}>
{files.map((file: FileIpfs) => ( {files?.map((file: FileIpfs) => (
<li key={file.path}> <li key={file.path}>
<a <a
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
href={`${ipfsGateway}/ipfs/${file.path}`} href={`${ipfsGateway}/ipfs/${file.cid.toString()}`}
> >
ipfs://{file.path} ipfs://{file.path}
</a> </a>
@ -61,6 +58,7 @@ export default function Add(): ReactElement {
</ul> </ul>
) : ( ) : (
<Dropzone <Dropzone
multiple
handleOnDrop={handleOnDrop} handleOnDrop={handleOnDrop}
disabled={!isIpfsReady} disabled={!isIpfsReady}
error={error || ipfsError} error={error || ipfsError}

View File

@ -43,7 +43,7 @@ export default function Dropzone({
`Drop it like it's hot!` `Drop it like it's hot!`
) : multiple === false ? ( ) : multiple === false ? (
`Drag 'n' drop a file here, or click to select a file.` `Drag 'n' drop a file here, or click to select a file.`
) : error !== '' ? ( ) : error !== undefined ? (
<div className={styles.error}>{error}</div> <div className={styles.error}>{error}</div>
) : ( ) : (
`Drag 'n' drop some files here, or click to select files.` `Drag 'n' drop some files here, or click to select files.`

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, ReactElement } from 'react' import React, { useState, useEffect, ReactElement, useCallback } from 'react'
import { pingUrl } from '../utils' import { pingUrl } from '../utils'
import { ipfsGateway, ipfsNodeUri } from '../../site.config' import { ipfsGateway, ipfsNodeUri } from '../../site.config'
import styles from './Status.module.css' import styles from './Status.module.css'
@ -7,7 +7,7 @@ export default function Status({ type }: { type: string }): ReactElement {
const [isOnline, setIsOnline] = useState(false) const [isOnline, setIsOnline] = useState(false)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
async function ping() { const ping = useCallback(async () => {
const url = const url =
type === 'gateway' type === 'gateway'
? `${ipfsGateway}/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/readme` ? `${ipfsGateway}/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/readme`
@ -15,8 +15,8 @@ export default function Status({ type }: { type: string }): ReactElement {
const ping = await pingUrl(url) const ping = await pingUrl(url)
setIsLoading(false) setIsLoading(false)
isOnline !== ping && setIsOnline(ping) setIsOnline(ping)
} }, [type])
useEffect(() => { useEffect(() => {
ping() ping()
@ -30,7 +30,7 @@ export default function Status({ type }: { type: string }): ReactElement {
setIsOnline(false) setIsOnline(false)
setIsLoading(false) setIsLoading(false)
} }
}, []) }, [ping])
const classes = isLoading const classes = isLoading
? styles.loading ? styles.loading

View File

@ -1,53 +1,76 @@
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import ipfsClient from 'ipfs-http-client' import { create, IPFSHTTPClient, Options } from 'ipfs-http-client'
import { parseHTML } from '../utils' import { formatBytes } from '../utils'
import { IpfsConfig } from '../@types/ipfs' import { FileDropzone } from '../components/Dropzone'
import { FileIpfs } from '../@types/ipfs'
let ipfs: any = null export interface IpfsApiValue {
let ipfsVersion = '' ipfs: IPFSHTTPClient | undefined
version: string | undefined
isIpfsReady: boolean
ipfsError: string | undefined
addFiles: (files: FileDropzone[]) => Promise<FileIpfs[] | undefined>
}
export default function useIpfsApi(config: IpfsConfig) { export default function useIpfsApi(config: Options): IpfsApiValue {
const [ipfs, setIpfs] = useState<IPFSHTTPClient>()
const [version, setVersion] = useState<string>()
const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs)) const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs))
const [ipfsError, setIpfsError] = useState('') const [ipfsError, setIpfsError] = useState<string>()
const addFiles = useCallback(
async (files: FileDropzone[]): Promise<FileIpfs[] | undefined> => {
if (!ipfs || !files?.length) return
const ipfsFiles = [
...files.map((file: FileDropzone) => {
return { path: file.path, content: file }
})
]
const options = {
wrapWithDirectory: true,
progress: (length: number) => formatBytes(length, 0)
}
const results: FileIpfs[] = []
for await (const result of ipfs.addAll(ipfsFiles, options)) {
console.log(result)
results.push(result)
}
return results
},
[ipfs]
)
useEffect(() => { useEffect(() => {
if (ipfs) return
async function initIpfs() { async function initIpfs() {
if (ipfs !== null) return
ipfs = await ipfsClient(config)
try { try {
const ipfs = create(config)
setIpfs(ipfs)
const version = await ipfs.version() const version = await ipfs.version()
ipfsVersion = version.version setVersion(version.version)
} catch (error) { setIpfsReady(Boolean(ipfs && (await ipfs.id())))
let { message } = error } catch (e) {
setIpfsError(`IPFS connection error: ${(e as Error).message}`)
if (!error.status) {
const htmlData = parseHTML(error)
message = htmlData.item(0)
message = message.textContent
}
setIpfsError(`IPFS connection error: ${message}`)
setIpfsReady(false) setIpfsReady(false)
return return
} }
setIpfsReady(Boolean(await ipfs.id()))
} }
initIpfs() initIpfs()
}, [config])
useEffect(() => { return () => {
// just like componentWillUnmount()
return function cleanup() {
if (ipfs) { if (ipfs) {
setIpfsReady(false) setIpfsReady(false)
ipfs = null setIpfs(undefined)
ipfsVersion = '' setVersion(undefined)
setIpfsError('') setIpfsError(undefined)
} }
} }
}, []) }, [config, ipfs])
return { ipfs, ipfsVersion, isIpfsReady, ipfsError } return { ipfs, version, isIpfsReady, ipfsError, addFiles }
} }

View File

@ -1,42 +0,0 @@
import { FileDropzone } from './components/Dropzone'
export interface FileIpfsAdd {
path: string
content: File | ReadableStream | Buffer | string
}
export interface FileIpfs {
path: string
hash: string
size: number
}
export function streamFiles(ipfs: any, ipfsFiles: FileIpfsAdd[]) {
return new Promise((resolve, reject) => {
const stream = ipfs.addReadableStream({
wrapWithDirectory: true
// progress: (length: number) => setFileSizeReceived(formatBytes(length, 0))
})
console.log(stream)
stream.on('data', (data: FileIpfs) => {
console.log(`Added ${data.path} hash: ${data.hash}`)
// The last data event will contain the directory hash
if (data.path === '') resolve(data.hash)
})
stream.on('error', reject)
ipfsFiles.forEach((file: FileIpfsAdd) => stream.write(file))
stream.end()
})
}
export async function addToIpfs(ipfs: any, files: FileDropzone[]) {
const ipfsFiles = [
...files.map((file: FileDropzone) => {
return { path: file.path, content: file }
})
]
const directoryCid = await streamFiles(ipfs, ipfsFiles)
return directoryCid
}

View File

@ -1,6 +1,6 @@
import axios from 'axios' import axios from 'axios'
export function formatBytes(a: number, b: number) { export function formatBytes(a: number, b: number): string {
if (a === 0) return '0 Bytes' if (a === 0) return '0 Bytes'
const c = 1024 const c = 1024
const d = b || 2 const d = b || 2
@ -9,18 +9,18 @@ export function formatBytes(a: number, b: number) {
return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f] return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f]
} }
export async function pingUrl(url: string) { export async function pingUrl(url: string): Promise<boolean> {
try { try {
const response = await axios(url, { timeout: 5000 }) const response = await axios(url, { timeout: 5000 })
if (!response || response.status !== 200) return false if (!response || response.status !== 200) return false
return true return true
} catch (error) { } catch (error) {
console.error(error.message) console.error((error as Error).message)
return false return false
} }
} }
export function parseHTML(str: string) { export function parseHTML(str: string): HTMLCollection {
const tmp = document.implementation.createHTMLDocument() const tmp = document.implementation.createHTMLDocument()
tmp.body.innerHTML = str tmp.body.innerHTML = str
return tmp.body.children return tmp.body.children