1
0
mirror of https://github.com/oceanprotocol/commons.git synced 2023-03-15 18:03:00 +01:00

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", "ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^4.1.2", "filesize": "^4.1.2",
"history": "^4.9.0", "history": "^4.9.0",
"ipfs": "^0.37.1",
"is-url-superb": "^3.0.0", "is-url-superb": "^3.0.0",
"moment": "^2.24.0", "moment": "^2.24.0",
"query-string": "^6.8.2", "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 classes = styles.button
} }
if (to) { return to ? (
return ( <Link to={to} className={cx(classes, className)} {...props}>
<Link to={to} className={cx(classes, className)} {...props}> {children}
{children} </Link>
</Link> ) : href ? (
) <a href={href} className={cx(classes, className)} {...props}>
} {children}
</a>
if (href) { ) : (
return (
<a href={href} className={cx(classes, className)} {...props}>
{children}
</a>
)
}
return (
<button className={cx(classes, className)} {...props}> <button className={cx(classes, className)} {...props}>
{children} {children}
</button> </button>

View File

@ -7,7 +7,7 @@
padding: .5rem; padding: .5rem;
} }
// default: red square /* default: red square */
.statusIndicator { .statusIndicator {
width: $font-size-small; width: $font-size-small;
height: $font-size-small; height: $font-size-small;
@ -15,7 +15,7 @@
background: $red; background: $red;
} }
// yellow triangle /* yellow triangle */
.statusIndicatorCloseEnough { .statusIndicatorCloseEnough {
composes: statusIndicator; composes: statusIndicator;
background: none; background: none;
@ -26,7 +26,7 @@
border-bottom: $font-size-small solid $yellow; border-bottom: $font-size-small solid $yellow;
} }
// green circle /* green circle */
.statusIndicatorActive { .statusIndicatorActive {
composes: statusIndicator; composes: statusIndicator;
border-radius: 50%; 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 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 mockAxios from 'jest-mock-axios'
import Files from '.' import Files from '.'
@ -56,16 +56,32 @@ describe('Files', () => {
const { container, getByText } = renderComponent() const { container, getByText } = renderComponent()
// open // open
fireEvent.click(getByText('+ Add a file')) fireEvent.click(getByText('+ Add a file URL'))
await waitForElement(() => getByText('- Cancel')) await waitForElement(() => getByText('- Cancel'))
expect(container.querySelector('.itemForm')).toBeInTheDocument() expect(container.querySelector('.itemForm')).toBeInTheDocument()
// close // close
fireEvent.click(getByText('- Cancel')) fireEvent.click(getByText('- Cancel'))
await waitForElement(() => getByText('+ Add a file')) await waitForElement(() => getByText('+ Add a file URL'))
expect(container.querySelector('.grow-exit')).toBeInTheDocument() 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 () => { it('item can be removed', async () => {
const { getByTitle } = renderComponent() const { getByTitle } = renderComponent()
@ -76,7 +92,7 @@ describe('Files', () => {
it('item can be added', async () => { it('item can be added', async () => {
const { getByText, getByPlaceholderText } = renderComponent() const { getByText, getByPlaceholderText } = renderComponent()
fireEvent.click(getByText('+ Add a file')) fireEvent.click(getByText('+ Add a file URL'))
await waitForElement(() => getByText('- Cancel')) await waitForElement(() => getByText('- Cancel'))
fireEvent.change(getByPlaceholderText('Hello'), { fireEvent.change(getByPlaceholderText('Hello'), {
target: { value: 'https://hello.com' } target: { value: 'https://hello.com' }

View File

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

18
package-lock.json generated
View File

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