diff --git a/.travis.yml b/.travis.yml index 772754b..d33b9b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,15 @@ dist: xenial +sudo: required language: node_js node_js: node cache: npm +before_install: + # Fixes an issue where the max file watch count is exceeded, triggering ENOSPC + # https://stackoverflow.com/questions/22475849/node-js-error-enospc#32600959 + - echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p + # will run `npm install` automatically here script: diff --git a/src/components/Add.module.css b/src/components/Add.module.css index e8943aa..203bd0b 100644 --- a/src/components/Add.module.css +++ b/src/components/Add.module.css @@ -1,3 +1,5 @@ +@import '../styles/_variables.css'; + .add { max-width: 40rem; width: 100%; @@ -5,3 +7,9 @@ word-wrap: break-word; word-break: break-all; } + +.error { + font-size: var(--font-size-small); + color: var(--red); + margin-top: var(--spacer); +} diff --git a/src/components/Add.tsx b/src/components/Add.tsx index b982048..36f01a4 100644 --- a/src/components/Add.tsx +++ b/src/components/Add.tsx @@ -1,25 +1,85 @@ -import React, { useState } from 'react' -import { saveToIpfs } from '../ipfs' -import { ipfsGateway } from '../../site.config' +import React, { useState, useEffect } from 'react' +import { ipfsNodeUri, ipfsGateway } from '../../site.config' import Dropzone from './Dropzone' import styles from './Add.module.css' import Spinner from './Spinner' +import useIpfsApi, { IpfsConfig } from '../hooks/use-ipfs-api' + +export function formatBytes(a: number, b: number) { + if (a === 0) return '0 Bytes' + const c = 1024 + const d = b || 2 + const e = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + const f = Math.floor(Math.log(a) / Math.log(c)) + return parseFloat((a / Math.pow(c, f)).toFixed(d)) + ' ' + e[f] +} + +const { hostname, port, protocol } = new URL(ipfsNodeUri) + +const ipfsConfig: IpfsConfig = { + protocol: protocol.replace(':', ''), + host: hostname, + port: port || '443' +} + +async function addToIpfs( + files: File[], + setFileSizeReceived: (size: string) => void +) { + const { ipfs } = useIpfsApi(ipfsConfig) + const file = [...files][0] + const fileDetails = { path: file.name, content: file } + + const response = await ipfs.add(fileDetails, { + wrapWithDirectory: true, + progress: (length: number) => setFileSizeReceived(formatBytes(length, 0)) + }) + + // CID of wrapping directory is returned last + const cid = response[response.length - 1].hash + return cid +} export default function Add() { + const { isIpfsReady, ipfsError } = useIpfsApi(ipfsConfig) const [fileHash, setFileHash] = useState() const [loading, setLoading] = useState(false) + const [message, setMessage] = useState() + const [error, setError] = useState() + const [fileSize, setFileSize] = useState() + const [fileSizeReceived, setFileSizeReceived] = useState('') + + useEffect(() => { + setMessage( + `Adding to IPFS
+ ${fileSizeReceived || 0}/${fileSize}
` + ) + }, [fileSize, fileSizeReceived]) + + async function handleOnDrop(acceptedFiles: File[]) { + if (!acceptedFiles[0]) return - const handleCaptureFile = async (files: File[]) => { setLoading(true) - const cid = await saveToIpfs(files) - setFileHash(cid) - setLoading(false) + setError(null) + + const totalSize = formatBytes(acceptedFiles[0].size, 0) + setFileSize(totalSize) + + try { + const cid = await addToIpfs(acceptedFiles, setFileSizeReceived) + if (!cid) return + setFileHash(cid) + setLoading(false) + } catch (error) { + setError(`Adding to IPFS failed: ${error.message}`) + return null + } } return (
{loading ? ( - + ) : fileHash ? ( ) : ( - + <> + + {(error || ipfsError) && ( +
{error || ipfsError}
+ )} + )}
) diff --git a/src/components/Dropzone.module.css b/src/components/Dropzone.module.css index 8a9161d..1bfcf53 100644 --- a/src/components/Dropzone.module.css +++ b/src/components/Dropzone.module.css @@ -22,7 +22,7 @@ .disabled { composes: dropzone; - opacity: 0.5; + opacity: 0.3; pointer-events: none; } diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx index db98922..8ec4949 100644 --- a/src/components/Spinner.tsx +++ b/src/components/Spinner.tsx @@ -1,6 +1,17 @@ import React from 'react' import styles from './Spinner.module.css' -export default function Spinner() { - return
+const Spinner = ({ message }: { message?: string }) => { + return ( +
+ {message && ( +
+ )} +
+ ) } + +export default Spinner diff --git a/src/hooks/use-ipfs-api.tsx b/src/hooks/use-ipfs-api.tsx new file mode 100644 index 0000000..0ce0ae7 --- /dev/null +++ b/src/hooks/use-ipfs-api.tsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from 'react' +import ipfsClient from 'ipfs-http-client' + +let ipfs: any = null +let ipfsVersion = '' + +export interface IpfsConfig { + protocol: string + host: string + port: string +} + +export default function useIpfsApi(config: IpfsConfig) { + const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs)) + const [ipfsError, setIpfsError] = useState('') + + async function initIpfs() { + if (ipfs !== null) return + // eslint-disable-next-line + ipfs = await ipfsClient(config) + + try { + const version = await ipfs.version() + ipfsVersion = version.version + } catch (error) { + setIpfsError(`IPFS connection error: ${error.message}`) + return + } + setIpfsReady(Boolean(await ipfs.id())) + } + + useEffect(() => { + initIpfs() + }, [config]) + + useEffect(() => { + // just like componentWillUnmount() + return function cleanup() { + if (ipfs) { + setIpfsReady(false) + ipfs = null + ipfsVersion = '' + setIpfsError('') + } + } + }, []) + + return { ipfs, ipfsVersion, isIpfsReady, ipfsError } +} diff --git a/src/ipfs.ts b/src/ipfs.ts deleted file mode 100644 index 053a56f..0000000 --- a/src/ipfs.ts +++ /dev/null @@ -1,35 +0,0 @@ -import ipfsClient from 'ipfs-http-client' -import { ipfsNodeUri } from '../site.config' - -export async function saveToIpfs(files: File[]) { - const { hostname, port, protocol } = new URL(ipfsNodeUri) - - const ipfsConfig = { - protocol: protocol.replace(':', ''), - host: hostname, - port: port || '443' - } - - const ipfs = ipfsClient(ipfsConfig) - - const file = [...files][0] - let ipfsId - const fileDetails = { - path: file.name, - content: file - } - const options = { - wrapWithDirectory: true, - progress: (prog: number) => console.log(`received: ${prog}`) - } - - try { - const response = await ipfs.add(fileDetails, options) - - // CID of wrapping directory is returned last - ipfsId = `${response[response.length - 1].hash}/${fileDetails.path}` - return ipfsId - } catch (error) { - console.error(error.message) - } -}