内容 :打造一款去中心化图床,用户可上传图片至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/Downloadstar xvfz go-ipfs_v0.4.13_darwin-amd64.tar.gz cd go-ipfsmv 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、上传整个目录
上传整个目录时,所有文件都有其对应的hash,并且每个目录都有其hash,访问某个文件有两种方式:
直接通过问价hash访问
通过目录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