DAPP结合IPFS — 去中心化图床

内容:打造一款去中心化图床,用户可上传图片至IPFS上,文件hash保存在以太坊的区块上,以此实现永存的去中心化图床。

技术栈:依旧使用truffle框架快速构建项目truffle unbox react

1、什么是 IPFS

星际文件系统IPFS(InterPlanetary File System)是一个面向全球的、点对点的分布式版本文件系统,目标是为了补充(甚至是取代)目前统治互联网的超文本传输协议(HTTP),将所有具有相同文件系统的计算设备连接在一起。原理用基于内容的地址替代基于域名的地址,也就是用户寻找的不是某个地址而是储存在某个地方的内容,不需要验证发送者的身份,而只需要验证内容的哈希,通过这样可以让网页的速度更快、更安全、更健壮、更持久。

直白了说,就是类似BT下载的p2p文件存储、传输系统。

2、安装 IPFS

IPFS官网下载对应系统的安装包(需要翻墙)
以Mac为例,终端执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cd /Users/ludis/Downloads

tar xvfz go-ipfs_v0.4.13_darwin-amd64.tar.gz

cd go-ipfs

mv ipfs /usr/local/bin/ipfs

// 创建本地节点
ipfs init

// 查看节点ID
ipfs id

// 启动节点服务器(可以上传文件至外网/同步外网文件)
ipfs daemon

ipfs节点服务器启动之后可以浏览器访问http://localhost:5001/webui查看管理界面,有本地配置信息、节点连接信息、本地节点文件信息等等。

3、IPFS 基本操作

1、新建文件并添加至IPFS节点

1
2
3
4
5
6
7
ludis@MacBook ~/Desktop/test cat > file.txt
hello ipfs!
^C
ludis@MacBook ~/Desktop/test cat file.txt
hello ipfs!
ludis@MacBook ~/Desktop/test ipfs add file.txt
added QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte file.txt

将文件添加至ipfs节点后,会返回文件的hash,上例QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte

2、查看IPFS上的文件

1
2
ludis@MacBook ~/Desktop/test ipfs cat QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte
hello ipfs!

此时文件只是添加到了本地的IPFS节点,可以通过终端读取到。
当通过ipfs daemon启动本地节点服务器后,也可以通过http://localhost:8080/ipfs/QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte访问到文件。
在启动节点服务器后,会将本地节点文件同步至外网,当同步完成后,就可以通过https://ipfs.io/iofs/QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte访问到文件。至此你的文件已经永存在ipfs网络上了!
由于目前IPFS网络暂未加入代币机制,所以存储读取文件均免费,当然了,速度也慢很多。

3、下载IPFS上的文件

1
2
3
ludis@MacBook ~/Desktop/test ipfs get QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte
Saving file(s) to QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte
20 B / 20 B [========================================] 100.00% 0s

通过get命令,会下载文件到当前目录。

4、新建目录

1
2
3
4
5
6
7
8
ludis@MacBook  ~/Desktop/test  ipfs files mkdir /ludis
ludis@MacBook ~/Desktop/test ipfs files cp /ipfs/QmZ5cRqiNsg1ngmzmKrv5STMoyfLaJhhHqXyMWTkre1qte /ludis/readme.txt
ludis@MacBook ~/Desktop/test ipfs files ls
ludis
ludis@MacBook ~/Desktop/test ipfs files ls /ludis
readme.txt
ludis@MacBook ~/Desktop/test ipfs files read /ludis/readme.txt
hello ipfs!

5、上传整个目录

1
ipfs add -r files/

上传整个目录时,所有文件都有其对应的hash,并且每个目录都有其hash,访问某个文件有两种方式:

  1. 直接通过问价hash访问
  2. 通过目录hash/文件名访问

IPFS还有很多有趣的地方。例如可以上传一个静态网站到ipfs,并通过浏览器访问,这样就创建了一个永存的网站。往IPFS上传相同的文件,由于hash相同,系统会自动识别,只给后上传的用户建立已有文件的索引,而不是上传一份相同的文件,这样节省很多空间。同时如果一个文件是在另一个文件的基础上修改了写内容而导致hash不同,那么他们相同的内容也不会重复存储,而只是在原文件上拼接不同的部分。总之,IPFS系统有很多高明之处,需要仔细研究。

4、设置跨域

当我们在前端通过js接口操作ipfs时,会遇到老生常谈的跨域问题,只需终端执行以下配置即可:

1
2
3
4
5
ipfs config —json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT","GET", "POST", "OPTIONS"]'
ipfs config —json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]'
ipfs config —json API.HTTPHeaders.Access-Control-Allow-Credentials '["true"]'
ipfs config —json API.HTTPHeaders.Access-Control-Allow-Headers '["Authorization"]'
ipfs config —json API.HTTPHeaders.Access-Control-Expose-Headers '["Location"]'

5、IPFS 与 DAPP 结合

由于以太坊区块的特性,往区块上存储大文件显然是不合理的。所以通用做法是文件存储到IPFS,之后将文件的hash存储到以太坊区块。当读取时,首先从以太坊区块上取到文件的hash,然后通过hash去IPFS网络上读取文件。

依然使用之前的truffle unbox react创建项目,不同的是只需要多安装一个依赖库ipfs-api,直接cnpm i -S ipfs-api安装即可,显而易见,这就是IPFS系统的js api,这样我们就能在前端调用IPFS的接口上传、读取文件。

话不多说,直接上代码,一个是智能合约,一个是前端react文件,合约交互前几篇已经比较熟悉了,主要看一下怎么通过ipfs-api上传下载文件。一切尽在代码中:

SimpleStorage.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity ^0.4.19;

contract SimpleStorage {

string[] public photoArr;

mapping(address => uint) storeAddress;

function storePhoto(string hash) public {
if(storeAddress[msg.sender]==0){
photoArr.push(hash);
storeAddress[msg.sender] = 1;
}
}

function getPhoto(uint index) public view returns (uint, string){
if(photoArr.length==0){
return (0, "");
}else{
return (photoArr.length, photoArr[index]);
}
}

function isStored() public view returns (bool) {
if(storeAddress[msg.sender]==0){
return false;
}else{
return true;
}
}

}

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import React, {Component} from 'react'
import SimpleStorageContract from '../build/contracts/SimpleStorage.json'
import getWeb3 from './utils/getWeb3'

import './css/oswald.css'
import './css/open-sans.css'
import './css/pure-min.css'
import './App.css'

import ipfsAPI from 'ipfs-api'
const ipfs = ipfsAPI({host: 'localhost', port: '5001', protocal: 'http'})

const contractAddress = "0x7ebeb83816b74da8173e3f406aeac012cf1718f5"
let simpleStorageInstance

// Promise 存储文件至ipfs
let saveImageOnIpfs = (reader) => {
return new Promise(function (resolve, reject) {
const buffer = Buffer.from(reader.result);
ipfs.add(buffer).then((response) => {
console.log(response)
resolve(response[0].hash);
})
.catch((err) => {
console.error(err)
reject(err);
})
})
}

class App extends Component {
constructor(props) {
super(props)

this.state = {
photos: [],
count: 0,
web3: null
}
}

componentWillMount() {
// Get network provider and web3 instance. See utils/getWeb3 for more info.
getWeb3.then(results => {
this.setState({web3: results.web3})
this.instantiateContract()
}).catch(() => {
console.log('Error finding web3.')
})
}

instantiateContract() {
const that = this
const contract = require('truffle-contract')
const simpleStorage = contract(SimpleStorageContract)
simpleStorage.setProvider(this.state.web3.currentProvider)

this.state.web3.eth.getAccounts((error, accounts) => {
simpleStorage.at(contractAddress).then((instance) => {
simpleStorageInstance = instance
})
.then(result => {
console.log('inint success')
return simpleStorageInstance.getPhoto(0)
})
.then(result => {
console.log(result)
let imgNum = result[0].c[0]
if(imgNum===0){
return
}
if(imgNum===1){
this.setState({
count: imgNum,
photos: this.state.photos.concat([result[1]])
})
}
if(imgNum>1){
// 闭包,读取存储的所有图片
for(let i=0;i<imgNum;i++){
(function(i){
simpleStorageInstance.getPhoto(i)
.then(result => {
that.setState({
photos: that.state.photos.concat([result[1]])
})
})
})(i)
}
}
})
})
}

render() {
let doms = [],
photos = this.state.photos
for(let i=0; i<photos.length;i++){
doms.push(<div key={i}><img src={"http://localhost:8080/ipfs/" + photos[i]}/></div>)
}

return (
<div className="App">
<header>上传图片至ipfs,并保存信息至以太坊区块</header>
<div className="upload-container">
<label id="file">选择图片</label>
<input type="file" ref="file" id="file" name="file" multiple="multip
le"/>
<button onClick={() => this.upload()}>上传</button>
</div>
<div className="img-container">
{doms}
</div>
</div>
);
}

upload() {
console.log("upload");
let isStored = false
simpleStorageInstance.isStored()
.then(result => {
console.log("is stored", result)
if(result) {
isStored = true
}
})
if(isStored) {
alert("每个钱包地址只能上传一张图片哦😯 ~")
return
}
var file = this.refs.file.files[0];
console.log(file)
var reader = new FileReader();
// reader.readAsDataURL(file);
reader.readAsArrayBuffer(file)
reader.onloadend = (e) => {
//console.log(reader);
saveImageOnIpfs(reader).then((hash) => {
console.log(hash);
return simpleStorageInstance.storePhoto(hash, {from: this.state.web3.eth.accounts[0]})
.then(result => {
console.log("写入区块成功", result)
this.setState({
photos: this.state.photos.concat([hash])
})
})
});
}
}

}

export default App

完整代码:GitHub

Author

Ludis

Posted on

2018-03-06

Updated on

2018-03-09

Licensed under

Comments