diff --git a/.travis.yml b/.travis.yml
index 9561a8e1a1..e18b74ad1e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -164,6 +164,41 @@ jobs:
script:
- npm run test:interface:core -- $RUN_SINCE -- -- --bail -t electron-renderer --timeout 60000
+ - stage: test
+ name: js-ipfs interface tests - ipfs-client - node
+ script:
+ - npm run test:interface:client -- $RUN_SINCE -- -- --bail -t node
+
+ - stage: test
+ name: js-ipfs interface tests - ipfs-client - chrome
+ script:
+ - npm run test:interface:client -- $RUN_SINCE -- -- --bail -t browser
+
+ - stage: test
+ name: js-ipfs interface tests - ipfs-client - chrome webworker
+ script:
+ - npm run test:interface:client -- $RUN_SINCE -- -- --bail -t webworker --timeout 60000
+
+ - stage: test
+ name: js-ipfs interface tests - ipfs-client - firefox
+ script:
+ - npm run test:interface:client -- $RUN_SINCE -- -- --bail -t browser --browsers FirefoxHeadless
+
+ - stage: test
+ name: js-ipfs interface tests - ipfs-client - firefox webworker
+ script:
+ - npm run test:interface:client -- $RUN_SINCE -- -- --bail -t webworker --browsers FirefoxHeadless --timeout 60000
+
+ - stage: test
+ name: js-ipfs interface tests - ipfs-client - electron main
+ script:
+ - npm run test:interface:client -- $RUN_SINCE -- -- --bail -t electron-main --timeout 60000
+
+ - stage: test
+ name: js-ipfs interface tests - ipfs-client - electron renderer
+ script:
+ - npm run test:interface:client -- $RUN_SINCE -- -- --bail -t electron-renderer --timeout 60000
+
- stage: test
name: http-api-client interface tests vs go-ipfs - node
script:
diff --git a/examples/browser-ipns-publish/package.json b/examples/browser-ipns-publish/package.json
index 417b7edb8a..020e6bfac5 100644
--- a/examples/browser-ipns-publish/package.json
+++ b/examples/browser-ipns-publish/package.json
@@ -27,7 +27,7 @@
"devDependencies": {
"delay": "^4.4.0",
"execa": "^4.0.3",
- "ipfsd-ctl": "^7.1.1",
+ "ipfsd-ctl": "^7.2.0",
"go-ipfs": "^0.7.0",
"parcel-bundler": "^1.12.4",
"path": "^0.12.7",
diff --git a/examples/explore-ethereum-blockchain/package.json b/examples/explore-ethereum-blockchain/package.json
index 0d4ec63e96..6cd4a169b2 100644
--- a/examples/explore-ethereum-blockchain/package.json
+++ b/examples/explore-ethereum-blockchain/package.json
@@ -12,7 +12,7 @@
"devDependencies": {
"ipfs": "^0.52.2",
"ipfs-http-client": "^48.1.2",
- "ipfsd-ctl": "^7.1.1",
+ "ipfsd-ctl": "^7.2.0",
"ipld-ethereum": "^5.0.1",
"test-ipfs-example": "^2.0.3"
}
diff --git a/examples/http-client-browser-pubsub/package.json b/examples/http-client-browser-pubsub/package.json
index a2620020c3..3df503397d 100644
--- a/examples/http-client-browser-pubsub/package.json
+++ b/examples/http-client-browser-pubsub/package.json
@@ -21,7 +21,7 @@
"execa": "^4.0.3",
"go-ipfs": "^0.7.0",
"ipfs": "^0.52.2",
- "ipfsd-ctl": "^7.1.1",
+ "ipfsd-ctl": "^7.2.0",
"parcel-bundler": "^1.12.4",
"test-ipfs-example": "^2.0.3"
}
diff --git a/examples/http-client-bundle-webpack/package.json b/examples/http-client-bundle-webpack/package.json
index 92fd387d19..f0363aef13 100644
--- a/examples/http-client-bundle-webpack/package.json
+++ b/examples/http-client-bundle-webpack/package.json
@@ -25,7 +25,7 @@
"copy-webpack-plugin": "^5.0.4",
"execa": "^4.0.3",
"ipfs": "^0.52.2",
- "ipfsd-ctl": "^7.1.1",
+ "ipfsd-ctl": "^7.2.0",
"react-hot-loader": "^4.12.21",
"rimraf": "^3.0.2",
"test-ipfs-example": "^2.0.3",
diff --git a/examples/http-client-name-api/package.json b/examples/http-client-name-api/package.json
index f7e7bf9df2..04080259b7 100644
--- a/examples/http-client-name-api/package.json
+++ b/examples/http-client-name-api/package.json
@@ -18,7 +18,7 @@
"devDependencies": {
"execa": "^4.0.3",
"go-ipfs": "^0.7.0",
- "ipfsd-ctl": "^7.1.1",
+ "ipfsd-ctl": "^7.2.0",
"parcel-bundler": "^1.12.4",
"rimraf": "^3.0.2",
"test-ipfs-example": "^2.0.3"
diff --git a/examples/ipfs-client-add-files/README.md b/examples/ipfs-client-add-files/README.md
new file mode 100644
index 0000000000..1fbd3bb0fd
--- /dev/null
+++ b/examples/ipfs-client-add-files/README.md
@@ -0,0 +1,21 @@
+# JS IPFS API - Example Browser - Name
+
+## Setup
+
+```sh
+npm install -g ipfs
+jsipfs init
+# Configure CORS to allow ipfs-http-client to access this IPFS node
+jsipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["http://127.0.0.1:8888"]'
+# Start the IPFS node
+jsipfs daemon
+```
+
+Then in this folder run
+
+```bash
+> npm install
+> npm start
+```
+
+and open your browser at `http://127.0.0.1:8888`.
diff --git a/examples/ipfs-client-add-files/index.html b/examples/ipfs-client-add-files/index.html
new file mode 100644
index 0000000000..619eb6b919
--- /dev/null
+++ b/examples/ipfs-client-add-files/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+ JS IPFS Client example
+
+
+
+
+ ipfs-client
+
+
+
+
+
+
+
diff --git a/examples/ipfs-client-add-files/index.js b/examples/ipfs-client-add-files/index.js
new file mode 100644
index 0000000000..d66a40ad4a
--- /dev/null
+++ b/examples/ipfs-client-add-files/index.js
@@ -0,0 +1,81 @@
+/* eslint-disable no-console */
+'use strict'
+
+const ipfsClient = require('ipfs-client')
+let ipfs
+
+const COLORS = {
+ active: 'blue',
+ success: 'green',
+ error: 'red'
+}
+
+const showStatus = (text, bg) => {
+ console.info(text)
+
+ const log = document.getElementById('output')
+
+ if (!log) {
+ return
+ }
+
+ const line = document.createElement('p')
+ line.innerText = text
+ line.style.color = bg
+
+ log.appendChild(line)
+}
+
+async function * streamFiles () {
+ for (let i = 0; i < 100; i++) {
+ await new Promise((resolve) => {
+ setTimeout(() => resolve(), 100)
+ })
+
+ showStatus(`Sending /file-${i}.txt`, COLORS.active)
+
+ yield {
+ path: `/file-${i}.txt`,
+ content: `file ${i}`
+ }
+ }
+}
+
+async function main (grpcApi, httpApi) {
+ showStatus(`Connecting to ${grpcApi} using ${httpApi} as fallback`, COLORS.active)
+
+ ipfs = ipfsClient({
+ grpc: grpcApi,
+ http: httpApi
+ })
+
+ const id = await ipfs.id()
+ showStatus(`Daemon active\nID: ${id.id}`, COLORS.success)
+
+ for await (const file of ipfs.addAll(streamFiles(), {
+ wrapWithDirectory: true,
+ // this is just to show the interleaving of uploads and progress events
+ // otherwise we'd have to upload 50 files before we see any response from
+ // the server. do not specify this so low in production as you'll have
+ // greatly degraded import performance
+ fileImportConcurrency: 1,
+ progress: (bytes, file) => {
+ showStatus(`File progress ${file} ${bytes}`, COLORS.active)
+ }
+ })) {
+ showStatus(`Added file: ${file.path} ${file.cid}`, COLORS.success)
+ }
+
+ showStatus('Finished!', COLORS.success)
+}
+
+// Event listeners
+document.getElementById('connect-submit').onclick = (e) => {
+ e.preventDefault()
+
+ main(document.getElementById('grpc-input').value, document.getElementById('http-input').value)
+ .catch(err => {
+ showStatus(err.message, COLORS.error)
+ console.error(err)
+ })
+}
diff --git a/examples/ipfs-client-add-files/package.json b/examples/ipfs-client-add-files/package.json
new file mode 100644
index 0000000000..39b48b2e1c
--- /dev/null
+++ b/examples/ipfs-client-add-files/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "example-ipfs-client-add-files",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "private": true,
+ "scripts": {
+ "clean": "rimraf ./dist",
+ "build": "parcel build index.html --public-url '.'",
+ "start": "parcel index.html -p 8888",
+ "test": "test-ipfs-example"
+ },
+ "dependencies": {
+ "ipfs-client": "^0.1.0"
+ },
+ "devDependencies": {
+ "execa": "^4.0.3",
+ "ipfs": "^0.52.0",
+ "ipfsd-ctl": "^7.2.0",
+ "parcel-bundler": "^1.12.4",
+ "rimraf": "^3.0.2",
+ "test-ipfs-example": "^2.0.3"
+ },
+ "browserslist": [
+ "last 2 versions and not dead and > 2%"
+ ]
+}
diff --git a/examples/ipfs-client-add-files/test.js b/examples/ipfs-client-add-files/test.js
new file mode 100644
index 0000000000..84f6892d9d
--- /dev/null
+++ b/examples/ipfs-client-add-files/test.js
@@ -0,0 +1,82 @@
+'use strict'
+
+const path = require('path')
+const execa = require('execa')
+const { createFactory } = require('ipfsd-ctl')
+const df = createFactory({
+ ipfsClientModule: require('ipfs-client'),
+ ipfsBin: require.resolve('ipfs/src/cli.js')
+})
+const {
+ startServer
+} = require('test-ipfs-example/utils')
+const pkg = require('./package.json')
+
+async function testUI (url, http, grpc, id) {
+ const proc = execa(require.resolve('test-ipfs-example/node_modules/.bin/nightwatch'), ['--config', require.resolve('test-ipfs-example/nightwatch.conf.js'), path.join(__dirname, 'test.js')], {
+ cwd: path.resolve(__dirname, '../'),
+ env: {
+ ...process.env,
+ CI: true,
+ IPFS_EXAMPLE_TEST_URL: url,
+ IPFS_GRPC_API_MULTIADDR: grpc,
+ IPFS_HTTP_API_MULTIADDR: http
+ },
+ all: true
+ })
+ proc.all.on('data', (data) => {
+ process.stdout.write(data)
+ })
+
+ await proc
+}
+
+async function runTest () {
+ const app = await startServer(__dirname)
+ const daemon = await df.spawn({
+ type: 'js',
+ test: true,
+ ipfsOptions: {
+ config: {
+ Addresses: {
+ API: '/ip4/127.0.0.1/tcp/0',
+ RPC: '/ip4/127.0.0.1/tcp/0'
+ },
+ API: {
+ HTTPHeaders: {
+ 'Access-Control-Allow-Origin': [
+ app.url
+ ]
+ }
+ }
+ }
+ }
+ })
+
+ try {
+ await testUI(app.url, daemon.apiAddr, daemon.grpcAddr, daemon.api.peerId.id)
+ } finally {
+ await daemon.stop()
+ await app.stop()
+ }
+}
+
+module.exports = runTest
+
+module.exports[pkg.name] = function (browser) {
+ browser
+ .url(process.env.IPFS_EXAMPLE_TEST_URL)
+ .waitForElementVisible('#grpc-input')
+ .clearValue('#grpc-input')
+ .setValue('#grpc-input', process.env.IPFS_GRPC_API_MULTIADDR)
+ .pause(1000)
+ .waitForElementVisible('#http-input')
+ .clearValue('#http-input')
+ .setValue('#http-input', process.env.IPFS_HTTP_API_MULTIADDR)
+ .pause(1000)
+ .click('#connect-submit')
+
+ browser.expect.element('#output').text.to.contain('Added file: file-0.txt QmUDLiEJwL3vUhhXNXDF2RrCnVkSB2LemWYffpCCPcQCeU')
+
+ browser.end()
+}
diff --git a/package.json b/package.json
index 9cc642064d..767f2b1e1f 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "JavaScript implementation of the IPFS specification",
"scripts": {
- "postinstall": "lerna bootstrap",
+ "postinstall": "lerna bootstrap && npm run build -- --scope=ipfs-grpc-protocol",
"link": "lerna link",
"reset": "lerna run clean && rimraf packages/*/node_modules node_modules",
"test": "lerna run test",
@@ -16,6 +16,7 @@
"test:external": "lerna run test:external",
"test:cli": "lerna run test:cli",
"test:interop": "lerna run test:interop",
+ "test:interface:client": "lerna run test:interface:client",
"test:interface:core": "lerna run test:interface:core",
"test:interface:http-go": "lerna run test:interface:http-go",
"test:interface:http-js": "lerna run test:interface:http-js",
diff --git a/packages/interface-ipfs-core/src/add-all.js b/packages/interface-ipfs-core/src/add-all.js
index 8dd06bc229..5b10ca77ac 100644
--- a/packages/interface-ipfs-core/src/add-all.js
+++ b/packages/interface-ipfs-core/src/add-all.js
@@ -422,6 +422,22 @@ module.exports = (common, options) => {
expect(files[0].size).to.equal(18)
})
+ it('should add directories with metadata', async () => {
+ const files = await all(ipfs.addAll([{
+ path: '/foo',
+ mode: 0o123,
+ mtime: {
+ secs: 1000,
+ nsecs: 0
+ }
+ }]))
+
+ expect(files.length).to.equal(1)
+ expect(files[0].cid.toString()).to.equal('QmaZTosBmPwo9LQ48ESPCEcNuX2kFxkpXYy8i3rxqBdzRG')
+ expect(files[0].cid.codec).to.equal('dag-pb')
+ expect(files[0].size).to.equal(11)
+ })
+
it('should support bidirectional streaming', async function () {
let progressInvoked
diff --git a/packages/ipfs-cli/src/commands/daemon.js b/packages/ipfs-cli/src/commands/daemon.js
index 83905dd12c..81340208b9 100644
--- a/packages/ipfs-cli/src/commands/daemon.js
+++ b/packages/ipfs-cli/src/commands/daemon.js
@@ -83,15 +83,17 @@ module.exports = {
try {
await daemon.start()
- // @ts-ignore - _httpApi is possibly undefined
+ // @ts-ignore - _apiServers is possibly undefined
daemon._httpApi._apiServers.forEach(apiServer => {
- print(`API listening on ${apiServer.info.ma}`)
+ print(`HTTP API listening on ${apiServer.info.ma}`)
})
+ // @ts-ignore - _grpcServer is possibly undefined
+ print(`gRPC listening on ${daemon._grpcServer.multiaddr}`)
// @ts-ignore - _httpGateway is possibly undefined
daemon._httpGateway._gatewayServers.forEach(gatewayServer => {
print(`Gateway (read only) listening on ${gatewayServer.info.ma}`)
})
- // @ts-ignore - _httpApi is possibly undefined
+ // @ts-ignore - _apiServers is possibly undefined
daemon._httpApi._apiServers.forEach(apiServer => {
print(`Web UI available at ${toUri(apiServer.info.ma)}/webui`)
})
diff --git a/packages/ipfs-client/.aegir.js b/packages/ipfs-client/.aegir.js
new file mode 100644
index 0000000000..b55dc70587
--- /dev/null
+++ b/packages/ipfs-client/.aegir.js
@@ -0,0 +1,7 @@
+'use strict'
+
+module.exports = {
+ bundlesize: {
+ maxSize: '112kB'
+ }
+}
diff --git a/packages/ipfs-client/README.md b/packages/ipfs-client/README.md
new file mode 100644
index 0000000000..23d7f8500e
--- /dev/null
+++ b/packages/ipfs-client/README.md
@@ -0,0 +1,55 @@
+# ipfs-client
+
+> A client for [ipfs][] daemons
+
+This module combines the [ipfs-grpc-client][] and [ipfs-http-client][] modules to give you a client that is capable of bidirectional streaming in the browser as well as node.
+
+## Install
+
+```console
+$ npm install ipfs-client
+```
+
+## API
+
+The client object created by the `createClient` function supports the [IPFS Core API](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api), see the docs for more.
+
+### `createClient([options])`
+
+### Parameters
+
+None
+
+### Options
+
+An optional object which may have the following keys:
+
+| Name | Type | Default | Description |
+| ---- | ---- | ------- | ----------- |
+| grpc | `Multiaddr` or `string` or `URL` | `undefined` | The address of a [ipfs-grpc-server][] to connect to |
+| http | `Multiaddr` or `string` or `URL` | `undefined` | The address of a [ipfs-http-server][] to connect to |
+
+### Returns
+
+| Type | Description |
+| -------- | -------- |
+| `object` | An instance of the client |
+
+### Example
+
+```js
+const createClient = require('ipfs-client')
+
+const client = createClient({
+ grpc: '/ipv4/127.0.0.1/tcp/5003/ws',
+ http: '/ipv4/127.0.0.1/tcp/5002/http'
+})
+
+const id = await client.id()
+```
+
+[ipfs]: https://www.npmjs.com/package/ipfs
+[ipfs-grpc-client]: https://www.npmjs.com/package/ipfs-grpc-client
+[ipfs-http-client]: https://www.npmjs.com/package/ipfs-http-client
+[ipfs-grpc-server]: https://www.npmjs.com/package/ipfs-grpc-server
+[ipfs-http-server]: https://www.npmjs.com/package/ipfs-http-server
diff --git a/packages/ipfs-client/package.json b/packages/ipfs-client/package.json
new file mode 100644
index 0000000000..1d3816a805
--- /dev/null
+++ b/packages/ipfs-client/package.json
@@ -0,0 +1,49 @@
+{
+ "name": "ipfs-client",
+ "version": "0.1.0",
+ "description": "A client library to talk to local IPFS daemons",
+ "keywords": [
+ "ipfs"
+ ],
+ "homepage": "https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-client#readme",
+ "bugs": "https://github.com/ipfs/js-ipfs/issues",
+ "license": "(Apache-2.0 OR MIT)",
+ "leadMaintainer": "Alex Potsides ",
+ "files": [
+ "src",
+ "dist"
+ ],
+ "main": "src/index.js",
+ "typesVersions": {
+ "*": {
+ "*": [
+ "dist/*",
+ "dist/*/index"
+ ]
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ipfs/js-ipfs.git"
+ },
+ "scripts": {
+ "test": "aegir test",
+ "lint": "aegir lint",
+ "build": "npm run build:js && npm run build:types",
+ "build:js": "aegir build",
+ "build:types": "tsc --build",
+ "coverage": "npx nyc -r html npm run test:node -- --bail",
+ "clean": "rimraf ./dist",
+ "dep-check": "aegir dep-check -i aegir -i typescript -i rimraf"
+ },
+ "dependencies": {
+ "ipfs-grpc-client": "0.0.0",
+ "ipfs-http-client": "^48.1.0",
+ "merge-options": "^2.0.0"
+ },
+ "devDependencies": {
+ "aegir": "^29.2.2",
+ "rimraf": "^3.0.2",
+ "typescript": "4.0.x"
+ }
+}
diff --git a/packages/ipfs-client/src/index.js b/packages/ipfs-client/src/index.js
new file mode 100644
index 0000000000..4f391e28fa
--- /dev/null
+++ b/packages/ipfs-client/src/index.js
@@ -0,0 +1,26 @@
+'use strict'
+
+const httpClient = require('ipfs-http-client')
+const grpcClient = require('ipfs-grpc-client')
+const mergeOptions = require('merge-options')
+
+module.exports = function createClient (opts = {}) {
+ const clients = []
+
+ if (opts.http) {
+ clients.push(httpClient({
+ ...opts,
+ url: opts.http
+ }))
+ }
+
+ if (opts.grpc) {
+ clients.push(grpcClient({
+ ...opts,
+ url: opts.grpc
+ }))
+ }
+
+ // override http methods with grpc if address is supplied
+ return mergeOptions.apply({ ignoreUndefined: true }, clients)
+}
diff --git a/packages/ipfs-client/tsconfig.json b/packages/ipfs-client/tsconfig.json
new file mode 100644
index 0000000000..979a39adab
--- /dev/null
+++ b/packages/ipfs-client/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist"
+ },
+ "include": [
+ "src",
+ "package.json"
+ ]
+}
diff --git a/packages/ipfs-core/.aegir.js b/packages/ipfs-core/.aegir.js
index 5b46973aa3..41e0cda3b0 100644
--- a/packages/ipfs-core/.aegir.js
+++ b/packages/ipfs-core/.aegir.js
@@ -8,7 +8,7 @@ let preloadNode = MockPreloadNode.createNode()
let ipfsdServer
module.exports = {
- bundlesize: { maxSize: '550kB' },
+ bundlesize: { maxSize: '560kB' },
karma: {
files: [{
pattern: 'node_modules/interface-ipfs-core/test/fixtures/**/*',
diff --git a/packages/ipfs-core/package.json b/packages/ipfs-core/package.json
index 0f39fad8a3..cf1968cc83 100644
--- a/packages/ipfs-core/package.json
+++ b/packages/ipfs-core/package.json
@@ -121,7 +121,7 @@
"delay": "^4.4.0",
"go-ipfs": "^0.7.0",
"interface-ipfs-core": "^0.142.3",
- "ipfsd-ctl": "^7.1.1",
+ "ipfsd-ctl": "^7.2.0",
"ipld-git": "^0.6.1",
"iso-url": "^1.0.0",
"nanoid": "^3.1.12",
diff --git a/packages/ipfs-core/src/runtime/config-browser.js b/packages/ipfs-core/src/runtime/config-browser.js
index 5793ac4acd..af04e1f652 100644
--- a/packages/ipfs-core/src/runtime/config-browser.js
+++ b/packages/ipfs-core/src/runtime/config-browser.js
@@ -7,6 +7,7 @@ module.exports = () => ({
Announce: [],
API: '',
Gateway: '',
+ RPC: '',
Delegates: [
'/dns4/node0.delegate.ipfs.io/tcp/443/https',
'/dns4/node1.delegate.ipfs.io/tcp/443/https',
diff --git a/packages/ipfs-core/src/runtime/config-nodejs.js b/packages/ipfs-core/src/runtime/config-nodejs.js
index 345ff72276..c93048cd82 100644
--- a/packages/ipfs-core/src/runtime/config-nodejs.js
+++ b/packages/ipfs-core/src/runtime/config-nodejs.js
@@ -9,6 +9,7 @@ module.exports = () => ({
Announce: [],
API: '/ip4/127.0.0.1/tcp/5002',
Gateway: '/ip4/127.0.0.1/tcp/9090',
+ RPC: '/ip4/127.0.0.1/tcp/5003',
Delegates: [
'/dns4/node0.delegate.ipfs.io/tcp/443/https',
'/dns4/node1.delegate.ipfs.io/tcp/443/https',
diff --git a/packages/ipfs-daemon/package.json b/packages/ipfs-daemon/package.json
index d6e055957c..5cbda33abc 100644
--- a/packages/ipfs-daemon/package.json
+++ b/packages/ipfs-daemon/package.json
@@ -32,6 +32,7 @@
"debug": "^4.1.1",
"dlv": "^1.1.3",
"ipfs-core": "^0.3.1",
+ "ipfs-grpc-server": "0.0.0",
"ipfs-http-client": "^48.1.3",
"ipfs-http-gateway": "^0.1.4",
"ipfs-http-server": "^0.1.4",
@@ -46,7 +47,8 @@
"devDependencies": {
"aegir": "^29.2.2",
"node-fetch": "^2.6.1",
- "typescript": "4.0.x"
+ "typescript": "4.0.x",
+ "ws": "^7.3.1"
},
"optionalDependencies": {
"prom-client": "^12.0.0",
diff --git a/packages/ipfs-daemon/src/index.js b/packages/ipfs-daemon/src/index.js
index 46b623f39c..8c36a2c435 100644
--- a/packages/ipfs-daemon/src/index.js
+++ b/packages/ipfs-daemon/src/index.js
@@ -11,6 +11,7 @@ const ipfsHttpClient = require('ipfs-http-client')
const IPFS = require('ipfs-core')
const HttpApi = require('ipfs-http-server')
const HttpGateway = require('ipfs-http-gateway')
+const gRPCServer = require('ipfs-grpc-server')
const createRepo = require('ipfs-core/src/runtime/repo-nodejs')
const { isElectron } = require('ipfs-utils/src/env')
@@ -59,6 +60,8 @@ class Daemon {
await repo.apiAddr.set(this._httpApi._apiServers[0].info.ma)
}
+ this._grpcServer = await gRPCServer(ipfs, ipfsOpts)
+
log('started')
return this
}
@@ -68,6 +71,7 @@ class Daemon {
await Promise.all([
this._httpApi && this._httpApi.stop(),
this._httpGateway && this._httpGateway.stop(),
+ this._grpcServer && this._grpcServer.stop(),
// @ts-ignore - may not have stop if init was false
this._ipfs && this._ipfs.stop()
])
diff --git a/packages/ipfs-daemon/test/index.spec.js b/packages/ipfs-daemon/test/index.spec.js
index 0b88f7ef2b..eb6cedb143 100644
--- a/packages/ipfs-daemon/test/index.spec.js
+++ b/packages/ipfs-daemon/test/index.spec.js
@@ -4,6 +4,7 @@
const { expect } = require('aegir/utils/chai')
const Daemon = require('../')
const fetch = require('node-fetch')
+const WebSocket = require('ws')
describe('daemon', () => {
let daemon
@@ -46,6 +47,41 @@ describe('daemon', () => {
await daemon.stop()
})
+ it('should start a gRPC server', async () => {
+ daemon = new Daemon({})
+
+ await daemon.start()
+
+ const {
+ uri
+ } = daemon._grpcServer.info
+
+ const socket = new WebSocket(`${uri}/ipfs.Root/id`)
+ let received = Buffer.alloc(0)
+
+ await new Promise((resolve) => {
+ socket.on('open', () => {
+ socket.send(Buffer.from('Y29udGVudC10eXBlOiBhcHBsaWNhdGlvbi9ncnBjLXdlYitwcm90bw0KeC1ncnBjLXdlYjogMQ0K', 'base64'))
+ socket.send(Buffer.from('AAAAAAAA', 'base64'))
+ })
+
+ socket.on('message', (data) => {
+ received = Buffer.concat([received, data], received.byteLength + data.byteLength)
+ })
+
+ socket.on('close', () => {
+ resolve()
+ })
+ })
+
+ const apiId = await daemon._ipfs.id()
+
+ // don't try to decode protobuf, just look for embedded string
+ expect(received.toString('utf8')).to.include(apiId.id)
+
+ await daemon.stop()
+ })
+
it('should stop', async () => {
daemon = new Daemon({})
diff --git a/packages/ipfs-daemon/tsconfig.json b/packages/ipfs-daemon/tsconfig.json
index f84b5f655c..2111e28905 100644
--- a/packages/ipfs-daemon/tsconfig.json
+++ b/packages/ipfs-daemon/tsconfig.json
@@ -14,6 +14,9 @@
{
"path": "../ipfs-core-utils"
},
+ {
+ "path": "../ipfs-grpc-server"
+ },
{
"path": "../ipfs-http-client"
},
diff --git a/packages/ipfs-grpc-client/.aegir.js b/packages/ipfs-grpc-client/.aegir.js
new file mode 100644
index 0000000000..4173cb261e
--- /dev/null
+++ b/packages/ipfs-grpc-client/.aegir.js
@@ -0,0 +1,7 @@
+'use strict'
+
+module.exports = {
+ bundlesize: {
+ maxSize: '48kB'
+ }
+}
diff --git a/packages/ipfs-grpc-client/README.md b/packages/ipfs-grpc-client/README.md
new file mode 100644
index 0000000000..1c2be2b0d9
--- /dev/null
+++ b/packages/ipfs-grpc-client/README.md
@@ -0,0 +1,57 @@
+# ipfs-grpc-client
+
+> A client for the [ipfs-grpc-server][] module
+
+This module implements part of the [IPFS Core API](https://github.com/ipfs/js-ipfs/tree/master/docs/core-api) using gRPC over websockets to achieve the bidirectional streaming necessary to have full duplex streams running in the browser.
+
+It's not recommended you use this directly, instead use the [ipfs-client](https://www.npmjs.com/package/ipfs-client) to combine this with the [ipfs-http-client](https://www.npmjs.com/package/ipfs-http-client) in order to have HTTP fallback for the missing parts of the API.
+
+## Why?
+
+The [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and [XHR](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) APIs do not allow for full-duplex streaming, that is, allowing the client to receive bytes from the response while also adding more bytes to the outgoing request.
+
+This limits what we can do in browsers in terms of the API, for example streaming arbitrarily sized payloads or exposing libp2p duplex streams.
+
+gPRC over websockets has no such limitations so allows us to harness the full power of a remote IPFS node in the browser without the need to work around browser behaviour.
+
+## Install
+
+```console
+$ npm install ipfs-grpc-client
+```
+
+## API
+
+### `createClient([options])`
+
+### Parameters
+
+None
+
+### Options
+
+An optional object which may have the following keys:
+
+| Name | Type | Default | Description |
+| ---- | ---- | ------- | ----------- |
+| url | `Multiaddr` or `string` or `URL` | `undefined` | The address of a [ipfs-grpc-server][] to connect to |
+
+### Returns
+
+| Type | Description |
+| -------- | -------- |
+| `object` | An instance of the client |
+
+### Example
+
+```js
+const createClient = require('ipfs-gprc-client')
+
+const client = createClient({
+ url: '/ipv4/127.0.0.1/tcp/1234/ws'
+})
+
+const id = await client.id()
+```
+
+[ipfs-grpc-server]: https://www.npmjs.com/package/ipfs-grpc-server
diff --git a/packages/ipfs-grpc-client/package.json b/packages/ipfs-grpc-client/package.json
new file mode 100644
index 0000000000..6d4b0e392b
--- /dev/null
+++ b/packages/ipfs-grpc-client/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "ipfs-grpc-client",
+ "version": "0.0.0",
+ "description": "A client library for the IPFS gRPC API",
+ "keywords": [
+ "ipfs"
+ ],
+ "homepage": "https://github.com/ipfs/js-ipfs/tree/master/packages/ipfs-grpc-client#readme",
+ "bugs": "https://github.com/ipfs/js-ipfs/issues",
+ "license": "(Apache-2.0 OR MIT)",
+ "leadMaintainer": "Alex Potsides ",
+ "files": [
+ "src",
+ "dist"
+ ],
+ "main": "src/index.js",
+ "browser": {
+ "./src/grpc/transport.js": "./src/grpc/transport.browser.js"
+ },
+ "typesVersions": {
+ "*": {
+ "*": [
+ "dist/*",
+ "dist/*/index"
+ ]
+ }
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/ipfs/js-ipfs.git"
+ },
+ "scripts": {
+ "test": "aegir test",
+ "lint": "aegir lint",
+ "build": "npm run build:js && npm run build:types",
+ "build:js": "aegir build",
+ "build:types": "tsc --build",
+ "coverage": "npx nyc -r html npm run test:node -- --bail",
+ "clean": "rimraf ./dist",
+ "dep-check": "aegir dep-check -i aegir -i typescript -i rimraf -i ipfs-grpc-protocol"
+ },
+ "dependencies": {
+ "@improbable-eng/grpc-web": "^0.13.0",
+ "change-case": "^4.1.1",
+ "cids": "^1.0.0",
+ "debug": "^4.1.1",
+ "err-code": "^2.0.3",
+ "ipfs-core-utils": "^0.5.0",
+ "ipfs-grpc-protocol": "0.0.0",
+ "it-first": "^1.0.4",
+ "it-pushable": "^1.4.0",
+ "protobufjs": "^6.10.2",
+ "multiaddr": "^8.0.0",
+ "wherearewe": "0.0.1",
+ "ws": "^7.3.1"
+ },
+ "devDependencies": {
+ "aegir": "^29.2.2",
+ "it-all": "^1.0.4",
+ "rimraf": "^3.0.2",
+ "sinon": "^9.0.3",
+ "typescript": "4.0.x"
+ }
+}
diff --git a/packages/ipfs-grpc-client/src/core-api/add-all.js b/packages/ipfs-grpc-client/src/core-api/add-all.js
new file mode 100644
index 0000000000..af11d3e4c5
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/core-api/add-all.js
@@ -0,0 +1,118 @@
+'use strict'
+
+const normaliseInput = require('ipfs-core-utils/src/files/normalise-input')
+const CID = require('cids')
+const bidiToDuplex = require('../utils/bidi-to-duplex')
+const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option')
+
+function sendDirectory (index, sink, path, mode, mtime) {
+ const message = {
+ index,
+ type: 'DIRECTORY',
+ path
+ }
+
+ if (mtime) {
+ message.mtime = mtime.secs
+ message.mtimeNsecs = mtime.nsecs
+ }
+
+ if (mode != null) {
+ message.mode = mode
+ }
+
+ sink.push(message)
+}
+
+async function sendFile (index, sink, content, path, mode, mtime) {
+ for await (const buf of content) {
+ const message = {
+ index,
+ type: 'FILE',
+ path
+ }
+
+ if (mtime) {
+ message.mtime = mtime.secs
+ message.mtimeNsecs = mtime.nsecs
+ }
+
+ if (mode != null) {
+ message.mode = mode
+ }
+
+ message.content = new Uint8Array(buf, buf.byteOffset, buf.byteLength)
+
+ sink.push(message)
+ }
+
+ // signal that the file data has finished
+ const message = {
+ index,
+ type: 'FILE',
+ path
+ }
+
+ sink.push(message)
+}
+
+async function sendFiles (stream, sink) {
+ let i = 1
+
+ for await (const { path, content, mode, mtime } of normaliseInput(stream)) {
+ const index = i
+ i++
+
+ if (content) {
+ await sendFile(index, sink, content, path, mode, mtime)
+ } else {
+ sendDirectory(index, sink, path, mode, mtime)
+ }
+ }
+}
+
+module.exports = function grpcAddAll (grpc, service, opts = {}) {
+ async function * addAll (stream, options = {}) {
+ const {
+ source,
+ sink
+ } = bidiToDuplex(grpc, service, {
+ host: opts.url,
+ debug: Boolean(process.env.DEBUG),
+ metadata: options
+ })
+
+ sendFiles(stream, sink)
+ .catch(err => {
+ sink.end(err)
+ })
+ .finally(() => {
+ sink.end()
+ })
+
+ for await (const result of source) {
+ // received progress result
+ if (result.type === 'PROGRESS') {
+ if (options.progress) {
+ options.progress(result.bytes, result.path)
+ }
+
+ continue
+ }
+
+ // received file/dir import result
+ yield {
+ path: result.path,
+ cid: new CID(result.cid),
+ mode: result.mode,
+ mtime: {
+ secs: result.mtime || 0,
+ nsecs: result.mtimeNsecs || 0
+ },
+ size: result.size
+ }
+ }
+ }
+
+ return withTimeoutOption(addAll)
+}
diff --git a/packages/ipfs-grpc-client/src/core-api/files/ls.js b/packages/ipfs-grpc-client/src/core-api/files/ls.js
new file mode 100644
index 0000000000..b3be56c0b0
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/core-api/files/ls.js
@@ -0,0 +1,33 @@
+'use strict'
+
+const CID = require('cids')
+const serverStreamToIterator = require('../../utils/server-stream-to-iterator')
+const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option')
+
+module.exports = function grpcMfsLs (grpc, service, opts = {}) {
+ async function * mfsLs (path, options = {}) {
+ const request = {
+ path
+ }
+
+ for await (const result of serverStreamToIterator(grpc, service, request, {
+ host: opts.url,
+ debug: Boolean(process.env.DEBUG),
+ metadata: options
+ })) {
+ yield {
+ name: result.name,
+ type: result.type.toLowerCase(),
+ size: result.size,
+ cid: new CID(result.cid),
+ mode: result.mode,
+ mtime: {
+ secs: result.mtime || 0,
+ nsecs: result.mtimeNsecs || 0
+ }
+ }
+ }
+ }
+
+ return withTimeoutOption(mfsLs)
+}
diff --git a/packages/ipfs-grpc-client/src/core-api/files/write.js b/packages/ipfs-grpc-client/src/core-api/files/write.js
new file mode 100644
index 0000000000..30856247a2
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/core-api/files/write.js
@@ -0,0 +1,46 @@
+'use strict'
+
+const clientStreamToPromise = require('../../utils/client-stream-to-promise')
+const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option')
+const normaliseInput = require('ipfs-core-utils/src/files/normalise-input')
+const { mtimeToObject, modeToNumber } = require('ipfs-core-utils/src/files/normalise-input/utils')
+
+async function * stream (path, content) {
+ for await (const { content: bufs } of normaliseInput(content)) {
+ if (!bufs) {
+ return
+ }
+
+ for await (const content of bufs) {
+ yield { path, content }
+ }
+ }
+}
+
+module.exports = function grpcMfsWrite (grpc, service, opts = {}) {
+ async function mfsWrite (path, content, options = {}) {
+ const mtime = mtimeToObject(options.mtime)
+
+ if (mtime != null) {
+ options = {
+ ...options,
+ mtime: mtime.secs,
+ mtimeNsecs: mtime.nsecs
+ }
+ }
+
+ const mode = modeToNumber(options.mode)
+
+ if (mode != null) {
+ options.mode = mode
+ }
+
+ await clientStreamToPromise(grpc, service, stream(path, content), {
+ host: opts.url,
+ debug: Boolean(process.env.DEBUG),
+ metadata: options
+ })
+ }
+
+ return withTimeoutOption(mfsWrite)
+}
diff --git a/packages/ipfs-grpc-client/src/core-api/id.js b/packages/ipfs-grpc-client/src/core-api/id.js
new file mode 100644
index 0000000000..b5486cbb68
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/core-api/id.js
@@ -0,0 +1,24 @@
+'use strict'
+
+const withTimeoutOption = require('ipfs-core-utils/src/with-timeout-option')
+const toHeaders = require('../utils/to-headers')
+const unaryToPromise = require('../utils/unary-to-promise')
+const multiaddr = require('multiaddr')
+
+module.exports = function grpcId (grpc, service, opts = {}) {
+ async function id (options = {}) {
+ const request = {}
+
+ const res = await unaryToPromise(grpc, service, request, {
+ host: opts.url,
+ metadata: toHeaders(options)
+ })
+
+ return {
+ ...res,
+ addresses: (res.addresses || []).map(multiaddr)
+ }
+ }
+
+ return withTimeoutOption(id)
+}
diff --git a/packages/ipfs-grpc-client/src/grpc/transport.browser.js b/packages/ipfs-grpc-client/src/grpc/transport.browser.js
new file mode 100644
index 0000000000..4981987221
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/grpc/transport.browser.js
@@ -0,0 +1,5 @@
+'use strict'
+
+const { grpc } = require('@improbable-eng/grpc-web')
+
+module.exports = grpc.WebsocketTransport
diff --git a/packages/ipfs-grpc-client/src/grpc/transport.js b/packages/ipfs-grpc-client/src/grpc/transport.js
new file mode 100644
index 0000000000..9fcea8d428
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/grpc/transport.js
@@ -0,0 +1,10 @@
+'use strict'
+
+const { isElectronRenderer } = require('wherearewe')
+
+// In electron-renderer we use the browser transport
+if (isElectronRenderer) {
+ module.exports = require('./transport.browser')
+} else {
+ module.exports = require('./transport.node')
+}
diff --git a/packages/ipfs-grpc-client/src/grpc/transport.node.js b/packages/ipfs-grpc-client/src/grpc/transport.node.js
new file mode 100644
index 0000000000..21a3dd53aa
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/grpc/transport.node.js
@@ -0,0 +1,125 @@
+'use strict'
+
+// copied from https://github.com/improbable-eng/grpc-web/blob/master/client/grpc-web/src/transports/websocket/websocket.ts
+// but uses the ws implementation of WebSockets
+// see: https://github.com/improbable-eng/grpc-web/issues/796
+
+const WebSocket = require('ws')
+const debug = require('debug')('ipfs:grpc-client:websocket-transport')
+
+const WebsocketSignal = {
+ FINISH_SEND: 1
+}
+
+const finishSendFrame = new Uint8Array([1])
+
+function WebsocketTransport () {
+ return (opts) => {
+ return websocketRequest(opts)
+ }
+}
+
+function websocketRequest (options) {
+ const webSocketAddress = constructWebSocketAddress(options.url)
+
+ let sendQueue = []
+ let ws
+
+ function sendToWebsocket (toSend) {
+ if (toSend === WebsocketSignal.FINISH_SEND) {
+ ws.send(finishSendFrame)
+ } else {
+ const byteArray = toSend
+ const c = new Int8Array(byteArray.byteLength + 1)
+ c.set(new Uint8Array([0]))
+ c.set(byteArray, 1)
+
+ ws.send(c)
+ }
+ }
+
+ return {
+ sendMessage: (msgBytes) => {
+ if (!ws || ws.readyState === ws.CONNECTING) {
+ sendQueue.push(msgBytes)
+ } else {
+ sendToWebsocket(msgBytes)
+ }
+ },
+ finishSend: () => {
+ if (!ws || ws.readyState === ws.CONNECTING) {
+ sendQueue.push(WebsocketSignal.FINISH_SEND)
+ } else {
+ sendToWebsocket(WebsocketSignal.FINISH_SEND)
+ }
+ },
+ start: (metadata) => {
+ ws = new WebSocket(webSocketAddress, ['grpc-websockets'])
+ ws.binaryType = 'arraybuffer'
+ ws.onopen = function () {
+ options.debug && debug('websocketRequest.onopen')
+ ws.send(headersToBytes(metadata))
+
+ // send any messages that were passed to sendMessage before the connection was ready
+ sendQueue.forEach(toSend => {
+ sendToWebsocket(toSend)
+ })
+ sendQueue = []
+ }
+
+ ws.onclose = function (closeEvent) {
+ options.onEnd()
+ }
+
+ ws.onerror = function (error) {
+ options.debug && debug('websocketRequest.onerror', error)
+ }
+
+ ws.onmessage = function (e) {
+ options.onChunk(new Uint8Array(e.data, 0, e.data.byteLength))
+ }
+ },
+ cancel: () => {
+ ws.close()
+ }
+ }
+}
+
+function constructWebSocketAddress (url) {
+ if (url.startsWith('wss://') || url.startsWith('ws://')) {
+ return url
+ } else if (url.substr(0, 8) === 'https://') {
+ return `wss://${url.substr(8)}`
+ } else if (url.substr(0, 7) === 'http://') {
+ return `ws://${url.substr(7)}`
+ }
+ throw new Error('Websocket transport constructed with non-https:// or http:// host.')
+}
+
+function headersToBytes (headers) {
+ let asString = ''
+ headers.forEach((key, values) => {
+ asString += `${key}: ${values.join(', ')}\r\n`
+ })
+ return encodeASCII(asString)
+}
+
+function encodeASCII (input) {
+ const encoded = new Uint8Array(input.length)
+ for (let i = 0; i !== input.length; ++i) {
+ const charCode = input.charCodeAt(i)
+ if (!isValidHeaderAscii(charCode)) {
+ throw new Error('Metadata contains invalid ASCII')
+ }
+ encoded[i] = charCode
+ }
+ return encoded
+}
+
+const isAllowedControlChars = (char) => char === 0x9 || char === 0xa || char === 0xd
+
+function isValidHeaderAscii (val) {
+ return isAllowedControlChars(val) || (val >= 0x20 && val <= 0x7e)
+}
+
+module.exports = WebsocketTransport
diff --git a/packages/ipfs-grpc-client/src/index.js b/packages/ipfs-grpc-client/src/index.js
new file mode 100644
index 0000000000..3b0c5d2088
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/index.js
@@ -0,0 +1,40 @@
+'use strict'
+
+const transport = require('./grpc/transport')
+const toUrlString = require('ipfs-core-utils/src/to-url-string')
+const loadServices = require('./utils/load-services')
+const { grpc } = require('@improbable-eng/grpc-web')
+grpc.setDefaultTransport(transport())
+
+const service = loadServices()
+
+const protocols = {
+ 'ws://': 'http://',
+ 'wss://': 'https://'
+}
+
+function normaliseUrls (opts) {
+ Object.keys(protocols).forEach(protocol => {
+ if (opts.url.startsWith(protocol)) {
+ opts.url = protocols[protocol] + opts.url.substring(protocol.length)
+ }
+ })
+}
+
+module.exports = function createClient (opts = {}) {
+ opts.url = toUrlString(opts.url)
+
+ // @improbable-eng/grpc-web requires http:// protocol URLs, not ws://
+ normaliseUrls(opts)
+
+ const client = {
+ addAll: require('./core-api/add-all')(grpc, service.Root.add, opts),
+ id: require('./core-api/id')(grpc, service.Root.id, opts),
+ files: {
+ ls: require('./core-api/files/ls')(grpc, service.MFS.ls, opts),
+ write: require('./core-api/files/write')(grpc, service.MFS.write, opts)
+ }
+ }
+
+ return client
+}
diff --git a/packages/ipfs-grpc-client/src/utils/bidi-to-duplex.js b/packages/ipfs-grpc-client/src/utils/bidi-to-duplex.js
new file mode 100644
index 0000000000..2bb0c16ab6
--- /dev/null
+++ b/packages/ipfs-grpc-client/src/utils/bidi-to-duplex.js
@@ -0,0 +1,69 @@
+'use strict'
+
+const pushable = require('it-pushable')
+const errCode = require('err-code')
+const toHeaders = require('./to-headers')
+
+async function sendMessages (service, client, source) {
+ for await (const obj of source) {
+ client.send({
+ serializeBinary: () => service.requestType.serializeBinary(obj)
+ })
+ }
+}
+
+/**
+ * Bidirectional streams are many-to-many operations so returns a sink
+ * for the caller to write client messages into and a source to read
+ * server messages from.
+ *
+ * @param {object} grpc - an @improbable-eng/grpc-web instance
+ * @param {object} service - an @improbable-eng/grpc-web service
+ * @param {object} options - RPC options
+ * @param {string} options.host - The remote host
+ * @param {boolean} [options.debug] - Whether to print debug messages
+ * @param {object} [options.metadata] - Metadata sent as headers
+ * @returns {{ source: AsyncIterable