上传!上传!

IPFS 是星际文件系统(InterPlanetary File System)的简称,它为前端应用展现了一种可能性:大部分轻量应用其实可以只开发前端,不需要后端!

如果你的应用只涉及轻量数据的慢速存取,那就可以来试试看用 IPFS 来从分布式存储网络上获取数据,再把用户产生的数据写到 IPFS 上。

以下是现在大家使用 IPFS 的几种招式。

[1] 带有进度条的网页端上传

原料:

  1. stream-buffers 用于提供进度条的 stream
  2. buffer 用于把文件变成能喂给 stream 的类型

在线地址:

codeSandbox github repo

由于 codeSandbox 上 IPFS 会有点问题,所以还请 clone github repo 来试用。

// @flow

import IPFS from 'ipfs';
import { Buffer } from 'buffer';
import streamBuffers from 'stream-buffers';

class IPFSUploader {
  node: any;
  progress: number;
  stream: any;

  constructor(http: Http) {
    // 用随机的仓库地址(IPFS 在本地缓存数据的地方)来初始化 IPFS 节点
    const repoPath = 'ipfs-' + Math.random();
    this.node = new IPFS({ repo: repoPath });

    // 节点完成初始化并开始连接其他节点后会触发 ready 事件
    this.node.on('ready', () => console.log('Online status: ', this.node.isOnline() ? 'online' : 'offline'));
  }

  uploadIPFS = (fileArrayBuffer: ArrayBuffer): Promise<Buffer> => {
    return new Promise((resolve, reject) => {
      // 先设置进度条到 0 的位置
      this.progress = 0;
      // 创建用于修改进度条进度的流
      const myReadableStreamBuffer = new streamBuffers.ReadableStreamBuffer({
        chunkSize: 25000, // 决定了传输速率
      });
      // 修改进度条进度
      myReadableStreamBuffer.on('data', (chunk: Buffer) => {
        this.progress += chunk.byteLength;
        myReadableStreamBuffer.resume();
      });

      // 创建 IPFS 读写文件的流,这是一个 Duplex 流,可读可写
      this.stream = this.node.files.addReadableStream();
      // 文件上传完毕后 resolve 这个 Promise
      this.stream.on('data', (file: Buffer) => resolve(file));

      // 对接好两个流,并开始上传
      this.stream.write(myReadableStreamBuffer);
      myReadableStreamBuffer.put(Buffer.from(fileArrayBuffer));

      // 上传完毕后关闭流
      myReadableStreamBuffer.on('end', this.stream.end);
      myReadableStreamBuffer.stop();
    });
  };
}

// 从用户处获取要上传的文件
const uploader = new IPFSUploader();
function onUpload(event: SyntheticInputEvent<EventTarget>) {
  const file: File = event.target.file;
  // 用 FileReader API
  // https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
  const reader = new FileReader();
  reader.onload = async () => {
    // FileReader 在 readAsArrayBuffer 结束后,会把 ArrayBuffer 放在 reader.result 里
    const ipfsObject: { path: string, hash: string, size: number } = await uploader.uploadIPFS(reader.result);
    const URL = `https://ipfs.io/ipfs/${ipfsObject.hash}`;
  };
  reader.readAsArrayBuffer(file);
}

[2] 自动下载 go-ipfs 用于启动 daemon

原料:

  1. @akashaproject/ipfs-connector 用于在 nodeJS(Electron)环境下自动下载 ipfs 二进制文件启动 daemon
// @flow
import { IpfsConnector, ipfsEvents, ConnectorState } from '@akashaproject/ipfs-connector';

const instance = IpfsConnector.getInstance();
const repoPathOnFs = '~/ipfs-test-repo';

// 如果电脑上没用 IPFS 的可执行文件,就会把可执行文件下载到这里
instance.setBinPath(repoPathOnFs);

// 设置 IPFS 本地 repo 的位置
instance.setIpfsFolder(path.join(repoPathOnFs, '/repo'));

instance.setOption('args', ['daemon', '--enable-pubsub-experiment']);
instance.enableDownloadEvents();

// install some event listeners
instance.on(ipfsEvents.SERVICE_STARTING, onServiceStarting);
instance.on(ipfsEvents.SERVICE_STARTED, onServiceStarted);
instance.on(ipfsEvents.UPGRADING_BINARY, onServiceUpgrade);
instance.on(ipfsEvents.SERVICE_STOPPING, onServiceStopping);
instance.on(ipfsEvents.SERVICE_FAILED, onServiceFailed);
instance.on(ipfsEvents.STATUS_UPDATE, onStatusUpdate);
instance.on(ipfsEvents.DOWNLOAD_PROGRESS, onDownloadProgress);
instance.on(ipfsEvents.DOWNLOAD_ERROR, onDownloadError);

// 启动 IPFS daemon,如果电脑上没有装 IPFS 就会自动下载到 setBinPath 设置的地址里
instance.start();

const stop = () => {
  // 重新获取单例,用它下线 IPFS 节点
  const instance = IpfsConnector.getInstance();
  instance.stop();
};

这段代码不方便执行,因为它会自动从 https://dist.ipfs.io/go-ipfs/v0.4.13/go-ipfs_v0.4.13_darwin-amd64.tar.gz 下载二进制文件,但是给 ipfs.io 签发数字证书的 CA 被墙了,所以在程序里会报错 RequestError: Hostname/IP does not match certificate's altnames: Host: dist.ipfs.io. is not in the cert's altnames: DNS:api.twitter.com,如果在浏览器里不翻墙访问这个地址也会说这个 HTTPS 有问题。翻墙了就没事,但作为开发者,我们没法保证用户都翻墙,对吧。

参考