diff --git a/.aegir.js b/.aegir.js
index 9aa1d126..af6f716d 100644
--- a/.aegir.js
+++ b/.aegir.js
@@ -1,28 +1,19 @@
-import * as ipfsModule from 'ipfs'
-import * as ipfsHttpModule from 'ipfs-http-client'
-import * as kuboRpcModule from 'kubo-rpc-client'
-import * as goIpfsModule from 'go-ipfs'
+import { create } from 'kubo-rpc-client'
+import { path } from 'kubo'
-/** @type {import('aegir').Options["build"]["config"]} */
+/** @type {import('aegir').PartialOptions} */
const config = {
- bundlesize: {
- maxSize: '35kB'
+ build: {
+ bundlesizeMax: '2.5kB',
},
test: {
before: async () => {
const { createServer } = await import('./dist/src/index.js')
const server = createServer(undefined, {
- ipfsModule,
- }, {
- go: {
- ipfsBin: goIpfsModule.path(),
- kuboRpcModule
- },
- js: {
- ipfsBin: ipfsModule.path(),
- ipfsHttpModule
- }
+ type: 'kubo',
+ bin: path(),
+ rpc: create
}
)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 0bc3b42d..d401a774 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,7 +5,7 @@ updates:
schedule:
interval: daily
time: "10:00"
- open-pull-requests-limit: 10
+ open-pull-requests-limit: 20
commit-message:
prefix: "deps"
prefix-development: "deps(dev)"
diff --git a/.github/workflows/js-test-and-release.yml b/.github/workflows/js-test-and-release.yml
index d31e0580..f7c04de0 100644
--- a/.github/workflows/js-test-and-release.yml
+++ b/.github/workflows/js-test-and-release.yml
@@ -19,10 +19,9 @@ concurrency:
jobs:
js-test-and-release:
- uses: ipdxco/unified-github-workflows/.github/workflows/js-test-and-release.yml@v1.0
+ uses: ipdxco/unified-github-workflows/.github/workflows/js-test-and-release.yml@v0.0
secrets:
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
UCI_GITHUB_TOKEN: ${{ secrets.UCI_GITHUB_TOKEN }}
- CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml
new file mode 100644
index 00000000..bd00f090
--- /dev/null
+++ b/.github/workflows/semantic-pull-request.yml
@@ -0,0 +1,12 @@
+name: Semantic PR
+
+on:
+ pull_request_target:
+ types:
+ - opened
+ - edited
+ - synchronize
+
+jobs:
+ main:
+ uses: pl-strflt/.github/.github/workflows/reusable-semantic-pull-request.yml@v0.3
diff --git a/.gitignore b/.gitignore
index 1531bdf9..9baf0602 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,8 @@
node_modules
+build
dist
.docs
.coverage
package-lock.json
yarn.lock
+.vscode
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..3662b370
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 8d835ead..578dcd5c 100644
--- a/README.md
+++ b/README.md
@@ -1,395 +1,173 @@
-# ipfsd-ctl
+# ipfsd-ctl
[![ipfs.tech](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.tech)
[![Discuss](https://img.shields.io/discourse/https/discuss.ipfs.tech/posts.svg?style=flat-square)](https://discuss.ipfs.tech)
[![codecov](https://img.shields.io/codecov/c/github/ipfs/js-ipfsd-ctl.svg?style=flat-square)](https://codecov.io/gh/ipfs/js-ipfsd-ctl)
[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/js-ipfsd-ctl/js-test-and-release.yml?branch=master\&style=flat-square)](https://github.com/ipfs/js-ipfsd-ctl/actions/workflows/js-test-and-release.yml?query=branch%3Amaster)
-> Spawn IPFS Daemons, JS or Go
-
-## Table of contents
-
-- [Install](#install)
- - [Browser `
-```
+
-If you are only using the `proc` type in-process IPFS node, you can skip installing `go-ipfs` and `ipfs-http-client`. (e.g. `npm i --save ipfs`)
+This module allows you to spawn long-lived IPFS implementations from any JS environment and interact with the as is they were in the local process.
-> You also need to explicitly defined the options `ipfsBin`, `ipfsModule` and `ipfsHttpModule` according to your needs. Check [ControllerOptions](#controlleroptions) and [ControllerOptionsOverrides](#controlleroptionsoverrides) for more information.
+It is designed mostly for testing interoperability and is not suitable for production use.
-## Usage
+## Spawning a single noder: `createNode`
-### Spawning a single IPFS controller: `createController`
+## Example - Spawning a Kubo node
-This is a shorthand for simpler use cases where factory is not needed.
+```TypeScript
+import { createNode } from 'ipfsd-ctl'
+import { path } from 'kubo'
+import { create } from 'kubo-rpc-client'
-```js
-// No need to create a factory when only a single controller is needed.
-// Use createController to spawn it instead.
-const Ctl = require('ipfsd-ctl')
-const ipfsd = await Ctl.createController({
- ipfsHttpModule,
- ipfsBin: goIpfsModule.path()
+const node = await createNode({
+ type: 'kubo',
+ rpc: create,
+ bin: path()
})
-const id = await ipfsd.api.id()
-
-console.log(id)
-await ipfsd.stop()
+console.info(await node.api.id())
```
-### Manage multiple IPFS controllers: `createFactory`
-
-Use a factory to spawn multiple controllers based on some common template.
-
-**Spawn an IPFS daemon from Node.js**
-
-```js
-// Create a factory to spawn two test disposable controllers, get access to an IPFS api
-// print node ids and clean all the controllers from the factory.
-const Ctl = require('ipfsd-ctl')
-
-const factory = Ctl.createFactory(
- {
- type: 'js',
- test: true,
- disposable: true,
- ipfsHttpModule,
- ipfsModule: (await import('ipfs')) // only if you gonna spawn 'proc' controllers
- },
- { // overrides per type
- js: {
- ipfsBin: ipfsModule.path()
- },
- go: {
- ipfsBin: goIpfsModule.path()
- }
- }
-)
-const ipfsd1 = await factory.spawn() // Spawns using options from `createFactory`
-const ipfsd2 = await factory.spawn({ type: 'go' }) // Spawns using options from `createFactory` but overrides `type` to spawn a `go` controller
-
-console.log(await ipfsd1.api.id())
-console.log(await ipfsd2.api.id())
-
-await factory.clean() // Clean all the controllers created by the factory calling `stop` on all of them.
-```
+## Manage multiple nodes: `createFactory`
-**Spawn an IPFS daemon from the Browser using the provided remote endpoint**
+Use a factory to spawn multiple nodes based on some common template.
-```js
-// Start a remote disposable node, and get access to the api
-// print the node id, and stop the temporary daemon
+## Example - Spawning multiple Kubo nodes
-const Ctl = require('ipfsd-ctl')
+```TypeScript
+import { createFactory } from 'ipfsd-ctl'
+import { path } from 'kubo'
+import { create } from 'kubo-rpc-client'
-const port = 9090
-const server = Ctl.createServer(port, {
- ipfsModule,
- ipfsHttpModule
-},
-{
- js: {
- ipfsBin: ipfsModule.path()
- },
- go: {
- ipfsBin: goIpfsModule.path()
- },
-})
-const factory = Ctl.createFactory({
- ipfsHttpModule,
- remote: true,
- endpoint: `http://localhost:${port}` // or you can set process.env.IPFSD_CTL_SERVER to http://localhost:9090
+const factory = createFactory({
+ type: 'kubo',
+ rpc: create,
+ bin: path()
})
-await server.start()
-const ipfsd = await factory.spawn()
-const id = await ipfsd.api.id()
+const node1 = await factory.spawn()
+const node2 = await factory.spawn()
+//...etc
-console.log(id)
-
-await ipfsd.stop()
-await server.stop()
+// later stop all nodes
+await factory.clean()
```
-## Disposable vs non Disposable nodes
-
-`ipfsd-ctl` can spawn `disposable` and `non-disposable` nodes.
-
-- `disposable`- Disposable nodes are useful for tests or other temporary use cases, by default they create a temporary repo and automatically initialise and start the node, plus they cleanup everything when stopped.
-- `non-disposable` - Non disposable nodes will by default attach to any nodes running on the default or the supplied repo. Requires the user to initialize and start the node, as well as stop and cleanup afterwards.
-
-## API
-
-### `createFactory([options], [overrides])`
-
-Creates a factory that can spawn multiple controllers and pre-define options for them.
-
-- `options` **[ControllerOptions](#controlleroptions)** Controllers options.
-- `overrides` **[ControllerOptionsOverrides](#controlleroptionsoverrides)** Pre-defined options overrides per controller type.
-
-Returns a **[Factory](#factory)**
-
-### `createController([options])`
-
-Creates a controller.
-
-- `options` **[ControllerOptions](#controlleroptions)** Factory options.
-
-Returns **Promise<[Controller](#controller)>**
-
-### `createServer([options])`
-
-Create an Endpoint Server. This server is used by a client node to control a remote node. Example: Spawning a go-ipfs node from a browser.
-
-- `options` **\[Object]** Factory options. Defaults to: `{ port: 43134 }`
- - `port` **number** Port to start the server on.
-
-Returns a **Server**
-
-### Factory
-
-#### `controllers`
-
-**Controller\[]** List of all the controllers spawned.
-
-#### `tmpDir()`
-
-Create a temporary repo to create controllers manually.
-
-Returns **Promise\** - Path to the repo.
-
-#### `spawn([options])`
-
-Creates a controller for a IPFS node.
-
-- `options` **[ControllerOptions](#controlleroptions)** Factory options.
-
-Returns **Promise<[Controller](#controller)>**
-
-#### `clean()`
-
-Cleans all controllers spawned.
-
-Returns **Promise<[Factory](#factory)>**
-
-### Controller
-
-Class controller for a IPFS node.
-
-#### `new Controller(options)`
-
-- `options` **[ControllerOptions](#controlleroptions)**
-
-#### `path`
-
-**String** Repo path.
+## Override config based on implementation type
-#### `exec`
+`createFactory` takes a second argument that can be used to pass default options to an implementation based on the `type` field.
-**String** Executable path.
+```TypeScript
+import { createFactory } from 'ipfsd-ctl'
+import { path } from 'kubo'
+import { create } from 'kubo-rpc-client'
-#### `env`
-
-**Object** ENV object.
-
-#### `initialized`
-
-**Boolean** Flag with the current init state.
-
-#### `started`
-
-**Boolean** Flag with the current start state.
-
-#### `clean`
-
-**Boolean** Flag with the current clean state.
-
-#### `apiAddr`
-
-**Multiaddr** API address
-
-#### `gatewayAddr`
-
-**Multiaddr** Gateway address
-
-#### `api`
-
-**Object** IPFS core interface
-
-#### `init([initOptions])`
-
-Initialises controlled node
-
-- `initOptions` **\[Object]** IPFS init options
-
-Returns **Promise<[Controller](#controller)>**
-
-#### `start()`
-
-Starts controlled node.
-
-Returns **Promise\**
-
-#### `stop()`
-
-Stops controlled node.
-
-Returns **Promise<[Controller](#controller)>**
-
-#### `cleanup()`
-
-Cleans controlled node, a disposable controller calls this automatically.
-
-Returns **Promise<[Controller](#controller)>**
-
-#### `pid()`
-
-Get the pid of the controlled node process if aplicable.
-
-Returns **Promise\**
-
-#### `version()`
-
-Get the version of the controlled node.
+const factory = createFactory({
+ type: 'kubo',
+ test: true
+}, {
+ otherImpl: {
+ //...other impl args
+ }
+})
-Returns **Promise\**
+const kuboNode = await factory.spawn()
+const otherImplNode = await factory.spawn({
+ type: 'otherImpl'
+})
+```
-### ControllerOptionsOverrides
+## Spawning nodes from browsers
-Type: \[Object]
+To spawn nodes from browsers, first start an ipfsd-ctl server from node.js and make the address known to the browser (the default way is to set `process.env.IPFSD_CTL_SERVER` in your bundle):
-#### Properties
+## Example - Create server
-- `js` **\[[ControllerOptions](#controlleroptions)]** Pre-defined defaults options for **JS** controllers these are deep merged with options passed to `Factory.spawn(options)`.
-- `go` **\[[ControllerOptions](#controlleroptions)]** Pre-defined defaults options for **Go** controllers these are deep merged with options passed to `Factory.spawn(options)`.
-- `proc` **\[[ControllerOptions](#controlleroptions)]** Pre-defined defaults options for **Proc** controllers these are deep merged with options passed to `Factory.spawn(options)`.
+In node.js:
-### ControllerOptions
+```TypeScript
+// Start a remote disposable node, and get access to the api
+// print the node id, and stop the temporary daemon
-Type: \[Object]
+import { createServer } from 'ipfsd-ctl'
-#### Properties
+const port = 9090
+const server = Ctl.createServer(port, {
+ type: 'kubo',
+ test: true
+}, {
+ // overrides
+})
+await server.start()
+```
-- `test` **\[boolean]** Flag to activate custom config for tests.
-- `remote` **\[boolean]** Use remote endpoint to spawn the nodes. Defaults to `true` when not in node.
-- `endpoint` **\[string]** Endpoint URL to manage remote Controllers. (Defaults: '').
-- `disposable` **\[boolean]** A new repo is created and initialized for each invocation, as well as cleaned up automatically once the process exits.
-- `type` **\[string]** The daemon type, see below the options:
- - go - spawn go-ipfs daemon
- - js - spawn js-ipfs daemon
- - proc - spawn in-process js-ipfs node
-- `env` **\[Object]** Additional environment variables, passed to executing shell. Only applies for Daemon controllers.
-- `args` **\[Array]** Custom cli args.
-- `ipfsHttpModule` **\[Object]** Reference to a IPFS HTTP Client object.
-- `ipfsModule` **\[Object]** Reference to a IPFS API object.
-- `ipfsBin` **\[string]** Path to a IPFS exectutable.
-- `ipfsOptions` **\[IpfsOptions]** Options for the IPFS instance same as . `proc` nodes receive these options as is, daemon nodes translate the options as far as possible to cli arguments.
-- `forceKill` **\[boolean]** - Whether to use SIGKILL to quit a daemon that does not stop after `.stop()` is called. (default `true`)
-- `forceKillTimeout` **\[Number]** - How long to wait before force killing a daemon in ms. (default `5000`)
+In a browser:
-## ipfsd-ctl environment variables
+```TypeScript
+import { createFactory } from 'ipfsd-ctl'
-In additional to the API described in previous sections, `ipfsd-ctl` also supports several environment variables. This are often very useful when running in different environments, such as CI or when doing integration/interop testing.
+const factory = createFactory({
+ // or you can set process.env.IPFSD_CTL_SERVER to http://localhost:9090
+ endpoint: `http://localhost:${port}`
+})
-*Environment variables precedence order is as follows. Top to bottom, top entry has highest precedence:*
+const node = await factory.createNode({
+ type: 'kubo'
+})
+console.info(await node.api.id())
+```
-- command line options/method arguments
-- env variables
-- default values
+## Disposable vs non Disposable nodes
-Meaning that, environment variables override defaults in the configuration file but are superseded by options to `df.spawn({...})`
+`ipfsd-ctl` can spawn `disposable` and `non-disposable` nodes.
-#### IPFS\_JS\_EXEC and IPFS\_GO\_EXEC
+- `disposable`- Disposable nodes are useful for tests or other temporary use cases, they create a temporary repo which is deleted automatically when the node is stopped
+- `non-disposable` - Disposable nodes will not delete their repo when stopped
-An alternative way of specifying the executable path for the `js-ipfs` or `go-ipfs` executable, respectively.
+# Install
-## Contribute
+```console
+$ npm i ipfsd-ctl
+```
-Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/js-ipfsd-ctl/issues)!
+## Browser `
+```
-## API Docs
+# API Docs
-
-## License
+# License
Licensed under either of
- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / )
- MIT ([LICENSE-MIT](LICENSE-MIT) / )
-## Contribute
+# Contribute
Contributions welcome! Please check out [the issues](https://github.com/ipfs/js-ipfsd-ctl/issues).
diff --git a/package.json b/package.json
index 8395172f..b72f6d28 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "ipfsd-ctl",
"version": "13.0.0",
- "description": "Spawn IPFS Daemons, JS or Go",
+ "description": "Spawn IPFS Daemons, Kubo or...",
"license": "Apache-2.0 OR MIT",
"homepage": "https://github.com/ipfs/js-ipfsd-ctl#readme",
"repository": {
@@ -11,15 +11,15 @@
"bugs": {
"url": "https://github.com/ipfs/js-ipfsd-ctl/issues"
},
+ "publishConfig": {
+ "access": "public",
+ "provenance": true
+ },
"keywords": [
"daemon",
"ipfs",
"node"
],
- "engines": {
- "node": ">=16.0.0",
- "npm": ">=7.0.0"
- },
"type": "module",
"types": "./dist/src/index.d.ts",
"files": [
@@ -37,6 +37,7 @@
"eslintConfig": {
"extends": "ipfs",
"parserOptions": {
+ "project": true,
"sourceType": "module"
}
},
@@ -140,34 +141,26 @@
"dependencies": {
"@hapi/boom": "^10.0.0",
"@hapi/hapi": "^21.1.0",
- "@libp2p/interface-peer-id": "^2.0.0",
- "@libp2p/logger": "^2.0.0",
- "@multiformats/multiaddr": "^11.0.0",
- "execa": "^6.1.0",
- "ipfs-utils": "^9.0.1",
+ "@libp2p/interface": "^1.2.0",
+ "@libp2p/logger": "^4.0.10",
+ "execa": "^8.0.1",
"joi": "^17.2.1",
+ "kubo-rpc-client": "^4.0.0",
"merge-options": "^3.0.1",
- "nanoid": "^4.0.0",
+ "nanoid": "^5.0.7",
+ "p-defer": "^4.0.1",
"p-wait-for": "^5.0.0",
- "temp-write": "^5.0.0",
"wherearewe": "^2.0.1"
},
"devDependencies": {
- "aegir": "^37.0.15",
- "go-ipfs": "^0.17.0",
- "ipfs": "^0.66.0",
- "ipfs-client": "^0.10.0",
- "ipfs-core-types": "^0.14.0",
- "ipfs-http-client": "^60.0.0",
- "kubo-rpc-client": "^3.0.0",
- "util": "^0.12.4"
+ "aegir": "^42.2.5",
+ "kubo": "^0.28.0"
},
"browser": {
"./dist/src/endpoint/server.js": "./dist/src/endpoint/server.browser.js",
- "./dist/src/utils.js": "./dist/src/utils.browser.js",
- "./dist/src/ipfsd-daemon.js": "./dist/src/ipfsd-client.js",
- "go-ipfs": false
+ "./dist/src/kubo/utils.js": false,
+ "./dist/src/kubo/daemon.js": "./dist/src/kubo/client.js",
+ "kubo": false
},
- "jsdelivr": "dist/index.min.js",
- "unpkg": "dist/index.min.js"
+ "sidEffects": false
}
diff --git a/src/config.ts b/src/config.ts
index d0672e73..d05037f8 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,24 +1,10 @@
-import { isBrowser, isWebWorker } from 'wherearewe'
-import type { ControllerType } from './index.js'
+import type { NodeType } from './index.js'
export interface ConfigInit {
- type?: ControllerType
+ type?: NodeType
}
-export default (init: ConfigInit) => {
- const { type } = init
- let swarm: string[]
-
- // from the browser tell remote nodes to listen over WS
- if (type !== 'proc' && (isBrowser || isWebWorker)) {
- swarm = ['/ip4/127.0.0.1/tcp/0/ws']
- // from the browser, in process nodes cannot listen on _any_ addrs
- } else if (type === 'proc' && (isBrowser || isWebWorker)) {
- swarm = []
- } else {
- swarm = ['/ip4/127.0.0.1/tcp/0']
- }
-
+export default (init: ConfigInit): any => {
return {
API: {
HTTPHeaders: {
@@ -31,7 +17,10 @@ export default (init: ConfigInit) => {
}
},
Addresses: {
- Swarm: swarm,
+ Swarm: [
+ '/ip4/127.0.0.1/tcp/0/ws',
+ '/ip4/127.0.0.1/tcp/0'
+ ],
API: '/ip4/127.0.0.1/tcp/0',
Gateway: '/ip4/127.0.0.1/tcp/0',
RPC: '/ip4/127.0.0.1/tcp/0'
diff --git a/src/endpoint/routes.ts b/src/endpoint/routes.ts
index ffa608fe..38d80e1f 100644
--- a/src/endpoint/routes.ts
+++ b/src/endpoint/routes.ts
@@ -1,10 +1,9 @@
-import { nanoid } from 'nanoid'
-import Joi from 'joi'
import boom from '@hapi/boom'
import { logger } from '@libp2p/logger'
-import { tmpDir } from '../utils.js'
+import Joi from 'joi'
+import { nanoid } from 'nanoid'
+import type { Node, Factory } from '../index.js'
import type { Server } from '@hapi/hapi'
-import type { Factory } from '../index.js'
const debug = logger('ipfsd-ctl:routes')
@@ -16,7 +15,7 @@ const routeOptions = {
}
}
-const badRequest = (err: Error & { stdout?: string }) => {
+const badRequest = (err: Error & { stdout?: string }): void => {
let msg
if (err.stdout != null) {
msg = err.stdout + ' - ' + err.message
@@ -27,30 +26,49 @@ const badRequest = (err: Error & { stdout?: string }) => {
throw boom.badRequest(msg)
}
-const nodes: Record = {}
+const nodes: Record = {}
export default (server: Server, createFactory: () => Factory | Promise): void => {
+ /**
+ * Spawn a controller
+ */
server.route({
- method: 'GET',
- path: '/util/tmp-dir',
+ method: 'POST',
+ path: '/spawn',
handler: async (request) => {
- const type = request.query.type ?? 'go'
+ const options: any = request.payload ?? {}
try {
- return { tmpDir: await tmpDir(type) }
+ const ipfsd = await createFactory()
+ const id = nanoid()
+ nodes[id] = await ipfsd.spawn({
+ ...options,
+ // init/start will be invoked by the client
+ init: false,
+ start: false
+ })
+
+ return {
+ id,
+ options,
+ info: await nodes[id].info()
+ }
} catch (err: any) {
badRequest(err)
}
}
})
+ /**
+ * Return node info
+ */
server.route({
method: 'GET',
- path: '/version',
+ path: '/info',
handler: async (request) => {
const id = request.query.id
try {
- return { version: await nodes[id].version() }
+ return await nodes[id].info()
} catch (err: any) {
badRequest(err)
}
@@ -58,36 +76,8 @@ export default (server: Server, createFactory: () => Factory | Promise)
options: routeOptions
})
- server.route({
- method: 'POST',
- path: '/spawn',
- handler: async (request) => {
- const opts = request.payload ?? {}
- try {
- const ipfsd = await createFactory()
- const id = nanoid()
- // @ts-expect-error opts is a json object
- nodes[id] = await ipfsd.spawn(opts)
- return {
- id: id,
- apiAddr: nodes[id].apiAddr?.toString(),
- gatewayAddr: nodes[id].gatewayAddr?.toString(),
- grpcAddr: nodes[id].grpcAddr?.toString(),
- initialized: nodes[id].initialized,
- started: nodes[id].started,
- disposable: nodes[id].disposable,
- env: nodes[id].env,
- path: nodes[id].path,
- clean: nodes[id].clean
- }
- } catch (err: any) {
- badRequest(err)
- }
- }
- })
-
/*
- * Initialize a repo.
+ * Initialize a repo
*/
server.route({
method: 'POST',
@@ -99,9 +89,7 @@ export default (server: Server, createFactory: () => Factory | Promise)
try {
await nodes[id].init(payload)
- return {
- initialized: nodes[id].initialized
- }
+ return await nodes[id].info()
} catch (err: any) {
badRequest(err)
}
@@ -110,22 +98,19 @@ export default (server: Server, createFactory: () => Factory | Promise)
})
/*
- * Start the daemon.
+ * Start the daemon
*/
server.route({
method: 'POST',
path: '/start',
handler: async (request) => {
const id = request.query.id
+ const payload = request.payload ?? {}
try {
- await nodes[id].start()
+ await nodes[id].start(payload)
- return {
- apiAddr: nodes[id].apiAddr?.toString(),
- gatewayAddr: nodes[id].gatewayAddr?.toString(),
- grpcAddr: nodes[id].grpcAddr?.toString()
- }
+ return await nodes[id].info()
} catch (err: any) {
badRequest(err)
}
@@ -134,18 +119,17 @@ export default (server: Server, createFactory: () => Factory | Promise)
})
/*
- * Delete the repo that was being used.
- * If the node was marked as `disposable` this will be called
- * automatically when the process is exited.
+ * Stop the daemon
*/
server.route({
method: 'POST',
- path: '/cleanup',
+ path: '/stop',
handler: async (request, h) => {
const id = request.query.id
+ const payload = request.payload ?? {}
try {
- await nodes[id].cleanup()
+ await nodes[id].stop(payload)
return h.response().code(200)
} catch (err: any) {
@@ -156,16 +140,19 @@ export default (server: Server, createFactory: () => Factory | Promise)
})
/*
- * Stop the daemon.
+ * Delete the repo that was being used.
+ * If the node was marked as `disposable` this will be called
+ * automatically when the process is exited.
*/
server.route({
method: 'POST',
- path: '/stop',
+ path: '/cleanup',
handler: async (request, h) => {
const id = request.query.id
+ const payload = request.payload ?? {}
try {
- await nodes[id].stop()
+ await nodes[id].cleanup(payload)
return h.response().code(200)
} catch (err: any) {
@@ -174,18 +161,4 @@ export default (server: Server, createFactory: () => Factory | Promise)
},
options: routeOptions
})
-
- /*
- * Get the pid of the `ipfs daemon` process.
- */
- server.route({
- method: 'GET',
- path: '/pid',
- handler: async (request) => {
- const id = request.query.id
-
- return { pid: await nodes[id].pid() }
- },
- options: routeOptions
- })
}
diff --git a/src/endpoint/server.browser.ts b/src/endpoint/server.browser.ts
index 3592fe8e..be5dfd5a 100644
--- a/src/endpoint/server.browser.ts
+++ b/src/endpoint/server.browser.ts
@@ -20,10 +20,8 @@ class Server {
/**
* Start the server
- *
- * @returns {Promise}
*/
- async start () {
+ async start (): Promise {
console.warn('Server not implemented in the browser')
return this
@@ -31,10 +29,8 @@ class Server {
/**
* Stop the server
- *
- * @returns {Promise}
*/
- async stop () {
+ async stop (): Promise {
console.warn('Server not implemented in the browser')
}
}
diff --git a/src/endpoint/server.ts b/src/endpoint/server.ts
index af406400..61c52358 100644
--- a/src/endpoint/server.ts
+++ b/src/endpoint/server.ts
@@ -1,6 +1,10 @@
import Hapi from '@hapi/hapi'
-import type { CreateFactory } from '../index.js'
import routes from './routes.js'
+import type { Factory } from '../index.js'
+
+interface CreateFactory {
+ (): Factory
+}
export interface ServerInit {
port?: number
@@ -31,7 +35,7 @@ class Server {
async start (port = this.port): Promise {
this.port = port
this.server = new Hapi.Server({
- port: port,
+ port,
host: this.host,
routes: {
cors: true
@@ -48,7 +52,7 @@ class Server {
/**
* Stop the server
*/
- async stop (options: { timeout: number }): Promise {
+ async stop (options?: { timeout: number }): Promise {
if (this.server != null) {
await this.server.stop(options)
}
diff --git a/src/factory.ts b/src/factory.ts
index 1b98bf13..8800342b 100644
--- a/src/factory.ts
+++ b/src/factory.ts
@@ -1,149 +1,97 @@
import mergeOptions from 'merge-options'
-import { tmpDir } from './utils.js'
import { isNode, isElectronMain } from 'wherearewe'
-import http from 'ipfs-utils/src/http.js'
-import ControllerDaemon from './ipfsd-daemon.js'
-import ControllerRemote from './ipfsd-client.js'
-import ControllerProc from './ipfsd-in-proc.js'
-import testsConfig from './config.js'
-import type { Controller, ControllerOptions, ControllerOptionsOverrides, Factory } from './index.js'
+import KuboClient from './kubo/client.js'
+import KuboDaemon from './kubo/daemon.js'
+import type { Node, NodeOptions, NodeOptionsOverrides, NodeType, Factory, KuboNode, KuboOptions, SpawnOptions } from './index.js'
const merge = mergeOptions.bind({ ignoreUndefined: true })
const defaults = {
remote: !isNode && !isElectronMain,
- endpoint: process.env.IPFSD_CTL_SERVER ?? 'http://localhost:43134',
disposable: true,
test: false,
- type: 'go',
+ type: 'kubo',
env: {},
args: [],
- ipfsOptions: {},
forceKill: true,
forceKillTimeout: 5000
}
-export interface ControllerOptionsOverridesWithEndpoint {
- js?: ControllerOptionsWithEndpoint
- go?: ControllerOptionsWithEndpoint
- proc?: ControllerOptionsWithEndpoint
-}
-
-export interface ControllerOptionsWithEndpoint extends ControllerOptions {
- endpoint: string
+export interface FactoryInit extends NodeOptions {
+ /**
+ * Endpoint URL to manage remote Nodes. (Defaults: 'http://127.0.0.1:43134')
+ */
+ endpoint?: string
}
/**
* Factory class to spawn ipfsd controllers
*/
-class DefaultFactory implements Factory {
- public opts: ControllerOptionsWithEndpoint
- public controllers: Controller[]
+class DefaultFactory implements Factory {
+ public options: NodeOptions
+ public controllers: Node[]
+ public readonly overrides: NodeOptionsOverrides
- private readonly overrides: ControllerOptionsOverridesWithEndpoint
+ private readonly endpoint: string
- constructor (options: ControllerOptions = {}, overrides: ControllerOptionsOverrides = {}) {
- this.opts = merge(defaults, options)
+ constructor (options: FactoryInit = {}, overrides: NodeOptionsOverrides = {}) {
+ this.endpoint = options.endpoint ?? process.env.IPFSD_CTL_SERVER ?? 'http://localhost:43134'
+ this.options = merge(defaults, options)
this.overrides = merge({
- js: merge(this.opts, { type: 'js' }),
- go: merge(this.opts, { type: 'go' }),
- proc: merge(this.opts, { type: 'proc' })
+ kubo: this.options
}, overrides)
this.controllers = []
}
/**
- * Utility method to get a temporary directory
- * useful in browsers to be able to generate temp
- * repos manually
+ * Spawn an IPFSd Node
*/
- async tmpDir (options: ControllerOptions = {}): Promise {
- const opts: ControllerOptions = merge(this.opts, options)
-
- if (opts.remote === true) {
- const res = await http.get(
- `${opts.endpoint ?? ''}/util/tmp-dir`,
- { searchParams: new URLSearchParams({ type: opts.type ?? '' }) }
- )
- const out = await res.json()
-
- return out.tmpDir
- }
-
- return await Promise.resolve(tmpDir(opts.type))
- }
-
- async _spawnRemote (options: ControllerOptionsWithEndpoint) {
- const opts = {
- json: {
- ...options,
- // avoid recursive spawning
- remote: false,
- ipfsBin: undefined,
- ipfsModule: undefined,
- ipfsHttpModule: undefined,
- kuboRpcModule: undefined
+ async spawn (options?: KuboOptions & SpawnOptions): Promise
+ async spawn (options?: NodeOptions & SpawnOptions): Promise {
+ const type: NodeType = options?.type ?? this.options.type ?? 'kubo'
+ const opts = merge({}, this.options, this.overrides[type], options)
+ let ctl: any
+
+ if (type === 'kubo') {
+ if (opts.remote === true) {
+ const req = await fetch(`${this.endpoint}/spawn`, {
+ method: 'POST',
+ headers: {
+ 'content-type': 'application/json'
+ },
+ body: JSON.stringify({
+ ...opts,
+ remote: false
+ })
+ })
+ const result = await req.json()
+
+ ctl = new KuboClient({
+ endpoint: this.endpoint,
+ ...opts,
+ ...result
+ })
+ } else {
+ ctl = new KuboDaemon(opts)
}
}
- const res = await http.post(
- `${options.endpoint}/spawn`,
- opts
- )
- return new ControllerRemote(
- options.endpoint,
- await res.json(),
- options
- )
- }
-
- /**
- * Spawn an IPFSd Controller
- */
- async spawn (options: ControllerOptions = { }): Promise {
- const type = options.type ?? this.opts.type ?? 'go'
- const opts: ControllerOptionsWithEndpoint = merge(
- this.overrides[type],
- options
- )
-
- // IPFS options defaults
- const ipfsOptions = merge(
- {
- start: false,
- init: false
- },
- opts.test === true
- ? {
- config: testsConfig(opts),
- preload: { enabled: false }
- }
- : {},
- opts.ipfsOptions
- )
-
- let ctl: Controller
- if (opts.type === 'proc') {
- // spawn in-proc controller
- ctl = new ControllerProc({ ...opts, ipfsOptions })
- } else if (opts.remote === true) {
- // spawn remote controller
- ctl = await this._spawnRemote({ ...opts, ipfsOptions })
- } else {
- // spawn daemon controller
- ctl = new ControllerDaemon({ ...opts, ipfsOptions })
+ if (ctl == null) {
+ throw new Error('Unsupported type')
}
// Save the controller
this.controllers.push(ctl)
- // Auto init and start controller
- if (opts.disposable === true && (options.ipfsOptions == null || options.ipfsOptions?.init !== false)) {
- await ctl.init(ipfsOptions.init)
+ // Auto start controller
+ if (opts.init !== false) {
+ await ctl.init(opts.init)
}
- if (opts.disposable === true && (options.ipfsOptions == null || options.ipfsOptions?.start !== false)) {
- await ctl.start()
+
+ // Auto start controller
+ if (opts.start !== false) {
+ await ctl.start(opts.start)
}
return ctl
@@ -153,7 +101,10 @@ class DefaultFactory implements Factory {
* Stop all controllers
*/
async clean (): Promise {
- await Promise.all(this.controllers.map(async n => await n.stop()))
+ await Promise.all(
+ this.controllers.map(async n => n.stop())
+ )
+
this.controllers = []
}
}
diff --git a/src/index.ts b/src/index.ts
index 6cde7e7e..87151be2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,70 +1,161 @@
-import DefaultFactory from './factory.js'
+/**
+ * @packageDocumentation
+ *
+ * This module allows you to spawn long-lived IPFS implementations from any JS environment and interact with the as is they were in the local process.
+ *
+ * It is designed mostly for testing interoperability and is not suitable for production use.
+ *
+ * ## Spawning a single noder: `createNode`
+ *
+ * @example Spawning a Kubo node
+ *
+ * ```TypeScript
+ * import { createNode } from 'ipfsd-ctl'
+ * import { path } from 'kubo'
+ * import { create } from 'kubo-rpc-client'
+ *
+ * const node = await createNode({
+ * type: 'kubo',
+ * rpc: create,
+ * bin: path()
+ * })
+ *
+ * console.info(await node.api.id())
+ * ```
+ *
+ * ## Manage multiple nodes: `createFactory`
+ *
+ * Use a factory to spawn multiple nodes based on some common template.
+ *
+ * @example Spawning multiple Kubo nodes
+ *
+ * ```TypeScript
+ * import { createFactory } from 'ipfsd-ctl'
+ * import { path } from 'kubo'
+ * import { create } from 'kubo-rpc-client'
+ *
+ * const factory = createFactory({
+ * type: 'kubo',
+ * rpc: create,
+ * bin: path()
+ * })
+ *
+ * const node1 = await factory.spawn()
+ * const node2 = await factory.spawn()
+ * //...etc
+ *
+ * // later stop all nodes
+ * await factory.clean()
+ * ```
+ *
+ * ## Override config based on implementation type
+ *
+ * `createFactory` takes a second argument that can be used to pass default options to an implementation based on the `type` field.
+ *
+ * ```TypeScript
+ * import { createFactory } from 'ipfsd-ctl'
+ * import { path } from 'kubo'
+ * import { create } from 'kubo-rpc-client'
+ *
+ * const factory = createFactory({
+ * type: 'kubo',
+ * test: true
+ * }, {
+ * otherImpl: {
+ * //...other impl args
+ * }
+ * })
+ *
+ * const kuboNode = await factory.spawn()
+ * const otherImplNode = await factory.spawn({
+ * type: 'otherImpl'
+ * })
+ * ```
+ *
+ * ## Spawning nodes from browsers
+ *
+ * To spawn nodes from browsers, first start an ipfsd-ctl server from node.js and make the address known to the browser (the default way is to set `process.env.IPFSD_CTL_SERVER` in your bundle):
+ *
+ * @example Create server
+ *
+ * In node.js:
+ *
+ * ```TypeScript
+ * // Start a remote disposable node, and get access to the api
+ * // print the node id, and stop the temporary daemon
+ *
+ * import { createServer } from 'ipfsd-ctl'
+ *
+ * const port = 9090
+ * const server = Ctl.createServer(port, {
+ * type: 'kubo',
+ * test: true
+ * }, {
+ * // overrides
+ * })
+ * await server.start()
+ * ```
+ *
+ * In a browser:
+ *
+ * ```TypeScript
+ * import { createFactory } from 'ipfsd-ctl'
+ *
+ * const factory = createFactory({
+ * // or you can set process.env.IPFSD_CTL_SERVER to http://localhost:9090
+ * endpoint: `http://localhost:${port}`
+ * })
+ *
+ * const node = await factory.createNode({
+ * type: 'kubo'
+ * })
+ * console.info(await node.api.id())
+ * ```
+ *
+ * ## Disposable vs non Disposable nodes
+ *
+ * `ipfsd-ctl` can spawn `disposable` and `non-disposable` nodes.
+ *
+ * - `disposable`- Disposable nodes are useful for tests or other temporary use cases, they create a temporary repo which is deleted automatically when the node is stopped
+ * - `non-disposable` - Disposable nodes will not delete their repo when stopped
+ */
+
import Server from './endpoint/server.js'
-import type { IPFS } from 'ipfs-core-types'
-import type { Multiaddr } from '@multiformats/multiaddr'
-import type { PeerId } from '@libp2p/interface-peer-id'
-import type { ExecaChildProcess } from 'execa'
-
-export interface PeerData {
- id: PeerId
- addresses: Multiaddr[]
-}
+import DefaultFactory from './factory.js'
+import type { KuboNode, KuboOptions } from './kubo/index.js'
-export type ControllerType = 'js' | 'go' | 'proc'
+export * from './kubo/index.js'
+export type NodeType = 'kubo'
-export interface Controller {
- /**
- * Initialize a repo
- */
- init: (options?: InitOptions) => Promise>
+export interface Node = Record, InitArgs = unknown, StartArgs = unknown, StopArgs = unknown, CleanupArgs = unknown> {
+ api: API
+ options: Options
/**
- * Start the daemon
+ * Return information about a node
*/
- start: () => Promise>
+ info(): Promise
/**
- * Stop the daemon
+ * Perform any pre-start tasks such as creating a repo, generating a peer id,
+ * etc
*/
- stop: () => Promise>
+ init(args?: InitArgs): Promise
/**
- * Delete the repo that was being used.
- * If the node was marked as `disposable` this will be called
- * automatically when the process is exited.
+ * Start the node
*/
- cleanup: () => Promise>
+ start(args?: StartArgs): Promise
/**
- * Get the pid of the `ipfs daemon` process
+ * Stop a node that has previously been started
*/
- pid: () => Promise
+ stop(args?: StopArgs): Promise
/**
- * Get the version of ipfs
+ * Perform any resource cleanup after stopping a disposable node
*/
- version: () => Promise
- path: string
- started: boolean
- initialized: boolean
- clean: boolean
- api: IPFSAPI
- subprocess?: ExecaChildProcess | null
- opts: ControllerOptions
- apiAddr: Multiaddr
- peer: PeerData
-}
-
-export interface RemoteState {
- id: string
- path: string
- initialized: boolean
- started: boolean
- disposable: boolean
- clean: boolean
- apiAddr: string
- gatewayAddr: string
- grpcAddr: string
+ cleanup(args?: CleanupArgs): Promise
}
export interface InitOptions {
@@ -97,180 +188,113 @@ export interface CircuitRelayOptions {
hop: CircuitRelayHopOptions
}
-export interface IPFSOptions {
- /**
- * The file path at which to store the IPFS node’s data. Alternatively, you can set up a customized storage system by providing an ipfs.Repo instance.
- */
- repo?: string | any
- /**
- * Initialize the repo when creating the IPFS node. Instead of a boolean, you may provide an object with custom initialization options. https://github.com/ipfs/js-ipfs/blob/master/README.md#optionsinit
- */
- init?: boolean | InitOptions
- /**
- * If false, do not automatically start the IPFS node. Instead, you’ll need to manually call node.start() yourself.
- */
- start?: boolean
- /**
- * A passphrase to encrypt/decrypt your keys.
- */
- pass?: string
- /**
- * Prevents all logging output from the IPFS node.
- */
- silent?: boolean
- /**
- * Configure circuit relay. https://github.com/ipfs/js-ipfs/blob/master/README.md#optionsrelay
- */
- relay?: any
- /**
- * Configure remote preload nodes. The remote will preload content added on this node, and also attempt to preload objects requested by this node. https://github.com/ipfs/js-ipfs/blob/master/README.md#optionspreload
- */
- preload?: boolean | PreloadOptions
- /**
- * Enable and configure experimental features. https://github.com/ipfs/js-ipfs/blob/master/README.md#optionsexperimental
- */
- EXPERIMENTAL?: ExperimentalOptions
+export interface NodeOptions {
/**
- * Modify the default IPFS node config. This object will be merged with the default config; it will not replace it. The default config is documented in the js-ipfs config file docs. https://github.com/ipfs/js-ipfs/blob/master/README.md#optionsconfig
+ * The type of controller
*/
- config?: any
- /**
- * Modify the default IPLD config. This object will be merged with the default config; it will not replace it. Check IPLD docs for more information on the available options. https://github.com/ipfs/js-ipfs/blob/master/README.md#optionsipld
- */
- ipld?: any
- /**
- * The libp2p option allows you to build your libp2p node by configuration, or via a bundle function. https://github.com/ipfs/js-ipfs/blob/master/README.md#optionslibp2p
- */
- libp2p?: any
- /**
- * Configure the libp2p connection manager. https://github.com/ipfs/js-ipfs/blob/master/README.md#optionsconnectionmanager
- */
- connectionManager?: any
- /**
- * Run the node offline
- */
- offline?: boolean
- /**
- * Perform any required repo migrations
- */
- repoAutoMigrate?: boolean
-}
+ type?: NodeType
-export interface ControllerOptions {
/**
* Flag to activate custom config for tests
*/
test?: boolean
+
/**
- * Use remote endpoint to spawn the controllers. Defaults to `true` when not in node
- */
- remote?: boolean
- /**
- * Endpoint URL to manage remote Controllers. (Defaults: 'http://localhost:43134')
- */
- endpoint?: string
- /**
- * A new repo is created and initialized for each invocation, as well as cleaned up automatically once the process exits
+ * A new repo is created and initialized for each invocation, as well as
+ * cleaned up automatically once the process exits
*/
disposable?: boolean
+
/**
- * The daemon type
- */
- type?: Type
- /**
- * Additional environment variables, passed to executing shell. Only applies for Daemon controllers
+ * Additional environment variables, passed to executing shell. Only applies
+ * for Daemon controllers
*/
env?: Record
+
/**
* Custom cli args
*/
args?: string[]
+
/**
- * Reference to an ipfs-http-client module
+ * How long to wait before force killing a daemon in ms
+ *
+ * @default 5000
*/
- ipfsHttpModule?: any
+ forceKillTimeout?: number
+
/**
- * Reference to a kubo-rpc-client module
+ * Init options
*/
- kuboRpcModule?: any
+ init?: InitOptions
+
/**
- * Reference to an ipfs or ipfs-core module
+ * Start options
*/
- ipfsModule?: any
+ start?: StartOptions
+}
+
+export interface NodeOptionsOverrides {
+ kubo?: KuboOptions
+}
+
+export interface SpawnOptions {
/**
- * Reference to an ipfs-core module
+ * Use remote endpoint to spawn the controllers. Defaults to `true` when not in node
*/
- ipfsClientModule?: any
+ remote?: true
+}
+
+export interface Factory {
/**
- * Path to a IPFS executable
+ * Create a node
*/
- ipfsBin?: string
+ spawn(options?: KuboOptions & SpawnOptions): Promise
+ spawn(options?: NodeOptions & SpawnOptions): Promise
+
/**
- * Options for the IPFS node
+ * Shut down all previously created nodes that are still running
*/
- ipfsOptions?: IPFSOptions
+ clean(): Promise
+
/**
- * Whether to use SIGKILL to quit a daemon that does not stop after `.stop()` is called. (default true)
+ * The previously created nodes that are still running
*/
- forceKill?: boolean
+ controllers: Node[]
+
/**
- * How long to wait before force killing a daemon in ms. (default 5000)
+ * The default options that will be applied to all nodes
*/
- forceKillTimeout?: number
-}
+ options: NodeOptions
-export interface ControllerOptionsOverrides {
- js?: ControllerOptions<'js'>
- go?: ControllerOptions<'go'>
- proc?: ControllerOptions<'proc'>
-}
-
-export interface Factory {
- tmpDir: (options?: ControllerOptions) => Promise
- spawn: (options?: ControllerOptions) => Promise>
- clean: () => Promise
- controllers: Array>
- opts: ControllerOptions
+ /**
+ * Config overrides that will be applied to specific node types
+ */
+ overrides: NodeOptionsOverrides
}
-export interface CreateFactory { (): Factory | Promise }
-
/**
* Creates a factory
- *
- * @param {ControllerOptions} [options]
- * @param {ControllerOptionsOverrides} [overrides]
- * @returns {Factory}
*/
-export const createFactory = (options?: ControllerOptions, overrides?: ControllerOptionsOverrides): Factory => {
+export function createFactory (options: KuboOptions, overrides?: NodeOptionsOverrides): Factory
+export function createFactory (options?: NodeOptions, overrides?: NodeOptionsOverrides): Factory
+export function createFactory (options?: NodeOptions, overrides?: NodeOptionsOverrides): Factory {
return new DefaultFactory(options, overrides)
}
/**
* Creates a node
*/
-export const createController = async (options?: ControllerOptions): Promise => {
+export async function createNode (options: KuboOptions & SpawnOptions): Promise
+export async function createNode (options?: any): Promise {
const f = new DefaultFactory()
- return await f.spawn(options)
-}
-
-export interface IPFSAPI extends IPFS {
- apiHost?: string
- apiPort?: number
- gatewayHost?: string
- gatewayPort?: number
- grpcHost?: string
- grpcPort?: number
+ return f.spawn(options)
}
/**
* Create a Endpoint Server
- *
- * @param {number | { port: number }} [options] - Configuration options or just the port.
- * @param {ControllerOptions} [factoryOptions]
- * @param {ControllerOptionsOverrides} [factoryOverrides]
*/
-export const createServer = (options?: number | { port: number }, factoryOptions: ControllerOptions = {}, factoryOverrides: ControllerOptionsOverrides = {}) => {
+export const createServer = (options?: number | { port: number }, factoryOptions: NodeOptions = {}, factoryOverrides: NodeOptionsOverrides = {}): Server => {
let port: number | undefined
if (typeof options === 'number') {
diff --git a/src/ipfsd-client.ts b/src/ipfsd-client.ts
deleted file mode 100644
index 7f75ced3..00000000
--- a/src/ipfsd-client.ts
+++ /dev/null
@@ -1,228 +0,0 @@
-import { Multiaddr, multiaddr } from '@multiformats/multiaddr'
-import http from 'ipfs-utils/src/http.js'
-import mergeOptions from 'merge-options'
-import { logger } from '@libp2p/logger'
-import type { Controller, ControllerOptions, InitOptions, IPFSAPI, PeerData, RemoteState } from './index.js'
-
-const merge = mergeOptions.bind({ ignoreUndefined: true })
-
-const daemonLog = {
- info: logger('ipfsd-ctl:client:stdout'),
- err: logger('ipfsd-ctl:client:stderr')
-}
-const rpcModuleLogger = logger('ipfsd-ctl:client')
-
-/**
- * Controller for remote nodes
- */
-class Client implements Controller {
- public path: string
- // @ts-expect-error set during startup
- public api: IPFSAPI
- public subprocess: null
- public opts: ControllerOptions
- public initialized: boolean
- public started: boolean
- public clean: boolean
- // @ts-expect-error set during startup
- public apiAddr: Multiaddr
-
- private readonly baseUrl: string
- private readonly id: string
- private readonly disposable: boolean
- private gatewayAddr?: Multiaddr
- private grpcAddr?: Multiaddr
- private _peerId: PeerData | null
-
- constructor (baseUrl: string, remoteState: RemoteState, options: ControllerOptions) {
- this.opts = options
- this.baseUrl = baseUrl
- this.id = remoteState.id
- this.path = remoteState.path
- this.initialized = remoteState.initialized
- this.started = remoteState.started
- this.disposable = remoteState.disposable
- this.clean = remoteState.clean
- this.subprocess = null
-
- this._setApi(remoteState.apiAddr)
- this._setGateway(remoteState.gatewayAddr)
- this._setGrpc(remoteState.grpcAddr)
- this._createApi()
- this._peerId = null
- }
-
- get peer () {
- if (this._peerId == null) {
- throw new Error('Not started')
- }
-
- return this._peerId
- }
-
- private _setApi (addr: string): void {
- if (addr != null) {
- this.apiAddr = multiaddr(addr)
- }
- }
-
- private _setGateway (addr: string): void {
- if (addr != null) {
- this.gatewayAddr = multiaddr(addr)
- }
- }
-
- private _setGrpc (addr: string): void {
- if (addr != null) {
- this.grpcAddr = multiaddr(addr)
- }
- }
-
- private _createApi (): void {
- if (this.opts.ipfsClientModule != null && this.grpcAddr != null && this.apiAddr != null) {
- this.api = this.opts.ipfsClientModule.create({
- grpc: this.grpcAddr,
- http: this.apiAddr
- })
- } else if (this.apiAddr != null) {
- if (this.opts.kuboRpcModule != null) {
- rpcModuleLogger('Using kubo-rpc-client')
- this.api = this.opts.kuboRpcModule.create(this.apiAddr)
- } else if (this.opts.ipfsHttpModule != null) {
- rpcModuleLogger('Using ipfs-http-client')
- this.api = this.opts.ipfsHttpModule.create(this.apiAddr)
- } else {
- throw new Error('You must pass either a kuboRpcModule or ipfsHttpModule')
- }
- }
-
- if (this.api != null) {
- if (this.apiAddr != null) {
- this.api.apiHost = this.apiAddr.nodeAddress().address
- this.api.apiPort = this.apiAddr.nodeAddress().port
- }
-
- if (this.gatewayAddr != null) {
- this.api.gatewayHost = this.gatewayAddr.nodeAddress().address
- this.api.gatewayPort = this.gatewayAddr.nodeAddress().port
- }
-
- if (this.grpcAddr != null) {
- this.api.grpcHost = this.grpcAddr.nodeAddress().address
- this.api.grpcPort = this.grpcAddr.nodeAddress().port
- }
- }
- }
-
- async init (initOptions: InitOptions = {}): Promise {
- if (this.initialized) {
- return this
- }
-
- let ipfsOptions = {}
-
- if (this.opts.ipfsOptions?.init != null && !(typeof this.opts.ipfsOptions.init === 'boolean')) {
- ipfsOptions = this.opts.ipfsOptions.init
- }
-
- const opts = merge(
- {
- emptyRepo: false,
- profiles: this.opts.test === true ? ['test'] : []
- },
- ipfsOptions,
- typeof initOptions === 'boolean' ? {} : initOptions
- )
-
- const req = await http.post(
- `${this.baseUrl}/init`,
- {
- searchParams: new URLSearchParams({ id: this.id }),
- json: opts
- }
- )
- const rsp = await req.json()
- this.initialized = rsp.initialized
- this.clean = false
- return this
- }
-
- async cleanup (): Promise {
- if (this.clean) {
- return this
- }
-
- await http.post(
- `${this.baseUrl}/cleanup`,
- { searchParams: new URLSearchParams({ id: this.id }) }
- )
- this.clean = true
- return this
- }
-
- async start (): Promise {
- if (!this.started) {
- const req = await http.post(
- `${this.baseUrl}/start`,
- { searchParams: new URLSearchParams({ id: this.id }) }
- )
- const res = await req.json()
-
- this._setApi(res.apiAddr)
- this._setGateway(res.gatewayAddr)
- this._setGrpc(res.grpcAddr)
- this._createApi()
-
- this.started = true
- }
-
- if (this.api == null) {
- throw new Error('api was not set')
- }
-
- // Add `peerId`
- const id = await this.api.id()
- this._peerId = id
- daemonLog.info(id)
- return this
- }
-
- async stop (): Promise {
- if (!this.started) {
- return this
- }
-
- await http.post(
- `${this.baseUrl}/stop`,
- { searchParams: new URLSearchParams({ id: this.id }) }
- )
- this.started = false
-
- if (this.disposable) {
- await this.cleanup()
- }
-
- return this
- }
-
- async pid (): Promise {
- const req = await http.get(
- `${this.baseUrl}/pid`,
- { searchParams: new URLSearchParams({ id: this.id }) }
- )
- const res = await req.json()
-
- return res.pid
- }
-
- async version (): Promise {
- const req = await http.get(
- `${this.baseUrl}/version`,
- { searchParams: new URLSearchParams({ id: this.id }) }
- )
- const res = await req.json()
- return res.version
- }
-}
-
-export default Client
diff --git a/src/ipfsd-daemon.ts b/src/ipfsd-daemon.ts
deleted file mode 100644
index bea3fced..00000000
--- a/src/ipfsd-daemon.ts
+++ /dev/null
@@ -1,441 +0,0 @@
-import { Multiaddr, multiaddr } from '@multiformats/multiaddr'
-import fs from 'fs/promises'
-import mergeOptions from 'merge-options'
-import { logger } from '@libp2p/logger'
-import { execa, ExecaChildProcess } from 'execa'
-import { nanoid } from 'nanoid'
-import path from 'path'
-import os from 'os'
-import { checkForRunningApi, repoExists, tmpDir, defaultRepo, buildInitArgs, buildStartArgs } from './utils.js'
-import waitFor from 'p-wait-for'
-import type { Controller, ControllerOptions, InitOptions, IPFSAPI, PeerData } from './index.js'
-
-const merge = mergeOptions.bind({ ignoreUndefined: true })
-
-const daemonLog = {
- info: logger('ipfsd-ctl:daemon:stdout'),
- err: logger('ipfsd-ctl:daemon:stderr')
-}
-const rpcModuleLogger = logger('ipfsd-ctl:daemon')
-
-function translateError (err: Error & { stdout: string, stderr: string }) {
- // get the actual error message to be the err.message
- err.message = `${err.stdout} \n\n ${err.stderr} \n\n ${err.message} \n\n`
-
- return err
-}
-
-interface TranslateUnknownErrorArgs {
- err: Error | unknown
- stdout: string
- stderr: string
- nameFallback?: string
- messageFallback?: string
-}
-
-function translateUnknownError ({ err, stdout, stderr, nameFallback = 'Unknown Error', messageFallback = 'Unknown Error Message' }: TranslateUnknownErrorArgs) {
- const error: Error = err as Error
- const name = error?.name ?? nameFallback
- const message = error?.message ?? messageFallback
- return translateError({
- name,
- message,
- stdout,
- stderr
- })
-}
-
-/**
- * Controller for daemon nodes
- */
-class Daemon implements Controller {
- public path: string
- // @ts-expect-error set during startup
- public api: IPFSAPI
- public subprocess?: ExecaChildProcess
- public opts: ControllerOptions
- public initialized: boolean
- public started: boolean
- public clean: boolean
- // @ts-expect-error set during startup
- public apiAddr: Multiaddr
-
- private gatewayAddr?: Multiaddr
- private grpcAddr?: Multiaddr
- private readonly exec?: string
- private readonly env: Record
- private readonly disposable: boolean
- private _peerId: PeerData | null
-
- constructor (opts: ControllerOptions) {
- this.opts = opts
- this.path = this.opts.ipfsOptions?.repo ?? (opts.disposable === true ? tmpDir(opts.type) : defaultRepo(opts.type))
- this.exec = this.opts.ipfsBin
- this.env = merge({ IPFS_PATH: this.path }, this.opts.env)
- this.disposable = Boolean(this.opts.disposable)
- this.initialized = false
- this.started = false
- this.clean = true
- this._peerId = null
- }
-
- get peer () {
- if (this._peerId == null) {
- throw new Error('Not started')
- }
-
- return this._peerId
- }
-
- private _setApi (addr: string): void {
- this.apiAddr = multiaddr(addr)
- }
-
- private _setGrpc (addr: string): void {
- this.grpcAddr = multiaddr(addr)
- }
-
- private _setGateway (addr: string): void {
- this.gatewayAddr = multiaddr(addr)
- }
-
- _createApi () {
- if (this.opts.ipfsClientModule != null && this.grpcAddr != null) {
- this.api = this.opts.ipfsClientModule.create({
- grpc: this.grpcAddr,
- http: this.apiAddr
- })
- } else if (this.apiAddr != null) {
- if (this.opts.kuboRpcModule != null) {
- rpcModuleLogger('Using kubo-rpc-client')
- this.api = this.opts.kuboRpcModule.create(this.apiAddr)
- } else if (this.opts.ipfsHttpModule != null) {
- rpcModuleLogger('Using ipfs-http-client')
- this.api = this.opts.ipfsHttpModule.create(this.apiAddr)
- } else {
- throw new Error('You must pass either a kuboRpcModule or ipfsHttpModule')
- }
- }
-
- if (this.api == null) {
- throw new Error(`Could not create API from http '${this.apiAddr.toString()}' and/or gRPC '${this.grpcAddr?.toString() ?? 'undefined'}'`)
- }
-
- if (this.apiAddr != null) {
- this.api.apiHost = this.apiAddr.nodeAddress().address
- this.api.apiPort = this.apiAddr.nodeAddress().port
- }
-
- if (this.gatewayAddr != null) {
- this.api.gatewayHost = this.gatewayAddr.nodeAddress().address
- this.api.gatewayPort = this.gatewayAddr.nodeAddress().port
- }
-
- if (this.grpcAddr != null) {
- this.api.grpcHost = this.grpcAddr.nodeAddress().address
- this.api.grpcPort = this.grpcAddr.nodeAddress().port
- }
- }
-
- async init (initOptions: InitOptions = {}): Promise {
- this.initialized = await repoExists(this.path)
- if (this.initialized) {
- this.clean = false
- return this
- }
-
- initOptions = merge({
- emptyRepo: false,
- profiles: this.opts.test === true ? ['test'] : []
- },
- typeof this.opts.ipfsOptions?.init === 'boolean' ? {} : this.opts.ipfsOptions?.init,
- typeof initOptions === 'boolean' ? {} : initOptions
- )
-
- const opts = merge(
- this.opts, {
- ipfsOptions: {
- init: initOptions
- }
- }
- )
-
- const args = buildInitArgs(opts)
-
- if (this.exec == null) {
- throw new Error('No executable specified')
- }
-
- const { stdout, stderr } = await execa(this.exec, args, {
- env: this.env
- })
- .catch(translateError)
-
- daemonLog.info(stdout)
- daemonLog.err(stderr)
-
- // default-config only for Go
- if (this.opts.type === 'go') {
- await this._replaceConfig(merge(
- await this._getConfig(),
- this.opts.ipfsOptions?.config
- ))
- }
-
- this.clean = false
- this.initialized = true
-
- return this
- }
-
- /**
- * Delete the repo that was being used. If the node was marked as disposable this will be called automatically when the process is exited.
- *
- * @returns {Promise}
- */
- async cleanup () {
- if (!this.clean) {
- await fs.rm(this.path, {
- recursive: true
- })
- this.clean = true
- }
- return this
- }
-
- /**
- * Start the daemon.
- *
- * @returns {Promise}
- */
- async start () {
- // Check if a daemon is already running
- const api = checkForRunningApi(this.path)
-
- if (api != null) {
- this._setApi(api)
- this._createApi()
- } else if (this.exec == null) {
- throw new Error('No executable specified')
- } else {
- const args = buildStartArgs(this.opts)
-
- let output = ''
-
- const ready = new Promise((resolve, reject) => {
- if (this.exec == null) {
- return reject(new Error('No executable specified'))
- }
-
- this.subprocess = execa(this.exec, args, {
- env: this.env
- })
-
- const { stdout, stderr } = this.subprocess
-
- if (stderr == null) {
- throw new Error('stderr was not defined on subprocess')
- }
-
- if (stdout == null) {
- throw new Error('stderr was not defined on subprocess')
- }
-
- stderr.on('data', data => daemonLog.err(data.toString()))
- stdout.on('data', data => daemonLog.info(data.toString()))
-
- const readyHandler = (data: Buffer) => {
- output += data.toString()
- const apiMatch = output.trim().match(/API .*listening on:? (.*)/)
- const gwMatch = output.trim().match(/Gateway .*listening on:? (.*)/)
- const grpcMatch = output.trim().match(/gRPC .*listening on:? (.*)/)
-
- if ((apiMatch != null) && apiMatch.length > 0) {
- this._setApi(apiMatch[1])
- }
-
- if ((gwMatch != null) && gwMatch.length > 0) {
- this._setGateway(gwMatch[1])
- }
-
- if ((grpcMatch != null) && grpcMatch.length > 0) {
- this._setGrpc(grpcMatch[1])
- }
-
- if (output.match(/(?:daemon is running|Daemon is ready)/) != null) {
- // we're good
- this._createApi()
- this.started = true
- stdout.off('data', readyHandler)
- resolve(this.api)
- }
- }
- stdout.on('data', readyHandler)
- this.subprocess.catch(err => reject(translateError(err)))
- void this.subprocess.on('exit', () => {
- this.started = false
- stderr.removeAllListeners()
- stdout.removeAllListeners()
-
- if (this.disposable) {
- this.cleanup().catch(() => {})
- }
- })
- })
-
- await ready
- }
-
- this.started = true
- // Add `peerId`
- const id = await this.api.id()
- this._peerId = id
-
- return this
- }
-
- async stop (options: { timeout?: number } = {}): Promise {
- const timeout = options.timeout ?? 60000
-
- if (!this.started) {
- return this
- }
-
- if (this.subprocess != null) {
- /** @type {ReturnType | undefined} */
- let killTimeout
- const subprocess = this.subprocess
-
- if (this.disposable) {
- // we're done with this node and will remove it's repo when we are done
- // so don't wait for graceful exit, just terminate the process
- this.subprocess.kill('SIGKILL')
- } else {
- if (this.opts.forceKill !== false) {
- killTimeout = setTimeout(() => {
- // eslint-disable-next-line no-console
- console.error(new Error(`Timeout stopping ${this.opts.type ?? 'unknown'} node after ${this.opts.forceKillTimeout ?? 'unknown'}ms. Process ${subprocess.pid ?? 'unknown'} will be force killed now.`))
- this.subprocess?.kill('SIGKILL')
- }, this.opts.forceKillTimeout)
- }
-
- this.subprocess.cancel()
- }
-
- // wait for the subprocess to exit and declare ourselves stopped
- await waitFor(() => !this.started, {
- timeout
- })
-
- if (killTimeout != null) {
- clearTimeout(killTimeout)
- }
-
- if (this.disposable) {
- // wait for the cleanup routine to run after the subprocess has exited
- await waitFor(() => this.clean, {
- timeout
- })
- }
- } else {
- await this.api.stop()
-
- this.started = false
- }
-
- return this
- }
-
- /**
- * Get the pid of the `ipfs daemon` process.
- *
- * @returns {Promise}
- */
- async pid () {
- if (this.subprocess?.pid != null) {
- return await Promise.resolve(this.subprocess?.pid)
- }
- throw new Error('Daemon process is not running.')
- }
-
- /**
- * Call `ipfs config`
- *
- * If no `key` is passed, the whole config is returned as an object.
- *
- * @private
- * @param {string} [key] - A specific config to retrieve.
- * @returns {Promise