DAPP结合IPFS — 去中心化图床

2018-03-06 21:31:35

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

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

1、什么是 IPFS

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

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

2、安装 IPFS

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

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节点

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上的文件

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上的文件

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

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

4、新建目录

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、上传整个目录

ipfs add -r files/

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

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

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

4、设置跨域

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

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

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

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

一个完整的智能合约—区块链上永存的留言

目标: 基于以太坊开发一个完整的DAPP应用,实现留言及随机展示留言功能。 技术栈:使用solidity语言的truffle框架,项目模板使用react,前端使用ES6语法。 项目线上地址: http://words.ldsun.com/ 项目代码:https://github.com/flute/blockChainWords 总结: 本次从零开发发哦部署上线约耗时六小时,前期在solidity合约开发上费时较多,主要是对其语言特性不了解,后续在react的动画上有些费时,源于对动画的生疏及类库的选择。 整个的开发流程比较清晰: 编写、调试合约 编写react前端页面、与合约交互的逻辑、显示逻辑、动画等 部署合约,与前端联调 编译项目,上传代码,配置nginx解析。 补充: 整个项目的的实现比较简单,这里不做具体分析,可以git查看,同时参考前几篇文章即可。这里贴一下solidity合约开发中开始的一些“美(错)好(误)想(代)法(码)

搭建ETH私链并创建ERC20 Token

1、下载ETH钱包 下载 Ethereum Wallet或Mist其一即可,二者功能相同。 2、安装geth Mac下使用: brew install geth 其余平台参考安装方法 3、编辑创世区块配置文件 genesis.json:(参考示例) ludis@MacBook ~ cd Desktop/ethereum ludis@MacBook ~/Desktop/ethereum cat > genesis.json { "config": { "chainId": 33, "homesteadBlock": 0, "eip155Block": 0, "eip158Block": 0