working prototype of adding files to IPFS during publish flow

This commit is contained in:
Matthias Kretschmann 2019-09-04 15:07:00 +02:00
parent 414dcd455a
commit 1c59d49d5d
Signed by: m
GPG Key ID: 606EEEF3C479A91F
11 changed files with 8392 additions and 741 deletions

8893
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -23,6 +23,7 @@
"ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^4.1.2",
"history": "^4.9.0",
"ipfs": "^0.37.1",
"is-url-superb": "^3.0.0",
"moment": "^2.24.0",
"query-string": "^6.8.2",

1
client/src/@types/ipfs/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'ipfs'

View File

@ -36,23 +36,15 @@ export default class Button extends PureComponent<ButtonProps, any> {
classes = styles.button
}
if (to) {
return (
<Link to={to} className={cx(classes, className)} {...props}>
{children}
</Link>
)
}
if (href) {
return (
<a href={href} className={cx(classes, className)} {...props}>
{children}
</a>
)
}
return (
return to ? (
<Link to={to} className={cx(classes, className)} {...props}>
{children}
</Link>
) : href ? (
<a href={href} className={cx(classes, className)} {...props}>
{children}
</a>
) : (
<button className={cx(classes, className)} {...props}>
{children}
</button>

View File

@ -7,7 +7,7 @@
padding: .5rem;
}
// default: red square
/* default: red square */
.statusIndicator {
width: $font-size-small;
height: $font-size-small;
@ -15,7 +15,7 @@
background: $red;
}
// yellow triangle
/* yellow triangle */
.statusIndicatorCloseEnough {
composes: statusIndicator;
background: none;
@ -26,7 +26,7 @@
border-bottom: $font-size-small solid $yellow;
}
// green circle
/* green circle */
.statusIndicatorActive {
composes: statusIndicator;
border-radius: 50%;

View File

@ -0,0 +1,54 @@
/* eslint-disable no-console */
import Ipfs from 'ipfs'
import { useEffect, useState } from 'react'
let ipfs: any = null
let ipfsMessage: string | null = null
export default function useIpfs() {
const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs))
const [ipfsInitError, setIpfsInitError] = useState(null)
async function startIpfs() {
if (ipfs) {
console.log('IPFS already started')
// } else if (window.ipfs && window.ipfs.enable) {
// console.log('Found window.ipfs')
// ipfs = await window.ipfs.enable()
} else {
try {
const message = 'IPFS Started'
console.time(message)
ipfs = await Ipfs.create()
console.timeEnd(message)
ipfsMessage = message
} catch (error) {
const message = `IPFS init error: ${error.message}`
ipfsMessage = message
console.error(message)
ipfs = null
setIpfsInitError(error)
}
}
setIpfsReady(Boolean(ipfs))
}
useEffect(() => {
startIpfs()
// just like componentDidUnmount()
return function cleanup() {
if (ipfs && ipfs.stop) {
console.time('IPFS Stopped')
ipfs.stop()
setIpfsReady(false)
ipfs = null
ipfsMessage = null
console.timeEnd('IPFS Stopped')
}
}
}, [])
return { ipfs, isIpfsReady, ipfsInitError, ipfsMessage }
}

View File

@ -0,0 +1,5 @@
@import '../../../styles/variables';
.ipfsForm {
margin-top: $spacer;
}

View File

@ -0,0 +1,61 @@
/* eslint-disable no-console */
import React from 'react'
import axios from 'axios'
import useIpfs from '../../../hooks/use-ipfs'
import styles from './Ipfs.module.scss'
async function pingUrl(url: string) {
try {
const response = await axios(url)
if (response.status !== 200) console.error(`Could not find ${url}`)
console.log(`File found under ${url}`)
return
} catch (error) {
console.error(error.message)
}
}
export default function Ipfs({ addItem }: { addItem(url: string): void }) {
const { ipfs, ipfsInitError, ipfsMessage } = useIpfs()
async function saveToIpfs(buffer: Buffer) {
try {
const response = await ipfs.add(buffer)
const cid = response[0].hash
console.log(`File added: ${cid}`)
// ping url to make it globally available
const url = `https://ipfs.io/ipfs/${cid}`
await pingUrl(url)
// add IPFS url to file.url
addItem(url)
} catch (error) {
console.error(error.message)
}
}
function handleCaptureFile(files: FileList | null) {
const reader: any = new window.FileReader()
const file = files && files[0]
reader.readAsArrayBuffer(file)
reader.onloadend = () => {
const buffer: any = Buffer.from(reader.result)
saveToIpfs(buffer)
}
}
return (
<div className={styles.ipfsForm}>
<input
type="file"
onChange={e => handleCaptureFile(e.target.files)}
/>
{ipfsMessage && <div>{ipfsMessage}</div>}
{ipfsInitError && <div>{ipfsInitError}</div>}
</div>
)
}

View File

@ -1,5 +1,5 @@
import React from 'react'
import { render, fireEvent, waitForElement } from '@testing-library/react'
import { render, fireEvent, waitForElement, act } from '@testing-library/react'
import mockAxios from 'jest-mock-axios'
import Files from '.'
@ -56,16 +56,32 @@ describe('Files', () => {
const { container, getByText } = renderComponent()
// open
fireEvent.click(getByText('+ Add a file'))
fireEvent.click(getByText('+ Add a file URL'))
await waitForElement(() => getByText('- Cancel'))
expect(container.querySelector('.itemForm')).toBeInTheDocument()
// close
fireEvent.click(getByText('- Cancel'))
await waitForElement(() => getByText('+ Add a file'))
await waitForElement(() => getByText('+ Add a file URL'))
expect(container.querySelector('.grow-exit')).toBeInTheDocument()
})
it('new IPFS file form can be opened and closed', async () => {
const { container, getByText } = renderComponent()
// open
act(async () => {
fireEvent.click(getByText('+ Add to IPFS'))
await waitForElement(() => getByText('- Cancel'))
expect(container.querySelector('.ipfsForm')).toBeInTheDocument()
// close
fireEvent.click(getByText('- Cancel'))
await waitForElement(() => getByText('+ Add to IPFS'))
expect(container.querySelector('.grow-exit')).toBeInTheDocument()
})
})
it('item can be removed', async () => {
const { getByTitle } = renderComponent()
@ -76,7 +92,7 @@ describe('Files', () => {
it('item can be added', async () => {
const { getByText, getByPlaceholderText } = renderComponent()
fireEvent.click(getByText('+ Add a file'))
fireEvent.click(getByText('+ Add a file URL'))
await waitForElement(() => getByText('- Cancel'))
fireEvent.change(getByPlaceholderText('Hello'), {
target: { value: 'https://hello.com' }

View File

@ -1,15 +1,16 @@
import React, { FormEvent, PureComponent, ChangeEvent } from 'react'
import { CSSTransition, TransitionGroup } from 'react-transition-group'
import axios from 'axios'
import { Logger } from '@oceanprotocol/squid'
import Button from '../../../components/atoms/Button'
import Help from '../../../components/atoms/Form/Help'
import ItemForm from './ItemForm'
import Item from './Item'
import Ipfs from './Ipfs'
import styles from './index.module.scss'
import { serviceUri } from '../../../config'
import cleanupContentType from '../../../utils/cleanupContentType'
import { Logger } from '@oceanprotocol/squid'
export interface File {
url: string
@ -39,11 +40,13 @@ interface FilesProps {
interface FilesStates {
isFormShown: boolean
isIpfsFormShown: boolean
}
export default class Files extends PureComponent<FilesProps, FilesStates> {
public state: FilesStates = {
isFormShown: false
isFormShown: false,
isIpfsFormShown: false
}
// for canceling axios requests
@ -55,13 +58,17 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
private toggleForm = (e: Event) => {
e.preventDefault()
this.setState({ isFormShown: !this.state.isFormShown })
}
private addItem = async (value: string) => {
private toggleIpfsForm = (e: Event) => {
e.preventDefault()
this.setState({ isIpfsFormShown: !this.state.isIpfsFormShown })
}
private addItem = async (url: string) => {
const file: File = {
url: value,
url,
contentType: '',
found: false // non-standard
}
@ -71,14 +78,14 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
url: `${serviceUri}/api/v1/urlcheck`,
data: { url: value },
data: { url },
cancelToken: this.signal.token
})
const { contentLength, contentType, found } = response.data.result
file.contentLength = contentLength
file.contentType = contentType
file.compression = await cleanupContentType(contentType)
file.compression = cleanupContentType(contentType)
file.found = found
} catch (error) {
!axios.isCancel(error) && Logger.error(error.message)
@ -92,7 +99,10 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
}
}
this.props.onChange(event as any)
this.setState({ isFormShown: !this.state.isFormShown })
this.setState({
isFormShown: !this.state.isFormShown,
isIpfsFormShown: !this.state.isIpfsFormShown
})
}
private removeItem = (index: number) => {
@ -108,8 +118,8 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
}
public render() {
const { isFormShown } = this.state
const { files, help, placeholder, name, onChange } = this.props
const { isFormShown, isIpfsFormShown } = this.state
return (
<>
@ -148,7 +158,11 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
)}
<Button link onClick={this.toggleForm}>
{isFormShown ? '- Cancel' : '+ Add a file'}
{isFormShown ? '- Cancel' : '+ Add a file URL'}
</Button>
<Button link onClick={this.toggleIpfsForm}>
{isIpfsFormShown ? '- Cancel' : '+ Add to IPFS'}
</Button>
<CSSTransition
@ -163,6 +177,16 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
addItem={this.addItem}
/>
</CSSTransition>
<CSSTransition
classNames="grow"
in={isIpfsFormShown}
timeout={200}
unmountOnExit
onExit={() => this.setState({ isIpfsFormShown: false })}
>
<Ipfs addItem={this.addItem} />
</CSSTransition>
</div>
</>
)

18
package-lock.json generated
View File

@ -559,15 +559,15 @@
}
},
"acorn": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz",
"integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
"integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
"dev": true
},
"acorn-jsx": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
"integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz",
"integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==",
"dev": true
},
"ajv": {
@ -2118,9 +2118,9 @@
"dev": true
},
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"dev": true
},
"strip-ansi": {