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:
parent
414dcd455a
commit
1c59d49d5d
8893
client/package-lock.json
generated
8893
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
1
client/src/@types/ipfs/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module 'ipfs'
|
@ -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>
|
||||||
|
@ -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%;
|
||||||
|
54
client/src/hooks/use-ipfs.tsx
Normal file
54
client/src/hooks/use-ipfs.tsx
Normal 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 }
|
||||||
|
}
|
5
client/src/routes/Publish/Files/Ipfs.module.scss
Normal file
5
client/src/routes/Publish/Files/Ipfs.module.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
@import '../../../styles/variables';
|
||||||
|
|
||||||
|
.ipfsForm {
|
||||||
|
margin-top: $spacer;
|
||||||
|
}
|
61
client/src/routes/Publish/Files/Ipfs.tsx
Normal file
61
client/src/routes/Publish/Files/Ipfs.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -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' }
|
||||||
|
@ -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
18
package-lock.json
generated
@ -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": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user