Skip to content

Commit

Permalink
Merge pull request tqtezos#33 from tqtezos/alex-kooper/ipfs
Browse files Browse the repository at this point in the history
Alex kooper/ipfs
  • Loading branch information
alex-kooper authored Aug 31, 2020
2 parents 8fa2dbd + 4e2adc0 commit 67bd392
Show file tree
Hide file tree
Showing 16 changed files with 880 additions and 52 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ yarn-error.log*
.pgdata
.flextesa/
.tzindex
.ipfs

# Runtime data
pids
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ Docker | `19.03.x` | [Link][docker]
- Tezos sandbox: [Flextesa][flextesa]
- Blockhain indexer: [TZ Index][tz-index]
- Database: [PostgreSQL][postgres]
- InterPlanetary File System: [IPFS][ipfs]

[tz-index]: https://github.com/blockwatch-cc/tzindex
[flextesa]: https://gitlab.com/tezos/flextesa
[postgres]: https://www.postgresql.org/
[ipfs]: https://ipfs.io/

## Usage

Expand Down Expand Up @@ -80,6 +82,15 @@ You can now open:
- [http://localhost:9000](http://localhost:9000) to view the application.
- [http://localhost:9000/graphql](http://localhost:9000/graphql) to open the GraphQL playground.

### Using Local IPFS Server

Once your've started the docker swarm services with `bin/start` a local
instance of IPFS server will be automatically configured and started.
No actions needed to use it for file upload.

However, if you wish to monitor the IPFS server or reconfigure it using its Web UI, you can use:
[http://localhost:5001/webui](http://localhost:5001/webui)

## Development

Note the names of the services printed by the start script and check their log
Expand Down
2 changes: 2 additions & 0 deletions bin/start
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ $PROJECT_ROOT_DIR/bin/wait-for-postgres $PROJECT_ROOT_DIR/bin/migrate-db

docker stack deploy -c $API_SERVER_STACK_FILE $STACK_NAME

$PROJECT_ROOT_DIR/bin/start-ipfs

docker stack services $STACK_NAME
27 changes: 27 additions & 0 deletions bin/start-ipfs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env bash

IPFS_DIR=$PROJECT_ROOT_DIR/.ipfs

configure_ipfs() {
docker run --rm -it --name ipfs-node \
-v $IPFS_DIR:/data/ipfs \
-p 8080:8080 -p 4001:4001 -p 5001:5001 \
ipfs/go-ipfs \
config "$@"
}

# Enable CORS for IPFS Gateway to be able to connect to it from Web application.
# It is only done on the first run when creating IPFS docker volume.
if [ ! -f $IPFS_DIR/config ]; then
printf "Configuring IPFS Server...\n"

configure_ipfs --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
configure_ipfs --json API.HTTPHeaders.Access-Control-Allow-Methods '["GET", "PUT", "POST"]'

printf "Done configuring IPFS Server!\n"
else
printf "Found an IPFS configuration in $IPFS_DIR\n"
fi

docker stack deploy -c $DOCKER_STACK_DIR/ipfs.yml $STACK_NAME

10 changes: 10 additions & 0 deletions client/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"semi": true,
"tabWidth": 2,
"trailingComma": "none",
"singleQuote": true,
"bracketSpacing": true,
"arrowParens": "avoid",
"printWidth": 80
}

3 changes: 3 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"antd": "^4.5.2",
"graphql-tag": "^2.11.0",
"ipfs-http-client": "^46.0.1",
"prettier": "^2.1.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1",
Expand Down
21 changes: 21 additions & 0 deletions client/src/@types/ipfs-http-client.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
declare module 'ipfs-http-client' {

export type FileContent = any | Blob | string;

export interface Cid {
toString: () => string;
}

export interface IpfsFile {
path: string;
cid: Cid;
size: number;
}

export interface IpfsClientApi {
add: (data: FileContent) => Promise<IpfsFile>;
}

export default function(any): IpfsClientApi;
}

48 changes: 48 additions & 0 deletions client/src/api/ipfsUploader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import url from 'url';
import IpfsClient from 'ipfs-http-client';

import config from '../config';

/**
* Promisified version of FileReader.readAsArrayBuffer
*/
const readBlobAsArrayBuffer = (blob: Blob): Promise<ArrayBuffer> => (
new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = () => { resolve(reader.result as ArrayBuffer); };
reader.onerror = () => { reject(reader.error); reader.abort(); };

reader.readAsArrayBuffer(blob);
})
)

const ipfsClient = IpfsClient(config.ipfs.apiUrl);

export interface IpfsContent {
// Content identifier, also known as 'hash'
cid: string;

// The size of the content in bytes
size: number;

// URL of the content on the IPFS server it was uploaded to (fast download)
url: string;

// URL of the content on one of the pubic IPFS servers (it may take a long time to download)
publicGatewayUrl: string;
}

const uploadToIpfs = async (blob: Blob): Promise<IpfsContent> => {
const buffer = await readBlobAsArrayBuffer(blob);
const ipfsFile = await ipfsClient.add(buffer);

return {
cid: ipfsFile.cid.toString(),
size: ipfsFile.size,
url: url.resolve(config.ipfs.gatewayUrl, `ipfs/${ipfsFile.cid}`),
publicGatewayUrl: url.resolve(config.ipfs.publicGatewayUrl, `ipfs/${ipfsFile.cid}`)
}
}

export default uploadToIpfs;
99 changes: 58 additions & 41 deletions client/src/components/CreateNonFungiblePage/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,65 @@
import { FC } from 'react';
import { jsx } from '@emotion/core';
import { Form, Input, Button } from 'antd';
import ImageIpfsUpload, { ImageIpfsUploadProps } from './ImageIpfsUpload';
import { IpfsContent } from '../../api/ipfsUploader';

const InputForm: FC = () => (
<Form layout="vertical" css={{width: '30em'}}>
<Form.Item
label="Name"
name="name"
>
<Input placeholder="Tezos Logo Token"/>
</Form.Item>
<Form.Item
label="Description"
name="description"
>
<Input.TextArea
placeholder="Lorem ipsum"
autoSize={{ minRows: 3, maxRows: 6 }}
/>
</Form.Item>
<Form.Item
label="Symbol"
name="symbol"
>
<Input />
</Form.Item>
<Form.Item
label="IPFS Hash"
name="ipfsHash"
>
<Input/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
shape="round"
size="large"
css={{width: '12em'}}
interface InputFormProps extends ImageIpfsUploadProps {
ipfsContent?: IpfsContent;
}

const InputForm: FC<InputFormProps> = ({ ipfsContent, onChange }) => {
const [form] = Form.useForm();
form.setFieldsValue({ ipfsCid: ipfsContent?.cid})

return (
<Form form={form} layout="vertical" css={{width: '30em'}}>
<Form.Item
label="Name"
name="name"
>
<Input placeholder="Tezos Logo Token"/>
</Form.Item>
<Form.Item
label="Description"
name="description"
>
<Input.TextArea
placeholder="Lorem ipsum"
autoSize={{ minRows: 3, maxRows: 6 }}
/>
</Form.Item>
<Form.Item
label="Symbol"
name="symbol"
>
<Input />
</Form.Item>
<Form.Item
label="Image Upload"
name="image"
>
<ImageIpfsUpload onChange={onChange} />
</Form.Item>
<Form.Item
label="IPFS Hash (CID)"
name="ipfsCid"
>
Create
</Button>
</Form.Item>
</Form>
);
<Input readOnly />
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
shape="round"
size="large"
css={{width: '12em'}}
>
Create
</Button>
</Form.Item>
</Form>
);
}

export default InputForm;
68 changes: 68 additions & 0 deletions client/src/components/CreateNonFungiblePage/ImageIpfsUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
import { FC } from 'react';
import { Upload, Button, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { UploadChangeParam } from 'antd/lib/upload';
import { RcCustomRequestOptions, RcFile } from 'antd/lib/upload/interface';
import uploadToIpfs, { IpfsContent } from '../../api/ipfsUploader';


export interface ImageIpfsUploadProps {
onChange: (info: IpfsContent) => void
}

const ImageIpfsUpload: FC<ImageIpfsUploadProps> = ({ onChange }) => {
const onChangeHandler = async (info: UploadChangeParam) => {
if (info.file.status === 'done') {
const hideLoadingMessage = message.loading('Uploading image to IPFS Server...', 0);;

try {
const ipfsContent = await uploadToIpfs(info.file.originFileObj as Blob);
message.success('Succesfully uploaded image to IPFS Server.')
onChange(ipfsContent)
} catch (error) {
message.error(`Problems uploading image to IPFS Server! Please try later.`, 10);
console.error(`Problem uploading to IPFS: ${error.toString()}`)
} finally {
hideLoadingMessage();
}
}
}

const dummyRequest = ({ file, onSuccess }: RcCustomRequestOptions) => {
setTimeout(() => {
onSuccess({}, file);
}, 0);
};

const validateImageType = (file: RcFile) => {
const isImage = file.type.startsWith('image')

if (!isImage) {
message.error(`${file.name} is not an image file`);
}

return isImage;
}

return (
<Upload
customRequest={dummyRequest}
showUploadList={false}
beforeUpload={validateImageType}
onChange={onChangeHandler}
>
<Button
type="primary"
shape="round"
size="large"
css={{width: '12em'}}
>
<UploadOutlined /> Click to Upload
</Button>
</Upload>
);
}

export default ImageIpfsUpload;
40 changes: 40 additions & 0 deletions client/src/components/CreateNonFungiblePage/ImagePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/** @jsx jsx */
import { jsx } from '@emotion/core';
import styled from '@emotion/styled'
import { FC } from 'react';
import { Form, Empty } from 'antd';
import { IpfsContent } from '../../api/ipfsUploader';

const Image = styled.img({
width: '100%'
});

const ImageContainer = styled.div({
width: '20em',
border: 'solid 1px #C8C8C8',
padding: '1.5em'
})

const ImagePreview: FC<{ipfsContent?: IpfsContent}> = ({ ipfsContent }) => (
<Form layout="vertical">
<Form.Item label="Image Preview">
<ImageContainer>
{ipfsContent ? (
<a
href={ipfsContent.publicGatewayUrl}
title="Click to download this image from the IPFS Public Gateway"
target="_blank"
rel="noopener noreferrer"
>
<Image src={ipfsContent.url} alt="token" />
</a>
) : (
<Empty description="Upload an Image" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</ImageContainer>
</Form.Item>
</Form>
);

export default ImagePreview;

Loading

0 comments on commit 67bd392

Please sign in to comment.