mirror of
https://github.com/oceanprotocol/commons.git
synced 2023-03-15 18:03:00 +01:00
Merge pull request #191 from oceanprotocol/feature/ipfs
Add files to IPFS during publish flow
This commit is contained in:
commit
54a4ba5c68
@ -11,10 +11,11 @@
|
||||
"extends": [
|
||||
"oceanprotocol",
|
||||
"oceanprotocol/react",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/react",
|
||||
"prettier/standard",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier/@typescript-eslint",
|
||||
"plugin:cypress/recommended"
|
||||
],
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,4 +26,5 @@ yarn-error.log*
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/fixtures/did.txt
|
||||
cypress/fixtures/did-ipfs.txt
|
||||
artifacts
|
||||
|
20
.travis.yml
20
.travis.yml
@ -26,8 +26,14 @@ env:
|
||||
- REACT_APP_SECRET_STORE_URI="http://localhost:12001"
|
||||
- REACT_APP_FAUCET_URI="http://localhost:3001"
|
||||
- REACT_APP_BRIZO_ADDRESS="0x068ed00cf0441e4829d9784fcbe7b9e26d4bd8d0"
|
||||
|
||||
# IPFS client & server config
|
||||
- REACT_APP_IPFS_GATEWAY_URI="https://ipfs.oceanprotocol.com"
|
||||
- REACT_APP_IPFS_NODE_URI="https://ipfs.oceanprotocol.com:443"
|
||||
- IPFS_GATEWAY_URI="https://ipfs.oceanprotocol.com"
|
||||
|
||||
# start Barge with these versions
|
||||
- BRIZO_VERSION=v0.4.2
|
||||
- BRIZO_VERSION=v0.4.5
|
||||
- AQUARIUS_VERSION=v0.3.8
|
||||
- KEEPER_VERSION=v0.11.1
|
||||
- EVENTS_HANDLER_VERSION=v0.1.2
|
||||
@ -57,16 +63,14 @@ script:
|
||||
- ./scripts/keeper.sh
|
||||
- ./scripts/test.sh || travis_terminate 1
|
||||
- ./scripts/coverage.sh
|
||||
# Pipe the coverage data to Code Climate
|
||||
- ./cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.client.json client/coverage/lcov.info
|
||||
- ./cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.server.json server/coverage/lcov.info
|
||||
- ./cc-test-reporter sum-coverage coverage/codeclimate.*.json -p 2
|
||||
- if [[ "$TRAVIS_TEST_RESULT" == 0 ]]; then ./cc-test-reporter upload-coverage; fi
|
||||
- npm run test:e2e || travis_terminate 1
|
||||
- ./scripts/build.sh
|
||||
|
||||
# Pipe the coverage data to Code Climate
|
||||
after_script:
|
||||
- ./cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.client.json client/coverage/lcov.info # Format client coverage
|
||||
- ./cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.server.json server/coverage/lcov.info # Format server coverage
|
||||
- ./cc-test-reporter sum-coverage coverage/codeclimate.*.json -p 2 # Sum both coverage parts into coverage/codeclimate.json
|
||||
- if [[ "$TRAVIS_TEST_RESULT" == 0 ]]; then ./cc-test-reporter upload-coverage; fi # Upload coverage/codeclimate.json
|
||||
|
||||
notifications:
|
||||
email: false
|
||||
|
||||
|
@ -31,6 +31,7 @@ If you're a developer and want to contribute to, or want to utilize this marketp
|
||||
- [Client](#client)
|
||||
- [Server](#server)
|
||||
- [Feature Switches](#feature-switches)
|
||||
- [More Settings](#more-settings)
|
||||
- [👩🔬 Testing](#-testing)
|
||||
- [Unit Tests](#unit-tests)
|
||||
- [End-to-End Integration Tests](#end-to-end-integration-tests)
|
||||
@ -144,6 +145,14 @@ Beside configuring the network endpopints, the client allows to activate some fe
|
||||
| `REACT_APP_SHOW_REQUEST_TOKENS_BUTTON` | Shows a second button on the `/faucet` route to request Ocean Tokens in addition to Ether. Will only work in Ocean testnets. |
|
||||
| `REACT_APP_ALLOW_PRICING` | Activate pricing feature. Will show a price input during publish flow, and output prices for each data asset. |
|
||||
|
||||
#### More Settings
|
||||
|
||||
| Env Variable | Example | Feature Description |
|
||||
| --------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------- |
|
||||
| client: `REACT_APP_IPFS_GATEWAY_URI`<br /> server: `IPFS_GATEWAY_URI` | `"https://ipfs.oceanprotocol.com"` | The IPFS gateway URI. |
|
||||
| `REACT_APP_IPFS_NODE_URI` | `"https://ipfs.oceanprotocol.com:443"` | The IPFS node URI used to add files to IPFS. |
|
||||
| `REACT_APP_REPORT_EMAIL` | `"jelly@mcjellyfish.com"` | The email used for the _report an asset_ feature. |
|
||||
|
||||
## 👩🔬 Testing
|
||||
|
||||
Test suite is setup with [Jest](https://jestjs.io) and [react-testing-library](https://github.com/kentcdodds/react-testing-library) for unit testing, and [Cypress](https://www.cypress.io) for integration testing.
|
||||
|
@ -59,3 +59,5 @@ REACT_APP_REPORT_EMAIL="test@example.com"
|
||||
# REACT_APP_SHOW_CHANNELS=true
|
||||
# REACT_APP_ALLOW_PRICING=true
|
||||
# REACT_APP_SHOW_REQUEST_TOKENS_BUTTON=true
|
||||
REACT_APP_IPFS_GATEWAY_URI="https://ipfs.oceanprotocol.com"
|
||||
REACT_APP_IPFS_NODE_URI="https://ipfs.oceanprotocol.com:443"
|
||||
|
8102
client/package-lock.json
generated
8102
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@oceanprotocol/art": "^2.2.0",
|
||||
"@oceanprotocol/squid": "^0.7.2",
|
||||
"@oceanprotocol/squid": "^0.7.3",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"@sindresorhus/slugify": "^0.9.1",
|
||||
"axios": "^0.19.0",
|
||||
@ -23,7 +23,9 @@
|
||||
"ethereum-blockies": "github:MyEtherWallet/blockies",
|
||||
"filesize": "^4.1.2",
|
||||
"history": "^4.9.0",
|
||||
"is-url": "^1.2.4",
|
||||
"ipfs": "^0.38.0",
|
||||
"ipfs-http-client": "^38.2.0",
|
||||
"is-url-superb": "^3.0.0",
|
||||
"moment": "^2.24.0",
|
||||
"query-string": "^6.8.2",
|
||||
"react": "16.8.6",
|
||||
@ -31,6 +33,7 @@
|
||||
"react-datepicker": "^2.8.0",
|
||||
"react-dom": "16.8.6",
|
||||
"react-dotdotdot": "^1.3.1",
|
||||
"react-dropzone": "^10.1.8",
|
||||
"react-ga": "^2.6.0",
|
||||
"react-helmet": "^5.2.1",
|
||||
"react-markdown": "^4.1.0",
|
||||
|
1
client/src/@types/ipfs-http-client/index.d.ts
vendored
Normal file
1
client/src/@types/ipfs-http-client/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'ipfs-http-client'
|
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'
|
@ -1,4 +1,4 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import cx from 'classnames'
|
||||
import styles from './Button.module.scss'
|
||||
@ -15,47 +15,34 @@ interface ButtonProps {
|
||||
name?: string
|
||||
}
|
||||
|
||||
export default class Button extends PureComponent<ButtonProps, any> {
|
||||
public render() {
|
||||
let classes
|
||||
const {
|
||||
primary,
|
||||
link,
|
||||
href,
|
||||
children,
|
||||
className,
|
||||
to,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
if (primary) {
|
||||
classes = styles.buttonPrimary
|
||||
} else if (link) {
|
||||
classes = styles.link
|
||||
} else {
|
||||
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 (
|
||||
<button className={cx(classes, className)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
function getClasses(primary: boolean | undefined, link: boolean | undefined) {
|
||||
return primary ? styles.buttonPrimary : link ? styles.link : styles.button
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
primary,
|
||||
link,
|
||||
href,
|
||||
children,
|
||||
className,
|
||||
to,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
const classes = getClasses(primary, link)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import Input from './Input'
|
||||
|
||||
describe('Input', () => {
|
||||
@ -7,10 +7,10 @@ describe('Input', () => {
|
||||
const { container } = render(<Input name="my-input" label="My Input" />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
expect(container.querySelector('.label')).toHaveTextContent('My Input')
|
||||
expect(container.querySelector('.input')).toHaveAttribute(
|
||||
'id',
|
||||
'my-input'
|
||||
)
|
||||
|
||||
const input = container.querySelector('.input')
|
||||
expect(input).toHaveAttribute('id', 'my-input')
|
||||
input && fireEvent.focus(input)
|
||||
})
|
||||
|
||||
it('renders as text input by default', () => {
|
||||
@ -25,13 +25,13 @@ describe('Input', () => {
|
||||
const { container } = render(
|
||||
<Input name="my-input" label="My Input" type="search" />
|
||||
)
|
||||
expect(container.querySelector('.input')).toHaveAttribute(
|
||||
'type',
|
||||
'search'
|
||||
)
|
||||
const input = container.querySelector('.input')
|
||||
expect(input).toHaveAttribute('type', 'search')
|
||||
expect(container.querySelector('label + div')).toHaveClass(
|
||||
'inputWrapSearch'
|
||||
)
|
||||
|
||||
input && fireEvent.focus(input)
|
||||
})
|
||||
|
||||
it('renders select', () => {
|
||||
|
@ -30,6 +30,7 @@ interface InputProps {
|
||||
rows?: number
|
||||
group?: any
|
||||
multiple?: boolean
|
||||
pattern?: string
|
||||
}
|
||||
|
||||
interface InputState {
|
||||
|
15
client/src/components/atoms/Spinner.test.tsx
Normal file
15
client/src/components/atoms/Spinner.test.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import Spinner from './Spinner'
|
||||
|
||||
describe('Spinner', () => {
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(<Spinner />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders small variant', () => {
|
||||
const { container } = render(<Spinner small />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -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%;
|
||||
|
45
client/src/components/molecules/Dropzone.module.scss
Normal file
45
client/src/components/molecules/Dropzone.module.scss
Normal file
@ -0,0 +1,45 @@
|
||||
@import '../../styles/variables';
|
||||
|
||||
.dropzone {
|
||||
margin-top: $spacer;
|
||||
margin-bottom: $spacer;
|
||||
border: .2rem dashed $brand-grey-lighter;
|
||||
border-radius: $border-radius * 2;
|
||||
padding: $spacer;
|
||||
background: $brand-white;
|
||||
transition: .2s ease-out;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
font-size: $font-size-small;
|
||||
color: $brand-grey-light;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
border-color: $brand-grey-light;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dragover {
|
||||
composes: dropzone;
|
||||
border-color: $brand-pink;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
composes: dropzone;
|
||||
opacity: .5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropzoneFiles {
|
||||
padding: $spacer 0;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
52
client/src/components/molecules/Dropzone.test.tsx
Normal file
52
client/src/components/molecules/Dropzone.test.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render } from '@testing-library/react'
|
||||
import Dropzone from './Dropzone'
|
||||
|
||||
function mockData(files: any) {
|
||||
return {
|
||||
dataTransfer: {
|
||||
files,
|
||||
items: files.map((file: any) => ({
|
||||
kind: 'file',
|
||||
type: file.type,
|
||||
getAsFile: () => file
|
||||
})),
|
||||
types: ['Files']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flushPromises(ui: any, container: any) {
|
||||
return new Promise(resolve =>
|
||||
setImmediate(() => {
|
||||
render(ui, { container })
|
||||
resolve(container)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function dispatchEvt(node: any, type: string, data: any) {
|
||||
const event = new Event(type, { bubbles: true })
|
||||
Object.assign(event, data)
|
||||
fireEvent(node, event)
|
||||
}
|
||||
|
||||
test('invoke onDragEnter when dragenter event occurs', async () => {
|
||||
const file = new File([JSON.stringify({ ping: true })], 'ping.json', {
|
||||
type: 'application/json'
|
||||
})
|
||||
const data = mockData([file])
|
||||
const handleOnDrop = jest.fn()
|
||||
|
||||
const ui = <Dropzone handleOnDrop={handleOnDrop} />
|
||||
const { container } = render(ui)
|
||||
|
||||
// drop a file
|
||||
const dropzone = container.querySelector('div')
|
||||
dispatchEvt(dropzone, 'dragenter', data)
|
||||
dispatchEvt(dropzone, 'dragover', data)
|
||||
dispatchEvt(dropzone, 'drop', data)
|
||||
await flushPromises(ui, container)
|
||||
|
||||
expect(handleOnDrop).toHaveBeenCalled()
|
||||
})
|
62
client/src/components/molecules/Dropzone.tsx
Normal file
62
client/src/components/molecules/Dropzone.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import styles from './Dropzone.module.scss'
|
||||
import { formatBytes } from '../../utils/utils'
|
||||
|
||||
export default function Dropzone({
|
||||
handleOnDrop,
|
||||
disabled,
|
||||
multiple
|
||||
}: {
|
||||
handleOnDrop(files: File[]): void
|
||||
disabled?: boolean
|
||||
multiple?: boolean
|
||||
}) {
|
||||
const onDrop = useCallback(acceptedFiles => handleOnDrop(acceptedFiles), [
|
||||
handleOnDrop
|
||||
])
|
||||
|
||||
const {
|
||||
acceptedFiles,
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
isDragReject
|
||||
} = useDropzone({ onDrop })
|
||||
|
||||
const files = acceptedFiles.map((file: any) => (
|
||||
<li key={file.path}>
|
||||
{file.path} - {formatBytes(file.size, 0)}
|
||||
</li>
|
||||
))
|
||||
|
||||
return (
|
||||
<>
|
||||
{acceptedFiles.length > 0 ? (
|
||||
<aside className={styles.dropzoneFiles}>
|
||||
<ul>{files}</ul>
|
||||
</aside>
|
||||
) : (
|
||||
<div
|
||||
{...getRootProps({
|
||||
className: isDragActive
|
||||
? styles.dragover
|
||||
: disabled
|
||||
? styles.disabled
|
||||
: styles.dropzone
|
||||
})}
|
||||
>
|
||||
<input {...getInputProps({ multiple })} />
|
||||
<p>
|
||||
{isDragActive && !isDragReject
|
||||
? `Drop it like it's hot!`
|
||||
: multiple
|
||||
? `Drag 'n' drop some files here, or click to select files`
|
||||
: `Drag 'n' drop a file here, or click to select a file`}
|
||||
{}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -7,7 +7,7 @@ import { userMockConnected } from '../../../__mocks__/user-mock'
|
||||
import { marketMock } from '../../../__mocks__/market-mock'
|
||||
|
||||
describe('WalletSelector', () => {
|
||||
it('renders without crashing', async () => {
|
||||
it('renders without crashing', () => {
|
||||
ReactModal.setAppElement(document.createElement('div'))
|
||||
|
||||
const { container, getByText } = render(
|
||||
@ -20,11 +20,11 @@ describe('WalletSelector', () => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
fireEvent.click(getByText('Select wallet'))
|
||||
|
||||
const burnerButton = await getByText('Burner Wallet')
|
||||
const burnerButton = getByText('Burner Wallet')
|
||||
fireEvent.click(burnerButton)
|
||||
|
||||
fireEvent.click(getByText('Select wallet'))
|
||||
// const metamaskButton = await getByText('MetaMask')
|
||||
// const metamaskButton = getByText('MetaMask')
|
||||
// fireEvent.click(metamaskButton)
|
||||
})
|
||||
})
|
||||
|
@ -34,7 +34,30 @@ describe('Report', () => {
|
||||
expect(comment).toHaveTextContent('Plants')
|
||||
fireEvent.click(getByTestId('report'))
|
||||
mockAxios.mockResponse(mockResponse)
|
||||
// expect(mockAxios.post).toHaveBeenCalled()
|
||||
expect(mockAxios).toHaveBeenCalled()
|
||||
|
||||
// close modal
|
||||
fireEvent.click(getByTestId('closeModal'))
|
||||
})
|
||||
|
||||
it('catches response error', async () => {
|
||||
ReactModal.setAppElement(document.createElement('div'))
|
||||
|
||||
const { getByText, getByLabelText, getByTestId } = render(
|
||||
<Report did="did:xxx" title="Hello" />
|
||||
)
|
||||
// open modal
|
||||
fireEvent.click(getByText('Report Data Set'))
|
||||
await wait(() => expect(getByText('did:xxx')).toBeInTheDocument())
|
||||
|
||||
// add comment
|
||||
const comment = getByLabelText('Comment')
|
||||
fireEvent.change(comment, {
|
||||
target: { value: 'Plants' }
|
||||
})
|
||||
expect(comment).toHaveTextContent('Plants')
|
||||
fireEvent.click(getByTestId('report'))
|
||||
mockAxios.mockError({ message: 'Error catch' })
|
||||
|
||||
// close modal
|
||||
fireEvent.click(getByTestId('closeModal'))
|
||||
|
@ -35,3 +35,8 @@ export const allowPricing =
|
||||
process.env.REACT_APP_ALLOW_PRICING === 'true' || false
|
||||
export const showRequestTokens =
|
||||
process.env.REACT_APP_SHOW_REQUEST_TOKENS_BUTTON === 'true' || false
|
||||
// https://ipfs.github.io/public-gateway-checker/
|
||||
export const ipfsGatewayUri =
|
||||
process.env.REACT_APP_IPFS_GATEWAY_URI || 'https://gateway.ipfs.io'
|
||||
export const ipfsNodeUri =
|
||||
process.env.REACT_APP_IPFS_NODE_URI || 'https://ipfs.infura.io:5001'
|
||||
|
54
client/src/hooks/use-ipfs-api.tsx
Normal file
54
client/src/hooks/use-ipfs-api.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import ipfsClient from 'ipfs-http-client'
|
||||
|
||||
let ipfs: any = null
|
||||
let ipfsMessage = ''
|
||||
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('')
|
||||
|
||||
useEffect(() => {
|
||||
async function initIpfs() {
|
||||
if (ipfs !== null) return
|
||||
|
||||
ipfsMessage = 'Checking IPFS gateway...'
|
||||
|
||||
try {
|
||||
ipfs = await ipfsClient(config)
|
||||
const version = await ipfs.version()
|
||||
ipfsVersion = version.version
|
||||
ipfsMessage = `Connected to ${config.host}`
|
||||
} catch (error) {
|
||||
setIpfsError(`IPFS connection error: ${error.message}`)
|
||||
}
|
||||
setIpfsReady(Boolean(await ipfs.id()))
|
||||
}
|
||||
|
||||
initIpfs()
|
||||
}, [config])
|
||||
|
||||
useEffect(() => {
|
||||
// just like componentWillUnmount()
|
||||
return function cleanup() {
|
||||
if (ipfs) {
|
||||
setIpfsReady(false)
|
||||
ipfs = null
|
||||
ipfsMessage = ''
|
||||
ipfsVersion = ''
|
||||
setIpfsError('')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { ipfs, ipfsVersion, isIpfsReady, ipfsError, ipfsMessage }
|
||||
}
|
22
client/src/hooks/use-ipfs.test.tsx
Normal file
22
client/src/hooks/use-ipfs.test.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import { render, wait } from '@testing-library/react'
|
||||
import useIpfs from './use-ipfs'
|
||||
|
||||
export default function TestComponent() {
|
||||
const { ipfsVersion, isIpfsReady, ipfsError, ipfsMessage } = useIpfs()
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isIpfsReady && <span>Ready</span>}
|
||||
{ipfsVersion} - {ipfsMessage} - {ipfsError}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
describe('use-ipfs', () => {
|
||||
it('renders without crashing', async () => {
|
||||
const { container, getByText } = render(<TestComponent />)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
await wait(() => getByText('Ready'))
|
||||
})
|
||||
})
|
74
client/src/hooks/use-ipfs.tsx
Normal file
74
client/src/hooks/use-ipfs.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import Ipfs from 'ipfs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import os from 'os'
|
||||
import shortid from 'shortid'
|
||||
|
||||
let ipfs: any = null
|
||||
let ipfsMessage = ''
|
||||
let ipfsVersion = ''
|
||||
|
||||
export default function useIpfs() {
|
||||
const [isIpfsReady, setIpfsReady] = useState(Boolean(ipfs))
|
||||
const [ipfsError, setIpfsError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
async function startIpfs() {
|
||||
ipfsMessage = 'Starting IPFS...'
|
||||
|
||||
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({
|
||||
repo: `${os.homedir()}/.jsipfs-${shortid.generate()}`,
|
||||
config: {
|
||||
Addresses: {
|
||||
// 0 for port so system just assigns a new free port
|
||||
// to allow multiple nodes running at same time
|
||||
Swarm: ['/ip4/0.0.0.0/tcp/0']
|
||||
}
|
||||
}
|
||||
})
|
||||
console.timeEnd(message)
|
||||
ipfsMessage = message
|
||||
|
||||
const { agentVersion } = await ipfs.id()
|
||||
ipfsVersion = agentVersion
|
||||
} catch (error) {
|
||||
const message = `IPFS init error: ${error.message}`
|
||||
ipfsMessage = message
|
||||
console.error(message)
|
||||
ipfs = null
|
||||
setIpfsError(error.message)
|
||||
}
|
||||
}
|
||||
setIpfsReady(Boolean(ipfs))
|
||||
}
|
||||
|
||||
startIpfs()
|
||||
|
||||
// just like componentWillUnmount()
|
||||
return function cleanup() {
|
||||
if (ipfs && ipfs.stop) {
|
||||
console.time('IPFS stopped')
|
||||
ipfs.stop()
|
||||
setIpfsReady(false)
|
||||
ipfs = null
|
||||
ipfsMessage = ''
|
||||
ipfsVersion = ''
|
||||
setIpfsError('')
|
||||
console.timeEnd('IPFS stopped')
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { ipfs, ipfsVersion, isIpfsReady, ipfsError, ipfsMessage }
|
||||
}
|
25
client/src/routes/Publish/Files/Ipfs/Form.module.scss
Normal file
25
client/src/routes/Publish/Files/Ipfs/Form.module.scss
Normal file
@ -0,0 +1,25 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
.ipfsForm {
|
||||
margin-top: $spacer / 2;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacer / 2;
|
||||
background: $body-background;
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border: .1rem solid $brand-grey-lighter;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacer / 2 $spacer / 2;
|
||||
margin-top: $spacer / 2;
|
||||
background: $brand-white;
|
||||
transition: border .2s ease-out;
|
||||
|
||||
&:hover {
|
||||
border-color: $brand-grey-light;
|
||||
}
|
||||
}
|
||||
}
|
32
client/src/routes/Publish/Files/Ipfs/Form.tsx
Normal file
32
client/src/routes/Publish/Files/Ipfs/Form.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import Label from '../../../../components/atoms/Form/Label'
|
||||
import Status from './Status'
|
||||
import styles from './Form.module.scss'
|
||||
|
||||
export default function Form({
|
||||
children,
|
||||
ipfsMessage,
|
||||
ipfsError,
|
||||
isIpfsReady,
|
||||
error
|
||||
}: {
|
||||
children: any
|
||||
ipfsMessage: string
|
||||
ipfsError?: string
|
||||
isIpfsReady: boolean
|
||||
error?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={styles.ipfsForm}>
|
||||
<Label htmlFor="fileUpload" required>
|
||||
Add File To IPFS
|
||||
</Label>
|
||||
{children}
|
||||
<Status
|
||||
message={ipfsMessage}
|
||||
isIpfsReady={isIpfsReady}
|
||||
error={ipfsError || error}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
36
client/src/routes/Publish/Files/Ipfs/Status.module.scss
Normal file
36
client/src/routes/Publish/Files/Ipfs/Status.module.scss
Normal file
@ -0,0 +1,36 @@
|
||||
@import '../../../../styles/variables';
|
||||
|
||||
.message {
|
||||
font-size: $font-size-small;
|
||||
margin-top: $spacer / 2;
|
||||
color: $brand-grey-light;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: .5rem;
|
||||
height: .5rem;
|
||||
display: inline-block;
|
||||
background: $yellow;
|
||||
border-radius: 50%;
|
||||
margin-right: $spacer / 6;
|
||||
margin-bottom: .1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
composes: message;
|
||||
|
||||
&:before {
|
||||
background: $green;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
composes: message;
|
||||
color: $red;
|
||||
|
||||
&:before {
|
||||
border-radius: 0;
|
||||
background: $red;
|
||||
}
|
||||
}
|
19
client/src/routes/Publish/Files/Ipfs/Status.tsx
Normal file
19
client/src/routes/Publish/Files/Ipfs/Status.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import styles from './Status.module.scss'
|
||||
|
||||
export default function Status({
|
||||
message,
|
||||
error,
|
||||
isIpfsReady
|
||||
}: {
|
||||
message: string
|
||||
error?: string
|
||||
isIpfsReady: boolean
|
||||
}) {
|
||||
const classes = isIpfsReady
|
||||
? styles.success
|
||||
: error
|
||||
? styles.error
|
||||
: styles.message
|
||||
return <div className={classes}>{error || message}</div>
|
||||
}
|
44
client/src/routes/Publish/Files/Ipfs/index.test.tsx
Normal file
44
client/src/routes/Publish/Files/Ipfs/index.test.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent, waitForElement, act } from '@testing-library/react'
|
||||
import Ipfs from '.'
|
||||
|
||||
const addFile = jest.fn()
|
||||
|
||||
describe('IPFS', () => {
|
||||
const ui = <Ipfs addFile={addFile} />
|
||||
const file = new File(['(⌐□_□)'], 'chucknorris.png', {
|
||||
type: 'image/png'
|
||||
})
|
||||
|
||||
it('HTTP API: files can be dropped', async () => {
|
||||
const { container, findByText, getByText } = render(ui)
|
||||
expect(container).toBeInTheDocument()
|
||||
|
||||
// wait for IPFS node
|
||||
await findByText(/Connected to /)
|
||||
|
||||
// drop a file
|
||||
const dropzoneInput = container.querySelector('.dropzone')
|
||||
Object.defineProperty(dropzoneInput, 'files', { value: [file] })
|
||||
act(() => {
|
||||
dropzoneInput && fireEvent.drop(dropzoneInput)
|
||||
})
|
||||
const addingText = await waitForElement(() => getByText(/Adding /))
|
||||
expect(addingText).toBeDefined()
|
||||
})
|
||||
|
||||
// it('Local Node: files can be dropped', async () => {
|
||||
// const { debug, container, findByText, getByText } = render(
|
||||
// <Ipfs addFile={addFile} node />
|
||||
// )
|
||||
// expect(container).toBeInTheDocument()
|
||||
// // wait for IPFS node
|
||||
// await findByText(/IPFS started/)
|
||||
// // drop a file
|
||||
// const dropzoneInput = container.querySelector('.dropzone input')
|
||||
// Object.defineProperty(dropzoneInput, 'files', { value: [file] })
|
||||
// dropzoneInput && fireEvent.drop(dropzoneInput)
|
||||
// await waitForElement(() => getByText(/File found/), { timeout: 100000 })
|
||||
// expect(addFile).toHaveBeenCalledTimes(1)
|
||||
// })
|
||||
})
|
102
client/src/routes/Publish/Files/Ipfs/index.tsx
Normal file
102
client/src/routes/Publish/Files/Ipfs/index.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
/* eslint-disable no-console */
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import useIpfsApi, { IpfsConfig } from '../../../../hooks/use-ipfs-api'
|
||||
import Spinner from '../../../../components/atoms/Spinner'
|
||||
import Dropzone from '../../../../components/molecules/Dropzone'
|
||||
import { formatBytes, pingUrl, readFileAsync } from '../../../../utils/utils'
|
||||
import { ipfsGatewayUri, ipfsNodeUri } from '../../../../config'
|
||||
import Form from './Form'
|
||||
|
||||
export default function Ipfs({ addFile }: { addFile(url: string): void }) {
|
||||
const { hostname, port, protocol } = new URL(ipfsNodeUri)
|
||||
|
||||
const ipfsConfig: IpfsConfig = {
|
||||
protocol: protocol.replace(':', ''),
|
||||
host: hostname,
|
||||
port: port || '443'
|
||||
}
|
||||
|
||||
const { ipfs, isIpfsReady, ipfsError, ipfsMessage } = useIpfsApi(ipfsConfig)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState('')
|
||||
const [fileSize, setFileSize] = useState('')
|
||||
const [fileSizeReceived, setFileSizeReceived] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setMessage(
|
||||
`Adding to IPFS<br />
|
||||
<small>${fileSizeReceived || 0}/${fileSize}</small><br />`
|
||||
)
|
||||
}, [fileSize, fileSizeReceived])
|
||||
|
||||
async function addToIpfs(data: any) {
|
||||
try {
|
||||
const response = await ipfs.add(data, {
|
||||
wrapWithDirectory: true,
|
||||
progress: (length: number) =>
|
||||
setFileSizeReceived(formatBytes(length, 0))
|
||||
})
|
||||
|
||||
// CID of wrapping directory is returned last
|
||||
const cid = response[response.length - 1].hash
|
||||
console.log(`File added: ${cid}`)
|
||||
return cid
|
||||
} catch (error) {
|
||||
setError(`Adding to IPFS failed: ${error.message}`)
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOnDrop(acceptedFiles: any) {
|
||||
if (!acceptedFiles[0]) return
|
||||
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
const { path, size } = acceptedFiles[0]
|
||||
const totalSize = formatBytes(size, 0)
|
||||
setFileSize(totalSize)
|
||||
|
||||
// Add file to IPFS node
|
||||
const content: any = await readFileAsync(acceptedFiles[0])
|
||||
const data = Buffer.from(content)
|
||||
const fileDetails = {
|
||||
path,
|
||||
content: data
|
||||
}
|
||||
|
||||
const cid = await addToIpfs(fileDetails)
|
||||
if (!cid) return
|
||||
|
||||
// Ping gateway url to make it globally available,
|
||||
// but store native url in DDO.
|
||||
const urlGateway = `${ipfsGatewayUri}/ipfs/${cid}/${path}`
|
||||
const url = `ipfs://${cid}/${path}`
|
||||
|
||||
setMessage('Checking IPFS gateway URL')
|
||||
|
||||
const isAvailable = await pingUrl(urlGateway)
|
||||
// add IPFS url to file.url
|
||||
isAvailable && addFile(url)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form
|
||||
error={error}
|
||||
ipfsMessage={ipfsMessage}
|
||||
ipfsError={ipfsError}
|
||||
isIpfsReady={isIpfsReady}
|
||||
>
|
||||
{loading ? (
|
||||
<Spinner message={message} />
|
||||
) : (
|
||||
<Dropzone
|
||||
multiple={false}
|
||||
handleOnDrop={handleOnDrop}
|
||||
disabled={!isIpfsReady}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
@ -4,8 +4,9 @@
|
||||
font-size: $font-size-small;
|
||||
display: block;
|
||||
margin-bottom: $spacer / 8;
|
||||
|
||||
// TODO: truncate long urls with ellipsis
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.remove {
|
||||
|
@ -5,7 +5,7 @@ import Dotdotdot from 'react-dotdotdot'
|
||||
|
||||
const Item = ({
|
||||
item,
|
||||
removeItem
|
||||
removeFile
|
||||
}: {
|
||||
item: {
|
||||
url: string
|
||||
@ -13,7 +13,7 @@ const Item = ({
|
||||
contentType: string
|
||||
contentLength: number
|
||||
}
|
||||
removeItem(): void
|
||||
removeFile(): void
|
||||
}) => (
|
||||
<li>
|
||||
<a href={item.url} className={styles.linkUrl} title={item.url}>
|
||||
@ -36,7 +36,7 @@ const Item = ({
|
||||
type="button"
|
||||
className={styles.remove}
|
||||
title="Remove item"
|
||||
onClick={removeItem}
|
||||
onClick={removeFile}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
.itemForm {
|
||||
margin-top: $spacer / 2;
|
||||
padding-left: $spacer / 2;
|
||||
border: 1px solid $brand-grey-lighter;
|
||||
border-radius: $border-radius;
|
||||
padding: $spacer / 2;
|
||||
background: $body-background;
|
||||
|
||||
button {
|
||||
margin-top: -($spacer * 2);
|
||||
|
@ -2,10 +2,10 @@ import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import ItemForm from './ItemForm'
|
||||
|
||||
const addItem = jest.fn()
|
||||
const addFile = jest.fn()
|
||||
|
||||
const setup = () => {
|
||||
const utils = render(<ItemForm placeholder="Hello" addItem={addItem} />)
|
||||
const utils = render(<ItemForm placeholder="Hello" addFile={addFile} />)
|
||||
const input = utils.getByPlaceholderText('Hello')
|
||||
const button = utils.getByText('Add File')
|
||||
const { container } = utils
|
||||
@ -23,17 +23,17 @@ describe('ItemForm', () => {
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fires addItem', async () => {
|
||||
it('fires addFile', async () => {
|
||||
const { input, button } = setup()
|
||||
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'https://hello.com' }
|
||||
})
|
||||
fireEvent.click(button)
|
||||
expect(addItem).toHaveBeenCalled()
|
||||
expect(addFile).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not fire addItem when no url present', () => {
|
||||
it('does not fire addFile when no url present', () => {
|
||||
const { input, button, container } = setup()
|
||||
|
||||
// empty url
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import isUrl from 'is-url'
|
||||
import isUrl from 'is-url-superb'
|
||||
import Input from '../../../components/atoms/Form/Input'
|
||||
import Button from '../../../components/atoms/Button'
|
||||
import styles from './ItemForm.module.scss'
|
||||
|
||||
interface ItemFormProps {
|
||||
addItem(url: string): void
|
||||
addFile(url: string): void
|
||||
placeholder: string
|
||||
}
|
||||
|
||||
@ -37,12 +37,12 @@ export default class ItemForm extends PureComponent<
|
||||
return
|
||||
}
|
||||
|
||||
if (url && !isUrl(url)) {
|
||||
if (url && !url.includes('ipfs://') && !isUrl(url)) {
|
||||
this.setState({ noUrl: true })
|
||||
return
|
||||
}
|
||||
|
||||
this.props.addItem(url)
|
||||
this.props.addFile(url)
|
||||
}
|
||||
|
||||
private onChangeUrl = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
@ -68,6 +68,7 @@ export default class ItemForm extends PureComponent<
|
||||
placeholder={this.props.placeholder}
|
||||
value={url}
|
||||
onChange={this.onChangeUrl}
|
||||
help="Supported protocols are http(s):// and ipfs://"
|
||||
/>
|
||||
|
||||
<Button onClick={(e: Event) => this.handleSubmit(e)}>
|
||||
|
@ -2,6 +2,10 @@
|
||||
|
||||
.newItems {
|
||||
margin-top: $spacer / 2;
|
||||
|
||||
> button {
|
||||
margin-right: $spacer;
|
||||
}
|
||||
}
|
||||
|
||||
.itemsList {
|
||||
|
@ -56,14 +56,28 @@ describe('Files', () => {
|
||||
const { container, getByText } = renderComponent()
|
||||
|
||||
// open
|
||||
fireEvent.click(getByText('+ Add a file'))
|
||||
fireEvent.click(getByText('+ From URL'))
|
||||
await waitForElement(() => getByText('- Cancel'))
|
||||
expect(container.querySelector('.itemForm')).toBeInTheDocument()
|
||||
|
||||
// close
|
||||
fireEvent.click(getByText('- Cancel'))
|
||||
await waitForElement(() => getByText('+ Add a file'))
|
||||
expect(container.querySelector('.grow-exit')).toBeInTheDocument()
|
||||
await waitForElement(() => getByText('+ From URL'))
|
||||
expect(container.querySelector('.itemForm')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('new IPFS file form can be opened and closed', async () => {
|
||||
const { container, getByText } = renderComponent()
|
||||
|
||||
// open
|
||||
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('.ipfsForm')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('item can be removed', async () => {
|
||||
@ -76,7 +90,7 @@ describe('Files', () => {
|
||||
it('item can be added', async () => {
|
||||
const { getByText, getByPlaceholderText } = renderComponent()
|
||||
|
||||
fireEvent.click(getByText('+ Add a file'))
|
||||
fireEvent.click(getByText('+ From URL'))
|
||||
await waitForElement(() => getByText('- Cancel'))
|
||||
fireEvent.change(getByPlaceholderText('Hello'), {
|
||||
target: { value: 'https://hello.com' }
|
||||
|
@ -1,25 +1,18 @@
|
||||
import React, { FormEvent, PureComponent, ChangeEvent } from 'react'
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||
import axios from 'axios'
|
||||
import { Logger, File } from '@oceanprotocol/squid'
|
||||
import shortid from 'shortid'
|
||||
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
|
||||
contentType: string
|
||||
checksum?: string
|
||||
checksumType?: string
|
||||
contentLength?: number
|
||||
resourceId?: string
|
||||
encoding?: string
|
||||
compression?: string
|
||||
export interface FilePublish extends File {
|
||||
found: boolean // non-standard
|
||||
}
|
||||
|
||||
@ -39,11 +32,26 @@ interface FilesProps {
|
||||
|
||||
interface FilesStates {
|
||||
isFormShown: boolean
|
||||
isIpfsFormShown: boolean
|
||||
}
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
id: 'url',
|
||||
title: '+ From URL',
|
||||
titleActive: '- Cancel'
|
||||
},
|
||||
{
|
||||
id: 'ipfs',
|
||||
title: '+ Add to IPFS',
|
||||
titleActive: '- Cancel'
|
||||
}
|
||||
]
|
||||
|
||||
export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
public state: FilesStates = {
|
||||
isFormShown: false
|
||||
isFormShown: false,
|
||||
isIpfsFormShown: false
|
||||
}
|
||||
|
||||
// for canceling axios requests
|
||||
@ -53,15 +61,19 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
this.signal.cancel()
|
||||
}
|
||||
|
||||
private toggleForm = (e: Event) => {
|
||||
private toggleForm = (e: Event, form: string) => {
|
||||
e.preventDefault()
|
||||
|
||||
this.setState({ isFormShown: !this.state.isFormShown })
|
||||
this.setState({
|
||||
isFormShown: form === 'url' ? !this.state.isFormShown : false,
|
||||
isIpfsFormShown:
|
||||
form === 'ipfs' ? !this.state.isIpfsFormShown : false
|
||||
})
|
||||
}
|
||||
|
||||
private addItem = async (value: string) => {
|
||||
const file: File = {
|
||||
url: value,
|
||||
private async getFile(url: string) {
|
||||
const file: FilePublish = {
|
||||
url,
|
||||
contentType: '',
|
||||
found: false // non-standard
|
||||
}
|
||||
@ -71,20 +83,42 @@ 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)
|
||||
|
||||
if (contentLength) file.contentLength = contentLength
|
||||
if (contentType) {
|
||||
file.contentType = contentType
|
||||
file.compression = cleanupContentType(contentType)
|
||||
}
|
||||
|
||||
file.found = found
|
||||
|
||||
return file
|
||||
} catch (error) {
|
||||
!axios.isCancel(error) && Logger.error(error.message)
|
||||
}
|
||||
}
|
||||
|
||||
private addFile = async (url: string) => {
|
||||
// check for duplicate urls
|
||||
const duplicateFiles = this.props.files.filter(props =>
|
||||
url.includes(props.url)
|
||||
)
|
||||
|
||||
if (duplicateFiles.length > 0) {
|
||||
return this.setState({
|
||||
isFormShown: false,
|
||||
isIpfsFormShown: false
|
||||
})
|
||||
}
|
||||
|
||||
const file: FilePublish | undefined = await this.getFile(url)
|
||||
file && this.props.files.push(file)
|
||||
|
||||
this.props.files.push(file)
|
||||
const event = {
|
||||
currentTarget: {
|
||||
name: 'files',
|
||||
@ -92,10 +126,16 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
}
|
||||
}
|
||||
this.props.onChange(event as any)
|
||||
this.setState({ isFormShown: !this.state.isFormShown })
|
||||
|
||||
this.setState({
|
||||
isFormShown: false,
|
||||
isIpfsFormShown: false
|
||||
})
|
||||
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
private removeItem = (index: number) => {
|
||||
private removeFile = (index: number) => {
|
||||
this.props.files.splice(index, 1)
|
||||
const event = {
|
||||
currentTarget: {
|
||||
@ -108,8 +148,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 (
|
||||
<>
|
||||
@ -126,43 +166,43 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
|
||||
|
||||
<div className={styles.newItems}>
|
||||
{files.length > 0 && (
|
||||
<TransitionGroup
|
||||
component="ul"
|
||||
className={styles.itemsList}
|
||||
>
|
||||
<ul className={styles.itemsList}>
|
||||
{files.map((item: any, index: number) => (
|
||||
<CSSTransition
|
||||
key={index}
|
||||
timeout={400}
|
||||
classNames="fade"
|
||||
>
|
||||
<Item
|
||||
item={item}
|
||||
removeItem={() =>
|
||||
this.removeItem(index)
|
||||
}
|
||||
/>
|
||||
</CSSTransition>
|
||||
<Item
|
||||
key={shortid.generate()}
|
||||
item={item}
|
||||
removeFile={() => this.removeFile(index)}
|
||||
/>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<Button link onClick={this.toggleForm}>
|
||||
{isFormShown ? '- Cancel' : '+ Add a file'}
|
||||
</Button>
|
||||
{buttons.map(button => {
|
||||
const isActive =
|
||||
(button.id === 'url' && isFormShown) ||
|
||||
(button.id === 'ipfs' && isIpfsFormShown)
|
||||
|
||||
<CSSTransition
|
||||
classNames="grow"
|
||||
in={isFormShown}
|
||||
timeout={200}
|
||||
unmountOnExit
|
||||
onExit={() => this.setState({ isFormShown: false })}
|
||||
>
|
||||
return (
|
||||
<Button
|
||||
key={shortid.generate()}
|
||||
link
|
||||
onClick={(e: Event) =>
|
||||
this.toggleForm(e, button.id)
|
||||
}
|
||||
>
|
||||
{isActive ? button.titleActive : button.title}
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
|
||||
{isFormShown && (
|
||||
<ItemForm
|
||||
placeholder={placeholder}
|
||||
addItem={this.addItem}
|
||||
addFile={this.addFile}
|
||||
/>
|
||||
</CSSTransition>
|
||||
)}
|
||||
|
||||
{isIpfsFormShown && <Ipfs addFile={this.addFile} />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
@ -32,12 +32,7 @@ describe('Publish', () => {
|
||||
<MemoryRouter>
|
||||
<Publish
|
||||
history={history}
|
||||
location={{
|
||||
pathname: '/publish',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: ''
|
||||
}}
|
||||
location={location}
|
||||
match={{ params: '', path: '', url: '', isExact: true }}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { ChangeEvent, Component, FormEvent } from 'react'
|
||||
import { Logger } from '@oceanprotocol/squid'
|
||||
import { Logger, File } from '@oceanprotocol/squid'
|
||||
import Web3 from 'web3'
|
||||
import Route from '../../components/templates/Route'
|
||||
import Form from '../../components/atoms/Form/Form'
|
||||
@ -11,7 +11,6 @@ import ReactGA from 'react-ga'
|
||||
import { allowPricing } from '../../config'
|
||||
import { steps } from '../../data/form-publish.json'
|
||||
import Content from '../../components/atoms/Content'
|
||||
import { File } from './Files'
|
||||
import withTracker from '../../hoc/withTracker'
|
||||
|
||||
type AssetType = 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other'
|
||||
|
@ -1,2 +1,21 @@
|
||||
/* eslint-disable no-console */
|
||||
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
import '@testing-library/react/cleanup-after-each'
|
||||
|
||||
// this is just a little hack to silence a warning that we'll get until we
|
||||
// upgrade to 16.9: https://github.com/facebook/react/pull/14853
|
||||
const originalError = console.error
|
||||
|
||||
beforeAll(() => {
|
||||
console.error = (...args) => {
|
||||
if (/Warning.*not wrapped in act/.test(args[0])) {
|
||||
return
|
||||
}
|
||||
originalError.call(console, ...args)
|
||||
}
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
console.error = originalError
|
||||
})
|
||||
|
69
client/src/utils/utils.test.ts
Normal file
69
client/src/utils/utils.test.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import mockAxios from 'jest-mock-axios'
|
||||
import { formatBytes, pingUrl, arraySum, readFileAsync } from './utils'
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('outputs as expected', () => {
|
||||
const number = 1024
|
||||
const output = formatBytes(number, 0)
|
||||
expect(output).toBe('1 KB')
|
||||
})
|
||||
|
||||
it('0 conversion', () => {
|
||||
const number = 0
|
||||
const output = formatBytes(number, 0)
|
||||
expect(output).toBe('0 Bytes')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pingUrl', () => {
|
||||
const mockResponse = {
|
||||
status: 200,
|
||||
data: {}
|
||||
}
|
||||
|
||||
const mockResponseFaulty = {
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
data: {}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mockAxios.reset()
|
||||
})
|
||||
|
||||
it('pingUrl can be called', () => {
|
||||
pingUrl('https://oceanprotocol.com')
|
||||
mockAxios.mockResponse(mockResponse)
|
||||
expect(mockAxios).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('pingUrl can be called with non 200 response', () => {
|
||||
pingUrl('https://oceanprotocol.com')
|
||||
mockAxios.mockResponse(mockResponseFaulty)
|
||||
})
|
||||
|
||||
it('pingUrl error catch', () => {
|
||||
pingUrl('https://oceanprotocol.com')
|
||||
mockAxios.mockError({ message: 'Error catch' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('arraySum', () => {
|
||||
it('outputs as expected', () => {
|
||||
const array = [2, 3]
|
||||
const output = arraySum(array)
|
||||
expect(output).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('readFileAsync', () => {
|
||||
it('outputs as expected', async () => {
|
||||
const file = new File(['ABC'], 'filename.txt', {
|
||||
type: 'text/plain',
|
||||
lastModified: Date.now()
|
||||
})
|
||||
|
||||
const output = await readFileAsync(file)
|
||||
expect(output).toBeInstanceOf(ArrayBuffer)
|
||||
})
|
||||
})
|
43
client/src/utils/utils.ts
Normal file
43
client/src/utils/utils.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/* eslint-disable no-console */
|
||||
import axios from 'axios'
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
export function arraySum(array: number[]) {
|
||||
return array.reduce((a, b) => a + b, 0)
|
||||
}
|
||||
|
||||
export async function pingUrl(url: string) {
|
||||
try {
|
||||
const response = await axios(url)
|
||||
if (response.status !== 200) console.error(`Not found: ${url}`)
|
||||
|
||||
console.log(`File found: ${url}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function readFileAsync(file: File) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onerror = () => {
|
||||
reader.abort()
|
||||
reject(new DOMException('Problem parsing input file.'))
|
||||
}
|
||||
reader.onload = () => {
|
||||
resolve(reader.result)
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
@ -15,5 +15,5 @@
|
||||
"noEmit": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["src", "config/config.ts"]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Publish', () => {
|
||||
before(() => {
|
||||
describe('Publish', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/publish')
|
||||
|
||||
cy.get('article>div', { timeout: 60000 }).should(
|
||||
@ -9,12 +9,12 @@ context('Publish', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('Publish flow', () => {
|
||||
it('should publish https:// file', () => {
|
||||
// Fill title
|
||||
cy.get('input#name').type('Commons Integration Test')
|
||||
// Open Add a file form
|
||||
cy.get('button')
|
||||
.contains('+ Add a file')
|
||||
.contains('+ From URL')
|
||||
.click()
|
||||
// Fill url of file
|
||||
cy.get('input#url').type(
|
||||
@ -76,4 +76,72 @@ context('Publish', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should publish ipfs:// file', () => {
|
||||
// Fill title
|
||||
cy.get('input#name').type('Commons Integration IPFS Test')
|
||||
// Open Add a file form
|
||||
cy.get('button')
|
||||
.contains('+ From URL')
|
||||
.click()
|
||||
// Fill url of file
|
||||
cy.get('input#url').type(
|
||||
'ipfs://QmX5LRpEVocfks9FNDnRoK2imf2fy9mPpP4wfgaDVXWfYD/video.mp4'
|
||||
)
|
||||
// Add file to main form
|
||||
cy.get('button')
|
||||
.contains('Add File')
|
||||
.click()
|
||||
// Verify and nove to next step
|
||||
cy.get('button')
|
||||
.contains('Next →')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
// Verify we are on next step
|
||||
cy.get('article>div').should('contain', 'Information')
|
||||
// Fill description
|
||||
cy.get('textarea#description').type('This is test description')
|
||||
// Pick category
|
||||
cy.get('select#categories').select('Biology')
|
||||
// Verify and move to next step
|
||||
cy.get('button')
|
||||
.contains('Next →')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
// Verify we are on next step
|
||||
cy.get('article>div').should('contain', 'Authorship')
|
||||
// Fill author
|
||||
cy.get('input#author').type('Super Author')
|
||||
// Fill copyright holder
|
||||
cy.get('input#copyrightHolder').type('Super Copyright Holder')
|
||||
// Pick author
|
||||
cy.get('select#license').select('Public Domain')
|
||||
// Verify and move to next step
|
||||
cy.get('button')
|
||||
.contains('Next →')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
// Verify we are on next step
|
||||
cy.get('article>div').should('contain', 'Register')
|
||||
// Start publish process
|
||||
cy.get('button')
|
||||
.contains('Register asset')
|
||||
.should('not.be.disabled')
|
||||
.click()
|
||||
// Wait for finish
|
||||
cy.contains('Your asset is published!', {
|
||||
timeout: 12000
|
||||
}).should('be.visible')
|
||||
|
||||
// Store DID
|
||||
cy.get('a')
|
||||
.contains('See published asset')
|
||||
.invoke('attr', 'href')
|
||||
.then(href => {
|
||||
cy.writeFile(
|
||||
'cypress/fixtures/did-ipfs.txt',
|
||||
href.replace('/asset/', '')
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -1,12 +1,12 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Search', () => {
|
||||
describe('Search', () => {
|
||||
before(() => {
|
||||
cy.visit('/')
|
||||
// Wait for end of loading
|
||||
cy.get('button', { timeout: 60000 }).should('have.length', 1)
|
||||
})
|
||||
|
||||
it('Search for assets from homepage', () => {
|
||||
it('should search for assets from homepage', () => {
|
||||
// Fill search phrase
|
||||
cy.get('input#search').type('Title test')
|
||||
// Start search
|
||||
|
@ -1,6 +1,6 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Consume', () => {
|
||||
before(() => {
|
||||
describe('Consume', () => {
|
||||
beforeEach(() => {
|
||||
cy.fixture('did').then(did => {
|
||||
cy.visit(`/asset/${did}`)
|
||||
})
|
||||
@ -15,7 +15,38 @@ context('Consume', () => {
|
||||
cy.get('button[name="Download"]').should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('Consume asset and check if there is no error', () => {
|
||||
it('should consume https:// file', () => {
|
||||
// eslint-disable-next-line
|
||||
cy.wait(10000)
|
||||
// Wait for faucet
|
||||
// Click consume button
|
||||
cy.get('button[name="Download"]').click()
|
||||
// Wait consume process to end
|
||||
cy.get('button[name="Download"]', { timeout: 600000 }).should(
|
||||
'contain',
|
||||
'Get file'
|
||||
)
|
||||
// check if there is no error
|
||||
cy.get('article>div').should(
|
||||
'not.contain',
|
||||
'. Sorry about that, can you try again?'
|
||||
)
|
||||
// eslint-disable-next-line
|
||||
cy.wait(10000)
|
||||
// wait for file to download before closing browser
|
||||
// to prevent alert poping up
|
||||
})
|
||||
|
||||
it('should consume ipfs:// file', () => {
|
||||
cy.fixture('did-ipfs').then(did => {
|
||||
cy.visit(`/asset/${did}`)
|
||||
})
|
||||
|
||||
// Alias button selector & wait for end of loading
|
||||
cy.get('button[name="Download"]', { timeout: 60000 })
|
||||
.first()
|
||||
.should('have.length', 1)
|
||||
|
||||
// eslint-disable-next-line
|
||||
cy.wait(10000)
|
||||
// Wait for faucet
|
||||
|
@ -1,5 +1,5 @@
|
||||
/// <reference types="Cypress" />
|
||||
context('Faucet', () => {
|
||||
describe('Faucet', () => {
|
||||
before(() => {
|
||||
cy.visit('/faucet')
|
||||
// Wait for end of loading
|
||||
@ -21,7 +21,7 @@ context('Faucet', () => {
|
||||
.should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('Execute faucet call', () => {
|
||||
it('should execute faucet call', () => {
|
||||
// Execute call
|
||||
cy.get('@button')
|
||||
.contains('Request ETH')
|
||||
|
64
package-lock.json
generated
64
package-lock.json
generated
@ -513,61 +513,69 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.0.0.tgz",
|
||||
"integrity": "sha512-Mo45nxTTELODdl7CgpZKJISvLb+Fu64OOO2ZFc2x8sYSnUpFrBUW3H+H/ZGYmEkfnL6VkdtOSxgdt+Av79j0sA==",
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz",
|
||||
"integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "2.0.0",
|
||||
"eslint-utils": "^1.4.0",
|
||||
"@typescript-eslint/experimental-utils": "1.13.0",
|
||||
"eslint-utils": "^1.3.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^2.0.1",
|
||||
"tsutils": "^3.14.0"
|
||||
"tsutils": "^3.7.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-XGJG6GNBXIEx/mN4eTRypN/EUmsd0VhVGQ1AG+WTgdvjHl0G8vHhVBHrd/5oI6RRYBRnedNymSYWW1HAdivtmg==",
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
|
||||
"integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.3",
|
||||
"@typescript-eslint/typescript-estree": "2.0.0",
|
||||
"@typescript-eslint/typescript-estree": "1.13.0",
|
||||
"eslint-scope": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.0.0.tgz",
|
||||
"integrity": "sha512-ibyMBMr0383ZKserIsp67+WnNVoM402HKkxqXGlxEZsXtnGGurbnY90pBO3e0nBUM7chEEOcxUhgw9aPq7fEBA==",
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz",
|
||||
"integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/eslint-visitor-keys": "^1.0.0",
|
||||
"@typescript-eslint/experimental-utils": "2.0.0",
|
||||
"@typescript-eslint/typescript-estree": "2.0.0",
|
||||
"@typescript-eslint/experimental-utils": "1.13.0",
|
||||
"@typescript-eslint/typescript-estree": "1.13.0",
|
||||
"eslint-visitor-keys": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.0.0.tgz",
|
||||
"integrity": "sha512-NXbmzA3vWrSgavymlzMWNecgNOuiMMp62MO3kI7awZRLRcsA1QrYWo6q08m++uuAGVbXH/prZi2y1AWuhSu63w==",
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
|
||||
"integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash.unescape": "4.0.1",
|
||||
"semver": "^6.2.0"
|
||||
"semver": "5.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
|
||||
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"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 +2126,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": {
|
||||
|
@ -25,8 +25,8 @@
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@release-it/bumper": "^1.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^2.0.0",
|
||||
"@typescript-eslint/parser": "^2.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||
"@typescript-eslint/parser": "^1.13.0",
|
||||
"auto-changelog": "^1.16.0",
|
||||
"concurrently": "^4.1.2",
|
||||
"cypress": "^3.4.1",
|
||||
|
@ -1 +1,2 @@
|
||||
SENDGRID_API_KEY='xxx'
|
||||
IPFS_GATEWAY_URI='https://ipfs.oceanprotocol.com'
|
||||
|
@ -4,12 +4,12 @@
|
||||
|
||||
This folder contains server component written in TypeScript using [Express](https://expressjs.com). The server provides various microservices.
|
||||
|
||||
- [Get Started](#Get-Started)
|
||||
- [✨ API Documentation](#-API-Documentation)
|
||||
- [Url Checker](#Url-Checker)
|
||||
- [Report](#Report)
|
||||
- [🎁 Contributing](#-Contributing)
|
||||
- [🏛 License](#-License)
|
||||
- [Get Started](#get-started)
|
||||
- [✨ API Documentation](#-api-documentation)
|
||||
- [Url Checker](#url-checker)
|
||||
- [Report](#report)
|
||||
- [🎁 Contributing](#-contributing)
|
||||
- [🏛 License](#-license)
|
||||
|
||||
## Get Started
|
||||
|
||||
@ -36,6 +36,12 @@ Url Checker returns size and additional information about requested file. This s
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "ipfs://QmQfpdcMWnLTXKKW9GPV7NgtEugghgD6HgzSF6gSrp2mL9"
|
||||
}
|
||||
```
|
||||
|
||||
**Response: Success**
|
||||
|
||||
```json
|
||||
|
9
server/src/config.ts
Normal file
9
server/src/config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
const config = {
|
||||
app: { port: 4000 },
|
||||
sendgridApiKey: process.env.SENDGRID_API_KEY,
|
||||
ipfsGatewayUri: process.env.IPFS_GATEWAY_URI || 'https://gateway.ipfs.io'
|
||||
}
|
||||
|
||||
export default config
|
@ -1,7 +0,0 @@
|
||||
const config = {
|
||||
app: {
|
||||
port: 4000
|
||||
}
|
||||
}
|
||||
|
||||
export default config
|
@ -1,8 +1,8 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import SendgridMail from '@sendgrid/mail'
|
||||
import 'dotenv/config'
|
||||
import config from '../config'
|
||||
|
||||
SendgridMail.setApiKey(process.env.SENDGRID_API_KEY)
|
||||
SendgridMail.setApiKey(config.sendgridApiKey)
|
||||
|
||||
export class ReportRouter {
|
||||
public router: Router
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Router, Request, Response } from 'express'
|
||||
import request from 'request'
|
||||
import config from '../config'
|
||||
|
||||
export class UrlCheckRouter {
|
||||
public router: Router
|
||||
@ -12,44 +13,64 @@ export class UrlCheckRouter {
|
||||
}
|
||||
|
||||
public checkUrl(req: Request, res: Response) {
|
||||
if (!req.body.url) {
|
||||
let { url } = req.body
|
||||
|
||||
if (!url) {
|
||||
return res.send({ status: 'error', message: 'missing url' })
|
||||
}
|
||||
|
||||
// map native IPFS URLs to gateway URLs
|
||||
if (url.includes('ipfs://')) {
|
||||
const cid = url.split('ipfs://')[1]
|
||||
url = `${config.ipfsGatewayUri}/ipfs/${cid}`
|
||||
}
|
||||
|
||||
request(
|
||||
{
|
||||
method: 'HEAD',
|
||||
url: req.body.url,
|
||||
url,
|
||||
headers: { Range: 'bytes=0-' }
|
||||
},
|
||||
(error, response) => {
|
||||
if (
|
||||
response &&
|
||||
(response.statusCode.toString().startsWith('2') ||
|
||||
response.statusCode.toString().startsWith('416'))
|
||||
) {
|
||||
const { headers, statusCode } = response
|
||||
const successResponses =
|
||||
statusCode.toString().startsWith('2') ||
|
||||
statusCode.toString().startsWith('416')
|
||||
|
||||
if (response && successResponses) {
|
||||
const result: any = {}
|
||||
result.found = true
|
||||
|
||||
if (response.headers['content-length']) {
|
||||
if (headers['content-length']) {
|
||||
result.contentLength = parseInt(
|
||||
response.headers['content-length']
|
||||
headers['content-length']
|
||||
) // convert to number
|
||||
}
|
||||
|
||||
if (response.headers['content-type']) {
|
||||
const typeAndCharset = response.headers[
|
||||
'content-type'
|
||||
].split(';')
|
||||
// sometimes servers send content-range header,
|
||||
// try to use it if content-length is not present
|
||||
if (
|
||||
headers['content-range'] &&
|
||||
!headers['content-length']
|
||||
) {
|
||||
const size = headers['content-range'].split('/')[1]
|
||||
result.contentLength = parseInt(size) // convert to number
|
||||
}
|
||||
|
||||
result.contentType = typeAndCharset[0] // eslint-disable-line prefer-destructuring
|
||||
if (headers['content-type']) {
|
||||
const typeAndCharset = headers['content-type'].split(
|
||||
';'
|
||||
)
|
||||
|
||||
/* eslint-disable prefer-destructuring */
|
||||
result.contentType = typeAndCharset[0]
|
||||
|
||||
if (typeAndCharset[1]) {
|
||||
/* eslint-disable prefer-destructuring */
|
||||
result.contentCharset = typeAndCharset[1].split(
|
||||
'='
|
||||
)[1]
|
||||
/* eslint-enable prefer-destructuring */
|
||||
}
|
||||
/* eslint-enable prefer-destructuring */
|
||||
}
|
||||
return res.send({ status: 'success', result })
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import UrlCheckRouter from './routes/UrlCheckRouter'
|
||||
import ReportRouter from './routes/ReportRouter'
|
||||
|
||||
// config
|
||||
import config from './config/config'
|
||||
import config from './config'
|
||||
|
||||
// debug
|
||||
const log = debug('server:index')
|
||||
|
@ -13,9 +13,23 @@ describe('GET /', () => {
|
||||
})
|
||||
|
||||
describe('POST /api/v1/urlcheck', () => {
|
||||
it('responds with json', async () => {
|
||||
const response = await request(server).post('/api/v1/urlcheck')
|
||||
it('responds with json on http://', async () => {
|
||||
const response = await request(server)
|
||||
.post('/api/v1/urlcheck')
|
||||
.send({ url: 'https://oceanprotocol.com/tech-whitepaper.pdf' })
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toBeTruthy()
|
||||
})
|
||||
|
||||
it('responds with json on ipfs://', async () => {
|
||||
const response = await request(server)
|
||||
.post('/api/v1/urlcheck')
|
||||
.send({
|
||||
url:
|
||||
'ipfs://QmX5LRpEVocfks9FNDnRoK2imf2fy9mPpP4wfgaDVXWfYD/video.mp4'
|
||||
})
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toBeTruthy()
|
||||
})
|
||||
|
||||
it('responds with error message when url is missing', async () => {
|
||||
@ -26,6 +40,31 @@ describe('POST /api/v1/urlcheck', () => {
|
||||
})
|
||||
|
||||
describe('POST /api/v1/report', () => {
|
||||
const msg = {
|
||||
to: 'test@example.com',
|
||||
from: 'test@example.com',
|
||||
subject: 'My Subject',
|
||||
text: 'Text',
|
||||
html: '<strong>HTML</strong>'
|
||||
}
|
||||
|
||||
it('responds with json', async () => {
|
||||
const response = await request(server)
|
||||
.post('/api/v1/report')
|
||||
.send({ msg })
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body).toBeTruthy()
|
||||
})
|
||||
|
||||
it('responds with error', async () => {
|
||||
const response = await request(server)
|
||||
.post('/api/v1/report')
|
||||
.send({ msg: 'Hello World' })
|
||||
expect(response.text).toBe(
|
||||
"undefined - Cannot create property 'isMultiple' on string 'Hello World'"
|
||||
)
|
||||
})
|
||||
|
||||
it('responds with error message when message is missing', async () => {
|
||||
const response = await request(server).post('/api/v1/report')
|
||||
const text = await JSON.parse(response.text)
|
||||
|
Loading…
Reference in New Issue
Block a user