From 20b0f351aa49b9a02f57dca27351f0078f897245 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Thu, 22 Dec 2022 16:08:02 +0000 Subject: [PATCH 01/18] feat: initial implementation - Adds modules for a core library, a cli and an RPC client/server - Creates a `~/.helia` directory on startup - `helia daemon` starts a node with an RPC-over-libp2p server - `helia id` prints out node information online or offline --- README.md | 61 +++- packages/cli/.aegir.js | 6 + packages/cli/LICENSE | 4 + packages/cli/LICENSE-APACHE | 5 + packages/cli/LICENSE-MIT | 19 ++ packages/cli/README.md | 44 +++ packages/cli/package.json | 171 ++++++++++ packages/cli/src/commands/cat.ts | 40 +++ packages/cli/src/commands/daemon.ts | 34 ++ packages/cli/src/commands/id.ts | 16 + packages/cli/src/commands/index.ts | 98 ++++++ packages/cli/src/commands/init.ts | 140 ++++++++ packages/cli/src/index.ts | 147 +++++++++ packages/cli/src/utils/create-helia.ts | 53 +++ packages/cli/src/utils/find-helia.ts | 69 ++++ packages/cli/src/utils/multiaddr-to-url.ts | 22 ++ packages/cli/src/utils/print-help.ts | 25 ++ packages/cli/test/index.spec.ts | 7 + packages/cli/tsconfig.json | 27 ++ packages/helia/LICENSE | 4 + packages/helia/LICENSE-APACHE | 5 + packages/helia/LICENSE-MIT | 19 ++ packages/helia/README.md | 53 +++ packages/helia/package.json | 156 +++++++++ packages/helia/src/commands/cat.ts | 26 ++ packages/helia/src/commands/id.ts | 18 + packages/helia/src/index.ts | 85 +++++ packages/helia/test/index.spec.ts | 8 + packages/helia/tsconfig.json | 15 + packages/interface/LICENSE | 4 + packages/interface/LICENSE-APACHE | 5 + packages/interface/LICENSE-MIT | 19 ++ packages/interface/README.md | 53 +++ packages/interface/package.json | 169 ++++++++++ packages/interface/src/errors.ts | 29 ++ packages/interface/src/index.ts | 82 +++++ packages/interface/tsconfig.json | 10 + packages/rpc-client/.aegir.js | 6 + packages/rpc-client/LICENSE | 4 + packages/rpc-client/LICENSE-APACHE | 5 + packages/rpc-client/LICENSE-MIT | 19 ++ packages/rpc-client/README.md | 53 +++ packages/rpc-client/package.json | 153 +++++++++ packages/rpc-client/src/commands/cat.ts | 2 + packages/rpc-client/src/commands/id.ts | 46 +++ packages/rpc-client/src/index.ts | 19 ++ packages/rpc-client/tsconfig.json | 18 + packages/rpc-protocol/LICENSE | 4 + packages/rpc-protocol/LICENSE-APACHE | 5 + packages/rpc-protocol/LICENSE-MIT | 19 ++ packages/rpc-protocol/README.md | 53 +++ packages/rpc-protocol/package.json | 175 ++++++++++ packages/rpc-protocol/src/index.ts | 16 + packages/rpc-protocol/src/root.proto | 24 ++ packages/rpc-protocol/src/root.ts | 312 ++++++++++++++++++ packages/rpc-protocol/src/rpc.proto | 22 ++ packages/rpc-protocol/src/rpc.ts | 215 ++++++++++++ packages/rpc-protocol/tsconfig.json | 11 + packages/rpc-server/.aegir.js | 6 + packages/rpc-server/LICENSE | 4 + packages/rpc-server/LICENSE-APACHE | 5 + packages/rpc-server/LICENSE-MIT | 19 ++ packages/rpc-server/README.md | 53 +++ packages/rpc-server/package.json | 156 +++++++++ packages/rpc-server/src/handlers/id.ts | 33 ++ packages/rpc-server/src/index.ts | 156 +++++++++ .../rpc-server/src/utils/multiaddr-to-url.ts | 22 ++ packages/rpc-server/tsconfig.json | 18 + packages/unixfs/LICENSE | 4 + packages/unixfs/LICENSE-APACHE | 5 + packages/unixfs/LICENSE-MIT | 19 ++ packages/unixfs/README.md | 53 +++ packages/unixfs/package.json | 152 +++++++++ packages/unixfs/src/index.ts | 58 ++++ packages/unixfs/tsconfig.json | 15 + 75 files changed, 3690 insertions(+), 17 deletions(-) create mode 100644 packages/cli/.aegir.js create mode 100644 packages/cli/LICENSE create mode 100644 packages/cli/LICENSE-APACHE create mode 100644 packages/cli/LICENSE-MIT create mode 100644 packages/cli/README.md create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/commands/cat.ts create mode 100644 packages/cli/src/commands/daemon.ts create mode 100644 packages/cli/src/commands/id.ts create mode 100644 packages/cli/src/commands/index.ts create mode 100644 packages/cli/src/commands/init.ts create mode 100644 packages/cli/src/index.ts create mode 100644 packages/cli/src/utils/create-helia.ts create mode 100644 packages/cli/src/utils/find-helia.ts create mode 100644 packages/cli/src/utils/multiaddr-to-url.ts create mode 100644 packages/cli/src/utils/print-help.ts create mode 100644 packages/cli/test/index.spec.ts create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/helia/LICENSE create mode 100644 packages/helia/LICENSE-APACHE create mode 100644 packages/helia/LICENSE-MIT create mode 100644 packages/helia/README.md create mode 100644 packages/helia/package.json create mode 100644 packages/helia/src/commands/cat.ts create mode 100644 packages/helia/src/commands/id.ts create mode 100644 packages/helia/src/index.ts create mode 100644 packages/helia/test/index.spec.ts create mode 100644 packages/helia/tsconfig.json create mode 100644 packages/interface/LICENSE create mode 100644 packages/interface/LICENSE-APACHE create mode 100644 packages/interface/LICENSE-MIT create mode 100644 packages/interface/README.md create mode 100644 packages/interface/package.json create mode 100644 packages/interface/src/errors.ts create mode 100644 packages/interface/src/index.ts create mode 100644 packages/interface/tsconfig.json create mode 100644 packages/rpc-client/.aegir.js create mode 100644 packages/rpc-client/LICENSE create mode 100644 packages/rpc-client/LICENSE-APACHE create mode 100644 packages/rpc-client/LICENSE-MIT create mode 100644 packages/rpc-client/README.md create mode 100644 packages/rpc-client/package.json create mode 100644 packages/rpc-client/src/commands/cat.ts create mode 100644 packages/rpc-client/src/commands/id.ts create mode 100644 packages/rpc-client/src/index.ts create mode 100644 packages/rpc-client/tsconfig.json create mode 100644 packages/rpc-protocol/LICENSE create mode 100644 packages/rpc-protocol/LICENSE-APACHE create mode 100644 packages/rpc-protocol/LICENSE-MIT create mode 100644 packages/rpc-protocol/README.md create mode 100644 packages/rpc-protocol/package.json create mode 100644 packages/rpc-protocol/src/index.ts create mode 100644 packages/rpc-protocol/src/root.proto create mode 100644 packages/rpc-protocol/src/root.ts create mode 100644 packages/rpc-protocol/src/rpc.proto create mode 100644 packages/rpc-protocol/src/rpc.ts create mode 100644 packages/rpc-protocol/tsconfig.json create mode 100644 packages/rpc-server/.aegir.js create mode 100644 packages/rpc-server/LICENSE create mode 100644 packages/rpc-server/LICENSE-APACHE create mode 100644 packages/rpc-server/LICENSE-MIT create mode 100644 packages/rpc-server/README.md create mode 100644 packages/rpc-server/package.json create mode 100644 packages/rpc-server/src/handlers/id.ts create mode 100644 packages/rpc-server/src/index.ts create mode 100644 packages/rpc-server/src/utils/multiaddr-to-url.ts create mode 100644 packages/rpc-server/tsconfig.json create mode 100644 packages/unixfs/LICENSE create mode 100644 packages/unixfs/LICENSE-APACHE create mode 100644 packages/unixfs/LICENSE-MIT create mode 100644 packages/unixfs/README.md create mode 100644 packages/unixfs/package.json create mode 100644 packages/unixfs/src/index.ts create mode 100644 packages/unixfs/tsconfig.json diff --git a/README.md b/README.md index af2120a1..1e834f5a 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,72 @@ -

An implementation of IPFS in JavaScript.

+# helia-monorepo -### Project status +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) -This project is pre-alpha and is planned for development in late 2022, with an initial "v1" release in [late Q1 2023](/ROADMAP.md#late-q1-march). The project will be built in the open. Community contributors are welcome. +> An implementation of IPFS in JavaScript -## Table of Contents +## Table of contents -- [Name](#name) +- [Structure](#structure) + - [Project status](#project-status) +- [What's in a name?](#whats-in-a-name) - [Background](#background) - [Roadmap](#roadmap) -- [Contribute](#contribute) +- [API Docs](#api-docs) - [License](#license) - - [Contribution](#contribution) +- [Contribute](#contribute) + +## Structure -## Name +- [`/packages/cli`](./packages/cli) Run a Helia node on the CLI +- [`/packages/helia`](./packages/helia) An implementation of IPFS in JavaScript +- [`/packages/interface`](./packages/interface) The Helia API +- [`/packages/rpc-client`](./packages/rpc-client) An implementation of IPFS in JavaScript +- [`/packages/rpc-protocol`](./packages/rpc-protocol) gRPC protocol for use by @helia/rpc-client and @helia/rpc-server +- [`/packages/rpc-server`](./packages/rpc-server) An implementation of IPFS in JavaScript +- [`/packages/unixfs`](./packages/unixfs) A Helia-compatible wrapper for UnixFS + +### Project status + +This project is pre-alpha and is planned for development in late 2022, with an initial "v1" release in [late Q1 2023](/ROADMAP.md#late-q1-march). The project will be built in the open. Community contributors are welcome. -Helia (_HEE-lee-ah_) is the Latin spelling of Ἡλιη -- in Greek mythology, one of the [Heliades](https://www.wikidata.org/wiki/Q12656412): the daughters of the sun god Helios. When their brother Phaethon died trying to drive the sun chariot across the sky, their tears of mourning fell to earth as amber, which is yellow (sort of), and so is JavaScript. They were then turned into [poplar](https://en.wiktionary.org/wiki/poplar) trees and, well, JavaScript is quite popular. +## What's in a name? + +Helia (*HEE-lee-ah*) is the Latin spelling of Ἡλιη -- in Greek mythology, one of the [Heliades](https://www.wikidata.org/wiki/Q12656412): the daughters of the sun god Helios. When their brother Phaethon died trying to drive the sun chariot across the sky, their tears of mourning fell to earth as amber, which is yellow (sort of), and so is JavaScript. They were then turned into [poplar](https://en.wiktionary.org/wiki/poplar) trees and, well, JavaScript is quite popular. In Oct–Dec 2022, IP Stewards [sought](https://github.com/ipfs/pomegranate/issues/3) community input for the name of this project. After considering 20 suggestions and holding a couple of polls, the name **Helia** was chosen. Here's [why it's not named IPFS](https://github.com/ipfs/ipfs/issues/470). ## Background -This project aims to build a lean, modular, modern implementation of IPFS, the Interplanetary File System. +This project aims to build a lean, modular, and modern implementation of IPFS, the Interplanetary File System. For more information, see the [State of IPFS in JS (blog post)](https://blog.ipfs.tech/state-of-ipfs-in-js/). ## Roadmap -The roadmap can be found here: https://github.com/ipfs/pomegranate/issues/5 +The roadmap can be found here: -## Contribute +## API Docs + +- -This IPFS implementation in JavaScript is a work in progress. [Here are some ways you can help](https://blog.ipfs.tech/state-of-ipfs-in-js/#%F0%9F%A4%9D-ways-you-can-help). - ## License Licensed under either of - * Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / http://www.apache.org/licenses/LICENSE-2.0) - * MIT ([LICENSE-MIT](LICENSE-MIT) / http://opensource.org/licenses/MIT) +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). -### Contribution +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/cli/.aegir.js b/packages/cli/.aegir.js new file mode 100644 index 00000000..e9c18f3e --- /dev/null +++ b/packages/cli/.aegir.js @@ -0,0 +1,6 @@ + +export default { + build: { + bundle: false + } +} diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/cli/LICENSE-APACHE b/packages/cli/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/cli/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/cli/LICENSE-MIT b/packages/cli/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/cli/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 00000000..78c05b48 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,44 @@ +# @helia/cli + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> Run a Helia node on the CLI + +## Table of contents + +- [Install](#install) +- [API Docs](#api-docs) +- [License](#license) +- [Contribute](#contribute) + +## Install + +```console +$ npm i @helia/cli +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 00000000..86671dd0 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,171 @@ +{ + "name": "@helia/cli", + "version": "0.0.0", + "description": "Run a Helia node on the CLI", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/cli#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "bin": { + "helia": "./dist/src/index.js" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:node": "aegir test -t node --cov", + "release": "aegir release" + }, + "dependencies": { + "@chainsafe/libp2p-gossipsub": "^5.3.0", + "@chainsafe/libp2p-noise": "^10.2.0", + "@chainsafe/libp2p-yamux": "^3.0.3", + "@helia/interface": "~0.0.0", + "@helia/rpc-client": "~0.0.0", + "@helia/rpc-server": "~0.0.0", + "@helia/unixfs": "~0.0.0", + "@libp2p/kad-dht": "^6.1.1", + "@libp2p/logger": "^2.0.2", + "@libp2p/mplex": "^7.1.1", + "@libp2p/peer-id": "^1.1.18", + "@libp2p/peer-id-factory": "^1.0.20", + "@libp2p/prometheus-metrics": "1.1.3", + "@libp2p/tcp": "^6.0.8", + "@libp2p/websockets": "^5.0.2", + "@libp2p/webtransport": "^1.0.6", + "@multiformats/multiaddr": "^11.1.4", + "@ucans/ucans": "^0.11.0-alpha", + "blockstore-datastore-adapter": "^4.0.0", + "datastore-fs": "^8.0.0", + "helia": "~0.0.0", + "libp2p": "next", + "multiformats": "^10.0.3", + "strip-json-comments": "^5.0.0", + "uint8arrays": "^4.0.2" + }, + "devDependencies": { + "aegir": "^37.7.5" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/cli/src/commands/cat.ts b/packages/cli/src/commands/cat.ts new file mode 100644 index 00000000..6d7dbda3 --- /dev/null +++ b/packages/cli/src/commands/cat.ts @@ -0,0 +1,40 @@ +import { CID } from 'multiformats' +import type { Command } from './index.js' + +interface CatArgs { + positionals?: string[] + offset?: string + length?: string +} + +export const cat: Command = { + description: 'Fetch and cat an IPFS path referencing a file', + example: '$ helia cat ', + offline: true, + options: { + offset: { + description: 'Where to start reading the file from', + type: 'string', + short: 'o' + }, + length: { + description: 'How many bytes to read from the file', + type: 'string', + short: 'l' + } + }, + async execute ({ positionals, offset, length, helia, stdout }) { + if (positionals == null || positionals.length === 0) { + throw new TypeError('Missing positionals') + } + + const cid = CID.parse(positionals[0]) + + for await (const buf of helia.cat(cid, { + offset: offset == null ? undefined : parseInt(offset), + length: length == null ? undefined : parseInt(length) + })) { + stdout.write(buf) + } + } +} diff --git a/packages/cli/src/commands/daemon.ts b/packages/cli/src/commands/daemon.ts new file mode 100644 index 00000000..d9eca0d9 --- /dev/null +++ b/packages/cli/src/commands/daemon.ts @@ -0,0 +1,34 @@ +import type { Command } from './index.js' +import { createHelia } from '../utils/create-helia.js' +import { createHeliaGrpcServer } from '@helia/rpc-server' +import { EdKeypair } from '@ucans/ucans' + +interface DaemonArgs { + positionals?: string[] +} + +export const daemon: Command = { + description: 'Starts a Helia daemon', + example: '$ helia daemon', + async execute ({ config, stdout }) { + const helia = await createHelia(config) + + const serverKey = EdKeypair.fromSecretKey(config.grpc.serverKey, { + format: 'base64url' + }) + + await createHeliaGrpcServer({ + helia, + ownerDID: '', + serviceDID: serverKey.did() + }) + + const id = await helia.id() + + stdout.write(`${id.agentVersion} is running\n`) + + id.multiaddrs.forEach(ma => { + stdout.write(`${ma.toString()}\n`) + }) + } +} diff --git a/packages/cli/src/commands/id.ts b/packages/cli/src/commands/id.ts new file mode 100644 index 00000000..7d87f8d5 --- /dev/null +++ b/packages/cli/src/commands/id.ts @@ -0,0 +1,16 @@ +import type { Command } from './index.js' + +interface IdArgs { + positionals?: string[] +} + +export const id: Command = { + description: 'Print information out this Helia node', + example: '$ helia id', + offline: true, + async execute ({ helia, stdout }) { + const result = await helia.id() + + stdout.write(JSON.stringify(result, null, 2) + '\n') + } +} diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts new file mode 100644 index 00000000..95b610ac --- /dev/null +++ b/packages/cli/src/commands/index.ts @@ -0,0 +1,98 @@ +import { cat } from './cat.js' +import { init } from './init.js' +import { daemon } from './daemon.js' +import { id } from './id.js' +import type { Helia } from '@helia/interface' +import type { ParseArgsConfig } from 'node:util' +import type { HeliaConfig } from '../index.js' + +/** + * Extends the internal node type to add a description to the options + */ +export interface ParseArgsOptionConfig { + /** + * Type of argument. + */ + type: 'string' | 'boolean' + + /** + * Whether this option can be provided multiple times. + * If `true`, all values will be collected in an array. + * If `false`, values for the option are last-wins. + * + * @default false. + */ + multiple?: boolean + + /** + * A single character alias for the option. + */ + short?: string + + /** + * The default option value when it is not set by args. + * It must be of the same type as the the `type` property. + * When `multiple` is `true`, it must be an array. + * + * @since v18.11.0 + */ + default?: string | boolean | string[] | boolean[] + + /** + * A description used to generate help text + */ + description: string +} + +interface ParseArgsOptionsConfig { + [longOption: string]: ParseArgsOptionConfig +} + +export interface CommandOptions extends ParseArgsConfig { + /** + * Used to describe arguments known to the parser. + */ + options?: ParseArgsOptionsConfig +} + +export interface Command { + /** + * Used to generate help text + */ + description: string + + /** + * Used to generate help text + */ + example?: string + + /** + * Specify if this command can be run offline + */ + offline?: boolean + + /** + * Configuration for the command + */ + options?: ParseArgsOptionsConfig + + /** + * Run the command + */ + execute: (ctx: Context & T) => Promise +} + +export interface Context { + helia: Helia + config: HeliaConfig + stdin: NodeJS.ReadStream + stdout: NodeJS.WriteStream + stderr: NodeJS.WriteStream +} + +export const commands: Record> = { + cat, + init, + daemon, + id +} diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts new file mode 100644 index 00000000..3b13b40a --- /dev/null +++ b/packages/cli/src/commands/init.ts @@ -0,0 +1,140 @@ +import type { Command } from './index.js' +import os from 'node:os' +import path from 'node:path' +import fs from 'node:fs/promises' +import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory' +import { InvalidParametersError } from '@helia/interface/errors' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { EdKeypair } from '@ucans/ucans' +import type { PeerId } from '@libp2p/interface-peer-id' + +interface InitArgs { + positionals?: string[] + keyType: string + bits: string + port: string + directory: string + directoryMode: string + configFileMode: string +} + +export const init: Command = { + description: 'Initialize the node', + options: { + keyType: { + description: 'The key type, valid options are "ed25519", "secp256k1" or "rsa"', + type: 'string', + short: 'k', + default: 'ed25519' + }, + bits: { + description: 'Key length (only applies to RSA keys)', + type: 'string', + short: 'b', + default: '2048' + }, + port: { + description: 'Where to listen for incoming gRPC connections', + type: 'string', + short: 'p', + default: '49832' + }, + directory: { + description: 'The directory to store config in', + type: 'string', + short: 'd', + default: path.join(os.homedir(), '.helia') + }, + directoryMode: { + description: 'If the config file directory does not exist, create it with this mode', + type: 'string', + default: '0700' + }, + configFileMode: { + description: 'If the config file does not exist, create it with this mode', + type: 'string', + default: '0600' + } + }, + async execute ({ keyType, bits, directory, directoryMode, configFileMode, port, stdout }) { + const configFile = path.join(directory, 'config.json') + const key = await generateKey(keyType, bits) + + if (key.publicKey == null || key.privateKey == null) { + throw new InvalidParametersError('Generated PeerId had missing components') + } + + const serverKeyPair = await EdKeypair.create({ + exportable: true + }) + const serverKey = await serverKeyPair.export('base64url') + + await fs.mkdir(directory, { + recursive: true, + mode: parseInt(directoryMode, 8) + }) + + await fs.writeFile(configFile, ` +{ + // Configuration for the gRPC API + "grpc": { + // A multiaddr that specifies the TCP port the gRPC server is listening on + "address": "/ip4/127.0.0.1/tcp/${port}/ws/p2p/${key.toString()}", + + // The server key used to create ucans for operation permissions - note this is separate + // to the peerId to let you rotate the server key while keeping the same peerId + "serverKey": "${serverKey}" + }, + + // The private key portion of the node's PeerId as a base64url encoded string + "peerId": { + "publicKey": "${uint8ArrayToString(key.publicKey, 'base64url')}", + "privateKey": "${uint8ArrayToString(key.privateKey, 'base64url')}" + }, + + // Where blocks are stored + "blocks": "${path.join(directory, 'blocks')}", + + // libp2p configuration + "libp2p": { + "addresses": { + "listen": [ + "/ip4/0.0.0.0/tcp/0", + "/ip4/0.0.0.0/tcp/0/ws", + + // this is the gRPC port + "/ip4/0.0.0.0/tcp/${port}/ws" + ], + "announce": [], + "noAnnounce": [ + // this is the gRPC port + "/ip4/0.0.0.0/tcp/${port}/ws" + ] + }, + "identify": { + + } + } +} +`, { + mode: parseInt(configFileMode, 8), + flag: 'ax' + }) + + stdout.write(`Wrote config file to ${configFile}\n`) + } +} + +async function generateKey (type: string, bits: string = '2048'): Promise { + if (type === 'ed25519') { + return await createEd25519PeerId() + } else if (type === 'secp256k1') { + return await createSecp256k1PeerId() + } else if (type === 'rsa') { + return await createRSAPeerId({ + bits: parseInt(bits) + }) + } + + throw new InvalidParametersError(`Unknown key type "${type}" - must be "ed25519", "secp256k1" or "rsa"`) +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts new file mode 100644 index 00000000..c311952d --- /dev/null +++ b/packages/cli/src/index.ts @@ -0,0 +1,147 @@ +#! /usr/bin/env node --trace-warnings +/* eslint-disable no-console */ + +import { parseArgs } from 'node:util' +import path from 'node:path' +import os from 'os' +import fs from 'node:fs' +import { Command, commands } from './commands/index.js' +import { InvalidParametersError } from '@helia/interface/errors' +import { printHelp } from './utils/print-help.js' +import { findHelia } from './utils/find-helia.js' +import stripJsonComments from 'strip-json-comments' +import type { Helia } from '@helia/interface' +import type { Libp2p } from '@libp2p/interface-libp2p' + +/** + * Typedef for the Helia config file + */ +export interface HeliaConfig { + peerId: { + publicKey: string + privateKey: string + } + grpc: { + address: string + serverKey: string + } + blocks: string + libp2p: { + addresses: { + listen: string[] + announce: string[] + noAnnounce: string[] + } + } +} + +interface RootConfig { + directory: string + help: boolean +} + +const root: Command = { + description: `Helia is an IPFS implementation in JavaScript + +Subcommands: + +${Object.entries(commands).map(([key, command]) => ` ${key}\t${command.description}`).sort().join('\n')}`, + options: { + directory: { + description: 'The directory to load config from', + type: 'string', + default: path.join(os.homedir(), '.helia') + }, + help: { + description: 'Show help text', + type: 'boolean' + } + }, + async execute () {} +} + +async function main () { + const command = parseArgs({ + allowPositionals: true, + strict: true, + options: root.options + }) + + // @ts-expect-error wat + const configDir = command.values.directory + + if (configDir == null) { + throw new InvalidParametersError('No config directory specified') + } + + if (!fs.existsSync(configDir)) { + // run the init command + const parsed = parseArgs({ + allowPositionals: true, + strict: true, + options: commands.init.options + }) + await commands.init.execute({ + ...parsed.values, + positionals: parsed.positionals.slice(1), + stdin: process.stdin, + stdout: process.stdout, + stderr: process.stderr + }) + } + + if (command.positionals.length > 0) { + const subCommand = command.positionals[0] + + if (commands[subCommand] != null) { + const com = commands[subCommand] + + // @ts-expect-error wat + if (command.values.help === true) { + printHelp(com, process.stdout) + } else { + const config = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8'))) + + const opts = parseArgs({ + allowPositionals: true, + strict: true, + options: com.options + }) + + let helia: Helia + let libp2p: Libp2p | undefined + + if (subCommand !== 'daemon') { + const res = await findHelia(config, com.offline) + helia = res.helia + libp2p = res.libp2p + } + + await commands[subCommand].execute({ + ...opts.values, + positionals: opts.positionals.slice(1), + // @ts-expect-error wat + helia, + stdin: process.stdin, + stdout: process.stdout, + stderr: process.stderr, + config + }) + + if (libp2p != null) { + await libp2p.stop() + } + } + + return + } + } + + // no command specified, print help + printHelp(root, process.stdout) +} + +main().catch(err => { + console.error(err) // eslint-disable-line no-console + process.exit(1) +}) diff --git a/packages/cli/src/utils/create-helia.ts b/packages/cli/src/utils/create-helia.ts new file mode 100644 index 00000000..cf9b5ab6 --- /dev/null +++ b/packages/cli/src/utils/create-helia.ts @@ -0,0 +1,53 @@ +import type { Helia } from '@helia/interface' +import type { HeliaConfig } from '../index.js' +import { createHelia as createHeliaNode } from 'helia' +import { FsDatastore } from 'datastore-fs' +import { BlockstoreDatastoreAdapter } from 'blockstore-datastore-adapter' +import { unixfs } from '@helia/unixfs' +import { createLibp2p } from 'libp2p' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { fromString as uint8ArrayFromString } from 'uint8arrays' +import { tcp } from '@libp2p/tcp' +import { webSockets } from '@libp2p/websockets' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { mplex } from '@libp2p/mplex' +import { gossipsub } from '@chainsafe/libp2p-gossipsub' +import { kadDHT } from '@libp2p/kad-dht' + +export async function createHelia (config: HeliaConfig, offline: boolean = false): Promise { + const peerId = await peerIdFromKeys( + uint8ArrayFromString(config.peerId.publicKey, 'base64url'), + uint8ArrayFromString(config.peerId.privateKey, 'base64url') + ) + + return await createHeliaNode({ + blockstore: new BlockstoreDatastoreAdapter(new FsDatastore(config.blocks)), + filesystems: [ + unixfs() + ], + libp2p: await createLibp2p({ + start: !offline, + peerId, + addresses: config.libp2p.addresses, + identify: { + host: { + agentVersion: 'helia/0.0.0' + } + }, + transports: [ + tcp(), + webSockets() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux(), + mplex() + ], + pubsub: gossipsub(), + dht: kadDHT() + }) + }) +} diff --git a/packages/cli/src/utils/find-helia.ts b/packages/cli/src/utils/find-helia.ts new file mode 100644 index 00000000..b6476ec8 --- /dev/null +++ b/packages/cli/src/utils/find-helia.ts @@ -0,0 +1,69 @@ +import type { Helia } from '@helia/interface' +import type { HeliaConfig } from '../index.js' +import { createHeliaRpcClient } from '@helia/rpc-client' +import { multiaddr } from '@multiformats/multiaddr' +import { createHelia } from './create-helia.js' +import { createLibp2p, Libp2p } from 'libp2p' +import { tcp } from '@libp2p/tcp' +import { webSockets } from '@libp2p/websockets' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { mplex } from '@libp2p/mplex' +import { logger } from '@libp2p/logger' + +const log = logger('helia:cli:utils:find-helia') + +export async function findHelia (config: HeliaConfig, offline: boolean = false): Promise<{ helia: Helia, libp2p: Libp2p | undefined }> { + let libp2p: Libp2p | undefined + let helia: Helia | undefined + + try { + log('create libp2p node') + // create a dial-only libp2p node + libp2p = await createLibp2p({ + transports: [ + tcp(), + webSockets() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux(), + mplex() + ] + }) + + log('create helia client') + helia = await createHeliaRpcClient({ + multiaddr: multiaddr(config.grpc.address), + libp2p, + authorization: 'sshh' + }) + } catch (err: any) { + log('could not create helia client', err) + + if (err.code !== 'ECONNREFUSED' && err.errors[0].code !== 'ECONNREFUSED') { + throw err + } + } + + if (helia == null) { + log('connecting to existing helia node failed') + + // could not connect to running node, start the server + if (!offline) { + log('could not create client and command cannot be run in offline mode') + throw new Error('Could not connect to Helia - is the node running?') + } + + // return an offline node + log('create offline helia node') + helia = await createHelia(config, offline) + } + + return { + helia, + libp2p + } +} diff --git a/packages/cli/src/utils/multiaddr-to-url.ts b/packages/cli/src/utils/multiaddr-to-url.ts new file mode 100644 index 00000000..94497ec9 --- /dev/null +++ b/packages/cli/src/utils/multiaddr-to-url.ts @@ -0,0 +1,22 @@ +import type { Multiaddr } from '@multiformats/multiaddr' +import { InvalidParametersError } from '@helia/interface/errors' + +export function multiaddrToUrl (addr: Multiaddr): URL { + const protoNames = addr.protoNames() + + if (protoNames.length !== 3) { + throw new InvalidParametersError('Helia gRPC address format incorrect') + } + + if (protoNames[0] !== 'ip4' && protoNames[0] !== 'ip6') { + throw new InvalidParametersError('Helia gRPC address format incorrect') + } + + if (protoNames[1] !== 'tcp' && protoNames[2] !== 'ws') { + throw new InvalidParametersError('Helia gRPC address format incorrect') + } + + const { host, port } = addr.toOptions() + + return new URL(`ws://${host}:${port}`) +} diff --git a/packages/cli/src/utils/print-help.ts b/packages/cli/src/utils/print-help.ts new file mode 100644 index 00000000..6daf75d6 --- /dev/null +++ b/packages/cli/src/utils/print-help.ts @@ -0,0 +1,25 @@ +import type { Command } from '../commands/index.js' + +export function printHelp (command: Command, stdout: NodeJS.WriteStream): void { + stdout.write('\n') + stdout.write(`${command.description}\n`) + stdout.write('\n') + + if (command.example != null) { + stdout.write('Example:\n') + stdout.write('\n') + stdout.write(`${command.example}\n`) + stdout.write('\n') + } + + const options = Object.entries(command.options ?? {}) + + if (options.length > 0) { + stdout.write('Options:\n') + + Object.entries(command.options ?? {}).forEach(([key, option]) => { + stdout.write(` --${key}\t${option.description}\n`) + }) + stdout.write('\n') + } +} diff --git a/packages/cli/test/index.spec.ts b/packages/cli/test/index.spec.ts new file mode 100644 index 00000000..e67dc48c --- /dev/null +++ b/packages/cli/test/index.spec.ts @@ -0,0 +1,7 @@ +import { expect } from 'aegir/chai' + +describe('cli', () => { + it('should start a node', () => { + expect(true).to.be.ok() + }) +}) diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 00000000..af0863af --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../helia" + }, + { + "path": "../interface" + }, + { + "path": "../rpc-client" + }, + { + "path": "../rpc-server" + }, + { + "path": "../unixfs" + } + ] +} diff --git a/packages/helia/LICENSE b/packages/helia/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/helia/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/helia/LICENSE-APACHE b/packages/helia/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/helia/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/helia/LICENSE-MIT b/packages/helia/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/helia/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/helia/README.md b/packages/helia/README.md new file mode 100644 index 00000000..7be60f02 --- /dev/null +++ b/packages/helia/README.md @@ -0,0 +1,53 @@ +# helia + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> An implementation of IPFS in JavaScript + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/helia/package.json b/packages/helia/package.json new file mode 100644 index 00000000..023077df --- /dev/null +++ b/packages/helia/package.json @@ -0,0 +1,156 @@ +{ + "name": "helia", + "version": "0.0.0", + "description": "An implementation of IPFS in JavaScript", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/helia#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" + }, + "dependencies": { + "@helia/interface": "~0.0.0", + "@libp2p/interface-libp2p": "^1.0.0", + "@ucans/ucans": "^0.11.0-alpha", + "interface-blockstore": "^3.0.2", + "interface-datastore": "^3.0.2", + "ipfs-bitswap": "^14.0.0", + "merge-options": "^3.0.4", + "multiformats": "^10.0.3" + }, + "devDependencies": { + "aegir": "^37.7.5" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/helia/src/commands/cat.ts b/packages/helia/src/commands/cat.ts new file mode 100644 index 00000000..6eeae346 --- /dev/null +++ b/packages/helia/src/commands/cat.ts @@ -0,0 +1,26 @@ +import mergeOpts from 'merge-options' +import type { Blockstore } from 'interface-blockstore' +import type { CID } from 'multiformats' +import type { CatOptions } from '../index.js' +import type { ReadableStream } from 'node:stream/web' +import type { FileSystem } from '@helia/interface' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) + +const defaultOptions: CatOptions = { + offset: 0, + length: Infinity +} + +interface CatComponents { + blockstore: Blockstore + filesystems: FileSystem[] +} + +export function createCat (components: CatComponents) { + return function cat (cid: CID, options: CatOptions = {}): ReadableStream { + options = mergeOptions(defaultOptions, options) + + return components.filesystems[0].cat(cid, options) + } +} diff --git a/packages/helia/src/commands/id.ts b/packages/helia/src/commands/id.ts new file mode 100644 index 00000000..f69a1109 --- /dev/null +++ b/packages/helia/src/commands/id.ts @@ -0,0 +1,18 @@ +import type { Libp2p } from '@libp2p/interface-libp2p' +import type { IdResponse } from '@helia/interface' + +interface IdComponents { + libp2p: Libp2p +} + +export function createId (components: IdComponents) { + return async function id (): Promise { + return { + peerId: components.libp2p.peerId, + multiaddrs: components.libp2p.getMultiaddrs(), + agentVersion: components.libp2p.identifyService.host.agentVersion, + protocolVersion: components.libp2p.identifyService.host.protocolVersion, + protocols: components.libp2p.getProtocols() + } + } +} diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts new file mode 100644 index 00000000..99e300e8 --- /dev/null +++ b/packages/helia/src/index.ts @@ -0,0 +1,85 @@ +/** + * @packageDocumentation + * + * Create a Helia node. + * + * @example + * + * ```typescript + * import { createHelia } from 'helia' + * import { CID } from 'multiformats/cid' + * + * const node = await createHelia() + * + * node.cat(CID.parse('bafyFoo')) + * ``` + */ + +import { createCat } from './commands/cat.js' +import { createId } from './commands/id.js' +import { createBitswap } from 'ipfs-bitswap' +import type { Helia, FileSystem } from '@helia/interface' +import type { Libp2p } from '@libp2p/interface-libp2p' +import type { Blockstore } from 'interface-blockstore' +import type { AbortOptions } from '@libp2p/interfaces' + +export interface CatOptions extends AbortOptions { + offset?: number + length?: number +} + +export interface HeliaComponents { + libp2p: Libp2p + blockstore: Blockstore + filesystems: FileSystem[] +} + +/** + * Options used to create a Helia node. + */ +export interface HeliaInit { + /** + * A libp2p node is required to perform network operations + */ + libp2p: Libp2p + + /** + * The blockstore is where blocks are stored + */ + blockstore: Blockstore + + /** + * Helia supports multiple filesystem implementations + */ + filesystems: Array<(components: HeliaComponents) => FileSystem> +} + +/** + * Create and return a Helia node. + * + * @param {HeliaInit} init + * @returns {Promise} + */ +export async function createHelia (init: HeliaInit): Promise { + const blockstore = createBitswap(init.libp2p, init.blockstore, { + + }) + + const components: HeliaComponents = { + libp2p: init.libp2p, + blockstore, + filesystems: [] + } + + components.filesystems = init.filesystems.map(fs => fs(components)) + + const helia: Helia = { + libp2p: init.libp2p, + blockstore, + + id: createId(components), + cat: createCat(components) + } + + return helia +} diff --git a/packages/helia/test/index.spec.ts b/packages/helia/test/index.spec.ts new file mode 100644 index 00000000..ff8710fb --- /dev/null +++ b/packages/helia/test/index.spec.ts @@ -0,0 +1,8 @@ +/* eslint-env mocha */ +import { expect } from 'aegir/chai' + +describe('helia', () => { + it('does a thing', () => { + expect(true).to.be.ok() + }) +}) diff --git a/packages/helia/tsconfig.json b/packages/helia/tsconfig.json new file mode 100644 index 00000000..4c0bdf77 --- /dev/null +++ b/packages/helia/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface" + } + ] +} diff --git a/packages/interface/LICENSE b/packages/interface/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/interface/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/interface/LICENSE-APACHE b/packages/interface/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/interface/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/interface/LICENSE-MIT b/packages/interface/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/interface/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/interface/README.md b/packages/interface/README.md new file mode 100644 index 00000000..9f0f1c0c --- /dev/null +++ b/packages/interface/README.md @@ -0,0 +1,53 @@ +# @helia/interface + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> The Helia API + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/interface/package.json b/packages/interface/package.json new file mode 100644 index 00000000..7abc0f7d --- /dev/null +++ b/packages/interface/package.json @@ -0,0 +1,169 @@ +{ + "name": "@helia/interface", + "version": "0.0.0", + "description": "The Helia API", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/interface#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./errors": { + "types": "./dist/src/errors.d.ts", + "import": "./dist/src/errors.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" + }, + "dependencies": { + "multiformats": "^10.0.3" + }, + "devDependencies": { + "aegir": "^37.7.5" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/interface/src/errors.ts b/packages/interface/src/errors.ts new file mode 100644 index 00000000..be8b5e40 --- /dev/null +++ b/packages/interface/src/errors.ts @@ -0,0 +1,29 @@ +export abstract class HeliaError extends Error { + public readonly name: string + public readonly code: string + + constructor (message: string, name: string, code: string) { + super(message) + + this.name = name + this.code = code + } +} + +export class NotAFileError extends HeliaError { + constructor (message = 'not a file') { + super(message, 'NotAFileError', 'ERR_NOT_FILE') + } +} + +export class NoContentError extends HeliaError { + constructor (message = 'no content') { + super(message, 'NoContentError', 'ERR_NO_CONTENT') + } +} + +export class InvalidParametersError extends HeliaError { + constructor (message = 'invalid parameters') { + super(message, 'InvalidParametersError', 'ERR_INVALID_PARAMETERS') + } +} diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts new file mode 100644 index 00000000..e9fd333b --- /dev/null +++ b/packages/interface/src/index.ts @@ -0,0 +1,82 @@ +/** + * @packageDocumentation + * + * The API defined by a Helia node + * + * @example + * + * ```typescript + * import type { Helia } from '@helia/interface' + * + * export function doSomething(helia: Helia) { + * // use helia node functions here + * } + * ``` + */ + +import type { Libp2p } from '@libp2p/interface-libp2p' +import type { Blockstore } from 'interface-blockstore' +import type { CID } from 'multiformats/cid' +import type { ReadableStream } from 'node:stream/web' +import type { AbortOptions } from '@libp2p/interfaces' +import type { PeerId } from '@libp2p/interface-peer-id' +import type { Multiaddr } from '@multiformats/multiaddr' + +export interface FileSystem { + cat: (cid: CID, options?: CatOptions) => ReadableStream +} + +/** + * The API presented by a Helia node. + */ +export interface Helia { + /** + * Returns information about this node + * + * @example + * + * ```typescript + * import { createHelia } from 'helia' + * + * const node = await createHelia() + * const id = await node.id() + * console.info(id) + * // { peerId: PeerId(12D3Foo), ... } + * ``` + */ + id: (options?: IdOptions) => Promise + + /** + * The cat method reads files sequentially, returning the bytes as a stream. + * + * If the passed CID does not resolve to a file, an error will be thrown. + */ + cat: (cid: CID, options?: CatOptions) => ReadableStream + + /** + * The underlying libp2p node + */ + libp2p: Libp2p + + /** + * Where the blocks are stored + */ + blockstore: Blockstore +} + +export interface CatOptions extends AbortOptions { + offset?: number + length?: number +} + +export interface IdOptions extends AbortOptions { + peerId?: PeerId +} + +export interface IdResponse { + peerId: PeerId + multiaddrs: Multiaddr[] + agentVersion: string + protocolVersion: string + protocols: string[] +} diff --git a/packages/interface/tsconfig.json b/packages/interface/tsconfig.json new file mode 100644 index 00000000..13a35996 --- /dev/null +++ b/packages/interface/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ] +} diff --git a/packages/rpc-client/.aegir.js b/packages/rpc-client/.aegir.js new file mode 100644 index 00000000..e9c18f3e --- /dev/null +++ b/packages/rpc-client/.aegir.js @@ -0,0 +1,6 @@ + +export default { + build: { + bundle: false + } +} diff --git a/packages/rpc-client/LICENSE b/packages/rpc-client/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/rpc-client/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/rpc-client/LICENSE-APACHE b/packages/rpc-client/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/rpc-client/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/rpc-client/LICENSE-MIT b/packages/rpc-client/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/rpc-client/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/rpc-client/README.md b/packages/rpc-client/README.md new file mode 100644 index 00000000..5b3bd630 --- /dev/null +++ b/packages/rpc-client/README.md @@ -0,0 +1,53 @@ +# @helia/rpc-client + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> An implementation of IPFS in JavaScript + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json new file mode 100644 index 00000000..98c2bd1b --- /dev/null +++ b/packages/rpc-client/package.json @@ -0,0 +1,153 @@ +{ + "name": "@helia/rpc-client", + "version": "0.0.0", + "description": "An implementation of IPFS in JavaScript", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/rpc-client#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" + }, + "dependencies": { + "@helia/interface": "~0.0.0", + "@helia/rpc-protocol": "~0.0.0", + "@libp2p/peer-id": "^1.1.18", + "@multiformats/multiaddr": "^11.1.4", + "it-pb-stream": "^2.0.3" + }, + "devDependencies": { + "aegir": "^37.7.5" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/rpc-client/src/commands/cat.ts b/packages/rpc-client/src/commands/cat.ts new file mode 100644 index 00000000..59ad568a --- /dev/null +++ b/packages/rpc-client/src/commands/cat.ts @@ -0,0 +1,2 @@ + +export {} diff --git a/packages/rpc-client/src/commands/id.ts b/packages/rpc-client/src/commands/id.ts new file mode 100644 index 00000000..b7dc72fc --- /dev/null +++ b/packages/rpc-client/src/commands/id.ts @@ -0,0 +1,46 @@ +import { multiaddr } from '@multiformats/multiaddr' +import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { IdOptions, IdResponse } from '@helia/rpc-protocol/root' +import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' +import type { Helia } from '@helia/interface' +import { peerIdFromString } from '@libp2p/peer-id' +import type { HeliaRpcClientConfig } from '../index.js' +import { pbStream } from 'it-pb-stream' + +export function createId (config: HeliaRpcClientConfig) { + const id: Helia['id'] = async (options = {}) => { + const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) + + const stream = pbStream(duplex) + stream.writePB({ + resource: '/id', + method: 'GET', + authorization: config.authorization, + options: IdOptions.encode({ + ...options, + peerId: options.peerId != null ? options.peerId.toString() : undefined + }) + }, RPCCallRequest) + const response = await stream.readPB(RPCCallResponse) + + duplex.close() + + if (response.type === RPCCallResponseType.message) { + if (response.message == null) { + throw new TypeError('RPC response had message type but no message') + } + + const idResponse = IdResponse.decode(response.message) + + return { + ...idResponse, + peerId: peerIdFromString(idResponse.peerId), + multiaddrs: idResponse.multiaddrs.map(str => multiaddr(str)) + } + } + + throw new RPCError(response) + } + + return id +} diff --git a/packages/rpc-client/src/index.ts b/packages/rpc-client/src/index.ts new file mode 100644 index 00000000..bdd3e608 --- /dev/null +++ b/packages/rpc-client/src/index.ts @@ -0,0 +1,19 @@ +import type { Helia } from '@helia/interface' +import { createId } from './commands/id.js' +import type { Libp2p } from '@libp2p/interface-libp2p' +import type { Multiaddr } from '@multiformats/multiaddr' + +export interface HeliaRpcClientConfig { + multiaddr: Multiaddr + libp2p: Libp2p + authorization: string +} + +export async function createHeliaRpcClient (config: HeliaRpcClientConfig): Promise { + await config.libp2p.dial(config.multiaddr) + + // @ts-expect-error incomplete implementation + return { + id: createId(config) + } +} diff --git a/packages/rpc-client/tsconfig.json b/packages/rpc-client/tsconfig.json new file mode 100644 index 00000000..021053e1 --- /dev/null +++ b/packages/rpc-client/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface" + }, + { + "path": "../rpc-protocol" + } + ] +} diff --git a/packages/rpc-protocol/LICENSE b/packages/rpc-protocol/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/rpc-protocol/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/rpc-protocol/LICENSE-APACHE b/packages/rpc-protocol/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/rpc-protocol/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/rpc-protocol/LICENSE-MIT b/packages/rpc-protocol/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/rpc-protocol/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/rpc-protocol/README.md b/packages/rpc-protocol/README.md new file mode 100644 index 00000000..bb27636b --- /dev/null +++ b/packages/rpc-protocol/README.md @@ -0,0 +1,53 @@ +# @helia/rpc-protocol + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> gRPC protocol for use by @helia/rpc-client and @helia/rpc-server + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json new file mode 100644 index 00000000..1eee241a --- /dev/null +++ b/packages/rpc-protocol/package.json @@ -0,0 +1,175 @@ +{ + "name": "@helia/rpc-protocol", + "version": "0.0.0", + "description": "gRPC protocol for use by @helia/rpc-client and @helia/rpc-server", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/rpc-protocol#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + }, + "./root": { + "types": "./dist/src/root.d.ts", + "import": "./dist/src/root.js" + }, + "./rpc": { + "types": "./dist/src/rpc.d.ts", + "import": "./dist/src/rpc.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release", + "generate": "protons src/*.proto" + }, + "dependencies": { + "protons-runtime": "^4.0.1" + }, + "devDependencies": { + "aegir": "^37.7.5", + "protons": "^6.0.1" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/rpc-protocol/src/index.ts b/packages/rpc-protocol/src/index.ts new file mode 100644 index 00000000..5822d4c6 --- /dev/null +++ b/packages/rpc-protocol/src/index.ts @@ -0,0 +1,16 @@ +import type { RPCCallResponse } from './rpc.js' + +export const HELIA_RPC_PROTOCOL = '/helia-rpc/0.0.1' + +export class RPCError extends Error { + public readonly name: string + public readonly code: string + + constructor (response: RPCCallResponse) { + super(response.errorMessage ?? 'RPC error') + + this.name = response.errorName ?? 'RPCError' + this.code = response.errorCode ?? 'ERR_RPC_ERROR' + this.stack = response.errorStack ?? this.stack + } +} diff --git a/packages/rpc-protocol/src/root.proto b/packages/rpc-protocol/src/root.proto new file mode 100644 index 00000000..8cf299a4 --- /dev/null +++ b/packages/rpc-protocol/src/root.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; + +message IdOptions { + optional string peer_id = 1; +} + +message IdResponse { + string peer_id = 1; + string server_did = 2; + repeated string multiaddrs = 3; + string agent_version = 4; + string protocol_version = 5; + repeated string protocols = 6; +} + +message CatOptions { + string cid = 1; + int32 offset = 2; + int32 length = 3; +} + +message CatResponse { + bytes bytes = 1; +} diff --git a/packages/rpc-protocol/src/root.ts b/packages/rpc-protocol/src/root.ts new file mode 100644 index 00000000..a85090ec --- /dev/null +++ b/packages/rpc-protocol/src/root.ts @@ -0,0 +1,312 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { Codec } from 'protons-runtime' + +export interface IdOptions { + peerId?: string +} + +export namespace IdOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (obj.peerId != null) { + w.uint32(10) + w.string(obj.peerId) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.peerId = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: IdOptions): Uint8Array => { + return encodeMessage(obj, IdOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): IdOptions => { + return decodeMessage(buf, IdOptions.codec()) + } +} + +export interface IdResponse { + peerId: string + serverDid: string + multiaddrs: string[] + agentVersion: string + protocolVersion: string + protocols: string[] +} + +export namespace IdResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.peerId !== '') { + w.uint32(10) + w.string(obj.peerId) + } + + if (opts.writeDefaults === true || obj.serverDid !== '') { + w.uint32(18) + w.string(obj.serverDid) + } + + if (obj.multiaddrs != null) { + for (const value of obj.multiaddrs) { + w.uint32(26) + w.string(value) + } + } + + if (opts.writeDefaults === true || obj.agentVersion !== '') { + w.uint32(34) + w.string(obj.agentVersion) + } + + if (opts.writeDefaults === true || obj.protocolVersion !== '') { + w.uint32(42) + w.string(obj.protocolVersion) + } + + if (obj.protocols != null) { + for (const value of obj.protocols) { + w.uint32(50) + w.string(value) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + peerId: '', + serverDid: '', + multiaddrs: [], + agentVersion: '', + protocolVersion: '', + protocols: [] + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.peerId = reader.string() + break + case 2: + obj.serverDid = reader.string() + break + case 3: + obj.multiaddrs.push(reader.string()) + break + case 4: + obj.agentVersion = reader.string() + break + case 5: + obj.protocolVersion = reader.string() + break + case 6: + obj.protocols.push(reader.string()) + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: IdResponse): Uint8Array => { + return encodeMessage(obj, IdResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): IdResponse => { + return decodeMessage(buf, IdResponse.codec()) + } +} + +export interface CatOptions { + cid: string + offset: number + length: number +} + +export namespace CatOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.cid !== '') { + w.uint32(10) + w.string(obj.cid) + } + + if (opts.writeDefaults === true || obj.offset !== 0) { + w.uint32(16) + w.int32(obj.offset) + } + + if (opts.writeDefaults === true || obj.length !== 0) { + w.uint32(24) + w.int32(obj.length) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: '', + offset: 0, + length: 0 + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.string() + break + case 2: + obj.offset = reader.int32() + break + case 3: + obj.length = reader.int32() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: CatOptions): Uint8Array => { + return encodeMessage(obj, CatOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): CatOptions => { + return decodeMessage(buf, CatOptions.codec()) + } +} + +export interface CatResponse { + bytes: Uint8Array +} + +export namespace CatResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.bytes != null && obj.bytes.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.bytes) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + bytes: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.bytes = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: CatResponse): Uint8Array => { + return encodeMessage(obj, CatResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): CatResponse => { + return decodeMessage(buf, CatResponse.codec()) + } +} diff --git a/packages/rpc-protocol/src/rpc.proto b/packages/rpc-protocol/src/rpc.proto new file mode 100644 index 00000000..95c9b745 --- /dev/null +++ b/packages/rpc-protocol/src/rpc.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +message RPCCallRequest { + string resource = 1; + string method = 2; + string authorization = 3; + bytes options = 4; +} + +message RPCCallResponse { + RPCCallResponseType type = 1; + optional bytes message = 2; + optional string error_name = 3; + optional string error_message = 4; + optional string error_stack = 5; + optional string error_code = 6; +} + +enum RPCCallResponseType { + message = 0; + error = 1; +} diff --git a/packages/rpc-protocol/src/rpc.ts b/packages/rpc-protocol/src/rpc.ts new file mode 100644 index 00000000..095b385e --- /dev/null +++ b/packages/rpc-protocol/src/rpc.ts @@ -0,0 +1,215 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ + +import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { Codec } from 'protons-runtime' + +export interface RPCCallRequest { + resource: string + method: string + authorization: string + options: Uint8Array +} + +export namespace RPCCallRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.resource !== '') { + w.uint32(10) + w.string(obj.resource) + } + + if (opts.writeDefaults === true || obj.method !== '') { + w.uint32(18) + w.string(obj.method) + } + + if (opts.writeDefaults === true || obj.authorization !== '') { + w.uint32(26) + w.string(obj.authorization) + } + + if (opts.writeDefaults === true || (obj.options != null && obj.options.byteLength > 0)) { + w.uint32(34) + w.bytes(obj.options) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + resource: '', + method: '', + authorization: '', + options: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.resource = reader.string() + break + case 2: + obj.method = reader.string() + break + case 3: + obj.authorization = reader.string() + break + case 4: + obj.options = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: RPCCallRequest): Uint8Array => { + return encodeMessage(obj, RPCCallRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallRequest => { + return decodeMessage(buf, RPCCallRequest.codec()) + } +} + +export interface RPCCallResponse { + type: RPCCallResponseType + message?: Uint8Array + errorName?: string + errorMessage?: string + errorStack?: string + errorCode?: string +} + +export namespace RPCCallResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.type != null && __RPCCallResponseTypeValues[obj.type] !== 0)) { + w.uint32(8) + RPCCallResponseType.codec().encode(obj.type, w) + } + + if (obj.message != null) { + w.uint32(18) + w.bytes(obj.message) + } + + if (obj.errorName != null) { + w.uint32(26) + w.string(obj.errorName) + } + + if (obj.errorMessage != null) { + w.uint32(34) + w.string(obj.errorMessage) + } + + if (obj.errorStack != null) { + w.uint32(42) + w.string(obj.errorStack) + } + + if (obj.errorCode != null) { + w.uint32(50) + w.string(obj.errorCode) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + type: RPCCallResponseType.message + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.type = RPCCallResponseType.codec().decode(reader) + break + case 2: + obj.message = reader.bytes() + break + case 3: + obj.errorName = reader.string() + break + case 4: + obj.errorMessage = reader.string() + break + case 5: + obj.errorStack = reader.string() + break + case 6: + obj.errorCode = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: RPCCallResponse): Uint8Array => { + return encodeMessage(obj, RPCCallResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallResponse => { + return decodeMessage(buf, RPCCallResponse.codec()) + } +} + +export enum RPCCallResponseType { + message = 'message', + error = 'error' +} + +enum __RPCCallResponseTypeValues { + message = 0, + error = 1 +} + +export namespace RPCCallResponseType { + export const codec = () => { + return enumeration(__RPCCallResponseTypeValues) + } +} diff --git a/packages/rpc-protocol/tsconfig.json b/packages/rpc-protocol/tsconfig.json new file mode 100644 index 00000000..f67b4ce9 --- /dev/null +++ b/packages/rpc-protocol/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "noUnusedLocals": false + }, + "include": [ + "src", + "test" + ] +} diff --git a/packages/rpc-server/.aegir.js b/packages/rpc-server/.aegir.js new file mode 100644 index 00000000..e9c18f3e --- /dev/null +++ b/packages/rpc-server/.aegir.js @@ -0,0 +1,6 @@ + +export default { + build: { + bundle: false + } +} diff --git a/packages/rpc-server/LICENSE b/packages/rpc-server/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/rpc-server/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/rpc-server/LICENSE-APACHE b/packages/rpc-server/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/rpc-server/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/rpc-server/LICENSE-MIT b/packages/rpc-server/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/rpc-server/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/rpc-server/README.md b/packages/rpc-server/README.md new file mode 100644 index 00000000..ab4fa46f --- /dev/null +++ b/packages/rpc-server/README.md @@ -0,0 +1,53 @@ +# @helia/rpc-server + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> An implementation of IPFS in JavaScript + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/rpc-server/package.json b/packages/rpc-server/package.json new file mode 100644 index 00000000..ed0dfb34 --- /dev/null +++ b/packages/rpc-server/package.json @@ -0,0 +1,156 @@ +{ + "name": "@helia/rpc-server", + "version": "0.0.0", + "description": "An implementation of IPFS in JavaScript", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/rpc-server#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" + }, + "dependencies": { + "@helia/interface": "~0.0.0", + "@helia/rpc-protocol": "~0.0.0", + "@libp2p/logger": "^2.0.2", + "@libp2p/peer-id": "^1.1.18", + "@ucans/ucans": "^0.11.0-alpha", + "it-length-prefixed": "^8.0.4", + "it-pushable": "^3.1.2", + "it-stream-types": "^1.0.5" + }, + "devDependencies": { + "aegir": "^37.7.5" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/rpc-server/src/handlers/id.ts b/packages/rpc-server/src/handlers/id.ts new file mode 100644 index 00000000..edb8b46b --- /dev/null +++ b/packages/rpc-server/src/handlers/id.ts @@ -0,0 +1,33 @@ +import { IdOptions, IdResponse } from '@helia/rpc-protocol/root' +import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { peerIdFromString } from '@libp2p/peer-id' +import type { Source } from 'it-stream-types' +import type { Pushable } from 'it-pushable' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { GRPCServerConfig, Service } from '../index.js' + +export function createId (config: GRPCServerConfig): Service { + return { + insecure: true, + async handle (options: Uint8Array, input: Source, output: Pushable, signal: AbortSignal): Promise { + const opts = IdOptions.decode(options) + + const result = await config.helia.id({ + peerId: opts.peerId != null ? peerIdFromString(opts.peerId) : undefined, + signal + }) + + output.push( + RPCCallResponse.encode({ + type: RPCCallResponseType.message, + message: IdResponse.encode({ + ...result, + peerId: result.peerId.toString(), + serverDid: config.serviceDID, + multiaddrs: result.multiaddrs.map(ma => ma.toString()) + }) + }) + ) + } + } +} diff --git a/packages/rpc-server/src/index.ts b/packages/rpc-server/src/index.ts new file mode 100644 index 00000000..43128c69 --- /dev/null +++ b/packages/rpc-server/src/index.ts @@ -0,0 +1,156 @@ +import type { Helia } from '@helia/interface' +import { HeliaError } from '@helia/interface/errors' +import { createId } from './handlers/id.js' +import { logger } from '@libp2p/logger' +import type { Source } from 'it-stream-types' +import type { Pushable } from 'it-pushable' +import { HELIA_RPC_PROTOCOL } from '@helia/rpc-protocol' +import { RPCCallRequest, RPCCallResponseType, RPCCallResponse } from '@helia/rpc-protocol/rpc' +import { decode, encode } from 'it-length-prefixed' +import { pushable } from 'it-pushable' +import type { Uint8ArrayList } from 'uint8arraylist' +import * as ucans from '@ucans/ucans' + +const log = logger('helia:grpc-server') + +export interface GRPCServerConfig { + helia: Helia + serviceDID: string + ownerDID: string +} + +export interface UnaryResponse { + value: ResponseType + metadata: Record +} + +export interface Service { + insecure?: true + handle: (options: Uint8Array, source: Source, sink: Pushable, signal: AbortSignal) => Promise +} + +class RPCError extends HeliaError { + constructor (message: string, code: string) { + super(message, 'RPCError', code) + } +} + +export async function createHeliaGrpcServer (config: GRPCServerConfig): Promise { + const { helia } = config + + const services: Record = { + '/id': createId(config) + } + + await helia.libp2p.handle(HELIA_RPC_PROTOCOL, ({ stream }) => { + const controller = new AbortController() + const outputStream = pushable() + const inputStream = pushable() + + Promise.resolve().then(async () => { + await stream.sink(encode()(outputStream)) + }) + .catch(err => { + log.error('error writing to stream', err) + controller.abort() + }) + + Promise.resolve().then(async () => { + let started = false + + for await (const buf of decode()(stream.source)) { + if (!started) { + // first message is request + started = true + + const request = RPCCallRequest.decode(buf) + + log('incoming RPC request %s %s', request.method, request.resource) + + const service = services[request.resource] + + if (service == null) { + log('no handler defined for %s %s', request.method, request.resource) + const error = new RPCError(`Request path "${request.resource}" unimplemented`, 'ERR_PATH_UNIMPLEMENTED') + + // no handler for path + outputStream.push(RPCCallResponse.encode({ + type: RPCCallResponseType.error, + errorName: error.name, + errorMessage: error.message, + errorStack: error.stack, + errorCode: error.code + })) + outputStream.end() + return + } + + if (service.insecure == null) { + // authorize request + const result = await ucans.verify(request.authorization, { + audience: config.serviceDID, + isRevoked: async ucan => false, + requiredCapabilities: [{ + capability: { + with: { scheme: "service", hierPart: request.resource }, + can: { namespace: "service", segments: [ request.method ] } + }, + rootIssuer: config.ownerDID + }] + }) + + if (!result.ok) { + log('authorization failed for %s %s', request.method, request.resource) + const error = new RPCError(`Authorisation failed for ${request.method} ${request.resource}`, 'ERR_AUTHORIZATION_FAILED') + + // no handler for path + outputStream.push(RPCCallResponse.encode({ + type: RPCCallResponseType.error, + errorName: error.name, + errorMessage: error.message, + errorStack: error.stack, + errorCode: error.code + })) + outputStream.end() + return + } + } + + service.handle(request.options, inputStream, outputStream, controller.signal) + .then(() => { + log.error('handler succeeded for %s %s', request.method, request.resource) + }) + .catch(err => { + log.error('handler failed for %s %s', request.method, request.resource, err) + outputStream.push(RPCCallResponse.encode({ + type: RPCCallResponseType.error, + errorName: err.name, + errorMessage: err.message, + errorStack: err.stack, + errorCode: err.code + })) + }) + .finally(() => { + log('handler finished for %s %s', request.method, request.resource) + inputStream.end() + outputStream.end() + }) + + continue + } + + // stream all other input to the handler + inputStream.push(buf) + } + }) + .catch(err => { + log.error('stream errored', err) + + stream.abort(err) + controller.abort() + }) + .finally(() => { + inputStream.end() + }) + }) +} diff --git a/packages/rpc-server/src/utils/multiaddr-to-url.ts b/packages/rpc-server/src/utils/multiaddr-to-url.ts new file mode 100644 index 00000000..94497ec9 --- /dev/null +++ b/packages/rpc-server/src/utils/multiaddr-to-url.ts @@ -0,0 +1,22 @@ +import type { Multiaddr } from '@multiformats/multiaddr' +import { InvalidParametersError } from '@helia/interface/errors' + +export function multiaddrToUrl (addr: Multiaddr): URL { + const protoNames = addr.protoNames() + + if (protoNames.length !== 3) { + throw new InvalidParametersError('Helia gRPC address format incorrect') + } + + if (protoNames[0] !== 'ip4' && protoNames[0] !== 'ip6') { + throw new InvalidParametersError('Helia gRPC address format incorrect') + } + + if (protoNames[1] !== 'tcp' && protoNames[2] !== 'ws') { + throw new InvalidParametersError('Helia gRPC address format incorrect') + } + + const { host, port } = addr.toOptions() + + return new URL(`ws://${host}:${port}`) +} diff --git a/packages/rpc-server/tsconfig.json b/packages/rpc-server/tsconfig.json new file mode 100644 index 00000000..021053e1 --- /dev/null +++ b/packages/rpc-server/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface" + }, + { + "path": "../rpc-protocol" + } + ] +} diff --git a/packages/unixfs/LICENSE b/packages/unixfs/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/unixfs/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/unixfs/LICENSE-APACHE b/packages/unixfs/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/unixfs/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/unixfs/LICENSE-MIT b/packages/unixfs/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/unixfs/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/unixfs/README.md b/packages/unixfs/README.md new file mode 100644 index 00000000..ffff3cb3 --- /dev/null +++ b/packages/unixfs/README.md @@ -0,0 +1,53 @@ +# @helia/unixfs + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> A Helia-compatible wrapper for UnixFS + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/unixfs/package.json b/packages/unixfs/package.json new file mode 100644 index 00000000..03777f7c --- /dev/null +++ b/packages/unixfs/package.json @@ -0,0 +1,152 @@ +{ + "name": "@helia/unixfs", + "version": "0.0.0", + "description": "A Helia-compatible wrapper for UnixFS", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/unixfs#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" + }, + "dependencies": { + "@helia/interface": "~0.0.0", + "interface-blockstore": "^3.0.2", + "ipfs-unixfs-exporter": "^9.0.2", + "multiformats": "^10.0.3" + }, + "devDependencies": { + "aegir": "^37.7.5" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/unixfs/src/index.ts b/packages/unixfs/src/index.ts new file mode 100644 index 00000000..bee49416 --- /dev/null +++ b/packages/unixfs/src/index.ts @@ -0,0 +1,58 @@ +import type { FileSystem, CatOptions } from '@helia/interface' +import { exporter } from 'ipfs-unixfs-exporter' +import type { CID } from 'multiformats' +import type { Blockstore } from 'interface-blockstore' +import { NotAFileError, NoContentError } from '@helia/interface/errors' +import type { ReadableStream } from 'node:stream/web' + +export interface UnixFSComponents { + blockstore: Blockstore +} + +class UnixFS { + private readonly components: UnixFSComponents + + constructor (components: UnixFSComponents) { + this.components = components + } + + cat (cid: CID, options: CatOptions = {}): ReadableStream { + const blockstore = this.components.blockstore + + const byteSource: UnderlyingByteSource = { + type: 'bytes', + async start (controller) { + const result = await exporter(cid, blockstore) + + if (result.type !== 'file') { + throw new NotAFileError() + } + + if (result.content == null) { + throw new NoContentError() + } + + try { + for await (const buf of result.content({ + offset: options.offset, + length: options.length + })) { + // TODO: backpressure? + controller.enqueue(buf) + } + } finally { + controller.close() + } + } + } + + // @ts-expect-error types are broken? + return new ReadableStream(byteSource) + } +} + +export function unixfs () { + return function createUnixfs (components: UnixFSComponents): FileSystem { + return new UnixFS(components) + } +} diff --git a/packages/unixfs/tsconfig.json b/packages/unixfs/tsconfig.json new file mode 100644 index 00000000..4c0bdf77 --- /dev/null +++ b/packages/unixfs/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../interface" + } + ] +} From 9e6fd323aefbc5ec588e1e6a2338fed3fb729000 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 23 Dec 2022 08:10:51 +0000 Subject: [PATCH 02/18] chore: linting --- packages/rpc-server/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rpc-server/src/index.ts b/packages/rpc-server/src/index.ts index 43128c69..f647ff82 100644 --- a/packages/rpc-server/src/index.ts +++ b/packages/rpc-server/src/index.ts @@ -92,8 +92,8 @@ export async function createHeliaGrpcServer (config: GRPCServerConfig): Promise< isRevoked: async ucan => false, requiredCapabilities: [{ capability: { - with: { scheme: "service", hierPart: request.resource }, - can: { namespace: "service", segments: [ request.method ] } + with: { scheme: 'service', hierPart: request.resource }, + can: { namespace: 'service', segments: [request.method] } }, rootIssuer: config.ownerDID }] From e731ead8624c8757789e8cc1936455c4eb9bf603 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 13 Jan 2023 18:54:14 +0000 Subject: [PATCH 03/18] feat: start a unix socket, detect if the server is running --- .github/workflows/main.yml | 9 +- .npmrc | 2 - lerna.json | 10 -- package.json | 36 +++--- packages/cli/package.json | 22 ++-- packages/cli/src/commands/cat.ts | 1 + packages/cli/src/commands/daemon.ts | 89 +++++++++++-- packages/cli/src/commands/id.ts | 1 + packages/cli/src/commands/index.ts | 27 ++-- packages/cli/src/commands/init.ts | 91 ++++++++------ packages/cli/src/commands/status.ts | 42 +++++++ packages/cli/src/index.ts | 160 ++++++++++++++---------- packages/cli/src/utils/create-helia.ts | 17 ++- packages/cli/src/utils/find-helia.ts | 74 ++++++----- packages/cli/src/utils/format.ts | 75 +++++++++++ packages/cli/src/utils/generate-auth.ts | 21 ++++ packages/cli/src/utils/print-help.ts | 52 +++++--- packages/helia/package.json | 10 +- packages/helia/src/index.ts | 6 + packages/interface/package.json | 11 +- packages/rpc-client/package.json | 4 +- packages/rpc-client/src/commands/id.ts | 3 +- packages/rpc-client/src/index.ts | 1 + packages/rpc-client/test/index.spec.ts | 9 ++ packages/rpc-protocol/package.json | 9 +- packages/rpc-protocol/src/rpc.proto | 9 +- packages/rpc-protocol/src/rpc.ts | 26 ++-- packages/rpc-server/package.json | 4 +- packages/rpc-server/src/handlers/id.ts | 6 +- packages/rpc-server/src/index.ts | 11 +- packages/rpc-server/test/index.spec.ts | 7 ++ packages/unixfs/package.json | 8 +- packages/unixfs/test/index.spec.ts | 7 ++ 33 files changed, 589 insertions(+), 271 deletions(-) delete mode 100644 .npmrc delete mode 100644 lerna.json create mode 100644 packages/cli/src/commands/status.ts create mode 100644 packages/cli/src/utils/format.ts create mode 100644 packages/cli/src/utils/generate-auth.ts create mode 100644 packages/rpc-client/test/index.spec.ts create mode 100644 packages/rpc-server/test/index.spec.ts create mode 100644 packages/unixfs/test/index.spec.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de2528a8..4c742230 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -140,16 +140,13 @@ jobs: - uses: ipfs/aegir/actions/cache-node-modules@master - if: ${{ steps.release.outputs.release_created }} name: Run release version - run: npm publish + run: npm run --if-present release env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ steps.release.outputs.release_created }} - name: Publish docs - run: npm run --if-present docs + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - if: ${{ !steps.release.outputs.release_created }} name: Run release rc run: | - npm version `node -p -e "require('./package.json').version"`-`git rev-parse --short HEAD` --no-git-tag-version - npm publish --tag next + npm run --if-present release:rc env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.npmrc b/.npmrc deleted file mode 100644 index c5ebf5e2..00000000 --- a/.npmrc +++ /dev/null @@ -1,2 +0,0 @@ -; package-lock with tarball deps breaks lerna/nx - remove when https://github.com/semantic-release/github/pull/487 is merged -package-lock=false diff --git a/lerna.json b/lerna.json deleted file mode 100644 index 9301e47a..00000000 --- a/lerna.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "lerna": "6.1.0", - "useWorkspaces": true, - "version": "independent", - "command": { - "run": { - "stream": true - } - } -} diff --git a/package.json b/package.json index a76e5c18..2759b5ea 100644 --- a/package.json +++ b/package.json @@ -20,28 +20,28 @@ }, "private": true, "scripts": { - "reset": "lerna run clean && rimraf ./node_modules ./package-lock.json packages/*/node_modules packages/*/package-lock.json packages/*/dist", - "test": "lerna run --concurrency 1 test -- --", - "test:node": "lerna run --concurrency 1 test:node -- --", - "test:chrome": "lerna run --concurrency 1 test:chrome -- --", - "test:chrome-webworker": "lerna --concurrency 1 run test:chrome-webworker -- --", - "test:firefox": "lerna run --concurrency 1 test:firefox -- --", - "test:firefox-webworker": "lerna run --concurrency 1 test:firefox-webworker -- --", - "test:electron-main": "lerna run --concurrency 1 test:electron-main -- --", - "test:electron-renderer": "lerna run --concurrency 1 test:electron-renderer -- --", - "clean": "lerna run clean", - "generate": "lerna run generate", - "build": "lerna run build", - "lint": "lerna run lint", + "reset": "aegir run clean && aegir clean **/node_modules **/package-lock.json", + "test": "aegir run test", + "test:node": "aegir run test:node", + "test:chrome": "aegir run test:chrome", + "test:chrome-webworker": "aegir run test:chrome-webworker", + "test:firefox": "aegir run test:firefox", + "test:firefox-webworker": "aegir run test:firefox-webworker", + "test:electron-main": "aegir run test:electron-main", + "test:electron-renderer": "aegir run test:electron-renderer", + "clean": "aegir run clean", + "generate": "aegir run generate", + "build": "aegir run build", + "lint": "aegir run lint", "docs": "NODE_OPTIONS=--max_old_space_size=4096 aegir docs", "docs:no-publish": "npm run docs -- --publish false", - "dep-check": "lerna run dep-check", - "release": "npm run docs:no-publish && lerna run --concurrency 1 release && npm run docs" + "dep-check": "aegir run dep-check", + "release": "npm run docs:no-publish && npm run release:npm && npm run docs", + "release:npm": "aegir exec npm -- publish", + "release:rc": "aegir release-rc" }, "dependencies": { - "aegir": "^37.7.6", - "lerna": "^6.1.0", - "rimraf": "^3.0.2" + "aegir": "^38.0.0" }, "workspaces": [ "packages/*" diff --git a/packages/cli/package.json b/packages/cli/package.json index 86671dd0..9e86199b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -136,34 +136,36 @@ "release": "aegir release" }, "dependencies": { - "@chainsafe/libp2p-gossipsub": "^5.3.0", - "@chainsafe/libp2p-noise": "^10.2.0", + "@chainsafe/libp2p-gossipsub": "^6.0.0", + "@chainsafe/libp2p-noise": "^11.0.0", "@chainsafe/libp2p-yamux": "^3.0.3", "@helia/interface": "~0.0.0", "@helia/rpc-client": "~0.0.0", "@helia/rpc-server": "~0.0.0", "@helia/unixfs": "~0.0.0", - "@libp2p/kad-dht": "^6.1.1", + "@libp2p/crypto": "^1.0.11", + "@libp2p/kad-dht": "^7.0.0", "@libp2p/logger": "^2.0.2", "@libp2p/mplex": "^7.1.1", - "@libp2p/peer-id": "^1.1.18", - "@libp2p/peer-id-factory": "^1.0.20", + "@libp2p/peer-id": "^2.0.0", + "@libp2p/peer-id-factory": "^2.0.0", "@libp2p/prometheus-metrics": "1.1.3", "@libp2p/tcp": "^6.0.8", "@libp2p/websockets": "^5.0.2", - "@libp2p/webtransport": "^1.0.6", + "@libp2p/webtransport": "^1.0.7", "@multiformats/multiaddr": "^11.1.4", "@ucans/ucans": "^0.11.0-alpha", - "blockstore-datastore-adapter": "^4.0.0", + "blockstore-datastore-adapter": "^5.0.0", "datastore-fs": "^8.0.0", "helia": "~0.0.0", - "libp2p": "next", - "multiformats": "^10.0.3", + "kleur": "^4.1.5", + "libp2p": "0.42.0", + "multiformats": "^11.0.0", "strip-json-comments": "^5.0.0", "uint8arrays": "^4.0.2" }, "devDependencies": { - "aegir": "^37.7.5" + "aegir": "^38.0.0" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/cli/src/commands/cat.ts b/packages/cli/src/commands/cat.ts index 6d7dbda3..d784fda0 100644 --- a/packages/cli/src/commands/cat.ts +++ b/packages/cli/src/commands/cat.ts @@ -8,6 +8,7 @@ interface CatArgs { } export const cat: Command = { + command: 'cat', description: 'Fetch and cat an IPFS path referencing a file', example: '$ helia cat ', offline: true, diff --git a/packages/cli/src/commands/daemon.ts b/packages/cli/src/commands/daemon.ts index d9eca0d9..0bdba06b 100644 --- a/packages/cli/src/commands/daemon.ts +++ b/packages/cli/src/commands/daemon.ts @@ -1,26 +1,64 @@ import type { Command } from './index.js' import { createHelia } from '../utils/create-helia.js' -import { createHeliaGrpcServer } from '@helia/rpc-server' +import { createHeliaRpcServer } from '@helia/rpc-server' import { EdKeypair } from '@ucans/ucans' +import fs from 'node:fs' +import path from 'node:path' +import os from 'node:os' +import { importKey } from '@libp2p/crypto/keys' +import { peerIdFromKeys } from '@libp2p/peer-id' +import { logger } from '@libp2p/logger' + +const log = logger('helia:cli:commands:daemon') interface DaemonArgs { positionals?: string[] } export const daemon: Command = { + command: 'daemon', description: 'Starts a Helia daemon', example: '$ helia daemon', - async execute ({ config, stdout }) { - const helia = await createHelia(config) + async execute ({ directory, stdout }) { + const lockfilePath = path.join(directory, 'helia.pid') + checkPidFile(lockfilePath) - const serverKey = EdKeypair.fromSecretKey(config.grpc.serverKey, { - format: 'base64url' - }) + const helia = await createHelia(directory) + + const keyName = 'rpc-server-key' + const keyPassword = 'temporary-password' + let pem: string + + try { + pem = await helia.libp2p.keychain.exportKey(keyName, keyPassword) + log('loaded rpc server key from libp2p keystore') + } catch (err: any) { + if (err.code !== 'ERR_NOT_FOUND') { + throw err + } + + log('creating rpc server key and storing in libp2p keystore') + await helia.libp2p.keychain.createKey(keyName, 'Ed25519') + pem = await helia.libp2p.keychain.exportKey(keyName, keyPassword) + } + + log('reading rpc server key as peer id') + const privateKey = await importKey(pem, keyPassword) + const peerId = await peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) + + if (peerId.privateKey == null || peerId.publicKey == null) { + throw new Error('Private key missing') + } + + const key = new EdKeypair( + peerId.privateKey.subarray(4), + peerId.publicKey.subarray(4), + false + ) - await createHeliaGrpcServer({ + await createHeliaRpcServer({ helia, - ownerDID: '', - serviceDID: serverKey.did() + serverDid: key.did() }) const id = await helia.id() @@ -30,5 +68,38 @@ export const daemon: Command = { id.multiaddrs.forEach(ma => { stdout.write(`${ma.toString()}\n`) }) + + fs.writeFileSync(lockfilePath, process.pid.toString()) + } +} + +/** + * Check the passed lockfile path exists, if it does it should contain the PID + * of the owning process. Read the file, check if the process with the PID is + * still running, throw an error if it is. + * + * @param pidFilePath + */ +function checkPidFile (pidFilePath: string): void { + if (!fs.existsSync(pidFilePath)) { + return + } + + const pid = Number(fs.readFileSync(pidFilePath, { + encoding: 'utf8' + }).trim()) + + try { + // this will throw if the process does not exist + os.getPriority(pid) + + throw new Error(`Helia already running with pid ${pid}`) + } catch (err: any) { + if (err.message.includes('no such process') === true) { + log('Removing stale pidfile') + fs.rmSync(pidFilePath) + } else { + throw err + } } } diff --git a/packages/cli/src/commands/id.ts b/packages/cli/src/commands/id.ts index 7d87f8d5..d257fe95 100644 --- a/packages/cli/src/commands/id.ts +++ b/packages/cli/src/commands/id.ts @@ -5,6 +5,7 @@ interface IdArgs { } export const id: Command = { + command: 'id', description: 'Print information out this Helia node', example: '$ helia id', offline: true, diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 95b610ac..d6ef21c7 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -2,9 +2,9 @@ import { cat } from './cat.js' import { init } from './init.js' import { daemon } from './daemon.js' import { id } from './id.js' +import { status } from './status.js' import type { Helia } from '@helia/interface' import type { ParseArgsConfig } from 'node:util' -import type { HeliaConfig } from '../index.js' /** * Extends the internal node type to add a description to the options @@ -44,9 +44,7 @@ export interface ParseArgsOptionConfig { description: string } -interface ParseArgsOptionsConfig { - [longOption: string]: ParseArgsOptionConfig -} +type ParseArgsOptionsConfig = Record export interface CommandOptions extends ParseArgsConfig { /** @@ -55,7 +53,12 @@ export interface CommandOptions extends ParseArgsConfig { options?: ParseArgsOptionsConfig } -export interface Command { +export interface Command { + /** + * The command name + */ + command: string + /** * Used to generate help text */ @@ -80,19 +83,25 @@ export interface Command { * Run the command */ execute: (ctx: Context & T) => Promise + + /** + * Subcommands of the current command + */ + subcommands?: Array> } export interface Context { helia: Helia - config: HeliaConfig + directory: string stdin: NodeJS.ReadStream stdout: NodeJS.WriteStream stderr: NodeJS.WriteStream } -export const commands: Record> = { +export const commands: Array> = [ cat, init, daemon, - id -} + id, + status +] diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 3b13b40a..e6922912 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -4,9 +4,10 @@ import path from 'node:path' import fs from 'node:fs/promises' import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory' import { InvalidParametersError } from '@helia/interface/errors' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { EdKeypair } from '@ucans/ucans' import type { PeerId } from '@libp2p/interface-peer-id' +import { logger } from '@libp2p/logger' + +const log = logger('helia:cli:commands:init') interface InitArgs { positionals?: string[] @@ -16,10 +17,15 @@ interface InitArgs { directory: string directoryMode: string configFileMode: string + publicKeyMode: string + privateKeyMode: string } export const init: Command = { + command: 'init', + offline: true, description: 'Initialize the node', + example: '$ helia init', options: { keyType: { description: 'The key type, valid options are "ed25519", "secp256k1" or "rsa"', @@ -54,46 +60,65 @@ export const init: Command = { description: 'If the config file does not exist, create it with this mode', type: 'string', default: '0600' + }, + privateKeyMode: { + description: 'If the config file does not exist, create it with this mode', + type: 'string', + default: '0600' + }, + publicKeyMode: { + description: 'If the config file does not exist, create it with this mode', + type: 'string', + default: '0644' } }, - async execute ({ keyType, bits, directory, directoryMode, configFileMode, port, stdout }) { + async execute ({ keyType, bits, directory, directoryMode, configFileMode, privateKeyMode, publicKeyMode, port, stdout }) { + try { + await fs.readdir(directory) + // don't init if we are already inited + throw new InvalidParametersError(`Cowardly refusing to reinitialize Helia at ${directory}`) + } catch (err: any) { + if (err.code !== 'ENOENT') { + throw err + } + } + const configFile = path.join(directory, 'config.json') - const key = await generateKey(keyType, bits) + const peerId = await generateKey(keyType, bits) - if (key.publicKey == null || key.privateKey == null) { + if (peerId.publicKey == null || peerId.privateKey == null) { throw new InvalidParametersError('Generated PeerId had missing components') } - const serverKeyPair = await EdKeypair.create({ - exportable: true - }) - const serverKey = await serverKeyPair.export('base64url') - + log('create helia dir %s', directory) await fs.mkdir(directory, { recursive: true, mode: parseInt(directoryMode, 8) }) - await fs.writeFile(configFile, ` -{ - // Configuration for the gRPC API - "grpc": { - // A multiaddr that specifies the TCP port the gRPC server is listening on - "address": "/ip4/127.0.0.1/tcp/${port}/ws/p2p/${key.toString()}", - - // The server key used to create ucans for operation permissions - note this is separate - // to the peerId to let you rotate the server key while keeping the same peerId - "serverKey": "${serverKey}" - }, + const publicKeyPath = path.join(directory, 'peer.pub') + log('create public key %s', publicKeyPath) + await fs.writeFile(publicKeyPath, peerId.publicKey, { + mode: parseInt(publicKeyMode, 8), + flag: 'ax' + }) - // The private key portion of the node's PeerId as a base64url encoded string - "peerId": { - "publicKey": "${uint8ArrayToString(key.publicKey, 'base64url')}", - "privateKey": "${uint8ArrayToString(key.privateKey, 'base64url')}" - }, + const privateKeyPath = path.join(directory, 'peer.key') + log('create private key %s', privateKeyPath) + await fs.writeFile(privateKeyPath, peerId.privateKey, { + mode: parseInt(privateKeyMode, 8), + flag: 'ax' + }) + const configFilePath = path.join(directory, 'config.json') + log('create config file %s', configFilePath) + await fs.writeFile(configFilePath, ` +{ // Where blocks are stored - "blocks": "${path.join(directory, 'blocks')}", + "blockstore": "${path.join(directory, 'blocks')}", + + // Where data is stored + "datastore": "${path.join(directory, 'data')}", // libp2p configuration "libp2p": { @@ -101,18 +126,8 @@ export const init: Command = { "listen": [ "/ip4/0.0.0.0/tcp/0", "/ip4/0.0.0.0/tcp/0/ws", - - // this is the gRPC port - "/ip4/0.0.0.0/tcp/${port}/ws" - ], - "announce": [], - "noAnnounce": [ - // this is the gRPC port - "/ip4/0.0.0.0/tcp/${port}/ws" + "/unix${directory}/rpc.sock" ] - }, - "identify": { - } } } diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts new file mode 100644 index 00000000..2d015fd0 --- /dev/null +++ b/packages/cli/src/commands/status.ts @@ -0,0 +1,42 @@ +import type { Command } from './index.js' +import fs from 'node:fs' +import { logger } from '@libp2p/logger' +import type { RootArgs } from '../index.js' +import { findOnlineHelia } from '../utils/find-helia.js' + +const log = logger('helia:cli:commands:status') + +export const status: Command = { + command: 'status', + description: 'Report the status of the Helia daemon', + example: '$ helia status', + offline: true, + async execute ({ directory, rpcAddress, stdout }) { + // socket file? + const socketFilePath = rpcAddress + + if (fs.existsSync(socketFilePath)) { + log(`Found socket file at ${socketFilePath}`) + + const { + helia, libp2p + } = await findOnlineHelia(directory, rpcAddress) + + if (libp2p != null) { + await libp2p.stop() + } + + if (helia == null) { + log(`Removing stale socket file at ${socketFilePath}`) + fs.rmSync(socketFilePath) + } else { + stdout.write('The daemon is running\n') + return + } + } else { + log(`Could not find socket file at ${socketFilePath}`) + } + + stdout.write('The daemon is not running\n') + } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index c311952d..19d3bffd 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -9,23 +9,17 @@ import { Command, commands } from './commands/index.js' import { InvalidParametersError } from '@helia/interface/errors' import { printHelp } from './utils/print-help.js' import { findHelia } from './utils/find-helia.js' -import stripJsonComments from 'strip-json-comments' +import kleur from 'kleur' import type { Helia } from '@helia/interface' import type { Libp2p } from '@libp2p/interface-libp2p' +import type { ParseArgsConfig } from 'node:util' /** * Typedef for the Helia config file */ export interface HeliaConfig { - peerId: { - publicKey: string - privateKey: string - } - grpc: { - address: string - serverKey: string - } - blocks: string + blockstore: string + datastore: string libp2p: { addresses: { listen: string[] @@ -35,102 +29,132 @@ export interface HeliaConfig { } } -interface RootConfig { +export interface RootArgs { + positionals: string[] directory: string help: boolean + rpcAddress: string } -const root: Command = { - description: `Helia is an IPFS implementation in JavaScript - -Subcommands: - -${Object.entries(commands).map(([key, command]) => ` ${key}\t${command.description}`).sort().join('\n')}`, +const root: Command = { + command: 'helia', + description: `${kleur.bold('Helia')} is an ${kleur.cyan('IPFS')} implementation in ${kleur.yellow('JavaScript')}`, + subcommands: commands, options: { directory: { - description: 'The directory to load config from', + description: 'The Helia directory', type: 'string', default: path.join(os.homedir(), '.helia') }, help: { description: 'Show help text', type: 'boolean' + }, + rpcAddress: { + description: 'The multiaddr of the Helia node', + type: 'string', + default: path.join(os.homedir(), '.helia', 'rpc.sock') } }, async execute () {} } -async function main () { - const command = parseArgs({ +function config (options: any): ParseArgsConfig { + return { allowPositionals: true, strict: true, - options: root.options - }) + options + } +} - // @ts-expect-error wat - const configDir = command.values.directory +async function main (): Promise { + const rootCommandArgs = parseArgs(config(root.options)) + const configDir = rootCommandArgs.values.directory - if (configDir == null) { + if (configDir == null || typeof configDir !== 'string') { throw new InvalidParametersError('No config directory specified') } + if (typeof rootCommandArgs.values.rpcAddress !== 'string') { + throw new InvalidParametersError('No RPC address specified') + } + if (!fs.existsSync(configDir)) { + const init = commands.find(command => command.command === 'init') + + if (init == null) { + throw new Error('Could not find init command') + } + // run the init command const parsed = parseArgs({ allowPositionals: true, strict: true, - options: commands.init.options + options: init.options }) - await commands.init.execute({ + await init.execute({ ...parsed.values, positionals: parsed.positionals.slice(1), stdin: process.stdin, stdout: process.stdout, stderr: process.stderr }) + + if (rootCommandArgs.positionals[0] === 'init') { + // if init was specified explicitly we can bail because we just ran init + return + } } - if (command.positionals.length > 0) { - const subCommand = command.positionals[0] - - if (commands[subCommand] != null) { - const com = commands[subCommand] - - // @ts-expect-error wat - if (command.values.help === true) { - printHelp(com, process.stdout) - } else { - const config = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8'))) - - const opts = parseArgs({ - allowPositionals: true, - strict: true, - options: com.options - }) - - let helia: Helia - let libp2p: Libp2p | undefined - - if (subCommand !== 'daemon') { - const res = await findHelia(config, com.offline) - helia = res.helia - libp2p = res.libp2p - } - - await commands[subCommand].execute({ - ...opts.values, - positionals: opts.positionals.slice(1), - // @ts-expect-error wat - helia, - stdin: process.stdin, - stdout: process.stdout, - stderr: process.stderr, - config - }) - - if (libp2p != null) { - await libp2p.stop() - } + if (rootCommandArgs.positionals.length > 0) { + const search: Command = root + let subCommand: Command | undefined + + for (let i = 0; i < rootCommandArgs.positionals.length; i++) { + const positional = rootCommandArgs.positionals[i] + + if (search.subcommands == null) { + break + } + + const sub = search.subcommands.find(c => c.command === positional) + + if (sub != null) { + subCommand = sub + } + } + + if (subCommand == null) { + throw new Error('Command not found') + } + + if (rootCommandArgs.values.help === true) { + printHelp(subCommand, process.stdout) + } else { + const subCommandArgs = parseArgs(config(subCommand.options)) + + let helia: Helia | undefined + let libp2p: Libp2p | undefined + + if (subCommand.command !== 'daemon' && subCommand.command !== 'status') { + const res = await findHelia(configDir, rootCommandArgs.values.rpcAddress, subCommand.offline) + helia = res.helia + libp2p = res.libp2p + } + + await subCommand.execute({ + ...rootCommandArgs.values, + ...subCommandArgs.values, + positionals: subCommandArgs.positionals.slice(1), + helia, + stdin: process.stdin, + stdout: process.stdout, + stderr: process.stderr, + directory: configDir + }) + + if (libp2p != null) { + await libp2p.stop() } return diff --git a/packages/cli/src/utils/create-helia.ts b/packages/cli/src/utils/create-helia.ts index cf9b5ab6..ecac37e4 100644 --- a/packages/cli/src/utils/create-helia.ts +++ b/packages/cli/src/utils/create-helia.ts @@ -6,7 +6,6 @@ import { BlockstoreDatastoreAdapter } from 'blockstore-datastore-adapter' import { unixfs } from '@helia/unixfs' import { createLibp2p } from 'libp2p' import { peerIdFromKeys } from '@libp2p/peer-id' -import { fromString as uint8ArrayFromString } from 'uint8arrays' import { tcp } from '@libp2p/tcp' import { webSockets } from '@libp2p/websockets' import { noise } from '@chainsafe/libp2p-noise' @@ -14,21 +13,29 @@ import { yamux } from '@chainsafe/libp2p-yamux' import { mplex } from '@libp2p/mplex' import { gossipsub } from '@chainsafe/libp2p-gossipsub' import { kadDHT } from '@libp2p/kad-dht' +import stripJsonComments from 'strip-json-comments' +import fs from 'node:fs' +import path from 'node:path' -export async function createHelia (config: HeliaConfig, offline: boolean = false): Promise { +export async function createHelia (configDir: string, offline: boolean = false): Promise { + const config: HeliaConfig = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8'))) const peerId = await peerIdFromKeys( - uint8ArrayFromString(config.peerId.publicKey, 'base64url'), - uint8ArrayFromString(config.peerId.privateKey, 'base64url') + fs.readFileSync(path.join(configDir, 'peer.pub')), + fs.readFileSync(path.join(configDir, 'peer.key')) ) + const datastore = new FsDatastore(config.datastore) + return await createHeliaNode({ - blockstore: new BlockstoreDatastoreAdapter(new FsDatastore(config.blocks)), + blockstore: new BlockstoreDatastoreAdapter(new FsDatastore(config.blockstore)), + datastore, filesystems: [ unixfs() ], libp2p: await createLibp2p({ start: !offline, peerId, + datastore, addresses: config.libp2p.addresses, identify: { host: { diff --git a/packages/cli/src/utils/find-helia.ts b/packages/cli/src/utils/find-helia.ts index b6476ec8..ba242ff2 100644 --- a/packages/cli/src/utils/find-helia.ts +++ b/packages/cli/src/utils/find-helia.ts @@ -1,5 +1,4 @@ import type { Helia } from '@helia/interface' -import type { HeliaConfig } from '../index.js' import { createHeliaRpcClient } from '@helia/rpc-client' import { multiaddr } from '@multiformats/multiaddr' import { createHelia } from './create-helia.js' @@ -13,14 +12,36 @@ import { logger } from '@libp2p/logger' const log = logger('helia:cli:utils:find-helia') -export async function findHelia (config: HeliaConfig, offline: boolean = false): Promise<{ helia: Helia, libp2p: Libp2p | undefined }> { - let libp2p: Libp2p | undefined - let helia: Helia | undefined +export async function findHelia (configDir: string, rpcAddress: string, offline: boolean = false): Promise<{ helia: Helia, libp2p: Libp2p | undefined }> { + let { + libp2p, helia + } = await findOnlineHelia(configDir, rpcAddress) + if (helia == null) { + log('connecting to existing helia node failed') + + // could not connect to running node, start the server + if (!offline) { + log('could not create client and command cannot be run in offline mode') + throw new Error('Could not connect to Helia - is the node running?') + } + + // return an offline node + log('create offline helia node') + helia = await createHelia(configDir, offline) + } + + return { + helia, + libp2p + } +} + +export async function findOnlineHelia (configDir: string, rpcAddress: string): Promise<{ helia?: Helia, libp2p?: Libp2p }> { try { log('create libp2p node') // create a dial-only libp2p node - libp2p = await createLibp2p({ + const libp2p = await createLibp2p({ transports: [ tcp(), webSockets() @@ -34,12 +55,24 @@ export async function findHelia (config: HeliaConfig, offline: boolean = false): ] }) - log('create helia client') - helia = await createHeliaRpcClient({ - multiaddr: multiaddr(config.grpc.address), - libp2p, - authorization: 'sshh' - }) + let helia: Helia | undefined + + try { + log('create helia client') + helia = await createHeliaRpcClient({ + multiaddr: multiaddr(`/unix/${rpcAddress}`), + libp2p, + user: `${process.env.USER}`, + authorization: 'sshh' + }) + } catch { + await libp2p.stop() + } + + return { + helia, + libp2p + } } catch (err: any) { log('could not create helia client', err) @@ -48,22 +81,5 @@ export async function findHelia (config: HeliaConfig, offline: boolean = false): } } - if (helia == null) { - log('connecting to existing helia node failed') - - // could not connect to running node, start the server - if (!offline) { - log('could not create client and command cannot be run in offline mode') - throw new Error('Could not connect to Helia - is the node running?') - } - - // return an offline node - log('create offline helia node') - helia = await createHelia(config, offline) - } - - return { - helia, - libp2p - } + return {} } diff --git a/packages/cli/src/utils/format.ts b/packages/cli/src/utils/format.ts new file mode 100644 index 00000000..010d068b --- /dev/null +++ b/packages/cli/src/utils/format.ts @@ -0,0 +1,75 @@ +import kleur from 'kleur' + +export function formatter (stdout: NodeJS.WriteStream, items: Formatable[]): void { + items.forEach(item => stdout.write(item())) +} + +export interface Formatable { + (): string +} + +export function header (string: string): Formatable { + return (): string => { + return `\n${string}\n` + } +} + +export function subheader (string: string): Formatable { + return (): string => { + return `\n${string}\n` + } +} + +export function paragraph (string: string): Formatable { + return (): string => { + return kleur.white(`\n${string}\n`) + } +} + +export function table (rows: FormatableRow[]): Formatable { + const cellLengths: string[] = [] + + for (const row of rows) { + const cells = row() + + for (let i = 0; i < cells.length; i++) { + const textLength = cells[i].length + + if (cellLengths[i] == null || cellLengths[i].length < textLength) { + cellLengths[i] = new Array(textLength).fill(' ').join('') + } + } + } + + return (): string => { + const output: string[] = [] + + for (const row of rows) { + const cells = row() + const text: string[] = [] + + for (let i = 0; i < cells.length; i++) { + const cell = cells[i] + text.push((cell + cellLengths[i]).substring(0, cellLengths[i].length)) + } + + output.push(text.join(' ') + '\n') + } + + return output.join('') + } +} + +export interface FormatableRow { + rowLengths: number[] + (): string[] +} + +export function row (...cells: string[]): FormatableRow { + const formatable = (): string[] => { + return cells + } + formatable.rowLengths = cells.map(str => str.length) + + return formatable +} diff --git a/packages/cli/src/utils/generate-auth.ts b/packages/cli/src/utils/generate-auth.ts new file mode 100644 index 00000000..3b6da3a1 --- /dev/null +++ b/packages/cli/src/utils/generate-auth.ts @@ -0,0 +1,21 @@ +import { EdKeypair, build, encode } from '@ucans/ucans' + +export async function generateAuth (serverKey: string): Promise { + const issuer = EdKeypair.fromSecretKey(serverKey, { + format: 'base64url' + }) + + const userKey = await EdKeypair.create() + + const clientUcan = await build({ + issuer, + audience: userKey.did(), + expiration: (Date.now() / 1000) + (60 * 60 * 24), + capabilities: [{ + with: { scheme: 'service', hierPart: '/cat' }, + can: { namespace: 'service', segments: ['GET'] } + }] + }) + + return encode(clientUcan) +} diff --git a/packages/cli/src/utils/print-help.ts b/packages/cli/src/utils/print-help.ts index 6daf75d6..64abbd7a 100644 --- a/packages/cli/src/utils/print-help.ts +++ b/packages/cli/src/utils/print-help.ts @@ -1,25 +1,47 @@ import type { Command } from '../commands/index.js' +import * as format from './format.js' +import type { Formatable } from './format.js' +import kleur from 'kleur' export function printHelp (command: Command, stdout: NodeJS.WriteStream): void { - stdout.write('\n') - stdout.write(`${command.description}\n`) - stdout.write('\n') + const items: Formatable[] = [ + format.header(command.description) + ] if (command.example != null) { - stdout.write('Example:\n') - stdout.write('\n') - stdout.write(`${command.example}\n`) - stdout.write('\n') + items.push( + format.subheader('Example:'), + format.paragraph(command.example) + ) } - const options = Object.entries(command.options ?? {}) - - if (options.length > 0) { - stdout.write('Options:\n') + if (command.subcommands != null) { + items.push( + format.subheader('Subcommands:'), + format.table( + command.subcommands.map(command => format.row( + ` ${command.command}`, + kleur.white(command.description) + )) + ) + ) + } - Object.entries(command.options ?? {}).forEach(([key, option]) => { - stdout.write(` --${key}\t${option.description}\n`) - }) - stdout.write('\n') + if (command.options != null) { + items.push( + format.subheader('Options:'), + format.table( + Object.entries(command.options).map(([key, option]) => format.row( + ` --${key}`, + kleur.white(option.description), + option.default != null ? kleur.grey(`[default: ${option.default}]`) : '' + )) + ) + ) } + + format.formatter( + stdout, + items + ) } diff --git a/packages/helia/package.json b/packages/helia/package.json index 023077df..1105b208 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -141,14 +141,14 @@ "@helia/interface": "~0.0.0", "@libp2p/interface-libp2p": "^1.0.0", "@ucans/ucans": "^0.11.0-alpha", - "interface-blockstore": "^3.0.2", - "interface-datastore": "^3.0.2", - "ipfs-bitswap": "^14.0.0", + "interface-blockstore": "^4.0.0", + "interface-datastore": "^7.0.3", + "ipfs-bitswap": "^15.0.0", "merge-options": "^3.0.4", - "multiformats": "^10.0.3" + "multiformats": "^11.0.0" }, "devDependencies": { - "aegir": "^37.7.5" + "aegir": "^38.0.0" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts index 99e300e8..1cb59838 100644 --- a/packages/helia/src/index.ts +++ b/packages/helia/src/index.ts @@ -22,6 +22,7 @@ import type { Helia, FileSystem } from '@helia/interface' import type { Libp2p } from '@libp2p/interface-libp2p' import type { Blockstore } from 'interface-blockstore' import type { AbortOptions } from '@libp2p/interfaces' +import type { Datastore } from 'interface-datastore' export interface CatOptions extends AbortOptions { offset?: number @@ -48,6 +49,11 @@ export interface HeliaInit { */ blockstore: Blockstore + /** + * The datastore is where data is stored + */ + datastore: Datastore + /** * Helia supports multiple filesystem implementations */ diff --git a/packages/interface/package.json b/packages/interface/package.json index 7abc0f7d..c3f12504 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -148,20 +148,13 @@ "lint": "aegir lint", "dep-check": "aegir dep-check", "build": "aegir build", - "test": "aegir test", - "test:chrome": "aegir test -t browser --cov", - "test:chrome-webworker": "aegir test -t webworker", - "test:firefox": "aegir test -t browser -- --browser firefox", - "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", - "test:node": "aegir test -t node --cov", - "test:electron-main": "aegir test -t electron-main", "release": "aegir release" }, "dependencies": { - "multiformats": "^10.0.3" + "multiformats": "^11.0.0" }, "devDependencies": { - "aegir": "^37.7.5" + "aegir": "^38.0.0" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json index 98c2bd1b..2f59cfe5 100644 --- a/packages/rpc-client/package.json +++ b/packages/rpc-client/package.json @@ -140,12 +140,12 @@ "dependencies": { "@helia/interface": "~0.0.0", "@helia/rpc-protocol": "~0.0.0", - "@libp2p/peer-id": "^1.1.18", + "@libp2p/peer-id": "^2.0.0", "@multiformats/multiaddr": "^11.1.4", "it-pb-stream": "^2.0.3" }, "devDependencies": { - "aegir": "^37.7.5" + "aegir": "^38.0.0" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/rpc-client/src/commands/id.ts b/packages/rpc-client/src/commands/id.ts index b7dc72fc..5085d109 100644 --- a/packages/rpc-client/src/commands/id.ts +++ b/packages/rpc-client/src/commands/id.ts @@ -7,7 +7,7 @@ import { peerIdFromString } from '@libp2p/peer-id' import type { HeliaRpcClientConfig } from '../index.js' import { pbStream } from 'it-pb-stream' -export function createId (config: HeliaRpcClientConfig) { +export function createId (config: HeliaRpcClientConfig): Helia['id'] { const id: Helia['id'] = async (options = {}) => { const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) @@ -15,6 +15,7 @@ export function createId (config: HeliaRpcClientConfig) { stream.writePB({ resource: '/id', method: 'GET', + user: config.user, authorization: config.authorization, options: IdOptions.encode({ ...options, diff --git a/packages/rpc-client/src/index.ts b/packages/rpc-client/src/index.ts index bdd3e608..ff115b45 100644 --- a/packages/rpc-client/src/index.ts +++ b/packages/rpc-client/src/index.ts @@ -6,6 +6,7 @@ import type { Multiaddr } from '@multiformats/multiaddr' export interface HeliaRpcClientConfig { multiaddr: Multiaddr libp2p: Libp2p + user: string authorization: string } diff --git a/packages/rpc-client/test/index.spec.ts b/packages/rpc-client/test/index.spec.ts new file mode 100644 index 00000000..86e6f958 --- /dev/null +++ b/packages/rpc-client/test/index.spec.ts @@ -0,0 +1,9 @@ +/* eslint-env mocha */ + +import '../src/index.js' + +describe('rpc-client', () => { + it('should work', async () => { + + }) +}) diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json index 1eee241a..5ebedb8c 100644 --- a/packages/rpc-protocol/package.json +++ b/packages/rpc-protocol/package.json @@ -152,13 +152,6 @@ "lint": "aegir lint", "dep-check": "aegir dep-check", "build": "aegir build", - "test": "aegir test", - "test:chrome": "aegir test -t browser --cov", - "test:chrome-webworker": "aegir test -t webworker", - "test:firefox": "aegir test -t browser -- --browser firefox", - "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", - "test:node": "aegir test -t node --cov", - "test:electron-main": "aegir test -t electron-main", "release": "aegir release", "generate": "protons src/*.proto" }, @@ -166,7 +159,7 @@ "protons-runtime": "^4.0.1" }, "devDependencies": { - "aegir": "^37.7.5", + "aegir": "^38.0.0", "protons": "^6.0.1" }, "typedoc": { diff --git a/packages/rpc-protocol/src/rpc.proto b/packages/rpc-protocol/src/rpc.proto index 95c9b745..8430f177 100644 --- a/packages/rpc-protocol/src/rpc.proto +++ b/packages/rpc-protocol/src/rpc.proto @@ -1,10 +1,11 @@ syntax = "proto3"; message RPCCallRequest { - string resource = 1; - string method = 2; - string authorization = 3; - bytes options = 4; + string user = 1; + string resource = 2; + string method = 3; + string authorization = 4; + bytes options = 5; } message RPCCallResponse { diff --git a/packages/rpc-protocol/src/rpc.ts b/packages/rpc-protocol/src/rpc.ts index 095b385e..3e72e34d 100644 --- a/packages/rpc-protocol/src/rpc.ts +++ b/packages/rpc-protocol/src/rpc.ts @@ -8,6 +8,7 @@ import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' export interface RPCCallRequest { + user: string resource: string method: string authorization: string @@ -24,23 +25,28 @@ export namespace RPCCallRequest { w.fork() } - if (opts.writeDefaults === true || obj.resource !== '') { + if (opts.writeDefaults === true || obj.user !== '') { w.uint32(10) + w.string(obj.user) + } + + if (opts.writeDefaults === true || obj.resource !== '') { + w.uint32(18) w.string(obj.resource) } if (opts.writeDefaults === true || obj.method !== '') { - w.uint32(18) + w.uint32(26) w.string(obj.method) } if (opts.writeDefaults === true || obj.authorization !== '') { - w.uint32(26) + w.uint32(34) w.string(obj.authorization) } if (opts.writeDefaults === true || (obj.options != null && obj.options.byteLength > 0)) { - w.uint32(34) + w.uint32(42) w.bytes(obj.options) } @@ -49,6 +55,7 @@ export namespace RPCCallRequest { } }, (reader, length) => { const obj: any = { + user: '', resource: '', method: '', authorization: '', @@ -62,15 +69,18 @@ export namespace RPCCallRequest { switch (tag >>> 3) { case 1: - obj.resource = reader.string() + obj.user = reader.string() break case 2: - obj.method = reader.string() + obj.resource = reader.string() break case 3: - obj.authorization = reader.string() + obj.method = reader.string() break case 4: + obj.authorization = reader.string() + break + case 5: obj.options = reader.bytes() break default: @@ -209,7 +219,7 @@ enum __RPCCallResponseTypeValues { } export namespace RPCCallResponseType { - export const codec = () => { + export const codec = (): Codec => { return enumeration(__RPCCallResponseTypeValues) } } diff --git a/packages/rpc-server/package.json b/packages/rpc-server/package.json index ed0dfb34..a1eca50e 100644 --- a/packages/rpc-server/package.json +++ b/packages/rpc-server/package.json @@ -141,14 +141,14 @@ "@helia/interface": "~0.0.0", "@helia/rpc-protocol": "~0.0.0", "@libp2p/logger": "^2.0.2", - "@libp2p/peer-id": "^1.1.18", + "@libp2p/peer-id": "^2.0.0", "@ucans/ucans": "^0.11.0-alpha", "it-length-prefixed": "^8.0.4", "it-pushable": "^3.1.2", "it-stream-types": "^1.0.5" }, "devDependencies": { - "aegir": "^37.7.5" + "aegir": "^38.0.0" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/rpc-server/src/handlers/id.ts b/packages/rpc-server/src/handlers/id.ts index edb8b46b..34875e82 100644 --- a/packages/rpc-server/src/handlers/id.ts +++ b/packages/rpc-server/src/handlers/id.ts @@ -4,9 +4,9 @@ import { peerIdFromString } from '@libp2p/peer-id' import type { Source } from 'it-stream-types' import type { Pushable } from 'it-pushable' import type { Uint8ArrayList } from 'uint8arraylist' -import type { GRPCServerConfig, Service } from '../index.js' +import type { RPCServerConfig, Service } from '../index.js' -export function createId (config: GRPCServerConfig): Service { +export function createId (config: RPCServerConfig): Service { return { insecure: true, async handle (options: Uint8Array, input: Source, output: Pushable, signal: AbortSignal): Promise { @@ -23,7 +23,7 @@ export function createId (config: GRPCServerConfig): Service { message: IdResponse.encode({ ...result, peerId: result.peerId.toString(), - serverDid: config.serviceDID, + serverDid: config.serverDid, multiaddrs: result.multiaddrs.map(ma => ma.toString()) }) }) diff --git a/packages/rpc-server/src/index.ts b/packages/rpc-server/src/index.ts index f647ff82..37c9274b 100644 --- a/packages/rpc-server/src/index.ts +++ b/packages/rpc-server/src/index.ts @@ -13,10 +13,9 @@ import * as ucans from '@ucans/ucans' const log = logger('helia:grpc-server') -export interface GRPCServerConfig { +export interface RPCServerConfig { helia: Helia - serviceDID: string - ownerDID: string + serverDid: string } export interface UnaryResponse { @@ -35,7 +34,7 @@ class RPCError extends HeliaError { } } -export async function createHeliaGrpcServer (config: GRPCServerConfig): Promise { +export async function createHeliaRpcServer (config: RPCServerConfig): Promise { const { helia } = config const services: Record = { @@ -88,14 +87,14 @@ export async function createHeliaGrpcServer (config: GRPCServerConfig): Promise< if (service.insecure == null) { // authorize request const result = await ucans.verify(request.authorization, { - audience: config.serviceDID, + audience: request.user, isRevoked: async ucan => false, requiredCapabilities: [{ capability: { with: { scheme: 'service', hierPart: request.resource }, can: { namespace: 'service', segments: [request.method] } }, - rootIssuer: config.ownerDID + rootIssuer: config.serverDid }] }) diff --git a/packages/rpc-server/test/index.spec.ts b/packages/rpc-server/test/index.spec.ts new file mode 100644 index 00000000..97bedd10 --- /dev/null +++ b/packages/rpc-server/test/index.spec.ts @@ -0,0 +1,7 @@ +import '../src/index.js' + +describe('rpc-server', () => { + it('should work', async () => { + + }) +}) diff --git a/packages/unixfs/package.json b/packages/unixfs/package.json index 03777f7c..77bd22d6 100644 --- a/packages/unixfs/package.json +++ b/packages/unixfs/package.json @@ -139,12 +139,12 @@ }, "dependencies": { "@helia/interface": "~0.0.0", - "interface-blockstore": "^3.0.2", - "ipfs-unixfs-exporter": "^9.0.2", - "multiformats": "^10.0.3" + "interface-blockstore": "^4.0.0", + "ipfs-unixfs-exporter": "^10.0.0", + "multiformats": "^11.0.0" }, "devDependencies": { - "aegir": "^37.7.5" + "aegir": "^38.0.0" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/unixfs/test/index.spec.ts b/packages/unixfs/test/index.spec.ts new file mode 100644 index 00000000..b074f02e --- /dev/null +++ b/packages/unixfs/test/index.spec.ts @@ -0,0 +1,7 @@ +import '../src/index.js' + +describe('unixfs', () => { + it('should work', async () => { + + }) +}) From d68d9b43b551f3f18e8261ddc04ee9ef6e2f6046 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Sat, 14 Jan 2023 08:55:59 +0000 Subject: [PATCH 04/18] chore: fix deps --- packages/cli/package.json | 8 ++++---- packages/cli/src/utils/create-helia.ts | 4 +++- packages/helia/package.json | 4 ++-- packages/interface/README.md | 9 --------- packages/interface/package.json | 5 +++++ packages/rpc-client/package.json | 3 ++- packages/rpc-protocol/README.md | 9 --------- packages/rpc-protocol/package.json | 3 ++- packages/rpc-server/package.json | 4 +++- 9 files changed, 21 insertions(+), 28 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 9e86199b..daf2224b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -144,6 +144,8 @@ "@helia/rpc-server": "~0.0.0", "@helia/unixfs": "~0.0.0", "@libp2p/crypto": "^1.0.11", + "@libp2p/interface-libp2p": "^1.1.0", + "@libp2p/interface-peer-id": "^2.0.0", "@libp2p/kad-dht": "^7.0.0", "@libp2p/logger": "^2.0.2", "@libp2p/mplex": "^7.1.1", @@ -152,8 +154,7 @@ "@libp2p/prometheus-metrics": "1.1.3", "@libp2p/tcp": "^6.0.8", "@libp2p/websockets": "^5.0.2", - "@libp2p/webtransport": "^1.0.7", - "@multiformats/multiaddr": "^11.1.4", + "@multiformats/multiaddr": "^11.1.5", "@ucans/ucans": "^0.11.0-alpha", "blockstore-datastore-adapter": "^5.0.0", "datastore-fs": "^8.0.0", @@ -161,8 +162,7 @@ "kleur": "^4.1.5", "libp2p": "0.42.0", "multiformats": "^11.0.0", - "strip-json-comments": "^5.0.0", - "uint8arrays": "^4.0.2" + "strip-json-comments": "^5.0.0" }, "devDependencies": { "aegir": "^38.0.0" diff --git a/packages/cli/src/utils/create-helia.ts b/packages/cli/src/utils/create-helia.ts index ecac37e4..3c8efcfa 100644 --- a/packages/cli/src/utils/create-helia.ts +++ b/packages/cli/src/utils/create-helia.ts @@ -11,6 +11,7 @@ import { webSockets } from '@libp2p/websockets' import { noise } from '@chainsafe/libp2p-noise' import { yamux } from '@chainsafe/libp2p-yamux' import { mplex } from '@libp2p/mplex' +import { prometheusMetrics } from '@libp2p/prometheus-metrics' import { gossipsub } from '@chainsafe/libp2p-gossipsub' import { kadDHT } from '@libp2p/kad-dht' import stripJsonComments from 'strip-json-comments' @@ -54,7 +55,8 @@ export async function createHelia (configDir: string, offline: boolean = false): mplex() ], pubsub: gossipsub(), - dht: kadDHT() + dht: kadDHT(), + metrics: prometheusMetrics() }) }) } diff --git a/packages/helia/package.json b/packages/helia/package.json index 1105b208..adeb72dd 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -139,8 +139,8 @@ }, "dependencies": { "@helia/interface": "~0.0.0", - "@libp2p/interface-libp2p": "^1.0.0", - "@ucans/ucans": "^0.11.0-alpha", + "@libp2p/interface-libp2p": "^1.1.0", + "@libp2p/interfaces": "^3.2.0", "interface-blockstore": "^4.0.0", "interface-datastore": "^7.0.3", "ipfs-bitswap": "^15.0.0", diff --git a/packages/interface/README.md b/packages/interface/README.md index 9f0f1c0c..94d30350 100644 --- a/packages/interface/README.md +++ b/packages/interface/README.md @@ -10,7 +10,6 @@ ## Table of contents - [Install](#install) - - [Browser ` -``` - ## API Docs - diff --git a/packages/interface/package.json b/packages/interface/package.json index c3f12504..f43ba7a5 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -151,6 +151,11 @@ "release": "aegir release" }, "dependencies": { + "@libp2p/interface-libp2p": "^1.1.0", + "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interfaces": "^3.2.0", + "@multiformats/multiaddr": "^11.1.5", + "interface-blockstore": "^4.0.0", "multiformats": "^11.0.0" }, "devDependencies": { diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json index 2f59cfe5..da254e4b 100644 --- a/packages/rpc-client/package.json +++ b/packages/rpc-client/package.json @@ -140,8 +140,9 @@ "dependencies": { "@helia/interface": "~0.0.0", "@helia/rpc-protocol": "~0.0.0", + "@libp2p/interface-libp2p": "^1.1.0", "@libp2p/peer-id": "^2.0.0", - "@multiformats/multiaddr": "^11.1.4", + "@multiformats/multiaddr": "^11.1.5", "it-pb-stream": "^2.0.3" }, "devDependencies": { diff --git a/packages/rpc-protocol/README.md b/packages/rpc-protocol/README.md index bb27636b..a187000a 100644 --- a/packages/rpc-protocol/README.md +++ b/packages/rpc-protocol/README.md @@ -10,7 +10,6 @@ ## Table of contents - [Install](#install) - - [Browser ` -``` - ## API Docs - diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json index 5ebedb8c..aafb4aeb 100644 --- a/packages/rpc-protocol/package.json +++ b/packages/rpc-protocol/package.json @@ -156,7 +156,8 @@ "generate": "protons src/*.proto" }, "dependencies": { - "protons-runtime": "^4.0.1" + "protons-runtime": "^4.0.1", + "uint8arraylist": "^2.4.3" }, "devDependencies": { "aegir": "^38.0.0", diff --git a/packages/rpc-server/package.json b/packages/rpc-server/package.json index a1eca50e..9b97c8a7 100644 --- a/packages/rpc-server/package.json +++ b/packages/rpc-server/package.json @@ -142,10 +142,12 @@ "@helia/rpc-protocol": "~0.0.0", "@libp2p/logger": "^2.0.2", "@libp2p/peer-id": "^2.0.0", + "@multiformats/multiaddr": "^11.1.5", "@ucans/ucans": "^0.11.0-alpha", "it-length-prefixed": "^8.0.4", "it-pushable": "^3.1.2", - "it-stream-types": "^1.0.5" + "it-stream-types": "^1.0.5", + "uint8arraylist": "^2.4.3" }, "devDependencies": { "aegir": "^38.0.0" From 752bd4471b48479295894dcf3fbd809b3322c037 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 16 Jan 2023 19:01:30 +0000 Subject: [PATCH 05/18] chore: create helia dir in os-specific location, expose blockstore over rpc --- packages/cli/package.json | 4 +- packages/cli/src/commands/add.ts | 38 ++ packages/cli/src/commands/cat.ts | 10 - packages/cli/src/commands/index.ts | 2 + packages/cli/src/commands/init.ts | 122 +++- packages/cli/src/index.ts | 87 +-- packages/cli/src/utils/create-helia.ts | 30 +- packages/cli/src/utils/find-helia-dir.ts | 16 + packages/cli/src/utils/find-helia.ts | 3 +- packages/helia/src/commands/cat.ts | 26 - packages/helia/src/index.ts | 18 +- packages/interface/package.json | 1 + packages/interface/src/index.ts | 19 +- packages/rpc-client/package.json | 3 +- .../src/commands/blockstore/delete.ts | 38 ++ .../rpc-client/src/commands/blockstore/get.ts | 44 ++ .../rpc-client/src/commands/blockstore/has.ts | 44 ++ .../rpc-client/src/commands/blockstore/put.ts | 39 ++ packages/rpc-protocol/package.json | 4 + packages/rpc-protocol/src/blockstore.proto | 50 ++ packages/rpc-protocol/src/blockstore.ts | 643 ++++++++++++++++++ packages/rpc-server/package.json | 2 + .../src/handlers/blockstore/delete.ts | 29 + .../rpc-server/src/handlers/blockstore/get.ts | 30 + .../rpc-server/src/handlers/blockstore/has.ts | 30 + .../rpc-server/src/handlers/blockstore/put.ts | 29 + packages/rpc-server/src/handlers/id.ts | 26 +- packages/rpc-server/src/index.ts | 32 +- packages/unixfs/package.json | 1 + packages/unixfs/src/index.ts | 13 +- packages/unixfs/test/index.spec.ts | 2 +- 31 files changed, 1264 insertions(+), 171 deletions(-) create mode 100644 packages/cli/src/commands/add.ts create mode 100644 packages/cli/src/utils/find-helia-dir.ts delete mode 100644 packages/helia/src/commands/cat.ts create mode 100644 packages/rpc-client/src/commands/blockstore/delete.ts create mode 100644 packages/rpc-client/src/commands/blockstore/get.ts create mode 100644 packages/rpc-client/src/commands/blockstore/has.ts create mode 100644 packages/rpc-client/src/commands/blockstore/put.ts create mode 100644 packages/rpc-protocol/src/blockstore.proto create mode 100644 packages/rpc-protocol/src/blockstore.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/delete.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/get.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/has.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/put.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index daf2224b..75893654 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -161,8 +161,8 @@ "helia": "~0.0.0", "kleur": "^4.1.5", "libp2p": "0.42.0", - "multiformats": "^11.0.0", - "strip-json-comments": "^5.0.0" + "strip-json-comments": "^5.0.0", + "uint8arrays": "^4.0.3" }, "devDependencies": { "aegir": "^38.0.0" diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts new file mode 100644 index 00000000..86273c8f --- /dev/null +++ b/packages/cli/src/commands/add.ts @@ -0,0 +1,38 @@ +import type { Command } from './index.js' +import { unixfs } from '@helia/unixfs' + +interface AddArgs { + positionals?: string[] + fs: string +} + +export const add: Command = { + command: 'add', + description: 'Add a file or directory to your helia node', + example: '$ helia add path/to/file.txt', + offline: true, + options: { + fs: { + description: 'Which filesystem to use', + type: 'string', + default: 'unixfs' + } + }, + async execute ({ positionals, helia, stdout }) { + const options = {} + + const fs = unixfs(helia) + + if (positionals == null || positionals.length === 0) { + // import from stdin + } else { + for (const input of positionals) { + for await (const result of fs.add({ + path: input + }, options)) { + stdout.write(result.cid.toString() + '\n') + } + } + } + } +} diff --git a/packages/cli/src/commands/cat.ts b/packages/cli/src/commands/cat.ts index d784fda0..d0ae9eb8 100644 --- a/packages/cli/src/commands/cat.ts +++ b/packages/cli/src/commands/cat.ts @@ -1,4 +1,3 @@ -import { CID } from 'multiformats' import type { Command } from './index.js' interface CatArgs { @@ -28,14 +27,5 @@ export const cat: Command = { if (positionals == null || positionals.length === 0) { throw new TypeError('Missing positionals') } - - const cid = CID.parse(positionals[0]) - - for await (const buf of helia.cat(cid, { - offset: offset == null ? undefined : parseInt(offset), - length: length == null ? undefined : parseInt(length) - })) { - stdout.write(buf) - } } } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index d6ef21c7..a3491cca 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -1,3 +1,4 @@ +import { add } from './add.js' import { cat } from './cat.js' import { init } from './init.js' import { daemon } from './daemon.js' @@ -99,6 +100,7 @@ export interface Context { } export const commands: Array> = [ + add, cat, init, daemon, diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index e6922912..ad69ae6b 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -1,11 +1,18 @@ import type { Command } from './index.js' -import os from 'node:os' import path from 'node:path' import fs from 'node:fs/promises' import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory' import { InvalidParametersError } from '@helia/interface/errors' import type { PeerId } from '@libp2p/interface-peer-id' import { logger } from '@libp2p/logger' +import { createLibp2p } from 'libp2p' +import { FsDatastore } from 'datastore-fs' +import { noise } from '@chainsafe/libp2p-noise' +import { tcp } from '@libp2p/tcp' +import { yamux } from '@chainsafe/libp2p-yamux' +import { findHeliaDir } from '../utils/find-helia-dir.js' +import { randomBytes } from '@libp2p/crypto' +import { toString as uint8ArrayToString } from 'uint8arrays/to-string' const log = logger('helia:cli:commands:init') @@ -18,9 +25,15 @@ interface InitArgs { directoryMode: string configFileMode: string publicKeyMode: string - privateKeyMode: string + keychainPassword: string + keychainSalt: string + storePassword: boolean } +// NIST SP 800-132 +const NIST_MINIMUM_SALT_LENGTH = 128 / 8 +const SALT_LENGTH = Math.ceil(NIST_MINIMUM_SALT_LENGTH / 3) * 3 // no base64 padding + export const init: Command = { command: 'init', offline: true, @@ -39,40 +52,44 @@ export const init: Command = { short: 'b', default: '2048' }, - port: { - description: 'Where to listen for incoming gRPC connections', - type: 'string', - short: 'p', - default: '49832' - }, directory: { - description: 'The directory to store config in', + description: 'The directory to store data in', type: 'string', short: 'd', - default: path.join(os.homedir(), '.helia') + default: findHeliaDir() }, directoryMode: { - description: 'If the config file directory does not exist, create it with this mode', + description: 'Create the data directory with this mode', type: 'string', default: '0700' }, configFileMode: { - description: 'If the config file does not exist, create it with this mode', - type: 'string', - default: '0600' - }, - privateKeyMode: { - description: 'If the config file does not exist, create it with this mode', + description: 'Create the config file with this mode', type: 'string', default: '0600' }, publicKeyMode: { - description: 'If the config file does not exist, create it with this mode', + description: 'Create the public key file with this mode', type: 'string', default: '0644' + }, + keychainPassword: { + description: 'The libp2p keychain will use a key derived from this password for encryption operations', + type: 'string', + default: uint8ArrayToString(randomBytes(20), 'base64') + }, + keychainSalt: { + description: 'The libp2p keychain will use use this salt when deriving the key from the password', + type: 'string', + default: uint8ArrayToString(randomBytes(SALT_LENGTH), 'base64') + }, + storePassword: { + description: 'If true, store the password used to derive the key used by the libp2p keychain in the config file', + type: 'boolean', + default: true } }, - async execute ({ keyType, bits, directory, directoryMode, configFileMode, privateKeyMode, publicKeyMode, port, stdout }) { + async execute ({ keyType, bits, directory, directoryMode, configFileMode, publicKeyMode, stdout, keychainPassword, keychainSalt, storePassword }) { try { await fs.readdir(directory) // don't init if we are already inited @@ -83,7 +100,18 @@ export const init: Command = { } } - const configFile = path.join(directory, 'config.json') + const configFilePath = path.join(directory, 'helia.json') + + try { + await fs.access(configFilePath) + // don't init if we are already inited + throw new InvalidParametersError(`Cowardly refusing to overwrite Helia config file at ${configFilePath}`) + } catch (err: any) { + if (err.code !== 'ENOENT') { + throw err + } + } + const peerId = await generateKey(keyType, bits) if (peerId.publicKey == null || peerId.privateKey == null) { @@ -96,21 +124,41 @@ export const init: Command = { mode: parseInt(directoryMode, 8) }) + const datastorePath = path.join(directory, 'data') + + // create a dial-only libp2p node configured with the datastore in the helia + // directory - this will store the peer id securely in the keychain + const node = await createLibp2p({ + peerId, + datastore: new FsDatastore(datastorePath, { + createIfMissing: true + }), + transports: [ + tcp() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + keychain: { + pass: keychainPassword, + dek: { + salt: keychainSalt + } + } + }) + await node.stop() + + // now write the public key from the PeerId out for use by the RPC client const publicKeyPath = path.join(directory, 'peer.pub') log('create public key %s', publicKeyPath) - await fs.writeFile(publicKeyPath, peerId.publicKey, { + await fs.writeFile(publicKeyPath, peerId.toString() + '\n', { mode: parseInt(publicKeyMode, 8), flag: 'ax' }) - const privateKeyPath = path.join(directory, 'peer.key') - log('create private key %s', privateKeyPath) - await fs.writeFile(privateKeyPath, peerId.privateKey, { - mode: parseInt(privateKeyMode, 8), - flag: 'ax' - }) - - const configFilePath = path.join(directory, 'config.json') log('create config file %s', configFilePath) await fs.writeFile(configFilePath, ` { @@ -118,7 +166,7 @@ export const init: Command = { "blockstore": "${path.join(directory, 'blocks')}", // Where data is stored - "datastore": "${path.join(directory, 'data')}", + "datastore": "${datastorePath}", // libp2p configuration "libp2p": { @@ -126,8 +174,20 @@ export const init: Command = { "listen": [ "/ip4/0.0.0.0/tcp/0", "/ip4/0.0.0.0/tcp/0/ws", + + // this is the rpc socket + "/unix${directory}/rpc.sock" + ], + "noAnnounce": [ + // do not announce the rpc socket to the outside world "/unix${directory}/rpc.sock" ] + }, + "keychain": { + "salt": "${keychainSalt}"${storePassword +? `, + "password": "${keychainPassword}"` +: ''} } } } @@ -136,7 +196,7 @@ export const init: Command = { flag: 'ax' }) - stdout.write(`Wrote config file to ${configFile}\n`) + stdout.write(`Wrote config file to ${configFilePath}\n`) } } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 19d3bffd..a4fe4e1f 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -13,6 +13,7 @@ import kleur from 'kleur' import type { Helia } from '@helia/interface' import type { Libp2p } from '@libp2p/interface-libp2p' import type { ParseArgsConfig } from 'node:util' +import { findHeliaDir } from './utils/find-helia-dir.js' /** * Typedef for the Helia config file @@ -26,6 +27,10 @@ export interface HeliaConfig { announce: string[] noAnnounce: string[] } + keychain: { + salt: string + password?: string + } } } @@ -42,14 +47,11 @@ const root: Command = { subcommands: commands, options: { directory: { - description: 'The Helia directory', + description: 'The directory used by Helia to store config and data', type: 'string', - default: path.join(os.homedir(), '.helia') - }, - help: { - description: 'Show help text', - type: 'boolean' + default: findHeliaDir() }, + rpcAddress: { description: 'The multiaddr of the Helia node', type: 'string', @@ -63,7 +65,13 @@ function config (options: any): ParseArgsConfig { return { allowPositionals: true, strict: true, - options + options: { + help: { + description: 'Show help text', + type: 'boolean' + }, + ...options + } } } @@ -79,6 +87,11 @@ async function main (): Promise { throw new InvalidParametersError('No RPC address specified') } + if (rootCommandArgs.values.help === true) { + printHelp(root, process.stdout) + return + } + if (!fs.existsSync(configDir)) { const init = commands.find(command => command.command === 'init') @@ -87,11 +100,13 @@ async function main (): Promise { } // run the init command - const parsed = parseArgs({ - allowPositionals: true, - strict: true, - options: init.options - }) + const parsed = parseArgs(config(init.options)) + + if (parsed.values.help === true) { + printHelp(init, process.stdout) + return + } + await init.execute({ ...parsed.values, positionals: parsed.positionals.slice(1), @@ -128,37 +143,33 @@ async function main (): Promise { throw new Error('Command not found') } - if (rootCommandArgs.values.help === true) { - printHelp(subCommand, process.stdout) - } else { - const subCommandArgs = parseArgs(config(subCommand.options)) + const subCommandArgs = parseArgs(config(subCommand.options)) - let helia: Helia | undefined - let libp2p: Libp2p | undefined + let helia: Helia | undefined + let libp2p: Libp2p | undefined - if (subCommand.command !== 'daemon' && subCommand.command !== 'status') { - const res = await findHelia(configDir, rootCommandArgs.values.rpcAddress, subCommand.offline) - helia = res.helia - libp2p = res.libp2p - } + if (subCommand.command !== 'daemon' && subCommand.command !== 'status') { + const res = await findHelia(configDir, rootCommandArgs.values.rpcAddress, subCommand.offline) + helia = res.helia + libp2p = res.libp2p + } - await subCommand.execute({ - ...rootCommandArgs.values, - ...subCommandArgs.values, - positionals: subCommandArgs.positionals.slice(1), - helia, - stdin: process.stdin, - stdout: process.stdout, - stderr: process.stderr, - directory: configDir - }) - - if (libp2p != null) { - await libp2p.stop() - } + await subCommand.execute({ + ...rootCommandArgs.values, + ...subCommandArgs.values, + positionals: subCommandArgs.positionals.slice(1), + helia, + stdin: process.stdin, + stdout: process.stdout, + stderr: process.stderr, + directory: configDir + }) - return + if (libp2p != null) { + await libp2p.stop() } + + return } // no command specified, print help diff --git a/packages/cli/src/utils/create-helia.ts b/packages/cli/src/utils/create-helia.ts index 3c8efcfa..1d3fa510 100644 --- a/packages/cli/src/utils/create-helia.ts +++ b/packages/cli/src/utils/create-helia.ts @@ -3,9 +3,7 @@ import type { HeliaConfig } from '../index.js' import { createHelia as createHeliaNode } from 'helia' import { FsDatastore } from 'datastore-fs' import { BlockstoreDatastoreAdapter } from 'blockstore-datastore-adapter' -import { unixfs } from '@helia/unixfs' import { createLibp2p } from 'libp2p' -import { peerIdFromKeys } from '@libp2p/peer-id' import { tcp } from '@libp2p/tcp' import { webSockets } from '@libp2p/websockets' import { noise } from '@chainsafe/libp2p-noise' @@ -17,25 +15,27 @@ import { kadDHT } from '@libp2p/kad-dht' import stripJsonComments from 'strip-json-comments' import fs from 'node:fs' import path from 'node:path' +import * as readline from 'node:readline/promises' export async function createHelia (configDir: string, offline: boolean = false): Promise { - const config: HeliaConfig = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'config.json'), 'utf-8'))) - const peerId = await peerIdFromKeys( - fs.readFileSync(path.join(configDir, 'peer.pub')), - fs.readFileSync(path.join(configDir, 'peer.key')) - ) - + const config: HeliaConfig = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'helia.json'), 'utf-8'))) const datastore = new FsDatastore(config.datastore) + let password = config.libp2p.keychain.password + + if (config.libp2p.keychain.password == null) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }) + password = await rl.question('Enter libp2p keychain password: ') + } + return await createHeliaNode({ blockstore: new BlockstoreDatastoreAdapter(new FsDatastore(config.blockstore)), datastore, - filesystems: [ - unixfs() - ], libp2p: await createLibp2p({ start: !offline, - peerId, datastore, addresses: config.libp2p.addresses, identify: { @@ -43,6 +43,12 @@ export async function createHelia (configDir: string, offline: boolean = false): agentVersion: 'helia/0.0.0' } }, + keychain: { + pass: password, + dek: { + salt: config.libp2p.keychain.salt + } + }, transports: [ tcp(), webSockets() diff --git a/packages/cli/src/utils/find-helia-dir.ts b/packages/cli/src/utils/find-helia-dir.ts new file mode 100644 index 00000000..2a60f51f --- /dev/null +++ b/packages/cli/src/utils/find-helia-dir.ts @@ -0,0 +1,16 @@ +import os from 'node:os' +import path from 'node:path' + +export function findHeliaDir (): string { + if (process.env.XDG_DATA_HOME != null) { + return process.env.XDG_DATA_HOME + } + + const platform = os.platform() + + if (platform === 'darwin') { + return path.join(`${process.env.HOME}`, 'Library', 'helia') + } + + return path.join(`${process.env.HOME}`, '.helia') +} diff --git a/packages/cli/src/utils/find-helia.ts b/packages/cli/src/utils/find-helia.ts index ba242ff2..f4225210 100644 --- a/packages/cli/src/utils/find-helia.ts +++ b/packages/cli/src/utils/find-helia.ts @@ -65,7 +65,8 @@ export async function findOnlineHelia (configDir: string, rpcAddress: string): P user: `${process.env.USER}`, authorization: 'sshh' }) - } catch { + } catch (err: any) { + log('could not create helia rpc client', err) await libp2p.stop() } diff --git a/packages/helia/src/commands/cat.ts b/packages/helia/src/commands/cat.ts deleted file mode 100644 index 6eeae346..00000000 --- a/packages/helia/src/commands/cat.ts +++ /dev/null @@ -1,26 +0,0 @@ -import mergeOpts from 'merge-options' -import type { Blockstore } from 'interface-blockstore' -import type { CID } from 'multiformats' -import type { CatOptions } from '../index.js' -import type { ReadableStream } from 'node:stream/web' -import type { FileSystem } from '@helia/interface' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) - -const defaultOptions: CatOptions = { - offset: 0, - length: Infinity -} - -interface CatComponents { - blockstore: Blockstore - filesystems: FileSystem[] -} - -export function createCat (components: CatComponents) { - return function cat (cid: CID, options: CatOptions = {}): ReadableStream { - options = mergeOptions(defaultOptions, options) - - return components.filesystems[0].cat(cid, options) - } -} diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts index 1cb59838..2c2186a1 100644 --- a/packages/helia/src/index.ts +++ b/packages/helia/src/index.ts @@ -15,10 +15,9 @@ * ``` */ -import { createCat } from './commands/cat.js' import { createId } from './commands/id.js' import { createBitswap } from 'ipfs-bitswap' -import type { Helia, FileSystem } from '@helia/interface' +import type { Helia } from '@helia/interface' import type { Libp2p } from '@libp2p/interface-libp2p' import type { Blockstore } from 'interface-blockstore' import type { AbortOptions } from '@libp2p/interfaces' @@ -32,7 +31,7 @@ export interface CatOptions extends AbortOptions { export interface HeliaComponents { libp2p: Libp2p blockstore: Blockstore - filesystems: FileSystem[] + datastore: Datastore } /** @@ -53,11 +52,6 @@ export interface HeliaInit { * The datastore is where data is stored */ datastore: Datastore - - /** - * Helia supports multiple filesystem implementations - */ - filesystems: Array<(components: HeliaComponents) => FileSystem> } /** @@ -74,17 +68,15 @@ export async function createHelia (init: HeliaInit): Promise { const components: HeliaComponents = { libp2p: init.libp2p, blockstore, - filesystems: [] + datastore: init.datastore } - components.filesystems = init.filesystems.map(fs => fs(components)) - const helia: Helia = { libp2p: init.libp2p, blockstore, + datastore: init.datastore, - id: createId(components), - cat: createCat(components) + id: createId(components) } return helia diff --git a/packages/interface/package.json b/packages/interface/package.json index f43ba7a5..e3a00c7d 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -156,6 +156,7 @@ "@libp2p/interfaces": "^3.2.0", "@multiformats/multiaddr": "^11.1.5", "interface-blockstore": "^4.0.0", + "interface-datastore": "^7.0.3", "multiformats": "^11.0.0" }, "devDependencies": { diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index e9fd333b..7ee3410b 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -16,15 +16,10 @@ import type { Libp2p } from '@libp2p/interface-libp2p' import type { Blockstore } from 'interface-blockstore' -import type { CID } from 'multiformats/cid' -import type { ReadableStream } from 'node:stream/web' import type { AbortOptions } from '@libp2p/interfaces' import type { PeerId } from '@libp2p/interface-peer-id' import type { Multiaddr } from '@multiformats/multiaddr' - -export interface FileSystem { - cat: (cid: CID, options?: CatOptions) => ReadableStream -} +import type { Datastore } from 'interface-datastore' /** * The API presented by a Helia node. @@ -46,13 +41,6 @@ export interface Helia { */ id: (options?: IdOptions) => Promise - /** - * The cat method reads files sequentially, returning the bytes as a stream. - * - * If the passed CID does not resolve to a file, an error will be thrown. - */ - cat: (cid: CID, options?: CatOptions) => ReadableStream - /** * The underlying libp2p node */ @@ -62,6 +50,11 @@ export interface Helia { * Where the blocks are stored */ blockstore: Blockstore + + /** + * A key/value store + */ + datastore: Datastore } export interface CatOptions extends AbortOptions { diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json index da254e4b..6f14dd4a 100644 --- a/packages/rpc-client/package.json +++ b/packages/rpc-client/package.json @@ -143,7 +143,8 @@ "@libp2p/interface-libp2p": "^1.1.0", "@libp2p/peer-id": "^2.0.0", "@multiformats/multiaddr": "^11.1.5", - "it-pb-stream": "^2.0.3" + "it-pb-stream": "^2.0.3", + "multiformats": "^11.0.0" }, "devDependencies": { "aegir": "^38.0.0" diff --git a/packages/rpc-client/src/commands/blockstore/delete.ts b/packages/rpc-client/src/commands/blockstore/delete.ts new file mode 100644 index 00000000..0471f9a7 --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/delete.ts @@ -0,0 +1,38 @@ +import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { DeleteOptions, DeleteRequest } from '@helia/rpc-protocol/blockstore' +import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' +import type { Helia } from '@helia/interface' +import type { HeliaRpcClientConfig } from '../../index.js' +import { pbStream } from 'it-pb-stream' +import type { CID } from 'multiformats/cid' + +export function createDelete (config: HeliaRpcClientConfig): Helia['blockstore']['delete'] { + const del: Helia['blockstore']['delete'] = async (cid: CID, options = {}) => { + const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) + + const stream = pbStream(duplex) + stream.writePB({ + resource: '/blockstore/delete', + method: 'GET', + user: config.user, + authorization: config.authorization, + options: DeleteOptions.encode({ + ...options + }) + }, RPCCallRequest) + stream.writePB({ + cid: cid.bytes + }, DeleteRequest) + const response = await stream.readPB(RPCCallResponse) + + duplex.close() + + if (response.type === RPCCallResponseType.message) { + return + } + + throw new RPCError(response) + } + + return del +} diff --git a/packages/rpc-client/src/commands/blockstore/get.ts b/packages/rpc-client/src/commands/blockstore/get.ts new file mode 100644 index 00000000..be9c7a47 --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/get.ts @@ -0,0 +1,44 @@ +import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' +import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' +import type { Helia } from '@helia/interface' +import type { HeliaRpcClientConfig } from '../../index.js' +import { pbStream } from 'it-pb-stream' +import type { CID } from 'multiformats/cid' + +export function createGet (config: HeliaRpcClientConfig): Helia['blockstore']['get'] { + const get: Helia['blockstore']['get'] = async (cid: CID, options = {}) => { + const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) + + const stream = pbStream(duplex) + stream.writePB({ + resource: '/blockstore/get', + method: 'GET', + user: config.user, + authorization: config.authorization, + options: GetOptions.encode({ + ...options + }) + }, RPCCallRequest) + stream.writePB({ + cid: cid.bytes + }, GetRequest) + const response = await stream.readPB(RPCCallResponse) + + duplex.close() + + if (response.type === RPCCallResponseType.message) { + if (response.message == null) { + throw new TypeError('RPC response had message type but no message') + } + + const message = GetResponse.decode(response.message) + + return message.block + } + + throw new RPCError(response) + } + + return get +} diff --git a/packages/rpc-client/src/commands/blockstore/has.ts b/packages/rpc-client/src/commands/blockstore/has.ts new file mode 100644 index 00000000..9dab9daf --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/has.ts @@ -0,0 +1,44 @@ +import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { HasOptions, HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' +import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' +import type { Helia } from '@helia/interface' +import type { HeliaRpcClientConfig } from '../../index.js' +import { pbStream } from 'it-pb-stream' +import type { CID } from 'multiformats/cid' + +export function createHas (config: HeliaRpcClientConfig): Helia['blockstore']['has'] { + const has: Helia['blockstore']['has'] = async (cid: CID, options = {}) => { + const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) + + const stream = pbStream(duplex) + stream.writePB({ + resource: '/blockstore/has', + method: 'GET', + user: config.user, + authorization: config.authorization, + options: HasOptions.encode({ + ...options + }) + }, RPCCallRequest) + stream.writePB({ + cid: cid.bytes + }, HasRequest) + const response = await stream.readPB(RPCCallResponse) + + duplex.close() + + if (response.type === RPCCallResponseType.message) { + if (response.message == null) { + throw new TypeError('RPC response had message type but no message') + } + + const message = HasResponse.decode(response.message) + + return message.has + } + + throw new RPCError(response) + } + + return has +} diff --git a/packages/rpc-client/src/commands/blockstore/put.ts b/packages/rpc-client/src/commands/blockstore/put.ts new file mode 100644 index 00000000..690f620b --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/put.ts @@ -0,0 +1,39 @@ +import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { PutOptions, PutRequest } from '@helia/rpc-protocol/blockstore' +import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' +import type { Helia } from '@helia/interface' +import type { HeliaRpcClientConfig } from '../../index.js' +import { pbStream } from 'it-pb-stream' +import type { CID } from 'multiformats/cid' + +export function createPut (config: HeliaRpcClientConfig): Helia['blockstore']['put'] { + const put: Helia['blockstore']['put'] = async (cid: CID, block: Uint8Array, options = {}) => { + const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) + + const stream = pbStream(duplex) + stream.writePB({ + resource: '/blockstore/has', + method: 'GET', + user: config.user, + authorization: config.authorization, + options: PutOptions.encode({ + ...options + }) + }, RPCCallRequest) + stream.writePB({ + cid: cid.bytes, + block + }, PutRequest) + const response = await stream.readPB(RPCCallResponse) + + duplex.close() + + if (response.type === RPCCallResponseType.message) { + return + } + + throw new RPCError(response) + } + + return put +} diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json index aafb4aeb..7b61b040 100644 --- a/packages/rpc-protocol/package.json +++ b/packages/rpc-protocol/package.json @@ -54,6 +54,10 @@ "./rpc": { "types": "./dist/src/rpc.d.ts", "import": "./dist/src/rpc.js" + }, + "./blockstore": { + "types": "./dist/src/blockstore.d.ts", + "import": "./dist/src/blockstore.js" } }, "eslintConfig": { diff --git a/packages/rpc-protocol/src/blockstore.proto b/packages/rpc-protocol/src/blockstore.proto new file mode 100644 index 00000000..4955fc4a --- /dev/null +++ b/packages/rpc-protocol/src/blockstore.proto @@ -0,0 +1,50 @@ +syntax = "proto3"; + +message PutOptions { + +} + +message PutRequest { + bytes cid = 1; + bytes block = 2; +} + +message PutResponse { + +} + +message GetOptions { + +} + +message GetRequest { + bytes cid = 1; +} + +message GetResponse { + bytes block = 1; +} + +message HasOptions { + +} + +message HasRequest { + bytes cid = 1; +} + +message HasResponse { + bool has = 1; +} + +message DeleteOptions { + +} + +message DeleteRequest { + bytes cid = 1; +} + +message DeleteResponse { + +} diff --git a/packages/rpc-protocol/src/blockstore.ts b/packages/rpc-protocol/src/blockstore.ts new file mode 100644 index 00000000..aab54aac --- /dev/null +++ b/packages/rpc-protocol/src/blockstore.ts @@ -0,0 +1,643 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { Codec } from 'protons-runtime' + +export interface PutOptions {} + +export namespace PutOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: PutOptions): Uint8Array => { + return encodeMessage(obj, PutOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutOptions => { + return decodeMessage(buf, PutOptions.codec()) + } +} + +export interface PutRequest { + cid: Uint8Array + block: Uint8Array +} + +export namespace PutRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid) + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.block) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0), + block: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + case 2: + obj.block = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: PutRequest): Uint8Array => { + return encodeMessage(obj, PutRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutRequest => { + return decodeMessage(buf, PutRequest.codec()) + } +} + +export interface PutResponse {} + +export namespace PutResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: PutResponse): Uint8Array => { + return encodeMessage(obj, PutResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutResponse => { + return decodeMessage(buf, PutResponse.codec()) + } +} + +export interface GetOptions {} + +export namespace GetOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: GetOptions): Uint8Array => { + return encodeMessage(obj, GetOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetOptions => { + return decodeMessage(buf, GetOptions.codec()) + } +} + +export interface GetRequest { + cid: Uint8Array +} + +export namespace GetRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: GetRequest): Uint8Array => { + return encodeMessage(obj, GetRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetRequest => { + return decodeMessage(buf, GetRequest.codec()) + } +} + +export interface GetResponse { + block: Uint8Array +} + +export namespace GetResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.block) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + block: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.block = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: GetResponse): Uint8Array => { + return encodeMessage(obj, GetResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse => { + return decodeMessage(buf, GetResponse.codec()) + } +} + +export interface HasOptions {} + +export namespace HasOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: HasOptions): Uint8Array => { + return encodeMessage(obj, HasOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasOptions => { + return decodeMessage(buf, HasOptions.codec()) + } +} + +export interface HasRequest { + cid: Uint8Array +} + +export namespace HasRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: HasRequest): Uint8Array => { + return encodeMessage(obj, HasRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasRequest => { + return decodeMessage(buf, HasRequest.codec()) + } +} + +export interface HasResponse { + has: boolean +} + +export namespace HasResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.has !== false) { + w.uint32(8) + w.bool(obj.has) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + has: false + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.has = reader.bool() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: HasResponse): Uint8Array => { + return encodeMessage(obj, HasResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasResponse => { + return decodeMessage(buf, HasResponse.codec()) + } +} + +export interface DeleteOptions {} + +export namespace DeleteOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: DeleteOptions): Uint8Array => { + return encodeMessage(obj, DeleteOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteOptions => { + return decodeMessage(buf, DeleteOptions.codec()) + } +} + +export interface DeleteRequest { + cid: Uint8Array +} + +export namespace DeleteRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: DeleteRequest): Uint8Array => { + return encodeMessage(obj, DeleteRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteRequest => { + return decodeMessage(buf, DeleteRequest.codec()) + } +} + +export interface DeleteResponse {} + +export namespace DeleteResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: DeleteResponse): Uint8Array => { + return encodeMessage(obj, DeleteResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteResponse => { + return decodeMessage(buf, DeleteResponse.codec()) + } +} diff --git a/packages/rpc-server/package.json b/packages/rpc-server/package.json index 9b97c8a7..1f51b69c 100644 --- a/packages/rpc-server/package.json +++ b/packages/rpc-server/package.json @@ -145,8 +145,10 @@ "@multiformats/multiaddr": "^11.1.5", "@ucans/ucans": "^0.11.0-alpha", "it-length-prefixed": "^8.0.4", + "it-pb-stream": "^2.0.3", "it-pushable": "^3.1.2", "it-stream-types": "^1.0.5", + "multiformats": "^11.0.0", "uint8arraylist": "^2.4.3" }, "devDependencies": { diff --git a/packages/rpc-server/src/handlers/blockstore/delete.ts b/packages/rpc-server/src/handlers/blockstore/delete.ts new file mode 100644 index 00000000..2dfb1236 --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/delete.ts @@ -0,0 +1,29 @@ +import { DeleteRequest, DeleteResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import type { Duplex } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { RPCServerConfig, Service } from '../../index.js' +import { CID } from 'multiformats/cid' +import { pbStream } from 'it-pb-stream' + +export function createDelete (config: RPCServerConfig): Service { + return { + async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { + // const opts = DeleteOptions.decode(options) + const pb = pbStream(stream) + const request = await pb.readPB(DeleteRequest) + const cid = CID.decode(request.cid) + + await config.helia.blockstore.delete(cid, { + signal + }) + + pb.writePB({ + type: RPCCallResponseType.message, + message: DeleteResponse.encode({ + }) + }, + RPCCallResponse) + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/get.ts b/packages/rpc-server/src/handlers/blockstore/get.ts new file mode 100644 index 00000000..cde851ac --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/get.ts @@ -0,0 +1,30 @@ +import { GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import type { Duplex } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { RPCServerConfig, Service } from '../../index.js' +import { CID } from 'multiformats/cid' +import { pbStream } from 'it-pb-stream' + +export function createGet (config: RPCServerConfig): Service { + return { + async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { + // const opts = GetOptions.decode(options) + const pb = pbStream(stream) + const request = await pb.readPB(GetRequest) + const cid = CID.decode(request.cid) + + const block = await config.helia.blockstore.get(cid, { + signal + }) + + pb.writePB({ + type: RPCCallResponseType.message, + message: GetResponse.encode({ + block + }) + }, + RPCCallResponse) + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/has.ts b/packages/rpc-server/src/handlers/blockstore/has.ts new file mode 100644 index 00000000..99c50334 --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/has.ts @@ -0,0 +1,30 @@ +import { HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import type { Duplex } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { RPCServerConfig, Service } from '../../index.js' +import { CID } from 'multiformats/cid' +import { pbStream } from 'it-pb-stream' + +export function createHas (config: RPCServerConfig): Service { + return { + async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { + // const opts = HasOptions.decode(options) + const pb = pbStream(stream) + const request = await pb.readPB(HasRequest) + const cid = CID.decode(request.cid) + + const has = await config.helia.blockstore.has(cid, { + signal + }) + + pb.writePB({ + type: RPCCallResponseType.message, + message: HasResponse.encode({ + has + }) + }, + RPCCallResponse) + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/put.ts b/packages/rpc-server/src/handlers/blockstore/put.ts new file mode 100644 index 00000000..546d7bfb --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/put.ts @@ -0,0 +1,29 @@ +import { PutRequest, PutResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import type { Duplex } from 'it-stream-types' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { RPCServerConfig, Service } from '../../index.js' +import { CID } from 'multiformats/cid' +import { pbStream } from 'it-pb-stream' + +export function createPut (config: RPCServerConfig): Service { + return { + async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { + // const opts = HasOptions.decode(options) + const pb = pbStream(stream) + const request = await pb.readPB(PutRequest) + const cid = CID.decode(request.cid) + + await config.helia.blockstore.put(cid, request.block, { + signal + }) + + pb.writePB({ + type: RPCCallResponseType.message, + message: PutResponse.encode({ + }) + }, + RPCCallResponse) + } + } +} diff --git a/packages/rpc-server/src/handlers/id.ts b/packages/rpc-server/src/handlers/id.ts index 34875e82..d50f1166 100644 --- a/packages/rpc-server/src/handlers/id.ts +++ b/packages/rpc-server/src/handlers/id.ts @@ -1,33 +1,31 @@ import { IdOptions, IdResponse } from '@helia/rpc-protocol/root' import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import { peerIdFromString } from '@libp2p/peer-id' -import type { Source } from 'it-stream-types' -import type { Pushable } from 'it-pushable' +import { pbStream } from 'it-pb-stream' +import type { Duplex } from 'it-stream-types' import type { Uint8ArrayList } from 'uint8arraylist' import type { RPCServerConfig, Service } from '../index.js' export function createId (config: RPCServerConfig): Service { return { insecure: true, - async handle (options: Uint8Array, input: Source, output: Pushable, signal: AbortSignal): Promise { + async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { const opts = IdOptions.decode(options) - + const pb = pbStream(stream) const result = await config.helia.id({ peerId: opts.peerId != null ? peerIdFromString(opts.peerId) : undefined, signal }) - output.push( - RPCCallResponse.encode({ - type: RPCCallResponseType.message, - message: IdResponse.encode({ - ...result, - peerId: result.peerId.toString(), - serverDid: config.serverDid, - multiaddrs: result.multiaddrs.map(ma => ma.toString()) - }) + pb.writePB({ + type: RPCCallResponseType.message, + message: IdResponse.encode({ + ...result, + peerId: result.peerId.toString(), + serverDid: config.serverDid, + multiaddrs: result.multiaddrs.map(ma => ma.toString()) }) - ) + }, RPCCallResponse) } } } diff --git a/packages/rpc-server/src/index.ts b/packages/rpc-server/src/index.ts index 37c9274b..80dcdd04 100644 --- a/packages/rpc-server/src/index.ts +++ b/packages/rpc-server/src/index.ts @@ -2,14 +2,17 @@ import type { Helia } from '@helia/interface' import { HeliaError } from '@helia/interface/errors' import { createId } from './handlers/id.js' import { logger } from '@libp2p/logger' -import type { Source } from 'it-stream-types' -import type { Pushable } from 'it-pushable' +import type { Duplex, Sink, Source } from 'it-stream-types' import { HELIA_RPC_PROTOCOL } from '@helia/rpc-protocol' import { RPCCallRequest, RPCCallResponseType, RPCCallResponse } from '@helia/rpc-protocol/rpc' import { decode, encode } from 'it-length-prefixed' import { pushable } from 'it-pushable' import type { Uint8ArrayList } from 'uint8arraylist' import * as ucans from '@ucans/ucans' +import { createDelete } from './handlers/blockstore/delete.js' +import { createGet } from './handlers/blockstore/get.js' +import { createHas } from './handlers/blockstore/has.js' +import { createPut } from './handlers/blockstore/put.js' const log = logger('helia:grpc-server') @@ -25,7 +28,7 @@ export interface UnaryResponse { export interface Service { insecure?: true - handle: (options: Uint8Array, source: Source, sink: Pushable, signal: AbortSignal) => Promise + handle: (options: Uint8Array, stream: Duplex, signal: AbortSignal) => Promise } class RPCError extends HeliaError { @@ -38,6 +41,10 @@ export async function createHeliaRpcServer (config: RPCServerConfig): Promise = { + '/blockstore/delete': createDelete(config), + '/blockstore/get': createGet(config), + '/blockstore/has': createHas(config), + '/blockstore/put': createPut(config), '/id': createId(config) } @@ -115,7 +122,24 @@ export async function createHeliaRpcServer (config: RPCServerConfig): Promise = async (source: Source) => { + try { + for await (const buf of source) { + outputStream.push(buf) + } + } catch (err: any) { + outputStream.push(RPCCallResponse.encode({ + type: RPCCallResponseType.error, + errorName: err.name, + errorMessage: err.message, + errorStack: err.stack, + errorCode: err.code + })) + outputStream.end() + } + } + + service.handle(request.options, { source: inputStream, sink }, controller.signal) .then(() => { log.error('handler succeeded for %s %s', request.method, request.resource) }) diff --git a/packages/unixfs/package.json b/packages/unixfs/package.json index 77bd22d6..de3bd206 100644 --- a/packages/unixfs/package.json +++ b/packages/unixfs/package.json @@ -141,6 +141,7 @@ "@helia/interface": "~0.0.0", "interface-blockstore": "^4.0.0", "ipfs-unixfs-exporter": "^10.0.0", + "ipfs-unixfs-importer": "^12.0.0", "multiformats": "^11.0.0" }, "devDependencies": { diff --git a/packages/unixfs/src/index.ts b/packages/unixfs/src/index.ts index bee49416..1b085455 100644 --- a/packages/unixfs/src/index.ts +++ b/packages/unixfs/src/index.ts @@ -1,5 +1,6 @@ -import type { FileSystem, CatOptions } from '@helia/interface' +import type { CatOptions, Helia } from '@helia/interface' import { exporter } from 'ipfs-unixfs-exporter' +import { ImportCandidate, importer, ImportResult, UserImporterOptions } from 'ipfs-unixfs-importer' import type { CID } from 'multiformats' import type { Blockstore } from 'interface-blockstore' import { NotAFileError, NoContentError } from '@helia/interface/errors' @@ -16,6 +17,10 @@ class UnixFS { this.components = components } + async * add (source: ImportCandidate, options?: UserImporterOptions): AsyncGenerator { + yield * importer(source, this.components.blockstore, options) + } + cat (cid: CID, options: CatOptions = {}): ReadableStream { const blockstore = this.components.blockstore @@ -51,8 +56,6 @@ class UnixFS { } } -export function unixfs () { - return function createUnixfs (components: UnixFSComponents): FileSystem { - return new UnixFS(components) - } +export function unixfs (helia: Helia): UnixFS { + return new UnixFS(helia) } diff --git a/packages/unixfs/test/index.spec.ts b/packages/unixfs/test/index.spec.ts index b074f02e..de84dfd2 100644 --- a/packages/unixfs/test/index.spec.ts +++ b/packages/unixfs/test/index.spec.ts @@ -1,7 +1,7 @@ import '../src/index.js' describe('unixfs', () => { - it('should work', async () => { + it('should add a Uint8Array', async () => { }) }) From ec6f4efce13b7a6dae83a228fa664ff082d85902 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 27 Jan 2023 10:22:22 +0100 Subject: [PATCH 06/18] feat: ucans for rpc server --- README.md | 2 +- packages/cli/package.json | 14 +- packages/cli/src/commands/add.ts | 105 +++++++-- packages/cli/src/commands/cat.ts | 17 +- packages/cli/src/commands/daemon.ts | 69 +++--- packages/cli/src/commands/id.ts | 1 - packages/cli/src/commands/index.ts | 16 +- packages/cli/src/commands/init.ts | 91 +++++--- packages/cli/src/commands/rpc/index.ts | 18 ++ packages/cli/src/commands/rpc/rmuser.ts | 25 ++ packages/cli/src/commands/rpc/useradd.ts | 41 ++++ packages/cli/src/commands/rpc/users.ts | 18 ++ packages/cli/src/commands/rpc/utils.ts | 38 ++++ packages/cli/src/commands/status.ts | 5 +- packages/cli/src/index.ts | 57 +++-- packages/cli/src/utils/config.ts | 15 ++ packages/cli/src/utils/date-to-mtime.ts | 11 + packages/cli/src/utils/find-helia.ts | 148 ++++++++---- packages/cli/src/utils/glob-source.ts | 95 ++++++++ packages/cli/src/utils/multiaddr-to-url.ts | 6 +- packages/helia/package.json | 4 +- packages/interface/package.json | 6 +- packages/rpc-client/package.json | 2 +- .../src/commands/authorization/get.ts | 44 ++++ .../src/commands/blockstore/delete.ts | 7 +- .../rpc-client/src/commands/blockstore/get.ts | 7 +- .../rpc-client/src/commands/blockstore/has.ts | 7 +- .../rpc-client/src/commands/blockstore/put.ts | 7 +- packages/rpc-client/src/commands/cat.ts | 2 - packages/rpc-client/src/commands/id.ts | 8 +- packages/rpc-client/src/index.ts | 29 ++- packages/rpc-protocol/README.md | 2 +- packages/rpc-protocol/package.json | 14 +- packages/rpc-protocol/src/authorization.proto | 13 ++ packages/rpc-protocol/src/authorization.ts | 171 ++++++++++++++ packages/rpc-protocol/src/root.proto | 9 +- packages/rpc-protocol/src/root.ts | 25 +- packages/rpc-protocol/src/rpc.proto | 9 +- packages/rpc-protocol/src/rpc.ts | 37 ++- packages/rpc-server/package.json | 9 +- .../src/handlers/authorization/get.ts | 75 ++++++ .../src/handlers/blockstore/delete.ts | 17 +- .../rpc-server/src/handlers/blockstore/get.ts | 17 +- .../rpc-server/src/handlers/blockstore/has.ts | 17 +- .../rpc-server/src/handlers/blockstore/put.ts | 17 +- packages/rpc-server/src/handlers/id.ts | 10 +- packages/rpc-server/src/index.ts | 215 ++++++++---------- .../rpc-server/src/utils/multiaddr-to-url.ts | 6 +- packages/unixfs/package.json | 2 +- packages/unixfs/src/index.ts | 2 +- 50 files changed, 1152 insertions(+), 430 deletions(-) create mode 100644 packages/cli/src/commands/rpc/index.ts create mode 100644 packages/cli/src/commands/rpc/rmuser.ts create mode 100644 packages/cli/src/commands/rpc/useradd.ts create mode 100644 packages/cli/src/commands/rpc/users.ts create mode 100644 packages/cli/src/commands/rpc/utils.ts create mode 100644 packages/cli/src/utils/config.ts create mode 100644 packages/cli/src/utils/date-to-mtime.ts create mode 100644 packages/cli/src/utils/glob-source.ts create mode 100644 packages/rpc-client/src/commands/authorization/get.ts delete mode 100644 packages/rpc-client/src/commands/cat.ts create mode 100644 packages/rpc-protocol/src/authorization.proto create mode 100644 packages/rpc-protocol/src/authorization.ts create mode 100644 packages/rpc-server/src/handlers/authorization/get.ts diff --git a/README.md b/README.md index 1e834f5a..7c53d811 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ - [`/packages/helia`](./packages/helia) An implementation of IPFS in JavaScript - [`/packages/interface`](./packages/interface) The Helia API - [`/packages/rpc-client`](./packages/rpc-client) An implementation of IPFS in JavaScript -- [`/packages/rpc-protocol`](./packages/rpc-protocol) gRPC protocol for use by @helia/rpc-client and @helia/rpc-server +- [`/packages/rpc-protocol`](./packages/rpc-protocol) RPC protocol for use by @helia/rpc-client and @helia/rpc-server - [`/packages/rpc-server`](./packages/rpc-server) An implementation of IPFS in JavaScript - [`/packages/unixfs`](./packages/unixfs) A Helia-compatible wrapper for UnixFS diff --git a/packages/cli/package.json b/packages/cli/package.json index 75893654..761c2045 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -144,12 +144,14 @@ "@helia/rpc-server": "~0.0.0", "@helia/unixfs": "~0.0.0", "@libp2p/crypto": "^1.0.11", + "@libp2p/interface-keychain": "^2.0.3", "@libp2p/interface-libp2p": "^1.1.0", - "@libp2p/interface-peer-id": "^2.0.0", + "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interfaces": "^3.3.1", "@libp2p/kad-dht": "^7.0.0", + "@libp2p/keychain": "^0.6.1", "@libp2p/logger": "^2.0.2", "@libp2p/mplex": "^7.1.1", - "@libp2p/peer-id": "^2.0.0", "@libp2p/peer-id-factory": "^2.0.0", "@libp2p/prometheus-metrics": "1.1.3", "@libp2p/tcp": "^6.0.8", @@ -159,8 +161,14 @@ "blockstore-datastore-adapter": "^5.0.0", "datastore-fs": "^8.0.0", "helia": "~0.0.0", + "ipfs-unixfs": "^9.0.0", + "ipfs-unixfs-exporter": "^10.0.0", + "ipfs-unixfs-importer": "^12.0.0", + "it-glob": "^2.0.0", + "it-merge": "^2.0.0", "kleur": "^4.1.5", - "libp2p": "0.42.0", + "libp2p": "^0.42.2", + "multiformats": "^11.0.1", "strip-json-comments": "^5.0.0", "uint8arrays": "^4.0.3" }, diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index 86273c8f..a97f66f5 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -1,8 +1,15 @@ import type { Command } from './index.js' import { unixfs } from '@helia/unixfs' +import merge from 'it-merge' +import path from 'node:path' +import { globSource } from '../utils/glob-source.js' +import fs from 'node:fs' +import { dateToMtime } from '../utils/date-to-mtime.js' +import type { Mtime } from 'ipfs-unixfs' +import type { ImportCandidate, UserImporterOptions } from 'ipfs-unixfs-importer' interface AddArgs { - positionals?: string[] + positionals: string[] fs: string } @@ -10,7 +17,6 @@ export const add: Command = { command: 'add', description: 'Add a file or directory to your helia node', example: '$ helia add path/to/file.txt', - offline: true, options: { fs: { description: 'Which filesystem to use', @@ -19,20 +25,91 @@ export const add: Command = { } }, async execute ({ positionals, helia, stdout }) { - const options = {} - + const options: UserImporterOptions = { + cidVersion: 1, + rawLeaves: true + } const fs = unixfs(helia) - if (positionals == null || positionals.length === 0) { - // import from stdin - } else { - for (const input of positionals) { - for await (const result of fs.add({ - path: input - }, options)) { - stdout.write(result.cid.toString() + '\n') - } - } + for await (const result of fs.add(parsePositionals(positionals), options)) { + stdout.write(result.cid.toString() + '\n') + } + } +} + +async function * parsePositionals (positionals: string[], mode?: number, mtime?: Mtime, hidden?: boolean, recursive?: boolean, preserveMode?: boolean, preserveMtime?: boolean): AsyncGenerator { + if (positionals.length === 0) { + yield { + content: process.stdin, + mode, + mtime + } + return + } + + yield * merge(...positionals.map(file => getSource(file, { + hidden, + recursive, + preserveMode, + preserveMtime, + mode, + mtime + }))) +} + +interface SourceOptions { + hidden?: boolean + recursive?: boolean + preserveMode?: boolean + preserveMtime?: boolean + mode?: number + mtime?: Mtime +} + +async function * getSource (target: string, options: SourceOptions = {}): AsyncGenerator { + const absolutePath = path.resolve(target) + const stats = await fs.promises.stat(absolutePath) + + if (stats.isFile()) { + let mtime = options.mtime + let mode = options.mode + + if (options.preserveMtime === true) { + mtime = dateToMtime(stats.mtime) + } + + if (options.preserveMode === true) { + mode = stats.mode + } + + yield { + path: path.basename(target), + content: fs.createReadStream(absolutePath), + mtime, + mode + } + + return + } + + const dirName = path.basename(absolutePath) + + let pattern = '*' + + if (options.recursive === true) { + pattern = '**/*' + } + + for await (const content of globSource(target, pattern, { + hidden: options.hidden, + preserveMode: options.preserveMode, + preserveMtime: options.preserveMtime, + mode: options.mode, + mtime: options.mtime + })) { + yield { + ...content, + path: `${dirName}${content.path}` } } } diff --git a/packages/cli/src/commands/cat.ts b/packages/cli/src/commands/cat.ts index d0ae9eb8..abbea230 100644 --- a/packages/cli/src/commands/cat.ts +++ b/packages/cli/src/commands/cat.ts @@ -1,4 +1,6 @@ import type { Command } from './index.js' +import { exporter } from 'ipfs-unixfs-exporter' +import { CID } from 'multiformats/cid' interface CatArgs { positionals?: string[] @@ -10,7 +12,6 @@ export const cat: Command = { command: 'cat', description: 'Fetch and cat an IPFS path referencing a file', example: '$ helia cat ', - offline: true, options: { offset: { description: 'Where to start reading the file from', @@ -27,5 +28,19 @@ export const cat: Command = { if (positionals == null || positionals.length === 0) { throw new TypeError('Missing positionals') } + + const cid = CID.parse(positionals[0]) + const entry = await exporter(cid, helia.blockstore, { + offset: offset != null ? Number(offset) : undefined, + length: length != null ? Number(length) : undefined + }) + + if (entry.type !== 'file') { + throw new Error('UnixFS path was not a file') + } + + for await (const buf of entry.content()) { + stdout.write(buf) + } } } diff --git a/packages/cli/src/commands/daemon.ts b/packages/cli/src/commands/daemon.ts index 0bdba06b..45ca4cbf 100644 --- a/packages/cli/src/commands/daemon.ts +++ b/packages/cli/src/commands/daemon.ts @@ -1,73 +1,57 @@ import type { Command } from './index.js' import { createHelia } from '../utils/create-helia.js' import { createHeliaRpcServer } from '@helia/rpc-server' -import { EdKeypair } from '@ucans/ucans' import fs from 'node:fs' import path from 'node:path' import os from 'node:os' -import { importKey } from '@libp2p/crypto/keys' -import { peerIdFromKeys } from '@libp2p/peer-id' import { logger } from '@libp2p/logger' +import { loadRpcKeychain } from './rpc/utils.js' const log = logger('helia:cli:commands:daemon') interface DaemonArgs { positionals?: string[] + authorizationValiditySeconds: number } export const daemon: Command = { command: 'daemon', description: 'Starts a Helia daemon', example: '$ helia daemon', - async execute ({ directory, stdout }) { + online: false, + options: { + authorizationValiditySeconds: { + description: 'How many seconds a request authorization token is valid for', + type: 'string', + default: '5' + } + }, + async execute ({ directory, stdout, authorizationValiditySeconds }) { const lockfilePath = path.join(directory, 'helia.pid') checkPidFile(lockfilePath) - const helia = await createHelia(directory) - - const keyName = 'rpc-server-key' - const keyPassword = 'temporary-password' - let pem: string - - try { - pem = await helia.libp2p.keychain.exportKey(keyName, keyPassword) - log('loaded rpc server key from libp2p keystore') - } catch (err: any) { - if (err.code !== 'ERR_NOT_FOUND') { - throw err - } - - log('creating rpc server key and storing in libp2p keystore') - await helia.libp2p.keychain.createKey(keyName, 'Ed25519') - pem = await helia.libp2p.keychain.exportKey(keyName, keyPassword) - } - - log('reading rpc server key as peer id') - const privateKey = await importKey(pem, keyPassword) - const peerId = await peerIdFromKeys(privateKey.public.bytes, privateKey.bytes) - - if (peerId.privateKey == null || peerId.publicKey == null) { - throw new Error('Private key missing') - } + const rpcSocketFilePath = path.join(directory, 'rpc.sock') + checkRpcSocketFile(rpcSocketFilePath) - const key = new EdKeypair( - peerId.privateKey.subarray(4), - peerId.publicKey.subarray(4), - false - ) + const helia = await createHelia(directory) await createHeliaRpcServer({ helia, - serverDid: key.did() + users: await loadRpcKeychain(directory), + authorizationValiditySeconds: Number(authorizationValiditySeconds) }) const id = await helia.id() stdout.write(`${id.agentVersion} is running\n`) - id.multiaddrs.forEach(ma => { - stdout.write(`${ma.toString()}\n`) - }) + if (id.multiaddrs.length > 0) { + stdout.write('Listening on:\n') + + id.multiaddrs.forEach(ma => { + stdout.write(` ${ma.toString()}\n`) + }) + } fs.writeFileSync(lockfilePath, process.pid.toString()) } @@ -103,3 +87,10 @@ function checkPidFile (pidFilePath: string): void { } } } + +function checkRpcSocketFile (rpcSocketFilePath: string): void { + if (fs.existsSync(rpcSocketFilePath)) { + log('Removing stale rpc socket file') + fs.rmSync(rpcSocketFilePath) + } +} diff --git a/packages/cli/src/commands/id.ts b/packages/cli/src/commands/id.ts index d257fe95..49a354ec 100644 --- a/packages/cli/src/commands/id.ts +++ b/packages/cli/src/commands/id.ts @@ -8,7 +8,6 @@ export const id: Command = { command: 'id', description: 'Print information out this Helia node', example: '$ helia id', - offline: true, async execute ({ helia, stdout }) { const result = await helia.id() diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index a3491cca..0a0865a9 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -6,6 +6,7 @@ import { id } from './id.js' import { status } from './status.js' import type { Helia } from '@helia/interface' import type { ParseArgsConfig } from 'node:util' +import { rpc } from './rpc/index.js' /** * Extends the internal node type to add a description to the options @@ -43,6 +44,11 @@ export interface ParseArgsOptionConfig { * A description used to generate help text */ description: string + + /** + * If specified the value must be in this list + */ + valid?: string[] } type ParseArgsOptionsConfig = Record @@ -71,10 +77,15 @@ export interface Command { example?: string /** - * Specify if this command can be run offline + * Specify if this command can be run offline (default true) */ offline?: boolean + /** + * Specify if this command can be run online (default true) + */ + online?: boolean + /** * Configuration for the command */ @@ -105,5 +116,6 @@ export const commands: Array> = [ init, daemon, id, - status + status, + rpc ] diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index ad69ae6b..dc8afda2 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -5,14 +5,13 @@ import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@li import { InvalidParametersError } from '@helia/interface/errors' import type { PeerId } from '@libp2p/interface-peer-id' import { logger } from '@libp2p/logger' -import { createLibp2p } from 'libp2p' import { FsDatastore } from 'datastore-fs' -import { noise } from '@chainsafe/libp2p-noise' -import { tcp } from '@libp2p/tcp' -import { yamux } from '@chainsafe/libp2p-yamux' import { findHeliaDir } from '../utils/find-helia-dir.js' import { randomBytes } from '@libp2p/crypto' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' +import { KeyChain } from '@libp2p/keychain' +import { loadRpcKeychain } from './rpc/utils.js' +import type { KeyType } from '@libp2p/interface-keychain' const log = logger('helia:cli:commands:init') @@ -28,6 +27,11 @@ interface InitArgs { keychainPassword: string keychainSalt: string storePassword: boolean + rpcKeychainPassword: string + rpcKeychainSalt: string + storeRpcPassword: boolean + rpcUser: string + rpcUserKeyType: KeyType } // NIST SP 800-132 @@ -36,7 +40,7 @@ const SALT_LENGTH = Math.ceil(NIST_MINIMUM_SALT_LENGTH / 3) * 3 // no base64 pad export const init: Command = { command: 'init', - offline: true, + online: false, description: 'Initialize the node', example: '$ helia init', options: { @@ -87,9 +91,35 @@ export const init: Command = { description: 'If true, store the password used to derive the key used by the libp2p keychain in the config file', type: 'boolean', default: true + }, + rpcKeychainPassword: { + description: 'The RPC server keychain will use a key derived from this password for encryption operations', + type: 'string', + default: uint8ArrayToString(randomBytes(20), 'base64') + }, + rpcKeychainSalt: { + description: 'The RPC server keychain will use use this salt when deriving the key from the password', + type: 'string', + default: uint8ArrayToString(randomBytes(SALT_LENGTH), 'base64') + }, + storeRpcPassword: { + description: 'If true, store the password used to derive the key used by the RPC server keychain in the config file', + type: 'boolean', + default: true + }, + rpcUser: { + description: 'The default RPC user', + type: 'string', + default: process.env.USER + }, + rpcUserKeyType: { + description: 'The default RPC user key tupe', + type: 'string', + default: 'Ed25519', + valid: ['RSA', 'Ed25519', 'secp256k1'] } }, - async execute ({ keyType, bits, directory, directoryMode, configFileMode, publicKeyMode, stdout, keychainPassword, keychainSalt, storePassword }) { + async execute ({ keyType, bits, directory, directoryMode, configFileMode, publicKeyMode, stdout, keychainPassword, keychainSalt, storePassword, rpcKeychainPassword, rpcKeychainSalt, storeRpcPassword, rpcUser, rpcUserKeyType }) { try { await fs.readdir(directory) // don't init if we are already inited @@ -125,31 +155,27 @@ export const init: Command = { }) const datastorePath = path.join(directory, 'data') + const rpcDatastorePath = path.join(directory, 'rpc') // create a dial-only libp2p node configured with the datastore in the helia // directory - this will store the peer id securely in the keychain - const node = await createLibp2p({ - peerId, - datastore: new FsDatastore(datastorePath, { - createIfMissing: true - }), - transports: [ - tcp() - ], - connectionEncryption: [ - noise() - ], - streamMuxers: [ - yamux() - ], - keychain: { - pass: keychainPassword, - dek: { - salt: keychainSalt - } + const datastore = new FsDatastore(datastorePath, { + createIfMissing: true + }) + await datastore.open() + const keychain = new KeyChain({ + datastore + }, { + pass: keychainPassword, + dek: { + keyLength: 512 / 8, + iterationCount: 10000, + hash: 'sha2-512', + salt: keychainSalt } }) - await node.stop() + await keychain.importPeer('self', peerId) + await datastore.close() // now write the public key from the PeerId out for use by the RPC client const publicKeyPath = path.join(directory, 'peer.pub') @@ -187,6 +213,15 @@ export const init: Command = { "salt": "${keychainSalt}"${storePassword ? `, "password": "${keychainPassword}"` +: ''} + } + }, + "rpc": { + "datastore": "${rpcDatastorePath}", + "keychain": { + "salt": "${rpcKeychainSalt}"${storeRpcPassword +? `, + "password": "${rpcKeychainPassword}"` : ''} } } @@ -196,6 +231,10 @@ export const init: Command = { flag: 'ax' }) + // create an rpc key for the first user + const rpcKeychain = await loadRpcKeychain(directory) + await rpcKeychain.createKey(`rpc-user-${rpcUser}`, rpcUserKeyType) + stdout.write(`Wrote config file to ${configFilePath}\n`) } } diff --git a/packages/cli/src/commands/rpc/index.ts b/packages/cli/src/commands/rpc/index.ts new file mode 100644 index 00000000..df2fabae --- /dev/null +++ b/packages/cli/src/commands/rpc/index.ts @@ -0,0 +1,18 @@ +import type { Command } from '../index.js' +import { rpcRmuser } from './rmuser.js' +import { rpcUseradd } from './useradd.js' +import { rpcUsers } from './users.js' + +export const rpc: Command = { + command: 'rpc', + description: 'Update the config of the Helia RPC server', + example: '$ helia rpc', + subcommands: [ + rpcRmuser, + rpcUseradd, + rpcUsers + ], + async execute ({ stdout }) { + stdout.write('Please enter a subcommand\n') + } +} diff --git a/packages/cli/src/commands/rpc/rmuser.ts b/packages/cli/src/commands/rpc/rmuser.ts new file mode 100644 index 00000000..1d51bc5c --- /dev/null +++ b/packages/cli/src/commands/rpc/rmuser.ts @@ -0,0 +1,25 @@ +import type { Command } from '../index.js' +import { loadRpcKeychain } from './utils.js' + +interface AddRpcUserArgs { + positionals: string[] +} + +export const rpcRmuser: Command = { + command: 'rmuser', + description: 'Remove a RPC user from your Helia node', + example: '$ helia rpc rmuser ', + async execute ({ directory, positionals, stdout }) { + const user = positionals[0] ?? process.env.USER + + if (user == null) { + throw new Error('No user specified') + } + + const keychain = await loadRpcKeychain(directory) + + await keychain.removeKey(`rpc-user-${user}`) + + stdout.write(`Removed user ${user}\n`) + } +} diff --git a/packages/cli/src/commands/rpc/useradd.ts b/packages/cli/src/commands/rpc/useradd.ts new file mode 100644 index 00000000..d27285b8 --- /dev/null +++ b/packages/cli/src/commands/rpc/useradd.ts @@ -0,0 +1,41 @@ +import type { Command } from '../index.js' +import type { KeyType } from '@libp2p/interface-keychain' +import { loadRpcKeychain } from './utils.js' + +interface AddRpcUserArgs { + positionals: string[] + keyType: KeyType +} + +export const rpcUseradd: Command = { + command: 'useradd', + description: 'Add an RPC user to your Helia node', + example: '$ helia rpc useradd ', + options: { + keyType: { + description: 'The type of key', + type: 'string', + default: 'Ed25519', + valid: ['Ed25519', 'secp256k1'] + } + }, + async execute ({ directory, positionals, keyType, stdout }) { + const user = positionals[0] ?? process.env.USER + + if (user == null) { + throw new Error('No user specified') + } + + const keychain = await loadRpcKeychain(directory) + const keyName = `rpc-user-${user}` + const keys = await keychain.listKeys() + + if (keys.some(info => info.name === keyName)) { + throw new Error(`User "${user}" already exists`) + } + + await keychain.createKey(`rpc-user-${user}`, keyType) + + stdout.write(`Created user ${user}\n`) + } +} diff --git a/packages/cli/src/commands/rpc/users.ts b/packages/cli/src/commands/rpc/users.ts new file mode 100644 index 00000000..57a82bf1 --- /dev/null +++ b/packages/cli/src/commands/rpc/users.ts @@ -0,0 +1,18 @@ +import type { Command } from '../index.js' +import { loadRpcKeychain } from './utils.js' + +export const rpcUsers: Command = { + command: 'users', + description: 'List user accounts on the Helia RPC server', + example: '$ helia rpc users', + async execute ({ directory, stdout }) { + const keychain = await loadRpcKeychain(directory) + const keys = await keychain.listKeys() + + for (const info of keys) { + if (info.name.startsWith('rpc-user-')) { + stdout.write(`${info.name.substring('rpc-user-'.length)}\n`) + } + } + } +} diff --git a/packages/cli/src/commands/rpc/utils.ts b/packages/cli/src/commands/rpc/utils.ts new file mode 100644 index 00000000..07d01397 --- /dev/null +++ b/packages/cli/src/commands/rpc/utils.ts @@ -0,0 +1,38 @@ +import type { HeliaConfig } from '../../index.js' +import { FsDatastore } from 'datastore-fs' +import stripJsonComments from 'strip-json-comments' +import fs from 'node:fs' +import path from 'node:path' +import * as readline from 'node:readline/promises' +import { KeyChain as DefaultKeyChain } from '@libp2p/keychain' +import type { KeyChain } from '@libp2p/interface-keychain' + +export async function loadRpcKeychain (configDir: string): Promise { + const config: HeliaConfig = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'helia.json'), 'utf-8'))) + const datastore = new FsDatastore(config.rpc.datastore, { + createIfMissing: true + }) + await datastore.open() + + let password = config.rpc.keychain.password + + if (password == null) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }) + password = await rl.question('Enter libp2p keychain password: ') + } + + return new DefaultKeyChain({ + datastore + }, { + pass: password, + dek: { + keyLength: 512 / 8, + iterationCount: 10000, + hash: 'sha2-512', + salt: config.rpc.keychain.salt + } + }) +} diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts index 2d015fd0..492311fb 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/cli/src/commands/status.ts @@ -10,8 +10,7 @@ export const status: Command = { command: 'status', description: 'Report the status of the Helia daemon', example: '$ helia status', - offline: true, - async execute ({ directory, rpcAddress, stdout }) { + async execute ({ directory, rpcAddress, stdout, user }) { // socket file? const socketFilePath = rpcAddress @@ -20,7 +19,7 @@ export const status: Command = { const { helia, libp2p - } = await findOnlineHelia(directory, rpcAddress) + } = await findOnlineHelia(directory, rpcAddress, user) if (libp2p != null) { await libp2p.stop() diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a4fe4e1f..f6f5abb2 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -3,7 +3,6 @@ import { parseArgs } from 'node:util' import path from 'node:path' -import os from 'os' import fs from 'node:fs' import { Command, commands } from './commands/index.js' import { InvalidParametersError } from '@helia/interface/errors' @@ -12,8 +11,8 @@ import { findHelia } from './utils/find-helia.js' import kleur from 'kleur' import type { Helia } from '@helia/interface' import type { Libp2p } from '@libp2p/interface-libp2p' -import type { ParseArgsConfig } from 'node:util' import { findHeliaDir } from './utils/find-helia-dir.js' +import { config } from './utils/config.js' /** * Typedef for the Helia config file @@ -32,6 +31,13 @@ export interface HeliaConfig { password?: string } } + rpc: { + datastore: string + keychain: { + salt: string + password?: string + } + } } export interface RootArgs { @@ -39,6 +45,7 @@ export interface RootArgs { directory: string help: boolean rpcAddress: string + user: string } const root: Command = { @@ -51,30 +58,20 @@ const root: Command = { type: 'string', default: findHeliaDir() }, - rpcAddress: { description: 'The multiaddr of the Helia node', type: 'string', - default: path.join(os.homedir(), '.helia', 'rpc.sock') + default: path.join(findHeliaDir(), 'rpc.sock') + }, + user: { + description: 'The name of the RPC user', + type: 'string', + default: process.env.USER } }, async execute () {} } -function config (options: any): ParseArgsConfig { - return { - allowPositionals: true, - strict: true, - options: { - help: { - description: 'Show help text', - type: 'boolean' - }, - ...options - } - } -} - async function main (): Promise { const rootCommandArgs = parseArgs(config(root.options)) const configDir = rootCommandArgs.values.directory @@ -87,7 +84,11 @@ async function main (): Promise { throw new InvalidParametersError('No RPC address specified') } - if (rootCommandArgs.values.help === true) { + if (typeof rootCommandArgs.values.user !== 'string') { + throw new InvalidParametersError('No RPC user specified') + } + + if (rootCommandArgs.values.help === true && rootCommandArgs.positionals.length === 0) { printHelp(root, process.stdout) return } @@ -122,19 +123,20 @@ async function main (): Promise { } if (rootCommandArgs.positionals.length > 0) { - const search: Command = root - let subCommand: Command | undefined + let subCommand: Command = root + let subCommandDepth = 0 for (let i = 0; i < rootCommandArgs.positionals.length; i++) { const positional = rootCommandArgs.positionals[i] - if (search.subcommands == null) { + if (subCommand.subcommands == null) { break } - const sub = search.subcommands.find(c => c.command === positional) + const sub = subCommand.subcommands.find(c => c.command === positional) if (sub != null) { + subCommandDepth++ subCommand = sub } } @@ -145,11 +147,16 @@ async function main (): Promise { const subCommandArgs = parseArgs(config(subCommand.options)) + if (subCommandArgs.values.help === true) { + printHelp(subCommand, process.stdout) + return + } + let helia: Helia | undefined let libp2p: Libp2p | undefined if (subCommand.command !== 'daemon' && subCommand.command !== 'status') { - const res = await findHelia(configDir, rootCommandArgs.values.rpcAddress, subCommand.offline) + const res = await findHelia(configDir, rootCommandArgs.values.rpcAddress, rootCommandArgs.values.user, subCommand.offline, subCommand.online) helia = res.helia libp2p = res.libp2p } @@ -157,7 +164,7 @@ async function main (): Promise { await subCommand.execute({ ...rootCommandArgs.values, ...subCommandArgs.values, - positionals: subCommandArgs.positionals.slice(1), + positionals: subCommandArgs.positionals.slice(subCommandDepth), helia, stdin: process.stdin, stdout: process.stdout, diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts new file mode 100644 index 00000000..ef112191 --- /dev/null +++ b/packages/cli/src/utils/config.ts @@ -0,0 +1,15 @@ +import type { ParseArgsConfig } from 'node:util' + +export function config (options: any): ParseArgsConfig { + return { + allowPositionals: true, + strict: true, + options: { + help: { + description: 'Show help text', + type: 'boolean' + }, + ...options + } + } +} diff --git a/packages/cli/src/utils/date-to-mtime.ts b/packages/cli/src/utils/date-to-mtime.ts new file mode 100644 index 00000000..642c34b0 --- /dev/null +++ b/packages/cli/src/utils/date-to-mtime.ts @@ -0,0 +1,11 @@ +import type { Mtime } from 'ipfs-unixfs' + +export function dateToMtime (date: Date): Mtime { + const ms = date.getTime() + const secs = Math.floor(ms / 1000) + + return { + secs, + nsecs: (ms - (secs * 1000)) * 1000 + } +} diff --git a/packages/cli/src/utils/find-helia.ts b/packages/cli/src/utils/find-helia.ts index f4225210..10b35fb4 100644 --- a/packages/cli/src/utils/find-helia.ts +++ b/packages/cli/src/utils/find-helia.ts @@ -4,31 +4,37 @@ import { multiaddr } from '@multiformats/multiaddr' import { createHelia } from './create-helia.js' import { createLibp2p, Libp2p } from 'libp2p' import { tcp } from '@libp2p/tcp' -import { webSockets } from '@libp2p/websockets' import { noise } from '@chainsafe/libp2p-noise' import { yamux } from '@chainsafe/libp2p-yamux' import { mplex } from '@libp2p/mplex' import { logger } from '@libp2p/logger' +import fs from 'node:fs' +import path from 'node:path' +import os from 'node:os' +import { FsDatastore } from 'datastore-fs' +import { loadRpcKeychain } from '../commands/rpc/utils.js' +import type { PeerId } from '@libp2p/interface-peer-id' const log = logger('helia:cli:utils:find-helia') -export async function findHelia (configDir: string, rpcAddress: string, offline: boolean = false): Promise<{ helia: Helia, libp2p: Libp2p | undefined }> { +export async function findHelia (configDir: string, rpcAddress: string, user: string, offline: boolean = true, online: boolean = true): Promise<{ helia: Helia, libp2p: Libp2p | undefined }> { let { libp2p, helia - } = await findOnlineHelia(configDir, rpcAddress) + } = await findOnlineHelia(configDir, rpcAddress, user) if (helia == null) { - log('connecting to existing helia node failed') + log('connecting to running helia node failed') - // could not connect to running node, start the server if (!offline) { - log('could not create client and command cannot be run in offline mode') + log('could not connect to running helia node and command cannot be run in offline mode') throw new Error('Could not connect to Helia - is the node running?') } - // return an offline node log('create offline helia node') helia = await createHelia(configDir, offline) + } else if (!online) { + log('connected to running helia node but command cannot be run in online mode') + throw new Error('This command cannot be run while a Helia daemon is running') } return { @@ -37,50 +43,106 @@ export async function findHelia (configDir: string, rpcAddress: string, offline: } } -export async function findOnlineHelia (configDir: string, rpcAddress: string): Promise<{ helia?: Helia, libp2p?: Libp2p }> { +export async function findOnlineHelia (configDir: string, rpcAddress: string, user: string): Promise<{ helia?: Helia, libp2p?: Libp2p }> { + const isRunning = await isHeliaRunning(configDir) + + if (!isRunning) { + log('helia daemon was not running') + return {} + } + + let peerId: PeerId | undefined + try { - log('create libp2p node') - // create a dial-only libp2p node - const libp2p = await createLibp2p({ - transports: [ - tcp(), - webSockets() - ], - connectionEncryption: [ - noise() - ], - streamMuxers: [ - yamux(), - mplex() - ] - }) + const rpcKeychain = await loadRpcKeychain(configDir) + peerId = await rpcKeychain.exportPeerId(`rpc-user-${user}`) + } catch (err) { + log('could not load peer id rpc-user-%s', user, err) + } - let helia: Helia | undefined - - try { - log('create helia client') - helia = await createHeliaRpcClient({ - multiaddr: multiaddr(`/unix/${rpcAddress}`), - libp2p, - user: `${process.env.USER}`, - authorization: 'sshh' - }) - } catch (err: any) { - log('could not create helia rpc client', err) - await libp2p.stop() + log('create dial-only libp2p node') + const libp2p = await createLibp2p({ + peerId, + datastore: new FsDatastore(path.join(configDir, 'rpc')), + transports: [ + tcp() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux(), + mplex() + ], + relay: { + enabled: false + }, + nat: { + enabled: false } + }) - return { - helia, - libp2p - } + let helia: Helia | undefined + + try { + log('create helia client') + helia = await createHeliaRpcClient({ + multiaddr: multiaddr(`/unix/${rpcAddress}`), + libp2p, + user + }) } catch (err: any) { - log('could not create helia client', err) + log('could not create helia rpc client', err) + await libp2p.stop() - if (err.code !== 'ECONNREFUSED' && err.errors[0].code !== 'ECONNREFUSED') { + if (err.name === 'AggregateError' && err.errors != null) { + throw err.errors[0] + } else { throw err } } - return {} + return { + helia, + libp2p + } +} + +export async function isHeliaRunning (configDir: string): Promise { + const pidFilePath = path.join(configDir, 'helia.pid') + + if (!fs.existsSync(pidFilePath)) { + log('pidfile at %s did not exist', pidFilePath) + return false + } + + const pid = Number(fs.readFileSync(pidFilePath, { + encoding: 'utf8' + }).trim()) + + if (isNaN(pid)) { + log('pidfile at %s had invalid contents', pidFilePath) + log('removing invalid pidfile') + fs.rmSync(pidFilePath) + return false + } + + try { + // this will throw if the process does not exist + os.getPriority(pid) + return true + } catch (err: any) { + log('getting process info for pid %d failed', pid) + + if (err.message.includes('no such process') === true) { + log('process for pid %d was not running', pid) + log('removing stale pidfile') + fs.rmSync(pidFilePath) + + return false + } + + log('error getting process priority for pid %d', pid, err) + throw err + } } diff --git a/packages/cli/src/utils/glob-source.ts b/packages/cli/src/utils/glob-source.ts new file mode 100644 index 00000000..17de99ab --- /dev/null +++ b/packages/cli/src/utils/glob-source.ts @@ -0,0 +1,95 @@ +import fsp from 'node:fs/promises' +import fs from 'node:fs' +import glob from 'it-glob' +import path from 'path' +import { CodeError } from '@libp2p/interfaces/errors' +import type { Mtime } from 'ipfs-unixfs' +import type { ImportCandidate } from 'ipfs-unixfs-importer' + +export interface GlobSourceOptions { + /** + * Include .dot files in matched paths + */ + hidden?: boolean + + /** + * follow symlinks + */ + followSymlinks?: boolean + + /** + * Preserve mode + */ + preserveMode?: boolean + + /** + * Preserve mtime + */ + preserveMtime?: boolean + + /** + * mode to use - if preserveMode is true this will be ignored + */ + mode?: number + + /** + * mtime to use - if preserveMtime is true this will be ignored + */ + mtime?: Mtime +} + +/** + * Create an async iterator that yields paths that match requested glob pattern + */ +export async function * globSource (cwd: string, pattern: string, options: GlobSourceOptions): AsyncGenerator { + options = options ?? {} + + if (typeof pattern !== 'string') { + throw new CodeError('Pattern must be a string', 'ERR_INVALID_PATH', { pattern }) + } + + if (!path.isAbsolute(cwd)) { + cwd = path.resolve(process.cwd(), cwd) + } + + const globOptions = Object.assign({}, { + nodir: false, + realpath: false, + absolute: true, + dot: Boolean(options.hidden), + follow: options.followSymlinks != null ? options.followSymlinks : true + }) + + for await (const p of glob(cwd, pattern, globOptions)) { + const stat = await fsp.stat(p) + + let mode = options.mode + + if (options.preserveMode === true) { + mode = stat.mode + } + + let mtime = options.mtime + + if (options.preserveMtime === true) { + const ms = stat.mtime.getTime() + const secs = Math.floor(ms / 1000) + + mtime = { + secs, + nsecs: (ms - (secs * 1000)) * 1000 + } + } + + yield { + path: toPosix(p.replace(cwd, '')), + content: stat.isFile() ? fs.createReadStream(p) : undefined, + mode, + mtime + } + } +} + +function toPosix (path: string): string { + return path.replace(/\\/g, '/') +} diff --git a/packages/cli/src/utils/multiaddr-to-url.ts b/packages/cli/src/utils/multiaddr-to-url.ts index 94497ec9..a9671308 100644 --- a/packages/cli/src/utils/multiaddr-to-url.ts +++ b/packages/cli/src/utils/multiaddr-to-url.ts @@ -5,15 +5,15 @@ export function multiaddrToUrl (addr: Multiaddr): URL { const protoNames = addr.protoNames() if (protoNames.length !== 3) { - throw new InvalidParametersError('Helia gRPC address format incorrect') + throw new InvalidParametersError('Helia RPC address format incorrect') } if (protoNames[0] !== 'ip4' && protoNames[0] !== 'ip6') { - throw new InvalidParametersError('Helia gRPC address format incorrect') + throw new InvalidParametersError('Helia RPC address format incorrect') } if (protoNames[1] !== 'tcp' && protoNames[2] !== 'ws') { - throw new InvalidParametersError('Helia gRPC address format incorrect') + throw new InvalidParametersError('Helia RPC address format incorrect') } const { host, port } = addr.toOptions() diff --git a/packages/helia/package.json b/packages/helia/package.json index adeb72dd..332c4444 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -140,12 +140,12 @@ "dependencies": { "@helia/interface": "~0.0.0", "@libp2p/interface-libp2p": "^1.1.0", - "@libp2p/interfaces": "^3.2.0", + "@libp2p/interfaces": "^3.3.1", "interface-blockstore": "^4.0.0", "interface-datastore": "^7.0.3", "ipfs-bitswap": "^15.0.0", "merge-options": "^3.0.4", - "multiformats": "^11.0.0" + "multiformats": "^11.0.1" }, "devDependencies": { "aegir": "^38.0.0" diff --git a/packages/interface/package.json b/packages/interface/package.json index e3a00c7d..6d326ad7 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -152,12 +152,12 @@ }, "dependencies": { "@libp2p/interface-libp2p": "^1.1.0", - "@libp2p/interface-peer-id": "^2.0.0", - "@libp2p/interfaces": "^3.2.0", + "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/interfaces": "^3.3.1", "@multiformats/multiaddr": "^11.1.5", "interface-blockstore": "^4.0.0", "interface-datastore": "^7.0.3", - "multiformats": "^11.0.0" + "multiformats": "^11.0.1" }, "devDependencies": { "aegir": "^38.0.0" diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json index 6f14dd4a..9c1ff885 100644 --- a/packages/rpc-client/package.json +++ b/packages/rpc-client/package.json @@ -144,7 +144,7 @@ "@libp2p/peer-id": "^2.0.0", "@multiformats/multiaddr": "^11.1.5", "it-pb-stream": "^2.0.3", - "multiformats": "^11.0.0" + "multiformats": "^11.0.1" }, "devDependencies": { "aegir": "^38.0.0" diff --git a/packages/rpc-client/src/commands/authorization/get.ts b/packages/rpc-client/src/commands/authorization/get.ts new file mode 100644 index 00000000..b156045c --- /dev/null +++ b/packages/rpc-client/src/commands/authorization/get.ts @@ -0,0 +1,44 @@ +import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/authorization' +import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' +import type { HeliaRpcClientConfig } from '../../index.js' +import { pbStream } from 'it-pb-stream' + +export function createAuthorizationGet (config: HeliaRpcClientConfig): (user: string, options?: any) => Promise { + const get = async (user: string, options = {}): Promise => { + const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) + + if (config.libp2p.peerId.publicKey == null || config.libp2p.peerId.privateKey == null) { + throw new Error('Public key component missing') + } + + const stream = pbStream(duplex) + stream.writePB({ + resource: '/authorization/get', + method: 'INVOKE', + options: GetOptions.encode({ + ...options + }) + }, RPCCallRequest) + stream.writePB({ + user + }, GetRequest) + const response = await stream.readPB(RPCCallResponse) + + duplex.close() + + if (response.type === RPCCallResponseType.message) { + if (response.message == null) { + throw new TypeError('RPC response had message type but no message') + } + + const message = GetResponse.decode(response.message) + + return message.authorization + } + + throw new RPCError(response) + } + + return get +} diff --git a/packages/rpc-client/src/commands/blockstore/delete.ts b/packages/rpc-client/src/commands/blockstore/delete.ts index 0471f9a7..a175c5ce 100644 --- a/packages/rpc-client/src/commands/blockstore/delete.ts +++ b/packages/rpc-client/src/commands/blockstore/delete.ts @@ -2,19 +2,18 @@ import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc import { DeleteOptions, DeleteRequest } from '@helia/rpc-protocol/blockstore' import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' -import type { HeliaRpcClientConfig } from '../../index.js' +import type { HeliaRpcMethodConfig } from '../../index.js' import { pbStream } from 'it-pb-stream' import type { CID } from 'multiformats/cid' -export function createDelete (config: HeliaRpcClientConfig): Helia['blockstore']['delete'] { +export function createBlockstoreDelete (config: HeliaRpcMethodConfig): Helia['blockstore']['delete'] { const del: Helia['blockstore']['delete'] = async (cid: CID, options = {}) => { const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) const stream = pbStream(duplex) stream.writePB({ resource: '/blockstore/delete', - method: 'GET', - user: config.user, + method: 'INVOKE', authorization: config.authorization, options: DeleteOptions.encode({ ...options diff --git a/packages/rpc-client/src/commands/blockstore/get.ts b/packages/rpc-client/src/commands/blockstore/get.ts index be9c7a47..6df98547 100644 --- a/packages/rpc-client/src/commands/blockstore/get.ts +++ b/packages/rpc-client/src/commands/blockstore/get.ts @@ -2,19 +2,18 @@ import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' -import type { HeliaRpcClientConfig } from '../../index.js' +import type { HeliaRpcMethodConfig } from '../../index.js' import { pbStream } from 'it-pb-stream' import type { CID } from 'multiformats/cid' -export function createGet (config: HeliaRpcClientConfig): Helia['blockstore']['get'] { +export function createBlockstoreGet (config: HeliaRpcMethodConfig): Helia['blockstore']['get'] { const get: Helia['blockstore']['get'] = async (cid: CID, options = {}) => { const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) const stream = pbStream(duplex) stream.writePB({ resource: '/blockstore/get', - method: 'GET', - user: config.user, + method: 'INVOKE', authorization: config.authorization, options: GetOptions.encode({ ...options diff --git a/packages/rpc-client/src/commands/blockstore/has.ts b/packages/rpc-client/src/commands/blockstore/has.ts index 9dab9daf..8a29e178 100644 --- a/packages/rpc-client/src/commands/blockstore/has.ts +++ b/packages/rpc-client/src/commands/blockstore/has.ts @@ -2,19 +2,18 @@ import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc import { HasOptions, HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' -import type { HeliaRpcClientConfig } from '../../index.js' +import type { HeliaRpcMethodConfig } from '../../index.js' import { pbStream } from 'it-pb-stream' import type { CID } from 'multiformats/cid' -export function createHas (config: HeliaRpcClientConfig): Helia['blockstore']['has'] { +export function createBlockstoreHas (config: HeliaRpcMethodConfig): Helia['blockstore']['has'] { const has: Helia['blockstore']['has'] = async (cid: CID, options = {}) => { const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) const stream = pbStream(duplex) stream.writePB({ resource: '/blockstore/has', - method: 'GET', - user: config.user, + method: 'INVOKE', authorization: config.authorization, options: HasOptions.encode({ ...options diff --git a/packages/rpc-client/src/commands/blockstore/put.ts b/packages/rpc-client/src/commands/blockstore/put.ts index 690f620b..852fc499 100644 --- a/packages/rpc-client/src/commands/blockstore/put.ts +++ b/packages/rpc-client/src/commands/blockstore/put.ts @@ -2,19 +2,18 @@ import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc import { PutOptions, PutRequest } from '@helia/rpc-protocol/blockstore' import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' -import type { HeliaRpcClientConfig } from '../../index.js' +import type { HeliaRpcMethodConfig } from '../../index.js' import { pbStream } from 'it-pb-stream' import type { CID } from 'multiformats/cid' -export function createPut (config: HeliaRpcClientConfig): Helia['blockstore']['put'] { +export function createBlockstorePut (config: HeliaRpcMethodConfig): Helia['blockstore']['put'] { const put: Helia['blockstore']['put'] = async (cid: CID, block: Uint8Array, options = {}) => { const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) const stream = pbStream(duplex) stream.writePB({ resource: '/blockstore/has', - method: 'GET', - user: config.user, + method: 'INVOKE', authorization: config.authorization, options: PutOptions.encode({ ...options diff --git a/packages/rpc-client/src/commands/cat.ts b/packages/rpc-client/src/commands/cat.ts deleted file mode 100644 index 59ad568a..00000000 --- a/packages/rpc-client/src/commands/cat.ts +++ /dev/null @@ -1,2 +0,0 @@ - -export {} diff --git a/packages/rpc-client/src/commands/id.ts b/packages/rpc-client/src/commands/id.ts index 5085d109..4d7fded2 100644 --- a/packages/rpc-client/src/commands/id.ts +++ b/packages/rpc-client/src/commands/id.ts @@ -4,19 +4,17 @@ import { IdOptions, IdResponse } from '@helia/rpc-protocol/root' import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' import { peerIdFromString } from '@libp2p/peer-id' -import type { HeliaRpcClientConfig } from '../index.js' +import type { HeliaRpcMethodConfig } from '../index.js' import { pbStream } from 'it-pb-stream' -export function createId (config: HeliaRpcClientConfig): Helia['id'] { +export function createId (config: HeliaRpcMethodConfig): Helia['id'] { const id: Helia['id'] = async (options = {}) => { const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) const stream = pbStream(duplex) stream.writePB({ resource: '/id', - method: 'GET', - user: config.user, - authorization: config.authorization, + method: 'INVOKE', options: IdOptions.encode({ ...options, peerId: options.peerId != null ? options.peerId.toString() : undefined diff --git a/packages/rpc-client/src/index.ts b/packages/rpc-client/src/index.ts index ff115b45..447043dc 100644 --- a/packages/rpc-client/src/index.ts +++ b/packages/rpc-client/src/index.ts @@ -2,19 +2,42 @@ import type { Helia } from '@helia/interface' import { createId } from './commands/id.js' import type { Libp2p } from '@libp2p/interface-libp2p' import type { Multiaddr } from '@multiformats/multiaddr' +import { createBlockstoreDelete } from './commands/blockstore/delete.js' +import { createBlockstoreGet } from './commands/blockstore/get.js' +import { createBlockstoreHas } from './commands/blockstore/has.js' +import { createBlockstorePut } from './commands/blockstore/put.js' +import { createAuthorizationGet } from './commands/authorization/get.js' export interface HeliaRpcClientConfig { multiaddr: Multiaddr libp2p: Libp2p user: string - authorization: string +} + +export interface HeliaRpcMethodConfig { + multiaddr: Multiaddr + libp2p: Libp2p + authorization?: string } export async function createHeliaRpcClient (config: HeliaRpcClientConfig): Promise { await config.libp2p.dial(config.multiaddr) - // @ts-expect-error incomplete implementation + const getAuthorization = createAuthorizationGet(config) + const authorization = await getAuthorization(config.user) + const methodConfig = { + ...config, + authorization + } + return { - id: createId(config) + id: createId(methodConfig), + // @ts-expect-error incomplete implementation + blockstore: { + delete: createBlockstoreDelete(methodConfig), + get: createBlockstoreGet(methodConfig), + has: createBlockstoreHas(methodConfig), + put: createBlockstorePut(methodConfig) + } } } diff --git a/packages/rpc-protocol/README.md b/packages/rpc-protocol/README.md index a187000a..d8a827b4 100644 --- a/packages/rpc-protocol/README.md +++ b/packages/rpc-protocol/README.md @@ -5,7 +5,7 @@ [![codecov](https://img.shields.io/codecov/c/github/ipfs/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) [![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) -> gRPC protocol for use by @helia/rpc-client and @helia/rpc-server +> RPC protocol for use by @helia/rpc-client and @helia/rpc-server ## Table of contents diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json index 7b61b040..ab810025 100644 --- a/packages/rpc-protocol/package.json +++ b/packages/rpc-protocol/package.json @@ -1,7 +1,7 @@ { "name": "@helia/rpc-protocol", "version": "0.0.0", - "description": "gRPC protocol for use by @helia/rpc-client and @helia/rpc-server", + "description": "RPC protocol for use by @helia/rpc-client and @helia/rpc-server", "license": "Apache-2.0 OR MIT", "homepage": "https://github.com/ipfs/helia/tree/master/packages/rpc-protocol#readme", "repository": { @@ -47,6 +47,14 @@ "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" }, + "./authorization": { + "types": "./dist/src/authorization.d.ts", + "import": "./dist/src/authorization.js" + }, + "./blockstore": { + "types": "./dist/src/blockstore.d.ts", + "import": "./dist/src/blockstore.js" + }, "./root": { "types": "./dist/src/root.d.ts", "import": "./dist/src/root.js" @@ -54,10 +62,6 @@ "./rpc": { "types": "./dist/src/rpc.d.ts", "import": "./dist/src/rpc.js" - }, - "./blockstore": { - "types": "./dist/src/blockstore.d.ts", - "import": "./dist/src/blockstore.js" } }, "eslintConfig": { diff --git a/packages/rpc-protocol/src/authorization.proto b/packages/rpc-protocol/src/authorization.proto new file mode 100644 index 00000000..9815f4b4 --- /dev/null +++ b/packages/rpc-protocol/src/authorization.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +message GetOptions { + +} + +message GetRequest { + string user = 1; +} + +message GetResponse { + string authorization = 1; +} diff --git a/packages/rpc-protocol/src/authorization.ts b/packages/rpc-protocol/src/authorization.ts new file mode 100644 index 00000000..5ab5a721 --- /dev/null +++ b/packages/rpc-protocol/src/authorization.ts @@ -0,0 +1,171 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' +import type { Codec } from 'protons-runtime' + +export interface GetOptions {} + +export namespace GetOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: GetOptions): Uint8Array => { + return encodeMessage(obj, GetOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetOptions => { + return decodeMessage(buf, GetOptions.codec()) + } +} + +export interface GetRequest { + user: string +} + +export namespace GetRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.user !== '') { + w.uint32(10) + w.string(obj.user) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + user: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.user = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: GetRequest): Uint8Array => { + return encodeMessage(obj, GetRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetRequest => { + return decodeMessage(buf, GetRequest.codec()) + } +} + +export interface GetResponse { + authorization: string +} + +export namespace GetResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.authorization !== '') { + w.uint32(10) + w.string(obj.authorization) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + authorization: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.authorization = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: GetResponse): Uint8Array => { + return encodeMessage(obj, GetResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse => { + return decodeMessage(buf, GetResponse.codec()) + } +} diff --git a/packages/rpc-protocol/src/root.proto b/packages/rpc-protocol/src/root.proto index 8cf299a4..9bd4e997 100644 --- a/packages/rpc-protocol/src/root.proto +++ b/packages/rpc-protocol/src/root.proto @@ -6,11 +6,10 @@ message IdOptions { message IdResponse { string peer_id = 1; - string server_did = 2; - repeated string multiaddrs = 3; - string agent_version = 4; - string protocol_version = 5; - repeated string protocols = 6; + repeated string multiaddrs = 2; + string agent_version = 3; + string protocol_version = 4; + repeated string protocols = 5; } message CatOptions { diff --git a/packages/rpc-protocol/src/root.ts b/packages/rpc-protocol/src/root.ts index a85090ec..01021cf9 100644 --- a/packages/rpc-protocol/src/root.ts +++ b/packages/rpc-protocol/src/root.ts @@ -2,6 +2,7 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' @@ -65,7 +66,6 @@ export namespace IdOptions { export interface IdResponse { peerId: string - serverDid: string multiaddrs: string[] agentVersion: string protocolVersion: string @@ -87,31 +87,26 @@ export namespace IdResponse { w.string(obj.peerId) } - if (opts.writeDefaults === true || obj.serverDid !== '') { - w.uint32(18) - w.string(obj.serverDid) - } - if (obj.multiaddrs != null) { for (const value of obj.multiaddrs) { - w.uint32(26) + w.uint32(18) w.string(value) } } if (opts.writeDefaults === true || obj.agentVersion !== '') { - w.uint32(34) + w.uint32(26) w.string(obj.agentVersion) } if (opts.writeDefaults === true || obj.protocolVersion !== '') { - w.uint32(42) + w.uint32(34) w.string(obj.protocolVersion) } if (obj.protocols != null) { for (const value of obj.protocols) { - w.uint32(50) + w.uint32(42) w.string(value) } } @@ -122,7 +117,6 @@ export namespace IdResponse { }, (reader, length) => { const obj: any = { peerId: '', - serverDid: '', multiaddrs: [], agentVersion: '', protocolVersion: '', @@ -139,18 +133,15 @@ export namespace IdResponse { obj.peerId = reader.string() break case 2: - obj.serverDid = reader.string() - break - case 3: obj.multiaddrs.push(reader.string()) break - case 4: + case 3: obj.agentVersion = reader.string() break - case 5: + case 4: obj.protocolVersion = reader.string() break - case 6: + case 5: obj.protocols.push(reader.string()) break default: diff --git a/packages/rpc-protocol/src/rpc.proto b/packages/rpc-protocol/src/rpc.proto index 8430f177..9de4328c 100644 --- a/packages/rpc-protocol/src/rpc.proto +++ b/packages/rpc-protocol/src/rpc.proto @@ -1,11 +1,10 @@ syntax = "proto3"; message RPCCallRequest { - string user = 1; - string resource = 2; - string method = 3; - string authorization = 4; - bytes options = 5; + string resource = 1; + string method = 2; + optional string authorization = 3; + optional bytes options = 4; } message RPCCallResponse { diff --git a/packages/rpc-protocol/src/rpc.ts b/packages/rpc-protocol/src/rpc.ts index 3e72e34d..e60f4c61 100644 --- a/packages/rpc-protocol/src/rpc.ts +++ b/packages/rpc-protocol/src/rpc.ts @@ -2,17 +2,17 @@ /* eslint-disable complexity */ /* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' export interface RPCCallRequest { - user: string resource: string method: string - authorization: string - options: Uint8Array + authorization?: string + options?: Uint8Array } export namespace RPCCallRequest { @@ -25,28 +25,23 @@ export namespace RPCCallRequest { w.fork() } - if (opts.writeDefaults === true || obj.user !== '') { - w.uint32(10) - w.string(obj.user) - } - if (opts.writeDefaults === true || obj.resource !== '') { - w.uint32(18) + w.uint32(10) w.string(obj.resource) } if (opts.writeDefaults === true || obj.method !== '') { - w.uint32(26) + w.uint32(18) w.string(obj.method) } - if (opts.writeDefaults === true || obj.authorization !== '') { - w.uint32(34) + if (obj.authorization != null) { + w.uint32(26) w.string(obj.authorization) } - if (opts.writeDefaults === true || (obj.options != null && obj.options.byteLength > 0)) { - w.uint32(42) + if (obj.options != null) { + w.uint32(34) w.bytes(obj.options) } @@ -55,11 +50,8 @@ export namespace RPCCallRequest { } }, (reader, length) => { const obj: any = { - user: '', resource: '', - method: '', - authorization: '', - options: new Uint8Array(0) + method: '' } const end = length == null ? reader.len : reader.pos + length @@ -69,18 +61,15 @@ export namespace RPCCallRequest { switch (tag >>> 3) { case 1: - obj.user = reader.string() - break - case 2: obj.resource = reader.string() break - case 3: + case 2: obj.method = reader.string() break - case 4: + case 3: obj.authorization = reader.string() break - case 5: + case 4: obj.options = reader.bytes() break default: diff --git a/packages/rpc-server/package.json b/packages/rpc-server/package.json index 1f51b69c..8f95970c 100644 --- a/packages/rpc-server/package.json +++ b/packages/rpc-server/package.json @@ -140,16 +140,15 @@ "dependencies": { "@helia/interface": "~0.0.0", "@helia/rpc-protocol": "~0.0.0", + "@libp2p/interface-keychain": "^2.0.3", + "@libp2p/interface-peer-id": "^2.0.1", "@libp2p/logger": "^2.0.2", "@libp2p/peer-id": "^2.0.0", "@multiformats/multiaddr": "^11.1.5", "@ucans/ucans": "^0.11.0-alpha", - "it-length-prefixed": "^8.0.4", "it-pb-stream": "^2.0.3", - "it-pushable": "^3.1.2", - "it-stream-types": "^1.0.5", - "multiformats": "^11.0.0", - "uint8arraylist": "^2.4.3" + "multiformats": "^11.0.1", + "uint8arrays": "^4.0.3" }, "devDependencies": { "aegir": "^38.0.0" diff --git a/packages/rpc-server/src/handlers/authorization/get.ts b/packages/rpc-server/src/handlers/authorization/get.ts new file mode 100644 index 00000000..27179acd --- /dev/null +++ b/packages/rpc-server/src/handlers/authorization/get.ts @@ -0,0 +1,75 @@ +import { GetRequest, GetResponse } from '@helia/rpc-protocol/authorization' +import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import type { RPCServerConfig, Service } from '../../index.js' +import * as ucans from '@ucans/ucans' +import { base58btc } from 'multiformats/bases/base58' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' + +export function createAuthorizationGet (config: RPCServerConfig): Service { + if (config.helia.libp2p.peerId.privateKey == null || config.helia.libp2p.peerId.publicKey == null) { + throw new Error('Public/private key missing from peer id') + } + + const issuer = new ucans.EdKeypair( + config.helia.libp2p.peerId.privateKey.subarray(4), + config.helia.libp2p.peerId.publicKey.subarray(4), + false + ) + + return { + insecure: true, + async handle ({ peerId, stream }): Promise { + const request = await stream.readPB(GetRequest) + const user = request.user + + const allowedPeerId = await config.users.exportPeerId(`rpc-user-${user}`) + + if (!allowedPeerId.equals(peerId)) { + throw new Error('PeerIds did not match') + } + + if (peerId.publicKey == null) { + throw new Error('Public key component missing') + } + + // derive the audience from the peer id + const audience = `did:key:${base58btc.encode(uint8ArrayConcat([ + Uint8Array.from([0xed, 0x01]), + peerId.publicKey.subarray(4) + ], peerId.publicKey.length - 2))}` + + // authorize the remote peer for these operations + const ucan = await ucans.build({ + audience, + issuer, + lifetimeInSeconds: config.authorizationValiditySeconds, + capabilities: [ + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/delete' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/get' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/has' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/put' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + } + ] + }) + + stream.writePB({ + type: RPCCallResponseType.message, + message: GetResponse.encode({ + authorization: ucans.encode(ucan) + }) + }, + RPCCallResponse) + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/delete.ts b/packages/rpc-server/src/handlers/blockstore/delete.ts index 2dfb1236..edafc128 100644 --- a/packages/rpc-server/src/handlers/blockstore/delete.ts +++ b/packages/rpc-server/src/handlers/blockstore/delete.ts @@ -1,24 +1,21 @@ -import { DeleteRequest, DeleteResponse } from '@helia/rpc-protocol/blockstore' +import { DeleteOptions, DeleteRequest, DeleteResponse } from '@helia/rpc-protocol/blockstore' import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' -import type { Duplex } from 'it-stream-types' -import type { Uint8ArrayList } from 'uint8arraylist' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' -import { pbStream } from 'it-pb-stream' export function createDelete (config: RPCServerConfig): Service { return { - async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { - // const opts = DeleteOptions.decode(options) - const pb = pbStream(stream) - const request = await pb.readPB(DeleteRequest) + async handle ({ options, stream, signal }): Promise { + const opts = DeleteOptions.decode(options) + const request = await stream.readPB(DeleteRequest) const cid = CID.decode(request.cid) await config.helia.blockstore.delete(cid, { - signal + signal, + ...opts }) - pb.writePB({ + stream.writePB({ type: RPCCallResponseType.message, message: DeleteResponse.encode({ }) diff --git a/packages/rpc-server/src/handlers/blockstore/get.ts b/packages/rpc-server/src/handlers/blockstore/get.ts index cde851ac..a9a562ce 100644 --- a/packages/rpc-server/src/handlers/blockstore/get.ts +++ b/packages/rpc-server/src/handlers/blockstore/get.ts @@ -1,24 +1,21 @@ -import { GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' +import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' -import type { Duplex } from 'it-stream-types' -import type { Uint8ArrayList } from 'uint8arraylist' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' -import { pbStream } from 'it-pb-stream' export function createGet (config: RPCServerConfig): Service { return { - async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { - // const opts = GetOptions.decode(options) - const pb = pbStream(stream) - const request = await pb.readPB(GetRequest) + async handle ({ options, stream, signal }): Promise { + const opts = GetOptions.decode(options) + const request = await stream.readPB(GetRequest) const cid = CID.decode(request.cid) const block = await config.helia.blockstore.get(cid, { - signal + signal, + ...opts }) - pb.writePB({ + stream.writePB({ type: RPCCallResponseType.message, message: GetResponse.encode({ block diff --git a/packages/rpc-server/src/handlers/blockstore/has.ts b/packages/rpc-server/src/handlers/blockstore/has.ts index 99c50334..7a27bc14 100644 --- a/packages/rpc-server/src/handlers/blockstore/has.ts +++ b/packages/rpc-server/src/handlers/blockstore/has.ts @@ -1,24 +1,21 @@ -import { HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' +import { HasOptions, HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' -import type { Duplex } from 'it-stream-types' -import type { Uint8ArrayList } from 'uint8arraylist' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' -import { pbStream } from 'it-pb-stream' export function createHas (config: RPCServerConfig): Service { return { - async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { - // const opts = HasOptions.decode(options) - const pb = pbStream(stream) - const request = await pb.readPB(HasRequest) + async handle ({ options, stream, signal }): Promise { + const opts = HasOptions.decode(options) + const request = await stream.readPB(HasRequest) const cid = CID.decode(request.cid) const has = await config.helia.blockstore.has(cid, { - signal + signal, + ...opts }) - pb.writePB({ + stream.writePB({ type: RPCCallResponseType.message, message: HasResponse.encode({ has diff --git a/packages/rpc-server/src/handlers/blockstore/put.ts b/packages/rpc-server/src/handlers/blockstore/put.ts index 546d7bfb..42cc715c 100644 --- a/packages/rpc-server/src/handlers/blockstore/put.ts +++ b/packages/rpc-server/src/handlers/blockstore/put.ts @@ -1,24 +1,21 @@ -import { PutRequest, PutResponse } from '@helia/rpc-protocol/blockstore' +import { PutOptions, PutRequest, PutResponse } from '@helia/rpc-protocol/blockstore' import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' -import type { Duplex } from 'it-stream-types' -import type { Uint8ArrayList } from 'uint8arraylist' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' -import { pbStream } from 'it-pb-stream' export function createPut (config: RPCServerConfig): Service { return { - async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { - // const opts = HasOptions.decode(options) - const pb = pbStream(stream) - const request = await pb.readPB(PutRequest) + async handle ({ options, stream, signal }): Promise { + const opts = PutOptions.decode(options) + const request = await stream.readPB(PutRequest) const cid = CID.decode(request.cid) await config.helia.blockstore.put(cid, request.block, { - signal + signal, + ...opts }) - pb.writePB({ + stream.writePB({ type: RPCCallResponseType.message, message: PutResponse.encode({ }) diff --git a/packages/rpc-server/src/handlers/id.ts b/packages/rpc-server/src/handlers/id.ts index d50f1166..aa8c8fe0 100644 --- a/packages/rpc-server/src/handlers/id.ts +++ b/packages/rpc-server/src/handlers/id.ts @@ -1,28 +1,24 @@ import { IdOptions, IdResponse } from '@helia/rpc-protocol/root' import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import { peerIdFromString } from '@libp2p/peer-id' -import { pbStream } from 'it-pb-stream' -import type { Duplex } from 'it-stream-types' -import type { Uint8ArrayList } from 'uint8arraylist' import type { RPCServerConfig, Service } from '../index.js' export function createId (config: RPCServerConfig): Service { return { insecure: true, - async handle (options: Uint8Array, stream: Duplex, signal: AbortSignal): Promise { + async handle ({ options, stream, signal }): Promise { const opts = IdOptions.decode(options) - const pb = pbStream(stream) + const result = await config.helia.id({ peerId: opts.peerId != null ? peerIdFromString(opts.peerId) : undefined, signal }) - pb.writePB({ + stream.writePB({ type: RPCCallResponseType.message, message: IdResponse.encode({ ...result, peerId: result.peerId.toString(), - serverDid: config.serverDid, multiaddrs: result.multiaddrs.map(ma => ma.toString()) }) }, RPCCallResponse) diff --git a/packages/rpc-server/src/index.ts b/packages/rpc-server/src/index.ts index 80dcdd04..27488d8d 100644 --- a/packages/rpc-server/src/index.ts +++ b/packages/rpc-server/src/index.ts @@ -2,23 +2,27 @@ import type { Helia } from '@helia/interface' import { HeliaError } from '@helia/interface/errors' import { createId } from './handlers/id.js' import { logger } from '@libp2p/logger' -import type { Duplex, Sink, Source } from 'it-stream-types' import { HELIA_RPC_PROTOCOL } from '@helia/rpc-protocol' import { RPCCallRequest, RPCCallResponseType, RPCCallResponse } from '@helia/rpc-protocol/rpc' -import { decode, encode } from 'it-length-prefixed' -import { pushable } from 'it-pushable' -import type { Uint8ArrayList } from 'uint8arraylist' import * as ucans from '@ucans/ucans' import { createDelete } from './handlers/blockstore/delete.js' import { createGet } from './handlers/blockstore/get.js' import { createHas } from './handlers/blockstore/has.js' import { createPut } from './handlers/blockstore/put.js' +import { pbStream, ProtobufStream } from 'it-pb-stream' +import { createAuthorizationGet } from './handlers/authorization/get.js' +import { EdKeypair } from '@ucans/ucans' +import type { KeyChain } from '@libp2p/interface-keychain' +import { base58btc } from 'multiformats/bases/base58' +import { concat as uint8ArrayConcat } from 'uint8arrays/concat' +import type { PeerId } from '@libp2p/interface-peer-id' -const log = logger('helia:grpc-server') +const log = logger('helia:rpc-server') export interface RPCServerConfig { helia: Helia - serverDid: string + users: KeyChain + authorizationValiditySeconds: number } export interface UnaryResponse { @@ -26,9 +30,16 @@ export interface UnaryResponse { metadata: Record } +export interface ServiceArgs { + peerId: PeerId + options: Uint8Array + stream: ProtobufStream + signal: AbortSignal +} + export interface Service { insecure?: true - handle: (options: Uint8Array, stream: Duplex, signal: AbortSignal) => Promise + handle: (args: ServiceArgs) => Promise } class RPCError extends HeliaError { @@ -40,7 +51,19 @@ class RPCError extends HeliaError { export async function createHeliaRpcServer (config: RPCServerConfig): Promise { const { helia } = config + if (helia.libp2p.peerId.privateKey == null || helia.libp2p.peerId.publicKey == null) { + // should never happen + throw new Error('helia.libp2p.peerId was missing public or private key component') + } + + const serverKey = new EdKeypair( + helia.libp2p.peerId.privateKey.subarray(4), + helia.libp2p.peerId.publicKey.subarray(4), + false + ) + const services: Record = { + '/authorization/get': createAuthorizationGet(config), '/blockstore/delete': createDelete(config), '/blockstore/get': createGet(config), '/blockstore/has': createHas(config), @@ -48,132 +71,78 @@ export async function createHeliaRpcServer (config: RPCServerConfig): Promise { + await helia.libp2p.handle(HELIA_RPC_PROTOCOL, ({ stream, connection }) => { const controller = new AbortController() - const outputStream = pushable() - const inputStream = pushable() - Promise.resolve().then(async () => { - await stream.sink(encode()(outputStream)) - }) - .catch(err => { - log.error('error writing to stream', err) - controller.abort() - }) - - Promise.resolve().then(async () => { - let started = false - - for await (const buf of decode()(stream.source)) { - if (!started) { - // first message is request - started = true - - const request = RPCCallRequest.decode(buf) - - log('incoming RPC request %s %s', request.method, request.resource) - - const service = services[request.resource] - - if (service == null) { - log('no handler defined for %s %s', request.method, request.resource) - const error = new RPCError(`Request path "${request.resource}" unimplemented`, 'ERR_PATH_UNIMPLEMENTED') - - // no handler for path - outputStream.push(RPCCallResponse.encode({ - type: RPCCallResponseType.error, - errorName: error.name, - errorMessage: error.message, - errorStack: error.stack, - errorCode: error.code - })) - outputStream.end() - return - } + void Promise.resolve().then(async () => { + const pb = pbStream(stream) - if (service.insecure == null) { - // authorize request - const result = await ucans.verify(request.authorization, { - audience: request.user, - isRevoked: async ucan => false, - requiredCapabilities: [{ - capability: { - with: { scheme: 'service', hierPart: request.resource }, - can: { namespace: 'service', segments: [request.method] } - }, - rootIssuer: config.serverDid - }] - }) - - if (!result.ok) { - log('authorization failed for %s %s', request.method, request.resource) - const error = new RPCError(`Authorisation failed for ${request.method} ${request.resource}`, 'ERR_AUTHORIZATION_FAILED') - - // no handler for path - outputStream.push(RPCCallResponse.encode({ - type: RPCCallResponseType.error, - errorName: error.name, - errorMessage: error.message, - errorStack: error.stack, - errorCode: error.code - })) - outputStream.end() - return - } + try { + const request = await pb.readPB(RPCCallRequest) + const service = services[request.resource] + + if (service == null) { + log('no handler defined for %s %s', request.method, request.resource) + throw new RPCError(`Request path "${request.resource}" unimplemented`, 'ERR_PATH_UNIMPLEMENTED') + } + + log('incoming RPC request %s %s', request.method, request.resource) + + if (service.insecure == null) { + if (request.authorization == null) { + log('authorization missing for %s %s', request.method, request.resource) + throw new RPCError(`Authorisation failed for ${request.method} ${request.resource}`, 'ERR_AUTHORIZATION_FAILED') } - const sink: Sink = async (source: Source) => { - try { - for await (const buf of source) { - outputStream.push(buf) - } - } catch (err: any) { - outputStream.push(RPCCallResponse.encode({ - type: RPCCallResponseType.error, - errorName: err.name, - errorMessage: err.message, - errorStack: err.stack, - errorCode: err.code - })) - outputStream.end() - } + log('authorizing request %s %s', request.method, request.resource) + + const peerId = connection.remotePeer + + if (peerId.publicKey == null) { + log('public key missing for %s %s', request.method, request.resource) + throw new RPCError(`Authorisation failed for ${request.method} ${request.resource}`, 'ERR_AUTHORIZATION_FAILED') } - service.handle(request.options, { source: inputStream, sink }, controller.signal) - .then(() => { - log.error('handler succeeded for %s %s', request.method, request.resource) - }) - .catch(err => { - log.error('handler failed for %s %s', request.method, request.resource, err) - outputStream.push(RPCCallResponse.encode({ - type: RPCCallResponseType.error, - errorName: err.name, - errorMessage: err.message, - errorStack: err.stack, - errorCode: err.code - })) - }) - .finally(() => { - log('handler finished for %s %s', request.method, request.resource) - inputStream.end() - outputStream.end() - }) - - continue + const audience = `did:key:${base58btc.encode(uint8ArrayConcat([ + Uint8Array.from([0xed, 0x01]), + peerId.publicKey.subarray(4) + ], peerId.publicKey.length - 2))}` + + // authorize request + const result = await ucans.verify(request.authorization, { + audience, + requiredCapabilities: [{ + capability: { + with: { scheme: 'helia-rpc', hierPart: request.resource }, + can: { namespace: 'helia-rpc', segments: [request.method] } + }, + rootIssuer: serverKey.did() + }] + }) + + if (!result.ok) { + log('authorization failed for %s %s', request.method, request.resource) + throw new RPCError(`Authorisation failed for ${request.method} ${request.resource}`, 'ERR_AUTHORIZATION_FAILED') + } } - // stream all other input to the handler - inputStream.push(buf) + await service.handle({ + peerId: connection.remotePeer, + options: request.options ?? new Uint8Array(), + stream: pb, + signal: controller.signal + }) + log('handler succeeded for %s %s', request.method, request.resource) + } catch (err: any) { + log.error('handler failed', err) + pb.writePB({ + type: RPCCallResponseType.error, + errorName: err.name, + errorMessage: err.message, + errorStack: err.stack, + errorCode: err.code + }, RPCCallResponse) } }) - .catch(err => { - log.error('stream errored', err) - - stream.abort(err) - controller.abort() - }) - .finally(() => { - inputStream.end() - }) }) } diff --git a/packages/rpc-server/src/utils/multiaddr-to-url.ts b/packages/rpc-server/src/utils/multiaddr-to-url.ts index 94497ec9..a9671308 100644 --- a/packages/rpc-server/src/utils/multiaddr-to-url.ts +++ b/packages/rpc-server/src/utils/multiaddr-to-url.ts @@ -5,15 +5,15 @@ export function multiaddrToUrl (addr: Multiaddr): URL { const protoNames = addr.protoNames() if (protoNames.length !== 3) { - throw new InvalidParametersError('Helia gRPC address format incorrect') + throw new InvalidParametersError('Helia RPC address format incorrect') } if (protoNames[0] !== 'ip4' && protoNames[0] !== 'ip6') { - throw new InvalidParametersError('Helia gRPC address format incorrect') + throw new InvalidParametersError('Helia RPC address format incorrect') } if (protoNames[1] !== 'tcp' && protoNames[2] !== 'ws') { - throw new InvalidParametersError('Helia gRPC address format incorrect') + throw new InvalidParametersError('Helia RPC address format incorrect') } const { host, port } = addr.toOptions() diff --git a/packages/unixfs/package.json b/packages/unixfs/package.json index de3bd206..f1cf9275 100644 --- a/packages/unixfs/package.json +++ b/packages/unixfs/package.json @@ -142,7 +142,7 @@ "interface-blockstore": "^4.0.0", "ipfs-unixfs-exporter": "^10.0.0", "ipfs-unixfs-importer": "^12.0.0", - "multiformats": "^11.0.0" + "multiformats": "^11.0.1" }, "devDependencies": { "aegir": "^38.0.0" diff --git a/packages/unixfs/src/index.ts b/packages/unixfs/src/index.ts index 1b085455..01412473 100644 --- a/packages/unixfs/src/index.ts +++ b/packages/unixfs/src/index.ts @@ -17,7 +17,7 @@ class UnixFS { this.components = components } - async * add (source: ImportCandidate, options?: UserImporterOptions): AsyncGenerator { + async * add (source: AsyncIterable | Iterable | ImportCandidate, options?: UserImporterOptions): AsyncGenerator { yield * importer(source, this.components.blockstore, options) } From 65a6af0424c94491d7687cd7b06e45379865e1c2 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 27 Jan 2023 13:33:54 +0100 Subject: [PATCH 07/18] chore: add, cat and stat --- packages/cli/src/commands/cat.ts | 2 +- packages/cli/src/commands/index.ts | 4 +- packages/cli/src/commands/stat.ts | 38 +++++ packages/cli/src/utils/create-helia.ts | 14 +- packages/helia/package.json | 5 +- packages/helia/src/index.ts | 14 +- packages/helia/src/utils/block-storage.ts | 136 ++++++++++++++++++ packages/interface/src/index.ts | 29 ++-- .../rpc-client/src/commands/blockstore/put.ts | 2 +- 9 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 packages/cli/src/commands/stat.ts create mode 100644 packages/helia/src/utils/block-storage.ts diff --git a/packages/cli/src/commands/cat.ts b/packages/cli/src/commands/cat.ts index abbea230..7998dfd8 100644 --- a/packages/cli/src/commands/cat.ts +++ b/packages/cli/src/commands/cat.ts @@ -35,7 +35,7 @@ export const cat: Command = { length: length != null ? Number(length) : undefined }) - if (entry.type !== 'file') { + if (entry.type !== 'file' && entry.type !== 'raw') { throw new Error('UnixFS path was not a file') } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 0a0865a9..49a04e20 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -4,6 +4,7 @@ import { init } from './init.js' import { daemon } from './daemon.js' import { id } from './id.js' import { status } from './status.js' +import { stat } from './stat.js' import type { Helia } from '@helia/interface' import type { ParseArgsConfig } from 'node:util' import { rpc } from './rpc/index.js' @@ -117,5 +118,6 @@ export const commands: Array> = [ daemon, id, status, - rpc + rpc, + stat ] diff --git a/packages/cli/src/commands/stat.ts b/packages/cli/src/commands/stat.ts new file mode 100644 index 00000000..8752ad1a --- /dev/null +++ b/packages/cli/src/commands/stat.ts @@ -0,0 +1,38 @@ +import type { Command } from './index.js' +import { exporter } from 'ipfs-unixfs-exporter' +import { CID } from 'multiformats/cid' +import * as format from '../utils/format.js' +import type { Formatable } from '../utils/format.js' + +interface StatArgs { + positionals?: string[] +} + +export const stat: Command = { + command: 'stat', + description: 'Display statistics about a dag', + example: '$ helia stat ', + options: { + }, + async execute ({ positionals, helia, stdout }) { + if (positionals == null || positionals.length === 0) { + throw new TypeError('Missing positionals') + } + + const cid = CID.parse(positionals[0]) + const entry = await exporter(cid, helia.blockstore) + + const items: Formatable[] = [ + format.table([ + format.row('CID', entry.cid.toString()), + format.row('Type', entry.type), + format.row('Size', `${entry.size}`) + ]) + ] + + format.formatter( + stdout, + items + ) + } +} diff --git a/packages/cli/src/utils/create-helia.ts b/packages/cli/src/utils/create-helia.ts index 1d3fa510..1fec1ce1 100644 --- a/packages/cli/src/utils/create-helia.ts +++ b/packages/cli/src/utils/create-helia.ts @@ -19,8 +19,6 @@ import * as readline from 'node:readline/promises' export async function createHelia (configDir: string, offline: boolean = false): Promise { const config: HeliaConfig = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'helia.json'), 'utf-8'))) - const datastore = new FsDatastore(config.datastore) - let password = config.libp2p.keychain.password if (config.libp2p.keychain.password == null) { @@ -31,8 +29,18 @@ export async function createHelia (configDir: string, offline: boolean = false): password = await rl.question('Enter libp2p keychain password: ') } + const datastore = new FsDatastore(config.datastore, { + createIfMissing: true + }) + await datastore.open() + + const blockstore = new BlockstoreDatastoreAdapter(new FsDatastore(config.blockstore, { + createIfMissing: true + })) + await blockstore.open() + return await createHeliaNode({ - blockstore: new BlockstoreDatastoreAdapter(new FsDatastore(config.blockstore)), + blockstore, datastore, libp2p: await createLibp2p({ start: !offline, diff --git a/packages/helia/package.json b/packages/helia/package.json index 60c25e59..2d3d2511 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -141,10 +141,13 @@ "@helia/interface": "~0.0.0", "@libp2p/interface-libp2p": "^1.1.0", "@libp2p/interfaces": "^3.3.1", + "blockstore-core": "^3.0.0", "interface-blockstore": "^4.0.0", "interface-datastore": "^7.0.3", "ipfs-bitswap": "^15.0.0", - "merge-options": "^3.0.4", + "it-filter": "^2.0.0", + "it-merge": "^2.0.0", + "it-pushable": "^3.1.2", "multiformats": "^11.0.1" }, "devDependencies": { diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts index 2c2186a1..8bcde868 100644 --- a/packages/helia/src/index.ts +++ b/packages/helia/src/index.ts @@ -17,6 +17,7 @@ import { createId } from './commands/id.js' import { createBitswap } from 'ipfs-bitswap' +import { BlockStorage } from './utils/block-storage.js' import type { Helia } from '@helia/interface' import type { Libp2p } from '@libp2p/interface-libp2p' import type { Blockstore } from 'interface-blockstore' @@ -61,22 +62,27 @@ export interface HeliaInit { * @returns {Promise} */ export async function createHelia (init: HeliaInit): Promise { - const blockstore = createBitswap(init.libp2p, init.blockstore, { + const bitswap = createBitswap(init.libp2p, init.blockstore, { }) + bitswap.start() const components: HeliaComponents = { libp2p: init.libp2p, - blockstore, + blockstore: new BlockStorage(init.blockstore, bitswap), datastore: init.datastore } const helia: Helia = { libp2p: init.libp2p, - blockstore, + blockstore: components.blockstore, datastore: init.datastore, - id: createId(components) + id: createId(components), + + stop: async () => { + bitswap.stop() + } } return helia diff --git a/packages/helia/src/utils/block-storage.ts b/packages/helia/src/utils/block-storage.ts new file mode 100644 index 00000000..6639ee02 --- /dev/null +++ b/packages/helia/src/utils/block-storage.ts @@ -0,0 +1,136 @@ +import { BaseBlockstore } from 'blockstore-core' +import merge from 'it-merge' +import { pushable } from 'it-pushable' +import filter from 'it-filter' +import type { Blockstore, KeyQuery, Query } from 'interface-blockstore' +import type { IPFSBitswap } from 'ipfs-bitswap' +import type { CID } from 'multiformats/cid' +import type { AbortOptions } from '@libp2p/interfaces' + +/** + * BlockStorage is a hybrid block datastore. It stores data in a local + * datastore and may retrieve data from a remote Exchange. + * It uses an internal `datastore.Datastore` instance to store values. + */ +export class BlockStorage extends BaseBlockstore implements Blockstore { + private readonly child: Blockstore + private readonly bitswap: IPFSBitswap + + /** + * Create a new BlockStorage + */ + constructor (blockstore: Blockstore, bitswap: IPFSBitswap) { + super() + + this.child = blockstore + this.bitswap = bitswap + } + + async open (): Promise { + await this.child.open() + } + + async close (): Promise { + await this.child.close() + } + + unwrap (): Blockstore { + return this.child + } + + /** + * Put a block to the underlying datastore + */ + async put (cid: CID, block: Uint8Array, options: AbortOptions = {}): Promise { + if (await this.has(cid)) { + return + } + + if (this.bitswap.isStarted()) { + await this.bitswap.put(cid, block, options) + } else { + await this.child.put(cid, block, options) + } + } + + /** + * Put a multiple blocks to the underlying datastore + */ + async * putMany (blocks: AsyncIterable<{ key: CID, value: Uint8Array }> | Iterable<{ key: CID, value: Uint8Array }>, options: AbortOptions = {}): AsyncGenerator<{ key: CID, value: Uint8Array }, void, undefined> { + const missingBlocks = filter(blocks, async ({ key }) => { return !(await this.has(key)) }) + + if (this.bitswap.isStarted()) { + yield * this.bitswap.putMany(missingBlocks, options) + } else { + yield * this.child.putMany(missingBlocks, options) + } + } + + /** + * Get a block by cid + */ + async get (cid: CID, options: AbortOptions = {}): Promise { + if (!(await this.has(cid)) && this.bitswap.isStarted()) { + return await this.bitswap.get(cid, options) + } else { + return await this.child.get(cid, options) + } + } + + /** + * Get multiple blocks back from an array of cids + * + * @param {AsyncIterable | Iterable} cids + * @param {AbortOptions} [options] + */ + async * getMany (cids: AsyncIterable | Iterable, options: AbortOptions = {}): AsyncGenerator { + const getFromBitswap = pushable({ objectMode: true }) + const getFromChild = pushable({ objectMode: true }) + + void Promise.resolve().then(async () => { + for await (const cid of cids) { + if (!(await this.has(cid)) && this.bitswap.isStarted()) { + getFromBitswap.push(cid) + } else { + getFromChild.push(cid) + } + } + + getFromBitswap.end() + getFromChild.end() + }).catch(err => { + getFromBitswap.throw(err) + }) + + yield * merge( + this.bitswap.getMany(getFromBitswap, options), + this.child.getMany(getFromChild, options) + ) + } + + /** + * Delete a block from the blockstore + */ + async delete (cid: CID, options: AbortOptions = {}): Promise { + await this.child.delete(cid, options) + } + + /** + * Delete multiple blocks from the blockstore + */ + async * deleteMany (cids: AsyncIterable | Iterable, options: AbortOptions = {}): AsyncGenerator { + yield * this.child.deleteMany(cids, options) + } + + async has (cid: CID, options: AbortOptions = {}): Promise { + return await this.child.has(cid, options) + } + + async * query (q: Query, options: AbortOptions = {}): AsyncGenerator<{ key: CID, value: Uint8Array }, void, undefined> { + yield * this.child.query(q, options) + } + + async * queryKeys (q: KeyQuery, options: AbortOptions = {}): AsyncGenerator { + yield * this.child.queryKeys(q, options) + } +} diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index 7ee3410b..e5ec0b64 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -25,6 +25,21 @@ import type { Datastore } from 'interface-datastore' * The API presented by a Helia node. */ export interface Helia { + /** + * The underlying libp2p node + */ + libp2p: Libp2p + + /** + * Where the blocks are stored + */ + blockstore: Blockstore + + /** + * A key/value store + */ + datastore: Datastore + /** * Returns information about this node * @@ -42,19 +57,9 @@ export interface Helia { id: (options?: IdOptions) => Promise /** - * The underlying libp2p node - */ - libp2p: Libp2p - - /** - * Where the blocks are stored + * Stops the Helia node */ - blockstore: Blockstore - - /** - * A key/value store - */ - datastore: Datastore + stop: () => Promise } export interface CatOptions extends AbortOptions { diff --git a/packages/rpc-client/src/commands/blockstore/put.ts b/packages/rpc-client/src/commands/blockstore/put.ts index 852fc499..cffc23f8 100644 --- a/packages/rpc-client/src/commands/blockstore/put.ts +++ b/packages/rpc-client/src/commands/blockstore/put.ts @@ -12,7 +12,7 @@ export function createBlockstorePut (config: HeliaRpcMethodConfig): Helia['block const stream = pbStream(duplex) stream.writePB({ - resource: '/blockstore/has', + resource: '/blockstore/put', method: 'INVOKE', authorization: config.authorization, options: PutOptions.encode({ From 892b9b04920c6e997ccc147be89ba7131d72b88d Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 27 Jan 2023 18:39:38 +0100 Subject: [PATCH 08/18] chore: update keychain --- README.md | 4 ++-- packages/cli/package.json | 3 ++- packages/cli/src/commands/init.ts | 4 ++-- packages/cli/src/commands/rpc/utils.ts | 2 +- packages/cli/src/utils/create-helia.ts | 11 ++++++++--- packages/unixfs/src/index.ts | 2 +- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d297ed71..957b4b74 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ - [Structure](#structure) - [Project status](#project-status) -- [What's in a name?](#whats-in-a-name) +- [Name](#name) - [Background](#background) - [Roadmap](#roadmap) - [API Docs](#api-docs) @@ -36,7 +36,7 @@ The core of IPFS is the [Files API](https://github.com/ipfs/js-ipfs/blob/master/ We are also sharing about the progress so far, and discussing how you can get involved, at [Helia Demo Day](https://lu.ma/helia) every couple weeks. We'd love to see you there! -## What's in a name? +## Name Helia (*HEE-lee-ah*) is the Latin spelling of Ἡλιη -- in Greek mythology, one of the [Heliades](https://www.wikidata.org/wiki/Q12656412): the daughters of the sun god Helios. When their brother Phaethon died trying to drive the sun chariot across the sky, their tears of mourning fell to earth as amber, which is yellow (sort of), and so is JavaScript. They were then turned into [poplar](https://en.wiktionary.org/wiki/poplar) trees and, well, JavaScript is quite popular. diff --git a/packages/cli/package.json b/packages/cli/package.json index 7aeee251..715a631e 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -149,7 +149,7 @@ "@libp2p/interface-peer-id": "^2.0.1", "@libp2p/interfaces": "^3.3.1", "@libp2p/kad-dht": "^7.0.0", - "@libp2p/keychain": "^0.6.1", + "@libp2p/keychain": "^1.0.0", "@libp2p/logger": "^2.0.2", "@libp2p/mplex": "^7.1.1", "@libp2p/peer-id-factory": "^2.0.0", @@ -159,6 +159,7 @@ "@multiformats/multiaddr": "^11.1.5", "@ucans/ucans": "^0.11.0-alpha", "blockstore-datastore-adapter": "^5.0.0", + "datastore-core": "^8.0.4", "datastore-fs": "^8.0.0", "helia": "~0.0.0", "ipfs-unixfs": "^9.0.0", diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index dc8afda2..1a9aa416 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -9,7 +9,7 @@ import { FsDatastore } from 'datastore-fs' import { findHeliaDir } from '../utils/find-helia-dir.js' import { randomBytes } from '@libp2p/crypto' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { KeyChain } from '@libp2p/keychain' +import { DefaultKeyChain } from '@libp2p/keychain' import { loadRpcKeychain } from './rpc/utils.js' import type { KeyType } from '@libp2p/interface-keychain' @@ -163,7 +163,7 @@ export const init: Command = { createIfMissing: true }) await datastore.open() - const keychain = new KeyChain({ + const keychain = new DefaultKeyChain({ datastore }, { pass: keychainPassword, diff --git a/packages/cli/src/commands/rpc/utils.ts b/packages/cli/src/commands/rpc/utils.ts index 07d01397..7fe842db 100644 --- a/packages/cli/src/commands/rpc/utils.ts +++ b/packages/cli/src/commands/rpc/utils.ts @@ -4,7 +4,7 @@ import stripJsonComments from 'strip-json-comments' import fs from 'node:fs' import path from 'node:path' import * as readline from 'node:readline/promises' -import { KeyChain as DefaultKeyChain } from '@libp2p/keychain' +import { DefaultKeyChain } from '@libp2p/keychain' import type { KeyChain } from '@libp2p/interface-keychain' export async function loadRpcKeychain (configDir: string): Promise { diff --git a/packages/cli/src/utils/create-helia.ts b/packages/cli/src/utils/create-helia.ts index 1fec1ce1..36de6f2c 100644 --- a/packages/cli/src/utils/create-helia.ts +++ b/packages/cli/src/utils/create-helia.ts @@ -16,6 +16,8 @@ import stripJsonComments from 'strip-json-comments' import fs from 'node:fs' import path from 'node:path' import * as readline from 'node:readline/promises' +import { ShardingDatastore } from 'datastore-core' +import { NextToLast } from 'datastore-core/shard' export async function createHelia (configDir: string, offline: boolean = false): Promise { const config: HeliaConfig = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'helia.json'), 'utf-8'))) @@ -34,9 +36,12 @@ export async function createHelia (configDir: string, offline: boolean = false): }) await datastore.open() - const blockstore = new BlockstoreDatastoreAdapter(new FsDatastore(config.blockstore, { - createIfMissing: true - })) + const blockstore = new BlockstoreDatastoreAdapter( + new ShardingDatastore( + new FsDatastore(config.blockstore), + new NextToLast(2) + ) + ) await blockstore.open() return await createHeliaNode({ diff --git a/packages/unixfs/src/index.ts b/packages/unixfs/src/index.ts index 01412473..d86665b2 100644 --- a/packages/unixfs/src/index.ts +++ b/packages/unixfs/src/index.ts @@ -29,7 +29,7 @@ class UnixFS { async start (controller) { const result = await exporter(cid, blockstore) - if (result.type !== 'file') { + if (result.type !== 'file' && result.type !== 'raw') { throw new NotAFileError() } From d757e37ee16b7e9cb9fb77418ecc541f0b97e488 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 30 Jan 2023 12:11:57 +0100 Subject: [PATCH 09/18] chore: split unixfs out of helia command --- README.md | 4 +- packages/{cli => cli-utils}/.aegir.js | 0 packages/{cli => cli-utils}/LICENSE | 0 packages/{cli => cli-utils}/LICENSE-APACHE | 0 packages/{cli => cli-utils}/LICENSE-MIT | 0 packages/cli-utils/README.md | 44 +++++ packages/{cli => cli-utils}/package.json | 56 ++++-- .../utils => cli-utils/src}/create-helia.ts | 2 +- .../utils => cli-utils/src}/find-helia-dir.ts | 0 .../src/utils => cli-utils/src}/find-helia.ts | 2 +- .../src/utils => cli-utils/src}/format.ts | 0 .../utils => cli-utils/src}/generate-auth.ts | 0 packages/{cli => cli-utils}/src/index.ts | 172 +++++++++++++++--- .../src/load-rpc-keychain.ts} | 2 +- .../src/utils => cli-utils/src}/print-help.ts | 2 +- .../{cli => cli-utils}/test/index.spec.ts | 0 packages/{cli => cli-utils}/tsconfig.json | 6 - packages/cli/src/commands/index.ts | 123 ------------- packages/cli/src/commands/stat.ts | 38 ---- packages/cli/src/utils/config.ts | 15 -- packages/cli/src/utils/multiaddr-to-url.ts | 22 --- packages/helia-cli/.aegir.js | 6 + packages/helia-cli/LICENSE | 4 + packages/helia-cli/LICENSE-APACHE | 5 + packages/helia-cli/LICENSE-MIT | 19 ++ packages/{cli => helia-cli}/README.md | 2 +- packages/helia-cli/package.json | 158 ++++++++++++++++ .../{cli => helia-cli}/src/commands/daemon.ts | 6 +- .../{cli => helia-cli}/src/commands/id.ts | 2 +- packages/helia-cli/src/commands/index.ts | 12 ++ .../{cli => helia-cli}/src/commands/init.ts | 11 +- .../src/commands/rpc/index.ts | 2 +- .../src/commands/rpc/rmuser.ts | 4 +- .../src/commands/rpc/useradd.ts | 4 +- .../src/commands/rpc/users.ts | 4 +- .../{cli => helia-cli}/src/commands/status.ts | 5 +- packages/helia-cli/src/index.ts | 18 ++ packages/helia-cli/test/index.spec.ts | 7 + packages/helia-cli/tsconfig.json | 21 +++ packages/helia/src/utils/block-storage.ts | 33 +++- packages/interface/package.json | 3 +- packages/rpc-client/package.json | 1 + .../rpc-client/src/commands/blockstore/get.ts | 48 +++-- packages/rpc-protocol/package.json | 2 +- packages/rpc-protocol/src/blockstore.proto | 10 +- packages/rpc-protocol/src/blockstore.ts | 127 ++++++++++++- .../rpc-server/src/handlers/blockstore/get.ts | 20 +- packages/unixfs-cli/.aegir.js | 6 + packages/unixfs-cli/LICENSE | 4 + packages/unixfs-cli/LICENSE-APACHE | 5 + packages/unixfs-cli/LICENSE-MIT | 19 ++ packages/unixfs-cli/README.md | 44 +++++ packages/unixfs-cli/package.json | 156 ++++++++++++++++ .../{cli => unixfs-cli}/src/commands/add.ts | 11 +- .../{cli => unixfs-cli}/src/commands/cat.ts | 9 +- packages/unixfs-cli/src/commands/index.ts | 10 + packages/unixfs-cli/src/commands/stat.ts | 55 ++++++ packages/unixfs-cli/src/index.ts | 18 ++ .../src/utils/date-to-mtime.ts | 0 .../src/utils/glob-source.ts | 0 packages/unixfs-cli/test/index.spec.ts | 7 + packages/unixfs-cli/tsconfig.json | 18 ++ 62 files changed, 1066 insertions(+), 318 deletions(-) rename packages/{cli => cli-utils}/.aegir.js (100%) rename packages/{cli => cli-utils}/LICENSE (100%) rename packages/{cli => cli-utils}/LICENSE-APACHE (100%) rename packages/{cli => cli-utils}/LICENSE-MIT (100%) create mode 100644 packages/cli-utils/README.md rename packages/{cli => cli-utils}/package.json (81%) rename packages/{cli/src/utils => cli-utils/src}/create-helia.ts (98%) rename packages/{cli/src/utils => cli-utils/src}/find-helia-dir.ts (100%) rename packages/{cli/src/utils => cli-utils/src}/find-helia.ts (98%) rename packages/{cli/src/utils => cli-utils/src}/format.ts (100%) rename packages/{cli/src/utils => cli-utils/src}/generate-auth.ts (100%) rename packages/{cli => cli-utils}/src/index.ts (53%) rename packages/{cli/src/commands/rpc/utils.ts => cli-utils/src/load-rpc-keychain.ts} (95%) rename packages/{cli/src/utils => cli-utils/src}/print-help.ts (95%) rename packages/{cli => cli-utils}/test/index.spec.ts (100%) rename packages/{cli => cli-utils}/tsconfig.json (78%) delete mode 100644 packages/cli/src/commands/index.ts delete mode 100644 packages/cli/src/commands/stat.ts delete mode 100644 packages/cli/src/utils/config.ts delete mode 100644 packages/cli/src/utils/multiaddr-to-url.ts create mode 100644 packages/helia-cli/.aegir.js create mode 100644 packages/helia-cli/LICENSE create mode 100644 packages/helia-cli/LICENSE-APACHE create mode 100644 packages/helia-cli/LICENSE-MIT rename packages/{cli => helia-cli}/README.md (98%) create mode 100644 packages/helia-cli/package.json rename packages/{cli => helia-cli}/src/commands/daemon.ts (93%) rename packages/{cli => helia-cli}/src/commands/id.ts (86%) create mode 100644 packages/helia-cli/src/commands/index.ts rename packages/{cli => helia-cli}/src/commands/init.ts (96%) rename packages/{cli => helia-cli}/src/commands/rpc/index.ts (89%) rename packages/{cli => helia-cli}/src/commands/rpc/rmuser.ts (82%) rename packages/{cli => helia-cli}/src/commands/rpc/useradd.ts (89%) rename packages/{cli => helia-cli}/src/commands/rpc/users.ts (79%) rename packages/{cli => helia-cli}/src/commands/status.ts (87%) create mode 100644 packages/helia-cli/src/index.ts create mode 100644 packages/helia-cli/test/index.spec.ts create mode 100644 packages/helia-cli/tsconfig.json create mode 100644 packages/unixfs-cli/.aegir.js create mode 100644 packages/unixfs-cli/LICENSE create mode 100644 packages/unixfs-cli/LICENSE-APACHE create mode 100644 packages/unixfs-cli/LICENSE-MIT create mode 100644 packages/unixfs-cli/README.md create mode 100644 packages/unixfs-cli/package.json rename packages/{cli => unixfs-cli}/src/commands/add.ts (92%) rename packages/{cli => unixfs-cli}/src/commands/cat.ts (83%) create mode 100644 packages/unixfs-cli/src/commands/index.ts create mode 100644 packages/unixfs-cli/src/commands/stat.ts create mode 100644 packages/unixfs-cli/src/index.ts rename packages/{cli => unixfs-cli}/src/utils/date-to-mtime.ts (100%) rename packages/{cli => unixfs-cli}/src/utils/glob-source.ts (100%) create mode 100644 packages/unixfs-cli/test/index.spec.ts create mode 100644 packages/unixfs-cli/tsconfig.json diff --git a/README.md b/README.md index 957b4b74..8c4b2121 100644 --- a/README.md +++ b/README.md @@ -20,13 +20,15 @@ ## Structure -- [`/packages/cli`](./packages/cli) Run a Helia node on the CLI +- [`/packages/cli-utils`](./packages/cli-utils) Common code for Helia CLI tools - [`/packages/helia`](./packages/helia) An implementation of IPFS in JavaScript +- [`/packages/helia-cli`](./packages/helia-cli) Run a Helia node on the cli - [`/packages/interface`](./packages/interface) The Helia API - [`/packages/rpc-client`](./packages/rpc-client) An implementation of IPFS in JavaScript - [`/packages/rpc-protocol`](./packages/rpc-protocol) RPC protocol for use by @helia/rpc-client and @helia/rpc-server - [`/packages/rpc-server`](./packages/rpc-server) An implementation of IPFS in JavaScript - [`/packages/unixfs`](./packages/unixfs) A Helia-compatible wrapper for UnixFS +- [`/packages/unixfs-cli`](./packages/unixfs-cli) Run unixfs commands against a Helia node on the CLI ## Project status diff --git a/packages/cli/.aegir.js b/packages/cli-utils/.aegir.js similarity index 100% rename from packages/cli/.aegir.js rename to packages/cli-utils/.aegir.js diff --git a/packages/cli/LICENSE b/packages/cli-utils/LICENSE similarity index 100% rename from packages/cli/LICENSE rename to packages/cli-utils/LICENSE diff --git a/packages/cli/LICENSE-APACHE b/packages/cli-utils/LICENSE-APACHE similarity index 100% rename from packages/cli/LICENSE-APACHE rename to packages/cli-utils/LICENSE-APACHE diff --git a/packages/cli/LICENSE-MIT b/packages/cli-utils/LICENSE-MIT similarity index 100% rename from packages/cli/LICENSE-MIT rename to packages/cli-utils/LICENSE-MIT diff --git a/packages/cli-utils/README.md b/packages/cli-utils/README.md new file mode 100644 index 00000000..6d704370 --- /dev/null +++ b/packages/cli-utils/README.md @@ -0,0 +1,44 @@ +# @helia/cli-utils + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> Common code for Helia CLI tools + +## Table of contents + +- [Install](#install) +- [API Docs](#api-docs) +- [License](#license) +- [Contribute](#contribute) + +## Install + +```console +$ npm i @helia/cli-utils +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/cli/package.json b/packages/cli-utils/package.json similarity index 81% rename from packages/cli/package.json rename to packages/cli-utils/package.json index 715a631e..de66848e 100644 --- a/packages/cli/package.json +++ b/packages/cli-utils/package.json @@ -1,9 +1,9 @@ { - "name": "@helia/cli", + "name": "@helia/cli-utils", "version": "0.0.0", - "description": "Run a Helia node on the CLI", + "description": "Common code for Helia CLI tools", "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/ipfs/helia/tree/master/packages/cli#readme", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/cli-utils#readme", "repository": { "type": "git", "url": "git+https://github.com/ipfs/helia.git" @@ -18,11 +18,24 @@ "node": ">=16.0.0", "npm": ">=7.0.0" }, - "bin": { - "helia": "./dist/src/index.js" - }, "type": "module", "types": "./dist/src/index.d.ts", + "typesVersions": { + "*": { + "*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ], + "src/*": [ + "*", + "dist/*", + "dist/src/*", + "dist/src/*/index" + ] + } + }, "files": [ "src", "dist", @@ -33,6 +46,22 @@ ".": { "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" + }, + "./create-helia": { + "types": "./dist/src/create-helia.d.ts", + "import": "./dist/src/create-helia.js" + }, + "./find-helia": { + "types": "./dist/src/find-helia.d.ts", + "import": "./dist/src/find-helia.js" + }, + "./format": { + "types": "./dist/src/format.d.ts", + "import": "./dist/src/format.js" + }, + "./load-rpc-keychain": { + "types": "./dist/src/load-rpc-keychain.d.ts", + "import": "./dist/src/load-rpc-keychain.js" } }, "eslintConfig": { @@ -141,18 +170,12 @@ "@chainsafe/libp2p-yamux": "^3.0.3", "@helia/interface": "~0.0.0", "@helia/rpc-client": "~0.0.0", - "@helia/rpc-server": "~0.0.0", - "@helia/unixfs": "~0.0.0", - "@libp2p/crypto": "^1.0.11", "@libp2p/interface-keychain": "^2.0.3", - "@libp2p/interface-libp2p": "^1.1.0", "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/interfaces": "^3.3.1", "@libp2p/kad-dht": "^7.0.0", "@libp2p/keychain": "^1.0.0", "@libp2p/logger": "^2.0.2", "@libp2p/mplex": "^7.1.1", - "@libp2p/peer-id-factory": "^2.0.0", "@libp2p/prometheus-metrics": "1.1.3", "@libp2p/tcp": "^6.0.8", "@libp2p/websockets": "^5.0.2", @@ -162,16 +185,9 @@ "datastore-core": "^8.0.4", "datastore-fs": "^8.0.0", "helia": "~0.0.0", - "ipfs-unixfs": "^9.0.0", - "ipfs-unixfs-exporter": "^10.0.0", - "ipfs-unixfs-importer": "^12.0.0", - "it-glob": "^2.0.0", - "it-merge": "^2.0.0", "kleur": "^4.1.5", "libp2p": "^0.42.2", - "multiformats": "^11.0.1", - "strip-json-comments": "^5.0.0", - "uint8arrays": "^4.0.3" + "strip-json-comments": "^5.0.0" }, "devDependencies": { "aegir": "^38.1.0" diff --git a/packages/cli/src/utils/create-helia.ts b/packages/cli-utils/src/create-helia.ts similarity index 98% rename from packages/cli/src/utils/create-helia.ts rename to packages/cli-utils/src/create-helia.ts index 36de6f2c..3b0ba336 100644 --- a/packages/cli/src/utils/create-helia.ts +++ b/packages/cli-utils/src/create-helia.ts @@ -1,5 +1,5 @@ import type { Helia } from '@helia/interface' -import type { HeliaConfig } from '../index.js' +import type { HeliaConfig } from './index.js' import { createHelia as createHeliaNode } from 'helia' import { FsDatastore } from 'datastore-fs' import { BlockstoreDatastoreAdapter } from 'blockstore-datastore-adapter' diff --git a/packages/cli/src/utils/find-helia-dir.ts b/packages/cli-utils/src/find-helia-dir.ts similarity index 100% rename from packages/cli/src/utils/find-helia-dir.ts rename to packages/cli-utils/src/find-helia-dir.ts diff --git a/packages/cli/src/utils/find-helia.ts b/packages/cli-utils/src/find-helia.ts similarity index 98% rename from packages/cli/src/utils/find-helia.ts rename to packages/cli-utils/src/find-helia.ts index 10b35fb4..ea7237a7 100644 --- a/packages/cli/src/utils/find-helia.ts +++ b/packages/cli-utils/src/find-helia.ts @@ -12,7 +12,7 @@ import fs from 'node:fs' import path from 'node:path' import os from 'node:os' import { FsDatastore } from 'datastore-fs' -import { loadRpcKeychain } from '../commands/rpc/utils.js' +import { loadRpcKeychain } from './load-rpc-keychain.js' import type { PeerId } from '@libp2p/interface-peer-id' const log = logger('helia:cli:utils:find-helia') diff --git a/packages/cli/src/utils/format.ts b/packages/cli-utils/src/format.ts similarity index 100% rename from packages/cli/src/utils/format.ts rename to packages/cli-utils/src/format.ts diff --git a/packages/cli/src/utils/generate-auth.ts b/packages/cli-utils/src/generate-auth.ts similarity index 100% rename from packages/cli/src/utils/generate-auth.ts rename to packages/cli-utils/src/generate-auth.ts diff --git a/packages/cli/src/index.ts b/packages/cli-utils/src/index.ts similarity index 53% rename from packages/cli/src/index.ts rename to packages/cli-utils/src/index.ts index f6f5abb2..30a4741a 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli-utils/src/index.ts @@ -1,18 +1,130 @@ -#! /usr/bin/env node --trace-warnings -/* eslint-disable no-console */ - +import type { ParseArgsConfig } from 'node:util' +import type { Helia } from '@helia/interface' +import { InvalidParametersError } from '@helia/interface/errors' import { parseArgs } from 'node:util' +import { findHeliaDir } from './find-helia-dir.js' import path from 'node:path' +import { printHelp } from './print-help.js' import fs from 'node:fs' -import { Command, commands } from './commands/index.js' -import { InvalidParametersError } from '@helia/interface/errors' -import { printHelp } from './utils/print-help.js' -import { findHelia } from './utils/find-helia.js' -import kleur from 'kleur' -import type { Helia } from '@helia/interface' -import type { Libp2p } from '@libp2p/interface-libp2p' -import { findHeliaDir } from './utils/find-helia-dir.js' -import { config } from './utils/config.js' +import { findHelia } from './find-helia.js' +import type { Libp2p } from 'libp2p' + +/** + * Extends the internal node type to add a description to the options + */ +export interface ParseArgsOptionConfig { + /** + * Type of argument. + */ + type: 'string' | 'boolean' + + /** + * Whether this option can be provided multiple times. + * If `true`, all values will be collected in an array. + * If `false`, values for the option are last-wins. + * + * @default false. + */ + multiple?: boolean + + /** + * A single character alias for the option. + */ + short?: string + + /** + * The default option value when it is not set by args. + * It must be of the same type as the the `type` property. + * When `multiple` is `true`, it must be an array. + * + * @since v18.11.0 + */ + default?: string | boolean | string[] | boolean[] + + /** + * A description used to generate help text + */ + description: string + + /** + * If specified the value must be in this list + */ + valid?: string[] +} + +type ParseArgsOptionsConfig = Record + +export interface CommandOptions extends ParseArgsConfig { + /** + * Used to describe arguments known to the parser. + */ + options?: T +} + +export interface Command { + /** + * The command name + */ + command: string + + /** + * Used to generate help text + */ + description: string + + /** + * Used to generate help text + */ + example?: string + + /** + * Specify if this command can be run offline (default true) + */ + offline?: boolean + + /** + * Specify if this command can be run online (default true) + */ + online?: boolean + + /** + * Configuration for the command + */ + options?: ParseArgsOptionsConfig + + /** + * Run the command + */ + execute: (ctx: Context & T) => Promise + + /** + * Subcommands of the current command + */ + subcommands?: Array> +} + +export interface Context { + helia: Helia + directory: string + stdin: NodeJS.ReadStream + stdout: NodeJS.WriteStream + stderr: NodeJS.WriteStream +} + +export function createCliConfig (options?: T, strict?: boolean): ParseArgsConfig { + return { + allowPositionals: true, + strict: strict ?? true, + options: { + help: { + // @ts-expect-error description field not defined + description: 'Show help text', + type: 'boolean' + }, + ...options + } + } +} /** * Typedef for the Helia config file @@ -49,9 +161,8 @@ export interface RootArgs { } const root: Command = { - command: 'helia', - description: `${kleur.bold('Helia')} is an ${kleur.cyan('IPFS')} implementation in ${kleur.yellow('JavaScript')}`, - subcommands: commands, + command: '', + description: '', options: { directory: { description: 'The directory used by Helia to store config and data', @@ -72,8 +183,15 @@ const root: Command = { async execute () {} } -async function main (): Promise { - const rootCommandArgs = parseArgs(config(root.options)) +export async function cli (command: string, description: string, subcommands: Array>): Promise { + const rootCommand: Command = { + ...root, + command, + description, + subcommands + } + const config = createCliConfig(rootCommand.options, false) + const rootCommandArgs = parseArgs(config) const configDir = rootCommandArgs.values.directory if (configDir == null || typeof configDir !== 'string') { @@ -89,19 +207,19 @@ async function main (): Promise { } if (rootCommandArgs.values.help === true && rootCommandArgs.positionals.length === 0) { - printHelp(root, process.stdout) + printHelp(rootCommand, process.stdout) return } if (!fs.existsSync(configDir)) { - const init = commands.find(command => command.command === 'init') + const init = subcommands.find(command => command.command === 'init') if (init == null) { throw new Error('Could not find init command') } // run the init command - const parsed = parseArgs(config(init.options)) + const parsed = parseArgs(createCliConfig(init.options, false)) if (parsed.values.help === true) { printHelp(init, process.stdout) @@ -113,7 +231,8 @@ async function main (): Promise { positionals: parsed.positionals.slice(1), stdin: process.stdin, stdout: process.stdout, - stderr: process.stderr + stderr: process.stderr, + directory: configDir }) if (rootCommandArgs.positionals[0] === 'init') { @@ -123,7 +242,7 @@ async function main (): Promise { } if (rootCommandArgs.positionals.length > 0) { - let subCommand: Command = root + let subCommand: Command = rootCommand let subCommandDepth = 0 for (let i = 0; i < rootCommandArgs.positionals.length; i++) { @@ -145,7 +264,7 @@ async function main (): Promise { throw new Error('Command not found') } - const subCommandArgs = parseArgs(config(subCommand.options)) + const subCommandArgs = parseArgs(createCliConfig(subCommand.options)) if (subCommandArgs.values.help === true) { printHelp(subCommand, process.stdout) @@ -180,10 +299,5 @@ async function main (): Promise { } // no command specified, print help - printHelp(root, process.stdout) + printHelp(rootCommand, process.stdout) } - -main().catch(err => { - console.error(err) // eslint-disable-line no-console - process.exit(1) -}) diff --git a/packages/cli/src/commands/rpc/utils.ts b/packages/cli-utils/src/load-rpc-keychain.ts similarity index 95% rename from packages/cli/src/commands/rpc/utils.ts rename to packages/cli-utils/src/load-rpc-keychain.ts index 7fe842db..a0fea503 100644 --- a/packages/cli/src/commands/rpc/utils.ts +++ b/packages/cli-utils/src/load-rpc-keychain.ts @@ -1,4 +1,4 @@ -import type { HeliaConfig } from '../../index.js' +import type { HeliaConfig } from './index.js' import { FsDatastore } from 'datastore-fs' import stripJsonComments from 'strip-json-comments' import fs from 'node:fs' diff --git a/packages/cli/src/utils/print-help.ts b/packages/cli-utils/src/print-help.ts similarity index 95% rename from packages/cli/src/utils/print-help.ts rename to packages/cli-utils/src/print-help.ts index 64abbd7a..72b4ba33 100644 --- a/packages/cli/src/utils/print-help.ts +++ b/packages/cli-utils/src/print-help.ts @@ -1,4 +1,4 @@ -import type { Command } from '../commands/index.js' +import type { Command } from './index.js' import * as format from './format.js' import type { Formatable } from './format.js' import kleur from 'kleur' diff --git a/packages/cli/test/index.spec.ts b/packages/cli-utils/test/index.spec.ts similarity index 100% rename from packages/cli/test/index.spec.ts rename to packages/cli-utils/test/index.spec.ts diff --git a/packages/cli/tsconfig.json b/packages/cli-utils/tsconfig.json similarity index 78% rename from packages/cli/tsconfig.json rename to packages/cli-utils/tsconfig.json index af0863af..f56be826 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli-utils/tsconfig.json @@ -16,12 +16,6 @@ }, { "path": "../rpc-client" - }, - { - "path": "../rpc-server" - }, - { - "path": "../unixfs" } ] } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts deleted file mode 100644 index 49a04e20..00000000 --- a/packages/cli/src/commands/index.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { add } from './add.js' -import { cat } from './cat.js' -import { init } from './init.js' -import { daemon } from './daemon.js' -import { id } from './id.js' -import { status } from './status.js' -import { stat } from './stat.js' -import type { Helia } from '@helia/interface' -import type { ParseArgsConfig } from 'node:util' -import { rpc } from './rpc/index.js' - -/** - * Extends the internal node type to add a description to the options - */ -export interface ParseArgsOptionConfig { - /** - * Type of argument. - */ - type: 'string' | 'boolean' - - /** - * Whether this option can be provided multiple times. - * If `true`, all values will be collected in an array. - * If `false`, values for the option are last-wins. - * - * @default false. - */ - multiple?: boolean - - /** - * A single character alias for the option. - */ - short?: string - - /** - * The default option value when it is not set by args. - * It must be of the same type as the the `type` property. - * When `multiple` is `true`, it must be an array. - * - * @since v18.11.0 - */ - default?: string | boolean | string[] | boolean[] - - /** - * A description used to generate help text - */ - description: string - - /** - * If specified the value must be in this list - */ - valid?: string[] -} - -type ParseArgsOptionsConfig = Record - -export interface CommandOptions extends ParseArgsConfig { - /** - * Used to describe arguments known to the parser. - */ - options?: ParseArgsOptionsConfig -} - -export interface Command { - /** - * The command name - */ - command: string - - /** - * Used to generate help text - */ - description: string - - /** - * Used to generate help text - */ - example?: string - - /** - * Specify if this command can be run offline (default true) - */ - offline?: boolean - - /** - * Specify if this command can be run online (default true) - */ - online?: boolean - - /** - * Configuration for the command - */ - options?: ParseArgsOptionsConfig - - /** - * Run the command - */ - execute: (ctx: Context & T) => Promise - - /** - * Subcommands of the current command - */ - subcommands?: Array> -} - -export interface Context { - helia: Helia - directory: string - stdin: NodeJS.ReadStream - stdout: NodeJS.WriteStream - stderr: NodeJS.WriteStream -} - -export const commands: Array> = [ - add, - cat, - init, - daemon, - id, - status, - rpc, - stat -] diff --git a/packages/cli/src/commands/stat.ts b/packages/cli/src/commands/stat.ts deleted file mode 100644 index 8752ad1a..00000000 --- a/packages/cli/src/commands/stat.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Command } from './index.js' -import { exporter } from 'ipfs-unixfs-exporter' -import { CID } from 'multiformats/cid' -import * as format from '../utils/format.js' -import type { Formatable } from '../utils/format.js' - -interface StatArgs { - positionals?: string[] -} - -export const stat: Command = { - command: 'stat', - description: 'Display statistics about a dag', - example: '$ helia stat ', - options: { - }, - async execute ({ positionals, helia, stdout }) { - if (positionals == null || positionals.length === 0) { - throw new TypeError('Missing positionals') - } - - const cid = CID.parse(positionals[0]) - const entry = await exporter(cid, helia.blockstore) - - const items: Formatable[] = [ - format.table([ - format.row('CID', entry.cid.toString()), - format.row('Type', entry.type), - format.row('Size', `${entry.size}`) - ]) - ] - - format.formatter( - stdout, - items - ) - } -} diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts deleted file mode 100644 index ef112191..00000000 --- a/packages/cli/src/utils/config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { ParseArgsConfig } from 'node:util' - -export function config (options: any): ParseArgsConfig { - return { - allowPositionals: true, - strict: true, - options: { - help: { - description: 'Show help text', - type: 'boolean' - }, - ...options - } - } -} diff --git a/packages/cli/src/utils/multiaddr-to-url.ts b/packages/cli/src/utils/multiaddr-to-url.ts deleted file mode 100644 index a9671308..00000000 --- a/packages/cli/src/utils/multiaddr-to-url.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Multiaddr } from '@multiformats/multiaddr' -import { InvalidParametersError } from '@helia/interface/errors' - -export function multiaddrToUrl (addr: Multiaddr): URL { - const protoNames = addr.protoNames() - - if (protoNames.length !== 3) { - throw new InvalidParametersError('Helia RPC address format incorrect') - } - - if (protoNames[0] !== 'ip4' && protoNames[0] !== 'ip6') { - throw new InvalidParametersError('Helia RPC address format incorrect') - } - - if (protoNames[1] !== 'tcp' && protoNames[2] !== 'ws') { - throw new InvalidParametersError('Helia RPC address format incorrect') - } - - const { host, port } = addr.toOptions() - - return new URL(`ws://${host}:${port}`) -} diff --git a/packages/helia-cli/.aegir.js b/packages/helia-cli/.aegir.js new file mode 100644 index 00000000..e9c18f3e --- /dev/null +++ b/packages/helia-cli/.aegir.js @@ -0,0 +1,6 @@ + +export default { + build: { + bundle: false + } +} diff --git a/packages/helia-cli/LICENSE b/packages/helia-cli/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/helia-cli/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/helia-cli/LICENSE-APACHE b/packages/helia-cli/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/helia-cli/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/helia-cli/LICENSE-MIT b/packages/helia-cli/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/helia-cli/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/cli/README.md b/packages/helia-cli/README.md similarity index 98% rename from packages/cli/README.md rename to packages/helia-cli/README.md index 78c05b48..13cf8337 100644 --- a/packages/cli/README.md +++ b/packages/helia-cli/README.md @@ -5,7 +5,7 @@ [![codecov](https://img.shields.io/codecov/c/github/ipfs/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) [![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) -> Run a Helia node on the CLI +> Run a Helia node on the cli ## Table of contents diff --git a/packages/helia-cli/package.json b/packages/helia-cli/package.json new file mode 100644 index 00000000..29a85743 --- /dev/null +++ b/packages/helia-cli/package.json @@ -0,0 +1,158 @@ +{ + "name": "@helia/cli", + "version": "0.0.0", + "description": "Run a Helia node on the cli", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/helia-cli#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "bin": { + "helia": "./dist/src/index.js" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:node": "aegir test -t node --cov", + "release": "aegir release" + }, + "dependencies": { + "@helia/cli-utils": "~0.0.0", + "@helia/interface": "~0.0.0", + "@helia/rpc-server": "~0.0.0", + "@libp2p/crypto": "^1.0.11", + "@libp2p/interface-keychain": "^2.0.3", + "@libp2p/interface-peer-id": "^2.0.1", + "@libp2p/keychain": "^1.0.0", + "@libp2p/logger": "^2.0.2", + "@libp2p/peer-id-factory": "^2.0.0", + "datastore-fs": "^8.0.0", + "kleur": "^4.1.5", + "uint8arrays": "^4.0.3" + }, + "devDependencies": { + "aegir": "^38.1.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/cli/src/commands/daemon.ts b/packages/helia-cli/src/commands/daemon.ts similarity index 93% rename from packages/cli/src/commands/daemon.ts rename to packages/helia-cli/src/commands/daemon.ts index 45ca4cbf..3ff8eeaa 100644 --- a/packages/cli/src/commands/daemon.ts +++ b/packages/helia-cli/src/commands/daemon.ts @@ -1,11 +1,11 @@ -import type { Command } from './index.js' -import { createHelia } from '../utils/create-helia.js' +import type { Command } from '@helia/cli-utils' +import { createHelia } from '@helia/cli-utils/create-helia' import { createHeliaRpcServer } from '@helia/rpc-server' import fs from 'node:fs' import path from 'node:path' import os from 'node:os' import { logger } from '@libp2p/logger' -import { loadRpcKeychain } from './rpc/utils.js' +import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' const log = logger('helia:cli:commands:daemon') diff --git a/packages/cli/src/commands/id.ts b/packages/helia-cli/src/commands/id.ts similarity index 86% rename from packages/cli/src/commands/id.ts rename to packages/helia-cli/src/commands/id.ts index 49a354ec..bedaf199 100644 --- a/packages/cli/src/commands/id.ts +++ b/packages/helia-cli/src/commands/id.ts @@ -1,4 +1,4 @@ -import type { Command } from './index.js' +import type { Command } from '@helia/cli-utils' interface IdArgs { positionals?: string[] diff --git a/packages/helia-cli/src/commands/index.ts b/packages/helia-cli/src/commands/index.ts new file mode 100644 index 00000000..022c9ab6 --- /dev/null +++ b/packages/helia-cli/src/commands/index.ts @@ -0,0 +1,12 @@ +import { init } from './init.js' +import { daemon } from './daemon.js' +import { id } from './id.js' +import { status } from './status.js' +import type { Command } from '@helia/cli-utils' + +export const commands: Array> = [ + init, + daemon, + id, + status +] diff --git a/packages/cli/src/commands/init.ts b/packages/helia-cli/src/commands/init.ts similarity index 96% rename from packages/cli/src/commands/init.ts rename to packages/helia-cli/src/commands/init.ts index 1a9aa416..1607553b 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/helia-cli/src/commands/init.ts @@ -1,4 +1,4 @@ -import type { Command } from './index.js' +import type { Command } from '@helia/cli-utils' import path from 'node:path' import fs from 'node:fs/promises' import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory' @@ -6,12 +6,11 @@ import { InvalidParametersError } from '@helia/interface/errors' import type { PeerId } from '@libp2p/interface-peer-id' import { logger } from '@libp2p/logger' import { FsDatastore } from 'datastore-fs' -import { findHeliaDir } from '../utils/find-helia-dir.js' import { randomBytes } from '@libp2p/crypto' import { toString as uint8ArrayToString } from 'uint8arrays/to-string' import { DefaultKeyChain } from '@libp2p/keychain' -import { loadRpcKeychain } from './rpc/utils.js' import type { KeyType } from '@libp2p/interface-keychain' +import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' const log = logger('helia:cli:commands:init') @@ -56,12 +55,6 @@ export const init: Command = { short: 'b', default: '2048' }, - directory: { - description: 'The directory to store data in', - type: 'string', - short: 'd', - default: findHeliaDir() - }, directoryMode: { description: 'Create the data directory with this mode', type: 'string', diff --git a/packages/cli/src/commands/rpc/index.ts b/packages/helia-cli/src/commands/rpc/index.ts similarity index 89% rename from packages/cli/src/commands/rpc/index.ts rename to packages/helia-cli/src/commands/rpc/index.ts index df2fabae..9dbcde1f 100644 --- a/packages/cli/src/commands/rpc/index.ts +++ b/packages/helia-cli/src/commands/rpc/index.ts @@ -1,4 +1,4 @@ -import type { Command } from '../index.js' +import type { Command } from '@helia/cli-utils' import { rpcRmuser } from './rmuser.js' import { rpcUseradd } from './useradd.js' import { rpcUsers } from './users.js' diff --git a/packages/cli/src/commands/rpc/rmuser.ts b/packages/helia-cli/src/commands/rpc/rmuser.ts similarity index 82% rename from packages/cli/src/commands/rpc/rmuser.ts rename to packages/helia-cli/src/commands/rpc/rmuser.ts index 1d51bc5c..2fdafc7a 100644 --- a/packages/cli/src/commands/rpc/rmuser.ts +++ b/packages/helia-cli/src/commands/rpc/rmuser.ts @@ -1,5 +1,5 @@ -import type { Command } from '../index.js' -import { loadRpcKeychain } from './utils.js' +import type { Command } from '@helia/cli-utils' +import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' interface AddRpcUserArgs { positionals: string[] diff --git a/packages/cli/src/commands/rpc/useradd.ts b/packages/helia-cli/src/commands/rpc/useradd.ts similarity index 89% rename from packages/cli/src/commands/rpc/useradd.ts rename to packages/helia-cli/src/commands/rpc/useradd.ts index d27285b8..026ea9cd 100644 --- a/packages/cli/src/commands/rpc/useradd.ts +++ b/packages/helia-cli/src/commands/rpc/useradd.ts @@ -1,6 +1,6 @@ -import type { Command } from '../index.js' +import type { Command } from '@helia/cli-utils' import type { KeyType } from '@libp2p/interface-keychain' -import { loadRpcKeychain } from './utils.js' +import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' interface AddRpcUserArgs { positionals: string[] diff --git a/packages/cli/src/commands/rpc/users.ts b/packages/helia-cli/src/commands/rpc/users.ts similarity index 79% rename from packages/cli/src/commands/rpc/users.ts rename to packages/helia-cli/src/commands/rpc/users.ts index 57a82bf1..4ea1561a 100644 --- a/packages/cli/src/commands/rpc/users.ts +++ b/packages/helia-cli/src/commands/rpc/users.ts @@ -1,5 +1,5 @@ -import type { Command } from '../index.js' -import { loadRpcKeychain } from './utils.js' +import type { Command } from '@helia/cli-utils' +import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' export const rpcUsers: Command = { command: 'users', diff --git a/packages/cli/src/commands/status.ts b/packages/helia-cli/src/commands/status.ts similarity index 87% rename from packages/cli/src/commands/status.ts rename to packages/helia-cli/src/commands/status.ts index 492311fb..7c40d250 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/helia-cli/src/commands/status.ts @@ -1,8 +1,7 @@ -import type { Command } from './index.js' +import type { Command, RootArgs } from '@helia/cli-utils' import fs from 'node:fs' import { logger } from '@libp2p/logger' -import type { RootArgs } from '../index.js' -import { findOnlineHelia } from '../utils/find-helia.js' +import { findOnlineHelia } from '@helia/cli-utils/find-helia' const log = logger('helia:cli:commands:status') diff --git a/packages/helia-cli/src/index.ts b/packages/helia-cli/src/index.ts new file mode 100644 index 00000000..706210f5 --- /dev/null +++ b/packages/helia-cli/src/index.ts @@ -0,0 +1,18 @@ +#! /usr/bin/env node --trace-warnings +/* eslint-disable no-console */ + +import { cli } from '@helia/cli-utils' +import kleur from 'kleur' +import { commands } from './commands/index.js' + +async function main (): Promise { + const command = 'helia' + const description = `${kleur.bold('Helia')} is an ${kleur.cyan('IPFS')} implementation written in ${kleur.yellow('JavaScript')}` + + await cli(command, description, commands) +} + +main().catch(err => { + console.error(err) // eslint-disable-line no-console + process.exit(1) +}) diff --git a/packages/helia-cli/test/index.spec.ts b/packages/helia-cli/test/index.spec.ts new file mode 100644 index 00000000..e67dc48c --- /dev/null +++ b/packages/helia-cli/test/index.spec.ts @@ -0,0 +1,7 @@ +import { expect } from 'aegir/chai' + +describe('cli', () => { + it('should start a node', () => { + expect(true).to.be.ok() + }) +}) diff --git a/packages/helia-cli/tsconfig.json b/packages/helia-cli/tsconfig.json new file mode 100644 index 00000000..013a40fe --- /dev/null +++ b/packages/helia-cli/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../cli-utils" + }, + { + "path": "../interface" + }, + { + "path": "../rpc-server" + } + ] +} diff --git a/packages/helia/src/utils/block-storage.ts b/packages/helia/src/utils/block-storage.ts index 6639ee02..e88935d4 100644 --- a/packages/helia/src/utils/block-storage.ts +++ b/packages/helia/src/utils/block-storage.ts @@ -6,6 +6,11 @@ import type { Blockstore, KeyQuery, Query } from 'interface-blockstore' import type { IPFSBitswap } from 'ipfs-bitswap' import type { CID } from 'multiformats/cid' import type { AbortOptions } from '@libp2p/interfaces' +import { CustomEvent } from '@libp2p/interfaces/events' + +export interface BlockStorageOptions extends AbortOptions { + progress?: (evt: Event) => void +} /** * BlockStorage is a hybrid block datastore. It stores data in a local @@ -69,10 +74,22 @@ export class BlockStorage extends BaseBlockstore implements Blockstore { /** * Get a block by cid */ - async get (cid: CID, options: AbortOptions = {}): Promise { + async get (cid: CID, options: BlockStorageOptions = {}): Promise { if (!(await this.has(cid)) && this.bitswap.isStarted()) { + if (options.progress != null) { + options.progress(new CustomEvent('fetchFromBitswap', { + detail: cid + })) + } + return await this.bitswap.get(cid, options) } else { + if (options.progress != null) { + options.progress(new CustomEvent('fetchFromBlockstore', { + detail: cid + })) + } + return await this.child.get(cid, options) } } @@ -83,15 +100,27 @@ export class BlockStorage extends BaseBlockstore implements Blockstore { * @param {AsyncIterable | Iterable} cids * @param {AbortOptions} [options] */ - async * getMany (cids: AsyncIterable | Iterable, options: AbortOptions = {}): AsyncGenerator { + async * getMany (cids: AsyncIterable | Iterable, options: BlockStorageOptions = {}): AsyncGenerator { const getFromBitswap = pushable({ objectMode: true }) const getFromChild = pushable({ objectMode: true }) void Promise.resolve().then(async () => { for await (const cid of cids) { if (!(await this.has(cid)) && this.bitswap.isStarted()) { + if (options.progress != null) { + options.progress(new CustomEvent('fetchFromBitswap', { + detail: cid + })) + } + getFromBitswap.push(cid) } else { + if (options.progress != null) { + options.progress(new CustomEvent('fetchFromBlockstore', { + detail: cid + })) + } + getFromChild.push(cid) } } diff --git a/packages/interface/package.json b/packages/interface/package.json index f713ff42..e1bd027c 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -156,8 +156,7 @@ "@libp2p/interfaces": "^3.3.1", "@multiformats/multiaddr": "^11.1.5", "interface-blockstore": "^4.0.0", - "interface-datastore": "^7.0.3", - "multiformats": "^11.0.1" + "interface-datastore": "^7.0.3" }, "devDependencies": { "aegir": "^38.1.0" diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json index 0c962d4e..ec418ad5 100644 --- a/packages/rpc-client/package.json +++ b/packages/rpc-client/package.json @@ -140,6 +140,7 @@ "dependencies": { "@helia/interface": "~0.0.0", "@helia/rpc-protocol": "~0.0.0", + "@libp2p/interfaces": "^3.3.1", "@libp2p/interface-libp2p": "^1.1.0", "@libp2p/peer-id": "^2.0.0", "@multiformats/multiaddr": "^11.1.5", diff --git a/packages/rpc-client/src/commands/blockstore/get.ts b/packages/rpc-client/src/commands/blockstore/get.ts index 6df98547..dbc3a939 100644 --- a/packages/rpc-client/src/commands/blockstore/get.ts +++ b/packages/rpc-client/src/commands/blockstore/get.ts @@ -1,10 +1,11 @@ import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' -import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' +import { GetOptions, GetRequest, GetResponse, GetResponseType } from '@helia/rpc-protocol/blockstore' import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' import type { HeliaRpcMethodConfig } from '../../index.js' import { pbStream } from 'it-pb-stream' import type { CID } from 'multiformats/cid' +import { CustomEvent } from '@libp2p/interfaces/events' export function createBlockstoreGet (config: HeliaRpcMethodConfig): Helia['blockstore']['get'] { const get: Helia['blockstore']['get'] = async (cid: CID, options = {}) => { @@ -22,21 +23,46 @@ export function createBlockstoreGet (config: HeliaRpcMethodConfig): Helia['block stream.writePB({ cid: cid.bytes }, GetRequest) - const response = await stream.readPB(RPCCallResponse) - duplex.close() + try { + while (true) { + const response = await stream.readPB(RPCCallResponse) - if (response.type === RPCCallResponseType.message) { - if (response.message == null) { - throw new TypeError('RPC response had message type but no message') - } + if (response.type === RPCCallResponseType.error) { + throw new RPCError(response) + } - const message = GetResponse.decode(response.message) + if (response.type === RPCCallResponseType.message) { + if (response.message == null) { + throw new TypeError('RPC response had message type but no message') + } - return message.block - } + const message = GetResponse.decode(response.message) + + if (message.type === GetResponseType.PROGRESS) { + if (message.progressEventType == null) { + throw new TypeError('GetResponse progress message missing event type') + } + + // @ts-expect-error not in interface + if (options.progress != null) { + const event = new CustomEvent(message.progressEventType) - throw new RPCError(response) + // @ts-expect-error not in interface + options.progress(event) + } + } else if (message.type === GetResponseType.RESULT) { + if (message.block == null) { + throw new TypeError('GetResponse result message missing block') + } + + return message.block + } + } + } + } finally { + duplex.close() + } } return get diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json index a15ec10d..62d60f3c 100644 --- a/packages/rpc-protocol/package.json +++ b/packages/rpc-protocol/package.json @@ -158,7 +158,7 @@ "scripts": { "clean": "aegir clean", "lint": "aegir lint", - "dep-check": "aegir dep-check", + "dep-check": "aegir dep-check -i protons", "build": "aegir build", "release": "aegir release", "generate": "protons src/*.proto" diff --git a/packages/rpc-protocol/src/blockstore.proto b/packages/rpc-protocol/src/blockstore.proto index 4955fc4a..fe3a72dd 100644 --- a/packages/rpc-protocol/src/blockstore.proto +++ b/packages/rpc-protocol/src/blockstore.proto @@ -21,8 +21,16 @@ message GetRequest { bytes cid = 1; } +enum GetResponseType { + PROGRESS = 0; + RESULT = 1; +} + message GetResponse { - bytes block = 1; + GetResponseType type = 1; + optional bytes block = 2; + optional string progress_event_type = 3; + map progress_event_data = 4; } message HasOptions { diff --git a/packages/rpc-protocol/src/blockstore.ts b/packages/rpc-protocol/src/blockstore.ts index aab54aac..928e1fca 100644 --- a/packages/rpc-protocol/src/blockstore.ts +++ b/packages/rpc-protocol/src/blockstore.ts @@ -4,7 +4,7 @@ /* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ /* eslint-disable @typescript-eslint/no-empty-interface */ -import { encodeMessage, decodeMessage, message } from 'protons-runtime' +import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' @@ -272,11 +272,97 @@ export namespace GetRequest { } } +export enum GetResponseType { + PROGRESS = 'PROGRESS', + RESULT = 'RESULT' +} + +enum __GetResponseTypeValues { + PROGRESS = 0, + RESULT = 1 +} + +export namespace GetResponseType { + export const codec = (): Codec => { + return enumeration(__GetResponseTypeValues) + } +} export interface GetResponse { - block: Uint8Array + type: GetResponseType + block?: Uint8Array + progressEventType?: string + progressEventData: Map } export namespace GetResponse { + export interface GetResponse$progressEventDataEntry { + key: string + value: string + } + + export namespace GetResponse$progressEventDataEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || obj.key !== '') { + w.uint32(10) + w.string(obj.key) + } + + if (opts.writeDefaults === true || obj.value !== '') { + w.uint32(18) + w.string(obj.value) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: GetResponse$progressEventDataEntry): Uint8Array => { + return encodeMessage(obj, GetResponse$progressEventDataEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse$progressEventDataEntry => { + return decodeMessage(buf, GetResponse$progressEventDataEntry.codec()) + } + } + let _codec: Codec export const codec = (): Codec => { @@ -286,17 +372,37 @@ export namespace GetResponse { w.fork() } - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(10) + if (opts.writeDefaults === true || (obj.type != null && __GetResponseTypeValues[obj.type] !== 0)) { + w.uint32(8) + GetResponseType.codec().encode(obj.type, w) + } + + if (obj.block != null) { + w.uint32(18) w.bytes(obj.block) } + if (obj.progressEventType != null) { + w.uint32(26) + w.string(obj.progressEventType) + } + + if (obj.progressEventData != null && obj.progressEventData.size !== 0) { + for (const [key, value] of obj.progressEventData.entries()) { + w.uint32(34) + GetResponse.GetResponse$progressEventDataEntry.codec().encode({ key, value }, w, { + writeDefaults: true + }) + } + } + if (opts.lengthDelimited !== false) { w.ldelim() } }, (reader, length) => { const obj: any = { - block: new Uint8Array(0) + type: GetResponseType.PROGRESS, + progressEventData: new Map() } const end = length == null ? reader.len : reader.pos + length @@ -306,8 +412,19 @@ export namespace GetResponse { switch (tag >>> 3) { case 1: + obj.type = GetResponseType.codec().decode(reader) + break + case 2: obj.block = reader.bytes() break + case 3: + obj.progressEventType = reader.string() + break + case 4: { + const entry = GetResponse.GetResponse$progressEventDataEntry.codec().decode(reader, reader.uint32()) + obj.progressEventData.set(entry.key, entry.value) + break + } default: reader.skipType(tag & 7) break diff --git a/packages/rpc-server/src/handlers/blockstore/get.ts b/packages/rpc-server/src/handlers/blockstore/get.ts index a9a562ce..9dc5e600 100644 --- a/packages/rpc-server/src/handlers/blockstore/get.ts +++ b/packages/rpc-server/src/handlers/blockstore/get.ts @@ -1,4 +1,4 @@ -import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' +import { GetOptions, GetRequest, GetResponse, GetResponseType } from '@helia/rpc-protocol/blockstore' import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' @@ -12,13 +12,27 @@ export function createGet (config: RPCServerConfig): Service { const block = await config.helia.blockstore.get(cid, { signal, - ...opts + ...opts, + // @ts-expect-error progress is not in the interface yet + progress: (evt) => { + stream.writePB({ + type: RPCCallResponseType.message, + message: GetResponse.encode({ + type: GetResponseType.PROGRESS, + progressEventType: evt.type, + progressEventData: new Map() + }) + }, + RPCCallResponse) + } }) stream.writePB({ type: RPCCallResponseType.message, message: GetResponse.encode({ - block + type: GetResponseType.RESULT, + block, + progressEventData: new Map() }) }, RPCCallResponse) diff --git a/packages/unixfs-cli/.aegir.js b/packages/unixfs-cli/.aegir.js new file mode 100644 index 00000000..e9c18f3e --- /dev/null +++ b/packages/unixfs-cli/.aegir.js @@ -0,0 +1,6 @@ + +export default { + build: { + bundle: false + } +} diff --git a/packages/unixfs-cli/LICENSE b/packages/unixfs-cli/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/unixfs-cli/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/unixfs-cli/LICENSE-APACHE b/packages/unixfs-cli/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/unixfs-cli/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/unixfs-cli/LICENSE-MIT b/packages/unixfs-cli/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/unixfs-cli/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/unixfs-cli/README.md b/packages/unixfs-cli/README.md new file mode 100644 index 00000000..c775f146 --- /dev/null +++ b/packages/unixfs-cli/README.md @@ -0,0 +1,44 @@ +# @helia/unixfs-cli + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> Run unixfs commands against a Helia node on the CLI + +## Table of contents + +- [Install](#install) +- [API Docs](#api-docs) +- [License](#license) +- [Contribute](#contribute) + +## Install + +```console +$ npm i @helia/unixfs-cli +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/unixfs-cli/package.json b/packages/unixfs-cli/package.json new file mode 100644 index 00000000..78f34191 --- /dev/null +++ b/packages/unixfs-cli/package.json @@ -0,0 +1,156 @@ +{ + "name": "@helia/unixfs-cli", + "version": "0.0.0", + "description": "Run unixfs commands against a Helia node on the CLI", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/unixfs-cli#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "bin": { + "unixfs": "./dist/src/index.js" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:node": "aegir test -t node --cov", + "release": "aegir release" + }, + "dependencies": { + "@helia/cli-utils": "~0.0.0", + "@helia/unixfs": "~0.0.0", + "@libp2p/interfaces": "^3.3.1", + "ipfs-unixfs": "^9.0.0", + "ipfs-unixfs-exporter": "^10.0.0", + "ipfs-unixfs-importer": "^12.0.0", + "it-glob": "^2.0.0", + "it-merge": "^2.0.0", + "kleur": "^4.1.5", + "multiformats": "^11.0.1" + }, + "devDependencies": { + "aegir": "^38.1.0" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/cli/src/commands/add.ts b/packages/unixfs-cli/src/commands/add.ts similarity index 92% rename from packages/cli/src/commands/add.ts rename to packages/unixfs-cli/src/commands/add.ts index a97f66f5..1eb3323a 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/unixfs-cli/src/commands/add.ts @@ -1,4 +1,3 @@ -import type { Command } from './index.js' import { unixfs } from '@helia/unixfs' import merge from 'it-merge' import path from 'node:path' @@ -7,6 +6,7 @@ import fs from 'node:fs' import { dateToMtime } from '../utils/date-to-mtime.js' import type { Mtime } from 'ipfs-unixfs' import type { ImportCandidate, UserImporterOptions } from 'ipfs-unixfs-importer' +import type { Command } from '@helia/cli-utils' interface AddArgs { positionals: string[] @@ -16,14 +16,7 @@ interface AddArgs { export const add: Command = { command: 'add', description: 'Add a file or directory to your helia node', - example: '$ helia add path/to/file.txt', - options: { - fs: { - description: 'Which filesystem to use', - type: 'string', - default: 'unixfs' - } - }, + example: '$ unixfs add path/to/file.txt', async execute ({ positionals, helia, stdout }) { const options: UserImporterOptions = { cidVersion: 1, diff --git a/packages/cli/src/commands/cat.ts b/packages/unixfs-cli/src/commands/cat.ts similarity index 83% rename from packages/cli/src/commands/cat.ts rename to packages/unixfs-cli/src/commands/cat.ts index 7998dfd8..eb34eaa4 100644 --- a/packages/cli/src/commands/cat.ts +++ b/packages/unixfs-cli/src/commands/cat.ts @@ -1,4 +1,4 @@ -import type { Command } from './index.js' +import type { Command } from '@helia/cli-utils' import { exporter } from 'ipfs-unixfs-exporter' import { CID } from 'multiformats/cid' @@ -11,7 +11,7 @@ interface CatArgs { export const cat: Command = { command: 'cat', description: 'Fetch and cat an IPFS path referencing a file', - example: '$ helia cat ', + example: '$ unixfs cat ', options: { offset: { description: 'Where to start reading the file from', @@ -22,6 +22,11 @@ export const cat: Command = { description: 'How many bytes to read from the file', type: 'string', short: 'l' + }, + progress: { + description: 'Display information about how the CID is being resolved', + type: 'boolean', + short: 'p' } }, async execute ({ positionals, offset, length, helia, stdout }) { diff --git a/packages/unixfs-cli/src/commands/index.ts b/packages/unixfs-cli/src/commands/index.ts new file mode 100644 index 00000000..d20b3507 --- /dev/null +++ b/packages/unixfs-cli/src/commands/index.ts @@ -0,0 +1,10 @@ +import type { Command } from '@helia/cli-utils' +import { add } from './add.js' +import { cat } from './cat.js' +import { stat } from './stat.js' + +export const commands: Array> = [ + add, + cat, + stat +] diff --git a/packages/unixfs-cli/src/commands/stat.ts b/packages/unixfs-cli/src/commands/stat.ts new file mode 100644 index 00000000..9c829bb3 --- /dev/null +++ b/packages/unixfs-cli/src/commands/stat.ts @@ -0,0 +1,55 @@ +import type { Command } from '@helia/cli-utils' +import { exporter } from 'ipfs-unixfs-exporter' +import { CID } from 'multiformats/cid' +import * as format from '@helia/cli-utils/format' +import type { Formatable } from '@helia/cli-utils/format' + +interface StatArgs { + positionals?: string[] + explain?: boolean +} + +export const stat: Command = { + command: 'stat', + description: 'Display statistics about a dag', + example: '$ unixfs stat ', + options: { + explain: { + description: 'Print diagnostic information while trying to resolve the block', + type: 'boolean', + default: false + } + }, + async execute ({ positionals, helia, stdout, explain }) { + if (positionals == null || positionals.length === 0) { + throw new TypeError('Missing positionals') + } + + let progress: (evt: Event) => void | undefined + + if (explain === true) { + progress = (evt: Event) => { + stdout.write(`${evt.type}\n`) + } + } + + const cid = CID.parse(positionals[0]) + const entry = await exporter(cid, helia.blockstore, { + // @ts-expect-error + progress + }) + + const items: Formatable[] = [ + format.table([ + format.row('CID', entry.cid.toString()), + format.row('Type', entry.type), + format.row('Size', `${entry.size}`) + ]) + ] + + format.formatter( + stdout, + items + ) + } +} diff --git a/packages/unixfs-cli/src/index.ts b/packages/unixfs-cli/src/index.ts new file mode 100644 index 00000000..2ce98cd1 --- /dev/null +++ b/packages/unixfs-cli/src/index.ts @@ -0,0 +1,18 @@ +#! /usr/bin/env node --trace-warnings +/* eslint-disable no-console */ + +import { cli } from '@helia/cli-utils' +import kleur from 'kleur' +import { commands } from './commands/index.js' + +async function main (): Promise { + const command = 'unixfs' + const description = `Run unixfs commands against a ${kleur.bold('Helia')} node` + + await cli(command, description, commands) +} + +main().catch(err => { + console.error(err) // eslint-disable-line no-console + process.exit(1) +}) diff --git a/packages/cli/src/utils/date-to-mtime.ts b/packages/unixfs-cli/src/utils/date-to-mtime.ts similarity index 100% rename from packages/cli/src/utils/date-to-mtime.ts rename to packages/unixfs-cli/src/utils/date-to-mtime.ts diff --git a/packages/cli/src/utils/glob-source.ts b/packages/unixfs-cli/src/utils/glob-source.ts similarity index 100% rename from packages/cli/src/utils/glob-source.ts rename to packages/unixfs-cli/src/utils/glob-source.ts diff --git a/packages/unixfs-cli/test/index.spec.ts b/packages/unixfs-cli/test/index.spec.ts new file mode 100644 index 00000000..e67dc48c --- /dev/null +++ b/packages/unixfs-cli/test/index.spec.ts @@ -0,0 +1,7 @@ +import { expect } from 'aegir/chai' + +describe('cli', () => { + it('should start a node', () => { + expect(true).to.be.ok() + }) +}) diff --git a/packages/unixfs-cli/tsconfig.json b/packages/unixfs-cli/tsconfig.json new file mode 100644 index 00000000..e505beaf --- /dev/null +++ b/packages/unixfs-cli/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../cli-utils" + }, + { + "path": "../unixfs" + } + ] +} From f1696e0a00cb543a53deea08ef4ea1c616daeb06 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 30 Jan 2023 15:30:57 +0100 Subject: [PATCH 10/18] chore: rename id to info --- packages/helia-cli/src/commands/daemon.ts | 8 +- packages/helia-cli/src/commands/id.ts | 2 +- .../helia/src/commands/{id.ts => info.ts} | 8 +- packages/helia/src/index.ts | 4 +- packages/interface/src/index.ts | 8 +- .../rpc-client/src/commands/blockstore/get.ts | 2 + .../src/commands/{id.ts => info.ts} | 20 +- packages/rpc-client/src/index.ts | 4 +- packages/rpc-protocol/src/root.proto | 14 +- packages/rpc-protocol/src/root.ts | 172 ++---------------- .../src/handlers/{id.ts => info.ts} | 10 +- packages/rpc-server/src/index.ts | 4 +- packages/unixfs-cli/src/commands/stat.ts | 2 +- 13 files changed, 57 insertions(+), 201 deletions(-) rename packages/helia/src/commands/{id.ts => info.ts} (67%) rename packages/rpc-client/src/commands/{id.ts => info.ts} (68%) rename packages/rpc-server/src/handlers/{id.ts => info.ts} (70%) diff --git a/packages/helia-cli/src/commands/daemon.ts b/packages/helia-cli/src/commands/daemon.ts index 3ff8eeaa..5fad7e03 100644 --- a/packages/helia-cli/src/commands/daemon.ts +++ b/packages/helia-cli/src/commands/daemon.ts @@ -41,14 +41,14 @@ export const daemon: Command = { authorizationValiditySeconds: Number(authorizationValiditySeconds) }) - const id = await helia.id() + const info = await helia.info() - stdout.write(`${id.agentVersion} is running\n`) + stdout.write(`${info.agentVersion} is running\n`) - if (id.multiaddrs.length > 0) { + if (info.multiaddrs.length > 0) { stdout.write('Listening on:\n') - id.multiaddrs.forEach(ma => { + info.multiaddrs.forEach(ma => { stdout.write(` ${ma.toString()}\n`) }) } diff --git a/packages/helia-cli/src/commands/id.ts b/packages/helia-cli/src/commands/id.ts index bedaf199..7c619554 100644 --- a/packages/helia-cli/src/commands/id.ts +++ b/packages/helia-cli/src/commands/id.ts @@ -9,7 +9,7 @@ export const id: Command = { description: 'Print information out this Helia node', example: '$ helia id', async execute ({ helia, stdout }) { - const result = await helia.id() + const result = await helia.info() stdout.write(JSON.stringify(result, null, 2) + '\n') } diff --git a/packages/helia/src/commands/id.ts b/packages/helia/src/commands/info.ts similarity index 67% rename from packages/helia/src/commands/id.ts rename to packages/helia/src/commands/info.ts index f69a1109..db0ee390 100644 --- a/packages/helia/src/commands/id.ts +++ b/packages/helia/src/commands/info.ts @@ -1,12 +1,12 @@ import type { Libp2p } from '@libp2p/interface-libp2p' -import type { IdResponse } from '@helia/interface' +import type { InfoResponse } from '@helia/interface' -interface IdComponents { +interface InfoComponents { libp2p: Libp2p } -export function createId (components: IdComponents) { - return async function id (): Promise { +export function createInfo (components: InfoComponents) { + return async function info (): Promise { return { peerId: components.libp2p.peerId, multiaddrs: components.libp2p.getMultiaddrs(), diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts index 8bcde868..0d99b1bd 100644 --- a/packages/helia/src/index.ts +++ b/packages/helia/src/index.ts @@ -15,7 +15,7 @@ * ``` */ -import { createId } from './commands/id.js' +import { createInfo } from './commands/info.js' import { createBitswap } from 'ipfs-bitswap' import { BlockStorage } from './utils/block-storage.js' import type { Helia } from '@helia/interface' @@ -78,7 +78,7 @@ export async function createHelia (init: HeliaInit): Promise { blockstore: components.blockstore, datastore: init.datastore, - id: createId(components), + info: createInfo(components), stop: async () => { bitswap.stop() diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index e5ec0b64..da946c8c 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -49,12 +49,12 @@ export interface Helia { * import { createHelia } from 'helia' * * const node = await createHelia() - * const id = await node.id() + * const id = await node.info() * console.info(id) * // { peerId: PeerId(12D3Foo), ... } * ``` */ - id: (options?: IdOptions) => Promise + info: (options?: InfoOptions) => Promise /** * Stops the Helia node @@ -67,11 +67,11 @@ export interface CatOptions extends AbortOptions { length?: number } -export interface IdOptions extends AbortOptions { +export interface InfoOptions extends AbortOptions { peerId?: PeerId } -export interface IdResponse { +export interface InfoResponse { peerId: PeerId multiaddrs: Multiaddr[] agentVersion: string diff --git a/packages/rpc-client/src/commands/blockstore/get.ts b/packages/rpc-client/src/commands/blockstore/get.ts index dbc3a939..c268a919 100644 --- a/packages/rpc-client/src/commands/blockstore/get.ts +++ b/packages/rpc-client/src/commands/blockstore/get.ts @@ -1,3 +1,5 @@ +/* eslint max-depth: ["error", 5] */ + import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import { GetOptions, GetRequest, GetResponse, GetResponseType } from '@helia/rpc-protocol/blockstore' import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' diff --git a/packages/rpc-client/src/commands/id.ts b/packages/rpc-client/src/commands/info.ts similarity index 68% rename from packages/rpc-client/src/commands/id.ts rename to packages/rpc-client/src/commands/info.ts index 4d7fded2..379d690d 100644 --- a/packages/rpc-client/src/commands/id.ts +++ b/packages/rpc-client/src/commands/info.ts @@ -1,21 +1,21 @@ import { multiaddr } from '@multiformats/multiaddr' import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' -import { IdOptions, IdResponse } from '@helia/rpc-protocol/root' +import { InfoOptions, InfoResponse } from '@helia/rpc-protocol/root' import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' import { peerIdFromString } from '@libp2p/peer-id' import type { HeliaRpcMethodConfig } from '../index.js' import { pbStream } from 'it-pb-stream' -export function createId (config: HeliaRpcMethodConfig): Helia['id'] { - const id: Helia['id'] = async (options = {}) => { +export function createInfo (config: HeliaRpcMethodConfig): Helia['info'] { + const info: Helia['info'] = async (options = {}) => { const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) const stream = pbStream(duplex) stream.writePB({ - resource: '/id', + resource: '/info', method: 'INVOKE', - options: IdOptions.encode({ + options: InfoOptions.encode({ ...options, peerId: options.peerId != null ? options.peerId.toString() : undefined }) @@ -29,17 +29,17 @@ export function createId (config: HeliaRpcMethodConfig): Helia['id'] { throw new TypeError('RPC response had message type but no message') } - const idResponse = IdResponse.decode(response.message) + const infoResponse = InfoResponse.decode(response.message) return { - ...idResponse, - peerId: peerIdFromString(idResponse.peerId), - multiaddrs: idResponse.multiaddrs.map(str => multiaddr(str)) + ...infoResponse, + peerId: peerIdFromString(infoResponse.peerId), + multiaddrs: infoResponse.multiaddrs.map(str => multiaddr(str)) } } throw new RPCError(response) } - return id + return info } diff --git a/packages/rpc-client/src/index.ts b/packages/rpc-client/src/index.ts index 447043dc..25d551f8 100644 --- a/packages/rpc-client/src/index.ts +++ b/packages/rpc-client/src/index.ts @@ -1,5 +1,5 @@ import type { Helia } from '@helia/interface' -import { createId } from './commands/id.js' +import { createInfo } from './commands/info.js' import type { Libp2p } from '@libp2p/interface-libp2p' import type { Multiaddr } from '@multiformats/multiaddr' import { createBlockstoreDelete } from './commands/blockstore/delete.js' @@ -31,7 +31,7 @@ export async function createHeliaRpcClient (config: HeliaRpcClientConfig): Promi } return { - id: createId(methodConfig), + info: createInfo(methodConfig), // @ts-expect-error incomplete implementation blockstore: { delete: createBlockstoreDelete(methodConfig), diff --git a/packages/rpc-protocol/src/root.proto b/packages/rpc-protocol/src/root.proto index 9bd4e997..31fbecd2 100644 --- a/packages/rpc-protocol/src/root.proto +++ b/packages/rpc-protocol/src/root.proto @@ -1,23 +1,13 @@ syntax = "proto3"; -message IdOptions { +message InfoOptions { optional string peer_id = 1; } -message IdResponse { +message InfoResponse { string peer_id = 1; repeated string multiaddrs = 2; string agent_version = 3; string protocol_version = 4; repeated string protocols = 5; } - -message CatOptions { - string cid = 1; - int32 offset = 2; - int32 length = 3; -} - -message CatResponse { - bytes bytes = 1; -} diff --git a/packages/rpc-protocol/src/root.ts b/packages/rpc-protocol/src/root.ts index 01021cf9..9dfd55db 100644 --- a/packages/rpc-protocol/src/root.ts +++ b/packages/rpc-protocol/src/root.ts @@ -8,16 +8,16 @@ import { encodeMessage, decodeMessage, message } from 'protons-runtime' import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' -export interface IdOptions { +export interface InfoOptions { peerId?: string } -export namespace IdOptions { - let _codec: Codec +export namespace InfoOptions { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } @@ -55,16 +55,16 @@ export namespace IdOptions { return _codec } - export const encode = (obj: IdOptions): Uint8Array => { - return encodeMessage(obj, IdOptions.codec()) + export const encode = (obj: InfoOptions): Uint8Array => { + return encodeMessage(obj, InfoOptions.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): IdOptions => { - return decodeMessage(buf, IdOptions.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): InfoOptions => { + return decodeMessage(buf, InfoOptions.codec()) } } -export interface IdResponse { +export interface InfoResponse { peerId: string multiaddrs: string[] agentVersion: string @@ -72,12 +72,12 @@ export interface IdResponse { protocols: string[] } -export namespace IdResponse { - let _codec: Codec +export namespace InfoResponse { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } @@ -157,147 +157,11 @@ export namespace IdResponse { return _codec } - export const encode = (obj: IdResponse): Uint8Array => { - return encodeMessage(obj, IdResponse.codec()) + export const encode = (obj: InfoResponse): Uint8Array => { + return encodeMessage(obj, InfoResponse.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): IdResponse => { - return decodeMessage(buf, IdResponse.codec()) - } -} - -export interface CatOptions { - cid: string - offset: number - length: number -} - -export namespace CatOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || obj.cid !== '') { - w.uint32(10) - w.string(obj.cid) - } - - if (opts.writeDefaults === true || obj.offset !== 0) { - w.uint32(16) - w.int32(obj.offset) - } - - if (opts.writeDefaults === true || obj.length !== 0) { - w.uint32(24) - w.int32(obj.length) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: '', - offset: 0, - length: 0 - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.string() - break - case 2: - obj.offset = reader.int32() - break - case 3: - obj.length = reader.int32() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: CatOptions): Uint8Array => { - return encodeMessage(obj, CatOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): CatOptions => { - return decodeMessage(buf, CatOptions.codec()) - } -} - -export interface CatResponse { - bytes: Uint8Array -} - -export namespace CatResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.bytes != null && obj.bytes.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.bytes) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - bytes: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.bytes = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: CatResponse): Uint8Array => { - return encodeMessage(obj, CatResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): CatResponse => { - return decodeMessage(buf, CatResponse.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): InfoResponse => { + return decodeMessage(buf, InfoResponse.codec()) } } diff --git a/packages/rpc-server/src/handlers/id.ts b/packages/rpc-server/src/handlers/info.ts similarity index 70% rename from packages/rpc-server/src/handlers/id.ts rename to packages/rpc-server/src/handlers/info.ts index aa8c8fe0..a7df8445 100644 --- a/packages/rpc-server/src/handlers/id.ts +++ b/packages/rpc-server/src/handlers/info.ts @@ -1,22 +1,22 @@ -import { IdOptions, IdResponse } from '@helia/rpc-protocol/root' +import { InfoOptions, InfoResponse } from '@helia/rpc-protocol/root' import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import { peerIdFromString } from '@libp2p/peer-id' import type { RPCServerConfig, Service } from '../index.js' -export function createId (config: RPCServerConfig): Service { +export function createInfo (config: RPCServerConfig): Service { return { insecure: true, async handle ({ options, stream, signal }): Promise { - const opts = IdOptions.decode(options) + const opts = InfoOptions.decode(options) - const result = await config.helia.id({ + const result = await config.helia.info({ peerId: opts.peerId != null ? peerIdFromString(opts.peerId) : undefined, signal }) stream.writePB({ type: RPCCallResponseType.message, - message: IdResponse.encode({ + message: InfoResponse.encode({ ...result, peerId: result.peerId.toString(), multiaddrs: result.multiaddrs.map(ma => ma.toString()) diff --git a/packages/rpc-server/src/index.ts b/packages/rpc-server/src/index.ts index 27488d8d..be3e025e 100644 --- a/packages/rpc-server/src/index.ts +++ b/packages/rpc-server/src/index.ts @@ -1,6 +1,6 @@ import type { Helia } from '@helia/interface' import { HeliaError } from '@helia/interface/errors' -import { createId } from './handlers/id.js' +import { createInfo } from './handlers/info.js' import { logger } from '@libp2p/logger' import { HELIA_RPC_PROTOCOL } from '@helia/rpc-protocol' import { RPCCallRequest, RPCCallResponseType, RPCCallResponse } from '@helia/rpc-protocol/rpc' @@ -68,7 +68,7 @@ export async function createHeliaRpcServer (config: RPCServerConfig): Promise { diff --git a/packages/unixfs-cli/src/commands/stat.ts b/packages/unixfs-cli/src/commands/stat.ts index 9c829bb3..38f21ca9 100644 --- a/packages/unixfs-cli/src/commands/stat.ts +++ b/packages/unixfs-cli/src/commands/stat.ts @@ -25,7 +25,7 @@ export const stat: Command = { throw new TypeError('Missing positionals') } - let progress: (evt: Event) => void | undefined + let progress: undefined | ((evt: Event) => void) if (explain === true) { progress = (evt: Event) => { From 7396887196e1b00ac57d43511d4ae7eb369b6245 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Mon, 30 Jan 2023 16:19:59 +0100 Subject: [PATCH 11/18] chore: add bootstrap --- packages/cli-utils/package.json | 1 + packages/cli-utils/src/create-helia.ts | 6 ++++++ packages/cli-utils/src/index.ts | 3 ++- packages/helia-cli/src/commands/init.ts | 8 +++++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index de66848e..19f5e188 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -170,6 +170,7 @@ "@chainsafe/libp2p-yamux": "^3.0.3", "@helia/interface": "~0.0.0", "@helia/rpc-client": "~0.0.0", + "@libp2p/bootstrap": "^6.0.0", "@libp2p/interface-keychain": "^2.0.3", "@libp2p/interface-peer-id": "^2.0.1", "@libp2p/kad-dht": "^7.0.0", diff --git a/packages/cli-utils/src/create-helia.ts b/packages/cli-utils/src/create-helia.ts index 3b0ba336..c2a67355 100644 --- a/packages/cli-utils/src/create-helia.ts +++ b/packages/cli-utils/src/create-helia.ts @@ -12,6 +12,7 @@ import { mplex } from '@libp2p/mplex' import { prometheusMetrics } from '@libp2p/prometheus-metrics' import { gossipsub } from '@chainsafe/libp2p-gossipsub' import { kadDHT } from '@libp2p/kad-dht' +import { bootstrap } from '@libp2p/bootstrap' import stripJsonComments from 'strip-json-comments' import fs from 'node:fs' import path from 'node:path' @@ -73,6 +74,11 @@ export async function createHelia (configDir: string, offline: boolean = false): yamux(), mplex() ], + peerDiscovery: [ + bootstrap({ + list: config.libp2p.bootstrap + }) + ], pubsub: gossipsub(), dht: kadDHT(), metrics: prometheusMetrics() diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts index 30a4741a..600bad55 100644 --- a/packages/cli-utils/src/index.ts +++ b/packages/cli-utils/src/index.ts @@ -141,7 +141,8 @@ export interface HeliaConfig { keychain: { salt: string password?: string - } + }, + bootstrap: string[] } rpc: { datastore: string diff --git a/packages/helia-cli/src/commands/init.ts b/packages/helia-cli/src/commands/init.ts index 1607553b..6440077f 100644 --- a/packages/helia-cli/src/commands/init.ts +++ b/packages/helia-cli/src/commands/init.ts @@ -207,7 +207,13 @@ export const init: Command = { ? `, "password": "${keychainPassword}"` : ''} - } + }, + "bootstrap": [ + "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", + "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", + "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", + "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt" + ] }, "rpc": { "datastore": "${rpcDatastorePath}", From 7e3e2dc98a7417488cec501d3363b7a2dac138f8 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 3 Feb 2023 00:06:56 +0100 Subject: [PATCH 12/18] chore: add unixfs implemenation --- README.md | 1 + package.json | 1 + packages/cli-utils/package.json | 2 +- packages/cli-utils/src/create-helia.ts | 4 +- packages/cli-utils/src/index.ts | 2 +- packages/helia-cli/package.json | 2 +- packages/helia/package.json | 2 +- packages/helia/src/index.ts | 1 + packages/interface/package.json | 2 +- packages/interface/src/errors.ts | 6 + packages/interface/src/index.ts | 5 - packages/interop/.aegir.js | 45 + packages/interop/LICENSE | 4 + packages/interop/LICENSE-APACHE | 5 + packages/interop/LICENSE-MIT | 19 + packages/interop/README.md | 53 + packages/interop/package.json | 166 ++ packages/interop/src/index.ts | 1 + packages/interop/test/files.spec.ts | 56 + .../test/fixtures/create-helia.browser.ts | 49 + .../interop/test/fixtures/create-helia.ts | 50 + packages/interop/test/fixtures/create-kubo.ts | 27 + packages/interop/tsconfig.json | 18 + packages/rpc-client/package.json | 4 +- .../src/commands/authorization/get.ts | 49 +- .../src/commands/blockstore/batch.ts | 84 + .../src/commands/blockstore/close.ts | 11 + .../src/commands/blockstore/delete-many.ts | 22 + .../src/commands/blockstore/delete.ts | 43 +- .../src/commands/blockstore/get-many.ts | 22 + .../rpc-client/src/commands/blockstore/get.ts | 77 +- .../rpc-client/src/commands/blockstore/has.ts | 47 +- .../src/commands/blockstore/open.ts | 11 + .../src/commands/blockstore/put-many.ts | 23 + .../rpc-client/src/commands/blockstore/put.ts | 45 +- .../src/commands/blockstore/query-keys.ts | 16 + .../src/commands/blockstore/query.ts | 19 + packages/rpc-client/src/commands/info.ts | 48 +- .../rpc-client/src/commands/utils/rpc-call.ts | 112 + packages/rpc-client/src/index.ts | 30 +- packages/rpc-protocol/package.json | 4 +- packages/rpc-protocol/src/authorization.ts | 16 +- packages/rpc-protocol/src/blockstore.proto | 130 +- packages/rpc-protocol/src/blockstore.ts | 1896 ++++++++++++--- packages/rpc-protocol/src/datastore.proto | 164 ++ packages/rpc-protocol/src/datastore.ts | 2085 +++++++++++++++++ packages/rpc-protocol/src/index.ts | 27 +- packages/rpc-protocol/src/root.ts | 18 +- packages/rpc-protocol/src/rpc.proto | 38 +- packages/rpc-protocol/src/rpc.ts | 329 ++- packages/rpc-server/package.json | 2 +- .../src/handlers/authorization/get.ts | 34 +- .../src/handlers/blockstore/batch.ts | 40 + .../src/handlers/blockstore/close.ts | 9 + .../src/handlers/blockstore/delete-many.ts | 32 + .../src/handlers/blockstore/delete.ts | 8 +- .../src/handlers/blockstore/get-many.ts | 32 + .../rpc-server/src/handlers/blockstore/get.ts | 28 +- .../rpc-server/src/handlers/blockstore/has.ts | 8 +- .../src/handlers/blockstore/open.ts | 9 + .../src/handlers/blockstore/put-many.ts | 32 + .../rpc-server/src/handlers/blockstore/put.ts | 8 +- .../src/handlers/blockstore/query-keys.ts | 25 + .../src/handlers/blockstore/query.ts | 26 + packages/rpc-server/src/handlers/index.ts | 36 + packages/rpc-server/src/handlers/info.ts | 6 +- packages/rpc-server/src/index.ts | 42 +- packages/unixfs-cli/src/commands/add.ts | 4 +- packages/unixfs/package.json | 19 +- packages/unixfs/src/commands/add.ts | 46 + packages/unixfs/src/commands/cat.ts | 31 + packages/unixfs/src/commands/chmod.ts | 133 ++ packages/unixfs/src/commands/cp.ts | 41 + packages/unixfs/src/commands/ls.ts | 36 + packages/unixfs/src/commands/mkdir.ts | 71 + packages/unixfs/src/commands/rm.ts | 31 + packages/unixfs/src/commands/stat.ts | 137 ++ packages/unixfs/src/commands/touch.ts | 136 ++ .../unixfs/src/commands/utils/add-link.ts | 319 +++ .../src/commands/utils/cid-to-directory.ts | 23 + .../src/commands/utils/cid-to-pblink.ts | 26 + .../unixfs/src/commands/utils/dir-sharded.ts | 219 ++ packages/unixfs/src/commands/utils/errors.ts | 31 + .../src/commands/utils/hamt-constants.ts | 14 + .../unixfs/src/commands/utils/hamt-utils.ts | 285 +++ packages/unixfs/src/commands/utils/persist.ts | 22 + .../unixfs/src/commands/utils/remove-link.ts | 151 ++ packages/unixfs/src/commands/utils/resolve.ts | 130 + packages/unixfs/src/index.ts | 189 +- packages/unixfs/test/cat.spec.ts | 87 + packages/unixfs/test/chmod.spec.ts | 100 + packages/unixfs/test/cp.spec.ts | 201 ++ packages/unixfs/test/index.spec.ts | 7 - packages/unixfs/test/ls.spec.ts | 119 + packages/unixfs/test/mkdir.spec.ts | 118 + packages/unixfs/test/rm.spec.ts | 177 ++ packages/unixfs/test/stat.spec.ts | 236 ++ packages/unixfs/test/touch.spec.ts | 129 + 98 files changed, 8790 insertions(+), 746 deletions(-) create mode 100644 packages/interop/.aegir.js create mode 100644 packages/interop/LICENSE create mode 100644 packages/interop/LICENSE-APACHE create mode 100644 packages/interop/LICENSE-MIT create mode 100644 packages/interop/README.md create mode 100644 packages/interop/package.json create mode 100644 packages/interop/src/index.ts create mode 100644 packages/interop/test/files.spec.ts create mode 100644 packages/interop/test/fixtures/create-helia.browser.ts create mode 100644 packages/interop/test/fixtures/create-helia.ts create mode 100644 packages/interop/test/fixtures/create-kubo.ts create mode 100644 packages/interop/tsconfig.json create mode 100644 packages/rpc-client/src/commands/blockstore/batch.ts create mode 100644 packages/rpc-client/src/commands/blockstore/close.ts create mode 100644 packages/rpc-client/src/commands/blockstore/delete-many.ts create mode 100644 packages/rpc-client/src/commands/blockstore/get-many.ts create mode 100644 packages/rpc-client/src/commands/blockstore/open.ts create mode 100644 packages/rpc-client/src/commands/blockstore/put-many.ts create mode 100644 packages/rpc-client/src/commands/blockstore/query-keys.ts create mode 100644 packages/rpc-client/src/commands/blockstore/query.ts create mode 100644 packages/rpc-client/src/commands/utils/rpc-call.ts create mode 100644 packages/rpc-protocol/src/datastore.proto create mode 100644 packages/rpc-protocol/src/datastore.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/batch.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/close.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/delete-many.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/get-many.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/open.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/put-many.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/query-keys.ts create mode 100644 packages/rpc-server/src/handlers/blockstore/query.ts create mode 100644 packages/rpc-server/src/handlers/index.ts create mode 100644 packages/unixfs/src/commands/add.ts create mode 100644 packages/unixfs/src/commands/cat.ts create mode 100644 packages/unixfs/src/commands/chmod.ts create mode 100644 packages/unixfs/src/commands/cp.ts create mode 100644 packages/unixfs/src/commands/ls.ts create mode 100644 packages/unixfs/src/commands/mkdir.ts create mode 100644 packages/unixfs/src/commands/rm.ts create mode 100644 packages/unixfs/src/commands/stat.ts create mode 100644 packages/unixfs/src/commands/touch.ts create mode 100644 packages/unixfs/src/commands/utils/add-link.ts create mode 100644 packages/unixfs/src/commands/utils/cid-to-directory.ts create mode 100644 packages/unixfs/src/commands/utils/cid-to-pblink.ts create mode 100644 packages/unixfs/src/commands/utils/dir-sharded.ts create mode 100644 packages/unixfs/src/commands/utils/errors.ts create mode 100644 packages/unixfs/src/commands/utils/hamt-constants.ts create mode 100644 packages/unixfs/src/commands/utils/hamt-utils.ts create mode 100644 packages/unixfs/src/commands/utils/persist.ts create mode 100644 packages/unixfs/src/commands/utils/remove-link.ts create mode 100644 packages/unixfs/src/commands/utils/resolve.ts create mode 100644 packages/unixfs/test/cat.spec.ts create mode 100644 packages/unixfs/test/chmod.spec.ts create mode 100644 packages/unixfs/test/cp.spec.ts delete mode 100644 packages/unixfs/test/index.spec.ts create mode 100644 packages/unixfs/test/ls.spec.ts create mode 100644 packages/unixfs/test/mkdir.spec.ts create mode 100644 packages/unixfs/test/rm.spec.ts create mode 100644 packages/unixfs/test/stat.spec.ts create mode 100644 packages/unixfs/test/touch.spec.ts diff --git a/README.md b/README.md index 8c4b2121..a4adcea8 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ - [`/packages/helia`](./packages/helia) An implementation of IPFS in JavaScript - [`/packages/helia-cli`](./packages/helia-cli) Run a Helia node on the cli - [`/packages/interface`](./packages/interface) The Helia API +- [`/packages/interop`](./packages/interop) Interop tests for Helia - [`/packages/rpc-client`](./packages/rpc-client) An implementation of IPFS in JavaScript - [`/packages/rpc-protocol`](./packages/rpc-protocol) RPC protocol for use by @helia/rpc-client and @helia/rpc-server - [`/packages/rpc-server`](./packages/rpc-server) An implementation of IPFS in JavaScript diff --git a/package.json b/package.json index 295c9c69..a2ce5243 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "dependencies": { "aegir": "^38.1.0" }, + "type": "module", "workspaces": [ "packages/*" ] diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json index 19f5e188..4b291d43 100644 --- a/packages/cli-utils/package.json +++ b/packages/cli-utils/package.json @@ -175,7 +175,7 @@ "@libp2p/interface-peer-id": "^2.0.1", "@libp2p/kad-dht": "^7.0.0", "@libp2p/keychain": "^1.0.0", - "@libp2p/logger": "^2.0.2", + "@libp2p/logger": "^2.0.5", "@libp2p/mplex": "^7.1.1", "@libp2p/prometheus-metrics": "1.1.3", "@libp2p/tcp": "^6.0.8", diff --git a/packages/cli-utils/src/create-helia.ts b/packages/cli-utils/src/create-helia.ts index c2a67355..7ebe2da4 100644 --- a/packages/cli-utils/src/create-helia.ts +++ b/packages/cli-utils/src/create-helia.ts @@ -45,7 +45,7 @@ export async function createHelia (configDir: string, offline: boolean = false): ) await blockstore.open() - return await createHeliaNode({ + const helia = await createHeliaNode({ blockstore, datastore, libp2p: await createLibp2p({ @@ -84,4 +84,6 @@ export async function createHelia (configDir: string, offline: boolean = false): metrics: prometheusMetrics() }) }) + + return helia } diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts index 600bad55..b46c753c 100644 --- a/packages/cli-utils/src/index.ts +++ b/packages/cli-utils/src/index.ts @@ -141,7 +141,7 @@ export interface HeliaConfig { keychain: { salt: string password?: string - }, + } bootstrap: string[] } rpc: { diff --git a/packages/helia-cli/package.json b/packages/helia-cli/package.json index 29a85743..88298451 100644 --- a/packages/helia-cli/package.json +++ b/packages/helia-cli/package.json @@ -143,7 +143,7 @@ "@libp2p/interface-keychain": "^2.0.3", "@libp2p/interface-peer-id": "^2.0.1", "@libp2p/keychain": "^1.0.0", - "@libp2p/logger": "^2.0.2", + "@libp2p/logger": "^2.0.5", "@libp2p/peer-id-factory": "^2.0.0", "datastore-fs": "^8.0.0", "kleur": "^4.1.5", diff --git a/packages/helia/package.json b/packages/helia/package.json index 2d3d2511..1d7ecffa 100644 --- a/packages/helia/package.json +++ b/packages/helia/package.json @@ -142,7 +142,7 @@ "@libp2p/interface-libp2p": "^1.1.0", "@libp2p/interfaces": "^3.3.1", "blockstore-core": "^3.0.0", - "interface-blockstore": "^4.0.0", + "interface-blockstore": "^4.0.1", "interface-datastore": "^7.0.3", "ipfs-bitswap": "^15.0.0", "it-filter": "^2.0.0", diff --git a/packages/helia/src/index.ts b/packages/helia/src/index.ts index 0d99b1bd..492552ee 100644 --- a/packages/helia/src/index.ts +++ b/packages/helia/src/index.ts @@ -82,6 +82,7 @@ export async function createHelia (init: HeliaInit): Promise { stop: async () => { bitswap.stop() + await init.libp2p.stop() } } diff --git a/packages/interface/package.json b/packages/interface/package.json index e1bd027c..96140295 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -155,7 +155,7 @@ "@libp2p/interface-peer-id": "^2.0.1", "@libp2p/interfaces": "^3.3.1", "@multiformats/multiaddr": "^11.1.5", - "interface-blockstore": "^4.0.0", + "interface-blockstore": "^4.0.1", "interface-datastore": "^7.0.3" }, "devDependencies": { diff --git a/packages/interface/src/errors.ts b/packages/interface/src/errors.ts index be8b5e40..ff01bff7 100644 --- a/packages/interface/src/errors.ts +++ b/packages/interface/src/errors.ts @@ -16,6 +16,12 @@ export class NotAFileError extends HeliaError { } } +export class NotADirectoryError extends HeliaError { + constructor (message = 'not a directory') { + super(message, 'NotAFileError', 'ERR_NOT_DIRECTORY') + } +} + export class NoContentError extends HeliaError { constructor (message = 'no content') { super(message, 'NoContentError', 'ERR_NO_CONTENT') diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index da946c8c..2417005e 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -62,11 +62,6 @@ export interface Helia { stop: () => Promise } -export interface CatOptions extends AbortOptions { - offset?: number - length?: number -} - export interface InfoOptions extends AbortOptions { peerId?: PeerId } diff --git a/packages/interop/.aegir.js b/packages/interop/.aegir.js new file mode 100644 index 00000000..498799ac --- /dev/null +++ b/packages/interop/.aegir.js @@ -0,0 +1,45 @@ +import getPort from 'aegir/get-port' +import { createServer } from 'ipfsd-ctl' +import * as kuboRpcClient from 'kubo-rpc-client' + +/** @type {import('aegir').PartialOptions} */ +export default { + test: { + before: async (options) => { + if (options.runner !== 'node') { + const ipfsdPort = await getPort() + const ipfsdServer = await createServer({ + host: '127.0.0.1', + port: ipfsdPort + }, { + ipfsBin: (await import('go-ipfs')).default.path(), + kuboRpcModule: kuboRpcClient, + ipfsOptions: { + config: { + Addresses: { + Swarm: [ + "/ip4/0.0.0.0/tcp/4001", + "/ip4/0.0.0.0/tcp/4002/ws" + ] + } + } + } + }).start() + + return { + env: { + IPFSD_SERVER: `http://127.0.0.1:${ipfsdPort}` + }, + ipfsdServer + } + } + + return {} + }, + after: async (options, beforeResult) => { + if (options.runner !== 'node') { + await beforeResult.ipfsdServer.stop() + } + } + } +} diff --git a/packages/interop/LICENSE b/packages/interop/LICENSE new file mode 100644 index 00000000..20ce483c --- /dev/null +++ b/packages/interop/LICENSE @@ -0,0 +1,4 @@ +This project is dual licensed under MIT and Apache-2.0. + +MIT: https://www.opensource.org/licenses/mit +Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/interop/LICENSE-APACHE b/packages/interop/LICENSE-APACHE new file mode 100644 index 00000000..14478a3b --- /dev/null +++ b/packages/interop/LICENSE-APACHE @@ -0,0 +1,5 @@ +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/interop/LICENSE-MIT b/packages/interop/LICENSE-MIT new file mode 100644 index 00000000..72dc60d8 --- /dev/null +++ b/packages/interop/LICENSE-MIT @@ -0,0 +1,19 @@ +The MIT License (MIT) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/packages/interop/README.md b/packages/interop/README.md new file mode 100644 index 00000000..2e3eed6c --- /dev/null +++ b/packages/interop/README.md @@ -0,0 +1,53 @@ +# @helia/interop + +[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) +[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) + +> Interop tests for Helia + +## Table of contents + +- [Install](#install) + - [Browser ` +``` + +## API Docs + +- + +## License + +Licensed under either of + +- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) +- MIT ([LICENSE-MIT](LICENSE-MIT) / ) + +## Contribute + +Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). + +Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. + +Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. + +[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/interop/package.json b/packages/interop/package.json new file mode 100644 index 00000000..493a44b2 --- /dev/null +++ b/packages/interop/package.json @@ -0,0 +1,166 @@ +{ + "name": "@helia/interop", + "version": "0.0.0", + "description": "Interop tests for Helia", + "license": "Apache-2.0 OR MIT", + "homepage": "https://github.com/ipfs/helia/tree/master/packages/interop#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/ipfs/helia.git" + }, + "bugs": { + "url": "https://github.com/ipfs/helia/issues" + }, + "keywords": [ + "IPFS" + ], + "engines": { + "node": ">=16.0.0", + "npm": ">=7.0.0" + }, + "type": "module", + "types": "./dist/src/index.d.ts", + "files": [ + "src", + "dist", + "!dist/test", + "!**/*.tsbuildinfo" + ], + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js" + } + }, + "eslintConfig": { + "extends": "ipfs", + "parserOptions": { + "sourceType": "module" + } + }, + "release": { + "branches": [ + "main" + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + { + "breaking": true, + "release": "major" + }, + { + "revert": true, + "release": "patch" + }, + { + "type": "feat", + "release": "minor" + }, + { + "type": "fix", + "release": "patch" + }, + { + "type": "docs", + "release": "patch" + }, + { + "type": "test", + "release": "patch" + }, + { + "type": "deps", + "release": "patch" + }, + { + "scope": "no-release", + "release": false + } + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "presetConfig": { + "types": [ + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "chore", + "section": "Trivial Changes" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "deps", + "section": "Dependencies" + }, + { + "type": "test", + "section": "Tests" + } + ] + } + } + ], + "@semantic-release/changelog", + "@semantic-release/npm", + "@semantic-release/github", + "@semantic-release/git" + ] + }, + "scripts": { + "clean": "aegir clean", + "lint": "aegir lint", + "dep-check": "aegir dep-check", + "build": "aegir build", + "test": "aegir test", + "test:chrome": "aegir test -t browser --cov", + "test:chrome-webworker": "aegir test -t webworker", + "test:firefox": "aegir test -t browser -- --browser firefox", + "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", + "test:node": "aegir test -t node --cov", + "test:electron-main": "aegir test -t electron-main", + "release": "aegir release" + }, + "devDependencies": { + "@chainsafe/libp2p-noise": "^11.0.0", + "@chainsafe/libp2p-yamux": "^3.0.5", + "@helia/interface": "~0.0.0", + "@libp2p/tcp": "^6.1.2", + "@libp2p/websockets": "^5.0.3", + "aegir": "^38.1.0", + "blockstore-core": "^3.0.0", + "datastore-core": "^8.0.4", + "go-ipfs": "^0.18.1", + "helia": "~0.0.0", + "ipfsd-ctl": "^13.0.0", + "it-to-buffer": "^3.0.0", + "kubo-rpc-client": "^3.0.0", + "libp2p": "^0.42.2", + "multiformats": "^11.0.1", + "wherearewe": "^2.0.1" + }, + "browser": { + "./dist/test/fixtures/create-helia.js": "./dist/test/fixtures/create-helia.browser.js", + "go-ipfs": false + }, + "private": true, + "typedoc": { + "entryPoint": "./src/index.ts" + } +} diff --git a/packages/interop/src/index.ts b/packages/interop/src/index.ts new file mode 100644 index 00000000..336ce12b --- /dev/null +++ b/packages/interop/src/index.ts @@ -0,0 +1 @@ +export {} diff --git a/packages/interop/test/files.spec.ts b/packages/interop/test/files.spec.ts new file mode 100644 index 00000000..be314264 --- /dev/null +++ b/packages/interop/test/files.spec.ts @@ -0,0 +1,56 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import { createHeliaNode } from './fixtures/create-helia.js' +import { createKuboNode } from './fixtures/create-kubo.js' +import type { Helia } from '@helia/interface' +import type { Controller } from 'ipfsd-ctl' +import toBuffer from 'it-to-buffer' +import { sha256 } from 'multiformats/hashes/sha2' +import { CID } from 'multiformats/cid' +import * as raw from 'multiformats/codecs/raw' + +describe('files', () => { + let helia: Helia + let kubo: Controller + + beforeEach(async () => { + helia = await createHeliaNode() + kubo = await createKuboNode() + + // connect the two nodes + await helia.libp2p.peerStore.addressBook.add(kubo.peer.id, kubo.peer.addresses) + await helia.libp2p.dial(kubo.peer.id) + }) + + afterEach(async () => { + if (helia != null) { + await helia.stop() + } + + if (kubo != null) { + await kubo.stop() + } + }) + + it('should be able to send a block', async () => { + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const digest = await sha256.digest(input) + const cid = CID.createV1(raw.code, digest) + await helia.blockstore.put(cid, input) + const output = await toBuffer(kubo.api.cat(cid)) + + expect(output).to.equalBytes(input) + }) + + it('should be able to receive a block', async () => { + const input = Uint8Array.from([0, 1, 2, 3, 4]) + const { cid } = await kubo.api.add({ content: input }, { + cidVersion: 1, + rawLeaves: true + }) + const output = await helia.blockstore.get(cid) + + expect(output).to.equalBytes(input) + }) +}) diff --git a/packages/interop/test/fixtures/create-helia.browser.ts b/packages/interop/test/fixtures/create-helia.browser.ts new file mode 100644 index 00000000..58502b0e --- /dev/null +++ b/packages/interop/test/fixtures/create-helia.browser.ts @@ -0,0 +1,49 @@ +import { createHelia } from 'helia' +import { createLibp2p } from 'libp2p' +import { webSockets } from '@libp2p/websockets' +import { all } from '@libp2p/websockets/filters' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { MemoryBlockstore } from 'blockstore-core' +import { MemoryDatastore } from 'datastore-core' +import type { Helia } from '@helia/interface' + +export async function createHeliaNode (): Promise { + const blockstore = new MemoryBlockstore() + const datastore = new MemoryDatastore() + + // dial-only in the browser until webrtc browser-to-browser arrives + const libp2p = await createLibp2p({ + transports: [ + webSockets({ + filter: all + }) + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + datastore, + identify: { + host: { + agentVersion: 'helia/0.0.0' + } + }, + nat: { + enabled: false + }, + relay: { + enabled: false + } + }) + + const helia = await createHelia({ + libp2p, + blockstore, + datastore + }) + + return helia +} diff --git a/packages/interop/test/fixtures/create-helia.ts b/packages/interop/test/fixtures/create-helia.ts new file mode 100644 index 00000000..0ba17dc7 --- /dev/null +++ b/packages/interop/test/fixtures/create-helia.ts @@ -0,0 +1,50 @@ +import { createHelia } from 'helia' +import { createLibp2p } from 'libp2p' +import { tcp } from '@libp2p/tcp' +import { noise } from '@chainsafe/libp2p-noise' +import { yamux } from '@chainsafe/libp2p-yamux' +import { MemoryBlockstore } from 'blockstore-core' +import { MemoryDatastore } from 'datastore-core' +import type { Helia } from '@helia/interface' + +export async function createHeliaNode (): Promise { + const blockstore = new MemoryBlockstore() + const datastore = new MemoryDatastore() + + const libp2p = await createLibp2p({ + addresses: { + listen: [ + '/ip4/0.0.0.0/tcp/0' + ] + }, + transports: [ + tcp() + ], + connectionEncryption: [ + noise() + ], + streamMuxers: [ + yamux() + ], + datastore, + identify: { + host: { + agentVersion: 'helia/0.0.0' + } + }, + nat: { + enabled: false + }, + relay: { + enabled: false + } + }) + + const helia = await createHelia({ + libp2p, + blockstore, + datastore + }) + + return helia +} diff --git a/packages/interop/test/fixtures/create-kubo.ts b/packages/interop/test/fixtures/create-kubo.ts new file mode 100644 index 00000000..29c01744 --- /dev/null +++ b/packages/interop/test/fixtures/create-kubo.ts @@ -0,0 +1,27 @@ + +// @ts-expect-error no types +import * as goIpfs from 'go-ipfs' +import { Controller, createController } from 'ipfsd-ctl' +import * as kuboRpcClient from 'kubo-rpc-client' +import { isNode } from 'wherearewe' + +export async function createKuboNode (): Promise { + const controller = await createController({ + kuboRpcModule: kuboRpcClient, + ipfsBin: isNode ? goIpfs.path() : undefined, + test: true, + endpoint: process.env.IPFSD_SERVER, + ipfsOptions: { + config: { + Addresses: { + Swarm: [ + '/ip4/0.0.0.0/tcp/4001', + '/ip4/0.0.0.0/tcp/4002/ws' + ] + } + } + } + }) + + return controller +} diff --git a/packages/interop/tsconfig.json b/packages/interop/tsconfig.json new file mode 100644 index 00000000..4eb21e77 --- /dev/null +++ b/packages/interop/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends": "aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "test" + ], + "references": [ + { + "path": "../helia" + }, + { + "path": "../interface" + } + ] +} diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json index ec418ad5..50b561b5 100644 --- a/packages/rpc-client/package.json +++ b/packages/rpc-client/package.json @@ -140,10 +140,12 @@ "dependencies": { "@helia/interface": "~0.0.0", "@helia/rpc-protocol": "~0.0.0", - "@libp2p/interfaces": "^3.3.1", "@libp2p/interface-libp2p": "^1.1.0", + "@libp2p/logger": "^2.0.5", "@libp2p/peer-id": "^2.0.0", "@multiformats/multiaddr": "^11.1.5", + "interface-blockstore": "^4.0.1", + "it-first": "^2.0.0", "it-pb-stream": "^2.0.3", "multiformats": "^11.0.1" }, diff --git a/packages/rpc-client/src/commands/authorization/get.ts b/packages/rpc-client/src/commands/authorization/get.ts index b156045c..f78fd3a2 100644 --- a/packages/rpc-client/src/commands/authorization/get.ts +++ b/packages/rpc-client/src/commands/authorization/get.ts @@ -1,44 +1,19 @@ -import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/authorization' -import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { HeliaRpcClientConfig } from '../../index.js' -import { pbStream } from 'it-pb-stream' +import { unaryCall } from '../utils/rpc-call.js' export function createAuthorizationGet (config: HeliaRpcClientConfig): (user: string, options?: any) => Promise { - const get = async (user: string, options = {}): Promise => { - const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) - - if (config.libp2p.peerId.publicKey == null || config.libp2p.peerId.privateKey == null) { - throw new Error('Public key component missing') - } - - const stream = pbStream(duplex) - stream.writePB({ - resource: '/authorization/get', - method: 'INVOKE', - options: GetOptions.encode({ - ...options - }) - }, RPCCallRequest) - stream.writePB({ - user - }, GetRequest) - const response = await stream.readPB(RPCCallResponse) - - duplex.close() - - if (response.type === RPCCallResponseType.message) { - if (response.message == null) { - throw new TypeError('RPC response had message type but no message') + return unaryCall({ + resource: '/authorization/get', + optionsCodec: GetOptions, + transformInput: (user) => { + return { + user } - - const message = GetResponse.decode(response.message) - - return message.authorization + }, + outputCodec: GetResponse, + transformOutput: (obj) => { + return obj.authorization } - - throw new RPCError(response) - } - - return get + })(config) } diff --git a/packages/rpc-client/src/commands/blockstore/batch.ts b/packages/rpc-client/src/commands/blockstore/batch.ts new file mode 100644 index 00000000..b7a753e8 --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/batch.ts @@ -0,0 +1,84 @@ +import { BatchOptions, BatchRequest, BatchRequestDelete, BatchRequestPut, BatchRequestType } from '@helia/rpc-protocol/blockstore' +import type { Helia } from '@helia/interface' +import type { HeliaRpcMethodConfig } from '../../index.js' +import type { CID } from 'multiformats/cid' +import { RPCCallMessage, RPCCallRequest, RPCCallMessageType } from '@helia/rpc-protocol/rpc' +import { HELIA_RPC_PROTOCOL } from '@helia/rpc-protocol' +import { pbStream } from 'it-pb-stream' +import type { Pair, Batch } from 'interface-blockstore' + +export function createBlockstoreBatch (config: HeliaRpcMethodConfig): Helia['blockstore']['batch'] { + const batch = (): Batch => { + let puts: Pair[] = [] + let dels: CID[] = [] + + const batch: Batch = { + put (key, value) { + puts.push({ key, value }) + }, + + delete (key) { + dels.push(key) + }, + + commit: async (options) => { + const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) + + try { + const stream = pbStream(duplex) + + stream.writePB({ + resource: '/blockstore/batch', + method: 'INVOKE', + authorization: config.authorization, + options: BatchOptions.encode({ + ...options + }) + }, RPCCallRequest) + + for (const { key, value } of puts) { + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message: BatchRequest.encode({ + type: BatchRequestType.BATCH_REQUEST_PUT, + message: BatchRequestPut.encode({ + cid: key.bytes, + block: value + }) + }) + }, RPCCallMessage) + } + + puts = [] + + for (const cid of dels) { + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message: BatchRequest.encode({ + type: BatchRequestType.BATCH_REQUEST_DELETE, + message: BatchRequestDelete.encode({ + cid: cid.bytes + }) + }) + }, RPCCallMessage) + } + + dels = [] + + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message: BatchRequest.encode({ + type: BatchRequestType.BATCH_REQUEST_COMMIT + }) + }, RPCCallMessage) + } finally { + duplex.close() + } + } + } + + return batch + } + + return batch +} diff --git a/packages/rpc-client/src/commands/blockstore/close.ts b/packages/rpc-client/src/commands/blockstore/close.ts new file mode 100644 index 00000000..eea558ef --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/close.ts @@ -0,0 +1,11 @@ +import type { Helia } from '@helia/interface' +import type { HeliaRpcMethodConfig } from '../../index.js' +import { unaryCall } from '../utils/rpc-call.js' +import { CloseOptions } from '@helia/rpc-protocol/blockstore' + +export function createBlockstoreClose (config: HeliaRpcMethodConfig): Helia['blockstore']['close'] { + return unaryCall({ + resource: '/blockstore/close', + optionsCodec: CloseOptions + })(config) +} diff --git a/packages/rpc-client/src/commands/blockstore/delete-many.ts b/packages/rpc-client/src/commands/blockstore/delete-many.ts new file mode 100644 index 00000000..f7901855 --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/delete-many.ts @@ -0,0 +1,22 @@ +import { DeleteManyOptions, DeleteManyRequest, DeleteManyResponse } from '@helia/rpc-protocol/blockstore' +import type { Helia } from '@helia/interface' +import type { HeliaRpcMethodConfig } from '../../index.js' +import { CID } from 'multiformats/cid' +import { streamingCall } from '../utils/rpc-call.js' + +export function createBlockstoreDeleteMany (config: HeliaRpcMethodConfig): Helia['blockstore']['deleteMany'] { + return streamingCall({ + resource: '/blockstore/delete-many', + optionsCodec: DeleteManyOptions, + transformInput: (cid: CID) => { + return { + cid: cid.bytes + } + }, + inputCodec: DeleteManyRequest, + outputCodec: DeleteManyResponse, + transformOutput: (obj: DeleteManyResponse) => { + return CID.decode(obj.cid) + } + })(config) +} diff --git a/packages/rpc-client/src/commands/blockstore/delete.ts b/packages/rpc-client/src/commands/blockstore/delete.ts index a175c5ce..a1316fb8 100644 --- a/packages/rpc-client/src/commands/blockstore/delete.ts +++ b/packages/rpc-client/src/commands/blockstore/delete.ts @@ -1,37 +1,18 @@ -import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' -import { DeleteOptions, DeleteRequest } from '@helia/rpc-protocol/blockstore' -import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' import type { HeliaRpcMethodConfig } from '../../index.js' -import { pbStream } from 'it-pb-stream' import type { CID } from 'multiformats/cid' +import { unaryCall } from '../utils/rpc-call.js' +import { DeleteOptions, DeleteRequest } from '@helia/rpc-protocol/blockstore' export function createBlockstoreDelete (config: HeliaRpcMethodConfig): Helia['blockstore']['delete'] { - const del: Helia['blockstore']['delete'] = async (cid: CID, options = {}) => { - const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) - - const stream = pbStream(duplex) - stream.writePB({ - resource: '/blockstore/delete', - method: 'INVOKE', - authorization: config.authorization, - options: DeleteOptions.encode({ - ...options - }) - }, RPCCallRequest) - stream.writePB({ - cid: cid.bytes - }, DeleteRequest) - const response = await stream.readPB(RPCCallResponse) - - duplex.close() - - if (response.type === RPCCallResponseType.message) { - return - } - - throw new RPCError(response) - } - - return del + return unaryCall({ + resource: '/blockstore/delete', + optionsCodec: DeleteOptions, + transformInput: (cid: CID) => { + return { + cid: cid.bytes + } + }, + inputCodec: DeleteRequest + })(config) } diff --git a/packages/rpc-client/src/commands/blockstore/get-many.ts b/packages/rpc-client/src/commands/blockstore/get-many.ts new file mode 100644 index 00000000..b17659c6 --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/get-many.ts @@ -0,0 +1,22 @@ +import { GetManyOptions, GetManyRequest, GetManyResponse } from '@helia/rpc-protocol/blockstore' +import type { Helia } from '@helia/interface' +import type { HeliaRpcMethodConfig } from '../../index.js' +import type { CID } from 'multiformats/cid' +import { streamingCall } from '../utils/rpc-call.js' + +export function createBlockstoreGetMany (config: HeliaRpcMethodConfig): Helia['blockstore']['getMany'] { + return streamingCall({ + resource: '/blockstore/get-many', + optionsCodec: GetManyOptions, + transformInput: (cid: CID) => { + return { + cid: cid.bytes + } + }, + inputCodec: GetManyRequest, + outputCodec: GetManyResponse, + transformOutput: (obj) => { + return obj.block + } + })(config) +} diff --git a/packages/rpc-client/src/commands/blockstore/get.ts b/packages/rpc-client/src/commands/blockstore/get.ts index c268a919..94141f2e 100644 --- a/packages/rpc-client/src/commands/blockstore/get.ts +++ b/packages/rpc-client/src/commands/blockstore/get.ts @@ -1,71 +1,22 @@ -/* eslint max-depth: ["error", 5] */ - -import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' -import { GetOptions, GetRequest, GetResponse, GetResponseType } from '@helia/rpc-protocol/blockstore' -import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' +import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' import type { Helia } from '@helia/interface' import type { HeliaRpcMethodConfig } from '../../index.js' -import { pbStream } from 'it-pb-stream' import type { CID } from 'multiformats/cid' -import { CustomEvent } from '@libp2p/interfaces/events' +import { unaryCall } from '../utils/rpc-call.js' export function createBlockstoreGet (config: HeliaRpcMethodConfig): Helia['blockstore']['get'] { - const get: Helia['blockstore']['get'] = async (cid: CID, options = {}) => { - const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) - - const stream = pbStream(duplex) - stream.writePB({ - resource: '/blockstore/get', - method: 'INVOKE', - authorization: config.authorization, - options: GetOptions.encode({ - ...options - }) - }, RPCCallRequest) - stream.writePB({ - cid: cid.bytes - }, GetRequest) - - try { - while (true) { - const response = await stream.readPB(RPCCallResponse) - - if (response.type === RPCCallResponseType.error) { - throw new RPCError(response) - } - - if (response.type === RPCCallResponseType.message) { - if (response.message == null) { - throw new TypeError('RPC response had message type but no message') - } - - const message = GetResponse.decode(response.message) - - if (message.type === GetResponseType.PROGRESS) { - if (message.progressEventType == null) { - throw new TypeError('GetResponse progress message missing event type') - } - - // @ts-expect-error not in interface - if (options.progress != null) { - const event = new CustomEvent(message.progressEventType) - - // @ts-expect-error not in interface - options.progress(event) - } - } else if (message.type === GetResponseType.RESULT) { - if (message.block == null) { - throw new TypeError('GetResponse result message missing block') - } - - return message.block - } - } + return unaryCall({ + resource: '/blockstore/get', + optionsCodec: GetOptions, + transformInput: (cid: CID) => { + return { + cid: cid.bytes } - } finally { - duplex.close() + }, + inputCodec: GetRequest, + outputCodec: GetResponse, + transformOutput: (obj: GetResponse): Uint8Array => { + return obj.block } - } - - return get + })(config) } diff --git a/packages/rpc-client/src/commands/blockstore/has.ts b/packages/rpc-client/src/commands/blockstore/has.ts index 8a29e178..9f660360 100644 --- a/packages/rpc-client/src/commands/blockstore/has.ts +++ b/packages/rpc-client/src/commands/blockstore/has.ts @@ -1,43 +1,22 @@ -import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import { HasOptions, HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' -import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' import type { HeliaRpcMethodConfig } from '../../index.js' -import { pbStream } from 'it-pb-stream' import type { CID } from 'multiformats/cid' +import { unaryCall } from '../utils/rpc-call.js' export function createBlockstoreHas (config: HeliaRpcMethodConfig): Helia['blockstore']['has'] { - const has: Helia['blockstore']['has'] = async (cid: CID, options = {}) => { - const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) - - const stream = pbStream(duplex) - stream.writePB({ - resource: '/blockstore/has', - method: 'INVOKE', - authorization: config.authorization, - options: HasOptions.encode({ - ...options - }) - }, RPCCallRequest) - stream.writePB({ - cid: cid.bytes - }, HasRequest) - const response = await stream.readPB(RPCCallResponse) - - duplex.close() - - if (response.type === RPCCallResponseType.message) { - if (response.message == null) { - throw new TypeError('RPC response had message type but no message') + return unaryCall({ + resource: '/blockstore/has', + optionsCodec: HasOptions, + transformInput: (cid: CID) => { + return { + cid: cid.bytes } - - const message = HasResponse.decode(response.message) - - return message.has + }, + inputCodec: HasRequest, + outputCodec: HasResponse, + transformOutput: (obj) => { + return obj.has } - - throw new RPCError(response) - } - - return has + })(config) } diff --git a/packages/rpc-client/src/commands/blockstore/open.ts b/packages/rpc-client/src/commands/blockstore/open.ts new file mode 100644 index 00000000..a9473035 --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/open.ts @@ -0,0 +1,11 @@ +import type { Helia } from '@helia/interface' +import type { HeliaRpcMethodConfig } from '../../index.js' +import { unaryCall } from '../utils/rpc-call.js' +import { OpenOptions } from '@helia/rpc-protocol/blockstore' + +export function createBlockstoreOpen (config: HeliaRpcMethodConfig): Helia['blockstore']['open'] { + return unaryCall({ + resource: '/blockstore/open', + optionsCodec: OpenOptions + })(config) +} diff --git a/packages/rpc-client/src/commands/blockstore/put-many.ts b/packages/rpc-client/src/commands/blockstore/put-many.ts new file mode 100644 index 00000000..9029c310 --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/put-many.ts @@ -0,0 +1,23 @@ +import { PutManyOptions, PutManyRequest, PutManyResponse } from '@helia/rpc-protocol/blockstore' +import type { Helia } from '@helia/interface' +import type { HeliaRpcMethodConfig } from '../../index.js' +import { streamingCall } from '../utils/rpc-call.js' +import type { Pair } from 'interface-blockstore' + +export function createBlockstorePutMany (config: HeliaRpcMethodConfig): Helia['blockstore']['putMany'] { + return streamingCall({ + resource: '/blockstore/put-many', + optionsCodec: PutManyOptions, + transformInput: (pair: Pair) => { + return { + cid: pair.key.bytes, + block: pair.value + } + }, + inputCodec: PutManyRequest, + outputCodec: PutManyResponse, + transformOutput: (obj): Uint8Array => { + return obj.block + } + })(config) +} diff --git a/packages/rpc-client/src/commands/blockstore/put.ts b/packages/rpc-client/src/commands/blockstore/put.ts index cffc23f8..704912ab 100644 --- a/packages/rpc-client/src/commands/blockstore/put.ts +++ b/packages/rpc-client/src/commands/blockstore/put.ts @@ -1,38 +1,19 @@ -import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import { PutOptions, PutRequest } from '@helia/rpc-protocol/blockstore' -import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' import type { HeliaRpcMethodConfig } from '../../index.js' -import { pbStream } from 'it-pb-stream' -import type { CID } from 'multiformats/cid' +import type { Pair } from 'interface-blockstore' +import { unaryCall } from '../utils/rpc-call.js' export function createBlockstorePut (config: HeliaRpcMethodConfig): Helia['blockstore']['put'] { - const put: Helia['blockstore']['put'] = async (cid: CID, block: Uint8Array, options = {}) => { - const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) - - const stream = pbStream(duplex) - stream.writePB({ - resource: '/blockstore/put', - method: 'INVOKE', - authorization: config.authorization, - options: PutOptions.encode({ - ...options - }) - }, RPCCallRequest) - stream.writePB({ - cid: cid.bytes, - block - }, PutRequest) - const response = await stream.readPB(RPCCallResponse) - - duplex.close() - - if (response.type === RPCCallResponseType.message) { - return - } - - throw new RPCError(response) - } - - return put + return unaryCall({ + resource: '/blockstore/put', + optionsCodec: PutOptions, + transformInput: (pair: Pair): PutRequest => { + return { + cid: pair.key.bytes, + block: pair.value + } + }, + inputCodec: PutRequest + })(config) } diff --git a/packages/rpc-client/src/commands/blockstore/query-keys.ts b/packages/rpc-client/src/commands/blockstore/query-keys.ts new file mode 100644 index 00000000..1ed75c5f --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/query-keys.ts @@ -0,0 +1,16 @@ +import type { Helia } from '@helia/interface' +import type { HeliaRpcMethodConfig } from '../../index.js' +import { CID } from 'multiformats/cid' +import { QueryKeysOptions, QueryKeysRequest, QueryKeysResponse } from '@helia/rpc-protocol/blockstore' +import { streamingCall } from '../utils/rpc-call.js' + +export function createBlockstoreQueryKeys (config: HeliaRpcMethodConfig): Helia['blockstore']['queryKeys'] { + return streamingCall({ + resource: '/blockstore/query-keys', + optionsCodec: QueryKeysOptions, + outputCodec: QueryKeysResponse, + transformOutput: (obj: QueryKeysResponse) => { + return CID.decode(obj.key) + } + })(config) +} diff --git a/packages/rpc-client/src/commands/blockstore/query.ts b/packages/rpc-client/src/commands/blockstore/query.ts new file mode 100644 index 00000000..43277a43 --- /dev/null +++ b/packages/rpc-client/src/commands/blockstore/query.ts @@ -0,0 +1,19 @@ +import type { Helia } from '@helia/interface' +import type { HeliaRpcMethodConfig } from '../../index.js' +import { CID } from 'multiformats/cid' +import { QueryOptions, QueryRequest, QueryResponse } from '@helia/rpc-protocol/blockstore' +import { streamingCall } from '../utils/rpc-call.js' + +export function createBlockstoreQuery (config: HeliaRpcMethodConfig): Helia['blockstore']['query'] { + return streamingCall({ + resource: '/blockstore/query', + optionsCodec: QueryOptions, + outputCodec: QueryResponse, + transformOutput: (obj: QueryResponse) => { + return { + key: CID.decode(obj.key), + value: obj.value + } + } + })(config) +} diff --git a/packages/rpc-client/src/commands/info.ts b/packages/rpc-client/src/commands/info.ts index 379d690d..54bb0e11 100644 --- a/packages/rpc-client/src/commands/info.ts +++ b/packages/rpc-client/src/commands/info.ts @@ -1,45 +1,27 @@ import { multiaddr } from '@multiformats/multiaddr' -import { RPCCallRequest, RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' import { InfoOptions, InfoResponse } from '@helia/rpc-protocol/root' -import { HELIA_RPC_PROTOCOL, RPCError } from '@helia/rpc-protocol' import type { Helia } from '@helia/interface' import { peerIdFromString } from '@libp2p/peer-id' import type { HeliaRpcMethodConfig } from '../index.js' -import { pbStream } from 'it-pb-stream' +import { unaryCall } from './utils/rpc-call.js' export function createInfo (config: HeliaRpcMethodConfig): Helia['info'] { - const info: Helia['info'] = async (options = {}) => { - const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) - - const stream = pbStream(duplex) - stream.writePB({ - resource: '/info', - method: 'INVOKE', - options: InfoOptions.encode({ - ...options, - peerId: options.peerId != null ? options.peerId.toString() : undefined - }) - }, RPCCallRequest) - const response = await stream.readPB(RPCCallResponse) - - duplex.close() - - if (response.type === RPCCallResponseType.message) { - if (response.message == null) { - throw new TypeError('RPC response had message type but no message') + return unaryCall({ + resource: '/info', + optionsCodec: InfoOptions, + transformOptions: (obj) => { + return { + ...obj, + peerId: obj.peerId != null ? obj.peerId.toString() : undefined } - - const infoResponse = InfoResponse.decode(response.message) - + }, + outputCodec: InfoResponse, + transformOutput: (obj) => { return { - ...infoResponse, - peerId: peerIdFromString(infoResponse.peerId), - multiaddrs: infoResponse.multiaddrs.map(str => multiaddr(str)) + ...obj, + peerId: peerIdFromString(obj.peerId), + multiaddrs: obj.multiaddrs.map(str => multiaddr(str)) } } - - throw new RPCError(response) - } - - return info + })(config) } diff --git a/packages/rpc-client/src/commands/utils/rpc-call.ts b/packages/rpc-client/src/commands/utils/rpc-call.ts new file mode 100644 index 00000000..5082d785 --- /dev/null +++ b/packages/rpc-client/src/commands/utils/rpc-call.ts @@ -0,0 +1,112 @@ +/* eslint max-depth: ["error", 5] */ + +import { RPCCallMessage, RPCCallRequest, RPCCallMessageType } from '@helia/rpc-protocol/rpc' +import { HELIA_RPC_PROTOCOL, RPCError, RPCProgressEvent } from '@helia/rpc-protocol' +import type { HeliaRpcMethodConfig } from '../../index.js' +import { pbStream } from 'it-pb-stream' +import first from 'it-first' +import { logger } from '@libp2p/logger' + +const log = logger('helia:rpc-client:utils:rpc-call') + +export interface Codec { + encode: (type: T) => Uint8Array + decode: (buf: Uint8Array) => T +} + +export interface CallOptions { + resource: string + optionsCodec: Codec + transformOptions?: (obj: any) => Options + transformInput?: (obj: any) => Request + inputCodec?: Codec + outputCodec?: Codec + transformOutput?: (obj: Response) => any +} + +export function streamingCall (opts: CallOptions): (config: HeliaRpcMethodConfig) => any { + return function createStreamingCall (config: HeliaRpcMethodConfig): any { + const streamingCall: any = async function * (source: any, options: any = {}) { + const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) + const stream = pbStream(duplex) + + stream.writePB({ + resource: opts.resource, + method: 'INVOKE', + authorization: config.authorization, + options: opts.optionsCodec.encode(opts.transformOptions == null ? options : opts.transformOptions(options)) + }, RPCCallRequest) + + void Promise.resolve().then(async () => { + for await (const input of source) { + let message: Uint8Array | undefined + + if (opts.inputCodec != null) { + message = opts.inputCodec.encode(opts.transformInput == null ? input : opts.transformInput(input)) + } + + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message + }, RPCCallMessage) + } + + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_DONE + }, RPCCallMessage) + }) + .catch(err => { + log('error encountered while sending RPC messages', err) + }) + .finally(() => { + duplex.closeWrite() + }) + + try { + while (true) { + const response = await stream.readPB(RPCCallMessage) + + switch (response.type) { + case RPCCallMessageType.RPC_CALL_DONE: + return + case RPCCallMessageType.RPC_CALL_ERROR: + throw new RPCError(response.message) + case RPCCallMessageType.RPC_CALL_MESSAGE: + if (opts.outputCodec != null) { + let message = opts.outputCodec.decode(response.message) + + if (opts.transformOutput != null) { + message = opts.transformOutput(message) + } + + yield message + } + continue + case RPCCallMessageType.RPC_CALL_PROGRESS: + if (options.progress != null) { + options.progress(new RPCProgressEvent(response.message)) + } + continue + default: + throw new Error('Unknown RPCCallMessageType') + } + } + } finally { + duplex.closeRead() + } + } + + return streamingCall + } +} + +export function unaryCall (opts: CallOptions): (config: HeliaRpcMethodConfig) => any { + return function createStreamingCall (config: HeliaRpcMethodConfig): any { + const unaryCall: any = async function (arg: any, options: any = {}): Promise { + const fn: any = streamingCall(opts)(config) + return await first(fn([arg], options)) + } + + return unaryCall + } +} diff --git a/packages/rpc-client/src/index.ts b/packages/rpc-client/src/index.ts index 25d551f8..8feb3228 100644 --- a/packages/rpc-client/src/index.ts +++ b/packages/rpc-client/src/index.ts @@ -7,6 +7,14 @@ import { createBlockstoreGet } from './commands/blockstore/get.js' import { createBlockstoreHas } from './commands/blockstore/has.js' import { createBlockstorePut } from './commands/blockstore/put.js' import { createAuthorizationGet } from './commands/authorization/get.js' +import { createBlockstoreDeleteMany } from './commands/blockstore/delete-many.js' +import { createBlockstoreGetMany } from './commands/blockstore/get-many.js' +import { createBlockstorePutMany } from './commands/blockstore/put-many.js' +import { createBlockstoreClose } from './commands/blockstore/close.js' +import { createBlockstoreOpen } from './commands/blockstore/open.js' +import { createBlockstoreBatch } from './commands/blockstore/batch.js' +import { createBlockstoreQueryKeys } from './commands/blockstore/query-keys.js' +import { createBlockstoreQuery } from './commands/blockstore/query.js' export interface HeliaRpcClientConfig { multiaddr: Multiaddr @@ -32,12 +40,30 @@ export async function createHeliaRpcClient (config: HeliaRpcClientConfig): Promi return { info: createInfo(methodConfig), - // @ts-expect-error incomplete implementation blockstore: { + batch: createBlockstoreBatch(methodConfig), + close: createBlockstoreClose(methodConfig), + deleteMany: createBlockstoreDeleteMany(methodConfig), delete: createBlockstoreDelete(methodConfig), + getMany: createBlockstoreGetMany(methodConfig), get: createBlockstoreGet(methodConfig), has: createBlockstoreHas(methodConfig), - put: createBlockstorePut(methodConfig) + open: createBlockstoreOpen(methodConfig), + putMany: createBlockstorePutMany(methodConfig), + put: createBlockstorePut(methodConfig), + queryKeys: createBlockstoreQueryKeys(methodConfig), + query: createBlockstoreQuery(methodConfig) + }, + // @ts-expect-error incomplete implementation + datastore: { + + }, + // @ts-expect-error incomplete implementation + libp2p: { + + }, + async stop () { + throw new Error('Not implemented') } } } diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json index 62d60f3c..b3bb9a64 100644 --- a/packages/rpc-protocol/package.json +++ b/packages/rpc-protocol/package.json @@ -164,12 +164,12 @@ "generate": "protons src/*.proto" }, "dependencies": { - "protons-runtime": "^4.0.1", + "protons-runtime": "^5.0.0", "uint8arraylist": "^2.4.3" }, "devDependencies": { "aegir": "^38.1.0", - "protons": "^6.0.1" + "protons": "^7.0.0" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/rpc-protocol/src/authorization.ts b/packages/rpc-protocol/src/authorization.ts index 5ab5a721..2f839d6b 100644 --- a/packages/rpc-protocol/src/authorization.ts +++ b/packages/rpc-protocol/src/authorization.ts @@ -5,8 +5,8 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' export interface GetOptions {} @@ -45,7 +45,7 @@ export namespace GetOptions { return _codec } - export const encode = (obj: GetOptions): Uint8Array => { + export const encode = (obj: Partial): Uint8Array => { return encodeMessage(obj, GetOptions.codec()) } @@ -68,9 +68,9 @@ export namespace GetRequest { w.fork() } - if (opts.writeDefaults === true || obj.user !== '') { + if (opts.writeDefaults === true || (obj.user != null && obj.user !== '')) { w.uint32(10) - w.string(obj.user) + w.string(obj.user ?? '') } if (opts.lengthDelimited !== false) { @@ -103,7 +103,7 @@ export namespace GetRequest { return _codec } - export const encode = (obj: GetRequest): Uint8Array => { + export const encode = (obj: Partial): Uint8Array => { return encodeMessage(obj, GetRequest.codec()) } @@ -126,9 +126,9 @@ export namespace GetResponse { w.fork() } - if (opts.writeDefaults === true || obj.authorization !== '') { + if (opts.writeDefaults === true || (obj.authorization != null && obj.authorization !== '')) { w.uint32(10) - w.string(obj.authorization) + w.string(obj.authorization ?? '') } if (opts.lengthDelimited !== false) { @@ -161,7 +161,7 @@ export namespace GetResponse { return _codec } - export const encode = (obj: GetResponse): Uint8Array => { + export const encode = (obj: Partial): Uint8Array => { return encodeMessage(obj, GetResponse.codec()) } diff --git a/packages/rpc-protocol/src/blockstore.proto b/packages/rpc-protocol/src/blockstore.proto index fe3a72dd..0ed107b3 100644 --- a/packages/rpc-protocol/src/blockstore.proto +++ b/packages/rpc-protocol/src/blockstore.proto @@ -1,5 +1,34 @@ syntax = "proto3"; +message Pair { + bytes cid = 1; + bytes block = 2; +} + +message OpenOptions { + +} + +message OpenRequest { + +} + +message OpenResponse { + +} + +message CloseOptions { + +} + +message CloseRequest { + +} + +message CloseResponse { + +} + message PutOptions { } @@ -21,16 +50,8 @@ message GetRequest { bytes cid = 1; } -enum GetResponseType { - PROGRESS = 0; - RESULT = 1; -} - message GetResponse { - GetResponseType type = 1; - optional bytes block = 2; - optional string progress_event_type = 3; - map progress_event_data = 4; + bytes block = 2; } message HasOptions { @@ -56,3 +77,94 @@ message DeleteRequest { message DeleteResponse { } + +message PutManyOptions { + +} + +message PutManyRequest { + bytes cid = 1; + bytes block = 2; +} + +message PutManyResponse { + bytes cid = 1; + bytes block = 2; +} + +message GetManyOptions { + +} + +message GetManyRequest { + bytes cid = 1; +} + +message GetManyResponse { + bytes block = 1; +} + +message DeleteManyOptions { + +} + +message DeleteManyRequest { + bytes cid = 1; +} + +message DeleteManyResponse { + bytes cid = 1; +} + +message BatchOptions { + +} + +enum BatchRequestType { + BATCH_REQUEST_PUT = 0; + BATCH_REQUEST_DELETE = 1; + BATCH_REQUEST_COMMIT = 2; +} + +message BatchRequest { + BatchRequestType type = 1; + bytes message = 2; +} + +message BatchRequestPut { + bytes cid = 1; + bytes block = 2; +} + +message BatchRequestDelete { + bytes cid = 1; +} + +message BatchResponse { + +} + +message QueryOptions { + +} + +message QueryRequest { + +} + +message QueryResponse { + bytes key = 1; + bytes value = 2; +} + +message QueryKeysOptions { + +} + +message QueryKeysRequest { + +} + +message QueryKeysResponse { + bytes key = 1; +} diff --git a/packages/rpc-protocol/src/blockstore.ts b/packages/rpc-protocol/src/blockstore.ts index 928e1fca..7b27753b 100644 --- a/packages/rpc-protocol/src/blockstore.ts +++ b/packages/rpc-protocol/src/blockstore.ts @@ -5,17 +5,315 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' -export interface PutOptions {} +export interface Pair { + cid: Uint8Array + block: Uint8Array +} -export namespace PutOptions { - let _codec: Codec +export namespace Pair { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.block ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0), + block: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + case 2: + obj.block = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, Pair.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): Pair => { + return decodeMessage(buf, Pair.codec()) + } +} + +export interface OpenOptions {} + +export namespace OpenOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, OpenOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): OpenOptions => { + return decodeMessage(buf, OpenOptions.codec()) + } +} + +export interface OpenRequest {} + +export namespace OpenRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, OpenRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): OpenRequest => { + return decodeMessage(buf, OpenRequest.codec()) + } +} + +export interface OpenResponse {} + +export namespace OpenResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, OpenResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): OpenResponse => { + return decodeMessage(buf, OpenResponse.codec()) + } +} + +export interface CloseOptions {} + +export namespace CloseOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, CloseOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): CloseOptions => { + return decodeMessage(buf, CloseOptions.codec()) + } +} + +export interface CloseRequest {} + +export namespace CloseRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, CloseRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): CloseRequest => { + return decodeMessage(buf, CloseRequest.codec()) + } +} + +export interface CloseResponse {} + +export namespace CloseResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } @@ -23,8 +321,1087 @@ export namespace PutOptions { if (opts.lengthDelimited !== false) { w.ldelim() } - }, (reader, length) => { - const obj: any = {} + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, CloseResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): CloseResponse => { + return decodeMessage(buf, CloseResponse.codec()) + } +} + +export interface PutOptions {} + +export namespace PutOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutOptions => { + return decodeMessage(buf, PutOptions.codec()) + } +} + +export interface PutRequest { + cid: Uint8Array + block: Uint8Array +} + +export namespace PutRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.block ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0), + block: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + case 2: + obj.block = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutRequest => { + return decodeMessage(buf, PutRequest.codec()) + } +} + +export interface PutResponse {} + +export namespace PutResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutResponse => { + return decodeMessage(buf, PutResponse.codec()) + } +} + +export interface GetOptions {} + +export namespace GetOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetOptions => { + return decodeMessage(buf, GetOptions.codec()) + } +} + +export interface GetRequest { + cid: Uint8Array +} + +export namespace GetRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetRequest => { + return decodeMessage(buf, GetRequest.codec()) + } +} + +export interface GetResponse { + block: Uint8Array +} + +export namespace GetResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.block ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + block: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 2: + obj.block = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse => { + return decodeMessage(buf, GetResponse.codec()) + } +} + +export interface HasOptions {} + +export namespace HasOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, HasOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasOptions => { + return decodeMessage(buf, HasOptions.codec()) + } +} + +export interface HasRequest { + cid: Uint8Array +} + +export namespace HasRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, HasRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasRequest => { + return decodeMessage(buf, HasRequest.codec()) + } +} + +export interface HasResponse { + has: boolean +} + +export namespace HasResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.has != null && obj.has !== false)) { + w.uint32(8) + w.bool(obj.has ?? false) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + has: false + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.has = reader.bool() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, HasResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasResponse => { + return decodeMessage(buf, HasResponse.codec()) + } +} + +export interface DeleteOptions {} + +export namespace DeleteOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteOptions => { + return decodeMessage(buf, DeleteOptions.codec()) + } +} + +export interface DeleteRequest { + cid: Uint8Array +} + +export namespace DeleteRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteRequest => { + return decodeMessage(buf, DeleteRequest.codec()) + } +} + +export interface DeleteResponse {} + +export namespace DeleteResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteResponse => { + return decodeMessage(buf, DeleteResponse.codec()) + } +} + +export interface PutManyOptions {} + +export namespace PutManyOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutManyOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyOptions => { + return decodeMessage(buf, PutManyOptions.codec()) + } +} + +export interface PutManyRequest { + cid: Uint8Array + block: Uint8Array +} + +export namespace PutManyRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.block ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0), + block: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + case 2: + obj.block = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutManyRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyRequest => { + return decodeMessage(buf, PutManyRequest.codec()) + } +} + +export interface PutManyResponse { + cid: Uint8Array + block: Uint8Array +} + +export namespace PutManyResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.block ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0), + block: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + case 2: + obj.block = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutManyResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyResponse => { + return decodeMessage(buf, PutManyResponse.codec()) + } +} + +export interface GetManyOptions {} + +export namespace GetManyOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetManyOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyOptions => { + return decodeMessage(buf, GetManyOptions.codec()) + } +} + +export interface GetManyRequest { + cid: Uint8Array +} + +export namespace GetManyRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetManyRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyRequest => { + return decodeMessage(buf, GetManyRequest.codec()) + } +} + +export interface GetManyResponse { + block: Uint8Array +} + +export namespace GetManyResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.block ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + block: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.block = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetManyResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyResponse => { + return decodeMessage(buf, GetManyResponse.codec()) + } +} + +export interface DeleteManyOptions {} + +export namespace DeleteManyOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteManyOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyOptions => { + return decodeMessage(buf, DeleteManyOptions.codec()) + } +} + +export interface DeleteManyRequest { + cid: Uint8Array +} + +export namespace DeleteManyRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } const end = length == null ? reader.len : reader.pos + length @@ -32,6 +1409,9 @@ export namespace PutOptions { const tag = reader.uint32() switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break default: reader.skipType(tag & 7) break @@ -45,38 +1425,32 @@ export namespace PutOptions { return _codec } - export const encode = (obj: PutOptions): Uint8Array => { - return encodeMessage(obj, PutOptions.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteManyRequest.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): PutOptions => { - return decodeMessage(buf, PutOptions.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyRequest => { + return decodeMessage(buf, DeleteManyRequest.codec()) } } -export interface PutRequest { +export interface DeleteManyResponse { cid: Uint8Array - block: Uint8Array } -export namespace PutRequest { - let _codec: Codec +export namespace DeleteManyResponse { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { w.uint32(10) - w.bytes(obj.cid) - } - - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.block) + w.bytes(obj.cid ?? new Uint8Array(0)) } if (opts.lengthDelimited !== false) { @@ -84,8 +1458,7 @@ export namespace PutRequest { } }, (reader, length) => { const obj: any = { - cid: new Uint8Array(0), - block: new Uint8Array(0) + cid: new Uint8Array(0) } const end = length == null ? reader.len : reader.pos + length @@ -97,9 +1470,6 @@ export namespace PutRequest { case 1: obj.cid = reader.bytes() break - case 2: - obj.block = reader.bytes() - break default: reader.skipType(tag & 7) break @@ -113,23 +1483,23 @@ export namespace PutRequest { return _codec } - export const encode = (obj: PutRequest): Uint8Array => { - return encodeMessage(obj, PutRequest.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteManyResponse.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): PutRequest => { - return decodeMessage(buf, PutRequest.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyResponse => { + return decodeMessage(buf, DeleteManyResponse.codec()) } } -export interface PutResponse {} +export interface BatchOptions {} -export namespace PutResponse { - let _codec: Codec +export namespace BatchOptions { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } @@ -159,32 +1529,65 @@ export namespace PutResponse { return _codec } - export const encode = (obj: PutResponse): Uint8Array => { - return encodeMessage(obj, PutResponse.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, BatchOptions.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): PutResponse => { - return decodeMessage(buf, PutResponse.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): BatchOptions => { + return decodeMessage(buf, BatchOptions.codec()) } } -export interface GetOptions {} +export enum BatchRequestType { + BATCH_REQUEST_PUT = 'BATCH_REQUEST_PUT', + BATCH_REQUEST_DELETE = 'BATCH_REQUEST_DELETE', + BATCH_REQUEST_COMMIT = 'BATCH_REQUEST_COMMIT' +} -export namespace GetOptions { - let _codec: Codec +enum __BatchRequestTypeValues { + BATCH_REQUEST_PUT = 0, + BATCH_REQUEST_DELETE = 1, + BATCH_REQUEST_COMMIT = 2 +} - export const codec = (): Codec => { +export namespace BatchRequestType { + export const codec = (): Codec => { + return enumeration(__BatchRequestTypeValues) + } +} +export interface BatchRequest { + type: BatchRequestType + message: Uint8Array +} + +export namespace BatchRequest { + let _codec: Codec + + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } + if (opts.writeDefaults === true || (obj.type != null && __BatchRequestTypeValues[obj.type] !== 0)) { + w.uint32(8) + BatchRequestType.codec().encode(obj.type ?? BatchRequestType.BATCH_REQUEST_PUT, w) + } + + if (opts.writeDefaults === true || (obj.message != null && obj.message.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.message ?? new Uint8Array(0)) + } + if (opts.lengthDelimited !== false) { w.ldelim() } }, (reader, length) => { - const obj: any = {} + const obj: any = { + type: BatchRequestType.BATCH_REQUEST_PUT, + message: new Uint8Array(0) + } const end = length == null ? reader.len : reader.pos + length @@ -192,6 +1595,12 @@ export namespace GetOptions { const tag = reader.uint32() switch (tag >>> 3) { + case 1: + obj.type = BatchRequestType.codec().decode(reader) + break + case 2: + obj.message = reader.bytes() + break default: reader.skipType(tag & 7) break @@ -205,32 +1614,38 @@ export namespace GetOptions { return _codec } - export const encode = (obj: GetOptions): Uint8Array => { - return encodeMessage(obj, GetOptions.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, BatchRequest.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): GetOptions => { - return decodeMessage(buf, GetOptions.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): BatchRequest => { + return decodeMessage(buf, BatchRequest.codec()) } } -export interface GetRequest { +export interface BatchRequestPut { cid: Uint8Array + block: Uint8Array } -export namespace GetRequest { - let _codec: Codec +export namespace BatchRequestPut { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { w.uint32(10) - w.bytes(obj.cid) + w.bytes(obj.cid ?? new Uint8Array(0)) + } + + if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.block ?? new Uint8Array(0)) } if (opts.lengthDelimited !== false) { @@ -238,7 +1653,8 @@ export namespace GetRequest { } }, (reader, length) => { const obj: any = { - cid: new Uint8Array(0) + cid: new Uint8Array(0), + block: new Uint8Array(0) } const end = length == null ? reader.len : reader.pos + length @@ -250,6 +1666,9 @@ export namespace GetRequest { case 1: obj.cid = reader.bytes() break + case 2: + obj.block = reader.bytes() + break default: reader.skipType(tag & 7) break @@ -263,147 +1682,90 @@ export namespace GetRequest { return _codec } - export const encode = (obj: GetRequest): Uint8Array => { - return encodeMessage(obj, GetRequest.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, BatchRequestPut.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): GetRequest => { - return decodeMessage(buf, GetRequest.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): BatchRequestPut => { + return decodeMessage(buf, BatchRequestPut.codec()) } } -export enum GetResponseType { - PROGRESS = 'PROGRESS', - RESULT = 'RESULT' -} - -enum __GetResponseTypeValues { - PROGRESS = 0, - RESULT = 1 -} - -export namespace GetResponseType { - export const codec = (): Codec => { - return enumeration(__GetResponseTypeValues) - } -} -export interface GetResponse { - type: GetResponseType - block?: Uint8Array - progressEventType?: string - progressEventData: Map +export interface BatchRequestDelete { + cid: Uint8Array } -export namespace GetResponse { - export interface GetResponse$progressEventDataEntry { - key: string - value: string - } - - export namespace GetResponse$progressEventDataEntry { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } +export namespace BatchRequestDelete { + let _codec: Codec - if (opts.writeDefaults === true || obj.key !== '') { - w.uint32(10) - w.string(obj.key) - } + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } - if (opts.writeDefaults === true || obj.value !== '') { - w.uint32(18) - w.string(obj.value) - } + if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.cid ?? new Uint8Array(0)) + } - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '', - value: '' - } + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + cid: new Uint8Array(0) + } - const end = length == null ? reader.len : reader.pos + length + const end = length == null ? reader.len : reader.pos + length - while (reader.pos < end) { - const tag = reader.uint32() + while (reader.pos < end) { + const tag = reader.uint32() - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - case 2: - obj.value = reader.string() - break - default: - reader.skipType(tag & 7) - break - } + switch (tag >>> 3) { + case 1: + obj.cid = reader.bytes() + break + default: + reader.skipType(tag & 7) + break } + } - return obj - }) - } - - return _codec + return obj + }) } - export const encode = (obj: GetResponse$progressEventDataEntry): Uint8Array => { - return encodeMessage(obj, GetResponse$progressEventDataEntry.codec()) - } + return _codec + } - export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse$progressEventDataEntry => { - return decodeMessage(buf, GetResponse$progressEventDataEntry.codec()) - } + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, BatchRequestDelete.codec()) } - let _codec: Codec + export const decode = (buf: Uint8Array | Uint8ArrayList): BatchRequestDelete => { + return decodeMessage(buf, BatchRequestDelete.codec()) + } +} - export const codec = (): Codec => { +export interface BatchResponse {} + +export namespace BatchResponse { + let _codec: Codec + + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } - if (opts.writeDefaults === true || (obj.type != null && __GetResponseTypeValues[obj.type] !== 0)) { - w.uint32(8) - GetResponseType.codec().encode(obj.type, w) - } - - if (obj.block != null) { - w.uint32(18) - w.bytes(obj.block) - } - - if (obj.progressEventType != null) { - w.uint32(26) - w.string(obj.progressEventType) - } - - if (obj.progressEventData != null && obj.progressEventData.size !== 0) { - for (const [key, value] of obj.progressEventData.entries()) { - w.uint32(34) - GetResponse.GetResponse$progressEventDataEntry.codec().encode({ key, value }, w, { - writeDefaults: true - }) - } - } - if (opts.lengthDelimited !== false) { w.ldelim() } }, (reader, length) => { - const obj: any = { - type: GetResponseType.PROGRESS, - progressEventData: new Map() - } + const obj: any = {} const end = length == null ? reader.len : reader.pos + length @@ -411,20 +1773,6 @@ export namespace GetResponse { const tag = reader.uint32() switch (tag >>> 3) { - case 1: - obj.type = GetResponseType.codec().decode(reader) - break - case 2: - obj.block = reader.bytes() - break - case 3: - obj.progressEventType = reader.string() - break - case 4: { - const entry = GetResponse.GetResponse$progressEventDataEntry.codec().decode(reader, reader.uint32()) - obj.progressEventData.set(entry.key, entry.value) - break - } default: reader.skipType(tag & 7) break @@ -438,23 +1786,23 @@ export namespace GetResponse { return _codec } - export const encode = (obj: GetResponse): Uint8Array => { - return encodeMessage(obj, GetResponse.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, BatchResponse.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse => { - return decodeMessage(buf, GetResponse.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): BatchResponse => { + return decodeMessage(buf, BatchResponse.codec()) } } -export interface HasOptions {} +export interface QueryOptions {} -export namespace HasOptions { - let _codec: Codec +export namespace QueryOptions { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } @@ -484,41 +1832,32 @@ export namespace HasOptions { return _codec } - export const encode = (obj: HasOptions): Uint8Array => { - return encodeMessage(obj, HasOptions.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryOptions.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): HasOptions => { - return decodeMessage(buf, HasOptions.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryOptions => { + return decodeMessage(buf, QueryOptions.codec()) } } -export interface HasRequest { - cid: Uint8Array -} +export interface QueryRequest {} -export namespace HasRequest { - let _codec: Codec +export namespace QueryRequest { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid) - } - if (opts.lengthDelimited !== false) { w.ldelim() } }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } + const obj: any = {} const end = length == null ? reader.len : reader.pos + length @@ -526,9 +1865,6 @@ export namespace HasRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break default: reader.skipType(tag & 7) break @@ -542,32 +1878,38 @@ export namespace HasRequest { return _codec } - export const encode = (obj: HasRequest): Uint8Array => { - return encodeMessage(obj, HasRequest.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryRequest.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): HasRequest => { - return decodeMessage(buf, HasRequest.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryRequest => { + return decodeMessage(buf, QueryRequest.codec()) } } -export interface HasResponse { - has: boolean +export interface QueryResponse { + key: Uint8Array + value: Uint8Array } -export namespace HasResponse { - let _codec: Codec +export namespace QueryResponse { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } - if (opts.writeDefaults === true || obj.has !== false) { - w.uint32(8) - w.bool(obj.has) + if (opts.writeDefaults === true || (obj.key != null && obj.key.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.key ?? new Uint8Array(0)) + } + + if (opts.writeDefaults === true || (obj.value != null && obj.value.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.value ?? new Uint8Array(0)) } if (opts.lengthDelimited !== false) { @@ -575,7 +1917,8 @@ export namespace HasResponse { } }, (reader, length) => { const obj: any = { - has: false + key: new Uint8Array(0), + value: new Uint8Array(0) } const end = length == null ? reader.len : reader.pos + length @@ -585,7 +1928,10 @@ export namespace HasResponse { switch (tag >>> 3) { case 1: - obj.has = reader.bool() + obj.key = reader.bytes() + break + case 2: + obj.value = reader.bytes() break default: reader.skipType(tag & 7) @@ -600,23 +1946,23 @@ export namespace HasResponse { return _codec } - export const encode = (obj: HasResponse): Uint8Array => { - return encodeMessage(obj, HasResponse.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryResponse.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): HasResponse => { - return decodeMessage(buf, HasResponse.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryResponse => { + return decodeMessage(buf, QueryResponse.codec()) } } -export interface DeleteOptions {} +export interface QueryKeysOptions {} -export namespace DeleteOptions { - let _codec: Codec +export namespace QueryKeysOptions { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } @@ -646,41 +1992,32 @@ export namespace DeleteOptions { return _codec } - export const encode = (obj: DeleteOptions): Uint8Array => { - return encodeMessage(obj, DeleteOptions.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryKeysOptions.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteOptions => { - return decodeMessage(buf, DeleteOptions.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysOptions => { + return decodeMessage(buf, QueryKeysOptions.codec()) } } -export interface DeleteRequest { - cid: Uint8Array -} +export interface QueryKeysRequest {} -export namespace DeleteRequest { - let _codec: Codec +export namespace QueryKeysRequest { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid) - } - if (opts.lengthDelimited !== false) { w.ldelim() } }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } + const obj: any = {} const end = length == null ? reader.len : reader.pos + length @@ -688,9 +2025,6 @@ export namespace DeleteRequest { const tag = reader.uint32() switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break default: reader.skipType(tag & 7) break @@ -704,32 +2038,41 @@ export namespace DeleteRequest { return _codec } - export const encode = (obj: DeleteRequest): Uint8Array => { - return encodeMessage(obj, DeleteRequest.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryKeysRequest.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteRequest => { - return decodeMessage(buf, DeleteRequest.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysRequest => { + return decodeMessage(buf, QueryKeysRequest.codec()) } } -export interface DeleteResponse {} +export interface QueryKeysResponse { + key: Uint8Array +} -export namespace DeleteResponse { - let _codec: Codec +export namespace QueryKeysResponse { + let _codec: Codec - export const codec = (): Codec => { + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } + if (opts.writeDefaults === true || (obj.key != null && obj.key.byteLength > 0)) { + w.uint32(10) + w.bytes(obj.key ?? new Uint8Array(0)) + } + if (opts.lengthDelimited !== false) { w.ldelim() } }, (reader, length) => { - const obj: any = {} + const obj: any = { + key: new Uint8Array(0) + } const end = length == null ? reader.len : reader.pos + length @@ -737,6 +2080,9 @@ export namespace DeleteResponse { const tag = reader.uint32() switch (tag >>> 3) { + case 1: + obj.key = reader.bytes() + break default: reader.skipType(tag & 7) break @@ -750,11 +2096,11 @@ export namespace DeleteResponse { return _codec } - export const encode = (obj: DeleteResponse): Uint8Array => { - return encodeMessage(obj, DeleteResponse.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryKeysResponse.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteResponse => { - return decodeMessage(buf, DeleteResponse.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysResponse => { + return decodeMessage(buf, QueryKeysResponse.codec()) } } diff --git a/packages/rpc-protocol/src/datastore.proto b/packages/rpc-protocol/src/datastore.proto new file mode 100644 index 00000000..6f9c3702 --- /dev/null +++ b/packages/rpc-protocol/src/datastore.proto @@ -0,0 +1,164 @@ +syntax = "proto3"; + +message OpenOptions { + +} + +message OpenRequest { + +} + +message OpenResponse { + +} + +message CloseOptions { + +} + +message CloseRequest { + +} + +message CloseResponse { + +} + +message PutOptions { + +} + +message PutRequest { + string key = 1; + bytes value = 2; +} + +message PutResponse { + +} + +message GetOptions { + +} + +message GetRequest { + string key = 1; +} + +enum GetResponseType { + GET_PROGRESS = 0; + GET_RESULT = 1; +} + +message GetResponse { + GetResponseType type = 1; + optional bytes value = 2; + optional string progress_event_type = 3; + map progress_event_data = 4; +} + +message HasOptions { + +} + +message HasRequest { + string key = 1; +} + +message HasResponse { + bool has = 1; +} + +message DeleteOptions { + +} + +message DeleteRequest { + string key = 1; +} + +message DeleteResponse { + +} + +message PutManyOptions { + +} + +message PutManyRequest { + string key = 1; + bytes value = 2; +} + +message PutManyResponse { + string key = 1; + bytes value = 2; +} + +message GetManyOptions { + +} + +message GetManyRequest { + string key = 1; +} + +enum GetManyResponseType { + GET_MANY_PROGRESS = 0; + GET_MANY_RESULT = 1; +} + +message GetManyResponse { + GetManyResponseType type = 1; + optional bytes value = 2; + optional string progress_event_type = 3; + map progress_event_data = 4; +} + +message DeleteManyOptions { + +} + +message DeleteManyRequest { + string key = 1; +} + +message DeleteManyResponse { + string key = 1; +} + +message BatchOptions { + +} + +message BatchRequest { + +} + +message BatchResponse { + string id = 1; +} + +message QueryOptions { + +} + +message QueryRequest { + +} + +message QueryResponse { + +} + +message QueryKeysOptions { + +} + +message QueryKeysRequest { + +} + +message QueryKeysResponse { + +} diff --git a/packages/rpc-protocol/src/datastore.ts b/packages/rpc-protocol/src/datastore.ts new file mode 100644 index 00000000..46ae8d84 --- /dev/null +++ b/packages/rpc-protocol/src/datastore.ts @@ -0,0 +1,2085 @@ +/* eslint-disable import/export */ +/* eslint-disable complexity */ +/* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' +import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' + +export interface OpenOptions {} + +export namespace OpenOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, OpenOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): OpenOptions => { + return decodeMessage(buf, OpenOptions.codec()) + } +} + +export interface OpenRequest {} + +export namespace OpenRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, OpenRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): OpenRequest => { + return decodeMessage(buf, OpenRequest.codec()) + } +} + +export interface OpenResponse {} + +export namespace OpenResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, OpenResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): OpenResponse => { + return decodeMessage(buf, OpenResponse.codec()) + } +} + +export interface CloseOptions {} + +export namespace CloseOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, CloseOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): CloseOptions => { + return decodeMessage(buf, CloseOptions.codec()) + } +} + +export interface CloseRequest {} + +export namespace CloseRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, CloseRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): CloseRequest => { + return decodeMessage(buf, CloseRequest.codec()) + } +} + +export interface CloseResponse {} + +export namespace CloseResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, CloseResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): CloseResponse => { + return decodeMessage(buf, CloseResponse.codec()) + } +} + +export interface PutOptions {} + +export namespace PutOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutOptions => { + return decodeMessage(buf, PutOptions.codec()) + } +} + +export interface PutRequest { + key: string + value: Uint8Array +} + +export namespace PutRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.writeDefaults === true || (obj.value != null && obj.value.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.value ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutRequest => { + return decodeMessage(buf, PutRequest.codec()) + } +} + +export interface PutResponse {} + +export namespace PutResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutResponse => { + return decodeMessage(buf, PutResponse.codec()) + } +} + +export interface GetOptions {} + +export namespace GetOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetOptions => { + return decodeMessage(buf, GetOptions.codec()) + } +} + +export interface GetRequest { + key: string +} + +export namespace GetRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetRequest => { + return decodeMessage(buf, GetRequest.codec()) + } +} + +export enum GetResponseType { + GET_PROGRESS = 'GET_PROGRESS', + GET_RESULT = 'GET_RESULT' +} + +enum __GetResponseTypeValues { + GET_PROGRESS = 0, + GET_RESULT = 1 +} + +export namespace GetResponseType { + export const codec = (): Codec => { + return enumeration(__GetResponseTypeValues) + } +} +export interface GetResponse { + type: GetResponseType + value?: Uint8Array + progressEventType?: string + progressEventData: Map +} + +export namespace GetResponse { + export interface GetResponse$progressEventDataEntry { + key: string + value: string + } + + export namespace GetResponse$progressEventDataEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.writeDefaults === true || (obj.value != null && obj.value !== '')) { + w.uint32(18) + w.string(obj.value ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetResponse$progressEventDataEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse$progressEventDataEntry => { + return decodeMessage(buf, GetResponse$progressEventDataEntry.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.type != null && __GetResponseTypeValues[obj.type] !== 0)) { + w.uint32(8) + GetResponseType.codec().encode(obj.type ?? GetResponseType.GET_PROGRESS, w) + } + + if (obj.value != null) { + w.uint32(18) + w.bytes(obj.value) + } + + if (obj.progressEventType != null) { + w.uint32(26) + w.string(obj.progressEventType) + } + + if (obj.progressEventData != null && obj.progressEventData.size !== 0) { + for (const [key, value] of obj.progressEventData.entries()) { + w.uint32(34) + GetResponse.GetResponse$progressEventDataEntry.codec().encode({ key, value }, w, { + writeDefaults: true + }) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + type: GetResponseType.GET_PROGRESS, + progressEventData: new Map() + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.type = GetResponseType.codec().decode(reader) + break + case 2: + obj.value = reader.bytes() + break + case 3: + obj.progressEventType = reader.string() + break + case 4: { + const entry = GetResponse.GetResponse$progressEventDataEntry.codec().decode(reader, reader.uint32()) + obj.progressEventData.set(entry.key, entry.value) + break + } + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse => { + return decodeMessage(buf, GetResponse.codec()) + } +} + +export interface HasOptions {} + +export namespace HasOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, HasOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasOptions => { + return decodeMessage(buf, HasOptions.codec()) + } +} + +export interface HasRequest { + key: string +} + +export namespace HasRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, HasRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasRequest => { + return decodeMessage(buf, HasRequest.codec()) + } +} + +export interface HasResponse { + has: boolean +} + +export namespace HasResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.has != null && obj.has !== false)) { + w.uint32(8) + w.bool(obj.has ?? false) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + has: false + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.has = reader.bool() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, HasResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): HasResponse => { + return decodeMessage(buf, HasResponse.codec()) + } +} + +export interface DeleteOptions {} + +export namespace DeleteOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteOptions => { + return decodeMessage(buf, DeleteOptions.codec()) + } +} + +export interface DeleteRequest { + key: string +} + +export namespace DeleteRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteRequest => { + return decodeMessage(buf, DeleteRequest.codec()) + } +} + +export interface DeleteResponse {} + +export namespace DeleteResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteResponse => { + return decodeMessage(buf, DeleteResponse.codec()) + } +} + +export interface PutManyOptions {} + +export namespace PutManyOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutManyOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyOptions => { + return decodeMessage(buf, PutManyOptions.codec()) + } +} + +export interface PutManyRequest { + key: string + value: Uint8Array +} + +export namespace PutManyRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.writeDefaults === true || (obj.value != null && obj.value.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.value ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutManyRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyRequest => { + return decodeMessage(buf, PutManyRequest.codec()) + } +} + +export interface PutManyResponse { + key: string + value: Uint8Array +} + +export namespace PutManyResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.writeDefaults === true || (obj.value != null && obj.value.byteLength > 0)) { + w.uint32(18) + w.bytes(obj.value ?? new Uint8Array(0)) + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: new Uint8Array(0) + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, PutManyResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyResponse => { + return decodeMessage(buf, PutManyResponse.codec()) + } +} + +export interface GetManyOptions {} + +export namespace GetManyOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetManyOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyOptions => { + return decodeMessage(buf, GetManyOptions.codec()) + } +} + +export interface GetManyRequest { + key: string +} + +export namespace GetManyRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetManyRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyRequest => { + return decodeMessage(buf, GetManyRequest.codec()) + } +} + +export enum GetManyResponseType { + GET_MANY_PROGRESS = 'GET_MANY_PROGRESS', + GET_MANY_RESULT = 'GET_MANY_RESULT' +} + +enum __GetManyResponseTypeValues { + GET_MANY_PROGRESS = 0, + GET_MANY_RESULT = 1 +} + +export namespace GetManyResponseType { + export const codec = (): Codec => { + return enumeration(__GetManyResponseTypeValues) + } +} +export interface GetManyResponse { + type: GetManyResponseType + value?: Uint8Array + progressEventType?: string + progressEventData: Map +} + +export namespace GetManyResponse { + export interface GetManyResponse$progressEventDataEntry { + key: string + value: string + } + + export namespace GetManyResponse$progressEventDataEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.writeDefaults === true || (obj.value != null && obj.value !== '')) { + w.uint32(18) + w.string(obj.value ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetManyResponse$progressEventDataEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyResponse$progressEventDataEntry => { + return decodeMessage(buf, GetManyResponse$progressEventDataEntry.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.type != null && __GetManyResponseTypeValues[obj.type] !== 0)) { + w.uint32(8) + GetManyResponseType.codec().encode(obj.type ?? GetManyResponseType.GET_MANY_PROGRESS, w) + } + + if (obj.value != null) { + w.uint32(18) + w.bytes(obj.value) + } + + if (obj.progressEventType != null) { + w.uint32(26) + w.string(obj.progressEventType) + } + + if (obj.progressEventData != null && obj.progressEventData.size !== 0) { + for (const [key, value] of obj.progressEventData.entries()) { + w.uint32(34) + GetManyResponse.GetManyResponse$progressEventDataEntry.codec().encode({ key, value }, w, { + writeDefaults: true + }) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + type: GetManyResponseType.GET_MANY_PROGRESS, + progressEventData: new Map() + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.type = GetManyResponseType.codec().decode(reader) + break + case 2: + obj.value = reader.bytes() + break + case 3: + obj.progressEventType = reader.string() + break + case 4: { + const entry = GetManyResponse.GetManyResponse$progressEventDataEntry.codec().decode(reader, reader.uint32()) + obj.progressEventData.set(entry.key, entry.value) + break + } + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, GetManyResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyResponse => { + return decodeMessage(buf, GetManyResponse.codec()) + } +} + +export interface DeleteManyOptions {} + +export namespace DeleteManyOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteManyOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyOptions => { + return decodeMessage(buf, DeleteManyOptions.codec()) + } +} + +export interface DeleteManyRequest { + key: string +} + +export namespace DeleteManyRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteManyRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyRequest => { + return decodeMessage(buf, DeleteManyRequest.codec()) + } +} + +export interface DeleteManyResponse { + key: string +} + +export namespace DeleteManyResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, DeleteManyResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyResponse => { + return decodeMessage(buf, DeleteManyResponse.codec()) + } +} + +export interface BatchOptions {} + +export namespace BatchOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, BatchOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): BatchOptions => { + return decodeMessage(buf, BatchOptions.codec()) + } +} + +export interface BatchRequest {} + +export namespace BatchRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, BatchRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): BatchRequest => { + return decodeMessage(buf, BatchRequest.codec()) + } +} + +export interface BatchResponse { + id: string +} + +export namespace BatchResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.id != null && obj.id !== '')) { + w.uint32(10) + w.string(obj.id ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + id: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.id = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, BatchResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): BatchResponse => { + return decodeMessage(buf, BatchResponse.codec()) + } +} + +export interface QueryOptions {} + +export namespace QueryOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryOptions => { + return decodeMessage(buf, QueryOptions.codec()) + } +} + +export interface QueryRequest {} + +export namespace QueryRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryRequest => { + return decodeMessage(buf, QueryRequest.codec()) + } +} + +export interface QueryResponse {} + +export namespace QueryResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryResponse => { + return decodeMessage(buf, QueryResponse.codec()) + } +} + +export interface QueryKeysOptions {} + +export namespace QueryKeysOptions { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryKeysOptions.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysOptions => { + return decodeMessage(buf, QueryKeysOptions.codec()) + } +} + +export interface QueryKeysRequest {} + +export namespace QueryKeysRequest { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryKeysRequest.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysRequest => { + return decodeMessage(buf, QueryKeysRequest.codec()) + } +} + +export interface QueryKeysResponse {} + +export namespace QueryKeysResponse { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = {} + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, QueryKeysResponse.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysResponse => { + return decodeMessage(buf, QueryKeysResponse.codec()) + } +} diff --git a/packages/rpc-protocol/src/index.ts b/packages/rpc-protocol/src/index.ts index 5822d4c6..23541f48 100644 --- a/packages/rpc-protocol/src/index.ts +++ b/packages/rpc-protocol/src/index.ts @@ -1,4 +1,4 @@ -import type { RPCCallResponse } from './rpc.js' +import { RPCCallError, RPCCallProgress } from './rpc.js' export const HELIA_RPC_PROTOCOL = '/helia-rpc/0.0.1' @@ -6,11 +6,26 @@ export class RPCError extends Error { public readonly name: string public readonly code: string - constructor (response: RPCCallResponse) { - super(response.errorMessage ?? 'RPC error') + constructor (buf: Uint8Array) { + const message = RPCCallError.decode(buf) - this.name = response.errorName ?? 'RPCError' - this.code = response.errorCode ?? 'ERR_RPC_ERROR' - this.stack = response.errorStack ?? this.stack + super(message.message ?? 'RPC error') + + this.name = message.name ?? 'RPCError' + this.code = message.code ?? 'ERR_RPC_ERROR' + this.stack = message.stack ?? this.stack + } +} + +export class RPCProgressEvent extends Event { + constructor (buf: Uint8Array) { + const event = RPCCallProgress.decode(buf) + + super(event.event ?? 'ProgressEvent') + + for (const [key, value] of event.data) { + // @ts-expect-error cannot use strings to index this type + this[key] = value + } } } diff --git a/packages/rpc-protocol/src/root.ts b/packages/rpc-protocol/src/root.ts index 9dfd55db..40b24efa 100644 --- a/packages/rpc-protocol/src/root.ts +++ b/packages/rpc-protocol/src/root.ts @@ -5,8 +5,8 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' export interface InfoOptions { peerId?: string @@ -55,7 +55,7 @@ export namespace InfoOptions { return _codec } - export const encode = (obj: InfoOptions): Uint8Array => { + export const encode = (obj: Partial): Uint8Array => { return encodeMessage(obj, InfoOptions.codec()) } @@ -82,9 +82,9 @@ export namespace InfoResponse { w.fork() } - if (opts.writeDefaults === true || obj.peerId !== '') { + if (opts.writeDefaults === true || (obj.peerId != null && obj.peerId !== '')) { w.uint32(10) - w.string(obj.peerId) + w.string(obj.peerId ?? '') } if (obj.multiaddrs != null) { @@ -94,14 +94,14 @@ export namespace InfoResponse { } } - if (opts.writeDefaults === true || obj.agentVersion !== '') { + if (opts.writeDefaults === true || (obj.agentVersion != null && obj.agentVersion !== '')) { w.uint32(26) - w.string(obj.agentVersion) + w.string(obj.agentVersion ?? '') } - if (opts.writeDefaults === true || obj.protocolVersion !== '') { + if (opts.writeDefaults === true || (obj.protocolVersion != null && obj.protocolVersion !== '')) { w.uint32(34) - w.string(obj.protocolVersion) + w.string(obj.protocolVersion ?? '') } if (obj.protocols != null) { @@ -157,7 +157,7 @@ export namespace InfoResponse { return _codec } - export const encode = (obj: InfoResponse): Uint8Array => { + export const encode = (obj: Partial): Uint8Array => { return encodeMessage(obj, InfoResponse.codec()) } diff --git a/packages/rpc-protocol/src/rpc.proto b/packages/rpc-protocol/src/rpc.proto index 9de4328c..177b2156 100644 --- a/packages/rpc-protocol/src/rpc.proto +++ b/packages/rpc-protocol/src/rpc.proto @@ -1,22 +1,32 @@ syntax = "proto3"; message RPCCallRequest { - string resource = 1; - string method = 2; - optional string authorization = 3; - optional bytes options = 4; + string resource = 1; + string method = 2; + string authorization = 3; + bytes options = 4; } -message RPCCallResponse { - RPCCallResponseType type = 1; - optional bytes message = 2; - optional string error_name = 3; - optional string error_message = 4; - optional string error_stack = 5; - optional string error_code = 6; +enum RPCCallMessageType { + RPC_CALL_DONE = 0; + RPC_CALL_ERROR = 1; + RPC_CALL_MESSAGE = 2; + RPC_CALL_PROGRESS = 3; } -enum RPCCallResponseType { - message = 0; - error = 1; +message RPCCallMessage { + RPCCallMessageType type = 1; + bytes message = 2; +} + +message RPCCallError { + optional string name = 1; + optional string message = 2; + optional string stack = 3; + optional string code = 4; +} + +message RPCCallProgress { + string event = 1; + map data = 4; } diff --git a/packages/rpc-protocol/src/rpc.ts b/packages/rpc-protocol/src/rpc.ts index e60f4c61..659c7124 100644 --- a/packages/rpc-protocol/src/rpc.ts +++ b/packages/rpc-protocol/src/rpc.ts @@ -5,14 +5,14 @@ /* eslint-disable @typescript-eslint/no-empty-interface */ import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' import type { Codec } from 'protons-runtime' +import type { Uint8ArrayList } from 'uint8arraylist' export interface RPCCallRequest { resource: string method: string - authorization?: string - options?: Uint8Array + authorization: string + options: Uint8Array } export namespace RPCCallRequest { @@ -25,24 +25,24 @@ export namespace RPCCallRequest { w.fork() } - if (opts.writeDefaults === true || obj.resource !== '') { + if (opts.writeDefaults === true || (obj.resource != null && obj.resource !== '')) { w.uint32(10) - w.string(obj.resource) + w.string(obj.resource ?? '') } - if (opts.writeDefaults === true || obj.method !== '') { + if (opts.writeDefaults === true || (obj.method != null && obj.method !== '')) { w.uint32(18) - w.string(obj.method) + w.string(obj.method ?? '') } - if (obj.authorization != null) { + if (opts.writeDefaults === true || (obj.authorization != null && obj.authorization !== '')) { w.uint32(26) - w.string(obj.authorization) + w.string(obj.authorization ?? '') } - if (obj.options != null) { + if (opts.writeDefaults === true || (obj.options != null && obj.options.byteLength > 0)) { w.uint32(34) - w.bytes(obj.options) + w.bytes(obj.options ?? new Uint8Array(0)) } if (opts.lengthDelimited !== false) { @@ -51,7 +51,9 @@ export namespace RPCCallRequest { }, (reader, length) => { const obj: any = { resource: '', - method: '' + method: '', + authorization: '', + options: new Uint8Array(0) } const end = length == null ? reader.len : reader.pos + length @@ -85,7 +87,7 @@ export namespace RPCCallRequest { return _codec } - export const encode = (obj: RPCCallRequest): Uint8Array => { + export const encode = (obj: Partial): Uint8Array => { return encodeMessage(obj, RPCCallRequest.codec()) } @@ -94,62 +96,135 @@ export namespace RPCCallRequest { } } -export interface RPCCallResponse { - type: RPCCallResponseType - message?: Uint8Array - errorName?: string - errorMessage?: string - errorStack?: string - errorCode?: string +export enum RPCCallMessageType { + RPC_CALL_DONE = 'RPC_CALL_DONE', + RPC_CALL_ERROR = 'RPC_CALL_ERROR', + RPC_CALL_MESSAGE = 'RPC_CALL_MESSAGE', + RPC_CALL_PROGRESS = 'RPC_CALL_PROGRESS' } -export namespace RPCCallResponse { - let _codec: Codec +enum __RPCCallMessageTypeValues { + RPC_CALL_DONE = 0, + RPC_CALL_ERROR = 1, + RPC_CALL_MESSAGE = 2, + RPC_CALL_PROGRESS = 3 +} - export const codec = (): Codec => { +export namespace RPCCallMessageType { + export const codec = (): Codec => { + return enumeration(__RPCCallMessageTypeValues) + } +} +export interface RPCCallMessage { + type: RPCCallMessageType + message: Uint8Array +} + +export namespace RPCCallMessage { + let _codec: Codec + + export const codec = (): Codec => { if (_codec == null) { - _codec = message((obj, w, opts = {}) => { + _codec = message((obj, w, opts = {}) => { if (opts.lengthDelimited !== false) { w.fork() } - if (opts.writeDefaults === true || (obj.type != null && __RPCCallResponseTypeValues[obj.type] !== 0)) { + if (opts.writeDefaults === true || (obj.type != null && __RPCCallMessageTypeValues[obj.type] !== 0)) { w.uint32(8) - RPCCallResponseType.codec().encode(obj.type, w) + RPCCallMessageType.codec().encode(obj.type ?? RPCCallMessageType.RPC_CALL_DONE, w) } - if (obj.message != null) { + if (opts.writeDefaults === true || (obj.message != null && obj.message.byteLength > 0)) { w.uint32(18) - w.bytes(obj.message) + w.bytes(obj.message ?? new Uint8Array(0)) } - if (obj.errorName != null) { - w.uint32(26) - w.string(obj.errorName) + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + type: RPCCallMessageType.RPC_CALL_DONE, + message: new Uint8Array(0) } - if (obj.errorMessage != null) { - w.uint32(34) - w.string(obj.errorMessage) + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.type = RPCCallMessageType.codec().decode(reader) + break + case 2: + obj.message = reader.bytes() + break + default: + reader.skipType(tag & 7) + break + } } - if (obj.errorStack != null) { - w.uint32(42) - w.string(obj.errorStack) + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, RPCCallMessage.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallMessage => { + return decodeMessage(buf, RPCCallMessage.codec()) + } +} + +export interface RPCCallError { + name?: string + message?: string + stack?: string + code?: string +} + +export namespace RPCCallError { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() } - if (obj.errorCode != null) { - w.uint32(50) - w.string(obj.errorCode) + if (obj.name != null) { + w.uint32(10) + w.string(obj.name) + } + + if (obj.message != null) { + w.uint32(18) + w.string(obj.message) + } + + if (obj.stack != null) { + w.uint32(26) + w.string(obj.stack) + } + + if (obj.code != null) { + w.uint32(34) + w.string(obj.code) } if (opts.lengthDelimited !== false) { w.ldelim() } }, (reader, length) => { - const obj: any = { - type: RPCCallResponseType.message - } + const obj: any = {} const end = length == null ? reader.len : reader.pos + length @@ -158,22 +233,16 @@ export namespace RPCCallResponse { switch (tag >>> 3) { case 1: - obj.type = RPCCallResponseType.codec().decode(reader) + obj.name = reader.string() break case 2: - obj.message = reader.bytes() + obj.message = reader.string() break case 3: - obj.errorName = reader.string() + obj.stack = reader.string() break case 4: - obj.errorMessage = reader.string() - break - case 5: - obj.errorStack = reader.string() - break - case 6: - obj.errorCode = reader.string() + obj.code = reader.string() break default: reader.skipType(tag & 7) @@ -188,27 +257,153 @@ export namespace RPCCallResponse { return _codec } - export const encode = (obj: RPCCallResponse): Uint8Array => { - return encodeMessage(obj, RPCCallResponse.codec()) + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, RPCCallError.codec()) } - export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallResponse => { - return decodeMessage(buf, RPCCallResponse.codec()) + export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallError => { + return decodeMessage(buf, RPCCallError.codec()) } } -export enum RPCCallResponseType { - message = 'message', - error = 'error' +export interface RPCCallProgress { + event: string + data: Map } -enum __RPCCallResponseTypeValues { - message = 0, - error = 1 -} +export namespace RPCCallProgress { + export interface RPCCallProgress$dataEntry { + key: string + value: string + } + + export namespace RPCCallProgress$dataEntry { + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { + w.uint32(10) + w.string(obj.key ?? '') + } + + if (opts.writeDefaults === true || (obj.value != null && obj.value !== '')) { + w.uint32(18) + w.string(obj.value ?? '') + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + key: '', + value: '' + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.key = reader.string() + break + case 2: + obj.value = reader.string() + break + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, RPCCallProgress$dataEntry.codec()) + } + + export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallProgress$dataEntry => { + return decodeMessage(buf, RPCCallProgress$dataEntry.codec()) + } + } + + let _codec: Codec + + export const codec = (): Codec => { + if (_codec == null) { + _codec = message((obj, w, opts = {}) => { + if (opts.lengthDelimited !== false) { + w.fork() + } + + if (opts.writeDefaults === true || (obj.event != null && obj.event !== '')) { + w.uint32(10) + w.string(obj.event ?? '') + } + + if (obj.data != null && obj.data.size !== 0) { + for (const [key, value] of obj.data.entries()) { + w.uint32(34) + RPCCallProgress.RPCCallProgress$dataEntry.codec().encode({ key, value }, w, { + writeDefaults: true + }) + } + } + + if (opts.lengthDelimited !== false) { + w.ldelim() + } + }, (reader, length) => { + const obj: any = { + event: '', + data: new Map() + } + + const end = length == null ? reader.len : reader.pos + length + + while (reader.pos < end) { + const tag = reader.uint32() + + switch (tag >>> 3) { + case 1: + obj.event = reader.string() + break + case 4: { + const entry = RPCCallProgress.RPCCallProgress$dataEntry.codec().decode(reader, reader.uint32()) + obj.data.set(entry.key, entry.value) + break + } + default: + reader.skipType(tag & 7) + break + } + } + + return obj + }) + } + + return _codec + } + + export const encode = (obj: Partial): Uint8Array => { + return encodeMessage(obj, RPCCallProgress.codec()) + } -export namespace RPCCallResponseType { - export const codec = (): Codec => { - return enumeration(__RPCCallResponseTypeValues) + export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallProgress => { + return decodeMessage(buf, RPCCallProgress.codec()) } } diff --git a/packages/rpc-server/package.json b/packages/rpc-server/package.json index 34355a96..6bd90035 100644 --- a/packages/rpc-server/package.json +++ b/packages/rpc-server/package.json @@ -142,7 +142,7 @@ "@helia/rpc-protocol": "~0.0.0", "@libp2p/interface-keychain": "^2.0.3", "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/logger": "^2.0.2", + "@libp2p/logger": "^2.0.5", "@libp2p/peer-id": "^2.0.0", "@multiformats/multiaddr": "^11.1.5", "@ucans/ucans": "^0.11.0-alpha", diff --git a/packages/rpc-server/src/handlers/authorization/get.ts b/packages/rpc-server/src/handlers/authorization/get.ts index 27179acd..ffad8484 100644 --- a/packages/rpc-server/src/handlers/authorization/get.ts +++ b/packages/rpc-server/src/handlers/authorization/get.ts @@ -1,5 +1,5 @@ import { GetRequest, GetResponse } from '@helia/rpc-protocol/authorization' -import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' import type { RPCServerConfig, Service } from '../../index.js' import * as ucans from '@ucans/ucans' import { base58btc } from 'multiformats/bases/base58' @@ -44,10 +44,26 @@ export function createAuthorizationGet (config: RPCServerConfig): Service { issuer, lifetimeInSeconds: config.authorizationValiditySeconds, capabilities: [ + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/batch' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/close' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/delete-many' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, { with: { scheme: 'helia-rpc', hierPart: '/blockstore/delete' }, can: { namespace: 'helia-rpc', segments: ['INVOKE'] } }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/get-many' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, { with: { scheme: 'helia-rpc', hierPart: '/blockstore/get' }, can: { namespace: 'helia-rpc', segments: ['INVOKE'] } @@ -56,20 +72,32 @@ export function createAuthorizationGet (config: RPCServerConfig): Service { with: { scheme: 'helia-rpc', hierPart: '/blockstore/has' }, can: { namespace: 'helia-rpc', segments: ['INVOKE'] } }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/put-many' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, { with: { scheme: 'helia-rpc', hierPart: '/blockstore/put' }, can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/query-keys' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } + }, + { + with: { scheme: 'helia-rpc', hierPart: '/blockstore/query' }, + can: { namespace: 'helia-rpc', segments: ['INVOKE'] } } ] }) stream.writePB({ - type: RPCCallResponseType.message, + type: RPCCallMessageType.RPC_CALL_MESSAGE, message: GetResponse.encode({ authorization: ucans.encode(ucan) }) }, - RPCCallResponse) + RPCCallMessage) } } } diff --git a/packages/rpc-server/src/handlers/blockstore/batch.ts b/packages/rpc-server/src/handlers/blockstore/batch.ts new file mode 100644 index 00000000..f76dd93f --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/batch.ts @@ -0,0 +1,40 @@ +import { BatchRequest, BatchRequestDelete, BatchRequestPut, BatchRequestType } from '@helia/rpc-protocol/blockstore' +import type { RPCServerConfig, Service } from '../../index.js' +import { CID } from 'multiformats/cid' + +export function createBlockstoreBatch (config: RPCServerConfig): Service { + return { + async handle ({ options, stream, signal }): Promise { + const batch = config.helia.blockstore.batch() + + while (true) { + const request = await stream.readPB(BatchRequest) + + for (let i = 0; i < 10; i++) { + if (i < 5) { + continue + } + } + + let putMessage + let deleteMessage + + switch (request.type) { + case BatchRequestType.BATCH_REQUEST_PUT: + putMessage = BatchRequestPut.decode(request.message) + batch.put(CID.decode(putMessage.cid), putMessage.block) + break + case BatchRequestType.BATCH_REQUEST_DELETE: + deleteMessage = BatchRequestDelete.decode(request.message) + batch.delete(CID.decode(deleteMessage.cid)) + break + case BatchRequestType.BATCH_REQUEST_COMMIT: + await batch.commit() + return + default: + throw new Error('Unkown batch message type') + } + } + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/close.ts b/packages/rpc-server/src/handlers/blockstore/close.ts new file mode 100644 index 00000000..b5e2956a --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/close.ts @@ -0,0 +1,9 @@ +import type { RPCServerConfig, Service } from '../../index.js' + +export function createBlockstoreClose (config: RPCServerConfig): Service { + return { + async handle ({ options, stream, signal }): Promise { + await config.helia.blockstore.close() + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/delete-many.ts b/packages/rpc-server/src/handlers/blockstore/delete-many.ts new file mode 100644 index 00000000..09b5e9eb --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/delete-many.ts @@ -0,0 +1,32 @@ +import { DeleteManyOptions, DeleteManyRequest, DeleteManyResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' +import type { RPCServerConfig, Service } from '../../index.js' +import { CID } from 'multiformats/cid' + +export function createBlockstoreDeleteMany (config: RPCServerConfig): Service { + return { + async handle ({ options, stream, signal }): Promise { + const opts = DeleteManyOptions.decode(options) + + for await (const cid of config.helia.blockstore.deleteMany( + (async function * () { + while (true) { + const request = await stream.readPB(DeleteManyRequest) + + yield CID.decode(request.cid) + } + })(), { + signal, + ...opts + })) { + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message: DeleteManyResponse.encode({ + cid: cid.bytes + }) + }, + RPCCallMessage) + } + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/delete.ts b/packages/rpc-server/src/handlers/blockstore/delete.ts index edafc128..4895eb2c 100644 --- a/packages/rpc-server/src/handlers/blockstore/delete.ts +++ b/packages/rpc-server/src/handlers/blockstore/delete.ts @@ -1,9 +1,9 @@ import { DeleteOptions, DeleteRequest, DeleteResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' -export function createDelete (config: RPCServerConfig): Service { +export function createBlockstoreDelete (config: RPCServerConfig): Service { return { async handle ({ options, stream, signal }): Promise { const opts = DeleteOptions.decode(options) @@ -16,11 +16,11 @@ export function createDelete (config: RPCServerConfig): Service { }) stream.writePB({ - type: RPCCallResponseType.message, + type: RPCCallMessageType.RPC_CALL_MESSAGE, message: DeleteResponse.encode({ }) }, - RPCCallResponse) + RPCCallMessage) } } } diff --git a/packages/rpc-server/src/handlers/blockstore/get-many.ts b/packages/rpc-server/src/handlers/blockstore/get-many.ts new file mode 100644 index 00000000..0188afa1 --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/get-many.ts @@ -0,0 +1,32 @@ +import { DeleteManyOptions, DeleteManyRequest, DeleteManyResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' +import type { RPCServerConfig, Service } from '../../index.js' +import { CID } from 'multiformats/cid' + +export function createBlockstoreGetMany (config: RPCServerConfig): Service { + return { + async handle ({ options, stream, signal }): Promise { + const opts = DeleteManyOptions.decode(options) + + for await (const cid of config.helia.blockstore.deleteMany( + (async function * () { + while (true) { + const request = await stream.readPB(DeleteManyRequest) + + yield CID.decode(request.cid) + } + })(), { + signal, + ...opts + })) { + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message: DeleteManyResponse.encode({ + cid: cid.bytes + }) + }, + RPCCallMessage) + } + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/get.ts b/packages/rpc-server/src/handlers/blockstore/get.ts index 9dc5e600..41f31e70 100644 --- a/packages/rpc-server/src/handlers/blockstore/get.ts +++ b/packages/rpc-server/src/handlers/blockstore/get.ts @@ -1,9 +1,9 @@ -import { GetOptions, GetRequest, GetResponse, GetResponseType } from '@helia/rpc-protocol/blockstore' -import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' -export function createGet (config: RPCServerConfig): Service { +export function createBlockstoreGet (config: RPCServerConfig): Service { return { async handle ({ options, stream, signal }): Promise { const opts = GetOptions.decode(options) @@ -12,30 +12,16 @@ export function createGet (config: RPCServerConfig): Service { const block = await config.helia.blockstore.get(cid, { signal, - ...opts, - // @ts-expect-error progress is not in the interface yet - progress: (evt) => { - stream.writePB({ - type: RPCCallResponseType.message, - message: GetResponse.encode({ - type: GetResponseType.PROGRESS, - progressEventType: evt.type, - progressEventData: new Map() - }) - }, - RPCCallResponse) - } + ...opts }) stream.writePB({ - type: RPCCallResponseType.message, + type: RPCCallMessageType.RPC_CALL_MESSAGE, message: GetResponse.encode({ - type: GetResponseType.RESULT, - block, - progressEventData: new Map() + block }) }, - RPCCallResponse) + RPCCallMessage) } } } diff --git a/packages/rpc-server/src/handlers/blockstore/has.ts b/packages/rpc-server/src/handlers/blockstore/has.ts index 7a27bc14..17cb0c03 100644 --- a/packages/rpc-server/src/handlers/blockstore/has.ts +++ b/packages/rpc-server/src/handlers/blockstore/has.ts @@ -1,9 +1,9 @@ import { HasOptions, HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' -export function createHas (config: RPCServerConfig): Service { +export function createBlockstoreHas (config: RPCServerConfig): Service { return { async handle ({ options, stream, signal }): Promise { const opts = HasOptions.decode(options) @@ -16,12 +16,12 @@ export function createHas (config: RPCServerConfig): Service { }) stream.writePB({ - type: RPCCallResponseType.message, + type: RPCCallMessageType.RPC_CALL_MESSAGE, message: HasResponse.encode({ has }) }, - RPCCallResponse) + RPCCallMessage) } } } diff --git a/packages/rpc-server/src/handlers/blockstore/open.ts b/packages/rpc-server/src/handlers/blockstore/open.ts new file mode 100644 index 00000000..606b6b93 --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/open.ts @@ -0,0 +1,9 @@ +import type { RPCServerConfig, Service } from '../../index.js' + +export function createBlockstoreOpen (config: RPCServerConfig): Service { + return { + async handle ({ options, stream, signal }): Promise { + await config.helia.blockstore.open() + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/put-many.ts b/packages/rpc-server/src/handlers/blockstore/put-many.ts new file mode 100644 index 00000000..09cf253a --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/put-many.ts @@ -0,0 +1,32 @@ +import { DeleteManyOptions, DeleteManyRequest, DeleteManyResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' +import type { RPCServerConfig, Service } from '../../index.js' +import { CID } from 'multiformats/cid' + +export function createBlockstorePutMany (config: RPCServerConfig): Service { + return { + async handle ({ options, stream, signal }): Promise { + const opts = DeleteManyOptions.decode(options) + + for await (const cid of config.helia.blockstore.deleteMany( + (async function * () { + while (true) { + const request = await stream.readPB(DeleteManyRequest) + + yield CID.decode(request.cid) + } + })(), { + signal, + ...opts + })) { + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message: DeleteManyResponse.encode({ + cid: cid.bytes + }) + }, + RPCCallMessage) + } + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/put.ts b/packages/rpc-server/src/handlers/blockstore/put.ts index 42cc715c..cb967c27 100644 --- a/packages/rpc-server/src/handlers/blockstore/put.ts +++ b/packages/rpc-server/src/handlers/blockstore/put.ts @@ -1,9 +1,9 @@ import { PutOptions, PutRequest, PutResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' import type { RPCServerConfig, Service } from '../../index.js' import { CID } from 'multiformats/cid' -export function createPut (config: RPCServerConfig): Service { +export function createBlockstorePut (config: RPCServerConfig): Service { return { async handle ({ options, stream, signal }): Promise { const opts = PutOptions.decode(options) @@ -16,11 +16,11 @@ export function createPut (config: RPCServerConfig): Service { }) stream.writePB({ - type: RPCCallResponseType.message, + type: RPCCallMessageType.RPC_CALL_MESSAGE, message: PutResponse.encode({ }) }, - RPCCallResponse) + RPCCallMessage) } } } diff --git a/packages/rpc-server/src/handlers/blockstore/query-keys.ts b/packages/rpc-server/src/handlers/blockstore/query-keys.ts new file mode 100644 index 00000000..0afb079f --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/query-keys.ts @@ -0,0 +1,25 @@ +import { QueryKeysOptions, QueryKeysResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' +import type { RPCServerConfig, Service } from '../../index.js' + +export function createBlockstoreQueryKeys (config: RPCServerConfig): Service { + return { + async handle ({ options, stream, signal }): Promise { + const opts = QueryKeysOptions.decode(options) + + for await (const cid of config.helia.blockstore.queryKeys({ + ...opts + }, { + signal + })) { + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message: QueryKeysResponse.encode({ + key: cid.bytes + }) + }, + RPCCallMessage) + } + } + } +} diff --git a/packages/rpc-server/src/handlers/blockstore/query.ts b/packages/rpc-server/src/handlers/blockstore/query.ts new file mode 100644 index 00000000..3c41c85c --- /dev/null +++ b/packages/rpc-server/src/handlers/blockstore/query.ts @@ -0,0 +1,26 @@ +import { QueryOptions, QueryResponse } from '@helia/rpc-protocol/blockstore' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' +import type { RPCServerConfig, Service } from '../../index.js' + +export function createBlockstoreQuery (config: RPCServerConfig): Service { + return { + async handle ({ options, stream, signal }): Promise { + const opts = QueryOptions.decode(options) + + for await (const { key, value } of config.helia.blockstore.query({ + ...opts + }, { + signal + })) { + stream.writePB({ + type: RPCCallMessageType.RPC_CALL_MESSAGE, + message: QueryResponse.encode({ + key: key.bytes, + value + }) + }, + RPCCallMessage) + } + } + } +} diff --git a/packages/rpc-server/src/handlers/index.ts b/packages/rpc-server/src/handlers/index.ts new file mode 100644 index 00000000..1856e16a --- /dev/null +++ b/packages/rpc-server/src/handlers/index.ts @@ -0,0 +1,36 @@ +import type { RPCServerConfig, Service } from '../index.js' +import { createInfo } from './info.js' +import { createAuthorizationGet } from './authorization/get.js' +import { createBlockstoreDelete } from './blockstore/delete.js' +import { createBlockstoreGet } from './blockstore/get.js' +import { createBlockstoreHas } from './blockstore/has.js' +import { createBlockstorePut } from './blockstore/put.js' +import { createBlockstoreDeleteMany } from './blockstore/delete-many.js' +import { createBlockstoreGetMany } from './blockstore/get-many.js' +import { createBlockstoreBatch } from './blockstore/batch.js' +import { createBlockstoreClose } from './blockstore/close.js' +import { createBlockstoreOpen } from './blockstore/open.js' +import { createBlockstorePutMany } from './blockstore/put-many.js' +import { createBlockstoreQueryKeys } from './blockstore/query-keys.js' +import { createBlockstoreQuery } from './blockstore/query.js' + +export function createServices (config: RPCServerConfig): Record { + const services: Record = { + '/authorization/get': createAuthorizationGet(config), + '/blockstore/batch': createBlockstoreBatch(config), + '/blockstore/close': createBlockstoreClose(config), + '/blockstore/delete-many': createBlockstoreDeleteMany(config), + '/blockstore/delete': createBlockstoreDelete(config), + '/blockstore/get-many': createBlockstoreGetMany(config), + '/blockstore/get': createBlockstoreGet(config), + '/blockstore/has': createBlockstoreHas(config), + '/blockstore/open': createBlockstoreOpen(config), + '/blockstore/put-many': createBlockstorePutMany(config), + '/blockstore/put': createBlockstorePut(config), + '/blockstore/query-keys': createBlockstoreQueryKeys(config), + '/blockstore/query': createBlockstoreQuery(config), + '/info': createInfo(config) + } + + return services +} diff --git a/packages/rpc-server/src/handlers/info.ts b/packages/rpc-server/src/handlers/info.ts index a7df8445..0a496f7e 100644 --- a/packages/rpc-server/src/handlers/info.ts +++ b/packages/rpc-server/src/handlers/info.ts @@ -1,5 +1,5 @@ import { InfoOptions, InfoResponse } from '@helia/rpc-protocol/root' -import { RPCCallResponse, RPCCallResponseType } from '@helia/rpc-protocol/rpc' +import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' import { peerIdFromString } from '@libp2p/peer-id' import type { RPCServerConfig, Service } from '../index.js' @@ -15,13 +15,13 @@ export function createInfo (config: RPCServerConfig): Service { }) stream.writePB({ - type: RPCCallResponseType.message, + type: RPCCallMessageType.RPC_CALL_MESSAGE, message: InfoResponse.encode({ ...result, peerId: result.peerId.toString(), multiaddrs: result.multiaddrs.map(ma => ma.toString()) }) - }, RPCCallResponse) + }, RPCCallMessage) } } } diff --git a/packages/rpc-server/src/index.ts b/packages/rpc-server/src/index.ts index be3e025e..ac665034 100644 --- a/packages/rpc-server/src/index.ts +++ b/packages/rpc-server/src/index.ts @@ -1,21 +1,16 @@ import type { Helia } from '@helia/interface' import { HeliaError } from '@helia/interface/errors' -import { createInfo } from './handlers/info.js' import { logger } from '@libp2p/logger' import { HELIA_RPC_PROTOCOL } from '@helia/rpc-protocol' -import { RPCCallRequest, RPCCallResponseType, RPCCallResponse } from '@helia/rpc-protocol/rpc' +import { RPCCallRequest, RPCCallError, RPCCallMessageType, RPCCallMessage } from '@helia/rpc-protocol/rpc' import * as ucans from '@ucans/ucans' -import { createDelete } from './handlers/blockstore/delete.js' -import { createGet } from './handlers/blockstore/get.js' -import { createHas } from './handlers/blockstore/has.js' -import { createPut } from './handlers/blockstore/put.js' import { pbStream, ProtobufStream } from 'it-pb-stream' -import { createAuthorizationGet } from './handlers/authorization/get.js' import { EdKeypair } from '@ucans/ucans' import type { KeyChain } from '@libp2p/interface-keychain' import { base58btc } from 'multiformats/bases/base58' import { concat as uint8ArrayConcat } from 'uint8arrays/concat' import type { PeerId } from '@libp2p/interface-peer-id' +import { createServices } from './handlers/index.js' const log = logger('helia:rpc-server') @@ -62,14 +57,7 @@ export async function createHeliaRpcServer (config: RPCServerConfig): Promise = { - '/authorization/get': createAuthorizationGet(config), - '/blockstore/delete': createDelete(config), - '/blockstore/get': createGet(config), - '/blockstore/has': createHas(config), - '/blockstore/put': createPut(config), - '/info': createInfo(config) - } + const services = createServices(config) await helia.libp2p.handle(HELIA_RPC_PROTOCOL, ({ stream, connection }) => { const controller = new AbortController() @@ -129,19 +117,27 @@ export async function createHeliaRpcServer (config: RPCServerConfig): Promise = { } const fs = unixfs(helia) - for await (const result of fs.add(parsePositionals(positionals), options)) { - stdout.write(result.cid.toString() + '\n') + for await (const result of fs.addStream(parsePositionals(positionals), options)) { + stdout.write(`${result.cid}\n`) } } } diff --git a/packages/unixfs/package.json b/packages/unixfs/package.json index d2e0dc44..7a6bf587 100644 --- a/packages/unixfs/package.json +++ b/packages/unixfs/package.json @@ -139,13 +139,28 @@ }, "dependencies": { "@helia/interface": "~0.0.0", - "interface-blockstore": "^4.0.0", + "@ipld/dag-pb": "^4.0.0", + "@libp2p/interfaces": "^3.3.1", + "@libp2p/logger": "^2.0.5", + "@multiformats/murmur3": "^2.1.2", + "hamt-sharding": "^3.0.2", + "interface-blockstore": "^4.0.1", + "ipfs-unixfs": "^9.0.0", "ipfs-unixfs-exporter": "^10.0.0", "ipfs-unixfs-importer": "^12.0.0", + "it-last": "^2.0.0", + "it-pipe": "^2.0.5", + "merge-options": "^3.0.4", "multiformats": "^11.0.1" }, "devDependencies": { - "aegir": "^38.1.0" + "aegir": "^38.1.0", + "blockstore-core": "^3.0.0", + "delay": "^5.0.0", + "it-all": "^2.0.0", + "it-drain": "^2.0.0", + "it-to-buffer": "^3.0.0", + "uint8arrays": "^4.0.3" }, "typedoc": { "entryPoint": "./src/index.ts" diff --git a/packages/unixfs/src/commands/add.ts b/packages/unixfs/src/commands/add.ts new file mode 100644 index 00000000..33f92869 --- /dev/null +++ b/packages/unixfs/src/commands/add.ts @@ -0,0 +1,46 @@ +import type { Blockstore } from 'interface-blockstore' +import { ImportCandidate, importer, ImportResult, UserImporterOptions } from 'ipfs-unixfs-importer' +import last from 'it-last' +import type { CID } from 'multiformats/cid' +import { UnknownError } from './utils/errors.js' + +function isIterable (obj: any): obj is Iterator { + return obj[Symbol.iterator] != null +} + +function isAsyncIterable (obj: any): obj is AsyncIterator { + return obj[Symbol.asyncIterator] != null +} + +export async function add (source: Uint8Array | Iterator | AsyncIterator | ImportCandidate, blockstore: Blockstore, options: UserImporterOptions = {}): Promise { + let importCandidate: ImportCandidate + + if (source instanceof Uint8Array || isIterable(source) || isAsyncIterable(source)) { + importCandidate = { + // @ts-expect-error FIXME: work out types + content: source + } + } else { + importCandidate = source + } + + const result = await last(importer(importCandidate, blockstore, { + cidVersion: 1, + rawLeaves: true, + ...options + })) + + if (result == null) { + throw new UnknownError('Could not import') + } + + return result.cid +} + +export async function * addStream (source: Iterable | AsyncIterable, blockstore: Blockstore, options: UserImporterOptions = {}): AsyncGenerator { + yield * importer(source, blockstore, { + cidVersion: 1, + rawLeaves: true, + ...options + }) +} diff --git a/packages/unixfs/src/commands/cat.ts b/packages/unixfs/src/commands/cat.ts new file mode 100644 index 00000000..5f51ad2b --- /dev/null +++ b/packages/unixfs/src/commands/cat.ts @@ -0,0 +1,31 @@ +import { NoContentError, NotAFileError } from '@helia/interface/errors' +import { Blockstore, exporter } from 'ipfs-unixfs-exporter' +import type { CID } from 'multiformats/cid' +import type { CatOptions } from '../index.js' +import { resolve } from './utils/resolve.js' +import mergeOpts from 'merge-options' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) + +const defaultOptions: CatOptions = { + +} + +export async function * cat (cid: CID, blockstore: Blockstore, options: Partial = {}): AsyncIterable { + const opts: CatOptions = mergeOptions(defaultOptions, options) + const resolved = await resolve(cid, opts.path, blockstore, opts) + const result = await exporter(resolved.cid, blockstore, opts) + + if (result.type !== 'file' && result.type !== 'raw') { + throw new NotAFileError() + } + + if (result.content == null) { + throw new NoContentError() + } + + yield * result.content({ + offset: opts.offset, + length: opts.length + }) +} diff --git a/packages/unixfs/src/commands/chmod.ts b/packages/unixfs/src/commands/chmod.ts new file mode 100644 index 00000000..a02400ad --- /dev/null +++ b/packages/unixfs/src/commands/chmod.ts @@ -0,0 +1,133 @@ +import { recursive } from 'ipfs-unixfs-exporter' +import { CID } from 'multiformats/cid' +import type { ChmodOptions } from '../index.js' +import mergeOpts from 'merge-options' +import { logger } from '@libp2p/logger' +import { UnixFS } from 'ipfs-unixfs' +import { pipe } from 'it-pipe' +import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js' +import * as dagPB from '@ipld/dag-pb' +import type { PBNode, PBLink } from '@ipld/dag-pb' +import { importer } from 'ipfs-unixfs-importer' +import { persist } from './utils/persist.js' +import type { Blockstore } from 'interface-blockstore' +import last from 'it-last' +import { sha256 } from 'multiformats/hashes/sha2' +import { resolve, updatePathCids } from './utils/resolve.js' +import * as raw from 'multiformats/codecs/raw' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) +const log = logger('helia:unixfs:chmod') + +const defaultOptions: ChmodOptions = { + recursive: false +} + +export async function chmod (cid: CID, mode: number, blockstore: Blockstore, options: Partial = {}): Promise { + const opts: ChmodOptions = mergeOptions(defaultOptions, options) + const resolved = await resolve(cid, opts.path, blockstore, options) + + log('chmod %c %d', resolved.cid, mode) + + if (opts.recursive) { + // recursively export from root CID, change perms of each entry then reimport + // but do not reimport files, only manipulate dag-pb nodes + const root = await pipe( + async function * () { + for await (const entry of recursive(resolved.cid, blockstore)) { + let metadata: UnixFS + let links: PBLink[] = [] + + if (entry.type === 'raw') { + // convert to UnixFS + metadata = new UnixFS({ type: 'file', data: entry.node }) + } else if (entry.type === 'file' || entry.type === 'directory') { + metadata = entry.unixfs + links = entry.node.Links + } else { + throw new NotUnixFSError() + } + + metadata.mode = mode + + const node = { + Data: metadata.marshal(), + Links: links + } + + yield { + path: entry.path, + content: node + } + } + }, + // @ts-expect-error we account for the incompatible source type with our custom dag builder below + (source) => importer(source, blockstore, { + ...opts, + pin: false, + dagBuilder: async function * (source, block, opts) { + for await (const entry of source) { + yield async function () { + // @ts-expect-error cannot derive type + const node: PBNode = entry.content + + const buf = dagPB.encode(node) + const cid = await persist(buf, block, opts) + + if (node.Data == null) { + throw new InvalidPBNodeError(`${cid} had no data`) + } + + const unixfs = UnixFS.unmarshal(node.Data) + + return { + cid, + size: buf.length, + path: entry.path, + unixfs + } + } + } + } + }), + async (nodes) => await last(nodes) + ) + + if (root == null) { + throw new UnknownError(`Could not chmod ${resolved.cid.toString()}`) + } + + return await updatePathCids(root.cid, resolved, blockstore, options) + } + + const block = await blockstore.get(resolved.cid) + let metadata: UnixFS + let links: PBLink[] = [] + + if (resolved.cid.code === raw.code) { + // convert to UnixFS + metadata = new UnixFS({ type: 'file', data: block }) + } else { + const node = dagPB.decode(block) + + if (node.Data == null) { + throw new InvalidPBNodeError(`${resolved.cid.toString()} had no data`) + } + + links = node.Links + metadata = UnixFS.unmarshal(node.Data) + } + + metadata.mode = mode + const updatedBlock = dagPB.encode({ + Data: metadata.marshal(), + Links: links + }) + + const hash = await sha256.digest(updatedBlock) + const updatedCid = CID.create(resolved.cid.version, dagPB.code, hash) + + await blockstore.put(updatedCid, updatedBlock) + + return await updatePathCids(updatedCid, resolved, blockstore, options) +} diff --git a/packages/unixfs/src/commands/cp.ts b/packages/unixfs/src/commands/cp.ts new file mode 100644 index 00000000..a107fcc9 --- /dev/null +++ b/packages/unixfs/src/commands/cp.ts @@ -0,0 +1,41 @@ +import { InvalidParametersError } from '@helia/interface/errors' +import type { Blockstore } from 'ipfs-unixfs-exporter' +import type { CID } from 'multiformats/cid' +import type { CpOptions } from '../index.js' +import mergeOpts from 'merge-options' +import { logger } from '@libp2p/logger' +import { addLink } from './utils/add-link.js' +import { cidToPBLink } from './utils/cid-to-pblink.js' +import { cidToDirectory } from './utils/cid-to-directory.js' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) +const log = logger('helia:unixfs:cp') + +const defaultOptions = { + force: false +} + +export async function cp (source: CID, target: CID, name: string, blockstore: Blockstore, options: Partial = {}): Promise { + const opts: CpOptions = mergeOptions(defaultOptions, options) + + if (name.includes('/')) { + throw new InvalidParametersError('Name must not have slashes') + } + + const [ + directory, + pblink + ] = await Promise.all([ + cidToDirectory(target, blockstore, opts), + cidToPBLink(source, name, blockstore, opts) + ]) + + log('Adding %c as "%s" to %c', source, name, target) + + const result = await addLink(directory, pblink, blockstore, { + allowOverwriting: opts.force, + ...opts + }) + + return result.cid +} diff --git a/packages/unixfs/src/commands/ls.ts b/packages/unixfs/src/commands/ls.ts new file mode 100644 index 00000000..2618d99f --- /dev/null +++ b/packages/unixfs/src/commands/ls.ts @@ -0,0 +1,36 @@ +import { NoContentError, NotADirectoryError } from '@helia/interface/errors' +import { Blockstore, exporter, UnixFSEntry } from 'ipfs-unixfs-exporter' +import type { CID } from 'multiformats/cid' +import type { LsOptions } from '../index.js' +import { resolve } from './utils/resolve.js' +import mergeOpts from 'merge-options' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) + +const defaultOptions = { + +} + +export async function * ls (cid: CID, blockstore: Blockstore, options: Partial = {}): AsyncIterable { + const opts: LsOptions = mergeOptions(defaultOptions, options) + const resolved = await resolve(cid, opts.path, blockstore, opts) + const result = await exporter(resolved.cid, blockstore) + + if (result.type === 'file' || result.type === 'raw') { + yield result + return + } + + if (result.content == null) { + throw new NoContentError() + } + + if (result.type !== 'directory') { + throw new NotADirectoryError() + } + + yield * result.content({ + offset: options.offset, + length: options.length + }) +} diff --git a/packages/unixfs/src/commands/mkdir.ts b/packages/unixfs/src/commands/mkdir.ts new file mode 100644 index 00000000..31db6448 --- /dev/null +++ b/packages/unixfs/src/commands/mkdir.ts @@ -0,0 +1,71 @@ +import { InvalidParametersError, NotADirectoryError } from '@helia/interface/errors' +import { CID } from 'multiformats/cid' +import mergeOpts from 'merge-options' +import { logger } from '@libp2p/logger' +import type { MkdirOptions } from '../index.js' +import * as dagPB from '@ipld/dag-pb' +import { addLink } from './utils/add-link.js' +import type { Blockstore } from 'interface-blockstore' +import { UnixFS } from 'ipfs-unixfs' +import { sha256 } from 'multiformats/hashes/sha2' +import { exporter } from 'ipfs-unixfs-exporter' +import { cidToDirectory } from './utils/cid-to-directory.js' +import { cidToPBLink } from './utils/cid-to-pblink.js' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) +const log = logger('helia:unixfs:mkdir') + +const defaultOptions = { + cidVersion: 1, + force: false +} + +export async function mkdir (parentCid: CID, dirname: string, blockstore: Blockstore, options: Partial = {}): Promise { + const opts: MkdirOptions = mergeOptions(defaultOptions, options) + + if (dirname.includes('/')) { + throw new InvalidParametersError('Path must not have slashes') + } + + const entry = await exporter(parentCid, blockstore, options) + + if (entry.type !== 'directory') { + throw new NotADirectoryError(`${parentCid.toString()} was not a UnixFS directory`) + } + + log('creating %s', dirname) + + const metadata = new UnixFS({ + type: 'directory', + mode: opts.mode, + mtime: opts.mtime + }) + + // Persist the new parent PBNode + const node = { + Data: metadata.marshal(), + Links: [] + } + const buf = dagPB.encode(node) + const hash = await sha256.digest(buf) + const emptyDirCid = CID.create(opts.cidVersion, dagPB.code, hash) + + await blockstore.put(emptyDirCid, buf) + + const [ + directory, + pblink + ] = await Promise.all([ + cidToDirectory(parentCid, blockstore, opts), + cidToPBLink(emptyDirCid, dirname, blockstore, opts) + ]) + + log('adding empty dir called %s to %c', dirname, parentCid) + + const result = await addLink(directory, pblink, blockstore, { + ...opts, + allowOverwriting: opts.force + }) + + return result.cid +} diff --git a/packages/unixfs/src/commands/rm.ts b/packages/unixfs/src/commands/rm.ts new file mode 100644 index 00000000..b5c68341 --- /dev/null +++ b/packages/unixfs/src/commands/rm.ts @@ -0,0 +1,31 @@ +import { InvalidParametersError } from '@helia/interface/errors' +import type { Blockstore } from 'ipfs-unixfs-exporter' +import type { CID } from 'multiformats/cid' +import type { RmOptions } from '../index.js' +import mergeOpts from 'merge-options' +import { logger } from '@libp2p/logger' +import { removeLink } from './utils/remove-link.js' +import { cidToDirectory } from './utils/cid-to-directory.js' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) +const log = logger('helia:unixfs:rm') + +const defaultOptions = { + +} + +export async function rm (target: CID, name: string, blockstore: Blockstore, options: Partial = {}): Promise { + const opts: RmOptions = mergeOptions(defaultOptions, options) + + if (name.includes('/')) { + throw new InvalidParametersError('Name must not have slashes') + } + + const directory = await cidToDirectory(target, blockstore, opts) + + log('Removing %s from %c', name, target) + + const result = await removeLink(directory, name, blockstore, opts) + + return result.cid +} diff --git a/packages/unixfs/src/commands/stat.ts b/packages/unixfs/src/commands/stat.ts new file mode 100644 index 00000000..e13ed476 --- /dev/null +++ b/packages/unixfs/src/commands/stat.ts @@ -0,0 +1,137 @@ +import { Blockstore, exporter } from 'ipfs-unixfs-exporter' +import type { CID } from 'multiformats/cid' +import type { StatOptions, UnixFSStats } from '../index.js' +import mergeOpts from 'merge-options' +import { logger } from '@libp2p/logger' +import { UnixFS } from 'ipfs-unixfs' +import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js' +import * as dagPb from '@ipld/dag-pb' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Mtime } from 'ipfs-unixfs' +import { resolve } from './utils/resolve.js' +import * as raw from 'multiformats/codecs/raw' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) +const log = logger('helia:unixfs:stat') + +const defaultOptions = { + +} + +export async function stat (cid: CID, blockstore: Blockstore, options: Partial = {}): Promise { + const opts: StatOptions = mergeOptions(defaultOptions, options) + const resolved = await resolve(cid, options.path, blockstore, opts) + + log('stat %c', resolved.cid) + + const result = await exporter(resolved.cid, blockstore, opts) + + if (result.type !== 'file' && result.type !== 'directory' && result.type !== 'raw') { + throw new NotUnixFSError() + } + + let fileSize: number = 0 + let dagSize: number = 0 + let localFileSize: number = 0 + let localDagSize: number = 0 + let blocks: number = 0 + let mode: number | undefined + let mtime: Mtime | undefined + const type = result.type + + if (result.type === 'raw') { + fileSize = result.node.byteLength + dagSize = result.node.byteLength + localFileSize = result.node.byteLength + localDagSize = result.node.byteLength + blocks = 1 + } + + if (result.type === 'directory') { + fileSize = 0 + dagSize = result.unixfs.marshal().byteLength + localFileSize = 0 + localDagSize = dagSize + blocks = 1 + mode = result.unixfs.mode + mtime = result.unixfs.mtime + } + + if (result.type === 'file') { + const results = await inspectDag(resolved.cid, blockstore, opts) + + fileSize = result.unixfs.fileSize() + dagSize = (result.node.Data?.byteLength ?? 0) + result.node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0) + localFileSize = results.localFileSize + localDagSize = results.localDagSize + blocks = results.blocks + mode = result.unixfs.mode + mtime = result.unixfs.mtime + } + + return { + cid: resolved.cid, + mode, + mtime, + fileSize, + dagSize, + localFileSize, + localDagSize, + blocks, + type + } +} + +interface InspectDagResults { + localFileSize: number + localDagSize: number + blocks: number +} + +async function inspectDag (cid: CID, blockstore: Blockstore, options: AbortOptions): Promise { + const results = { + localFileSize: 0, + localDagSize: 0, + blocks: 0 + } + + if (await blockstore.has(cid, options)) { + const block = await blockstore.get(cid, options) + results.blocks++ + results.localDagSize += block.byteLength + + if (cid.code === raw.code) { + results.localFileSize += block.byteLength + } else if (cid.code === dagPb.code) { + const pbNode = dagPb.decode(block) + + if (pbNode.Links.length > 0) { + // intermediate node + for (const link of pbNode.Links) { + const linkResult = await inspectDag(link.Hash, blockstore, options) + + results.localFileSize += linkResult.localFileSize + results.localDagSize += linkResult.localDagSize + results.blocks += linkResult.blocks + } + } else { + // leaf node + if (pbNode.Data == null) { + throw new InvalidPBNodeError(`PBNode ${cid.toString()} had no data`) + } + + const unixfs = UnixFS.unmarshal(pbNode.Data) + + if (unixfs.data == null) { + throw new InvalidPBNodeError(`UnixFS node ${cid.toString()} had no data`) + } + + results.localFileSize += unixfs.data.byteLength ?? 0 + } + } else { + throw new UnknownError(`${cid.toString()} was neither DAG_PB nor RAW`) + } + } + + return results +} diff --git a/packages/unixfs/src/commands/touch.ts b/packages/unixfs/src/commands/touch.ts new file mode 100644 index 00000000..a4887653 --- /dev/null +++ b/packages/unixfs/src/commands/touch.ts @@ -0,0 +1,136 @@ +import { recursive } from 'ipfs-unixfs-exporter' +import { CID } from 'multiformats/cid' +import type { TouchOptions } from '../index.js' +import mergeOpts from 'merge-options' +import { logger } from '@libp2p/logger' +import { UnixFS } from 'ipfs-unixfs' +import { pipe } from 'it-pipe' +import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js' +import * as dagPB from '@ipld/dag-pb' +import type { PBNode, PBLink } from '@ipld/dag-pb' +import { importer } from 'ipfs-unixfs-importer' +import { persist } from './utils/persist.js' +import type { Blockstore } from 'interface-blockstore' +import last from 'it-last' +import { sha256 } from 'multiformats/hashes/sha2' +import { resolve, updatePathCids } from './utils/resolve.js' +import * as raw from 'multiformats/codecs/raw' + +const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) +const log = logger('helia:unixfs:touch') + +const defaultOptions = { + recursive: false +} + +export async function touch (cid: CID, blockstore: Blockstore, options: Partial = {}): Promise { + const opts: TouchOptions = mergeOptions(defaultOptions, options) + const resolved = await resolve(cid, opts.path, blockstore, opts) + const mtime = opts.mtime ?? { + secs: Date.now() / 1000, + nsecs: 0 + } + + log('touch %c %o', resolved.cid, mtime) + + if (opts.recursive) { + // recursively export from root CID, change perms of each entry then reimport + // but do not reimport files, only manipulate dag-pb nodes + const root = await pipe( + async function * () { + for await (const entry of recursive(resolved.cid, blockstore)) { + let metadata: UnixFS + let links: PBLink[] + + if (entry.type === 'raw') { + metadata = new UnixFS({ data: entry.node }) + links = [] + } else if (entry.type === 'file' || entry.type === 'directory') { + metadata = entry.unixfs + links = entry.node.Links + } else { + throw new NotUnixFSError() + } + + metadata.mtime = mtime + + const node = { + Data: metadata.marshal(), + Links: links + } + + yield { + path: entry.path, + content: node + } + } + }, + // @ts-expect-error we account for the incompatible source type with our custom dag builder below + (source) => importer(source, blockstore, { + ...opts, + pin: false, + dagBuilder: async function * (source, block, opts) { + for await (const entry of source) { + yield async function () { + // @ts-expect-error cannot derive type + const node: PBNode = entry.content + + const buf = dagPB.encode(node) + const cid = await persist(buf, block, opts) + + if (node.Data == null) { + throw new InvalidPBNodeError(`${cid} had no data`) + } + + const unixfs = UnixFS.unmarshal(node.Data) + + return { + cid, + size: buf.length, + path: entry.path, + unixfs + } + } + } + } + }), + async (nodes) => await last(nodes) + ) + + if (root == null) { + throw new UnknownError(`Could not chmod ${resolved.cid.toString()}`) + } + + return await updatePathCids(root.cid, resolved, blockstore, options) + } + + const block = await blockstore.get(resolved.cid) + let metadata: UnixFS + let links: PBLink[] = [] + + if (resolved.cid.code === raw.code) { + metadata = new UnixFS({ data: block }) + } else { + const node = dagPB.decode(block) + links = node.Links + + if (node.Data == null) { + throw new InvalidPBNodeError(`${resolved.cid.toString()} had no data`) + } + + metadata = UnixFS.unmarshal(node.Data) + } + + metadata.mtime = mtime + const updatedBlock = dagPB.encode({ + Data: metadata.marshal(), + Links: links + }) + + const hash = await sha256.digest(updatedBlock) + const updatedCid = CID.create(resolved.cid.version, dagPB.code, hash) + + await blockstore.put(updatedCid, updatedBlock) + + return await updatePathCids(updatedCid, resolved, blockstore, options) +} diff --git a/packages/unixfs/src/commands/utils/add-link.ts b/packages/unixfs/src/commands/utils/add-link.ts new file mode 100644 index 00000000..e33fa8f8 --- /dev/null +++ b/packages/unixfs/src/commands/utils/add-link.ts @@ -0,0 +1,319 @@ +import * as dagPB from '@ipld/dag-pb' +import { CID } from 'multiformats/cid' +import { logger } from '@libp2p/logger' +import { UnixFS } from 'ipfs-unixfs' +import { DirSharded } from './dir-sharded.js' +import { + updateHamtDirectory, + recreateHamtLevel, + recreateInitialHamtLevel, + createShard, + toPrefix, + addLinksToHamtBucket +} from './hamt-utils.js' +import last from 'it-last' +import type { Blockstore } from 'ipfs-unixfs-exporter' +import type { PBNode, PBLink } from '@ipld/dag-pb/interface' +import { sha256 } from 'multiformats/hashes/sha2' +import type { Bucket } from 'hamt-sharding' +import { AlreadyExistsError, InvalidPBNodeError } from './errors.js' +import { InvalidParametersError } from '@helia/interface/errors' +import type { ImportResult } from 'ipfs-unixfs-importer' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Directory } from './cid-to-directory.js' + +const log = logger('helia:unixfs:components:utils:add-link') + +export interface AddLinkResult { + node: PBNode + cid: CID + size: number +} + +export interface AddLinkOptions extends AbortOptions { + allowOverwriting: boolean +} + +export async function addLink (parent: Directory, child: Required, blockstore: Blockstore, options: AddLinkOptions): Promise { + if (parent.node.Data == null) { + throw new InvalidParametersError('Invalid parent passed to addLink') + } + + // FIXME: this should work on block size not number of links + if (parent.node.Links.length >= 1000) { + log('converting directory to sharded directory') + + const result = await convertToShardedDirectory(parent, blockstore) + parent.cid = result.cid + parent.node = dagPB.decode(await blockstore.get(result.cid)) + } + + if (parent.node.Data == null) { + throw new InvalidParametersError('Invalid parent passed to addLink') + } + + const meta = UnixFS.unmarshal(parent.node.Data) + + if (meta.type === 'hamt-sharded-directory') { + log('adding link to sharded directory') + + return await addToShardedDirectory(parent, child, blockstore, options) + } + + log(`adding ${child.Name} (${child.Hash}) to regular directory`) + + return await addToDirectory(parent, child, blockstore, options) +} + +const convertToShardedDirectory = async (parent: Directory, blockstore: Blockstore): Promise => { + if (parent.node.Data == null) { + throw new InvalidParametersError('Invalid parent passed to convertToShardedDirectory') + } + + const unixfs = UnixFS.unmarshal(parent.node.Data) + + const result = await createShard(blockstore, parent.node.Links.map(link => ({ + name: (link.Name ?? ''), + size: link.Tsize ?? 0, + cid: link.Hash + })), { + mode: unixfs.mode, + mtime: unixfs.mtime + }) + + log(`Converted directory to sharded directory ${result.cid}`) + + return result +} + +const addToDirectory = async (parent: Directory, child: PBLink, blockstore: Blockstore, options: AddLinkOptions): Promise => { + // Remove existing link if it exists + const parentLinks = parent.node.Links.filter((link) => { + const matches = link.Name === child.Name + + if (matches && !options.allowOverwriting) { + throw new AlreadyExistsError() + } + + return !matches + }) + parentLinks.push(child) + + if (parent.node.Data == null) { + throw new InvalidPBNodeError('Parent node with no data passed to addToDirectory') + } + + const node = UnixFS.unmarshal(parent.node.Data) + + let data + if (node.mtime != null) { + // Update mtime if previously set + const ms = Date.now() + const secs = Math.floor(ms / 1000) + + node.mtime = { + secs, + nsecs: (ms - (secs * 1000)) * 1000 + } + + data = node.marshal() + } else { + data = parent.node.Data + } + parent.node = dagPB.prepare({ + Data: data, + Links: parentLinks + }) + + // Persist the new parent PbNode + const buf = dagPB.encode(parent.node) + const hash = await sha256.digest(buf) + const cid = CID.create(parent.cid.version, dagPB.code, hash) + + await blockstore.put(cid, buf) + + return { + node: parent.node, + cid, + size: buf.length + } +} + +const addToShardedDirectory = async (parent: Directory, child: Required, blockstore: Blockstore, options: AddLinkOptions): Promise => { + const { + shard, path + } = await addFileToShardedDirectory(parent, child, blockstore, options) + const result = await last(shard.flush(blockstore)) + + if (result == null) { + throw new Error('No result from flushing shard') + } + + const block = await blockstore.get(result.cid) + const node = dagPB.decode(block) + + // we have written out the shard, but only one sub-shard will have been written so replace it in the original shard + const parentLinks = parent.node.Links.filter((link) => { + const matches = (link.Name ?? '').substring(0, 2) === path[0].prefix + + if (matches && !options.allowOverwriting) { + throw new AlreadyExistsError() + } + + return !matches + }) + + const newLink = node.Links + .find(link => (link.Name ?? '').substring(0, 2) === path[0].prefix) + + if (newLink == null) { + throw new Error(`No link found with prefix ${path[0].prefix}`) + } + + parentLinks.push(newLink) + + return await updateHamtDirectory(parent, blockstore, parentLinks, path[0].bucket, options) +} + +const addFileToShardedDirectory = async (parent: Directory, child: Required, blockstore: Blockstore, options: AddLinkOptions): Promise<{ shard: DirSharded, path: BucketPath[] }> => { + if (parent.node.Data == null) { + throw new InvalidPBNodeError('Parent node with no data passed to addFileToShardedDirectory') + } + + // start at the root bucket and descend, loading nodes as we go + const rootBucket = await recreateInitialHamtLevel(parent.node.Links) + const node = UnixFS.unmarshal(parent.node.Data) + + const shard = new DirSharded({ + root: true, + dir: true, + parent: undefined, + parentKey: undefined, + path: '', + dirty: true, + flat: false, + mode: node.mode + }, { + ...options, + cidVersion: parent.cid.version + }) + shard._bucket = rootBucket + + if (node.mtime != null) { + // update mtime if previously set + shard.mtime = { + secs: Math.round(Date.now() / 1000) + } + } + + // load subshards until the bucket & position no longer changes + const position = await rootBucket._findNewBucketAndPos(child.Name) + const path = toBucketPath(position) + path[0].node = parent.node + let index = 0 + + while (index < path.length) { + const segment = path[index] + index++ + const node = segment.node + + if (node == null) { + throw new Error('Segment had no node') + } + + const link = node.Links + .find(link => (link.Name ?? '').substring(0, 2) === segment.prefix) + + if (link == null) { + // prefix is new, file will be added to the current bucket + log(`Link ${segment.prefix}${child.Name} will be added`) + index = path.length + + break + } + + if (link.Name === `${segment.prefix}${child.Name}`) { + // file already existed, file will be added to the current bucket + log(`Link ${segment.prefix}${child.Name} will be replaced`) + index = path.length + + break + } + + if ((link.Name ?? '').length > 2) { + // another file had the same prefix, will be replaced with a subshard + log(`Link ${link.Name} ${link.Hash} will be replaced with a subshard`) + index = path.length + + break + } + + // load sub-shard + log(`Found subshard ${segment.prefix}`) + const block = await blockstore.get(link.Hash) + const subShard = dagPB.decode(block) + + // subshard hasn't been loaded, descend to the next level of the HAMT + if (path[index] == null) { + log(`Loaded new subshard ${segment.prefix}`) + await recreateHamtLevel(blockstore, subShard.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options) + + const position = await rootBucket._findNewBucketAndPos(child.Name) + + path.push({ + bucket: position.bucket, + prefix: toPrefix(position.pos), + node: subShard + }) + + break + } + + const nextSegment = path[index] + + // add next levels worth of links to bucket + await addLinksToHamtBucket(blockstore, subShard.Links, nextSegment.bucket, rootBucket, options) + + nextSegment.node = subShard + } + + // finally add the new file into the shard + await shard._bucket.put(child.Name, { + size: child.Tsize, + cid: child.Hash + }) + + return { + shard, path + } +} + +export interface BucketPath { + bucket: Bucket + prefix: string + node?: PBNode +} + +const toBucketPath = (position: { pos: number, bucket: Bucket }): BucketPath[] => { + const path = [{ + bucket: position.bucket, + prefix: toPrefix(position.pos) + }] + + let bucket = position.bucket._parent + let positionInBucket = position.bucket._posAtParent + + while (bucket != null) { + path.push({ + bucket, + prefix: toPrefix(positionInBucket) + }) + + positionInBucket = bucket._posAtParent + bucket = bucket._parent + } + + path.reverse() + + return path +} diff --git a/packages/unixfs/src/commands/utils/cid-to-directory.ts b/packages/unixfs/src/commands/utils/cid-to-directory.ts new file mode 100644 index 00000000..0e6ce758 --- /dev/null +++ b/packages/unixfs/src/commands/utils/cid-to-directory.ts @@ -0,0 +1,23 @@ +import { NotADirectoryError } from '@helia/interface/errors' +import { Blockstore, exporter } from 'ipfs-unixfs-exporter' +import type { CID } from 'multiformats/cid' +import type { PBNode } from '@ipld/dag-pb' +import type { AbortOptions } from '@libp2p/interfaces' + +export interface Directory { + cid: CID + node: PBNode +} + +export async function cidToDirectory (cid: CID, blockstore: Blockstore, options: AbortOptions = {}): Promise { + const entry = await exporter(cid, blockstore, options) + + if (entry.type !== 'directory') { + throw new NotADirectoryError(`${cid.toString()} was not a UnixFS directory`) + } + + return { + cid, + node: entry.node + } +} diff --git a/packages/unixfs/src/commands/utils/cid-to-pblink.ts b/packages/unixfs/src/commands/utils/cid-to-pblink.ts new file mode 100644 index 00000000..6a6a68c5 --- /dev/null +++ b/packages/unixfs/src/commands/utils/cid-to-pblink.ts @@ -0,0 +1,26 @@ +import { Blockstore, exporter } from 'ipfs-unixfs-exporter' +import type { CID } from 'multiformats/cid' +import { NotUnixFSError } from './errors.js' +import * as dagPb from '@ipld/dag-pb' +import type { PBNode, PBLink } from '@ipld/dag-pb' +import type { AbortOptions } from '@libp2p/interfaces' + +export async function cidToPBLink (cid: CID, name: string, blockstore: Blockstore, options?: AbortOptions): Promise> { + const sourceEntry = await exporter(cid, blockstore, options) + + if (sourceEntry.type !== 'directory' && sourceEntry.type !== 'file' && sourceEntry.type !== 'raw') { + throw new NotUnixFSError(`${cid.toString()} was not a UnixFS node`) + } + + return { + Name: name, + Tsize: sourceEntry.node instanceof Uint8Array ? sourceEntry.node.byteLength : dagNodeTsize(sourceEntry.node), + Hash: cid + } +} + +function dagNodeTsize (node: PBNode): number { + const linkSizes = node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0) + + return dagPb.encode(node).byteLength + linkSizes +} diff --git a/packages/unixfs/src/commands/utils/dir-sharded.ts b/packages/unixfs/src/commands/utils/dir-sharded.ts new file mode 100644 index 00000000..fa160ed6 --- /dev/null +++ b/packages/unixfs/src/commands/utils/dir-sharded.ts @@ -0,0 +1,219 @@ +import { encode, prepare } from '@ipld/dag-pb' +import { UnixFS } from 'ipfs-unixfs' +import { persist } from './persist.js' +import { createHAMT, Bucket, BucketChild } from 'hamt-sharding' +import { + hamtHashCode, + hamtHashFn, + hamtBucketBits +} from './hamt-constants.js' +import type { CID, Version } from 'multiformats/cid' +import type { PBNode } from '@ipld/dag-pb/interface' +import type { Mtime } from 'ipfs-unixfs' +import type { BlockCodec } from 'multiformats/codecs/interface' +import type { Blockstore } from 'ipfs-unixfs-importer' + +export interface ImportResult { + cid: CID + node: PBNode + size: number +} + +export interface DirContents { + cid?: CID + size?: number +} + +export interface DirOptions { + mtime?: Mtime + mode?: number + codec?: BlockCodec + cidVersion?: Version + onlyHash?: boolean + signal?: AbortSignal +} + +export interface DirProps { + root: boolean + dir: boolean + path: string + dirty: boolean + flat: boolean + parent?: Dir + parentKey?: string + unixfs?: UnixFS + mode?: number + mtime?: Mtime +} + +export abstract class Dir { + protected options: DirOptions + protected root: boolean + protected dir: boolean + protected path: string + protected dirty: boolean + protected flat: boolean + protected parent?: Dir + protected parentKey?: string + protected unixfs?: UnixFS + protected mode?: number + public mtime?: Mtime + protected cid?: CID + protected size?: number + + constructor (props: DirProps, options: DirOptions) { + this.options = options ?? {} + this.root = props.root + this.dir = props.dir + this.path = props.path + this.dirty = props.dirty + this.flat = props.flat + this.parent = props.parent + this.parentKey = props.parentKey + this.unixfs = props.unixfs + this.mode = props.mode + this.mtime = props.mtime + } +} + +export class DirSharded extends Dir { + public _bucket: Bucket + + constructor (props: DirProps, options: DirOptions) { + super(props, options) + + /** @type {Bucket} */ + this._bucket = createHAMT({ + hashFn: hamtHashFn, + bits: hamtBucketBits + }) + } + + async put (name: string, value: DirContents): Promise { + await this._bucket.put(name, value) + } + + async get (name: string): Promise { + return await this._bucket.get(name) + } + + childCount (): number { + return this._bucket.leafCount() + } + + directChildrenCount (): number { + return this._bucket.childrenCount() + } + + onlyChild (): Bucket | BucketChild { + return this._bucket.onlyChild() + } + + async * eachChildSeries (): AsyncGenerator<{ key: string, child: DirContents }> { + for await (const { key, value } of this._bucket.eachLeafSeries()) { + yield { + key, + child: value + } + } + } + + async * flush (blockstore: Blockstore): AsyncIterable { + yield * flush(this._bucket, blockstore, this, this.options) + } +} + +async function * flush (bucket: Bucket, blockstore: Blockstore, shardRoot: any, options: DirOptions): AsyncIterable { + const children = bucket._children + const links = [] + let childrenSize = 0 + + for (let i = 0; i < children.length; i++) { + const child = children.get(i) + + if (child == null) { + continue + } + + const labelPrefix = i.toString(16).toUpperCase().padStart(2, '0') + + if (child instanceof Bucket) { + let shard: ImportResult | undefined + + for await (const subShard of flush(child, blockstore, null, options)) { + shard = subShard + } + + if (shard == null) { + throw new Error('Could not flush sharded directory, no subshard found') + } + + links.push({ + Name: labelPrefix, + Tsize: shard.size, + Hash: shard.cid + }) + childrenSize += shard.size + } else if (typeof child.value.flush === 'function') { + const dir = child.value + let flushedDir + + for await (const entry of dir.flush(blockstore)) { + flushedDir = entry + + yield flushedDir + } + + const label = labelPrefix + child.key + links.push({ + Name: label, + Tsize: flushedDir.size, + Hash: flushedDir.cid + }) + + childrenSize += flushedDir.size // eslint-disable-line @typescript-eslint/restrict-plus-operands + } else { + const value = child.value + + if (value.cid == null) { + continue + } + + const label = labelPrefix + child.key + const size = value.size + + links.push({ + Name: label, + Tsize: size, + Hash: value.cid + }) + childrenSize += size ?? 0 // eslint-disable-line @typescript-eslint/restrict-plus-operands + } + } + + // go-ipfs uses little endian, that's why we have to + // reverse the bit field before storing it + const data = Uint8Array.from(children.bitField().reverse()) + const dir = new UnixFS({ + type: 'hamt-sharded-directory', + data, + fanout: bucket.tableSize(), + hashType: hamtHashCode, + mtime: shardRoot?.mtime, + mode: shardRoot?.mode + }) + + const node = { + Data: dir.marshal(), + Links: links + } + const buffer = encode(prepare(node)) + const cid = await persist(buffer, blockstore, options) + const size = buffer.length + childrenSize + + yield { + cid, + node, + size + } +} diff --git a/packages/unixfs/src/commands/utils/errors.ts b/packages/unixfs/src/commands/utils/errors.ts new file mode 100644 index 00000000..379c9ac6 --- /dev/null +++ b/packages/unixfs/src/commands/utils/errors.ts @@ -0,0 +1,31 @@ +import { HeliaError } from '@helia/interface/errors' + +export class NotUnixFSError extends HeliaError { + constructor (message = 'not a Unixfs node') { + super(message, 'NotUnixFSError', 'ERR_NOT_UNIXFS') + } +} + +export class InvalidPBNodeError extends HeliaError { + constructor (message = 'invalid PBNode') { + super(message, 'InvalidPBNodeError', 'ERR_INVALID_PBNODE') + } +} + +export class UnknownError extends HeliaError { + constructor (message = 'unknown error') { + super(message, 'InvalidPBNodeError', 'ERR_UNKNOWN_ERROR') + } +} + +export class AlreadyExistsError extends HeliaError { + constructor (message = 'path already exists') { + super(message, 'NotUnixFSError', 'ERR_ALREADY_EXISTS') + } +} + +export class DoesNotExistError extends HeliaError { + constructor (message = 'path does not exist') { + super(message, 'NotUnixFSError', 'ERR_DOES_NOT_EXIST') + } +} diff --git a/packages/unixfs/src/commands/utils/hamt-constants.ts b/packages/unixfs/src/commands/utils/hamt-constants.ts new file mode 100644 index 00000000..6f510236 --- /dev/null +++ b/packages/unixfs/src/commands/utils/hamt-constants.ts @@ -0,0 +1,14 @@ +import { murmur3128 } from '@multiformats/murmur3' + +export const hamtHashCode = murmur3128.code +export const hamtBucketBits = 8 + +export async function hamtHashFn (buf: Uint8Array): Promise { + return (await murmur3128.encode(buf)) + // Murmur3 outputs 128 bit but, accidentally, IPFS Go's + // implementation only uses the first 64, so we must do the same + // for parity.. + .subarray(0, 8) + // Invert buffer because that's how Go impl does it + .reverse() +} diff --git a/packages/unixfs/src/commands/utils/hamt-utils.ts b/packages/unixfs/src/commands/utils/hamt-utils.ts new file mode 100644 index 00000000..2cbed1f6 --- /dev/null +++ b/packages/unixfs/src/commands/utils/hamt-utils.ts @@ -0,0 +1,285 @@ +import * as dagPB from '@ipld/dag-pb' +import { + Bucket, + createHAMT +} from 'hamt-sharding' +import { DirSharded } from './dir-sharded.js' +import { logger } from '@libp2p/logger' +import { UnixFS } from 'ipfs-unixfs' +import last from 'it-last' +import { CID } from 'multiformats/cid' +import { + hamtHashCode, + hamtHashFn, + hamtBucketBits +} from './hamt-constants.js' +import type { PBLink, PBNode } from '@ipld/dag-pb/interface' +import { sha256 } from 'multiformats/hashes/sha2' +import type { Blockstore } from 'interface-blockstore' +import type { Mtime } from 'ipfs-unixfs' +import type { Directory } from './cid-to-directory.js' +import type { AbortOptions } from '@libp2p/interfaces' +import type { ImportResult } from 'ipfs-unixfs-importer' + +const log = logger('helia:unixfs:commands:utils:hamt-utils') + +export interface UpdateHamtResult { + node: PBNode + cid: CID + size: number +} + +export const updateHamtDirectory = async (parent: Directory, blockstore: Blockstore, links: PBLink[], bucket: Bucket, options: AbortOptions): Promise => { + if (parent.node.Data == null) { + throw new Error('Could not update HAMT directory because parent had no data') + } + + // update parent with new bit field + const data = Uint8Array.from(bucket._children.bitField().reverse()) + const node = UnixFS.unmarshal(parent.node.Data) + const dir = new UnixFS({ + type: 'hamt-sharded-directory', + data, + fanout: bucket.tableSize(), + hashType: hamtHashCode, + mode: node.mode, + mtime: node.mtime + }) + + parent.node = { + Data: dir.marshal(), + Links: links.sort((a, b) => (a.Name ?? '').localeCompare(b.Name ?? '')) + } + const buf = dagPB.encode(parent.node) + const hash = await sha256.digest(buf) + const cid = CID.create(parent.cid.version, dagPB.code, hash) + + await blockstore.put(cid, buf, options) + + return { + node: parent.node, + cid, + size: links.reduce((sum, link) => sum + (link.Tsize ?? 0), buf.length) + } +} + +export const recreateHamtLevel = async (blockstore: Blockstore, links: PBLink[], rootBucket: Bucket, parentBucket: Bucket, positionAtParent: number, options: AbortOptions): Promise> => { + // recreate this level of the HAMT + const bucket = new Bucket({ + hash: rootBucket._options.hash, + bits: rootBucket._options.bits + }, parentBucket, positionAtParent) + parentBucket._putObjectAt(positionAtParent, bucket) + + await addLinksToHamtBucket(blockstore, links, bucket, rootBucket, options) + + return bucket +} + +export const recreateInitialHamtLevel = async (links: PBLink[]): Promise> => { + const bucket = createHAMT({ + hashFn: hamtHashFn, + bits: hamtBucketBits + }) + + // populate sub bucket but do not recurse as we do not want to pull whole shard in + await Promise.all( + links.map(async link => { + const linkName = (link.Name ?? '') + + if (linkName.length === 2) { + const pos = parseInt(linkName, 16) + + const subBucket = new Bucket({ + hash: bucket._options.hash, + bits: bucket._options.bits + }, bucket, pos) + bucket._putObjectAt(pos, subBucket) + + await Promise.resolve(); return + } + + await bucket.put(linkName.substring(2), { + size: link.Tsize, + cid: link.Hash + }) + }) + ) + + return bucket +} + +export const addLinksToHamtBucket = async (blockstore: Blockstore, links: PBLink[], bucket: Bucket, rootBucket: Bucket, options: AbortOptions): Promise => { + await Promise.all( + links.map(async link => { + const linkName = (link.Name ?? '') + + if (linkName.length === 2) { + log('Populating sub bucket', linkName) + const pos = parseInt(linkName, 16) + const block = await blockstore.get(link.Hash, options) + const node = dagPB.decode(block) + + const subBucket = new Bucket({ + hash: rootBucket._options.hash, + bits: rootBucket._options.bits + }, bucket, pos) + bucket._putObjectAt(pos, subBucket) + + await addLinksToHamtBucket(blockstore, node.Links, subBucket, rootBucket, options) + + await Promise.resolve(); return + } + + await rootBucket.put(linkName.substring(2), { + size: link.Tsize, + cid: link.Hash + }) + }) + ) +} + +export const toPrefix = (position: number): string => { + return position + .toString(16) + .toUpperCase() + .padStart(2, '0') + .substring(0, 2) +} + +export interface HamtPath { + rootBucket: Bucket + path: Array<{ + bucket: Bucket + prefix: string + node?: PBNode + }> +} + +export const generatePath = async (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise => { + // start at the root bucket and descend, loading nodes as we go + const rootBucket = await recreateInitialHamtLevel(parent.node.Links) + const position = await rootBucket._findNewBucketAndPos(name) + + // the path to the root bucket + const path: Array<{ bucket: Bucket, prefix: string, node?: PBNode }> = [{ + bucket: position.bucket, + prefix: toPrefix(position.pos) + }] + let currentBucket = position.bucket + + while (currentBucket !== rootBucket) { + path.push({ + bucket: currentBucket, + prefix: toPrefix(currentBucket._posAtParent) + }) + + // @ts-expect-error - only the root bucket's parent will be undefined + currentBucket = currentBucket._parent + } + + path.reverse() + path[0].node = parent.node + + // load PbNode for each path segment + for (let i = 0; i < path.length; i++) { + const segment = path[i] + + if (segment.node == null) { + throw new Error('Could not generate HAMT path') + } + + // find prefix in links + const link = segment.node.Links + .filter(link => (link.Name ?? '').substring(0, 2) === segment.prefix) + .pop() + + // entry was not in shard + if (link == null) { + // reached bottom of tree, file will be added to the current bucket + log(`Link ${segment.prefix}${name} will be added`) + // return path + continue + } + + // found entry + if (link.Name === `${segment.prefix}${name}`) { + log(`Link ${segment.prefix}${name} will be replaced`) + // file already existed, file will be added to the current bucket + // return path + continue + } + + // found subshard + log(`Found subshard ${segment.prefix}`) + const block = await blockstore.get(link.Hash, options) + const node = dagPB.decode(block) + + // subshard hasn't been loaded, descend to the next level of the HAMT + if (path[i + 1] == null) { + log(`Loaded new subshard ${segment.prefix}`) + + await recreateHamtLevel(blockstore, node.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options) + const position = await rootBucket._findNewBucketAndPos(name) + + // i-- + path.push({ + bucket: position.bucket, + prefix: toPrefix(position.pos), + node + }) + + continue + } + + const nextSegment = path[i + 1] + + // add intermediate links to bucket + await addLinksToHamtBucket(blockstore, node.Links, nextSegment.bucket, rootBucket, options) + + nextSegment.node = node + } + + await rootBucket.put(name, true) + + path.reverse() + + return { + rootBucket, + path + } +} + +export interface CreateShardOptions { + mtime?: Mtime + mode?: number +} + +export const createShard = async (blockstore: Blockstore, contents: Array<{ name: string, size: number, cid: CID }>, options: CreateShardOptions = {}): Promise => { + const shard = new DirSharded({ + root: true, + dir: true, + parent: undefined, + parentKey: undefined, + path: '', + dirty: true, + flat: false, + mtime: options.mtime, + mode: options.mode + }, options) + + for (let i = 0; i < contents.length; i++) { + await shard._bucket.put(contents[i].name, { + size: contents[i].size, + cid: contents[i].cid + }) + } + + const res = await last(shard.flush(blockstore)) + + if (res == null) { + throw new Error('Flushing shard yielded no result') + } + + return res +} diff --git a/packages/unixfs/src/commands/utils/persist.ts b/packages/unixfs/src/commands/utils/persist.ts new file mode 100644 index 00000000..1e287c6f --- /dev/null +++ b/packages/unixfs/src/commands/utils/persist.ts @@ -0,0 +1,22 @@ +import { CID, Version } from 'multiformats/cid' +import * as dagPB from '@ipld/dag-pb' +import { sha256 } from 'multiformats/hashes/sha2' +import type { BlockCodec } from 'multiformats/codecs/interface' +import type { AbortOptions } from '@libp2p/interfaces' +import type { Blockstore } from 'interface-blockstore' + +export interface PersistOptions extends AbortOptions { + codec?: BlockCodec + cidVersion?: Version +} + +export const persist = async (buffer: Uint8Array, blockstore: Blockstore, options: PersistOptions = {}): Promise => { + const multihash = await sha256.digest(buffer) + const cid = CID.create(options.cidVersion ?? 1, dagPB.code, multihash) + + await blockstore.put(cid, buffer, { + signal: options.signal + }) + + return cid +} diff --git a/packages/unixfs/src/commands/utils/remove-link.ts b/packages/unixfs/src/commands/utils/remove-link.ts new file mode 100644 index 00000000..b2608690 --- /dev/null +++ b/packages/unixfs/src/commands/utils/remove-link.ts @@ -0,0 +1,151 @@ + +import * as dagPB from '@ipld/dag-pb' +import { CID } from 'multiformats/cid' +import { logger } from '@libp2p/logger' +import { UnixFS } from 'ipfs-unixfs' +import { + generatePath, + updateHamtDirectory, + UpdateHamtResult +} from './hamt-utils.js' +import type { PBNode, PBLink } from '@ipld/dag-pb' +import type { Blockstore } from 'interface-blockstore' +import { sha256 } from 'multiformats/hashes/sha2' +import type { Bucket } from 'hamt-sharding' +import type { Directory } from './cid-to-directory.js' +import type { AbortOptions } from '@libp2p/interfaces' +import { InvalidPBNodeError } from './errors.js' +import { InvalidParametersError } from '@helia/interface/errors' + +const log = logger('helia:unixfs:utils:remove-link') + +export interface RemoveLinkResult { + node: PBNode + cid: CID +} + +export async function removeLink (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise { + if (parent.node.Data == null) { + throw new InvalidPBNodeError('Parent node had no data') + } + + const meta = UnixFS.unmarshal(parent.node.Data) + + if (meta.type === 'hamt-sharded-directory') { + log(`Removing ${name} from sharded directory`) + + return await removeFromShardedDirectory(parent, name, blockstore, options) + } + + log(`Removing link ${name} regular directory`) + + return await removeFromDirectory(parent, name, blockstore, options) +} + +const removeFromDirectory = async (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise => { + // Remove existing link if it exists + parent.node.Links = parent.node.Links.filter((link) => { + return link.Name !== name + }) + + const parentBlock = dagPB.encode(parent.node) + const hash = await sha256.digest(parentBlock) + const parentCid = CID.create(parent.cid.version, dagPB.code, hash) + + await blockstore.put(parentCid, parentBlock, options) + + log(`Updated regular directory ${parentCid}`) + + return { + node: parent.node, + cid: parentCid + } +} + +const removeFromShardedDirectory = async (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise => { + const { + rootBucket, path + } = await generatePath(parent, name, blockstore, options) + + await rootBucket.del(name) + + const { + node + } = await updateShard(parent, blockstore, path, name, options) + + return await updateHamtDirectory(parent, blockstore, node.Links, rootBucket, options) +} + +const updateShard = async (parent: Directory, blockstore: Blockstore, positions: Array<{ bucket: Bucket, prefix: string, node?: PBNode }>, name: string, options: AbortOptions): Promise<{ node: PBNode, cid: CID, size: number }> => { + const last = positions.pop() + + if (last == null) { + throw new InvalidParametersError('Could not find parent') + } + + const { + bucket, + prefix, + node + } = last + + if (node == null) { + throw new InvalidParametersError('Could not find parent') + } + + const link = node.Links + .find(link => (link.Name ?? '').substring(0, 2) === prefix) + + if (link == null) { + throw new InvalidParametersError(`No link found with prefix ${prefix} for file ${name}`) + } + + if (link.Name === `${prefix}${name}`) { + log(`Removing existing link ${link.Name}`) + + const links = node.Links.filter((nodeLink) => { + return nodeLink.Name !== link.Name + }) + + await bucket.del(name) + + parent.node = node + + return await updateHamtDirectory(parent, blockstore, links, bucket, options) + } + + log(`Descending into sub-shard ${link.Name} for ${prefix}${name}`) + + const result = await updateShard(parent, blockstore, positions, name, options) + + const child: Required = { + Hash: result.cid, + Tsize: result.size, + Name: prefix + } + + if (result.node.Links.length === 1) { + log(`Removing subshard for ${prefix}`) + + // convert shard back to normal dir + const link = result.node.Links[0] + + child.Name = `${prefix}${(link.Name ?? '').substring(2)}` + child.Hash = link.Hash + child.Tsize = link.Tsize ?? 0 + } + + log(`Updating shard ${prefix} with name ${child.Name}`) + + return await updateShardParent(parent, child, prefix, blockstore, bucket, options) +} + +const updateShardParent = async (parent: Directory, child: Required, oldName: string, blockstore: Blockstore, bucket: Bucket, options: AbortOptions): Promise => { + // Remove existing link if it exists + const parentLinks = parent.node.Links.filter((link) => { + return link.Name !== oldName + }) + parentLinks.push(child) + + return await updateHamtDirectory(parent, blockstore, parentLinks, bucket, options) +} diff --git a/packages/unixfs/src/commands/utils/resolve.ts b/packages/unixfs/src/commands/utils/resolve.ts new file mode 100644 index 00000000..ef5a76bc --- /dev/null +++ b/packages/unixfs/src/commands/utils/resolve.ts @@ -0,0 +1,130 @@ +import type { CID } from 'multiformats/cid' +import { Blockstore, exporter } from 'ipfs-unixfs-exporter' +import type { AbortOptions } from '@libp2p/interfaces' +import { InvalidParametersError } from '@helia/interface/errors' +import { logger } from '@libp2p/logger' +import { DoesNotExistError } from './errors.js' +import { addLink } from './add-link.js' +import { cidToDirectory } from './cid-to-directory.js' +import { cidToPBLink } from './cid-to-pblink.js' + +const log = logger('helia:unixfs:components:utils:add-link') + +export interface Segment { + name: string + cid: CID + size: number +} + +export interface ResolveResult { + /** + * The CID at the end of the path + */ + cid: CID + + path?: string + + /** + * If present, these are the CIDs and path segments that were traversed through to reach the final CID + * + * If not present, there was no path passed or the path was an empty string + */ + segments?: Segment[] +} + +export async function resolve (cid: CID, path: string | undefined, blockstore: Blockstore, options: AbortOptions): Promise { + log('resolve "%s" under %c', path, cid) + + if (path == null || path === '') { + return { cid } + } + + const parts = path.split('/').filter(Boolean) + const segments: Segment[] = [{ + name: '', + cid, + size: 0 + }] + + for (let i = 0; i < parts.length; i++) { + const part = parts[i] + const result = await exporter(cid, blockstore, options) + + if (result.type === 'file') { + if (i < parts.length - 1) { + throw new InvalidParametersError('Path was invalid') + } + + cid = result.cid + } else if (result.type === 'directory') { + let dirCid: CID | undefined + + for await (const entry of result.content()) { + if (entry.name === part) { + dirCid = entry.cid + } + } + + if (dirCid == null) { + throw new DoesNotExistError('Could not find path in directory') + } + + cid = dirCid + + segments.push({ + name: part, + cid, + size: result.size + }) + } else { + throw new InvalidParametersError('Could not resolve path') + } + } + + return { + cid, + path, + segments + } +} + +/** + * Where we have descended into a DAG to update a child node, ascend up the DAG creating + * new hashes and blocks for the changed content + */ +export async function updatePathCids (cid: CID, result: ResolveResult, blockstore: Blockstore, options: AbortOptions): Promise { + if (result.segments == null || result.segments.length === 0) { + return cid + } + + let child = result.segments.pop() + + if (child == null) { + throw new Error('Insufficient segments') + } + + child.cid = cid + + result.segments.reverse() + + for (const parent of result.segments) { + const [ + directory, + pblink + ] = await Promise.all([ + cidToDirectory(parent.cid, blockstore, options), + cidToPBLink(child.cid, child.name, blockstore, options) + ]) + + const result = await addLink(directory, pblink, blockstore, { + ...options, + allowOverwriting: true + }) + + cid = result.cid + parent.cid = cid + child = parent + } + + return cid +} diff --git a/packages/unixfs/src/index.ts b/packages/unixfs/src/index.ts index d86665b2..832fd159 100644 --- a/packages/unixfs/src/index.ts +++ b/packages/unixfs/src/index.ts @@ -1,61 +1,174 @@ -import type { CatOptions, Helia } from '@helia/interface' -import { exporter } from 'ipfs-unixfs-exporter' -import { ImportCandidate, importer, ImportResult, UserImporterOptions } from 'ipfs-unixfs-importer' -import type { CID } from 'multiformats' +import type { CID, Version } from 'multiformats/cid' import type { Blockstore } from 'interface-blockstore' -import { NotAFileError, NoContentError } from '@helia/interface/errors' -import type { ReadableStream } from 'node:stream/web' +import type { AbortOptions } from '@libp2p/interfaces' +import type { ImportCandidate, ImportResult, UserImporterOptions } from 'ipfs-unixfs-importer' +import { add, addStream } from './commands/add.js' +import { cat } from './commands/cat.js' +import { mkdir } from './commands/mkdir.js' +import type { Mtime } from 'ipfs-unixfs' +import { cp } from './commands/cp.js' +import { rm } from './commands/rm.js' +import { stat } from './commands/stat.js' +import { touch } from './commands/touch.js' +import { chmod } from './commands/chmod.js' +import type { UnixFSEntry } from 'ipfs-unixfs-exporter' +import { ls } from './commands/ls.js' export interface UnixFSComponents { blockstore: Blockstore } -class UnixFS { +export interface CatOptions extends AbortOptions { + offset?: number + length?: number + path?: string +} + +export interface ChmodOptions extends AbortOptions { + recursive: boolean + path?: string +} + +export interface CpOptions extends AbortOptions { + force: boolean +} + +export interface LsOptions extends AbortOptions { + path?: string + offset?: number + length?: number +} + +export interface MkdirOptions extends AbortOptions { + cidVersion: Version + force: boolean + mode?: number + mtime?: Mtime +} + +export interface RmOptions extends AbortOptions { + +} + +export interface StatOptions extends AbortOptions { + path?: string +} + +export interface UnixFSStats { + /** + * The file or directory CID + */ + cid: CID + + /** + * The file or directory mode + */ + mode?: number + + /** + * The file or directory mtime + */ + mtime?: Mtime + + /** + * The size of the file in bytes + */ + fileSize: number + + /** + * The size of the DAG that holds the file in bytes + */ + dagSize: number + + /** + * How much of the file is in the local block store + */ + localFileSize: number + + /** + * How much of the DAG that holds the file is in the local blockstore + */ + localDagSize: number + + /** + * How many blocks make up the DAG - nb. this will only be accurate + * if all blocks are present in the local blockstore + */ + blocks: number + + /** + * The type of file + */ + type: 'file' | 'directory' | 'raw' +} + +export interface TouchOptions extends AbortOptions { + mtime?: Mtime + path?: string + recursive: boolean +} + +export interface UnixFS { + add: (source: Uint8Array | Iterator | AsyncIterator | ImportCandidate, options?: Partial) => Promise + addStream: (source: Iterable | AsyncIterable, options?: Partial) => AsyncGenerator + cat: (cid: CID, options?: Partial) => AsyncIterable + chmod: (source: CID, mode: number, options?: Partial) => Promise + cp: (source: CID, target: CID, name: string, options?: Partial) => Promise + ls: (cid: CID, options?: Partial) => AsyncIterable + mkdir: (cid: CID, dirname: string, options?: Partial) => Promise + rm: (cid: CID, path: string, options?: Partial) => Promise + stat: (cid: CID, options?: Partial) => Promise + touch: (cid: CID, options?: Partial) => Promise +} + +class DefaultUnixFS implements UnixFS { private readonly components: UnixFSComponents constructor (components: UnixFSComponents) { this.components = components } - async * add (source: AsyncIterable | Iterable | ImportCandidate, options?: UserImporterOptions): AsyncGenerator { - yield * importer(source, this.components.blockstore, options) + async add (source: Uint8Array | Iterator | AsyncIterator | ImportCandidate, options: Partial = {}): Promise { + return await add(source, this.components.blockstore, options) + } + + async * addStream (source: Iterable | AsyncIterable, options: Partial = {}): AsyncGenerator { + yield * addStream(source, this.components.blockstore, options) + } + + async * cat (cid: CID, options: Partial = {}): AsyncIterable { + yield * cat(cid, this.components.blockstore, options) + } + + async chmod (source: CID, mode: number, options: Partial = {}): Promise { + return await chmod(source, mode, this.components.blockstore, options) } - cat (cid: CID, options: CatOptions = {}): ReadableStream { - const blockstore = this.components.blockstore + async cp (source: CID, target: CID, name: string, options: Partial = {}): Promise { + return await cp(source, target, name, this.components.blockstore, options) + } - const byteSource: UnderlyingByteSource = { - type: 'bytes', - async start (controller) { - const result = await exporter(cid, blockstore) + async * ls (cid: CID, options: Partial = {}): AsyncIterable { + yield * ls(cid, this.components.blockstore, options) + } - if (result.type !== 'file' && result.type !== 'raw') { - throw new NotAFileError() - } + async mkdir (cid: CID, dirname: string, options: Partial = {}): Promise { + return await mkdir(cid, dirname, this.components.blockstore, options) + } - if (result.content == null) { - throw new NoContentError() - } + async rm (cid: CID, path: string, options: Partial = {}): Promise { + return await rm(cid, path, this.components.blockstore, options) + } - try { - for await (const buf of result.content({ - offset: options.offset, - length: options.length - })) { - // TODO: backpressure? - controller.enqueue(buf) - } - } finally { - controller.close() - } - } - } + async stat (cid: CID, options: Partial = {}): Promise { + return await stat(cid, this.components.blockstore, options) + } - // @ts-expect-error types are broken? - return new ReadableStream(byteSource) + async touch (cid: CID, options: Partial = {}): Promise { + return await touch(cid, this.components.blockstore, options) } } -export function unixfs (helia: Helia): UnixFS { - return new UnixFS(helia) +export function unixfs (helia: { blockstore: Blockstore }): UnixFS { + return new DefaultUnixFS(helia) } diff --git a/packages/unixfs/test/cat.spec.ts b/packages/unixfs/test/cat.spec.ts new file mode 100644 index 00000000..4aa97216 --- /dev/null +++ b/packages/unixfs/test/cat.spec.ts @@ -0,0 +1,87 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import type { CID } from 'multiformats/cid' +import type { Blockstore } from 'interface-blockstore' +import { unixfs, UnixFS } from '../src/index.js' +import { MemoryBlockstore } from 'blockstore-core' +import toBuffer from 'it-to-buffer' +import drain from 'it-drain' + +const smallFile = Uint8Array.from(new Array(13).fill(0).map(() => Math.random() * 100)) + +describe('cat', () => { + let blockstore: Blockstore + let fs: UnixFS + let emptyDirCid: CID + + beforeEach(async () => { + blockstore = new MemoryBlockstore() + + fs = unixfs({ blockstore }) + + emptyDirCid = await fs.add({ path: 'empty' }) + }) + + it('reads a small file', async () => { + const cid = await fs.add(smallFile) + const bytes = await toBuffer(fs.cat(cid)) + + expect(bytes).to.equalBytes(smallFile) + }) + + it('reads a file with an offset', async () => { + const offset = 10 + const cid = await fs.add(smallFile) + const bytes = await toBuffer(fs.cat(cid, { + offset + })) + + expect(bytes).to.equalBytes(smallFile.subarray(offset)) + }) + + it('reads a file with a length', async () => { + const length = 10 + const cid = await fs.add(smallFile) + const bytes = await toBuffer(fs.cat(cid, { + length + })) + + expect(bytes).to.equalBytes(smallFile.subarray(0, length)) + }) + + it('reads a file with an offset and a length', async () => { + const offset = 2 + const length = 5 + const cid = await fs.add(smallFile) + const bytes = await toBuffer(fs.cat(cid, { + offset, + length + })) + + expect(bytes).to.equalBytes(smallFile.subarray(offset, offset + length)) + }) + + it('refuses to read a directory', async () => { + await expect(drain(fs.cat(emptyDirCid))).to.eventually.be.rejected + .with.property('code', 'ERR_NOT_FILE') + }) + +/* + describe('with sharding', () => { + it('reads file from inside a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + const filePath = `${shardedDirPath}/file-${Math.random()}.txt` + const content = Uint8Array.from([0, 1, 2, 3, 4]) + + await ipfs.files.write(filePath, content, { + create: true + }) + + const bytes = uint8ArrayConcat(await all(ipfs.files.read(filePath))) + + expect(bytes).to.deep.equal(content) + }) + }) + */ +}) diff --git a/packages/unixfs/test/chmod.spec.ts b/packages/unixfs/test/chmod.spec.ts new file mode 100644 index 00000000..b787ff5e --- /dev/null +++ b/packages/unixfs/test/chmod.spec.ts @@ -0,0 +1,100 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import type { Blockstore } from 'interface-blockstore' +import { MemoryBlockstore } from 'blockstore-core' +import { UnixFS, unixfs } from '../src/index.js' +import type { CID } from 'multiformats/cid' + +const smallFile = Uint8Array.from(new Array(13).fill(0).map(() => Math.random() * 100)) + +describe('chmod', () => { + let blockstore: Blockstore + let fs: UnixFS + let emptyDirCid: CID + + beforeEach(async () => { + blockstore = new MemoryBlockstore() + + fs = unixfs({ blockstore }) + emptyDirCid = await fs.add({ path: 'empty' }) + }) + + it('should update the mode for a raw node', async () => { + const cid = await fs.add(smallFile) + const originalMode = (await fs.stat(cid)).mode + const updatedCid = await fs.chmod(cid, 0o777) + + const updatedMode = (await fs.stat(updatedCid)).mode + expect(updatedMode).to.not.equal(originalMode) + expect(updatedMode).to.equal(0o777) + }) + + it('should update the mode for a file', async () => { + const cid = await fs.add(smallFile, { + rawLeaves: false + }) + const originalMode = (await fs.stat(cid)).mode + const updatedCid = await fs.chmod(cid, 0o777) + + const updatedMode = (await fs.stat(updatedCid)).mode + expect(updatedMode).to.not.equal(originalMode) + expect(updatedMode).to.equal(0o777) + }) + + it('should update the mode for a directory', async () => { + const path = `foo-${Math.random()}` + + const dirCid = await fs.mkdir(emptyDirCid, path) + const originalMode = (await fs.stat(dirCid, { + path + })).mode + const updatedCid = await fs.chmod(dirCid, 0o777, { + path + }) + + const updatedMode = (await fs.stat(updatedCid, { + path + })).mode + expect(updatedMode).to.not.equal(originalMode) + expect(updatedMode).to.equal(0o777) + }) + + it('should update mode recursively', async () => { + const path = 'path' + const cid = await fs.add(smallFile) + const dirCid = await fs.cp(cid, emptyDirCid, path) + const originalMode = (await fs.stat(dirCid, { + path + })).mode + const updatedCid = await fs.chmod(dirCid, 0o777, { + recursive: true + }) + + const updatedMode = (await fs.stat(updatedCid, { + path + })).mode + expect(updatedMode).to.not.equal(originalMode) + expect(updatedMode).to.equal(0o777) + }) + +/* + it('should update the mode for a hamt-sharded-directory', async () => { + const path = `/foo-${Math.random()}` + + await ipfs.files.mkdir(path) + await ipfs.files.write(`${path}/foo.txt`, uint8ArrayFromString('Hello world'), { + create: true, + shardSplitThreshold: 0 + }) + const originalMode = (await ipfs.files.stat(path)).mode + await ipfs.files.chmod(path, '0777', { + flush: true + }) + + const updatedMode = (await ipfs.files.stat(path)).mode + expect(updatedMode).to.not.equal(originalMode) + expect(updatedMode).to.equal(parseInt('0777', 8)) + }) + */ +}) diff --git a/packages/unixfs/test/cp.spec.ts b/packages/unixfs/test/cp.spec.ts new file mode 100644 index 00000000..b8f42d75 --- /dev/null +++ b/packages/unixfs/test/cp.spec.ts @@ -0,0 +1,201 @@ +/* eslint-env mocha */ + +import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' +import { expect } from 'aegir/chai' +import { identity } from 'multiformats/hashes/identity' +import { CID } from 'multiformats/cid' +import type { Blockstore } from 'interface-blockstore' +import { unixfs, UnixFS } from '../src/index.js' +import { MemoryBlockstore } from 'blockstore-core' +import toBuffer from 'it-to-buffer' + +describe('cp', () => { + let blockstore: Blockstore + let fs: UnixFS + let emptyDirCid: CID + + beforeEach(async () => { + blockstore = new MemoryBlockstore() + + fs = unixfs({ blockstore }) + emptyDirCid = await fs.add({ path: 'empty' }) + }) + + it('refuses to copy files without a source', async () => { + // @ts-expect-error invalid args + await expect(fs.cp()).to.eventually.be.rejected.with('Please supply at least one source') + }) + + it('refuses to copy files without a source, even with options', async () => { + // @ts-expect-error invalid args + await expect(fs.cp({})).to.eventually.be.rejected.with('Please supply at least one source') + }) + + it('refuses to copy files without a destination', async () => { + // @ts-expect-error invalid args + await expect(fs.cp('/source')).to.eventually.be.rejected.with('Please supply at least one source') + }) + + it('refuses to copy files without a destination, even with options', async () => { + // @ts-expect-error invalid args + await expect(fs.cp('/source', {})).to.eventually.be.rejected.with('Please supply at least one source') + }) + + it('refuses to copy files to an unreadable node', async () => { + const hash = identity.digest(uint8ArrayFromString('derp')) + const source = await fs.add(Uint8Array.from([0, 1, 3, 4])) + const target = CID.createV1(identity.code, hash) + + await expect(fs.cp(source, target, 'foo')).to.eventually.be.rejected + .with.property('code', 'ERR_NOT_DIRECTORY') + }) + + it('refuses to copy files from an unreadable node', async () => { + const hash = identity.digest(uint8ArrayFromString('derp')) + const source = CID.createV1(identity.code, hash) + + await expect(fs.cp(source, emptyDirCid, 'foo')).to.eventually.be.rejected + .with.property('code', 'ERR_NOT_UNIXFS') + }) + + it('refuses to copy files to an existing file', async () => { + const path = 'path' + const source = await fs.add(Uint8Array.from([0, 1, 3, 4])) + const target = await fs.cp(source, emptyDirCid, path) + + await expect(fs.cp(source, target, path)).to.eventually.be.rejected + .with.property('code', 'ERR_ALREADY_EXISTS') + }) + + it('copies a file to new location', async () => { + const data = Uint8Array.from([0, 1, 3, 4]) + const path = 'path' + const source = await fs.add(data) + const dirCid = await fs.cp(source, emptyDirCid, path) + + const bytes = await toBuffer(fs.cat(dirCid, { + path + })) + + expect(bytes).to.deep.equal(data) + }) + + it('copies directories', async () => { + const path = 'path' + const dirCid = await fs.cp(emptyDirCid, emptyDirCid, path) + + await expect(fs.stat(dirCid, { + path + })).to.eventually.include({ + type: 'directory' + }) + }) + +/* + describe('with sharding', () => { + it('copies a sharded directory to a normal directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + + const normalDir = `dir-${Math.random()}` + const normalDirPath = `/${normalDir}` + + await ipfs.files.mkdir(normalDirPath) + + await ipfs.files.cp(shardedDirPath, normalDirPath) + + const finalShardedDirPath = `${normalDirPath}${shardedDirPath}` + + // should still be a sharded directory + await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory') + + const files = await all(ipfs.files.ls(finalShardedDirPath)) + + expect(files.length).to.be.ok() + }) + + it('copies a normal directory to a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + + const normalDir = `dir-${Math.random()}` + const normalDirPath = `/${normalDir}` + + await ipfs.files.mkdir(normalDirPath) + + await ipfs.files.cp(normalDirPath, shardedDirPath) + + const finalDirPath = `${shardedDirPath}${normalDirPath}` + + // should still be a sharded directory + await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory') + expect((await ipfs.files.stat(finalDirPath)).type).to.equal('directory') + }) + + it('copies a file from a normal directory to a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + + const file = `file-${Math.random()}.txt` + const filePath = `/${file}` + const finalFilePath = `${shardedDirPath}/${file}` + + await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), { + create: true + }) + + await ipfs.files.cp(filePath, finalFilePath) + + // should still be a sharded directory + await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory') + expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file') + }) + + it('copies a file from a sharded directory to a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + const othershardedDirPath = await createShardedDirectory(ipfs) + + const file = `file-${Math.random()}.txt` + const filePath = `${shardedDirPath}/${file}` + const finalFilePath = `${othershardedDirPath}/${file}` + + await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), { + create: true + }) + + await ipfs.files.cp(filePath, finalFilePath) + + // should still be a sharded directory + await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory') + await expect(isShardAtPath(othershardedDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(othershardedDirPath)).type).to.equal('directory') + expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file') + }) + + it('copies a file from a sharded directory to a normal directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + const dir = `dir-${Math.random()}` + const dirPath = `/${dir}` + + const file = `file-${Math.random()}.txt` + const filePath = `${shardedDirPath}/${file}` + const finalFilePath = `${dirPath}/${file}` + + await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), { + create: true + }) + + await ipfs.files.mkdir(dirPath) + + await ipfs.files.cp(filePath, finalFilePath) + + // should still be a sharded directory + await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory') + expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') + expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file') + }) + }) + */ +}) diff --git a/packages/unixfs/test/index.spec.ts b/packages/unixfs/test/index.spec.ts deleted file mode 100644 index de84dfd2..00000000 --- a/packages/unixfs/test/index.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import '../src/index.js' - -describe('unixfs', () => { - it('should add a Uint8Array', async () => { - - }) -}) diff --git a/packages/unixfs/test/ls.spec.ts b/packages/unixfs/test/ls.spec.ts new file mode 100644 index 00000000..a8fb1160 --- /dev/null +++ b/packages/unixfs/test/ls.spec.ts @@ -0,0 +1,119 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import type { CID } from 'multiformats/cid' +import all from 'it-all' +import type { Blockstore } from 'interface-blockstore' +import { unixfs, UnixFS } from '../src/index.js' +import { MemoryBlockstore } from 'blockstore-core' + +describe('ls', () => { + let blockstore: Blockstore + let fs: UnixFS + let emptyDirCid: CID + + beforeEach(async () => { + blockstore = new MemoryBlockstore() + + fs = unixfs({ blockstore }) + emptyDirCid = await fs.add({ path: 'empty' }) + }) + + it('should require a path', async () => { + // @ts-expect-error invalid args + await expect(all(fs.ls())).to.eventually.be.rejected() + }) + + it('lists files in a directory', async () => { + const path = 'path' + const data = Uint8Array.from([0, 1, 2, 3]) + const fileCid = await fs.add(data) + const dirCid = await fs.cp(fileCid, emptyDirCid, path) + const files = await all(fs.ls(dirCid)) + + expect(files).to.have.lengthOf(1).and.to.containSubset([{ + cid: fileCid, + name: path, + size: data.byteLength, + type: 'raw' + }]) + }) + + it('lists a file', async () => { + const path = 'path' + const data = Uint8Array.from([0, 1, 2, 3]) + const fileCid = await fs.add(data, { + rawLeaves: false + }) + const dirCid = await fs.cp(fileCid, emptyDirCid, path) + const files = await all(fs.ls(dirCid, { + path + })) + + expect(files).to.have.lengthOf(1).and.to.containSubset([{ + cid: fileCid, + size: data.byteLength, + type: 'file' + }]) + }) + + it('lists a raw node', async () => { + const path = 'path' + const data = Uint8Array.from([0, 1, 2, 3]) + const fileCid = await fs.add(data) + const dirCid = await fs.cp(fileCid, emptyDirCid, path) + const files = await all(fs.ls(dirCid, { + path + })) + + expect(files).to.have.lengthOf(1).and.to.containSubset([{ + cid: fileCid, + size: data.byteLength, + type: 'raw' + }]) + }) + + /* + describe('with sharding', () => { + it('lists a sharded directory contents', async () => { + const fileCount = 1001 + const dirPath = await createShardedDirectory(ipfs, fileCount) + const files = await all(ipfs.files.ls(dirPath)) + + expect(files.length).to.equal(fileCount) + + files.forEach(file => { + // should be a file + expect(file.type).to.equal('file') + }) + }) + + it('lists a file inside a sharded directory directly', async () => { + const dirPath = await createShardedDirectory(ipfs) + const files = await all(ipfs.files.ls(dirPath)) + const filePath = `${dirPath}/${files[0].name}` + + // should be able to ls new file directly + const file = await all(ipfs.files.ls(filePath)) + + expect(file).to.have.lengthOf(1).and.to.containSubset([files[0]]) + }) + + it('lists the contents of a directory inside a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + const dirPath = `${shardedDirPath}/subdir-${Math.random()}` + const fileName = `small-file-${Math.random()}.txt` + + await ipfs.files.mkdir(`${dirPath}`) + await ipfs.files.write(`${dirPath}/${fileName}`, Uint8Array.from([0, 1, 2, 3]), { + create: true + }) + + const files = await all(ipfs.files.ls(dirPath)) + + expect(files.length).to.equal(1) + expect(files.filter(file => file.name === fileName)).to.be.ok() + }) + }) + */ +}) diff --git a/packages/unixfs/test/mkdir.spec.ts b/packages/unixfs/test/mkdir.spec.ts new file mode 100644 index 00000000..843bc0fb --- /dev/null +++ b/packages/unixfs/test/mkdir.spec.ts @@ -0,0 +1,118 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import all from 'it-all' +import type { Blockstore } from 'interface-blockstore' +import { unixfs, UnixFS } from '../src/index.js' +import { MemoryBlockstore } from 'blockstore-core' +import type { CID } from 'multiformats/cid' +import type { Mtime } from 'ipfs-unixfs' + +describe('mkdir', () => { + let blockstore: Blockstore + let fs: UnixFS + let emptyDirCid: CID + let emptyDirCidV0: CID + + beforeEach(async () => { + blockstore = new MemoryBlockstore() + + fs = unixfs({ blockstore }) + + emptyDirCid = await fs.add({ path: 'empty' }) + emptyDirCidV0 = await fs.add({ path: 'empty' }, { + cidVersion: 0 + }) + }) + + async function testMode (mode: number | undefined, expectedMode: number): Promise { + const path = 'sub-directory' + const dirCid = await fs.mkdir(emptyDirCid, path, { + mode + }) + + await expect(fs.stat(dirCid, { + path + })).to.eventually.have.property('mode', expectedMode) + } + + async function testMtime (mtime: Mtime, expectedMtime: Mtime): Promise { + const path = 'sub-directory' + const dirCid = await fs.mkdir(emptyDirCid, path, { + mtime + }) + + await expect(fs.stat(dirCid, { + path + })).to.eventually.have.deep.property('mtime', expectedMtime) + } + + it('requires a directory', async () => { + // @ts-expect-error not enough arguments + await expect(fs.mkdir(emptyDirCid)).to.eventually.be.rejected() + }) + + it('creates a directory', async () => { + const path = 'foo' + const dirCid = await fs.mkdir(emptyDirCid, path) + + const stats = await fs.stat(dirCid) + expect(stats.type).to.equal('directory') + + const files = await all(fs.ls(dirCid)) + + expect(files.length).to.equal(1) + expect(files).to.have.nested.property('[0].name', path) + }) + + it('refuses to create a directory that already exists', async () => { + const path = 'qux' + const dirCid = await fs.mkdir(emptyDirCid, path) + + await expect(fs.mkdir(dirCid, path)).to.eventually.be.rejected() + .with.property('code', 'ERR_ALREADY_EXISTS') + }) + + it('creates a nested directory with a different CID version to the parent', async () => { + const subDirectory = 'sub-dir' + + expect(emptyDirCidV0).to.have.property('version', 0) + + const dirCid = await fs.mkdir(emptyDirCidV0, subDirectory, { + cidVersion: 1 + }) + + await expect(fs.stat(dirCid)).to.eventually.have.nested.property('cid.version', 0) + await expect(fs.stat(dirCid, { + path: subDirectory + })).to.eventually.have.nested.property('cid.version', 1) + }) + + it('should make directory and have default mode', async function () { + await testMode(undefined, parseInt('0755', 8)) + }) + + it('should make directory and specify mtime as { nsecs, secs }', async function () { + const mtime = { + secs: 5, + nsecs: 0 + } + await testMtime(mtime, mtime) + }) +/* + describe('with sharding', () => { + it('makes a directory inside a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + const dirPath = `${shardedDirPath}/subdir-${Math.random()}` + + await ipfs.files.mkdir(`${dirPath}`) + + await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() + await expect(ipfs.files.stat(shardedDirPath)).to.eventually.have.property('type', 'directory') + + await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.false() + await expect(ipfs.files.stat(dirPath)).to.eventually.have.property('type', 'directory') + }) + }) + */ +}) diff --git a/packages/unixfs/test/rm.spec.ts b/packages/unixfs/test/rm.spec.ts new file mode 100644 index 00000000..fdd3d246 --- /dev/null +++ b/packages/unixfs/test/rm.spec.ts @@ -0,0 +1,177 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import type { Blockstore } from 'interface-blockstore' +import { unixfs, UnixFS } from '../src/index.js' +import { MemoryBlockstore } from 'blockstore-core' +import type { CID } from 'multiformats/cid' + +const smallFile = Uint8Array.from(new Array(13).fill(0).map(() => Math.random() * 100)) + +describe('rm', () => { + let blockstore: Blockstore + let fs: UnixFS + let emptyDirCid: CID + + beforeEach(async () => { + blockstore = new MemoryBlockstore() + + fs = unixfs({ blockstore }) + + emptyDirCid = await fs.add({ path: 'empty' }) + }) + + it('refuses to remove files without arguments', async () => { + // @ts-expect-error invalid args + await expect(fs.rm()).to.eventually.be.rejected() + }) + + it('removes a file', async () => { + const path = 'foo' + const fileCid = await fs.add(smallFile) + const dirCid = await fs.cp(fileCid, emptyDirCid, path) + const updatedDirCid = await fs.rm(dirCid, path) + + await expect(fs.stat(updatedDirCid, { + path + })).to.eventually.be.rejected + .with.property('code', 'ERR_DOES_NOT_EXIST') + }) + + it('removes a directory', async () => { + const path = 'foo' + const dirCid = await fs.cp(emptyDirCid, emptyDirCid, path) + const updatedDirCid = await fs.rm(dirCid, path) + + await expect(fs.stat(updatedDirCid, { + path + })).to.eventually.be.rejected + .with.property('code', 'ERR_DOES_NOT_EXIST') + }) +/* + describe('with sharding', () => { + it('recursively removes a sharded directory inside a normal directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + const dir = `dir-${Math.random()}` + const dirPath = `/${dir}` + + await ipfs.files.mkdir(dirPath) + + await ipfs.files.mv(shardedDirPath, dirPath) + + const finalShardedDirPath = `${dirPath}${shardedDirPath}` + + await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory') + + await ipfs.files.rm(dirPath, { + recursive: true + }) + + await expect(ipfs.files.stat(dirPath)).to.eventually.be.rejectedWith(/does not exist/) + await expect(ipfs.files.stat(shardedDirPath)).to.eventually.be.rejectedWith(/does not exist/) + }) + + it('recursively removes a sharded directory inside a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + const otherDirPath = await createShardedDirectory(ipfs) + + await ipfs.files.mv(shardedDirPath, otherDirPath) + + const finalShardedDirPath = `${otherDirPath}${shardedDirPath}` + + await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory') + await expect(isShardAtPath(otherDirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(otherDirPath)).type).to.equal('directory') + + await ipfs.files.rm(otherDirPath, { + recursive: true + }) + + await expect(ipfs.files.stat(otherDirPath)).to.eventually.be.rejectedWith(/does not exist/) + await expect(ipfs.files.stat(finalShardedDirPath)).to.eventually.be.rejectedWith(/does not exist/) + }) + }) + + it('results in the same hash as a sharded directory created by the importer when removing a file', async function () { + const { + nextFile, + dirWithAllFiles, + dirWithSomeFiles, + dirPath + } = await createTwoShards(ipfs, 1001) + + await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath) + + await ipfs.files.rm(nextFile.path) + + const stats = await ipfs.files.stat(dirPath) + const updatedDirCid = stats.cid + + await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') + expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString()) + }) + + it('results in the same hash as a sharded directory created by the importer when removing a subshard', async function () { + const { + nextFile, + dirWithAllFiles, + dirWithSomeFiles, + dirPath + } = await createTwoShards(ipfs, 31) + + await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath) + + await ipfs.files.rm(nextFile.path) + + const stats = await ipfs.files.stat(dirPath) + const updatedDirCid = stats.cid + + await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') + expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString()) + }) + + it('results in the same hash as a sharded directory created by the importer when removing a file from a subshard of a subshard', async function () { + const { + nextFile, + dirWithAllFiles, + dirWithSomeFiles, + dirPath + } = await createTwoShards(ipfs, 2187) + + await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath) + + await ipfs.files.rm(nextFile.path) + + const stats = await ipfs.files.stat(dirPath) + const updatedDirCid = stats.cid + + await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') + expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString()) + }) + + it('results in the same hash as a sharded directory created by the importer when removing a subshard of a subshard', async function () { + const { + nextFile, + dirWithAllFiles, + dirWithSomeFiles, + dirPath + } = await createTwoShards(ipfs, 139) + + await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath) + + await ipfs.files.rm(nextFile.path) + + const stats = await ipfs.files.stat(dirPath) + const updatedDirCid = stats.cid + + await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true() + expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') + expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString()) + }) + */ +}) diff --git a/packages/unixfs/test/stat.spec.ts b/packages/unixfs/test/stat.spec.ts new file mode 100644 index 00000000..69892f1e --- /dev/null +++ b/packages/unixfs/test/stat.spec.ts @@ -0,0 +1,236 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import type { Blockstore } from 'interface-blockstore' +import { unixfs, UnixFS } from '../src/index.js' +import { MemoryBlockstore } from 'blockstore-core' +import type { CID } from 'multiformats/cid' +import * as dagPb from '@ipld/dag-pb' + +const smallFile = Uint8Array.from(new Array(13).fill(0).map(() => Math.random() * 100)) +const largeFile = Uint8Array.from(new Array(490668).fill(0).map(() => Math.random() * 100)) + +describe('stat', function () { + this.timeout(120 * 1000) + + let blockstore: Blockstore + let fs: UnixFS + let emptyDirCid: CID + + beforeEach(async () => { + blockstore = new MemoryBlockstore() + + fs = unixfs({ blockstore }) + + emptyDirCid = await fs.add({ path: 'empty' }) + }) + + it('stats an empty directory', async () => { + await expect(fs.stat(emptyDirCid)).to.eventually.include({ + fileSize: 0, + dagSize: 2, + blocks: 1, + type: 'directory' + }) + }) + + it('computes how much of the DAG is local', async () => { + const largeFileCid = await fs.add({ content: largeFile }) + const block = await blockstore.get(largeFileCid) + const node = dagPb.decode(block) + + expect(node.Links).to.have.lengthOf(2) + + await expect(fs.stat(largeFileCid)).to.eventually.include({ + fileSize: 490668, + blocks: 3, + localDagSize: 490776 + }) + + // remove one of the blocks so we now have an incomplete DAG + await blockstore.delete(node.Links[0].Hash) + + // block count and local file/dag sizes should be smaller + await expect(fs.stat(largeFileCid)).to.eventually.include({ + fileSize: 490668, + blocks: 2, + localFileSize: 228524, + localDagSize: 228632 + }) + }) + + it('stats a raw node', async () => { + const fileCid = await fs.add({ content: smallFile }) + + await expect(fs.stat(fileCid)).to.eventually.include({ + fileSize: smallFile.length, + dagSize: 13, + blocks: 1, + type: 'raw' + }) + }) + + it('stats a small file', async () => { + const fileCid = await fs.add({ content: smallFile }, { + cidVersion: 0, + rawLeaves: false + }) + + await expect(fs.stat(fileCid)).to.eventually.include({ + fileSize: smallFile.length, + dagSize: 19, + blocks: 1, + type: 'file' + }) + }) + + it('stats a large file', async () => { + const cid = await fs.add({ content: largeFile }) + + await expect(fs.stat(cid)).to.eventually.include({ + fileSize: largeFile.length, + dagSize: 490682, + blocks: 3, + type: 'file' + }) + }) + + it('should stat file with mode', async () => { + const mode = 0o644 + const cid = await fs.add({ + content: smallFile, + mode + }) + + await expect(fs.stat(cid)).to.eventually.include({ + mode + }) + }) + + it('should stat file with mtime', async function () { + const mtime = { + secs: 5, + nsecs: 0 + } + const cid = await fs.add({ + content: smallFile, + mtime + }) + + await expect(fs.stat(cid)).to.eventually.deep.include({ + mtime + }) + }) + + it('should stat a directory', async function () { + await expect(fs.stat(emptyDirCid)).to.eventually.include({ + type: 'directory', + blocks: 1, + fileSize: 0 + }) + }) + + it('should stat dir with mode', async function () { + const mode = 0o755 + const path = 'test-dir' + const dirCid = await fs.mkdir(emptyDirCid, path, { + mode + }) + + await expect(fs.stat(dirCid, { + path + })).to.eventually.include({ + mode + }) + }) + + it('should stat dir with mtime', async function () { + const mtime = { + secs: 5, + nsecs: 0 + } + + const path = 'test-dir' + const dirCid = await fs.mkdir(emptyDirCid, path, { + mtime + }) + + await expect(fs.stat(dirCid, { + path + })).to.eventually.deep.include({ + mtime + }) + }) +/* + it('should stat sharded dir with mode', async function () { + const testDir = `/test-${nanoid()}` + + await ipfs.files.mkdir(testDir, { parents: true }) + await ipfs.files.write(`${testDir}/a`, uint8ArrayFromString('Hello, world!'), { + create: true, + shardSplitThreshold: 0 + }) + + const stat = await ipfs.files.stat(testDir) + + await expect(isShardAtPath(testDir, ipfs)).to.eventually.be.true() + expect(stat).to.have.property('type', 'directory') + expect(stat).to.include({ + mode: 0o755 + }) + }) + + it('should stat sharded dir with mtime', async function () { + const testDir = `/test-${nanoid()}` + + await ipfs.files.mkdir(testDir, { + parents: true, + mtime: { + secs: 5, + nsecs: 0 + } + }) + await ipfs.files.write(`${testDir}/a`, uint8ArrayFromString('Hello, world!'), { + create: true, + shardSplitThreshold: 0 + }) + + const stat = await ipfs.files.stat(testDir) + + await expect(isShardAtPath(testDir, ipfs)).to.eventually.be.true() + expect(stat).to.have.property('type', 'directory') + expect(stat).to.deep.include({ + mtime: { + secs: 5, + nsecs: 0 + } + }) + }) + + describe('with sharding', () => { + it('stats a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + + const stats = await ipfs.files.stat(`${shardedDirPath}`) + + expect(stats.type).to.equal('directory') + expect(stats.size).to.equal(0) + }) + + it('stats a file inside a sharded directory', async () => { + const shardedDirPath = await createShardedDirectory(ipfs) + const files = [] + + for await (const file of ipfs.files.ls(`${shardedDirPath}`)) { + files.push(file) + } + + const stats = await ipfs.files.stat(`${shardedDirPath}/${files[0].name}`) + + expect(stats.type).to.equal('file') + expect(stats.size).to.equal(7) + }) + }) + + */ +}) diff --git a/packages/unixfs/test/touch.spec.ts b/packages/unixfs/test/touch.spec.ts new file mode 100644 index 00000000..df53eb5e --- /dev/null +++ b/packages/unixfs/test/touch.spec.ts @@ -0,0 +1,129 @@ +/* eslint-env mocha */ + +import { expect } from 'aegir/chai' +import type { Blockstore } from 'interface-blockstore' +import { unixfs, UnixFS } from '../src/index.js' +import { MemoryBlockstore } from 'blockstore-core' +import type { CID } from 'multiformats/cid' +import delay from 'delay' + +describe('.files.touch', () => { + let blockstore: Blockstore + let fs: UnixFS + let emptyDirCid: CID + + beforeEach(async () => { + blockstore = new MemoryBlockstore() + + fs = unixfs({ blockstore }) + + emptyDirCid = await fs.add({ path: 'empty' }) + }) + + it('should have default mtime', async () => { + const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) + + await expect(fs.stat(cid)).to.eventually.have.property('mtime') + .that.is.undefined() + + const updatedCid = await fs.touch(cid) + + await expect(fs.stat(updatedCid)).to.eventually.have.property('mtime') + .that.is.not.undefined().and.does.not.deep.equal({ + secs: 0, + nsecs: 0 + }) + }) + + it('should update file mtime', async function () { + this.slow(5 * 1000) + const mtime = new Date() + const seconds = Math.floor(mtime.getTime() / 1000) + + const cid = await fs.add({ + content: Uint8Array.from([0, 1, 2, 3, 4]), + mtime: { + secs: seconds + } + }) + + await delay(2000) + const updatedCid = await fs.touch(cid) + + await expect(fs.stat(updatedCid)).to.eventually.have.nested.property('mtime.secs') + .that.is.greaterThan(seconds) + }) + + it('should update directory mtime', async function () { + this.slow(5 * 1000) + const path = 'path' + const mtime = new Date() + const seconds = Math.floor(mtime.getTime() / 1000) + + const cid = await fs.mkdir(emptyDirCid, path, { + mtime: { + secs: seconds + } + }) + await delay(2000) + const updateCid = await fs.touch(cid) + + await expect(fs.stat(updateCid)).to.eventually.have.nested.property('mtime.secs') + .that.is.greaterThan(seconds) + }) + + it('should update mtime recursively', async function () { + this.slow(5 * 1000) + const path = 'path' + const mtime = new Date() + const seconds = Math.floor(mtime.getTime() / 1000) + + const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) + const dirCid = await fs.cp(cid, emptyDirCid, path) + + await delay(2000) + + const updatedCid = await fs.touch(dirCid, { + recursive: true + }) + + await expect(fs.stat(updatedCid)).to.eventually.have.nested.property('mtime.secs') + .that.is.greaterThan(seconds) + + await expect(fs.stat(updatedCid, { + path + })).to.eventually.have.nested.property('mtime.secs') + .that.is.greaterThan(seconds) + }) +/* + it('should update the mtime for a hamt-sharded-directory', async () => { + const path = `/foo-${Math.random()}` + + await ipfs.files.mkdir(path, { + mtime: new Date() + }) + await ipfs.files.write(`${path}/foo.txt`, uint8ArrayFromString('Hello world'), { + create: true, + shardSplitThreshold: 0 + }) + const originalMtime = (await ipfs.files.stat(path)).mtime + + if (!originalMtime) { + throw new Error('No originalMtime found') + } + + await delay(1000) + await ipfs.files.touch(path, { + flush: true + }) + + const updatedMtime = (await ipfs.files.stat(path)).mtime + + if (!updatedMtime) { + throw new Error('No updatedMtime found') + } + + expect(updatedMtime.secs).to.be.greaterThan(originalMtime.secs) + }) +*/ +}) From b8dc815ade45023c61d7ba09ab1a337f5e838d98 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 3 Feb 2023 00:08:34 +0100 Subject: [PATCH 13/18] chore: just helia --- README.md | 7 - packages/cli-utils/.aegir.js | 6 - packages/cli-utils/LICENSE | 4 - packages/cli-utils/LICENSE-APACHE | 5 - packages/cli-utils/LICENSE-MIT | 19 - packages/cli-utils/README.md | 44 - packages/cli-utils/package.json | 199 -- packages/cli-utils/src/create-helia.ts | 89 - packages/cli-utils/src/find-helia-dir.ts | 16 - packages/cli-utils/src/find-helia.ts | 148 -- packages/cli-utils/src/format.ts | 75 - packages/cli-utils/src/generate-auth.ts | 21 - packages/cli-utils/src/index.ts | 304 --- packages/cli-utils/src/load-rpc-keychain.ts | 38 - packages/cli-utils/src/print-help.ts | 47 - packages/cli-utils/test/index.spec.ts | 7 - packages/cli-utils/tsconfig.json | 21 - packages/helia-cli/.aegir.js | 6 - packages/helia-cli/LICENSE | 4 - packages/helia-cli/LICENSE-APACHE | 5 - packages/helia-cli/LICENSE-MIT | 19 - packages/helia-cli/README.md | 44 - packages/helia-cli/package.json | 158 -- packages/helia-cli/src/commands/daemon.ts | 96 - packages/helia-cli/src/commands/id.ts | 16 - packages/helia-cli/src/commands/index.ts | 12 - packages/helia-cli/src/commands/init.ts | 253 -- packages/helia-cli/src/commands/rpc/index.ts | 18 - packages/helia-cli/src/commands/rpc/rmuser.ts | 25 - .../helia-cli/src/commands/rpc/useradd.ts | 41 - packages/helia-cli/src/commands/rpc/users.ts | 18 - packages/helia-cli/src/commands/status.ts | 40 - packages/helia-cli/src/index.ts | 18 - packages/helia-cli/test/index.spec.ts | 7 - packages/helia-cli/tsconfig.json | 21 - packages/rpc-client/.aegir.js | 6 - packages/rpc-client/LICENSE | 4 - packages/rpc-client/LICENSE-APACHE | 5 - packages/rpc-client/LICENSE-MIT | 19 - packages/rpc-client/README.md | 53 - packages/rpc-client/package.json | 158 -- .../src/commands/authorization/get.ts | 19 - .../src/commands/blockstore/batch.ts | 84 - .../src/commands/blockstore/close.ts | 11 - .../src/commands/blockstore/delete-many.ts | 22 - .../src/commands/blockstore/delete.ts | 18 - .../src/commands/blockstore/get-many.ts | 22 - .../rpc-client/src/commands/blockstore/get.ts | 22 - .../rpc-client/src/commands/blockstore/has.ts | 22 - .../src/commands/blockstore/open.ts | 11 - .../src/commands/blockstore/put-many.ts | 23 - .../rpc-client/src/commands/blockstore/put.ts | 19 - .../src/commands/blockstore/query-keys.ts | 16 - .../src/commands/blockstore/query.ts | 19 - packages/rpc-client/src/commands/info.ts | 27 - .../rpc-client/src/commands/utils/rpc-call.ts | 112 - packages/rpc-client/src/index.ts | 69 - packages/rpc-client/test/index.spec.ts | 9 - packages/rpc-client/tsconfig.json | 18 - packages/rpc-protocol/LICENSE | 4 - packages/rpc-protocol/LICENSE-APACHE | 5 - packages/rpc-protocol/LICENSE-MIT | 19 - packages/rpc-protocol/README.md | 44 - packages/rpc-protocol/package.json | 177 -- packages/rpc-protocol/src/authorization.proto | 13 - packages/rpc-protocol/src/authorization.ts | 171 -- packages/rpc-protocol/src/blockstore.proto | 170 -- packages/rpc-protocol/src/blockstore.ts | 2106 ----------------- packages/rpc-protocol/src/datastore.proto | 164 -- packages/rpc-protocol/src/datastore.ts | 2085 ---------------- packages/rpc-protocol/src/index.ts | 31 - packages/rpc-protocol/src/root.proto | 13 - packages/rpc-protocol/src/root.ts | 167 -- packages/rpc-protocol/src/rpc.proto | 32 - packages/rpc-protocol/src/rpc.ts | 409 ---- packages/rpc-protocol/tsconfig.json | 11 - packages/rpc-server/.aegir.js | 6 - packages/rpc-server/LICENSE | 4 - packages/rpc-server/LICENSE-APACHE | 5 - packages/rpc-server/LICENSE-MIT | 19 - packages/rpc-server/README.md | 53 - packages/rpc-server/package.json | 159 -- .../src/handlers/authorization/get.ts | 103 - .../src/handlers/blockstore/batch.ts | 40 - .../src/handlers/blockstore/close.ts | 9 - .../src/handlers/blockstore/delete-many.ts | 32 - .../src/handlers/blockstore/delete.ts | 26 - .../src/handlers/blockstore/get-many.ts | 32 - .../rpc-server/src/handlers/blockstore/get.ts | 27 - .../rpc-server/src/handlers/blockstore/has.ts | 27 - .../src/handlers/blockstore/open.ts | 9 - .../src/handlers/blockstore/put-many.ts | 32 - .../rpc-server/src/handlers/blockstore/put.ts | 26 - .../src/handlers/blockstore/query-keys.ts | 25 - .../src/handlers/blockstore/query.ts | 26 - packages/rpc-server/src/handlers/index.ts | 36 - packages/rpc-server/src/handlers/info.ts | 27 - packages/rpc-server/src/index.ts | 144 -- .../rpc-server/src/utils/multiaddr-to-url.ts | 22 - packages/rpc-server/test/index.spec.ts | 7 - packages/rpc-server/tsconfig.json | 18 - packages/unixfs-cli/.aegir.js | 6 - packages/unixfs-cli/LICENSE | 4 - packages/unixfs-cli/LICENSE-APACHE | 5 - packages/unixfs-cli/LICENSE-MIT | 19 - packages/unixfs-cli/README.md | 44 - packages/unixfs-cli/package.json | 156 -- packages/unixfs-cli/src/commands/add.ts | 108 - packages/unixfs-cli/src/commands/cat.ts | 51 - packages/unixfs-cli/src/commands/index.ts | 10 - packages/unixfs-cli/src/commands/stat.ts | 55 - packages/unixfs-cli/src/index.ts | 18 - .../unixfs-cli/src/utils/date-to-mtime.ts | 11 - packages/unixfs-cli/src/utils/glob-source.ts | 95 - packages/unixfs-cli/test/index.spec.ts | 7 - packages/unixfs-cli/tsconfig.json | 18 - packages/unixfs/LICENSE | 4 - packages/unixfs/LICENSE-APACHE | 5 - packages/unixfs/LICENSE-MIT | 19 - packages/unixfs/README.md | 53 - packages/unixfs/package.json | 168 -- packages/unixfs/src/commands/add.ts | 46 - packages/unixfs/src/commands/cat.ts | 31 - packages/unixfs/src/commands/chmod.ts | 133 -- packages/unixfs/src/commands/cp.ts | 41 - packages/unixfs/src/commands/ls.ts | 36 - packages/unixfs/src/commands/mkdir.ts | 71 - packages/unixfs/src/commands/rm.ts | 31 - packages/unixfs/src/commands/stat.ts | 137 -- packages/unixfs/src/commands/touch.ts | 136 -- .../unixfs/src/commands/utils/add-link.ts | 319 --- .../src/commands/utils/cid-to-directory.ts | 23 - .../src/commands/utils/cid-to-pblink.ts | 26 - .../unixfs/src/commands/utils/dir-sharded.ts | 219 -- packages/unixfs/src/commands/utils/errors.ts | 31 - .../src/commands/utils/hamt-constants.ts | 14 - .../unixfs/src/commands/utils/hamt-utils.ts | 285 --- packages/unixfs/src/commands/utils/persist.ts | 22 - .../unixfs/src/commands/utils/remove-link.ts | 151 -- packages/unixfs/src/commands/utils/resolve.ts | 130 - packages/unixfs/src/index.ts | 174 -- packages/unixfs/test/cat.spec.ts | 87 - packages/unixfs/test/chmod.spec.ts | 100 - packages/unixfs/test/cp.spec.ts | 201 -- packages/unixfs/test/ls.spec.ts | 119 - packages/unixfs/test/mkdir.spec.ts | 118 - packages/unixfs/test/rm.spec.ts | 177 -- packages/unixfs/test/stat.spec.ts | 236 -- packages/unixfs/test/touch.spec.ts | 129 - packages/unixfs/tsconfig.json | 15 - 150 files changed, 13268 deletions(-) delete mode 100644 packages/cli-utils/.aegir.js delete mode 100644 packages/cli-utils/LICENSE delete mode 100644 packages/cli-utils/LICENSE-APACHE delete mode 100644 packages/cli-utils/LICENSE-MIT delete mode 100644 packages/cli-utils/README.md delete mode 100644 packages/cli-utils/package.json delete mode 100644 packages/cli-utils/src/create-helia.ts delete mode 100644 packages/cli-utils/src/find-helia-dir.ts delete mode 100644 packages/cli-utils/src/find-helia.ts delete mode 100644 packages/cli-utils/src/format.ts delete mode 100644 packages/cli-utils/src/generate-auth.ts delete mode 100644 packages/cli-utils/src/index.ts delete mode 100644 packages/cli-utils/src/load-rpc-keychain.ts delete mode 100644 packages/cli-utils/src/print-help.ts delete mode 100644 packages/cli-utils/test/index.spec.ts delete mode 100644 packages/cli-utils/tsconfig.json delete mode 100644 packages/helia-cli/.aegir.js delete mode 100644 packages/helia-cli/LICENSE delete mode 100644 packages/helia-cli/LICENSE-APACHE delete mode 100644 packages/helia-cli/LICENSE-MIT delete mode 100644 packages/helia-cli/README.md delete mode 100644 packages/helia-cli/package.json delete mode 100644 packages/helia-cli/src/commands/daemon.ts delete mode 100644 packages/helia-cli/src/commands/id.ts delete mode 100644 packages/helia-cli/src/commands/index.ts delete mode 100644 packages/helia-cli/src/commands/init.ts delete mode 100644 packages/helia-cli/src/commands/rpc/index.ts delete mode 100644 packages/helia-cli/src/commands/rpc/rmuser.ts delete mode 100644 packages/helia-cli/src/commands/rpc/useradd.ts delete mode 100644 packages/helia-cli/src/commands/rpc/users.ts delete mode 100644 packages/helia-cli/src/commands/status.ts delete mode 100644 packages/helia-cli/src/index.ts delete mode 100644 packages/helia-cli/test/index.spec.ts delete mode 100644 packages/helia-cli/tsconfig.json delete mode 100644 packages/rpc-client/.aegir.js delete mode 100644 packages/rpc-client/LICENSE delete mode 100644 packages/rpc-client/LICENSE-APACHE delete mode 100644 packages/rpc-client/LICENSE-MIT delete mode 100644 packages/rpc-client/README.md delete mode 100644 packages/rpc-client/package.json delete mode 100644 packages/rpc-client/src/commands/authorization/get.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/batch.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/close.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/delete-many.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/delete.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/get-many.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/get.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/has.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/open.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/put-many.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/put.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/query-keys.ts delete mode 100644 packages/rpc-client/src/commands/blockstore/query.ts delete mode 100644 packages/rpc-client/src/commands/info.ts delete mode 100644 packages/rpc-client/src/commands/utils/rpc-call.ts delete mode 100644 packages/rpc-client/src/index.ts delete mode 100644 packages/rpc-client/test/index.spec.ts delete mode 100644 packages/rpc-client/tsconfig.json delete mode 100644 packages/rpc-protocol/LICENSE delete mode 100644 packages/rpc-protocol/LICENSE-APACHE delete mode 100644 packages/rpc-protocol/LICENSE-MIT delete mode 100644 packages/rpc-protocol/README.md delete mode 100644 packages/rpc-protocol/package.json delete mode 100644 packages/rpc-protocol/src/authorization.proto delete mode 100644 packages/rpc-protocol/src/authorization.ts delete mode 100644 packages/rpc-protocol/src/blockstore.proto delete mode 100644 packages/rpc-protocol/src/blockstore.ts delete mode 100644 packages/rpc-protocol/src/datastore.proto delete mode 100644 packages/rpc-protocol/src/datastore.ts delete mode 100644 packages/rpc-protocol/src/index.ts delete mode 100644 packages/rpc-protocol/src/root.proto delete mode 100644 packages/rpc-protocol/src/root.ts delete mode 100644 packages/rpc-protocol/src/rpc.proto delete mode 100644 packages/rpc-protocol/src/rpc.ts delete mode 100644 packages/rpc-protocol/tsconfig.json delete mode 100644 packages/rpc-server/.aegir.js delete mode 100644 packages/rpc-server/LICENSE delete mode 100644 packages/rpc-server/LICENSE-APACHE delete mode 100644 packages/rpc-server/LICENSE-MIT delete mode 100644 packages/rpc-server/README.md delete mode 100644 packages/rpc-server/package.json delete mode 100644 packages/rpc-server/src/handlers/authorization/get.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/batch.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/close.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/delete-many.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/delete.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/get-many.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/get.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/has.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/open.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/put-many.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/put.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/query-keys.ts delete mode 100644 packages/rpc-server/src/handlers/blockstore/query.ts delete mode 100644 packages/rpc-server/src/handlers/index.ts delete mode 100644 packages/rpc-server/src/handlers/info.ts delete mode 100644 packages/rpc-server/src/index.ts delete mode 100644 packages/rpc-server/src/utils/multiaddr-to-url.ts delete mode 100644 packages/rpc-server/test/index.spec.ts delete mode 100644 packages/rpc-server/tsconfig.json delete mode 100644 packages/unixfs-cli/.aegir.js delete mode 100644 packages/unixfs-cli/LICENSE delete mode 100644 packages/unixfs-cli/LICENSE-APACHE delete mode 100644 packages/unixfs-cli/LICENSE-MIT delete mode 100644 packages/unixfs-cli/README.md delete mode 100644 packages/unixfs-cli/package.json delete mode 100644 packages/unixfs-cli/src/commands/add.ts delete mode 100644 packages/unixfs-cli/src/commands/cat.ts delete mode 100644 packages/unixfs-cli/src/commands/index.ts delete mode 100644 packages/unixfs-cli/src/commands/stat.ts delete mode 100644 packages/unixfs-cli/src/index.ts delete mode 100644 packages/unixfs-cli/src/utils/date-to-mtime.ts delete mode 100644 packages/unixfs-cli/src/utils/glob-source.ts delete mode 100644 packages/unixfs-cli/test/index.spec.ts delete mode 100644 packages/unixfs-cli/tsconfig.json delete mode 100644 packages/unixfs/LICENSE delete mode 100644 packages/unixfs/LICENSE-APACHE delete mode 100644 packages/unixfs/LICENSE-MIT delete mode 100644 packages/unixfs/README.md delete mode 100644 packages/unixfs/package.json delete mode 100644 packages/unixfs/src/commands/add.ts delete mode 100644 packages/unixfs/src/commands/cat.ts delete mode 100644 packages/unixfs/src/commands/chmod.ts delete mode 100644 packages/unixfs/src/commands/cp.ts delete mode 100644 packages/unixfs/src/commands/ls.ts delete mode 100644 packages/unixfs/src/commands/mkdir.ts delete mode 100644 packages/unixfs/src/commands/rm.ts delete mode 100644 packages/unixfs/src/commands/stat.ts delete mode 100644 packages/unixfs/src/commands/touch.ts delete mode 100644 packages/unixfs/src/commands/utils/add-link.ts delete mode 100644 packages/unixfs/src/commands/utils/cid-to-directory.ts delete mode 100644 packages/unixfs/src/commands/utils/cid-to-pblink.ts delete mode 100644 packages/unixfs/src/commands/utils/dir-sharded.ts delete mode 100644 packages/unixfs/src/commands/utils/errors.ts delete mode 100644 packages/unixfs/src/commands/utils/hamt-constants.ts delete mode 100644 packages/unixfs/src/commands/utils/hamt-utils.ts delete mode 100644 packages/unixfs/src/commands/utils/persist.ts delete mode 100644 packages/unixfs/src/commands/utils/remove-link.ts delete mode 100644 packages/unixfs/src/commands/utils/resolve.ts delete mode 100644 packages/unixfs/src/index.ts delete mode 100644 packages/unixfs/test/cat.spec.ts delete mode 100644 packages/unixfs/test/chmod.spec.ts delete mode 100644 packages/unixfs/test/cp.spec.ts delete mode 100644 packages/unixfs/test/ls.spec.ts delete mode 100644 packages/unixfs/test/mkdir.spec.ts delete mode 100644 packages/unixfs/test/rm.spec.ts delete mode 100644 packages/unixfs/test/stat.spec.ts delete mode 100644 packages/unixfs/test/touch.spec.ts delete mode 100644 packages/unixfs/tsconfig.json diff --git a/README.md b/README.md index a4adcea8..cc1cc0d3 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,9 @@ ## Structure -- [`/packages/cli-utils`](./packages/cli-utils) Common code for Helia CLI tools - [`/packages/helia`](./packages/helia) An implementation of IPFS in JavaScript -- [`/packages/helia-cli`](./packages/helia-cli) Run a Helia node on the cli - [`/packages/interface`](./packages/interface) The Helia API - [`/packages/interop`](./packages/interop) Interop tests for Helia -- [`/packages/rpc-client`](./packages/rpc-client) An implementation of IPFS in JavaScript -- [`/packages/rpc-protocol`](./packages/rpc-protocol) RPC protocol for use by @helia/rpc-client and @helia/rpc-server -- [`/packages/rpc-server`](./packages/rpc-server) An implementation of IPFS in JavaScript -- [`/packages/unixfs`](./packages/unixfs) A Helia-compatible wrapper for UnixFS -- [`/packages/unixfs-cli`](./packages/unixfs-cli) Run unixfs commands against a Helia node on the CLI ## Project status diff --git a/packages/cli-utils/.aegir.js b/packages/cli-utils/.aegir.js deleted file mode 100644 index e9c18f3e..00000000 --- a/packages/cli-utils/.aegir.js +++ /dev/null @@ -1,6 +0,0 @@ - -export default { - build: { - bundle: false - } -} diff --git a/packages/cli-utils/LICENSE b/packages/cli-utils/LICENSE deleted file mode 100644 index 20ce483c..00000000 --- a/packages/cli-utils/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -This project is dual licensed under MIT and Apache-2.0. - -MIT: https://www.opensource.org/licenses/mit -Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/cli-utils/LICENSE-APACHE b/packages/cli-utils/LICENSE-APACHE deleted file mode 100644 index 14478a3b..00000000 --- a/packages/cli-utils/LICENSE-APACHE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/cli-utils/LICENSE-MIT b/packages/cli-utils/LICENSE-MIT deleted file mode 100644 index 72dc60d8..00000000 --- a/packages/cli-utils/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/packages/cli-utils/README.md b/packages/cli-utils/README.md deleted file mode 100644 index 6d704370..00000000 --- a/packages/cli-utils/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# @helia/cli-utils - -[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) -[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) - -> Common code for Helia CLI tools - -## Table of contents - -- [Install](#install) -- [API Docs](#api-docs) -- [License](#license) -- [Contribute](#contribute) - -## Install - -```console -$ npm i @helia/cli-utils -``` - -## API Docs - -- - -## License - -Licensed under either of - -- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) -- MIT ([LICENSE-MIT](LICENSE-MIT) / ) - -## Contribute - -Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). - -Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. - -Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/cli-utils/package.json b/packages/cli-utils/package.json deleted file mode 100644 index 4b291d43..00000000 --- a/packages/cli-utils/package.json +++ /dev/null @@ -1,199 +0,0 @@ -{ - "name": "@helia/cli-utils", - "version": "0.0.0", - "description": "Common code for Helia CLI tools", - "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/ipfs/helia/tree/master/packages/cli-utils#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/ipfs/helia.git" - }, - "bugs": { - "url": "https://github.com/ipfs/helia/issues" - }, - "keywords": [ - "IPFS" - ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "typesVersions": { - "*": { - "*": [ - "*", - "dist/*", - "dist/src/*", - "dist/src/*/index" - ], - "src/*": [ - "*", - "dist/*", - "dist/src/*", - "dist/src/*/index" - ] - } - }, - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - }, - "./create-helia": { - "types": "./dist/src/create-helia.d.ts", - "import": "./dist/src/create-helia.js" - }, - "./find-helia": { - "types": "./dist/src/find-helia.d.ts", - "import": "./dist/src/find-helia.js" - }, - "./format": { - "types": "./dist/src/format.d.ts", - "import": "./dist/src/format.js" - }, - "./load-rpc-keychain": { - "types": "./dist/src/load-rpc-keychain.d.ts", - "import": "./dist/src/load-rpc-keychain.js" - } - }, - "eslintConfig": { - "extends": "ipfs", - "parserOptions": { - "sourceType": "module" - } - }, - "release": { - "branches": [ - "main" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, - "scripts": { - "clean": "aegir clean", - "lint": "aegir lint", - "dep-check": "aegir dep-check", - "build": "aegir build", - "test": "aegir test", - "test:node": "aegir test -t node --cov", - "release": "aegir release" - }, - "dependencies": { - "@chainsafe/libp2p-gossipsub": "^6.0.0", - "@chainsafe/libp2p-noise": "^11.0.0", - "@chainsafe/libp2p-yamux": "^3.0.3", - "@helia/interface": "~0.0.0", - "@helia/rpc-client": "~0.0.0", - "@libp2p/bootstrap": "^6.0.0", - "@libp2p/interface-keychain": "^2.0.3", - "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/kad-dht": "^7.0.0", - "@libp2p/keychain": "^1.0.0", - "@libp2p/logger": "^2.0.5", - "@libp2p/mplex": "^7.1.1", - "@libp2p/prometheus-metrics": "1.1.3", - "@libp2p/tcp": "^6.0.8", - "@libp2p/websockets": "^5.0.2", - "@multiformats/multiaddr": "^11.1.5", - "@ucans/ucans": "^0.11.0-alpha", - "blockstore-datastore-adapter": "^5.0.0", - "datastore-core": "^8.0.4", - "datastore-fs": "^8.0.0", - "helia": "~0.0.0", - "kleur": "^4.1.5", - "libp2p": "^0.42.2", - "strip-json-comments": "^5.0.0" - }, - "devDependencies": { - "aegir": "^38.1.0" - }, - "typedoc": { - "entryPoint": "./src/index.ts" - } -} diff --git a/packages/cli-utils/src/create-helia.ts b/packages/cli-utils/src/create-helia.ts deleted file mode 100644 index 7ebe2da4..00000000 --- a/packages/cli-utils/src/create-helia.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { Helia } from '@helia/interface' -import type { HeliaConfig } from './index.js' -import { createHelia as createHeliaNode } from 'helia' -import { FsDatastore } from 'datastore-fs' -import { BlockstoreDatastoreAdapter } from 'blockstore-datastore-adapter' -import { createLibp2p } from 'libp2p' -import { tcp } from '@libp2p/tcp' -import { webSockets } from '@libp2p/websockets' -import { noise } from '@chainsafe/libp2p-noise' -import { yamux } from '@chainsafe/libp2p-yamux' -import { mplex } from '@libp2p/mplex' -import { prometheusMetrics } from '@libp2p/prometheus-metrics' -import { gossipsub } from '@chainsafe/libp2p-gossipsub' -import { kadDHT } from '@libp2p/kad-dht' -import { bootstrap } from '@libp2p/bootstrap' -import stripJsonComments from 'strip-json-comments' -import fs from 'node:fs' -import path from 'node:path' -import * as readline from 'node:readline/promises' -import { ShardingDatastore } from 'datastore-core' -import { NextToLast } from 'datastore-core/shard' - -export async function createHelia (configDir: string, offline: boolean = false): Promise { - const config: HeliaConfig = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'helia.json'), 'utf-8'))) - let password = config.libp2p.keychain.password - - if (config.libp2p.keychain.password == null) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }) - password = await rl.question('Enter libp2p keychain password: ') - } - - const datastore = new FsDatastore(config.datastore, { - createIfMissing: true - }) - await datastore.open() - - const blockstore = new BlockstoreDatastoreAdapter( - new ShardingDatastore( - new FsDatastore(config.blockstore), - new NextToLast(2) - ) - ) - await blockstore.open() - - const helia = await createHeliaNode({ - blockstore, - datastore, - libp2p: await createLibp2p({ - start: !offline, - datastore, - addresses: config.libp2p.addresses, - identify: { - host: { - agentVersion: 'helia/0.0.0' - } - }, - keychain: { - pass: password, - dek: { - salt: config.libp2p.keychain.salt - } - }, - transports: [ - tcp(), - webSockets() - ], - connectionEncryption: [ - noise() - ], - streamMuxers: [ - yamux(), - mplex() - ], - peerDiscovery: [ - bootstrap({ - list: config.libp2p.bootstrap - }) - ], - pubsub: gossipsub(), - dht: kadDHT(), - metrics: prometheusMetrics() - }) - }) - - return helia -} diff --git a/packages/cli-utils/src/find-helia-dir.ts b/packages/cli-utils/src/find-helia-dir.ts deleted file mode 100644 index 2a60f51f..00000000 --- a/packages/cli-utils/src/find-helia-dir.ts +++ /dev/null @@ -1,16 +0,0 @@ -import os from 'node:os' -import path from 'node:path' - -export function findHeliaDir (): string { - if (process.env.XDG_DATA_HOME != null) { - return process.env.XDG_DATA_HOME - } - - const platform = os.platform() - - if (platform === 'darwin') { - return path.join(`${process.env.HOME}`, 'Library', 'helia') - } - - return path.join(`${process.env.HOME}`, '.helia') -} diff --git a/packages/cli-utils/src/find-helia.ts b/packages/cli-utils/src/find-helia.ts deleted file mode 100644 index ea7237a7..00000000 --- a/packages/cli-utils/src/find-helia.ts +++ /dev/null @@ -1,148 +0,0 @@ -import type { Helia } from '@helia/interface' -import { createHeliaRpcClient } from '@helia/rpc-client' -import { multiaddr } from '@multiformats/multiaddr' -import { createHelia } from './create-helia.js' -import { createLibp2p, Libp2p } from 'libp2p' -import { tcp } from '@libp2p/tcp' -import { noise } from '@chainsafe/libp2p-noise' -import { yamux } from '@chainsafe/libp2p-yamux' -import { mplex } from '@libp2p/mplex' -import { logger } from '@libp2p/logger' -import fs from 'node:fs' -import path from 'node:path' -import os from 'node:os' -import { FsDatastore } from 'datastore-fs' -import { loadRpcKeychain } from './load-rpc-keychain.js' -import type { PeerId } from '@libp2p/interface-peer-id' - -const log = logger('helia:cli:utils:find-helia') - -export async function findHelia (configDir: string, rpcAddress: string, user: string, offline: boolean = true, online: boolean = true): Promise<{ helia: Helia, libp2p: Libp2p | undefined }> { - let { - libp2p, helia - } = await findOnlineHelia(configDir, rpcAddress, user) - - if (helia == null) { - log('connecting to running helia node failed') - - if (!offline) { - log('could not connect to running helia node and command cannot be run in offline mode') - throw new Error('Could not connect to Helia - is the node running?') - } - - log('create offline helia node') - helia = await createHelia(configDir, offline) - } else if (!online) { - log('connected to running helia node but command cannot be run in online mode') - throw new Error('This command cannot be run while a Helia daemon is running') - } - - return { - helia, - libp2p - } -} - -export async function findOnlineHelia (configDir: string, rpcAddress: string, user: string): Promise<{ helia?: Helia, libp2p?: Libp2p }> { - const isRunning = await isHeliaRunning(configDir) - - if (!isRunning) { - log('helia daemon was not running') - return {} - } - - let peerId: PeerId | undefined - - try { - const rpcKeychain = await loadRpcKeychain(configDir) - peerId = await rpcKeychain.exportPeerId(`rpc-user-${user}`) - } catch (err) { - log('could not load peer id rpc-user-%s', user, err) - } - - log('create dial-only libp2p node') - const libp2p = await createLibp2p({ - peerId, - datastore: new FsDatastore(path.join(configDir, 'rpc')), - transports: [ - tcp() - ], - connectionEncryption: [ - noise() - ], - streamMuxers: [ - yamux(), - mplex() - ], - relay: { - enabled: false - }, - nat: { - enabled: false - } - }) - - let helia: Helia | undefined - - try { - log('create helia client') - helia = await createHeliaRpcClient({ - multiaddr: multiaddr(`/unix/${rpcAddress}`), - libp2p, - user - }) - } catch (err: any) { - log('could not create helia rpc client', err) - await libp2p.stop() - - if (err.name === 'AggregateError' && err.errors != null) { - throw err.errors[0] - } else { - throw err - } - } - - return { - helia, - libp2p - } -} - -export async function isHeliaRunning (configDir: string): Promise { - const pidFilePath = path.join(configDir, 'helia.pid') - - if (!fs.existsSync(pidFilePath)) { - log('pidfile at %s did not exist', pidFilePath) - return false - } - - const pid = Number(fs.readFileSync(pidFilePath, { - encoding: 'utf8' - }).trim()) - - if (isNaN(pid)) { - log('pidfile at %s had invalid contents', pidFilePath) - log('removing invalid pidfile') - fs.rmSync(pidFilePath) - return false - } - - try { - // this will throw if the process does not exist - os.getPriority(pid) - return true - } catch (err: any) { - log('getting process info for pid %d failed', pid) - - if (err.message.includes('no such process') === true) { - log('process for pid %d was not running', pid) - log('removing stale pidfile') - fs.rmSync(pidFilePath) - - return false - } - - log('error getting process priority for pid %d', pid, err) - throw err - } -} diff --git a/packages/cli-utils/src/format.ts b/packages/cli-utils/src/format.ts deleted file mode 100644 index 010d068b..00000000 --- a/packages/cli-utils/src/format.ts +++ /dev/null @@ -1,75 +0,0 @@ -import kleur from 'kleur' - -export function formatter (stdout: NodeJS.WriteStream, items: Formatable[]): void { - items.forEach(item => stdout.write(item())) -} - -export interface Formatable { - (): string -} - -export function header (string: string): Formatable { - return (): string => { - return `\n${string}\n` - } -} - -export function subheader (string: string): Formatable { - return (): string => { - return `\n${string}\n` - } -} - -export function paragraph (string: string): Formatable { - return (): string => { - return kleur.white(`\n${string}\n`) - } -} - -export function table (rows: FormatableRow[]): Formatable { - const cellLengths: string[] = [] - - for (const row of rows) { - const cells = row() - - for (let i = 0; i < cells.length; i++) { - const textLength = cells[i].length - - if (cellLengths[i] == null || cellLengths[i].length < textLength) { - cellLengths[i] = new Array(textLength).fill(' ').join('') - } - } - } - - return (): string => { - const output: string[] = [] - - for (const row of rows) { - const cells = row() - const text: string[] = [] - - for (let i = 0; i < cells.length; i++) { - const cell = cells[i] - text.push((cell + cellLengths[i]).substring(0, cellLengths[i].length)) - } - - output.push(text.join(' ') + '\n') - } - - return output.join('') - } -} - -export interface FormatableRow { - rowLengths: number[] - (): string[] -} - -export function row (...cells: string[]): FormatableRow { - const formatable = (): string[] => { - return cells - } - formatable.rowLengths = cells.map(str => str.length) - - return formatable -} diff --git a/packages/cli-utils/src/generate-auth.ts b/packages/cli-utils/src/generate-auth.ts deleted file mode 100644 index 3b6da3a1..00000000 --- a/packages/cli-utils/src/generate-auth.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { EdKeypair, build, encode } from '@ucans/ucans' - -export async function generateAuth (serverKey: string): Promise { - const issuer = EdKeypair.fromSecretKey(serverKey, { - format: 'base64url' - }) - - const userKey = await EdKeypair.create() - - const clientUcan = await build({ - issuer, - audience: userKey.did(), - expiration: (Date.now() / 1000) + (60 * 60 * 24), - capabilities: [{ - with: { scheme: 'service', hierPart: '/cat' }, - can: { namespace: 'service', segments: ['GET'] } - }] - }) - - return encode(clientUcan) -} diff --git a/packages/cli-utils/src/index.ts b/packages/cli-utils/src/index.ts deleted file mode 100644 index b46c753c..00000000 --- a/packages/cli-utils/src/index.ts +++ /dev/null @@ -1,304 +0,0 @@ -import type { ParseArgsConfig } from 'node:util' -import type { Helia } from '@helia/interface' -import { InvalidParametersError } from '@helia/interface/errors' -import { parseArgs } from 'node:util' -import { findHeliaDir } from './find-helia-dir.js' -import path from 'node:path' -import { printHelp } from './print-help.js' -import fs from 'node:fs' -import { findHelia } from './find-helia.js' -import type { Libp2p } from 'libp2p' - -/** - * Extends the internal node type to add a description to the options - */ -export interface ParseArgsOptionConfig { - /** - * Type of argument. - */ - type: 'string' | 'boolean' - - /** - * Whether this option can be provided multiple times. - * If `true`, all values will be collected in an array. - * If `false`, values for the option are last-wins. - * - * @default false. - */ - multiple?: boolean - - /** - * A single character alias for the option. - */ - short?: string - - /** - * The default option value when it is not set by args. - * It must be of the same type as the the `type` property. - * When `multiple` is `true`, it must be an array. - * - * @since v18.11.0 - */ - default?: string | boolean | string[] | boolean[] - - /** - * A description used to generate help text - */ - description: string - - /** - * If specified the value must be in this list - */ - valid?: string[] -} - -type ParseArgsOptionsConfig = Record - -export interface CommandOptions extends ParseArgsConfig { - /** - * Used to describe arguments known to the parser. - */ - options?: T -} - -export interface Command { - /** - * The command name - */ - command: string - - /** - * Used to generate help text - */ - description: string - - /** - * Used to generate help text - */ - example?: string - - /** - * Specify if this command can be run offline (default true) - */ - offline?: boolean - - /** - * Specify if this command can be run online (default true) - */ - online?: boolean - - /** - * Configuration for the command - */ - options?: ParseArgsOptionsConfig - - /** - * Run the command - */ - execute: (ctx: Context & T) => Promise - - /** - * Subcommands of the current command - */ - subcommands?: Array> -} - -export interface Context { - helia: Helia - directory: string - stdin: NodeJS.ReadStream - stdout: NodeJS.WriteStream - stderr: NodeJS.WriteStream -} - -export function createCliConfig (options?: T, strict?: boolean): ParseArgsConfig { - return { - allowPositionals: true, - strict: strict ?? true, - options: { - help: { - // @ts-expect-error description field not defined - description: 'Show help text', - type: 'boolean' - }, - ...options - } - } -} - -/** - * Typedef for the Helia config file - */ -export interface HeliaConfig { - blockstore: string - datastore: string - libp2p: { - addresses: { - listen: string[] - announce: string[] - noAnnounce: string[] - } - keychain: { - salt: string - password?: string - } - bootstrap: string[] - } - rpc: { - datastore: string - keychain: { - salt: string - password?: string - } - } -} - -export interface RootArgs { - positionals: string[] - directory: string - help: boolean - rpcAddress: string - user: string -} - -const root: Command = { - command: '', - description: '', - options: { - directory: { - description: 'The directory used by Helia to store config and data', - type: 'string', - default: findHeliaDir() - }, - rpcAddress: { - description: 'The multiaddr of the Helia node', - type: 'string', - default: path.join(findHeliaDir(), 'rpc.sock') - }, - user: { - description: 'The name of the RPC user', - type: 'string', - default: process.env.USER - } - }, - async execute () {} -} - -export async function cli (command: string, description: string, subcommands: Array>): Promise { - const rootCommand: Command = { - ...root, - command, - description, - subcommands - } - const config = createCliConfig(rootCommand.options, false) - const rootCommandArgs = parseArgs(config) - const configDir = rootCommandArgs.values.directory - - if (configDir == null || typeof configDir !== 'string') { - throw new InvalidParametersError('No config directory specified') - } - - if (typeof rootCommandArgs.values.rpcAddress !== 'string') { - throw new InvalidParametersError('No RPC address specified') - } - - if (typeof rootCommandArgs.values.user !== 'string') { - throw new InvalidParametersError('No RPC user specified') - } - - if (rootCommandArgs.values.help === true && rootCommandArgs.positionals.length === 0) { - printHelp(rootCommand, process.stdout) - return - } - - if (!fs.existsSync(configDir)) { - const init = subcommands.find(command => command.command === 'init') - - if (init == null) { - throw new Error('Could not find init command') - } - - // run the init command - const parsed = parseArgs(createCliConfig(init.options, false)) - - if (parsed.values.help === true) { - printHelp(init, process.stdout) - return - } - - await init.execute({ - ...parsed.values, - positionals: parsed.positionals.slice(1), - stdin: process.stdin, - stdout: process.stdout, - stderr: process.stderr, - directory: configDir - }) - - if (rootCommandArgs.positionals[0] === 'init') { - // if init was specified explicitly we can bail because we just ran init - return - } - } - - if (rootCommandArgs.positionals.length > 0) { - let subCommand: Command = rootCommand - let subCommandDepth = 0 - - for (let i = 0; i < rootCommandArgs.positionals.length; i++) { - const positional = rootCommandArgs.positionals[i] - - if (subCommand.subcommands == null) { - break - } - - const sub = subCommand.subcommands.find(c => c.command === positional) - - if (sub != null) { - subCommandDepth++ - subCommand = sub - } - } - - if (subCommand == null) { - throw new Error('Command not found') - } - - const subCommandArgs = parseArgs(createCliConfig(subCommand.options)) - - if (subCommandArgs.values.help === true) { - printHelp(subCommand, process.stdout) - return - } - - let helia: Helia | undefined - let libp2p: Libp2p | undefined - - if (subCommand.command !== 'daemon' && subCommand.command !== 'status') { - const res = await findHelia(configDir, rootCommandArgs.values.rpcAddress, rootCommandArgs.values.user, subCommand.offline, subCommand.online) - helia = res.helia - libp2p = res.libp2p - } - - await subCommand.execute({ - ...rootCommandArgs.values, - ...subCommandArgs.values, - positionals: subCommandArgs.positionals.slice(subCommandDepth), - helia, - stdin: process.stdin, - stdout: process.stdout, - stderr: process.stderr, - directory: configDir - }) - - if (libp2p != null) { - await libp2p.stop() - } - - return - } - - // no command specified, print help - printHelp(rootCommand, process.stdout) -} diff --git a/packages/cli-utils/src/load-rpc-keychain.ts b/packages/cli-utils/src/load-rpc-keychain.ts deleted file mode 100644 index a0fea503..00000000 --- a/packages/cli-utils/src/load-rpc-keychain.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { HeliaConfig } from './index.js' -import { FsDatastore } from 'datastore-fs' -import stripJsonComments from 'strip-json-comments' -import fs from 'node:fs' -import path from 'node:path' -import * as readline from 'node:readline/promises' -import { DefaultKeyChain } from '@libp2p/keychain' -import type { KeyChain } from '@libp2p/interface-keychain' - -export async function loadRpcKeychain (configDir: string): Promise { - const config: HeliaConfig = JSON.parse(stripJsonComments(fs.readFileSync(path.join(configDir, 'helia.json'), 'utf-8'))) - const datastore = new FsDatastore(config.rpc.datastore, { - createIfMissing: true - }) - await datastore.open() - - let password = config.rpc.keychain.password - - if (password == null) { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }) - password = await rl.question('Enter libp2p keychain password: ') - } - - return new DefaultKeyChain({ - datastore - }, { - pass: password, - dek: { - keyLength: 512 / 8, - iterationCount: 10000, - hash: 'sha2-512', - salt: config.rpc.keychain.salt - } - }) -} diff --git a/packages/cli-utils/src/print-help.ts b/packages/cli-utils/src/print-help.ts deleted file mode 100644 index 72b4ba33..00000000 --- a/packages/cli-utils/src/print-help.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { Command } from './index.js' -import * as format from './format.js' -import type { Formatable } from './format.js' -import kleur from 'kleur' - -export function printHelp (command: Command, stdout: NodeJS.WriteStream): void { - const items: Formatable[] = [ - format.header(command.description) - ] - - if (command.example != null) { - items.push( - format.subheader('Example:'), - format.paragraph(command.example) - ) - } - - if (command.subcommands != null) { - items.push( - format.subheader('Subcommands:'), - format.table( - command.subcommands.map(command => format.row( - ` ${command.command}`, - kleur.white(command.description) - )) - ) - ) - } - - if (command.options != null) { - items.push( - format.subheader('Options:'), - format.table( - Object.entries(command.options).map(([key, option]) => format.row( - ` --${key}`, - kleur.white(option.description), - option.default != null ? kleur.grey(`[default: ${option.default}]`) : '' - )) - ) - ) - } - - format.formatter( - stdout, - items - ) -} diff --git a/packages/cli-utils/test/index.spec.ts b/packages/cli-utils/test/index.spec.ts deleted file mode 100644 index e67dc48c..00000000 --- a/packages/cli-utils/test/index.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect } from 'aegir/chai' - -describe('cli', () => { - it('should start a node', () => { - expect(true).to.be.ok() - }) -}) diff --git a/packages/cli-utils/tsconfig.json b/packages/cli-utils/tsconfig.json deleted file mode 100644 index f56be826..00000000 --- a/packages/cli-utils/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src", - "test" - ], - "references": [ - { - "path": "../helia" - }, - { - "path": "../interface" - }, - { - "path": "../rpc-client" - } - ] -} diff --git a/packages/helia-cli/.aegir.js b/packages/helia-cli/.aegir.js deleted file mode 100644 index e9c18f3e..00000000 --- a/packages/helia-cli/.aegir.js +++ /dev/null @@ -1,6 +0,0 @@ - -export default { - build: { - bundle: false - } -} diff --git a/packages/helia-cli/LICENSE b/packages/helia-cli/LICENSE deleted file mode 100644 index 20ce483c..00000000 --- a/packages/helia-cli/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -This project is dual licensed under MIT and Apache-2.0. - -MIT: https://www.opensource.org/licenses/mit -Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/helia-cli/LICENSE-APACHE b/packages/helia-cli/LICENSE-APACHE deleted file mode 100644 index 14478a3b..00000000 --- a/packages/helia-cli/LICENSE-APACHE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/helia-cli/LICENSE-MIT b/packages/helia-cli/LICENSE-MIT deleted file mode 100644 index 72dc60d8..00000000 --- a/packages/helia-cli/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/packages/helia-cli/README.md b/packages/helia-cli/README.md deleted file mode 100644 index 13cf8337..00000000 --- a/packages/helia-cli/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# @helia/cli - -[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) -[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) - -> Run a Helia node on the cli - -## Table of contents - -- [Install](#install) -- [API Docs](#api-docs) -- [License](#license) -- [Contribute](#contribute) - -## Install - -```console -$ npm i @helia/cli -``` - -## API Docs - -- - -## License - -Licensed under either of - -- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) -- MIT ([LICENSE-MIT](LICENSE-MIT) / ) - -## Contribute - -Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). - -Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. - -Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/helia-cli/package.json b/packages/helia-cli/package.json deleted file mode 100644 index 88298451..00000000 --- a/packages/helia-cli/package.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "name": "@helia/cli", - "version": "0.0.0", - "description": "Run a Helia node on the cli", - "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/ipfs/helia/tree/master/packages/helia-cli#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/ipfs/helia.git" - }, - "bugs": { - "url": "https://github.com/ipfs/helia/issues" - }, - "keywords": [ - "IPFS" - ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, - "bin": { - "helia": "./dist/src/index.js" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "eslintConfig": { - "extends": "ipfs", - "parserOptions": { - "sourceType": "module" - } - }, - "release": { - "branches": [ - "main" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, - "scripts": { - "clean": "aegir clean", - "lint": "aegir lint", - "dep-check": "aegir dep-check", - "build": "aegir build", - "test": "aegir test", - "test:node": "aegir test -t node --cov", - "release": "aegir release" - }, - "dependencies": { - "@helia/cli-utils": "~0.0.0", - "@helia/interface": "~0.0.0", - "@helia/rpc-server": "~0.0.0", - "@libp2p/crypto": "^1.0.11", - "@libp2p/interface-keychain": "^2.0.3", - "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/keychain": "^1.0.0", - "@libp2p/logger": "^2.0.5", - "@libp2p/peer-id-factory": "^2.0.0", - "datastore-fs": "^8.0.0", - "kleur": "^4.1.5", - "uint8arrays": "^4.0.3" - }, - "devDependencies": { - "aegir": "^38.1.0" - }, - "typedoc": { - "entryPoint": "./src/index.ts" - } -} diff --git a/packages/helia-cli/src/commands/daemon.ts b/packages/helia-cli/src/commands/daemon.ts deleted file mode 100644 index 5fad7e03..00000000 --- a/packages/helia-cli/src/commands/daemon.ts +++ /dev/null @@ -1,96 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import { createHelia } from '@helia/cli-utils/create-helia' -import { createHeliaRpcServer } from '@helia/rpc-server' -import fs from 'node:fs' -import path from 'node:path' -import os from 'node:os' -import { logger } from '@libp2p/logger' -import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' - -const log = logger('helia:cli:commands:daemon') - -interface DaemonArgs { - positionals?: string[] - authorizationValiditySeconds: number -} - -export const daemon: Command = { - command: 'daemon', - description: 'Starts a Helia daemon', - example: '$ helia daemon', - online: false, - options: { - authorizationValiditySeconds: { - description: 'How many seconds a request authorization token is valid for', - type: 'string', - default: '5' - } - }, - async execute ({ directory, stdout, authorizationValiditySeconds }) { - const lockfilePath = path.join(directory, 'helia.pid') - checkPidFile(lockfilePath) - - const rpcSocketFilePath = path.join(directory, 'rpc.sock') - checkRpcSocketFile(rpcSocketFilePath) - - const helia = await createHelia(directory) - - await createHeliaRpcServer({ - helia, - users: await loadRpcKeychain(directory), - authorizationValiditySeconds: Number(authorizationValiditySeconds) - }) - - const info = await helia.info() - - stdout.write(`${info.agentVersion} is running\n`) - - if (info.multiaddrs.length > 0) { - stdout.write('Listening on:\n') - - info.multiaddrs.forEach(ma => { - stdout.write(` ${ma.toString()}\n`) - }) - } - - fs.writeFileSync(lockfilePath, process.pid.toString()) - } -} - -/** - * Check the passed lockfile path exists, if it does it should contain the PID - * of the owning process. Read the file, check if the process with the PID is - * still running, throw an error if it is. - * - * @param pidFilePath - */ -function checkPidFile (pidFilePath: string): void { - if (!fs.existsSync(pidFilePath)) { - return - } - - const pid = Number(fs.readFileSync(pidFilePath, { - encoding: 'utf8' - }).trim()) - - try { - // this will throw if the process does not exist - os.getPriority(pid) - - throw new Error(`Helia already running with pid ${pid}`) - } catch (err: any) { - if (err.message.includes('no such process') === true) { - log('Removing stale pidfile') - fs.rmSync(pidFilePath) - } else { - throw err - } - } -} - -function checkRpcSocketFile (rpcSocketFilePath: string): void { - if (fs.existsSync(rpcSocketFilePath)) { - log('Removing stale rpc socket file') - fs.rmSync(rpcSocketFilePath) - } -} diff --git a/packages/helia-cli/src/commands/id.ts b/packages/helia-cli/src/commands/id.ts deleted file mode 100644 index 7c619554..00000000 --- a/packages/helia-cli/src/commands/id.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Command } from '@helia/cli-utils' - -interface IdArgs { - positionals?: string[] -} - -export const id: Command = { - command: 'id', - description: 'Print information out this Helia node', - example: '$ helia id', - async execute ({ helia, stdout }) { - const result = await helia.info() - - stdout.write(JSON.stringify(result, null, 2) + '\n') - } -} diff --git a/packages/helia-cli/src/commands/index.ts b/packages/helia-cli/src/commands/index.ts deleted file mode 100644 index 022c9ab6..00000000 --- a/packages/helia-cli/src/commands/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { init } from './init.js' -import { daemon } from './daemon.js' -import { id } from './id.js' -import { status } from './status.js' -import type { Command } from '@helia/cli-utils' - -export const commands: Array> = [ - init, - daemon, - id, - status -] diff --git a/packages/helia-cli/src/commands/init.ts b/packages/helia-cli/src/commands/init.ts deleted file mode 100644 index 6440077f..00000000 --- a/packages/helia-cli/src/commands/init.ts +++ /dev/null @@ -1,253 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import path from 'node:path' -import fs from 'node:fs/promises' -import { createEd25519PeerId, createRSAPeerId, createSecp256k1PeerId } from '@libp2p/peer-id-factory' -import { InvalidParametersError } from '@helia/interface/errors' -import type { PeerId } from '@libp2p/interface-peer-id' -import { logger } from '@libp2p/logger' -import { FsDatastore } from 'datastore-fs' -import { randomBytes } from '@libp2p/crypto' -import { toString as uint8ArrayToString } from 'uint8arrays/to-string' -import { DefaultKeyChain } from '@libp2p/keychain' -import type { KeyType } from '@libp2p/interface-keychain' -import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' - -const log = logger('helia:cli:commands:init') - -interface InitArgs { - positionals?: string[] - keyType: string - bits: string - port: string - directory: string - directoryMode: string - configFileMode: string - publicKeyMode: string - keychainPassword: string - keychainSalt: string - storePassword: boolean - rpcKeychainPassword: string - rpcKeychainSalt: string - storeRpcPassword: boolean - rpcUser: string - rpcUserKeyType: KeyType -} - -// NIST SP 800-132 -const NIST_MINIMUM_SALT_LENGTH = 128 / 8 -const SALT_LENGTH = Math.ceil(NIST_MINIMUM_SALT_LENGTH / 3) * 3 // no base64 padding - -export const init: Command = { - command: 'init', - online: false, - description: 'Initialize the node', - example: '$ helia init', - options: { - keyType: { - description: 'The key type, valid options are "ed25519", "secp256k1" or "rsa"', - type: 'string', - short: 'k', - default: 'ed25519' - }, - bits: { - description: 'Key length (only applies to RSA keys)', - type: 'string', - short: 'b', - default: '2048' - }, - directoryMode: { - description: 'Create the data directory with this mode', - type: 'string', - default: '0700' - }, - configFileMode: { - description: 'Create the config file with this mode', - type: 'string', - default: '0600' - }, - publicKeyMode: { - description: 'Create the public key file with this mode', - type: 'string', - default: '0644' - }, - keychainPassword: { - description: 'The libp2p keychain will use a key derived from this password for encryption operations', - type: 'string', - default: uint8ArrayToString(randomBytes(20), 'base64') - }, - keychainSalt: { - description: 'The libp2p keychain will use use this salt when deriving the key from the password', - type: 'string', - default: uint8ArrayToString(randomBytes(SALT_LENGTH), 'base64') - }, - storePassword: { - description: 'If true, store the password used to derive the key used by the libp2p keychain in the config file', - type: 'boolean', - default: true - }, - rpcKeychainPassword: { - description: 'The RPC server keychain will use a key derived from this password for encryption operations', - type: 'string', - default: uint8ArrayToString(randomBytes(20), 'base64') - }, - rpcKeychainSalt: { - description: 'The RPC server keychain will use use this salt when deriving the key from the password', - type: 'string', - default: uint8ArrayToString(randomBytes(SALT_LENGTH), 'base64') - }, - storeRpcPassword: { - description: 'If true, store the password used to derive the key used by the RPC server keychain in the config file', - type: 'boolean', - default: true - }, - rpcUser: { - description: 'The default RPC user', - type: 'string', - default: process.env.USER - }, - rpcUserKeyType: { - description: 'The default RPC user key tupe', - type: 'string', - default: 'Ed25519', - valid: ['RSA', 'Ed25519', 'secp256k1'] - } - }, - async execute ({ keyType, bits, directory, directoryMode, configFileMode, publicKeyMode, stdout, keychainPassword, keychainSalt, storePassword, rpcKeychainPassword, rpcKeychainSalt, storeRpcPassword, rpcUser, rpcUserKeyType }) { - try { - await fs.readdir(directory) - // don't init if we are already inited - throw new InvalidParametersError(`Cowardly refusing to reinitialize Helia at ${directory}`) - } catch (err: any) { - if (err.code !== 'ENOENT') { - throw err - } - } - - const configFilePath = path.join(directory, 'helia.json') - - try { - await fs.access(configFilePath) - // don't init if we are already inited - throw new InvalidParametersError(`Cowardly refusing to overwrite Helia config file at ${configFilePath}`) - } catch (err: any) { - if (err.code !== 'ENOENT') { - throw err - } - } - - const peerId = await generateKey(keyType, bits) - - if (peerId.publicKey == null || peerId.privateKey == null) { - throw new InvalidParametersError('Generated PeerId had missing components') - } - - log('create helia dir %s', directory) - await fs.mkdir(directory, { - recursive: true, - mode: parseInt(directoryMode, 8) - }) - - const datastorePath = path.join(directory, 'data') - const rpcDatastorePath = path.join(directory, 'rpc') - - // create a dial-only libp2p node configured with the datastore in the helia - // directory - this will store the peer id securely in the keychain - const datastore = new FsDatastore(datastorePath, { - createIfMissing: true - }) - await datastore.open() - const keychain = new DefaultKeyChain({ - datastore - }, { - pass: keychainPassword, - dek: { - keyLength: 512 / 8, - iterationCount: 10000, - hash: 'sha2-512', - salt: keychainSalt - } - }) - await keychain.importPeer('self', peerId) - await datastore.close() - - // now write the public key from the PeerId out for use by the RPC client - const publicKeyPath = path.join(directory, 'peer.pub') - log('create public key %s', publicKeyPath) - await fs.writeFile(publicKeyPath, peerId.toString() + '\n', { - mode: parseInt(publicKeyMode, 8), - flag: 'ax' - }) - - log('create config file %s', configFilePath) - await fs.writeFile(configFilePath, ` -{ - // Where blocks are stored - "blockstore": "${path.join(directory, 'blocks')}", - - // Where data is stored - "datastore": "${datastorePath}", - - // libp2p configuration - "libp2p": { - "addresses": { - "listen": [ - "/ip4/0.0.0.0/tcp/0", - "/ip4/0.0.0.0/tcp/0/ws", - - // this is the rpc socket - "/unix${directory}/rpc.sock" - ], - "noAnnounce": [ - // do not announce the rpc socket to the outside world - "/unix${directory}/rpc.sock" - ] - }, - "keychain": { - "salt": "${keychainSalt}"${storePassword -? `, - "password": "${keychainPassword}"` -: ''} - }, - "bootstrap": [ - "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN", - "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa", - "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb", - "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt" - ] - }, - "rpc": { - "datastore": "${rpcDatastorePath}", - "keychain": { - "salt": "${rpcKeychainSalt}"${storeRpcPassword -? `, - "password": "${rpcKeychainPassword}"` -: ''} - } - } -} -`, { - mode: parseInt(configFileMode, 8), - flag: 'ax' - }) - - // create an rpc key for the first user - const rpcKeychain = await loadRpcKeychain(directory) - await rpcKeychain.createKey(`rpc-user-${rpcUser}`, rpcUserKeyType) - - stdout.write(`Wrote config file to ${configFilePath}\n`) - } -} - -async function generateKey (type: string, bits: string = '2048'): Promise { - if (type === 'ed25519') { - return await createEd25519PeerId() - } else if (type === 'secp256k1') { - return await createSecp256k1PeerId() - } else if (type === 'rsa') { - return await createRSAPeerId({ - bits: parseInt(bits) - }) - } - - throw new InvalidParametersError(`Unknown key type "${type}" - must be "ed25519", "secp256k1" or "rsa"`) -} diff --git a/packages/helia-cli/src/commands/rpc/index.ts b/packages/helia-cli/src/commands/rpc/index.ts deleted file mode 100644 index 9dbcde1f..00000000 --- a/packages/helia-cli/src/commands/rpc/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import { rpcRmuser } from './rmuser.js' -import { rpcUseradd } from './useradd.js' -import { rpcUsers } from './users.js' - -export const rpc: Command = { - command: 'rpc', - description: 'Update the config of the Helia RPC server', - example: '$ helia rpc', - subcommands: [ - rpcRmuser, - rpcUseradd, - rpcUsers - ], - async execute ({ stdout }) { - stdout.write('Please enter a subcommand\n') - } -} diff --git a/packages/helia-cli/src/commands/rpc/rmuser.ts b/packages/helia-cli/src/commands/rpc/rmuser.ts deleted file mode 100644 index 2fdafc7a..00000000 --- a/packages/helia-cli/src/commands/rpc/rmuser.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' - -interface AddRpcUserArgs { - positionals: string[] -} - -export const rpcRmuser: Command = { - command: 'rmuser', - description: 'Remove a RPC user from your Helia node', - example: '$ helia rpc rmuser ', - async execute ({ directory, positionals, stdout }) { - const user = positionals[0] ?? process.env.USER - - if (user == null) { - throw new Error('No user specified') - } - - const keychain = await loadRpcKeychain(directory) - - await keychain.removeKey(`rpc-user-${user}`) - - stdout.write(`Removed user ${user}\n`) - } -} diff --git a/packages/helia-cli/src/commands/rpc/useradd.ts b/packages/helia-cli/src/commands/rpc/useradd.ts deleted file mode 100644 index 026ea9cd..00000000 --- a/packages/helia-cli/src/commands/rpc/useradd.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import type { KeyType } from '@libp2p/interface-keychain' -import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' - -interface AddRpcUserArgs { - positionals: string[] - keyType: KeyType -} - -export const rpcUseradd: Command = { - command: 'useradd', - description: 'Add an RPC user to your Helia node', - example: '$ helia rpc useradd ', - options: { - keyType: { - description: 'The type of key', - type: 'string', - default: 'Ed25519', - valid: ['Ed25519', 'secp256k1'] - } - }, - async execute ({ directory, positionals, keyType, stdout }) { - const user = positionals[0] ?? process.env.USER - - if (user == null) { - throw new Error('No user specified') - } - - const keychain = await loadRpcKeychain(directory) - const keyName = `rpc-user-${user}` - const keys = await keychain.listKeys() - - if (keys.some(info => info.name === keyName)) { - throw new Error(`User "${user}" already exists`) - } - - await keychain.createKey(`rpc-user-${user}`, keyType) - - stdout.write(`Created user ${user}\n`) - } -} diff --git a/packages/helia-cli/src/commands/rpc/users.ts b/packages/helia-cli/src/commands/rpc/users.ts deleted file mode 100644 index 4ea1561a..00000000 --- a/packages/helia-cli/src/commands/rpc/users.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import { loadRpcKeychain } from '@helia/cli-utils/load-rpc-keychain' - -export const rpcUsers: Command = { - command: 'users', - description: 'List user accounts on the Helia RPC server', - example: '$ helia rpc users', - async execute ({ directory, stdout }) { - const keychain = await loadRpcKeychain(directory) - const keys = await keychain.listKeys() - - for (const info of keys) { - if (info.name.startsWith('rpc-user-')) { - stdout.write(`${info.name.substring('rpc-user-'.length)}\n`) - } - } - } -} diff --git a/packages/helia-cli/src/commands/status.ts b/packages/helia-cli/src/commands/status.ts deleted file mode 100644 index 7c40d250..00000000 --- a/packages/helia-cli/src/commands/status.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { Command, RootArgs } from '@helia/cli-utils' -import fs from 'node:fs' -import { logger } from '@libp2p/logger' -import { findOnlineHelia } from '@helia/cli-utils/find-helia' - -const log = logger('helia:cli:commands:status') - -export const status: Command = { - command: 'status', - description: 'Report the status of the Helia daemon', - example: '$ helia status', - async execute ({ directory, rpcAddress, stdout, user }) { - // socket file? - const socketFilePath = rpcAddress - - if (fs.existsSync(socketFilePath)) { - log(`Found socket file at ${socketFilePath}`) - - const { - helia, libp2p - } = await findOnlineHelia(directory, rpcAddress, user) - - if (libp2p != null) { - await libp2p.stop() - } - - if (helia == null) { - log(`Removing stale socket file at ${socketFilePath}`) - fs.rmSync(socketFilePath) - } else { - stdout.write('The daemon is running\n') - return - } - } else { - log(`Could not find socket file at ${socketFilePath}`) - } - - stdout.write('The daemon is not running\n') - } -} diff --git a/packages/helia-cli/src/index.ts b/packages/helia-cli/src/index.ts deleted file mode 100644 index 706210f5..00000000 --- a/packages/helia-cli/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -#! /usr/bin/env node --trace-warnings -/* eslint-disable no-console */ - -import { cli } from '@helia/cli-utils' -import kleur from 'kleur' -import { commands } from './commands/index.js' - -async function main (): Promise { - const command = 'helia' - const description = `${kleur.bold('Helia')} is an ${kleur.cyan('IPFS')} implementation written in ${kleur.yellow('JavaScript')}` - - await cli(command, description, commands) -} - -main().catch(err => { - console.error(err) // eslint-disable-line no-console - process.exit(1) -}) diff --git a/packages/helia-cli/test/index.spec.ts b/packages/helia-cli/test/index.spec.ts deleted file mode 100644 index e67dc48c..00000000 --- a/packages/helia-cli/test/index.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect } from 'aegir/chai' - -describe('cli', () => { - it('should start a node', () => { - expect(true).to.be.ok() - }) -}) diff --git a/packages/helia-cli/tsconfig.json b/packages/helia-cli/tsconfig.json deleted file mode 100644 index 013a40fe..00000000 --- a/packages/helia-cli/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src", - "test" - ], - "references": [ - { - "path": "../cli-utils" - }, - { - "path": "../interface" - }, - { - "path": "../rpc-server" - } - ] -} diff --git a/packages/rpc-client/.aegir.js b/packages/rpc-client/.aegir.js deleted file mode 100644 index e9c18f3e..00000000 --- a/packages/rpc-client/.aegir.js +++ /dev/null @@ -1,6 +0,0 @@ - -export default { - build: { - bundle: false - } -} diff --git a/packages/rpc-client/LICENSE b/packages/rpc-client/LICENSE deleted file mode 100644 index 20ce483c..00000000 --- a/packages/rpc-client/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -This project is dual licensed under MIT and Apache-2.0. - -MIT: https://www.opensource.org/licenses/mit -Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/rpc-client/LICENSE-APACHE b/packages/rpc-client/LICENSE-APACHE deleted file mode 100644 index 14478a3b..00000000 --- a/packages/rpc-client/LICENSE-APACHE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/rpc-client/LICENSE-MIT b/packages/rpc-client/LICENSE-MIT deleted file mode 100644 index 72dc60d8..00000000 --- a/packages/rpc-client/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/packages/rpc-client/README.md b/packages/rpc-client/README.md deleted file mode 100644 index 5b3bd630..00000000 --- a/packages/rpc-client/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# @helia/rpc-client - -[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) -[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) - -> An implementation of IPFS in JavaScript - -## Table of contents - -- [Install](#install) - - [Browser ` -``` - -## API Docs - -- - -## License - -Licensed under either of - -- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) -- MIT ([LICENSE-MIT](LICENSE-MIT) / ) - -## Contribute - -Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). - -Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. - -Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/rpc-client/package.json b/packages/rpc-client/package.json deleted file mode 100644 index 50b561b5..00000000 --- a/packages/rpc-client/package.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "name": "@helia/rpc-client", - "version": "0.0.0", - "description": "An implementation of IPFS in JavaScript", - "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/ipfs/helia/tree/master/packages/rpc-client#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/ipfs/helia.git" - }, - "bugs": { - "url": "https://github.com/ipfs/helia/issues" - }, - "keywords": [ - "IPFS" - ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "eslintConfig": { - "extends": "ipfs", - "parserOptions": { - "sourceType": "module" - } - }, - "release": { - "branches": [ - "main" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, - "scripts": { - "clean": "aegir clean", - "lint": "aegir lint", - "dep-check": "aegir dep-check", - "build": "aegir build", - "test": "aegir test", - "test:chrome": "aegir test -t browser --cov", - "test:chrome-webworker": "aegir test -t webworker", - "test:firefox": "aegir test -t browser -- --browser firefox", - "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", - "test:node": "aegir test -t node --cov", - "test:electron-main": "aegir test -t electron-main", - "release": "aegir release" - }, - "dependencies": { - "@helia/interface": "~0.0.0", - "@helia/rpc-protocol": "~0.0.0", - "@libp2p/interface-libp2p": "^1.1.0", - "@libp2p/logger": "^2.0.5", - "@libp2p/peer-id": "^2.0.0", - "@multiformats/multiaddr": "^11.1.5", - "interface-blockstore": "^4.0.1", - "it-first": "^2.0.0", - "it-pb-stream": "^2.0.3", - "multiformats": "^11.0.1" - }, - "devDependencies": { - "aegir": "^38.1.0" - }, - "typedoc": { - "entryPoint": "./src/index.ts" - } -} diff --git a/packages/rpc-client/src/commands/authorization/get.ts b/packages/rpc-client/src/commands/authorization/get.ts deleted file mode 100644 index f78fd3a2..00000000 --- a/packages/rpc-client/src/commands/authorization/get.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/authorization' -import type { HeliaRpcClientConfig } from '../../index.js' -import { unaryCall } from '../utils/rpc-call.js' - -export function createAuthorizationGet (config: HeliaRpcClientConfig): (user: string, options?: any) => Promise { - return unaryCall({ - resource: '/authorization/get', - optionsCodec: GetOptions, - transformInput: (user) => { - return { - user - } - }, - outputCodec: GetResponse, - transformOutput: (obj) => { - return obj.authorization - } - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/batch.ts b/packages/rpc-client/src/commands/blockstore/batch.ts deleted file mode 100644 index b7a753e8..00000000 --- a/packages/rpc-client/src/commands/blockstore/batch.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { BatchOptions, BatchRequest, BatchRequestDelete, BatchRequestPut, BatchRequestType } from '@helia/rpc-protocol/blockstore' -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import type { CID } from 'multiformats/cid' -import { RPCCallMessage, RPCCallRequest, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import { HELIA_RPC_PROTOCOL } from '@helia/rpc-protocol' -import { pbStream } from 'it-pb-stream' -import type { Pair, Batch } from 'interface-blockstore' - -export function createBlockstoreBatch (config: HeliaRpcMethodConfig): Helia['blockstore']['batch'] { - const batch = (): Batch => { - let puts: Pair[] = [] - let dels: CID[] = [] - - const batch: Batch = { - put (key, value) { - puts.push({ key, value }) - }, - - delete (key) { - dels.push(key) - }, - - commit: async (options) => { - const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) - - try { - const stream = pbStream(duplex) - - stream.writePB({ - resource: '/blockstore/batch', - method: 'INVOKE', - authorization: config.authorization, - options: BatchOptions.encode({ - ...options - }) - }, RPCCallRequest) - - for (const { key, value } of puts) { - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: BatchRequest.encode({ - type: BatchRequestType.BATCH_REQUEST_PUT, - message: BatchRequestPut.encode({ - cid: key.bytes, - block: value - }) - }) - }, RPCCallMessage) - } - - puts = [] - - for (const cid of dels) { - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: BatchRequest.encode({ - type: BatchRequestType.BATCH_REQUEST_DELETE, - message: BatchRequestDelete.encode({ - cid: cid.bytes - }) - }) - }, RPCCallMessage) - } - - dels = [] - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: BatchRequest.encode({ - type: BatchRequestType.BATCH_REQUEST_COMMIT - }) - }, RPCCallMessage) - } finally { - duplex.close() - } - } - } - - return batch - } - - return batch -} diff --git a/packages/rpc-client/src/commands/blockstore/close.ts b/packages/rpc-client/src/commands/blockstore/close.ts deleted file mode 100644 index eea558ef..00000000 --- a/packages/rpc-client/src/commands/blockstore/close.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import { unaryCall } from '../utils/rpc-call.js' -import { CloseOptions } from '@helia/rpc-protocol/blockstore' - -export function createBlockstoreClose (config: HeliaRpcMethodConfig): Helia['blockstore']['close'] { - return unaryCall({ - resource: '/blockstore/close', - optionsCodec: CloseOptions - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/delete-many.ts b/packages/rpc-client/src/commands/blockstore/delete-many.ts deleted file mode 100644 index f7901855..00000000 --- a/packages/rpc-client/src/commands/blockstore/delete-many.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { DeleteManyOptions, DeleteManyRequest, DeleteManyResponse } from '@helia/rpc-protocol/blockstore' -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import { CID } from 'multiformats/cid' -import { streamingCall } from '../utils/rpc-call.js' - -export function createBlockstoreDeleteMany (config: HeliaRpcMethodConfig): Helia['blockstore']['deleteMany'] { - return streamingCall({ - resource: '/blockstore/delete-many', - optionsCodec: DeleteManyOptions, - transformInput: (cid: CID) => { - return { - cid: cid.bytes - } - }, - inputCodec: DeleteManyRequest, - outputCodec: DeleteManyResponse, - transformOutput: (obj: DeleteManyResponse) => { - return CID.decode(obj.cid) - } - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/delete.ts b/packages/rpc-client/src/commands/blockstore/delete.ts deleted file mode 100644 index a1316fb8..00000000 --- a/packages/rpc-client/src/commands/blockstore/delete.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import type { CID } from 'multiformats/cid' -import { unaryCall } from '../utils/rpc-call.js' -import { DeleteOptions, DeleteRequest } from '@helia/rpc-protocol/blockstore' - -export function createBlockstoreDelete (config: HeliaRpcMethodConfig): Helia['blockstore']['delete'] { - return unaryCall({ - resource: '/blockstore/delete', - optionsCodec: DeleteOptions, - transformInput: (cid: CID) => { - return { - cid: cid.bytes - } - }, - inputCodec: DeleteRequest - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/get-many.ts b/packages/rpc-client/src/commands/blockstore/get-many.ts deleted file mode 100644 index b17659c6..00000000 --- a/packages/rpc-client/src/commands/blockstore/get-many.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GetManyOptions, GetManyRequest, GetManyResponse } from '@helia/rpc-protocol/blockstore' -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import type { CID } from 'multiformats/cid' -import { streamingCall } from '../utils/rpc-call.js' - -export function createBlockstoreGetMany (config: HeliaRpcMethodConfig): Helia['blockstore']['getMany'] { - return streamingCall({ - resource: '/blockstore/get-many', - optionsCodec: GetManyOptions, - transformInput: (cid: CID) => { - return { - cid: cid.bytes - } - }, - inputCodec: GetManyRequest, - outputCodec: GetManyResponse, - transformOutput: (obj) => { - return obj.block - } - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/get.ts b/packages/rpc-client/src/commands/blockstore/get.ts deleted file mode 100644 index 94141f2e..00000000 --- a/packages/rpc-client/src/commands/blockstore/get.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import type { CID } from 'multiformats/cid' -import { unaryCall } from '../utils/rpc-call.js' - -export function createBlockstoreGet (config: HeliaRpcMethodConfig): Helia['blockstore']['get'] { - return unaryCall({ - resource: '/blockstore/get', - optionsCodec: GetOptions, - transformInput: (cid: CID) => { - return { - cid: cid.bytes - } - }, - inputCodec: GetRequest, - outputCodec: GetResponse, - transformOutput: (obj: GetResponse): Uint8Array => { - return obj.block - } - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/has.ts b/packages/rpc-client/src/commands/blockstore/has.ts deleted file mode 100644 index 9f660360..00000000 --- a/packages/rpc-client/src/commands/blockstore/has.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { HasOptions, HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import type { CID } from 'multiformats/cid' -import { unaryCall } from '../utils/rpc-call.js' - -export function createBlockstoreHas (config: HeliaRpcMethodConfig): Helia['blockstore']['has'] { - return unaryCall({ - resource: '/blockstore/has', - optionsCodec: HasOptions, - transformInput: (cid: CID) => { - return { - cid: cid.bytes - } - }, - inputCodec: HasRequest, - outputCodec: HasResponse, - transformOutput: (obj) => { - return obj.has - } - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/open.ts b/packages/rpc-client/src/commands/blockstore/open.ts deleted file mode 100644 index a9473035..00000000 --- a/packages/rpc-client/src/commands/blockstore/open.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import { unaryCall } from '../utils/rpc-call.js' -import { OpenOptions } from '@helia/rpc-protocol/blockstore' - -export function createBlockstoreOpen (config: HeliaRpcMethodConfig): Helia['blockstore']['open'] { - return unaryCall({ - resource: '/blockstore/open', - optionsCodec: OpenOptions - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/put-many.ts b/packages/rpc-client/src/commands/blockstore/put-many.ts deleted file mode 100644 index 9029c310..00000000 --- a/packages/rpc-client/src/commands/blockstore/put-many.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { PutManyOptions, PutManyRequest, PutManyResponse } from '@helia/rpc-protocol/blockstore' -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import { streamingCall } from '../utils/rpc-call.js' -import type { Pair } from 'interface-blockstore' - -export function createBlockstorePutMany (config: HeliaRpcMethodConfig): Helia['blockstore']['putMany'] { - return streamingCall({ - resource: '/blockstore/put-many', - optionsCodec: PutManyOptions, - transformInput: (pair: Pair) => { - return { - cid: pair.key.bytes, - block: pair.value - } - }, - inputCodec: PutManyRequest, - outputCodec: PutManyResponse, - transformOutput: (obj): Uint8Array => { - return obj.block - } - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/put.ts b/packages/rpc-client/src/commands/blockstore/put.ts deleted file mode 100644 index 704912ab..00000000 --- a/packages/rpc-client/src/commands/blockstore/put.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { PutOptions, PutRequest } from '@helia/rpc-protocol/blockstore' -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import type { Pair } from 'interface-blockstore' -import { unaryCall } from '../utils/rpc-call.js' - -export function createBlockstorePut (config: HeliaRpcMethodConfig): Helia['blockstore']['put'] { - return unaryCall({ - resource: '/blockstore/put', - optionsCodec: PutOptions, - transformInput: (pair: Pair): PutRequest => { - return { - cid: pair.key.bytes, - block: pair.value - } - }, - inputCodec: PutRequest - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/query-keys.ts b/packages/rpc-client/src/commands/blockstore/query-keys.ts deleted file mode 100644 index 1ed75c5f..00000000 --- a/packages/rpc-client/src/commands/blockstore/query-keys.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import { CID } from 'multiformats/cid' -import { QueryKeysOptions, QueryKeysRequest, QueryKeysResponse } from '@helia/rpc-protocol/blockstore' -import { streamingCall } from '../utils/rpc-call.js' - -export function createBlockstoreQueryKeys (config: HeliaRpcMethodConfig): Helia['blockstore']['queryKeys'] { - return streamingCall({ - resource: '/blockstore/query-keys', - optionsCodec: QueryKeysOptions, - outputCodec: QueryKeysResponse, - transformOutput: (obj: QueryKeysResponse) => { - return CID.decode(obj.key) - } - })(config) -} diff --git a/packages/rpc-client/src/commands/blockstore/query.ts b/packages/rpc-client/src/commands/blockstore/query.ts deleted file mode 100644 index 43277a43..00000000 --- a/packages/rpc-client/src/commands/blockstore/query.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Helia } from '@helia/interface' -import type { HeliaRpcMethodConfig } from '../../index.js' -import { CID } from 'multiformats/cid' -import { QueryOptions, QueryRequest, QueryResponse } from '@helia/rpc-protocol/blockstore' -import { streamingCall } from '../utils/rpc-call.js' - -export function createBlockstoreQuery (config: HeliaRpcMethodConfig): Helia['blockstore']['query'] { - return streamingCall({ - resource: '/blockstore/query', - optionsCodec: QueryOptions, - outputCodec: QueryResponse, - transformOutput: (obj: QueryResponse) => { - return { - key: CID.decode(obj.key), - value: obj.value - } - } - })(config) -} diff --git a/packages/rpc-client/src/commands/info.ts b/packages/rpc-client/src/commands/info.ts deleted file mode 100644 index 54bb0e11..00000000 --- a/packages/rpc-client/src/commands/info.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { multiaddr } from '@multiformats/multiaddr' -import { InfoOptions, InfoResponse } from '@helia/rpc-protocol/root' -import type { Helia } from '@helia/interface' -import { peerIdFromString } from '@libp2p/peer-id' -import type { HeliaRpcMethodConfig } from '../index.js' -import { unaryCall } from './utils/rpc-call.js' - -export function createInfo (config: HeliaRpcMethodConfig): Helia['info'] { - return unaryCall({ - resource: '/info', - optionsCodec: InfoOptions, - transformOptions: (obj) => { - return { - ...obj, - peerId: obj.peerId != null ? obj.peerId.toString() : undefined - } - }, - outputCodec: InfoResponse, - transformOutput: (obj) => { - return { - ...obj, - peerId: peerIdFromString(obj.peerId), - multiaddrs: obj.multiaddrs.map(str => multiaddr(str)) - } - } - })(config) -} diff --git a/packages/rpc-client/src/commands/utils/rpc-call.ts b/packages/rpc-client/src/commands/utils/rpc-call.ts deleted file mode 100644 index 5082d785..00000000 --- a/packages/rpc-client/src/commands/utils/rpc-call.ts +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint max-depth: ["error", 5] */ - -import { RPCCallMessage, RPCCallRequest, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import { HELIA_RPC_PROTOCOL, RPCError, RPCProgressEvent } from '@helia/rpc-protocol' -import type { HeliaRpcMethodConfig } from '../../index.js' -import { pbStream } from 'it-pb-stream' -import first from 'it-first' -import { logger } from '@libp2p/logger' - -const log = logger('helia:rpc-client:utils:rpc-call') - -export interface Codec { - encode: (type: T) => Uint8Array - decode: (buf: Uint8Array) => T -} - -export interface CallOptions { - resource: string - optionsCodec: Codec - transformOptions?: (obj: any) => Options - transformInput?: (obj: any) => Request - inputCodec?: Codec - outputCodec?: Codec - transformOutput?: (obj: Response) => any -} - -export function streamingCall (opts: CallOptions): (config: HeliaRpcMethodConfig) => any { - return function createStreamingCall (config: HeliaRpcMethodConfig): any { - const streamingCall: any = async function * (source: any, options: any = {}) { - const duplex = await config.libp2p.dialProtocol(config.multiaddr, HELIA_RPC_PROTOCOL) - const stream = pbStream(duplex) - - stream.writePB({ - resource: opts.resource, - method: 'INVOKE', - authorization: config.authorization, - options: opts.optionsCodec.encode(opts.transformOptions == null ? options : opts.transformOptions(options)) - }, RPCCallRequest) - - void Promise.resolve().then(async () => { - for await (const input of source) { - let message: Uint8Array | undefined - - if (opts.inputCodec != null) { - message = opts.inputCodec.encode(opts.transformInput == null ? input : opts.transformInput(input)) - } - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message - }, RPCCallMessage) - } - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_DONE - }, RPCCallMessage) - }) - .catch(err => { - log('error encountered while sending RPC messages', err) - }) - .finally(() => { - duplex.closeWrite() - }) - - try { - while (true) { - const response = await stream.readPB(RPCCallMessage) - - switch (response.type) { - case RPCCallMessageType.RPC_CALL_DONE: - return - case RPCCallMessageType.RPC_CALL_ERROR: - throw new RPCError(response.message) - case RPCCallMessageType.RPC_CALL_MESSAGE: - if (opts.outputCodec != null) { - let message = opts.outputCodec.decode(response.message) - - if (opts.transformOutput != null) { - message = opts.transformOutput(message) - } - - yield message - } - continue - case RPCCallMessageType.RPC_CALL_PROGRESS: - if (options.progress != null) { - options.progress(new RPCProgressEvent(response.message)) - } - continue - default: - throw new Error('Unknown RPCCallMessageType') - } - } - } finally { - duplex.closeRead() - } - } - - return streamingCall - } -} - -export function unaryCall (opts: CallOptions): (config: HeliaRpcMethodConfig) => any { - return function createStreamingCall (config: HeliaRpcMethodConfig): any { - const unaryCall: any = async function (arg: any, options: any = {}): Promise { - const fn: any = streamingCall(opts)(config) - return await first(fn([arg], options)) - } - - return unaryCall - } -} diff --git a/packages/rpc-client/src/index.ts b/packages/rpc-client/src/index.ts deleted file mode 100644 index 8feb3228..00000000 --- a/packages/rpc-client/src/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { Helia } from '@helia/interface' -import { createInfo } from './commands/info.js' -import type { Libp2p } from '@libp2p/interface-libp2p' -import type { Multiaddr } from '@multiformats/multiaddr' -import { createBlockstoreDelete } from './commands/blockstore/delete.js' -import { createBlockstoreGet } from './commands/blockstore/get.js' -import { createBlockstoreHas } from './commands/blockstore/has.js' -import { createBlockstorePut } from './commands/blockstore/put.js' -import { createAuthorizationGet } from './commands/authorization/get.js' -import { createBlockstoreDeleteMany } from './commands/blockstore/delete-many.js' -import { createBlockstoreGetMany } from './commands/blockstore/get-many.js' -import { createBlockstorePutMany } from './commands/blockstore/put-many.js' -import { createBlockstoreClose } from './commands/blockstore/close.js' -import { createBlockstoreOpen } from './commands/blockstore/open.js' -import { createBlockstoreBatch } from './commands/blockstore/batch.js' -import { createBlockstoreQueryKeys } from './commands/blockstore/query-keys.js' -import { createBlockstoreQuery } from './commands/blockstore/query.js' - -export interface HeliaRpcClientConfig { - multiaddr: Multiaddr - libp2p: Libp2p - user: string -} - -export interface HeliaRpcMethodConfig { - multiaddr: Multiaddr - libp2p: Libp2p - authorization?: string -} - -export async function createHeliaRpcClient (config: HeliaRpcClientConfig): Promise { - await config.libp2p.dial(config.multiaddr) - - const getAuthorization = createAuthorizationGet(config) - const authorization = await getAuthorization(config.user) - const methodConfig = { - ...config, - authorization - } - - return { - info: createInfo(methodConfig), - blockstore: { - batch: createBlockstoreBatch(methodConfig), - close: createBlockstoreClose(methodConfig), - deleteMany: createBlockstoreDeleteMany(methodConfig), - delete: createBlockstoreDelete(methodConfig), - getMany: createBlockstoreGetMany(methodConfig), - get: createBlockstoreGet(methodConfig), - has: createBlockstoreHas(methodConfig), - open: createBlockstoreOpen(methodConfig), - putMany: createBlockstorePutMany(methodConfig), - put: createBlockstorePut(methodConfig), - queryKeys: createBlockstoreQueryKeys(methodConfig), - query: createBlockstoreQuery(methodConfig) - }, - // @ts-expect-error incomplete implementation - datastore: { - - }, - // @ts-expect-error incomplete implementation - libp2p: { - - }, - async stop () { - throw new Error('Not implemented') - } - } -} diff --git a/packages/rpc-client/test/index.spec.ts b/packages/rpc-client/test/index.spec.ts deleted file mode 100644 index 86e6f958..00000000 --- a/packages/rpc-client/test/index.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-env mocha */ - -import '../src/index.js' - -describe('rpc-client', () => { - it('should work', async () => { - - }) -}) diff --git a/packages/rpc-client/tsconfig.json b/packages/rpc-client/tsconfig.json deleted file mode 100644 index 021053e1..00000000 --- a/packages/rpc-client/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src", - "test" - ], - "references": [ - { - "path": "../interface" - }, - { - "path": "../rpc-protocol" - } - ] -} diff --git a/packages/rpc-protocol/LICENSE b/packages/rpc-protocol/LICENSE deleted file mode 100644 index 20ce483c..00000000 --- a/packages/rpc-protocol/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -This project is dual licensed under MIT and Apache-2.0. - -MIT: https://www.opensource.org/licenses/mit -Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/rpc-protocol/LICENSE-APACHE b/packages/rpc-protocol/LICENSE-APACHE deleted file mode 100644 index 14478a3b..00000000 --- a/packages/rpc-protocol/LICENSE-APACHE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/rpc-protocol/LICENSE-MIT b/packages/rpc-protocol/LICENSE-MIT deleted file mode 100644 index 72dc60d8..00000000 --- a/packages/rpc-protocol/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/packages/rpc-protocol/README.md b/packages/rpc-protocol/README.md deleted file mode 100644 index d8a827b4..00000000 --- a/packages/rpc-protocol/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# @helia/rpc-protocol - -[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) -[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) - -> RPC protocol for use by @helia/rpc-client and @helia/rpc-server - -## Table of contents - -- [Install](#install) -- [API Docs](#api-docs) -- [License](#license) -- [Contribute](#contribute) - -## Install - -```console -$ npm i @helia/rpc-protocol -``` - -## API Docs - -- - -## License - -Licensed under either of - -- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) -- MIT ([LICENSE-MIT](LICENSE-MIT) / ) - -## Contribute - -Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). - -Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. - -Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json deleted file mode 100644 index b3bb9a64..00000000 --- a/packages/rpc-protocol/package.json +++ /dev/null @@ -1,177 +0,0 @@ -{ - "name": "@helia/rpc-protocol", - "version": "0.0.0", - "description": "RPC protocol for use by @helia/rpc-client and @helia/rpc-server", - "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/ipfs/helia/tree/master/packages/rpc-protocol#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/ipfs/helia.git" - }, - "bugs": { - "url": "https://github.com/ipfs/helia/issues" - }, - "keywords": [ - "IPFS" - ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "typesVersions": { - "*": { - "*": [ - "*", - "dist/*", - "dist/src/*", - "dist/src/*/index" - ], - "src/*": [ - "*", - "dist/*", - "dist/src/*", - "dist/src/*/index" - ] - } - }, - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - }, - "./authorization": { - "types": "./dist/src/authorization.d.ts", - "import": "./dist/src/authorization.js" - }, - "./blockstore": { - "types": "./dist/src/blockstore.d.ts", - "import": "./dist/src/blockstore.js" - }, - "./root": { - "types": "./dist/src/root.d.ts", - "import": "./dist/src/root.js" - }, - "./rpc": { - "types": "./dist/src/rpc.d.ts", - "import": "./dist/src/rpc.js" - } - }, - "eslintConfig": { - "extends": "ipfs", - "parserOptions": { - "sourceType": "module" - } - }, - "release": { - "branches": [ - "main" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, - "scripts": { - "clean": "aegir clean", - "lint": "aegir lint", - "dep-check": "aegir dep-check -i protons", - "build": "aegir build", - "release": "aegir release", - "generate": "protons src/*.proto" - }, - "dependencies": { - "protons-runtime": "^5.0.0", - "uint8arraylist": "^2.4.3" - }, - "devDependencies": { - "aegir": "^38.1.0", - "protons": "^7.0.0" - }, - "typedoc": { - "entryPoint": "./src/index.ts" - } -} diff --git a/packages/rpc-protocol/src/authorization.proto b/packages/rpc-protocol/src/authorization.proto deleted file mode 100644 index 9815f4b4..00000000 --- a/packages/rpc-protocol/src/authorization.proto +++ /dev/null @@ -1,13 +0,0 @@ -syntax = "proto3"; - -message GetOptions { - -} - -message GetRequest { - string user = 1; -} - -message GetResponse { - string authorization = 1; -} diff --git a/packages/rpc-protocol/src/authorization.ts b/packages/rpc-protocol/src/authorization.ts deleted file mode 100644 index 2f839d6b..00000000 --- a/packages/rpc-protocol/src/authorization.ts +++ /dev/null @@ -1,171 +0,0 @@ -/* eslint-disable import/export */ -/* eslint-disable complexity */ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import { encodeMessage, decodeMessage, message } from 'protons-runtime' -import type { Codec } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' - -export interface GetOptions {} - -export namespace GetOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetOptions => { - return decodeMessage(buf, GetOptions.codec()) - } -} - -export interface GetRequest { - user: string -} - -export namespace GetRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.user != null && obj.user !== '')) { - w.uint32(10) - w.string(obj.user ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - user: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.user = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetRequest => { - return decodeMessage(buf, GetRequest.codec()) - } -} - -export interface GetResponse { - authorization: string -} - -export namespace GetResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.authorization != null && obj.authorization !== '')) { - w.uint32(10) - w.string(obj.authorization ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - authorization: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.authorization = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse => { - return decodeMessage(buf, GetResponse.codec()) - } -} diff --git a/packages/rpc-protocol/src/blockstore.proto b/packages/rpc-protocol/src/blockstore.proto deleted file mode 100644 index 0ed107b3..00000000 --- a/packages/rpc-protocol/src/blockstore.proto +++ /dev/null @@ -1,170 +0,0 @@ -syntax = "proto3"; - -message Pair { - bytes cid = 1; - bytes block = 2; -} - -message OpenOptions { - -} - -message OpenRequest { - -} - -message OpenResponse { - -} - -message CloseOptions { - -} - -message CloseRequest { - -} - -message CloseResponse { - -} - -message PutOptions { - -} - -message PutRequest { - bytes cid = 1; - bytes block = 2; -} - -message PutResponse { - -} - -message GetOptions { - -} - -message GetRequest { - bytes cid = 1; -} - -message GetResponse { - bytes block = 2; -} - -message HasOptions { - -} - -message HasRequest { - bytes cid = 1; -} - -message HasResponse { - bool has = 1; -} - -message DeleteOptions { - -} - -message DeleteRequest { - bytes cid = 1; -} - -message DeleteResponse { - -} - -message PutManyOptions { - -} - -message PutManyRequest { - bytes cid = 1; - bytes block = 2; -} - -message PutManyResponse { - bytes cid = 1; - bytes block = 2; -} - -message GetManyOptions { - -} - -message GetManyRequest { - bytes cid = 1; -} - -message GetManyResponse { - bytes block = 1; -} - -message DeleteManyOptions { - -} - -message DeleteManyRequest { - bytes cid = 1; -} - -message DeleteManyResponse { - bytes cid = 1; -} - -message BatchOptions { - -} - -enum BatchRequestType { - BATCH_REQUEST_PUT = 0; - BATCH_REQUEST_DELETE = 1; - BATCH_REQUEST_COMMIT = 2; -} - -message BatchRequest { - BatchRequestType type = 1; - bytes message = 2; -} - -message BatchRequestPut { - bytes cid = 1; - bytes block = 2; -} - -message BatchRequestDelete { - bytes cid = 1; -} - -message BatchResponse { - -} - -message QueryOptions { - -} - -message QueryRequest { - -} - -message QueryResponse { - bytes key = 1; - bytes value = 2; -} - -message QueryKeysOptions { - -} - -message QueryKeysRequest { - -} - -message QueryKeysResponse { - bytes key = 1; -} diff --git a/packages/rpc-protocol/src/blockstore.ts b/packages/rpc-protocol/src/blockstore.ts deleted file mode 100644 index 7b27753b..00000000 --- a/packages/rpc-protocol/src/blockstore.ts +++ /dev/null @@ -1,2106 +0,0 @@ -/* eslint-disable import/export */ -/* eslint-disable complexity */ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' -import type { Codec } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' - -export interface Pair { - cid: Uint8Array - block: Uint8Array -} - -export namespace Pair { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.block ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0), - block: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - case 2: - obj.block = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, Pair.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): Pair => { - return decodeMessage(buf, Pair.codec()) - } -} - -export interface OpenOptions {} - -export namespace OpenOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, OpenOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): OpenOptions => { - return decodeMessage(buf, OpenOptions.codec()) - } -} - -export interface OpenRequest {} - -export namespace OpenRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, OpenRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): OpenRequest => { - return decodeMessage(buf, OpenRequest.codec()) - } -} - -export interface OpenResponse {} - -export namespace OpenResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, OpenResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): OpenResponse => { - return decodeMessage(buf, OpenResponse.codec()) - } -} - -export interface CloseOptions {} - -export namespace CloseOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, CloseOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): CloseOptions => { - return decodeMessage(buf, CloseOptions.codec()) - } -} - -export interface CloseRequest {} - -export namespace CloseRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, CloseRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): CloseRequest => { - return decodeMessage(buf, CloseRequest.codec()) - } -} - -export interface CloseResponse {} - -export namespace CloseResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, CloseResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): CloseResponse => { - return decodeMessage(buf, CloseResponse.codec()) - } -} - -export interface PutOptions {} - -export namespace PutOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutOptions => { - return decodeMessage(buf, PutOptions.codec()) - } -} - -export interface PutRequest { - cid: Uint8Array - block: Uint8Array -} - -export namespace PutRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.block ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0), - block: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - case 2: - obj.block = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutRequest => { - return decodeMessage(buf, PutRequest.codec()) - } -} - -export interface PutResponse {} - -export namespace PutResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutResponse => { - return decodeMessage(buf, PutResponse.codec()) - } -} - -export interface GetOptions {} - -export namespace GetOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetOptions => { - return decodeMessage(buf, GetOptions.codec()) - } -} - -export interface GetRequest { - cid: Uint8Array -} - -export namespace GetRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetRequest => { - return decodeMessage(buf, GetRequest.codec()) - } -} - -export interface GetResponse { - block: Uint8Array -} - -export namespace GetResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.block ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - block: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 2: - obj.block = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse => { - return decodeMessage(buf, GetResponse.codec()) - } -} - -export interface HasOptions {} - -export namespace HasOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, HasOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): HasOptions => { - return decodeMessage(buf, HasOptions.codec()) - } -} - -export interface HasRequest { - cid: Uint8Array -} - -export namespace HasRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, HasRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): HasRequest => { - return decodeMessage(buf, HasRequest.codec()) - } -} - -export interface HasResponse { - has: boolean -} - -export namespace HasResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.has != null && obj.has !== false)) { - w.uint32(8) - w.bool(obj.has ?? false) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - has: false - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.has = reader.bool() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, HasResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): HasResponse => { - return decodeMessage(buf, HasResponse.codec()) - } -} - -export interface DeleteOptions {} - -export namespace DeleteOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteOptions => { - return decodeMessage(buf, DeleteOptions.codec()) - } -} - -export interface DeleteRequest { - cid: Uint8Array -} - -export namespace DeleteRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteRequest => { - return decodeMessage(buf, DeleteRequest.codec()) - } -} - -export interface DeleteResponse {} - -export namespace DeleteResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteResponse => { - return decodeMessage(buf, DeleteResponse.codec()) - } -} - -export interface PutManyOptions {} - -export namespace PutManyOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutManyOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyOptions => { - return decodeMessage(buf, PutManyOptions.codec()) - } -} - -export interface PutManyRequest { - cid: Uint8Array - block: Uint8Array -} - -export namespace PutManyRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.block ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0), - block: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - case 2: - obj.block = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutManyRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyRequest => { - return decodeMessage(buf, PutManyRequest.codec()) - } -} - -export interface PutManyResponse { - cid: Uint8Array - block: Uint8Array -} - -export namespace PutManyResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.block ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0), - block: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - case 2: - obj.block = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutManyResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyResponse => { - return decodeMessage(buf, PutManyResponse.codec()) - } -} - -export interface GetManyOptions {} - -export namespace GetManyOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetManyOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyOptions => { - return decodeMessage(buf, GetManyOptions.codec()) - } -} - -export interface GetManyRequest { - cid: Uint8Array -} - -export namespace GetManyRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetManyRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyRequest => { - return decodeMessage(buf, GetManyRequest.codec()) - } -} - -export interface GetManyResponse { - block: Uint8Array -} - -export namespace GetManyResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.block ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - block: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.block = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetManyResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyResponse => { - return decodeMessage(buf, GetManyResponse.codec()) - } -} - -export interface DeleteManyOptions {} - -export namespace DeleteManyOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteManyOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyOptions => { - return decodeMessage(buf, DeleteManyOptions.codec()) - } -} - -export interface DeleteManyRequest { - cid: Uint8Array -} - -export namespace DeleteManyRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteManyRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyRequest => { - return decodeMessage(buf, DeleteManyRequest.codec()) - } -} - -export interface DeleteManyResponse { - cid: Uint8Array -} - -export namespace DeleteManyResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteManyResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyResponse => { - return decodeMessage(buf, DeleteManyResponse.codec()) - } -} - -export interface BatchOptions {} - -export namespace BatchOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, BatchOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): BatchOptions => { - return decodeMessage(buf, BatchOptions.codec()) - } -} - -export enum BatchRequestType { - BATCH_REQUEST_PUT = 'BATCH_REQUEST_PUT', - BATCH_REQUEST_DELETE = 'BATCH_REQUEST_DELETE', - BATCH_REQUEST_COMMIT = 'BATCH_REQUEST_COMMIT' -} - -enum __BatchRequestTypeValues { - BATCH_REQUEST_PUT = 0, - BATCH_REQUEST_DELETE = 1, - BATCH_REQUEST_COMMIT = 2 -} - -export namespace BatchRequestType { - export const codec = (): Codec => { - return enumeration(__BatchRequestTypeValues) - } -} -export interface BatchRequest { - type: BatchRequestType - message: Uint8Array -} - -export namespace BatchRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.type != null && __BatchRequestTypeValues[obj.type] !== 0)) { - w.uint32(8) - BatchRequestType.codec().encode(obj.type ?? BatchRequestType.BATCH_REQUEST_PUT, w) - } - - if (opts.writeDefaults === true || (obj.message != null && obj.message.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.message ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - type: BatchRequestType.BATCH_REQUEST_PUT, - message: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.type = BatchRequestType.codec().decode(reader) - break - case 2: - obj.message = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, BatchRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): BatchRequest => { - return decodeMessage(buf, BatchRequest.codec()) - } -} - -export interface BatchRequestPut { - cid: Uint8Array - block: Uint8Array -} - -export namespace BatchRequestPut { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.writeDefaults === true || (obj.block != null && obj.block.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.block ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0), - block: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - case 2: - obj.block = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, BatchRequestPut.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): BatchRequestPut => { - return decodeMessage(buf, BatchRequestPut.codec()) - } -} - -export interface BatchRequestDelete { - cid: Uint8Array -} - -export namespace BatchRequestDelete { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.cid != null && obj.cid.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.cid ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - cid: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.cid = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, BatchRequestDelete.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): BatchRequestDelete => { - return decodeMessage(buf, BatchRequestDelete.codec()) - } -} - -export interface BatchResponse {} - -export namespace BatchResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, BatchResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): BatchResponse => { - return decodeMessage(buf, BatchResponse.codec()) - } -} - -export interface QueryOptions {} - -export namespace QueryOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryOptions => { - return decodeMessage(buf, QueryOptions.codec()) - } -} - -export interface QueryRequest {} - -export namespace QueryRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryRequest => { - return decodeMessage(buf, QueryRequest.codec()) - } -} - -export interface QueryResponse { - key: Uint8Array - value: Uint8Array -} - -export namespace QueryResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.key ?? new Uint8Array(0)) - } - - if (opts.writeDefaults === true || (obj.value != null && obj.value.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.value ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: new Uint8Array(0), - value: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.bytes() - break - case 2: - obj.value = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryResponse => { - return decodeMessage(buf, QueryResponse.codec()) - } -} - -export interface QueryKeysOptions {} - -export namespace QueryKeysOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryKeysOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysOptions => { - return decodeMessage(buf, QueryKeysOptions.codec()) - } -} - -export interface QueryKeysRequest {} - -export namespace QueryKeysRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryKeysRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysRequest => { - return decodeMessage(buf, QueryKeysRequest.codec()) - } -} - -export interface QueryKeysResponse { - key: Uint8Array -} - -export namespace QueryKeysResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key.byteLength > 0)) { - w.uint32(10) - w.bytes(obj.key ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryKeysResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysResponse => { - return decodeMessage(buf, QueryKeysResponse.codec()) - } -} diff --git a/packages/rpc-protocol/src/datastore.proto b/packages/rpc-protocol/src/datastore.proto deleted file mode 100644 index 6f9c3702..00000000 --- a/packages/rpc-protocol/src/datastore.proto +++ /dev/null @@ -1,164 +0,0 @@ -syntax = "proto3"; - -message OpenOptions { - -} - -message OpenRequest { - -} - -message OpenResponse { - -} - -message CloseOptions { - -} - -message CloseRequest { - -} - -message CloseResponse { - -} - -message PutOptions { - -} - -message PutRequest { - string key = 1; - bytes value = 2; -} - -message PutResponse { - -} - -message GetOptions { - -} - -message GetRequest { - string key = 1; -} - -enum GetResponseType { - GET_PROGRESS = 0; - GET_RESULT = 1; -} - -message GetResponse { - GetResponseType type = 1; - optional bytes value = 2; - optional string progress_event_type = 3; - map progress_event_data = 4; -} - -message HasOptions { - -} - -message HasRequest { - string key = 1; -} - -message HasResponse { - bool has = 1; -} - -message DeleteOptions { - -} - -message DeleteRequest { - string key = 1; -} - -message DeleteResponse { - -} - -message PutManyOptions { - -} - -message PutManyRequest { - string key = 1; - bytes value = 2; -} - -message PutManyResponse { - string key = 1; - bytes value = 2; -} - -message GetManyOptions { - -} - -message GetManyRequest { - string key = 1; -} - -enum GetManyResponseType { - GET_MANY_PROGRESS = 0; - GET_MANY_RESULT = 1; -} - -message GetManyResponse { - GetManyResponseType type = 1; - optional bytes value = 2; - optional string progress_event_type = 3; - map progress_event_data = 4; -} - -message DeleteManyOptions { - -} - -message DeleteManyRequest { - string key = 1; -} - -message DeleteManyResponse { - string key = 1; -} - -message BatchOptions { - -} - -message BatchRequest { - -} - -message BatchResponse { - string id = 1; -} - -message QueryOptions { - -} - -message QueryRequest { - -} - -message QueryResponse { - -} - -message QueryKeysOptions { - -} - -message QueryKeysRequest { - -} - -message QueryKeysResponse { - -} diff --git a/packages/rpc-protocol/src/datastore.ts b/packages/rpc-protocol/src/datastore.ts deleted file mode 100644 index 46ae8d84..00000000 --- a/packages/rpc-protocol/src/datastore.ts +++ /dev/null @@ -1,2085 +0,0 @@ -/* eslint-disable import/export */ -/* eslint-disable complexity */ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' -import type { Codec } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' - -export interface OpenOptions {} - -export namespace OpenOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, OpenOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): OpenOptions => { - return decodeMessage(buf, OpenOptions.codec()) - } -} - -export interface OpenRequest {} - -export namespace OpenRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, OpenRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): OpenRequest => { - return decodeMessage(buf, OpenRequest.codec()) - } -} - -export interface OpenResponse {} - -export namespace OpenResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, OpenResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): OpenResponse => { - return decodeMessage(buf, OpenResponse.codec()) - } -} - -export interface CloseOptions {} - -export namespace CloseOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, CloseOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): CloseOptions => { - return decodeMessage(buf, CloseOptions.codec()) - } -} - -export interface CloseRequest {} - -export namespace CloseRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, CloseRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): CloseRequest => { - return decodeMessage(buf, CloseRequest.codec()) - } -} - -export interface CloseResponse {} - -export namespace CloseResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, CloseResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): CloseResponse => { - return decodeMessage(buf, CloseResponse.codec()) - } -} - -export interface PutOptions {} - -export namespace PutOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutOptions => { - return decodeMessage(buf, PutOptions.codec()) - } -} - -export interface PutRequest { - key: string - value: Uint8Array -} - -export namespace PutRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.writeDefaults === true || (obj.value != null && obj.value.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.value ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '', - value: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - case 2: - obj.value = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutRequest => { - return decodeMessage(buf, PutRequest.codec()) - } -} - -export interface PutResponse {} - -export namespace PutResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutResponse => { - return decodeMessage(buf, PutResponse.codec()) - } -} - -export interface GetOptions {} - -export namespace GetOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetOptions => { - return decodeMessage(buf, GetOptions.codec()) - } -} - -export interface GetRequest { - key: string -} - -export namespace GetRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetRequest => { - return decodeMessage(buf, GetRequest.codec()) - } -} - -export enum GetResponseType { - GET_PROGRESS = 'GET_PROGRESS', - GET_RESULT = 'GET_RESULT' -} - -enum __GetResponseTypeValues { - GET_PROGRESS = 0, - GET_RESULT = 1 -} - -export namespace GetResponseType { - export const codec = (): Codec => { - return enumeration(__GetResponseTypeValues) - } -} -export interface GetResponse { - type: GetResponseType - value?: Uint8Array - progressEventType?: string - progressEventData: Map -} - -export namespace GetResponse { - export interface GetResponse$progressEventDataEntry { - key: string - value: string - } - - export namespace GetResponse$progressEventDataEntry { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.writeDefaults === true || (obj.value != null && obj.value !== '')) { - w.uint32(18) - w.string(obj.value ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '', - value: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - case 2: - obj.value = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetResponse$progressEventDataEntry.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse$progressEventDataEntry => { - return decodeMessage(buf, GetResponse$progressEventDataEntry.codec()) - } - } - - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.type != null && __GetResponseTypeValues[obj.type] !== 0)) { - w.uint32(8) - GetResponseType.codec().encode(obj.type ?? GetResponseType.GET_PROGRESS, w) - } - - if (obj.value != null) { - w.uint32(18) - w.bytes(obj.value) - } - - if (obj.progressEventType != null) { - w.uint32(26) - w.string(obj.progressEventType) - } - - if (obj.progressEventData != null && obj.progressEventData.size !== 0) { - for (const [key, value] of obj.progressEventData.entries()) { - w.uint32(34) - GetResponse.GetResponse$progressEventDataEntry.codec().encode({ key, value }, w, { - writeDefaults: true - }) - } - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - type: GetResponseType.GET_PROGRESS, - progressEventData: new Map() - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.type = GetResponseType.codec().decode(reader) - break - case 2: - obj.value = reader.bytes() - break - case 3: - obj.progressEventType = reader.string() - break - case 4: { - const entry = GetResponse.GetResponse$progressEventDataEntry.codec().decode(reader, reader.uint32()) - obj.progressEventData.set(entry.key, entry.value) - break - } - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetResponse => { - return decodeMessage(buf, GetResponse.codec()) - } -} - -export interface HasOptions {} - -export namespace HasOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, HasOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): HasOptions => { - return decodeMessage(buf, HasOptions.codec()) - } -} - -export interface HasRequest { - key: string -} - -export namespace HasRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, HasRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): HasRequest => { - return decodeMessage(buf, HasRequest.codec()) - } -} - -export interface HasResponse { - has: boolean -} - -export namespace HasResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.has != null && obj.has !== false)) { - w.uint32(8) - w.bool(obj.has ?? false) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - has: false - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.has = reader.bool() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, HasResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): HasResponse => { - return decodeMessage(buf, HasResponse.codec()) - } -} - -export interface DeleteOptions {} - -export namespace DeleteOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteOptions => { - return decodeMessage(buf, DeleteOptions.codec()) - } -} - -export interface DeleteRequest { - key: string -} - -export namespace DeleteRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteRequest => { - return decodeMessage(buf, DeleteRequest.codec()) - } -} - -export interface DeleteResponse {} - -export namespace DeleteResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteResponse => { - return decodeMessage(buf, DeleteResponse.codec()) - } -} - -export interface PutManyOptions {} - -export namespace PutManyOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutManyOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyOptions => { - return decodeMessage(buf, PutManyOptions.codec()) - } -} - -export interface PutManyRequest { - key: string - value: Uint8Array -} - -export namespace PutManyRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.writeDefaults === true || (obj.value != null && obj.value.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.value ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '', - value: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - case 2: - obj.value = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutManyRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyRequest => { - return decodeMessage(buf, PutManyRequest.codec()) - } -} - -export interface PutManyResponse { - key: string - value: Uint8Array -} - -export namespace PutManyResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.writeDefaults === true || (obj.value != null && obj.value.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.value ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '', - value: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - case 2: - obj.value = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, PutManyResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): PutManyResponse => { - return decodeMessage(buf, PutManyResponse.codec()) - } -} - -export interface GetManyOptions {} - -export namespace GetManyOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetManyOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyOptions => { - return decodeMessage(buf, GetManyOptions.codec()) - } -} - -export interface GetManyRequest { - key: string -} - -export namespace GetManyRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetManyRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyRequest => { - return decodeMessage(buf, GetManyRequest.codec()) - } -} - -export enum GetManyResponseType { - GET_MANY_PROGRESS = 'GET_MANY_PROGRESS', - GET_MANY_RESULT = 'GET_MANY_RESULT' -} - -enum __GetManyResponseTypeValues { - GET_MANY_PROGRESS = 0, - GET_MANY_RESULT = 1 -} - -export namespace GetManyResponseType { - export const codec = (): Codec => { - return enumeration(__GetManyResponseTypeValues) - } -} -export interface GetManyResponse { - type: GetManyResponseType - value?: Uint8Array - progressEventType?: string - progressEventData: Map -} - -export namespace GetManyResponse { - export interface GetManyResponse$progressEventDataEntry { - key: string - value: string - } - - export namespace GetManyResponse$progressEventDataEntry { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.writeDefaults === true || (obj.value != null && obj.value !== '')) { - w.uint32(18) - w.string(obj.value ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '', - value: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - case 2: - obj.value = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetManyResponse$progressEventDataEntry.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyResponse$progressEventDataEntry => { - return decodeMessage(buf, GetManyResponse$progressEventDataEntry.codec()) - } - } - - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.type != null && __GetManyResponseTypeValues[obj.type] !== 0)) { - w.uint32(8) - GetManyResponseType.codec().encode(obj.type ?? GetManyResponseType.GET_MANY_PROGRESS, w) - } - - if (obj.value != null) { - w.uint32(18) - w.bytes(obj.value) - } - - if (obj.progressEventType != null) { - w.uint32(26) - w.string(obj.progressEventType) - } - - if (obj.progressEventData != null && obj.progressEventData.size !== 0) { - for (const [key, value] of obj.progressEventData.entries()) { - w.uint32(34) - GetManyResponse.GetManyResponse$progressEventDataEntry.codec().encode({ key, value }, w, { - writeDefaults: true - }) - } - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - type: GetManyResponseType.GET_MANY_PROGRESS, - progressEventData: new Map() - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.type = GetManyResponseType.codec().decode(reader) - break - case 2: - obj.value = reader.bytes() - break - case 3: - obj.progressEventType = reader.string() - break - case 4: { - const entry = GetManyResponse.GetManyResponse$progressEventDataEntry.codec().decode(reader, reader.uint32()) - obj.progressEventData.set(entry.key, entry.value) - break - } - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, GetManyResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): GetManyResponse => { - return decodeMessage(buf, GetManyResponse.codec()) - } -} - -export interface DeleteManyOptions {} - -export namespace DeleteManyOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteManyOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyOptions => { - return decodeMessage(buf, DeleteManyOptions.codec()) - } -} - -export interface DeleteManyRequest { - key: string -} - -export namespace DeleteManyRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteManyRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyRequest => { - return decodeMessage(buf, DeleteManyRequest.codec()) - } -} - -export interface DeleteManyResponse { - key: string -} - -export namespace DeleteManyResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, DeleteManyResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): DeleteManyResponse => { - return decodeMessage(buf, DeleteManyResponse.codec()) - } -} - -export interface BatchOptions {} - -export namespace BatchOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, BatchOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): BatchOptions => { - return decodeMessage(buf, BatchOptions.codec()) - } -} - -export interface BatchRequest {} - -export namespace BatchRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, BatchRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): BatchRequest => { - return decodeMessage(buf, BatchRequest.codec()) - } -} - -export interface BatchResponse { - id: string -} - -export namespace BatchResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.id != null && obj.id !== '')) { - w.uint32(10) - w.string(obj.id ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - id: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.id = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, BatchResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): BatchResponse => { - return decodeMessage(buf, BatchResponse.codec()) - } -} - -export interface QueryOptions {} - -export namespace QueryOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryOptions => { - return decodeMessage(buf, QueryOptions.codec()) - } -} - -export interface QueryRequest {} - -export namespace QueryRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryRequest => { - return decodeMessage(buf, QueryRequest.codec()) - } -} - -export interface QueryResponse {} - -export namespace QueryResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryResponse => { - return decodeMessage(buf, QueryResponse.codec()) - } -} - -export interface QueryKeysOptions {} - -export namespace QueryKeysOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryKeysOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysOptions => { - return decodeMessage(buf, QueryKeysOptions.codec()) - } -} - -export interface QueryKeysRequest {} - -export namespace QueryKeysRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryKeysRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysRequest => { - return decodeMessage(buf, QueryKeysRequest.codec()) - } -} - -export interface QueryKeysResponse {} - -export namespace QueryKeysResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, QueryKeysResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): QueryKeysResponse => { - return decodeMessage(buf, QueryKeysResponse.codec()) - } -} diff --git a/packages/rpc-protocol/src/index.ts b/packages/rpc-protocol/src/index.ts deleted file mode 100644 index 23541f48..00000000 --- a/packages/rpc-protocol/src/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { RPCCallError, RPCCallProgress } from './rpc.js' - -export const HELIA_RPC_PROTOCOL = '/helia-rpc/0.0.1' - -export class RPCError extends Error { - public readonly name: string - public readonly code: string - - constructor (buf: Uint8Array) { - const message = RPCCallError.decode(buf) - - super(message.message ?? 'RPC error') - - this.name = message.name ?? 'RPCError' - this.code = message.code ?? 'ERR_RPC_ERROR' - this.stack = message.stack ?? this.stack - } -} - -export class RPCProgressEvent extends Event { - constructor (buf: Uint8Array) { - const event = RPCCallProgress.decode(buf) - - super(event.event ?? 'ProgressEvent') - - for (const [key, value] of event.data) { - // @ts-expect-error cannot use strings to index this type - this[key] = value - } - } -} diff --git a/packages/rpc-protocol/src/root.proto b/packages/rpc-protocol/src/root.proto deleted file mode 100644 index 31fbecd2..00000000 --- a/packages/rpc-protocol/src/root.proto +++ /dev/null @@ -1,13 +0,0 @@ -syntax = "proto3"; - -message InfoOptions { - optional string peer_id = 1; -} - -message InfoResponse { - string peer_id = 1; - repeated string multiaddrs = 2; - string agent_version = 3; - string protocol_version = 4; - repeated string protocols = 5; -} diff --git a/packages/rpc-protocol/src/root.ts b/packages/rpc-protocol/src/root.ts deleted file mode 100644 index 40b24efa..00000000 --- a/packages/rpc-protocol/src/root.ts +++ /dev/null @@ -1,167 +0,0 @@ -/* eslint-disable import/export */ -/* eslint-disable complexity */ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import { encodeMessage, decodeMessage, message } from 'protons-runtime' -import type { Codec } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' - -export interface InfoOptions { - peerId?: string -} - -export namespace InfoOptions { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (obj.peerId != null) { - w.uint32(10) - w.string(obj.peerId) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.peerId = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, InfoOptions.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): InfoOptions => { - return decodeMessage(buf, InfoOptions.codec()) - } -} - -export interface InfoResponse { - peerId: string - multiaddrs: string[] - agentVersion: string - protocolVersion: string - protocols: string[] -} - -export namespace InfoResponse { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.peerId != null && obj.peerId !== '')) { - w.uint32(10) - w.string(obj.peerId ?? '') - } - - if (obj.multiaddrs != null) { - for (const value of obj.multiaddrs) { - w.uint32(18) - w.string(value) - } - } - - if (opts.writeDefaults === true || (obj.agentVersion != null && obj.agentVersion !== '')) { - w.uint32(26) - w.string(obj.agentVersion ?? '') - } - - if (opts.writeDefaults === true || (obj.protocolVersion != null && obj.protocolVersion !== '')) { - w.uint32(34) - w.string(obj.protocolVersion ?? '') - } - - if (obj.protocols != null) { - for (const value of obj.protocols) { - w.uint32(42) - w.string(value) - } - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - peerId: '', - multiaddrs: [], - agentVersion: '', - protocolVersion: '', - protocols: [] - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.peerId = reader.string() - break - case 2: - obj.multiaddrs.push(reader.string()) - break - case 3: - obj.agentVersion = reader.string() - break - case 4: - obj.protocolVersion = reader.string() - break - case 5: - obj.protocols.push(reader.string()) - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, InfoResponse.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): InfoResponse => { - return decodeMessage(buf, InfoResponse.codec()) - } -} diff --git a/packages/rpc-protocol/src/rpc.proto b/packages/rpc-protocol/src/rpc.proto deleted file mode 100644 index 177b2156..00000000 --- a/packages/rpc-protocol/src/rpc.proto +++ /dev/null @@ -1,32 +0,0 @@ -syntax = "proto3"; - -message RPCCallRequest { - string resource = 1; - string method = 2; - string authorization = 3; - bytes options = 4; -} - -enum RPCCallMessageType { - RPC_CALL_DONE = 0; - RPC_CALL_ERROR = 1; - RPC_CALL_MESSAGE = 2; - RPC_CALL_PROGRESS = 3; -} - -message RPCCallMessage { - RPCCallMessageType type = 1; - bytes message = 2; -} - -message RPCCallError { - optional string name = 1; - optional string message = 2; - optional string stack = 3; - optional string code = 4; -} - -message RPCCallProgress { - string event = 1; - map data = 4; -} diff --git a/packages/rpc-protocol/src/rpc.ts b/packages/rpc-protocol/src/rpc.ts deleted file mode 100644 index 659c7124..00000000 --- a/packages/rpc-protocol/src/rpc.ts +++ /dev/null @@ -1,409 +0,0 @@ -/* eslint-disable import/export */ -/* eslint-disable complexity */ -/* eslint-disable @typescript-eslint/no-namespace */ -/* eslint-disable @typescript-eslint/no-unnecessary-boolean-literal-compare */ -/* eslint-disable @typescript-eslint/no-empty-interface */ - -import { encodeMessage, decodeMessage, message, enumeration } from 'protons-runtime' -import type { Codec } from 'protons-runtime' -import type { Uint8ArrayList } from 'uint8arraylist' - -export interface RPCCallRequest { - resource: string - method: string - authorization: string - options: Uint8Array -} - -export namespace RPCCallRequest { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.resource != null && obj.resource !== '')) { - w.uint32(10) - w.string(obj.resource ?? '') - } - - if (opts.writeDefaults === true || (obj.method != null && obj.method !== '')) { - w.uint32(18) - w.string(obj.method ?? '') - } - - if (opts.writeDefaults === true || (obj.authorization != null && obj.authorization !== '')) { - w.uint32(26) - w.string(obj.authorization ?? '') - } - - if (opts.writeDefaults === true || (obj.options != null && obj.options.byteLength > 0)) { - w.uint32(34) - w.bytes(obj.options ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - resource: '', - method: '', - authorization: '', - options: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.resource = reader.string() - break - case 2: - obj.method = reader.string() - break - case 3: - obj.authorization = reader.string() - break - case 4: - obj.options = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, RPCCallRequest.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallRequest => { - return decodeMessage(buf, RPCCallRequest.codec()) - } -} - -export enum RPCCallMessageType { - RPC_CALL_DONE = 'RPC_CALL_DONE', - RPC_CALL_ERROR = 'RPC_CALL_ERROR', - RPC_CALL_MESSAGE = 'RPC_CALL_MESSAGE', - RPC_CALL_PROGRESS = 'RPC_CALL_PROGRESS' -} - -enum __RPCCallMessageTypeValues { - RPC_CALL_DONE = 0, - RPC_CALL_ERROR = 1, - RPC_CALL_MESSAGE = 2, - RPC_CALL_PROGRESS = 3 -} - -export namespace RPCCallMessageType { - export const codec = (): Codec => { - return enumeration(__RPCCallMessageTypeValues) - } -} -export interface RPCCallMessage { - type: RPCCallMessageType - message: Uint8Array -} - -export namespace RPCCallMessage { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.type != null && __RPCCallMessageTypeValues[obj.type] !== 0)) { - w.uint32(8) - RPCCallMessageType.codec().encode(obj.type ?? RPCCallMessageType.RPC_CALL_DONE, w) - } - - if (opts.writeDefaults === true || (obj.message != null && obj.message.byteLength > 0)) { - w.uint32(18) - w.bytes(obj.message ?? new Uint8Array(0)) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - type: RPCCallMessageType.RPC_CALL_DONE, - message: new Uint8Array(0) - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.type = RPCCallMessageType.codec().decode(reader) - break - case 2: - obj.message = reader.bytes() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, RPCCallMessage.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallMessage => { - return decodeMessage(buf, RPCCallMessage.codec()) - } -} - -export interface RPCCallError { - name?: string - message?: string - stack?: string - code?: string -} - -export namespace RPCCallError { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (obj.name != null) { - w.uint32(10) - w.string(obj.name) - } - - if (obj.message != null) { - w.uint32(18) - w.string(obj.message) - } - - if (obj.stack != null) { - w.uint32(26) - w.string(obj.stack) - } - - if (obj.code != null) { - w.uint32(34) - w.string(obj.code) - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = {} - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.name = reader.string() - break - case 2: - obj.message = reader.string() - break - case 3: - obj.stack = reader.string() - break - case 4: - obj.code = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, RPCCallError.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallError => { - return decodeMessage(buf, RPCCallError.codec()) - } -} - -export interface RPCCallProgress { - event: string - data: Map -} - -export namespace RPCCallProgress { - export interface RPCCallProgress$dataEntry { - key: string - value: string - } - - export namespace RPCCallProgress$dataEntry { - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.key != null && obj.key !== '')) { - w.uint32(10) - w.string(obj.key ?? '') - } - - if (opts.writeDefaults === true || (obj.value != null && obj.value !== '')) { - w.uint32(18) - w.string(obj.value ?? '') - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - key: '', - value: '' - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.key = reader.string() - break - case 2: - obj.value = reader.string() - break - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, RPCCallProgress$dataEntry.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallProgress$dataEntry => { - return decodeMessage(buf, RPCCallProgress$dataEntry.codec()) - } - } - - let _codec: Codec - - export const codec = (): Codec => { - if (_codec == null) { - _codec = message((obj, w, opts = {}) => { - if (opts.lengthDelimited !== false) { - w.fork() - } - - if (opts.writeDefaults === true || (obj.event != null && obj.event !== '')) { - w.uint32(10) - w.string(obj.event ?? '') - } - - if (obj.data != null && obj.data.size !== 0) { - for (const [key, value] of obj.data.entries()) { - w.uint32(34) - RPCCallProgress.RPCCallProgress$dataEntry.codec().encode({ key, value }, w, { - writeDefaults: true - }) - } - } - - if (opts.lengthDelimited !== false) { - w.ldelim() - } - }, (reader, length) => { - const obj: any = { - event: '', - data: new Map() - } - - const end = length == null ? reader.len : reader.pos + length - - while (reader.pos < end) { - const tag = reader.uint32() - - switch (tag >>> 3) { - case 1: - obj.event = reader.string() - break - case 4: { - const entry = RPCCallProgress.RPCCallProgress$dataEntry.codec().decode(reader, reader.uint32()) - obj.data.set(entry.key, entry.value) - break - } - default: - reader.skipType(tag & 7) - break - } - } - - return obj - }) - } - - return _codec - } - - export const encode = (obj: Partial): Uint8Array => { - return encodeMessage(obj, RPCCallProgress.codec()) - } - - export const decode = (buf: Uint8Array | Uint8ArrayList): RPCCallProgress => { - return decodeMessage(buf, RPCCallProgress.codec()) - } -} diff --git a/packages/rpc-protocol/tsconfig.json b/packages/rpc-protocol/tsconfig.json deleted file mode 100644 index f67b4ce9..00000000 --- a/packages/rpc-protocol/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist", - "noUnusedLocals": false - }, - "include": [ - "src", - "test" - ] -} diff --git a/packages/rpc-server/.aegir.js b/packages/rpc-server/.aegir.js deleted file mode 100644 index e9c18f3e..00000000 --- a/packages/rpc-server/.aegir.js +++ /dev/null @@ -1,6 +0,0 @@ - -export default { - build: { - bundle: false - } -} diff --git a/packages/rpc-server/LICENSE b/packages/rpc-server/LICENSE deleted file mode 100644 index 20ce483c..00000000 --- a/packages/rpc-server/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -This project is dual licensed under MIT and Apache-2.0. - -MIT: https://www.opensource.org/licenses/mit -Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/rpc-server/LICENSE-APACHE b/packages/rpc-server/LICENSE-APACHE deleted file mode 100644 index 14478a3b..00000000 --- a/packages/rpc-server/LICENSE-APACHE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/rpc-server/LICENSE-MIT b/packages/rpc-server/LICENSE-MIT deleted file mode 100644 index 72dc60d8..00000000 --- a/packages/rpc-server/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/packages/rpc-server/README.md b/packages/rpc-server/README.md deleted file mode 100644 index ab4fa46f..00000000 --- a/packages/rpc-server/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# @helia/rpc-server - -[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) -[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) - -> An implementation of IPFS in JavaScript - -## Table of contents - -- [Install](#install) - - [Browser ` -``` - -## API Docs - -- - -## License - -Licensed under either of - -- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) -- MIT ([LICENSE-MIT](LICENSE-MIT) / ) - -## Contribute - -Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). - -Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. - -Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/rpc-server/package.json b/packages/rpc-server/package.json deleted file mode 100644 index 6bd90035..00000000 --- a/packages/rpc-server/package.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "name": "@helia/rpc-server", - "version": "0.0.0", - "description": "An implementation of IPFS in JavaScript", - "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/ipfs/helia/tree/master/packages/rpc-server#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/ipfs/helia.git" - }, - "bugs": { - "url": "https://github.com/ipfs/helia/issues" - }, - "keywords": [ - "IPFS" - ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "eslintConfig": { - "extends": "ipfs", - "parserOptions": { - "sourceType": "module" - } - }, - "release": { - "branches": [ - "main" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, - "scripts": { - "clean": "aegir clean", - "lint": "aegir lint", - "dep-check": "aegir dep-check", - "build": "aegir build", - "test": "aegir test", - "test:chrome": "aegir test -t browser --cov", - "test:chrome-webworker": "aegir test -t webworker", - "test:firefox": "aegir test -t browser -- --browser firefox", - "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", - "test:node": "aegir test -t node --cov", - "test:electron-main": "aegir test -t electron-main", - "release": "aegir release" - }, - "dependencies": { - "@helia/interface": "~0.0.0", - "@helia/rpc-protocol": "~0.0.0", - "@libp2p/interface-keychain": "^2.0.3", - "@libp2p/interface-peer-id": "^2.0.1", - "@libp2p/logger": "^2.0.5", - "@libp2p/peer-id": "^2.0.0", - "@multiformats/multiaddr": "^11.1.5", - "@ucans/ucans": "^0.11.0-alpha", - "it-pb-stream": "^2.0.3", - "multiformats": "^11.0.1", - "uint8arrays": "^4.0.3" - }, - "devDependencies": { - "aegir": "^38.1.0" - }, - "typedoc": { - "entryPoint": "./src/index.ts" - } -} diff --git a/packages/rpc-server/src/handlers/authorization/get.ts b/packages/rpc-server/src/handlers/authorization/get.ts deleted file mode 100644 index ffad8484..00000000 --- a/packages/rpc-server/src/handlers/authorization/get.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { GetRequest, GetResponse } from '@helia/rpc-protocol/authorization' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' -import * as ucans from '@ucans/ucans' -import { base58btc } from 'multiformats/bases/base58' -import { concat as uint8ArrayConcat } from 'uint8arrays/concat' - -export function createAuthorizationGet (config: RPCServerConfig): Service { - if (config.helia.libp2p.peerId.privateKey == null || config.helia.libp2p.peerId.publicKey == null) { - throw new Error('Public/private key missing from peer id') - } - - const issuer = new ucans.EdKeypair( - config.helia.libp2p.peerId.privateKey.subarray(4), - config.helia.libp2p.peerId.publicKey.subarray(4), - false - ) - - return { - insecure: true, - async handle ({ peerId, stream }): Promise { - const request = await stream.readPB(GetRequest) - const user = request.user - - const allowedPeerId = await config.users.exportPeerId(`rpc-user-${user}`) - - if (!allowedPeerId.equals(peerId)) { - throw new Error('PeerIds did not match') - } - - if (peerId.publicKey == null) { - throw new Error('Public key component missing') - } - - // derive the audience from the peer id - const audience = `did:key:${base58btc.encode(uint8ArrayConcat([ - Uint8Array.from([0xed, 0x01]), - peerId.publicKey.subarray(4) - ], peerId.publicKey.length - 2))}` - - // authorize the remote peer for these operations - const ucan = await ucans.build({ - audience, - issuer, - lifetimeInSeconds: config.authorizationValiditySeconds, - capabilities: [ - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/batch' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/close' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/delete-many' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/delete' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/get-many' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/get' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/has' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/put-many' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/put' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/query-keys' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - }, - { - with: { scheme: 'helia-rpc', hierPart: '/blockstore/query' }, - can: { namespace: 'helia-rpc', segments: ['INVOKE'] } - } - ] - }) - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: GetResponse.encode({ - authorization: ucans.encode(ucan) - }) - }, - RPCCallMessage) - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/batch.ts b/packages/rpc-server/src/handlers/blockstore/batch.ts deleted file mode 100644 index f76dd93f..00000000 --- a/packages/rpc-server/src/handlers/blockstore/batch.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { BatchRequest, BatchRequestDelete, BatchRequestPut, BatchRequestType } from '@helia/rpc-protocol/blockstore' -import type { RPCServerConfig, Service } from '../../index.js' -import { CID } from 'multiformats/cid' - -export function createBlockstoreBatch (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const batch = config.helia.blockstore.batch() - - while (true) { - const request = await stream.readPB(BatchRequest) - - for (let i = 0; i < 10; i++) { - if (i < 5) { - continue - } - } - - let putMessage - let deleteMessage - - switch (request.type) { - case BatchRequestType.BATCH_REQUEST_PUT: - putMessage = BatchRequestPut.decode(request.message) - batch.put(CID.decode(putMessage.cid), putMessage.block) - break - case BatchRequestType.BATCH_REQUEST_DELETE: - deleteMessage = BatchRequestDelete.decode(request.message) - batch.delete(CID.decode(deleteMessage.cid)) - break - case BatchRequestType.BATCH_REQUEST_COMMIT: - await batch.commit() - return - default: - throw new Error('Unkown batch message type') - } - } - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/close.ts b/packages/rpc-server/src/handlers/blockstore/close.ts deleted file mode 100644 index b5e2956a..00000000 --- a/packages/rpc-server/src/handlers/blockstore/close.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { RPCServerConfig, Service } from '../../index.js' - -export function createBlockstoreClose (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - await config.helia.blockstore.close() - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/delete-many.ts b/packages/rpc-server/src/handlers/blockstore/delete-many.ts deleted file mode 100644 index 09b5e9eb..00000000 --- a/packages/rpc-server/src/handlers/blockstore/delete-many.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { DeleteManyOptions, DeleteManyRequest, DeleteManyResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' -import { CID } from 'multiformats/cid' - -export function createBlockstoreDeleteMany (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = DeleteManyOptions.decode(options) - - for await (const cid of config.helia.blockstore.deleteMany( - (async function * () { - while (true) { - const request = await stream.readPB(DeleteManyRequest) - - yield CID.decode(request.cid) - } - })(), { - signal, - ...opts - })) { - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: DeleteManyResponse.encode({ - cid: cid.bytes - }) - }, - RPCCallMessage) - } - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/delete.ts b/packages/rpc-server/src/handlers/blockstore/delete.ts deleted file mode 100644 index 4895eb2c..00000000 --- a/packages/rpc-server/src/handlers/blockstore/delete.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DeleteOptions, DeleteRequest, DeleteResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' -import { CID } from 'multiformats/cid' - -export function createBlockstoreDelete (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = DeleteOptions.decode(options) - const request = await stream.readPB(DeleteRequest) - const cid = CID.decode(request.cid) - - await config.helia.blockstore.delete(cid, { - signal, - ...opts - }) - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: DeleteResponse.encode({ - }) - }, - RPCCallMessage) - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/get-many.ts b/packages/rpc-server/src/handlers/blockstore/get-many.ts deleted file mode 100644 index 0188afa1..00000000 --- a/packages/rpc-server/src/handlers/blockstore/get-many.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { DeleteManyOptions, DeleteManyRequest, DeleteManyResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' -import { CID } from 'multiformats/cid' - -export function createBlockstoreGetMany (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = DeleteManyOptions.decode(options) - - for await (const cid of config.helia.blockstore.deleteMany( - (async function * () { - while (true) { - const request = await stream.readPB(DeleteManyRequest) - - yield CID.decode(request.cid) - } - })(), { - signal, - ...opts - })) { - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: DeleteManyResponse.encode({ - cid: cid.bytes - }) - }, - RPCCallMessage) - } - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/get.ts b/packages/rpc-server/src/handlers/blockstore/get.ts deleted file mode 100644 index 41f31e70..00000000 --- a/packages/rpc-server/src/handlers/blockstore/get.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { GetOptions, GetRequest, GetResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' -import { CID } from 'multiformats/cid' - -export function createBlockstoreGet (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = GetOptions.decode(options) - const request = await stream.readPB(GetRequest) - const cid = CID.decode(request.cid) - - const block = await config.helia.blockstore.get(cid, { - signal, - ...opts - }) - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: GetResponse.encode({ - block - }) - }, - RPCCallMessage) - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/has.ts b/packages/rpc-server/src/handlers/blockstore/has.ts deleted file mode 100644 index 17cb0c03..00000000 --- a/packages/rpc-server/src/handlers/blockstore/has.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { HasOptions, HasRequest, HasResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' -import { CID } from 'multiformats/cid' - -export function createBlockstoreHas (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = HasOptions.decode(options) - const request = await stream.readPB(HasRequest) - const cid = CID.decode(request.cid) - - const has = await config.helia.blockstore.has(cid, { - signal, - ...opts - }) - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: HasResponse.encode({ - has - }) - }, - RPCCallMessage) - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/open.ts b/packages/rpc-server/src/handlers/blockstore/open.ts deleted file mode 100644 index 606b6b93..00000000 --- a/packages/rpc-server/src/handlers/blockstore/open.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { RPCServerConfig, Service } from '../../index.js' - -export function createBlockstoreOpen (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - await config.helia.blockstore.open() - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/put-many.ts b/packages/rpc-server/src/handlers/blockstore/put-many.ts deleted file mode 100644 index 09cf253a..00000000 --- a/packages/rpc-server/src/handlers/blockstore/put-many.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { DeleteManyOptions, DeleteManyRequest, DeleteManyResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' -import { CID } from 'multiformats/cid' - -export function createBlockstorePutMany (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = DeleteManyOptions.decode(options) - - for await (const cid of config.helia.blockstore.deleteMany( - (async function * () { - while (true) { - const request = await stream.readPB(DeleteManyRequest) - - yield CID.decode(request.cid) - } - })(), { - signal, - ...opts - })) { - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: DeleteManyResponse.encode({ - cid: cid.bytes - }) - }, - RPCCallMessage) - } - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/put.ts b/packages/rpc-server/src/handlers/blockstore/put.ts deleted file mode 100644 index cb967c27..00000000 --- a/packages/rpc-server/src/handlers/blockstore/put.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { PutOptions, PutRequest, PutResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' -import { CID } from 'multiformats/cid' - -export function createBlockstorePut (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = PutOptions.decode(options) - const request = await stream.readPB(PutRequest) - const cid = CID.decode(request.cid) - - await config.helia.blockstore.put(cid, request.block, { - signal, - ...opts - }) - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: PutResponse.encode({ - }) - }, - RPCCallMessage) - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/query-keys.ts b/packages/rpc-server/src/handlers/blockstore/query-keys.ts deleted file mode 100644 index 0afb079f..00000000 --- a/packages/rpc-server/src/handlers/blockstore/query-keys.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { QueryKeysOptions, QueryKeysResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' - -export function createBlockstoreQueryKeys (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = QueryKeysOptions.decode(options) - - for await (const cid of config.helia.blockstore.queryKeys({ - ...opts - }, { - signal - })) { - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: QueryKeysResponse.encode({ - key: cid.bytes - }) - }, - RPCCallMessage) - } - } - } -} diff --git a/packages/rpc-server/src/handlers/blockstore/query.ts b/packages/rpc-server/src/handlers/blockstore/query.ts deleted file mode 100644 index 3c41c85c..00000000 --- a/packages/rpc-server/src/handlers/blockstore/query.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { QueryOptions, QueryResponse } from '@helia/rpc-protocol/blockstore' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import type { RPCServerConfig, Service } from '../../index.js' - -export function createBlockstoreQuery (config: RPCServerConfig): Service { - return { - async handle ({ options, stream, signal }): Promise { - const opts = QueryOptions.decode(options) - - for await (const { key, value } of config.helia.blockstore.query({ - ...opts - }, { - signal - })) { - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: QueryResponse.encode({ - key: key.bytes, - value - }) - }, - RPCCallMessage) - } - } - } -} diff --git a/packages/rpc-server/src/handlers/index.ts b/packages/rpc-server/src/handlers/index.ts deleted file mode 100644 index 1856e16a..00000000 --- a/packages/rpc-server/src/handlers/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { RPCServerConfig, Service } from '../index.js' -import { createInfo } from './info.js' -import { createAuthorizationGet } from './authorization/get.js' -import { createBlockstoreDelete } from './blockstore/delete.js' -import { createBlockstoreGet } from './blockstore/get.js' -import { createBlockstoreHas } from './blockstore/has.js' -import { createBlockstorePut } from './blockstore/put.js' -import { createBlockstoreDeleteMany } from './blockstore/delete-many.js' -import { createBlockstoreGetMany } from './blockstore/get-many.js' -import { createBlockstoreBatch } from './blockstore/batch.js' -import { createBlockstoreClose } from './blockstore/close.js' -import { createBlockstoreOpen } from './blockstore/open.js' -import { createBlockstorePutMany } from './blockstore/put-many.js' -import { createBlockstoreQueryKeys } from './blockstore/query-keys.js' -import { createBlockstoreQuery } from './blockstore/query.js' - -export function createServices (config: RPCServerConfig): Record { - const services: Record = { - '/authorization/get': createAuthorizationGet(config), - '/blockstore/batch': createBlockstoreBatch(config), - '/blockstore/close': createBlockstoreClose(config), - '/blockstore/delete-many': createBlockstoreDeleteMany(config), - '/blockstore/delete': createBlockstoreDelete(config), - '/blockstore/get-many': createBlockstoreGetMany(config), - '/blockstore/get': createBlockstoreGet(config), - '/blockstore/has': createBlockstoreHas(config), - '/blockstore/open': createBlockstoreOpen(config), - '/blockstore/put-many': createBlockstorePutMany(config), - '/blockstore/put': createBlockstorePut(config), - '/blockstore/query-keys': createBlockstoreQueryKeys(config), - '/blockstore/query': createBlockstoreQuery(config), - '/info': createInfo(config) - } - - return services -} diff --git a/packages/rpc-server/src/handlers/info.ts b/packages/rpc-server/src/handlers/info.ts deleted file mode 100644 index 0a496f7e..00000000 --- a/packages/rpc-server/src/handlers/info.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { InfoOptions, InfoResponse } from '@helia/rpc-protocol/root' -import { RPCCallMessage, RPCCallMessageType } from '@helia/rpc-protocol/rpc' -import { peerIdFromString } from '@libp2p/peer-id' -import type { RPCServerConfig, Service } from '../index.js' - -export function createInfo (config: RPCServerConfig): Service { - return { - insecure: true, - async handle ({ options, stream, signal }): Promise { - const opts = InfoOptions.decode(options) - - const result = await config.helia.info({ - peerId: opts.peerId != null ? peerIdFromString(opts.peerId) : undefined, - signal - }) - - stream.writePB({ - type: RPCCallMessageType.RPC_CALL_MESSAGE, - message: InfoResponse.encode({ - ...result, - peerId: result.peerId.toString(), - multiaddrs: result.multiaddrs.map(ma => ma.toString()) - }) - }, RPCCallMessage) - } - } -} diff --git a/packages/rpc-server/src/index.ts b/packages/rpc-server/src/index.ts deleted file mode 100644 index ac665034..00000000 --- a/packages/rpc-server/src/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -import type { Helia } from '@helia/interface' -import { HeliaError } from '@helia/interface/errors' -import { logger } from '@libp2p/logger' -import { HELIA_RPC_PROTOCOL } from '@helia/rpc-protocol' -import { RPCCallRequest, RPCCallError, RPCCallMessageType, RPCCallMessage } from '@helia/rpc-protocol/rpc' -import * as ucans from '@ucans/ucans' -import { pbStream, ProtobufStream } from 'it-pb-stream' -import { EdKeypair } from '@ucans/ucans' -import type { KeyChain } from '@libp2p/interface-keychain' -import { base58btc } from 'multiformats/bases/base58' -import { concat as uint8ArrayConcat } from 'uint8arrays/concat' -import type { PeerId } from '@libp2p/interface-peer-id' -import { createServices } from './handlers/index.js' - -const log = logger('helia:rpc-server') - -export interface RPCServerConfig { - helia: Helia - users: KeyChain - authorizationValiditySeconds: number -} - -export interface UnaryResponse { - value: ResponseType - metadata: Record -} - -export interface ServiceArgs { - peerId: PeerId - options: Uint8Array - stream: ProtobufStream - signal: AbortSignal -} - -export interface Service { - insecure?: true - handle: (args: ServiceArgs) => Promise -} - -class RPCError extends HeliaError { - constructor (message: string, code: string) { - super(message, 'RPCError', code) - } -} - -export async function createHeliaRpcServer (config: RPCServerConfig): Promise { - const { helia } = config - - if (helia.libp2p.peerId.privateKey == null || helia.libp2p.peerId.publicKey == null) { - // should never happen - throw new Error('helia.libp2p.peerId was missing public or private key component') - } - - const serverKey = new EdKeypair( - helia.libp2p.peerId.privateKey.subarray(4), - helia.libp2p.peerId.publicKey.subarray(4), - false - ) - - const services = createServices(config) - - await helia.libp2p.handle(HELIA_RPC_PROTOCOL, ({ stream, connection }) => { - const controller = new AbortController() - - void Promise.resolve().then(async () => { - const pb = pbStream(stream) - - try { - const request = await pb.readPB(RPCCallRequest) - const service = services[request.resource] - - if (service == null) { - log('no handler defined for %s %s', request.method, request.resource) - throw new RPCError(`Request path "${request.resource}" unimplemented`, 'ERR_PATH_UNIMPLEMENTED') - } - - log('incoming RPC request %s %s', request.method, request.resource) - - if (service.insecure == null) { - if (request.authorization == null) { - log('authorization missing for %s %s', request.method, request.resource) - throw new RPCError(`Authorisation failed for ${request.method} ${request.resource}`, 'ERR_AUTHORIZATION_FAILED') - } - - log('authorizing request %s %s', request.method, request.resource) - - const peerId = connection.remotePeer - - if (peerId.publicKey == null) { - log('public key missing for %s %s', request.method, request.resource) - throw new RPCError(`Authorisation failed for ${request.method} ${request.resource}`, 'ERR_AUTHORIZATION_FAILED') - } - - const audience = `did:key:${base58btc.encode(uint8ArrayConcat([ - Uint8Array.from([0xed, 0x01]), - peerId.publicKey.subarray(4) - ], peerId.publicKey.length - 2))}` - - // authorize request - const result = await ucans.verify(request.authorization, { - audience, - requiredCapabilities: [{ - capability: { - with: { scheme: 'helia-rpc', hierPart: request.resource }, - can: { namespace: 'helia-rpc', segments: [request.method] } - }, - rootIssuer: serverKey.did() - }] - }) - - if (!result.ok) { - log('authorization failed for %s %s', request.method, request.resource) - throw new RPCError(`Authorisation failed for ${request.method} ${request.resource}`, 'ERR_AUTHORIZATION_FAILED') - } - } - - await service.handle({ - peerId: connection.remotePeer, - options: request.options ?? new Uint8Array(), - signal: controller.signal, - stream: pb - }) - log('handler succeeded for %s %s', request.method, request.resource) - - pb.writePB({ - type: RPCCallMessageType.RPC_CALL_DONE - }, RPCCallMessage) - } catch (err: any) { - log.error('handler failed', err) - pb.writePB({ - type: RPCCallMessageType.RPC_CALL_ERROR, - message: RPCCallError.encode({ - name: err.name, - message: err.message, - stack: err.stack, - code: err.code - }) - }, RPCCallMessage) - } finally { - stream.closeWrite() - } - }) - }) -} diff --git a/packages/rpc-server/src/utils/multiaddr-to-url.ts b/packages/rpc-server/src/utils/multiaddr-to-url.ts deleted file mode 100644 index a9671308..00000000 --- a/packages/rpc-server/src/utils/multiaddr-to-url.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Multiaddr } from '@multiformats/multiaddr' -import { InvalidParametersError } from '@helia/interface/errors' - -export function multiaddrToUrl (addr: Multiaddr): URL { - const protoNames = addr.protoNames() - - if (protoNames.length !== 3) { - throw new InvalidParametersError('Helia RPC address format incorrect') - } - - if (protoNames[0] !== 'ip4' && protoNames[0] !== 'ip6') { - throw new InvalidParametersError('Helia RPC address format incorrect') - } - - if (protoNames[1] !== 'tcp' && protoNames[2] !== 'ws') { - throw new InvalidParametersError('Helia RPC address format incorrect') - } - - const { host, port } = addr.toOptions() - - return new URL(`ws://${host}:${port}`) -} diff --git a/packages/rpc-server/test/index.spec.ts b/packages/rpc-server/test/index.spec.ts deleted file mode 100644 index 97bedd10..00000000 --- a/packages/rpc-server/test/index.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import '../src/index.js' - -describe('rpc-server', () => { - it('should work', async () => { - - }) -}) diff --git a/packages/rpc-server/tsconfig.json b/packages/rpc-server/tsconfig.json deleted file mode 100644 index 021053e1..00000000 --- a/packages/rpc-server/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src", - "test" - ], - "references": [ - { - "path": "../interface" - }, - { - "path": "../rpc-protocol" - } - ] -} diff --git a/packages/unixfs-cli/.aegir.js b/packages/unixfs-cli/.aegir.js deleted file mode 100644 index e9c18f3e..00000000 --- a/packages/unixfs-cli/.aegir.js +++ /dev/null @@ -1,6 +0,0 @@ - -export default { - build: { - bundle: false - } -} diff --git a/packages/unixfs-cli/LICENSE b/packages/unixfs-cli/LICENSE deleted file mode 100644 index 20ce483c..00000000 --- a/packages/unixfs-cli/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -This project is dual licensed under MIT and Apache-2.0. - -MIT: https://www.opensource.org/licenses/mit -Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/unixfs-cli/LICENSE-APACHE b/packages/unixfs-cli/LICENSE-APACHE deleted file mode 100644 index 14478a3b..00000000 --- a/packages/unixfs-cli/LICENSE-APACHE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/unixfs-cli/LICENSE-MIT b/packages/unixfs-cli/LICENSE-MIT deleted file mode 100644 index 72dc60d8..00000000 --- a/packages/unixfs-cli/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/packages/unixfs-cli/README.md b/packages/unixfs-cli/README.md deleted file mode 100644 index c775f146..00000000 --- a/packages/unixfs-cli/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# @helia/unixfs-cli - -[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) -[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) - -> Run unixfs commands against a Helia node on the CLI - -## Table of contents - -- [Install](#install) -- [API Docs](#api-docs) -- [License](#license) -- [Contribute](#contribute) - -## Install - -```console -$ npm i @helia/unixfs-cli -``` - -## API Docs - -- - -## License - -Licensed under either of - -- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) -- MIT ([LICENSE-MIT](LICENSE-MIT) / ) - -## Contribute - -Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). - -Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. - -Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/unixfs-cli/package.json b/packages/unixfs-cli/package.json deleted file mode 100644 index 78f34191..00000000 --- a/packages/unixfs-cli/package.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "name": "@helia/unixfs-cli", - "version": "0.0.0", - "description": "Run unixfs commands against a Helia node on the CLI", - "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/ipfs/helia/tree/master/packages/unixfs-cli#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/ipfs/helia.git" - }, - "bugs": { - "url": "https://github.com/ipfs/helia/issues" - }, - "keywords": [ - "IPFS" - ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, - "bin": { - "unixfs": "./dist/src/index.js" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "eslintConfig": { - "extends": "ipfs", - "parserOptions": { - "sourceType": "module" - } - }, - "release": { - "branches": [ - "main" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, - "scripts": { - "clean": "aegir clean", - "lint": "aegir lint", - "dep-check": "aegir dep-check", - "build": "aegir build", - "test": "aegir test", - "test:node": "aegir test -t node --cov", - "release": "aegir release" - }, - "dependencies": { - "@helia/cli-utils": "~0.0.0", - "@helia/unixfs": "~0.0.0", - "@libp2p/interfaces": "^3.3.1", - "ipfs-unixfs": "^9.0.0", - "ipfs-unixfs-exporter": "^10.0.0", - "ipfs-unixfs-importer": "^12.0.0", - "it-glob": "^2.0.0", - "it-merge": "^2.0.0", - "kleur": "^4.1.5", - "multiformats": "^11.0.1" - }, - "devDependencies": { - "aegir": "^38.1.0" - }, - "typedoc": { - "entryPoint": "./src/index.ts" - } -} diff --git a/packages/unixfs-cli/src/commands/add.ts b/packages/unixfs-cli/src/commands/add.ts deleted file mode 100644 index b03ae38b..00000000 --- a/packages/unixfs-cli/src/commands/add.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { unixfs } from '@helia/unixfs' -import merge from 'it-merge' -import path from 'node:path' -import { globSource } from '../utils/glob-source.js' -import fs from 'node:fs' -import { dateToMtime } from '../utils/date-to-mtime.js' -import type { Mtime } from 'ipfs-unixfs' -import type { ImportCandidate, UserImporterOptions } from 'ipfs-unixfs-importer' -import type { Command } from '@helia/cli-utils' - -interface AddArgs { - positionals: string[] - fs: string -} - -export const add: Command = { - command: 'add', - description: 'Add a file or directory to your helia node', - example: '$ unixfs add path/to/file.txt', - async execute ({ positionals, helia, stdout }) { - const options: UserImporterOptions = { - cidVersion: 1, - rawLeaves: true - } - const fs = unixfs(helia) - - for await (const result of fs.addStream(parsePositionals(positionals), options)) { - stdout.write(`${result.cid}\n`) - } - } -} - -async function * parsePositionals (positionals: string[], mode?: number, mtime?: Mtime, hidden?: boolean, recursive?: boolean, preserveMode?: boolean, preserveMtime?: boolean): AsyncGenerator { - if (positionals.length === 0) { - yield { - content: process.stdin, - mode, - mtime - } - return - } - - yield * merge(...positionals.map(file => getSource(file, { - hidden, - recursive, - preserveMode, - preserveMtime, - mode, - mtime - }))) -} - -interface SourceOptions { - hidden?: boolean - recursive?: boolean - preserveMode?: boolean - preserveMtime?: boolean - mode?: number - mtime?: Mtime -} - -async function * getSource (target: string, options: SourceOptions = {}): AsyncGenerator { - const absolutePath = path.resolve(target) - const stats = await fs.promises.stat(absolutePath) - - if (stats.isFile()) { - let mtime = options.mtime - let mode = options.mode - - if (options.preserveMtime === true) { - mtime = dateToMtime(stats.mtime) - } - - if (options.preserveMode === true) { - mode = stats.mode - } - - yield { - path: path.basename(target), - content: fs.createReadStream(absolutePath), - mtime, - mode - } - - return - } - - const dirName = path.basename(absolutePath) - - let pattern = '*' - - if (options.recursive === true) { - pattern = '**/*' - } - - for await (const content of globSource(target, pattern, { - hidden: options.hidden, - preserveMode: options.preserveMode, - preserveMtime: options.preserveMtime, - mode: options.mode, - mtime: options.mtime - })) { - yield { - ...content, - path: `${dirName}${content.path}` - } - } -} diff --git a/packages/unixfs-cli/src/commands/cat.ts b/packages/unixfs-cli/src/commands/cat.ts deleted file mode 100644 index eb34eaa4..00000000 --- a/packages/unixfs-cli/src/commands/cat.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import { exporter } from 'ipfs-unixfs-exporter' -import { CID } from 'multiformats/cid' - -interface CatArgs { - positionals?: string[] - offset?: string - length?: string -} - -export const cat: Command = { - command: 'cat', - description: 'Fetch and cat an IPFS path referencing a file', - example: '$ unixfs cat ', - options: { - offset: { - description: 'Where to start reading the file from', - type: 'string', - short: 'o' - }, - length: { - description: 'How many bytes to read from the file', - type: 'string', - short: 'l' - }, - progress: { - description: 'Display information about how the CID is being resolved', - type: 'boolean', - short: 'p' - } - }, - async execute ({ positionals, offset, length, helia, stdout }) { - if (positionals == null || positionals.length === 0) { - throw new TypeError('Missing positionals') - } - - const cid = CID.parse(positionals[0]) - const entry = await exporter(cid, helia.blockstore, { - offset: offset != null ? Number(offset) : undefined, - length: length != null ? Number(length) : undefined - }) - - if (entry.type !== 'file' && entry.type !== 'raw') { - throw new Error('UnixFS path was not a file') - } - - for await (const buf of entry.content()) { - stdout.write(buf) - } - } -} diff --git a/packages/unixfs-cli/src/commands/index.ts b/packages/unixfs-cli/src/commands/index.ts deleted file mode 100644 index d20b3507..00000000 --- a/packages/unixfs-cli/src/commands/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import { add } from './add.js' -import { cat } from './cat.js' -import { stat } from './stat.js' - -export const commands: Array> = [ - add, - cat, - stat -] diff --git a/packages/unixfs-cli/src/commands/stat.ts b/packages/unixfs-cli/src/commands/stat.ts deleted file mode 100644 index 38f21ca9..00000000 --- a/packages/unixfs-cli/src/commands/stat.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Command } from '@helia/cli-utils' -import { exporter } from 'ipfs-unixfs-exporter' -import { CID } from 'multiformats/cid' -import * as format from '@helia/cli-utils/format' -import type { Formatable } from '@helia/cli-utils/format' - -interface StatArgs { - positionals?: string[] - explain?: boolean -} - -export const stat: Command = { - command: 'stat', - description: 'Display statistics about a dag', - example: '$ unixfs stat ', - options: { - explain: { - description: 'Print diagnostic information while trying to resolve the block', - type: 'boolean', - default: false - } - }, - async execute ({ positionals, helia, stdout, explain }) { - if (positionals == null || positionals.length === 0) { - throw new TypeError('Missing positionals') - } - - let progress: undefined | ((evt: Event) => void) - - if (explain === true) { - progress = (evt: Event) => { - stdout.write(`${evt.type}\n`) - } - } - - const cid = CID.parse(positionals[0]) - const entry = await exporter(cid, helia.blockstore, { - // @ts-expect-error - progress - }) - - const items: Formatable[] = [ - format.table([ - format.row('CID', entry.cid.toString()), - format.row('Type', entry.type), - format.row('Size', `${entry.size}`) - ]) - ] - - format.formatter( - stdout, - items - ) - } -} diff --git a/packages/unixfs-cli/src/index.ts b/packages/unixfs-cli/src/index.ts deleted file mode 100644 index 2ce98cd1..00000000 --- a/packages/unixfs-cli/src/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -#! /usr/bin/env node --trace-warnings -/* eslint-disable no-console */ - -import { cli } from '@helia/cli-utils' -import kleur from 'kleur' -import { commands } from './commands/index.js' - -async function main (): Promise { - const command = 'unixfs' - const description = `Run unixfs commands against a ${kleur.bold('Helia')} node` - - await cli(command, description, commands) -} - -main().catch(err => { - console.error(err) // eslint-disable-line no-console - process.exit(1) -}) diff --git a/packages/unixfs-cli/src/utils/date-to-mtime.ts b/packages/unixfs-cli/src/utils/date-to-mtime.ts deleted file mode 100644 index 642c34b0..00000000 --- a/packages/unixfs-cli/src/utils/date-to-mtime.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Mtime } from 'ipfs-unixfs' - -export function dateToMtime (date: Date): Mtime { - const ms = date.getTime() - const secs = Math.floor(ms / 1000) - - return { - secs, - nsecs: (ms - (secs * 1000)) * 1000 - } -} diff --git a/packages/unixfs-cli/src/utils/glob-source.ts b/packages/unixfs-cli/src/utils/glob-source.ts deleted file mode 100644 index 17de99ab..00000000 --- a/packages/unixfs-cli/src/utils/glob-source.ts +++ /dev/null @@ -1,95 +0,0 @@ -import fsp from 'node:fs/promises' -import fs from 'node:fs' -import glob from 'it-glob' -import path from 'path' -import { CodeError } from '@libp2p/interfaces/errors' -import type { Mtime } from 'ipfs-unixfs' -import type { ImportCandidate } from 'ipfs-unixfs-importer' - -export interface GlobSourceOptions { - /** - * Include .dot files in matched paths - */ - hidden?: boolean - - /** - * follow symlinks - */ - followSymlinks?: boolean - - /** - * Preserve mode - */ - preserveMode?: boolean - - /** - * Preserve mtime - */ - preserveMtime?: boolean - - /** - * mode to use - if preserveMode is true this will be ignored - */ - mode?: number - - /** - * mtime to use - if preserveMtime is true this will be ignored - */ - mtime?: Mtime -} - -/** - * Create an async iterator that yields paths that match requested glob pattern - */ -export async function * globSource (cwd: string, pattern: string, options: GlobSourceOptions): AsyncGenerator { - options = options ?? {} - - if (typeof pattern !== 'string') { - throw new CodeError('Pattern must be a string', 'ERR_INVALID_PATH', { pattern }) - } - - if (!path.isAbsolute(cwd)) { - cwd = path.resolve(process.cwd(), cwd) - } - - const globOptions = Object.assign({}, { - nodir: false, - realpath: false, - absolute: true, - dot: Boolean(options.hidden), - follow: options.followSymlinks != null ? options.followSymlinks : true - }) - - for await (const p of glob(cwd, pattern, globOptions)) { - const stat = await fsp.stat(p) - - let mode = options.mode - - if (options.preserveMode === true) { - mode = stat.mode - } - - let mtime = options.mtime - - if (options.preserveMtime === true) { - const ms = stat.mtime.getTime() - const secs = Math.floor(ms / 1000) - - mtime = { - secs, - nsecs: (ms - (secs * 1000)) * 1000 - } - } - - yield { - path: toPosix(p.replace(cwd, '')), - content: stat.isFile() ? fs.createReadStream(p) : undefined, - mode, - mtime - } - } -} - -function toPosix (path: string): string { - return path.replace(/\\/g, '/') -} diff --git a/packages/unixfs-cli/test/index.spec.ts b/packages/unixfs-cli/test/index.spec.ts deleted file mode 100644 index e67dc48c..00000000 --- a/packages/unixfs-cli/test/index.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { expect } from 'aegir/chai' - -describe('cli', () => { - it('should start a node', () => { - expect(true).to.be.ok() - }) -}) diff --git a/packages/unixfs-cli/tsconfig.json b/packages/unixfs-cli/tsconfig.json deleted file mode 100644 index e505beaf..00000000 --- a/packages/unixfs-cli/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src", - "test" - ], - "references": [ - { - "path": "../cli-utils" - }, - { - "path": "../unixfs" - } - ] -} diff --git a/packages/unixfs/LICENSE b/packages/unixfs/LICENSE deleted file mode 100644 index 20ce483c..00000000 --- a/packages/unixfs/LICENSE +++ /dev/null @@ -1,4 +0,0 @@ -This project is dual licensed under MIT and Apache-2.0. - -MIT: https://www.opensource.org/licenses/mit -Apache-2.0: https://www.apache.org/licenses/license-2.0 diff --git a/packages/unixfs/LICENSE-APACHE b/packages/unixfs/LICENSE-APACHE deleted file mode 100644 index 14478a3b..00000000 --- a/packages/unixfs/LICENSE-APACHE +++ /dev/null @@ -1,5 +0,0 @@ -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. diff --git a/packages/unixfs/LICENSE-MIT b/packages/unixfs/LICENSE-MIT deleted file mode 100644 index 72dc60d8..00000000 --- a/packages/unixfs/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -The MIT License (MIT) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/packages/unixfs/README.md b/packages/unixfs/README.md deleted file mode 100644 index ffff3cb3..00000000 --- a/packages/unixfs/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# @helia/unixfs - -[![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/helia.svg?style=flat-square)](https://codecov.io/gh/ipfs/helia) -[![CI](https://img.shields.io/github/actions/workflow/status/ipfs/helia/js-test-and-release.yml?branch=main\&style=flat-square)](https://github.com/ipfs/helia/actions/workflows/js-test-and-release.yml?query=branch%3Amain) - -> A Helia-compatible wrapper for UnixFS - -## Table of contents - -- [Install](#install) - - [Browser ` -``` - -## API Docs - -- - -## License - -Licensed under either of - -- Apache 2.0, ([LICENSE-APACHE](LICENSE-APACHE) / ) -- MIT ([LICENSE-MIT](LICENSE-MIT) / ) - -## Contribute - -Contributions welcome! Please check out [the issues](https://github.com/ipfs/helia/issues). - -Also see our [contributing document](https://github.com/ipfs/community/blob/master/CONTRIBUTING_JS.md) for more information on how we work, and about contributing in general. - -Please be aware that all interactions related to this repo are subject to the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) diff --git a/packages/unixfs/package.json b/packages/unixfs/package.json deleted file mode 100644 index 7a6bf587..00000000 --- a/packages/unixfs/package.json +++ /dev/null @@ -1,168 +0,0 @@ -{ - "name": "@helia/unixfs", - "version": "0.0.0", - "description": "A Helia-compatible wrapper for UnixFS", - "license": "Apache-2.0 OR MIT", - "homepage": "https://github.com/ipfs/helia/tree/master/packages/unixfs#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/ipfs/helia.git" - }, - "bugs": { - "url": "https://github.com/ipfs/helia/issues" - }, - "keywords": [ - "IPFS" - ], - "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" - }, - "type": "module", - "types": "./dist/src/index.d.ts", - "files": [ - "src", - "dist", - "!dist/test", - "!**/*.tsbuildinfo" - ], - "exports": { - ".": { - "types": "./dist/src/index.d.ts", - "import": "./dist/src/index.js" - } - }, - "eslintConfig": { - "extends": "ipfs", - "parserOptions": { - "sourceType": "module" - } - }, - "release": { - "branches": [ - "main" - ], - "plugins": [ - [ - "@semantic-release/commit-analyzer", - { - "preset": "conventionalcommits", - "releaseRules": [ - { - "breaking": true, - "release": "major" - }, - { - "revert": true, - "release": "patch" - }, - { - "type": "feat", - "release": "minor" - }, - { - "type": "fix", - "release": "patch" - }, - { - "type": "docs", - "release": "patch" - }, - { - "type": "test", - "release": "patch" - }, - { - "type": "deps", - "release": "patch" - }, - { - "scope": "no-release", - "release": false - } - ] - } - ], - [ - "@semantic-release/release-notes-generator", - { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - { - "type": "feat", - "section": "Features" - }, - { - "type": "fix", - "section": "Bug Fixes" - }, - { - "type": "chore", - "section": "Trivial Changes" - }, - { - "type": "docs", - "section": "Documentation" - }, - { - "type": "deps", - "section": "Dependencies" - }, - { - "type": "test", - "section": "Tests" - } - ] - } - } - ], - "@semantic-release/changelog", - "@semantic-release/npm", - "@semantic-release/github", - "@semantic-release/git" - ] - }, - "scripts": { - "clean": "aegir clean", - "lint": "aegir lint", - "dep-check": "aegir dep-check", - "build": "aegir build", - "test": "aegir test", - "test:chrome": "aegir test -t browser --cov", - "test:chrome-webworker": "aegir test -t webworker", - "test:firefox": "aegir test -t browser -- --browser firefox", - "test:firefox-webworker": "aegir test -t webworker -- --browser firefox", - "test:node": "aegir test -t node --cov", - "test:electron-main": "aegir test -t electron-main", - "release": "aegir release" - }, - "dependencies": { - "@helia/interface": "~0.0.0", - "@ipld/dag-pb": "^4.0.0", - "@libp2p/interfaces": "^3.3.1", - "@libp2p/logger": "^2.0.5", - "@multiformats/murmur3": "^2.1.2", - "hamt-sharding": "^3.0.2", - "interface-blockstore": "^4.0.1", - "ipfs-unixfs": "^9.0.0", - "ipfs-unixfs-exporter": "^10.0.0", - "ipfs-unixfs-importer": "^12.0.0", - "it-last": "^2.0.0", - "it-pipe": "^2.0.5", - "merge-options": "^3.0.4", - "multiformats": "^11.0.1" - }, - "devDependencies": { - "aegir": "^38.1.0", - "blockstore-core": "^3.0.0", - "delay": "^5.0.0", - "it-all": "^2.0.0", - "it-drain": "^2.0.0", - "it-to-buffer": "^3.0.0", - "uint8arrays": "^4.0.3" - }, - "typedoc": { - "entryPoint": "./src/index.ts" - } -} diff --git a/packages/unixfs/src/commands/add.ts b/packages/unixfs/src/commands/add.ts deleted file mode 100644 index 33f92869..00000000 --- a/packages/unixfs/src/commands/add.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Blockstore } from 'interface-blockstore' -import { ImportCandidate, importer, ImportResult, UserImporterOptions } from 'ipfs-unixfs-importer' -import last from 'it-last' -import type { CID } from 'multiformats/cid' -import { UnknownError } from './utils/errors.js' - -function isIterable (obj: any): obj is Iterator { - return obj[Symbol.iterator] != null -} - -function isAsyncIterable (obj: any): obj is AsyncIterator { - return obj[Symbol.asyncIterator] != null -} - -export async function add (source: Uint8Array | Iterator | AsyncIterator | ImportCandidate, blockstore: Blockstore, options: UserImporterOptions = {}): Promise { - let importCandidate: ImportCandidate - - if (source instanceof Uint8Array || isIterable(source) || isAsyncIterable(source)) { - importCandidate = { - // @ts-expect-error FIXME: work out types - content: source - } - } else { - importCandidate = source - } - - const result = await last(importer(importCandidate, blockstore, { - cidVersion: 1, - rawLeaves: true, - ...options - })) - - if (result == null) { - throw new UnknownError('Could not import') - } - - return result.cid -} - -export async function * addStream (source: Iterable | AsyncIterable, blockstore: Blockstore, options: UserImporterOptions = {}): AsyncGenerator { - yield * importer(source, blockstore, { - cidVersion: 1, - rawLeaves: true, - ...options - }) -} diff --git a/packages/unixfs/src/commands/cat.ts b/packages/unixfs/src/commands/cat.ts deleted file mode 100644 index 5f51ad2b..00000000 --- a/packages/unixfs/src/commands/cat.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NoContentError, NotAFileError } from '@helia/interface/errors' -import { Blockstore, exporter } from 'ipfs-unixfs-exporter' -import type { CID } from 'multiformats/cid' -import type { CatOptions } from '../index.js' -import { resolve } from './utils/resolve.js' -import mergeOpts from 'merge-options' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) - -const defaultOptions: CatOptions = { - -} - -export async function * cat (cid: CID, blockstore: Blockstore, options: Partial = {}): AsyncIterable { - const opts: CatOptions = mergeOptions(defaultOptions, options) - const resolved = await resolve(cid, opts.path, blockstore, opts) - const result = await exporter(resolved.cid, blockstore, opts) - - if (result.type !== 'file' && result.type !== 'raw') { - throw new NotAFileError() - } - - if (result.content == null) { - throw new NoContentError() - } - - yield * result.content({ - offset: opts.offset, - length: opts.length - }) -} diff --git a/packages/unixfs/src/commands/chmod.ts b/packages/unixfs/src/commands/chmod.ts deleted file mode 100644 index a02400ad..00000000 --- a/packages/unixfs/src/commands/chmod.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { recursive } from 'ipfs-unixfs-exporter' -import { CID } from 'multiformats/cid' -import type { ChmodOptions } from '../index.js' -import mergeOpts from 'merge-options' -import { logger } from '@libp2p/logger' -import { UnixFS } from 'ipfs-unixfs' -import { pipe } from 'it-pipe' -import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js' -import * as dagPB from '@ipld/dag-pb' -import type { PBNode, PBLink } from '@ipld/dag-pb' -import { importer } from 'ipfs-unixfs-importer' -import { persist } from './utils/persist.js' -import type { Blockstore } from 'interface-blockstore' -import last from 'it-last' -import { sha256 } from 'multiformats/hashes/sha2' -import { resolve, updatePathCids } from './utils/resolve.js' -import * as raw from 'multiformats/codecs/raw' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) -const log = logger('helia:unixfs:chmod') - -const defaultOptions: ChmodOptions = { - recursive: false -} - -export async function chmod (cid: CID, mode: number, blockstore: Blockstore, options: Partial = {}): Promise { - const opts: ChmodOptions = mergeOptions(defaultOptions, options) - const resolved = await resolve(cid, opts.path, blockstore, options) - - log('chmod %c %d', resolved.cid, mode) - - if (opts.recursive) { - // recursively export from root CID, change perms of each entry then reimport - // but do not reimport files, only manipulate dag-pb nodes - const root = await pipe( - async function * () { - for await (const entry of recursive(resolved.cid, blockstore)) { - let metadata: UnixFS - let links: PBLink[] = [] - - if (entry.type === 'raw') { - // convert to UnixFS - metadata = new UnixFS({ type: 'file', data: entry.node }) - } else if (entry.type === 'file' || entry.type === 'directory') { - metadata = entry.unixfs - links = entry.node.Links - } else { - throw new NotUnixFSError() - } - - metadata.mode = mode - - const node = { - Data: metadata.marshal(), - Links: links - } - - yield { - path: entry.path, - content: node - } - } - }, - // @ts-expect-error we account for the incompatible source type with our custom dag builder below - (source) => importer(source, blockstore, { - ...opts, - pin: false, - dagBuilder: async function * (source, block, opts) { - for await (const entry of source) { - yield async function () { - // @ts-expect-error cannot derive type - const node: PBNode = entry.content - - const buf = dagPB.encode(node) - const cid = await persist(buf, block, opts) - - if (node.Data == null) { - throw new InvalidPBNodeError(`${cid} had no data`) - } - - const unixfs = UnixFS.unmarshal(node.Data) - - return { - cid, - size: buf.length, - path: entry.path, - unixfs - } - } - } - } - }), - async (nodes) => await last(nodes) - ) - - if (root == null) { - throw new UnknownError(`Could not chmod ${resolved.cid.toString()}`) - } - - return await updatePathCids(root.cid, resolved, blockstore, options) - } - - const block = await blockstore.get(resolved.cid) - let metadata: UnixFS - let links: PBLink[] = [] - - if (resolved.cid.code === raw.code) { - // convert to UnixFS - metadata = new UnixFS({ type: 'file', data: block }) - } else { - const node = dagPB.decode(block) - - if (node.Data == null) { - throw new InvalidPBNodeError(`${resolved.cid.toString()} had no data`) - } - - links = node.Links - metadata = UnixFS.unmarshal(node.Data) - } - - metadata.mode = mode - const updatedBlock = dagPB.encode({ - Data: metadata.marshal(), - Links: links - }) - - const hash = await sha256.digest(updatedBlock) - const updatedCid = CID.create(resolved.cid.version, dagPB.code, hash) - - await blockstore.put(updatedCid, updatedBlock) - - return await updatePathCids(updatedCid, resolved, blockstore, options) -} diff --git a/packages/unixfs/src/commands/cp.ts b/packages/unixfs/src/commands/cp.ts deleted file mode 100644 index a107fcc9..00000000 --- a/packages/unixfs/src/commands/cp.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { InvalidParametersError } from '@helia/interface/errors' -import type { Blockstore } from 'ipfs-unixfs-exporter' -import type { CID } from 'multiformats/cid' -import type { CpOptions } from '../index.js' -import mergeOpts from 'merge-options' -import { logger } from '@libp2p/logger' -import { addLink } from './utils/add-link.js' -import { cidToPBLink } from './utils/cid-to-pblink.js' -import { cidToDirectory } from './utils/cid-to-directory.js' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) -const log = logger('helia:unixfs:cp') - -const defaultOptions = { - force: false -} - -export async function cp (source: CID, target: CID, name: string, blockstore: Blockstore, options: Partial = {}): Promise { - const opts: CpOptions = mergeOptions(defaultOptions, options) - - if (name.includes('/')) { - throw new InvalidParametersError('Name must not have slashes') - } - - const [ - directory, - pblink - ] = await Promise.all([ - cidToDirectory(target, blockstore, opts), - cidToPBLink(source, name, blockstore, opts) - ]) - - log('Adding %c as "%s" to %c', source, name, target) - - const result = await addLink(directory, pblink, blockstore, { - allowOverwriting: opts.force, - ...opts - }) - - return result.cid -} diff --git a/packages/unixfs/src/commands/ls.ts b/packages/unixfs/src/commands/ls.ts deleted file mode 100644 index 2618d99f..00000000 --- a/packages/unixfs/src/commands/ls.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NoContentError, NotADirectoryError } from '@helia/interface/errors' -import { Blockstore, exporter, UnixFSEntry } from 'ipfs-unixfs-exporter' -import type { CID } from 'multiformats/cid' -import type { LsOptions } from '../index.js' -import { resolve } from './utils/resolve.js' -import mergeOpts from 'merge-options' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) - -const defaultOptions = { - -} - -export async function * ls (cid: CID, blockstore: Blockstore, options: Partial = {}): AsyncIterable { - const opts: LsOptions = mergeOptions(defaultOptions, options) - const resolved = await resolve(cid, opts.path, blockstore, opts) - const result = await exporter(resolved.cid, blockstore) - - if (result.type === 'file' || result.type === 'raw') { - yield result - return - } - - if (result.content == null) { - throw new NoContentError() - } - - if (result.type !== 'directory') { - throw new NotADirectoryError() - } - - yield * result.content({ - offset: options.offset, - length: options.length - }) -} diff --git a/packages/unixfs/src/commands/mkdir.ts b/packages/unixfs/src/commands/mkdir.ts deleted file mode 100644 index 31db6448..00000000 --- a/packages/unixfs/src/commands/mkdir.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { InvalidParametersError, NotADirectoryError } from '@helia/interface/errors' -import { CID } from 'multiformats/cid' -import mergeOpts from 'merge-options' -import { logger } from '@libp2p/logger' -import type { MkdirOptions } from '../index.js' -import * as dagPB from '@ipld/dag-pb' -import { addLink } from './utils/add-link.js' -import type { Blockstore } from 'interface-blockstore' -import { UnixFS } from 'ipfs-unixfs' -import { sha256 } from 'multiformats/hashes/sha2' -import { exporter } from 'ipfs-unixfs-exporter' -import { cidToDirectory } from './utils/cid-to-directory.js' -import { cidToPBLink } from './utils/cid-to-pblink.js' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) -const log = logger('helia:unixfs:mkdir') - -const defaultOptions = { - cidVersion: 1, - force: false -} - -export async function mkdir (parentCid: CID, dirname: string, blockstore: Blockstore, options: Partial = {}): Promise { - const opts: MkdirOptions = mergeOptions(defaultOptions, options) - - if (dirname.includes('/')) { - throw new InvalidParametersError('Path must not have slashes') - } - - const entry = await exporter(parentCid, blockstore, options) - - if (entry.type !== 'directory') { - throw new NotADirectoryError(`${parentCid.toString()} was not a UnixFS directory`) - } - - log('creating %s', dirname) - - const metadata = new UnixFS({ - type: 'directory', - mode: opts.mode, - mtime: opts.mtime - }) - - // Persist the new parent PBNode - const node = { - Data: metadata.marshal(), - Links: [] - } - const buf = dagPB.encode(node) - const hash = await sha256.digest(buf) - const emptyDirCid = CID.create(opts.cidVersion, dagPB.code, hash) - - await blockstore.put(emptyDirCid, buf) - - const [ - directory, - pblink - ] = await Promise.all([ - cidToDirectory(parentCid, blockstore, opts), - cidToPBLink(emptyDirCid, dirname, blockstore, opts) - ]) - - log('adding empty dir called %s to %c', dirname, parentCid) - - const result = await addLink(directory, pblink, blockstore, { - ...opts, - allowOverwriting: opts.force - }) - - return result.cid -} diff --git a/packages/unixfs/src/commands/rm.ts b/packages/unixfs/src/commands/rm.ts deleted file mode 100644 index b5c68341..00000000 --- a/packages/unixfs/src/commands/rm.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { InvalidParametersError } from '@helia/interface/errors' -import type { Blockstore } from 'ipfs-unixfs-exporter' -import type { CID } from 'multiformats/cid' -import type { RmOptions } from '../index.js' -import mergeOpts from 'merge-options' -import { logger } from '@libp2p/logger' -import { removeLink } from './utils/remove-link.js' -import { cidToDirectory } from './utils/cid-to-directory.js' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) -const log = logger('helia:unixfs:rm') - -const defaultOptions = { - -} - -export async function rm (target: CID, name: string, blockstore: Blockstore, options: Partial = {}): Promise { - const opts: RmOptions = mergeOptions(defaultOptions, options) - - if (name.includes('/')) { - throw new InvalidParametersError('Name must not have slashes') - } - - const directory = await cidToDirectory(target, blockstore, opts) - - log('Removing %s from %c', name, target) - - const result = await removeLink(directory, name, blockstore, opts) - - return result.cid -} diff --git a/packages/unixfs/src/commands/stat.ts b/packages/unixfs/src/commands/stat.ts deleted file mode 100644 index e13ed476..00000000 --- a/packages/unixfs/src/commands/stat.ts +++ /dev/null @@ -1,137 +0,0 @@ -import { Blockstore, exporter } from 'ipfs-unixfs-exporter' -import type { CID } from 'multiformats/cid' -import type { StatOptions, UnixFSStats } from '../index.js' -import mergeOpts from 'merge-options' -import { logger } from '@libp2p/logger' -import { UnixFS } from 'ipfs-unixfs' -import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js' -import * as dagPb from '@ipld/dag-pb' -import type { AbortOptions } from '@libp2p/interfaces' -import type { Mtime } from 'ipfs-unixfs' -import { resolve } from './utils/resolve.js' -import * as raw from 'multiformats/codecs/raw' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) -const log = logger('helia:unixfs:stat') - -const defaultOptions = { - -} - -export async function stat (cid: CID, blockstore: Blockstore, options: Partial = {}): Promise { - const opts: StatOptions = mergeOptions(defaultOptions, options) - const resolved = await resolve(cid, options.path, blockstore, opts) - - log('stat %c', resolved.cid) - - const result = await exporter(resolved.cid, blockstore, opts) - - if (result.type !== 'file' && result.type !== 'directory' && result.type !== 'raw') { - throw new NotUnixFSError() - } - - let fileSize: number = 0 - let dagSize: number = 0 - let localFileSize: number = 0 - let localDagSize: number = 0 - let blocks: number = 0 - let mode: number | undefined - let mtime: Mtime | undefined - const type = result.type - - if (result.type === 'raw') { - fileSize = result.node.byteLength - dagSize = result.node.byteLength - localFileSize = result.node.byteLength - localDagSize = result.node.byteLength - blocks = 1 - } - - if (result.type === 'directory') { - fileSize = 0 - dagSize = result.unixfs.marshal().byteLength - localFileSize = 0 - localDagSize = dagSize - blocks = 1 - mode = result.unixfs.mode - mtime = result.unixfs.mtime - } - - if (result.type === 'file') { - const results = await inspectDag(resolved.cid, blockstore, opts) - - fileSize = result.unixfs.fileSize() - dagSize = (result.node.Data?.byteLength ?? 0) + result.node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0) - localFileSize = results.localFileSize - localDagSize = results.localDagSize - blocks = results.blocks - mode = result.unixfs.mode - mtime = result.unixfs.mtime - } - - return { - cid: resolved.cid, - mode, - mtime, - fileSize, - dagSize, - localFileSize, - localDagSize, - blocks, - type - } -} - -interface InspectDagResults { - localFileSize: number - localDagSize: number - blocks: number -} - -async function inspectDag (cid: CID, blockstore: Blockstore, options: AbortOptions): Promise { - const results = { - localFileSize: 0, - localDagSize: 0, - blocks: 0 - } - - if (await blockstore.has(cid, options)) { - const block = await blockstore.get(cid, options) - results.blocks++ - results.localDagSize += block.byteLength - - if (cid.code === raw.code) { - results.localFileSize += block.byteLength - } else if (cid.code === dagPb.code) { - const pbNode = dagPb.decode(block) - - if (pbNode.Links.length > 0) { - // intermediate node - for (const link of pbNode.Links) { - const linkResult = await inspectDag(link.Hash, blockstore, options) - - results.localFileSize += linkResult.localFileSize - results.localDagSize += linkResult.localDagSize - results.blocks += linkResult.blocks - } - } else { - // leaf node - if (pbNode.Data == null) { - throw new InvalidPBNodeError(`PBNode ${cid.toString()} had no data`) - } - - const unixfs = UnixFS.unmarshal(pbNode.Data) - - if (unixfs.data == null) { - throw new InvalidPBNodeError(`UnixFS node ${cid.toString()} had no data`) - } - - results.localFileSize += unixfs.data.byteLength ?? 0 - } - } else { - throw new UnknownError(`${cid.toString()} was neither DAG_PB nor RAW`) - } - } - - return results -} diff --git a/packages/unixfs/src/commands/touch.ts b/packages/unixfs/src/commands/touch.ts deleted file mode 100644 index a4887653..00000000 --- a/packages/unixfs/src/commands/touch.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { recursive } from 'ipfs-unixfs-exporter' -import { CID } from 'multiformats/cid' -import type { TouchOptions } from '../index.js' -import mergeOpts from 'merge-options' -import { logger } from '@libp2p/logger' -import { UnixFS } from 'ipfs-unixfs' -import { pipe } from 'it-pipe' -import { InvalidPBNodeError, NotUnixFSError, UnknownError } from './utils/errors.js' -import * as dagPB from '@ipld/dag-pb' -import type { PBNode, PBLink } from '@ipld/dag-pb' -import { importer } from 'ipfs-unixfs-importer' -import { persist } from './utils/persist.js' -import type { Blockstore } from 'interface-blockstore' -import last from 'it-last' -import { sha256 } from 'multiformats/hashes/sha2' -import { resolve, updatePathCids } from './utils/resolve.js' -import * as raw from 'multiformats/codecs/raw' - -const mergeOptions = mergeOpts.bind({ ignoreUndefined: true }) -const log = logger('helia:unixfs:touch') - -const defaultOptions = { - recursive: false -} - -export async function touch (cid: CID, blockstore: Blockstore, options: Partial = {}): Promise { - const opts: TouchOptions = mergeOptions(defaultOptions, options) - const resolved = await resolve(cid, opts.path, blockstore, opts) - const mtime = opts.mtime ?? { - secs: Date.now() / 1000, - nsecs: 0 - } - - log('touch %c %o', resolved.cid, mtime) - - if (opts.recursive) { - // recursively export from root CID, change perms of each entry then reimport - // but do not reimport files, only manipulate dag-pb nodes - const root = await pipe( - async function * () { - for await (const entry of recursive(resolved.cid, blockstore)) { - let metadata: UnixFS - let links: PBLink[] - - if (entry.type === 'raw') { - metadata = new UnixFS({ data: entry.node }) - links = [] - } else if (entry.type === 'file' || entry.type === 'directory') { - metadata = entry.unixfs - links = entry.node.Links - } else { - throw new NotUnixFSError() - } - - metadata.mtime = mtime - - const node = { - Data: metadata.marshal(), - Links: links - } - - yield { - path: entry.path, - content: node - } - } - }, - // @ts-expect-error we account for the incompatible source type with our custom dag builder below - (source) => importer(source, blockstore, { - ...opts, - pin: false, - dagBuilder: async function * (source, block, opts) { - for await (const entry of source) { - yield async function () { - // @ts-expect-error cannot derive type - const node: PBNode = entry.content - - const buf = dagPB.encode(node) - const cid = await persist(buf, block, opts) - - if (node.Data == null) { - throw new InvalidPBNodeError(`${cid} had no data`) - } - - const unixfs = UnixFS.unmarshal(node.Data) - - return { - cid, - size: buf.length, - path: entry.path, - unixfs - } - } - } - } - }), - async (nodes) => await last(nodes) - ) - - if (root == null) { - throw new UnknownError(`Could not chmod ${resolved.cid.toString()}`) - } - - return await updatePathCids(root.cid, resolved, blockstore, options) - } - - const block = await blockstore.get(resolved.cid) - let metadata: UnixFS - let links: PBLink[] = [] - - if (resolved.cid.code === raw.code) { - metadata = new UnixFS({ data: block }) - } else { - const node = dagPB.decode(block) - links = node.Links - - if (node.Data == null) { - throw new InvalidPBNodeError(`${resolved.cid.toString()} had no data`) - } - - metadata = UnixFS.unmarshal(node.Data) - } - - metadata.mtime = mtime - const updatedBlock = dagPB.encode({ - Data: metadata.marshal(), - Links: links - }) - - const hash = await sha256.digest(updatedBlock) - const updatedCid = CID.create(resolved.cid.version, dagPB.code, hash) - - await blockstore.put(updatedCid, updatedBlock) - - return await updatePathCids(updatedCid, resolved, blockstore, options) -} diff --git a/packages/unixfs/src/commands/utils/add-link.ts b/packages/unixfs/src/commands/utils/add-link.ts deleted file mode 100644 index e33fa8f8..00000000 --- a/packages/unixfs/src/commands/utils/add-link.ts +++ /dev/null @@ -1,319 +0,0 @@ -import * as dagPB from '@ipld/dag-pb' -import { CID } from 'multiformats/cid' -import { logger } from '@libp2p/logger' -import { UnixFS } from 'ipfs-unixfs' -import { DirSharded } from './dir-sharded.js' -import { - updateHamtDirectory, - recreateHamtLevel, - recreateInitialHamtLevel, - createShard, - toPrefix, - addLinksToHamtBucket -} from './hamt-utils.js' -import last from 'it-last' -import type { Blockstore } from 'ipfs-unixfs-exporter' -import type { PBNode, PBLink } from '@ipld/dag-pb/interface' -import { sha256 } from 'multiformats/hashes/sha2' -import type { Bucket } from 'hamt-sharding' -import { AlreadyExistsError, InvalidPBNodeError } from './errors.js' -import { InvalidParametersError } from '@helia/interface/errors' -import type { ImportResult } from 'ipfs-unixfs-importer' -import type { AbortOptions } from '@libp2p/interfaces' -import type { Directory } from './cid-to-directory.js' - -const log = logger('helia:unixfs:components:utils:add-link') - -export interface AddLinkResult { - node: PBNode - cid: CID - size: number -} - -export interface AddLinkOptions extends AbortOptions { - allowOverwriting: boolean -} - -export async function addLink (parent: Directory, child: Required, blockstore: Blockstore, options: AddLinkOptions): Promise { - if (parent.node.Data == null) { - throw new InvalidParametersError('Invalid parent passed to addLink') - } - - // FIXME: this should work on block size not number of links - if (parent.node.Links.length >= 1000) { - log('converting directory to sharded directory') - - const result = await convertToShardedDirectory(parent, blockstore) - parent.cid = result.cid - parent.node = dagPB.decode(await blockstore.get(result.cid)) - } - - if (parent.node.Data == null) { - throw new InvalidParametersError('Invalid parent passed to addLink') - } - - const meta = UnixFS.unmarshal(parent.node.Data) - - if (meta.type === 'hamt-sharded-directory') { - log('adding link to sharded directory') - - return await addToShardedDirectory(parent, child, blockstore, options) - } - - log(`adding ${child.Name} (${child.Hash}) to regular directory`) - - return await addToDirectory(parent, child, blockstore, options) -} - -const convertToShardedDirectory = async (parent: Directory, blockstore: Blockstore): Promise => { - if (parent.node.Data == null) { - throw new InvalidParametersError('Invalid parent passed to convertToShardedDirectory') - } - - const unixfs = UnixFS.unmarshal(parent.node.Data) - - const result = await createShard(blockstore, parent.node.Links.map(link => ({ - name: (link.Name ?? ''), - size: link.Tsize ?? 0, - cid: link.Hash - })), { - mode: unixfs.mode, - mtime: unixfs.mtime - }) - - log(`Converted directory to sharded directory ${result.cid}`) - - return result -} - -const addToDirectory = async (parent: Directory, child: PBLink, blockstore: Blockstore, options: AddLinkOptions): Promise => { - // Remove existing link if it exists - const parentLinks = parent.node.Links.filter((link) => { - const matches = link.Name === child.Name - - if (matches && !options.allowOverwriting) { - throw new AlreadyExistsError() - } - - return !matches - }) - parentLinks.push(child) - - if (parent.node.Data == null) { - throw new InvalidPBNodeError('Parent node with no data passed to addToDirectory') - } - - const node = UnixFS.unmarshal(parent.node.Data) - - let data - if (node.mtime != null) { - // Update mtime if previously set - const ms = Date.now() - const secs = Math.floor(ms / 1000) - - node.mtime = { - secs, - nsecs: (ms - (secs * 1000)) * 1000 - } - - data = node.marshal() - } else { - data = parent.node.Data - } - parent.node = dagPB.prepare({ - Data: data, - Links: parentLinks - }) - - // Persist the new parent PbNode - const buf = dagPB.encode(parent.node) - const hash = await sha256.digest(buf) - const cid = CID.create(parent.cid.version, dagPB.code, hash) - - await blockstore.put(cid, buf) - - return { - node: parent.node, - cid, - size: buf.length - } -} - -const addToShardedDirectory = async (parent: Directory, child: Required, blockstore: Blockstore, options: AddLinkOptions): Promise => { - const { - shard, path - } = await addFileToShardedDirectory(parent, child, blockstore, options) - const result = await last(shard.flush(blockstore)) - - if (result == null) { - throw new Error('No result from flushing shard') - } - - const block = await blockstore.get(result.cid) - const node = dagPB.decode(block) - - // we have written out the shard, but only one sub-shard will have been written so replace it in the original shard - const parentLinks = parent.node.Links.filter((link) => { - const matches = (link.Name ?? '').substring(0, 2) === path[0].prefix - - if (matches && !options.allowOverwriting) { - throw new AlreadyExistsError() - } - - return !matches - }) - - const newLink = node.Links - .find(link => (link.Name ?? '').substring(0, 2) === path[0].prefix) - - if (newLink == null) { - throw new Error(`No link found with prefix ${path[0].prefix}`) - } - - parentLinks.push(newLink) - - return await updateHamtDirectory(parent, blockstore, parentLinks, path[0].bucket, options) -} - -const addFileToShardedDirectory = async (parent: Directory, child: Required, blockstore: Blockstore, options: AddLinkOptions): Promise<{ shard: DirSharded, path: BucketPath[] }> => { - if (parent.node.Data == null) { - throw new InvalidPBNodeError('Parent node with no data passed to addFileToShardedDirectory') - } - - // start at the root bucket and descend, loading nodes as we go - const rootBucket = await recreateInitialHamtLevel(parent.node.Links) - const node = UnixFS.unmarshal(parent.node.Data) - - const shard = new DirSharded({ - root: true, - dir: true, - parent: undefined, - parentKey: undefined, - path: '', - dirty: true, - flat: false, - mode: node.mode - }, { - ...options, - cidVersion: parent.cid.version - }) - shard._bucket = rootBucket - - if (node.mtime != null) { - // update mtime if previously set - shard.mtime = { - secs: Math.round(Date.now() / 1000) - } - } - - // load subshards until the bucket & position no longer changes - const position = await rootBucket._findNewBucketAndPos(child.Name) - const path = toBucketPath(position) - path[0].node = parent.node - let index = 0 - - while (index < path.length) { - const segment = path[index] - index++ - const node = segment.node - - if (node == null) { - throw new Error('Segment had no node') - } - - const link = node.Links - .find(link => (link.Name ?? '').substring(0, 2) === segment.prefix) - - if (link == null) { - // prefix is new, file will be added to the current bucket - log(`Link ${segment.prefix}${child.Name} will be added`) - index = path.length - - break - } - - if (link.Name === `${segment.prefix}${child.Name}`) { - // file already existed, file will be added to the current bucket - log(`Link ${segment.prefix}${child.Name} will be replaced`) - index = path.length - - break - } - - if ((link.Name ?? '').length > 2) { - // another file had the same prefix, will be replaced with a subshard - log(`Link ${link.Name} ${link.Hash} will be replaced with a subshard`) - index = path.length - - break - } - - // load sub-shard - log(`Found subshard ${segment.prefix}`) - const block = await blockstore.get(link.Hash) - const subShard = dagPB.decode(block) - - // subshard hasn't been loaded, descend to the next level of the HAMT - if (path[index] == null) { - log(`Loaded new subshard ${segment.prefix}`) - await recreateHamtLevel(blockstore, subShard.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options) - - const position = await rootBucket._findNewBucketAndPos(child.Name) - - path.push({ - bucket: position.bucket, - prefix: toPrefix(position.pos), - node: subShard - }) - - break - } - - const nextSegment = path[index] - - // add next levels worth of links to bucket - await addLinksToHamtBucket(blockstore, subShard.Links, nextSegment.bucket, rootBucket, options) - - nextSegment.node = subShard - } - - // finally add the new file into the shard - await shard._bucket.put(child.Name, { - size: child.Tsize, - cid: child.Hash - }) - - return { - shard, path - } -} - -export interface BucketPath { - bucket: Bucket - prefix: string - node?: PBNode -} - -const toBucketPath = (position: { pos: number, bucket: Bucket }): BucketPath[] => { - const path = [{ - bucket: position.bucket, - prefix: toPrefix(position.pos) - }] - - let bucket = position.bucket._parent - let positionInBucket = position.bucket._posAtParent - - while (bucket != null) { - path.push({ - bucket, - prefix: toPrefix(positionInBucket) - }) - - positionInBucket = bucket._posAtParent - bucket = bucket._parent - } - - path.reverse() - - return path -} diff --git a/packages/unixfs/src/commands/utils/cid-to-directory.ts b/packages/unixfs/src/commands/utils/cid-to-directory.ts deleted file mode 100644 index 0e6ce758..00000000 --- a/packages/unixfs/src/commands/utils/cid-to-directory.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NotADirectoryError } from '@helia/interface/errors' -import { Blockstore, exporter } from 'ipfs-unixfs-exporter' -import type { CID } from 'multiformats/cid' -import type { PBNode } from '@ipld/dag-pb' -import type { AbortOptions } from '@libp2p/interfaces' - -export interface Directory { - cid: CID - node: PBNode -} - -export async function cidToDirectory (cid: CID, blockstore: Blockstore, options: AbortOptions = {}): Promise { - const entry = await exporter(cid, blockstore, options) - - if (entry.type !== 'directory') { - throw new NotADirectoryError(`${cid.toString()} was not a UnixFS directory`) - } - - return { - cid, - node: entry.node - } -} diff --git a/packages/unixfs/src/commands/utils/cid-to-pblink.ts b/packages/unixfs/src/commands/utils/cid-to-pblink.ts deleted file mode 100644 index 6a6a68c5..00000000 --- a/packages/unixfs/src/commands/utils/cid-to-pblink.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Blockstore, exporter } from 'ipfs-unixfs-exporter' -import type { CID } from 'multiformats/cid' -import { NotUnixFSError } from './errors.js' -import * as dagPb from '@ipld/dag-pb' -import type { PBNode, PBLink } from '@ipld/dag-pb' -import type { AbortOptions } from '@libp2p/interfaces' - -export async function cidToPBLink (cid: CID, name: string, blockstore: Blockstore, options?: AbortOptions): Promise> { - const sourceEntry = await exporter(cid, blockstore, options) - - if (sourceEntry.type !== 'directory' && sourceEntry.type !== 'file' && sourceEntry.type !== 'raw') { - throw new NotUnixFSError(`${cid.toString()} was not a UnixFS node`) - } - - return { - Name: name, - Tsize: sourceEntry.node instanceof Uint8Array ? sourceEntry.node.byteLength : dagNodeTsize(sourceEntry.node), - Hash: cid - } -} - -function dagNodeTsize (node: PBNode): number { - const linkSizes = node.Links.reduce((acc, curr) => acc + (curr.Tsize ?? 0), 0) - - return dagPb.encode(node).byteLength + linkSizes -} diff --git a/packages/unixfs/src/commands/utils/dir-sharded.ts b/packages/unixfs/src/commands/utils/dir-sharded.ts deleted file mode 100644 index fa160ed6..00000000 --- a/packages/unixfs/src/commands/utils/dir-sharded.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { encode, prepare } from '@ipld/dag-pb' -import { UnixFS } from 'ipfs-unixfs' -import { persist } from './persist.js' -import { createHAMT, Bucket, BucketChild } from 'hamt-sharding' -import { - hamtHashCode, - hamtHashFn, - hamtBucketBits -} from './hamt-constants.js' -import type { CID, Version } from 'multiformats/cid' -import type { PBNode } from '@ipld/dag-pb/interface' -import type { Mtime } from 'ipfs-unixfs' -import type { BlockCodec } from 'multiformats/codecs/interface' -import type { Blockstore } from 'ipfs-unixfs-importer' - -export interface ImportResult { - cid: CID - node: PBNode - size: number -} - -export interface DirContents { - cid?: CID - size?: number -} - -export interface DirOptions { - mtime?: Mtime - mode?: number - codec?: BlockCodec - cidVersion?: Version - onlyHash?: boolean - signal?: AbortSignal -} - -export interface DirProps { - root: boolean - dir: boolean - path: string - dirty: boolean - flat: boolean - parent?: Dir - parentKey?: string - unixfs?: UnixFS - mode?: number - mtime?: Mtime -} - -export abstract class Dir { - protected options: DirOptions - protected root: boolean - protected dir: boolean - protected path: string - protected dirty: boolean - protected flat: boolean - protected parent?: Dir - protected parentKey?: string - protected unixfs?: UnixFS - protected mode?: number - public mtime?: Mtime - protected cid?: CID - protected size?: number - - constructor (props: DirProps, options: DirOptions) { - this.options = options ?? {} - this.root = props.root - this.dir = props.dir - this.path = props.path - this.dirty = props.dirty - this.flat = props.flat - this.parent = props.parent - this.parentKey = props.parentKey - this.unixfs = props.unixfs - this.mode = props.mode - this.mtime = props.mtime - } -} - -export class DirSharded extends Dir { - public _bucket: Bucket - - constructor (props: DirProps, options: DirOptions) { - super(props, options) - - /** @type {Bucket} */ - this._bucket = createHAMT({ - hashFn: hamtHashFn, - bits: hamtBucketBits - }) - } - - async put (name: string, value: DirContents): Promise { - await this._bucket.put(name, value) - } - - async get (name: string): Promise { - return await this._bucket.get(name) - } - - childCount (): number { - return this._bucket.leafCount() - } - - directChildrenCount (): number { - return this._bucket.childrenCount() - } - - onlyChild (): Bucket | BucketChild { - return this._bucket.onlyChild() - } - - async * eachChildSeries (): AsyncGenerator<{ key: string, child: DirContents }> { - for await (const { key, value } of this._bucket.eachLeafSeries()) { - yield { - key, - child: value - } - } - } - - async * flush (blockstore: Blockstore): AsyncIterable { - yield * flush(this._bucket, blockstore, this, this.options) - } -} - -async function * flush (bucket: Bucket, blockstore: Blockstore, shardRoot: any, options: DirOptions): AsyncIterable { - const children = bucket._children - const links = [] - let childrenSize = 0 - - for (let i = 0; i < children.length; i++) { - const child = children.get(i) - - if (child == null) { - continue - } - - const labelPrefix = i.toString(16).toUpperCase().padStart(2, '0') - - if (child instanceof Bucket) { - let shard: ImportResult | undefined - - for await (const subShard of flush(child, blockstore, null, options)) { - shard = subShard - } - - if (shard == null) { - throw new Error('Could not flush sharded directory, no subshard found') - } - - links.push({ - Name: labelPrefix, - Tsize: shard.size, - Hash: shard.cid - }) - childrenSize += shard.size - } else if (typeof child.value.flush === 'function') { - const dir = child.value - let flushedDir - - for await (const entry of dir.flush(blockstore)) { - flushedDir = entry - - yield flushedDir - } - - const label = labelPrefix + child.key - links.push({ - Name: label, - Tsize: flushedDir.size, - Hash: flushedDir.cid - }) - - childrenSize += flushedDir.size // eslint-disable-line @typescript-eslint/restrict-plus-operands - } else { - const value = child.value - - if (value.cid == null) { - continue - } - - const label = labelPrefix + child.key - const size = value.size - - links.push({ - Name: label, - Tsize: size, - Hash: value.cid - }) - childrenSize += size ?? 0 // eslint-disable-line @typescript-eslint/restrict-plus-operands - } - } - - // go-ipfs uses little endian, that's why we have to - // reverse the bit field before storing it - const data = Uint8Array.from(children.bitField().reverse()) - const dir = new UnixFS({ - type: 'hamt-sharded-directory', - data, - fanout: bucket.tableSize(), - hashType: hamtHashCode, - mtime: shardRoot?.mtime, - mode: shardRoot?.mode - }) - - const node = { - Data: dir.marshal(), - Links: links - } - const buffer = encode(prepare(node)) - const cid = await persist(buffer, blockstore, options) - const size = buffer.length + childrenSize - - yield { - cid, - node, - size - } -} diff --git a/packages/unixfs/src/commands/utils/errors.ts b/packages/unixfs/src/commands/utils/errors.ts deleted file mode 100644 index 379c9ac6..00000000 --- a/packages/unixfs/src/commands/utils/errors.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { HeliaError } from '@helia/interface/errors' - -export class NotUnixFSError extends HeliaError { - constructor (message = 'not a Unixfs node') { - super(message, 'NotUnixFSError', 'ERR_NOT_UNIXFS') - } -} - -export class InvalidPBNodeError extends HeliaError { - constructor (message = 'invalid PBNode') { - super(message, 'InvalidPBNodeError', 'ERR_INVALID_PBNODE') - } -} - -export class UnknownError extends HeliaError { - constructor (message = 'unknown error') { - super(message, 'InvalidPBNodeError', 'ERR_UNKNOWN_ERROR') - } -} - -export class AlreadyExistsError extends HeliaError { - constructor (message = 'path already exists') { - super(message, 'NotUnixFSError', 'ERR_ALREADY_EXISTS') - } -} - -export class DoesNotExistError extends HeliaError { - constructor (message = 'path does not exist') { - super(message, 'NotUnixFSError', 'ERR_DOES_NOT_EXIST') - } -} diff --git a/packages/unixfs/src/commands/utils/hamt-constants.ts b/packages/unixfs/src/commands/utils/hamt-constants.ts deleted file mode 100644 index 6f510236..00000000 --- a/packages/unixfs/src/commands/utils/hamt-constants.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { murmur3128 } from '@multiformats/murmur3' - -export const hamtHashCode = murmur3128.code -export const hamtBucketBits = 8 - -export async function hamtHashFn (buf: Uint8Array): Promise { - return (await murmur3128.encode(buf)) - // Murmur3 outputs 128 bit but, accidentally, IPFS Go's - // implementation only uses the first 64, so we must do the same - // for parity.. - .subarray(0, 8) - // Invert buffer because that's how Go impl does it - .reverse() -} diff --git a/packages/unixfs/src/commands/utils/hamt-utils.ts b/packages/unixfs/src/commands/utils/hamt-utils.ts deleted file mode 100644 index 2cbed1f6..00000000 --- a/packages/unixfs/src/commands/utils/hamt-utils.ts +++ /dev/null @@ -1,285 +0,0 @@ -import * as dagPB from '@ipld/dag-pb' -import { - Bucket, - createHAMT -} from 'hamt-sharding' -import { DirSharded } from './dir-sharded.js' -import { logger } from '@libp2p/logger' -import { UnixFS } from 'ipfs-unixfs' -import last from 'it-last' -import { CID } from 'multiformats/cid' -import { - hamtHashCode, - hamtHashFn, - hamtBucketBits -} from './hamt-constants.js' -import type { PBLink, PBNode } from '@ipld/dag-pb/interface' -import { sha256 } from 'multiformats/hashes/sha2' -import type { Blockstore } from 'interface-blockstore' -import type { Mtime } from 'ipfs-unixfs' -import type { Directory } from './cid-to-directory.js' -import type { AbortOptions } from '@libp2p/interfaces' -import type { ImportResult } from 'ipfs-unixfs-importer' - -const log = logger('helia:unixfs:commands:utils:hamt-utils') - -export interface UpdateHamtResult { - node: PBNode - cid: CID - size: number -} - -export const updateHamtDirectory = async (parent: Directory, blockstore: Blockstore, links: PBLink[], bucket: Bucket, options: AbortOptions): Promise => { - if (parent.node.Data == null) { - throw new Error('Could not update HAMT directory because parent had no data') - } - - // update parent with new bit field - const data = Uint8Array.from(bucket._children.bitField().reverse()) - const node = UnixFS.unmarshal(parent.node.Data) - const dir = new UnixFS({ - type: 'hamt-sharded-directory', - data, - fanout: bucket.tableSize(), - hashType: hamtHashCode, - mode: node.mode, - mtime: node.mtime - }) - - parent.node = { - Data: dir.marshal(), - Links: links.sort((a, b) => (a.Name ?? '').localeCompare(b.Name ?? '')) - } - const buf = dagPB.encode(parent.node) - const hash = await sha256.digest(buf) - const cid = CID.create(parent.cid.version, dagPB.code, hash) - - await blockstore.put(cid, buf, options) - - return { - node: parent.node, - cid, - size: links.reduce((sum, link) => sum + (link.Tsize ?? 0), buf.length) - } -} - -export const recreateHamtLevel = async (blockstore: Blockstore, links: PBLink[], rootBucket: Bucket, parentBucket: Bucket, positionAtParent: number, options: AbortOptions): Promise> => { - // recreate this level of the HAMT - const bucket = new Bucket({ - hash: rootBucket._options.hash, - bits: rootBucket._options.bits - }, parentBucket, positionAtParent) - parentBucket._putObjectAt(positionAtParent, bucket) - - await addLinksToHamtBucket(blockstore, links, bucket, rootBucket, options) - - return bucket -} - -export const recreateInitialHamtLevel = async (links: PBLink[]): Promise> => { - const bucket = createHAMT({ - hashFn: hamtHashFn, - bits: hamtBucketBits - }) - - // populate sub bucket but do not recurse as we do not want to pull whole shard in - await Promise.all( - links.map(async link => { - const linkName = (link.Name ?? '') - - if (linkName.length === 2) { - const pos = parseInt(linkName, 16) - - const subBucket = new Bucket({ - hash: bucket._options.hash, - bits: bucket._options.bits - }, bucket, pos) - bucket._putObjectAt(pos, subBucket) - - await Promise.resolve(); return - } - - await bucket.put(linkName.substring(2), { - size: link.Tsize, - cid: link.Hash - }) - }) - ) - - return bucket -} - -export const addLinksToHamtBucket = async (blockstore: Blockstore, links: PBLink[], bucket: Bucket, rootBucket: Bucket, options: AbortOptions): Promise => { - await Promise.all( - links.map(async link => { - const linkName = (link.Name ?? '') - - if (linkName.length === 2) { - log('Populating sub bucket', linkName) - const pos = parseInt(linkName, 16) - const block = await blockstore.get(link.Hash, options) - const node = dagPB.decode(block) - - const subBucket = new Bucket({ - hash: rootBucket._options.hash, - bits: rootBucket._options.bits - }, bucket, pos) - bucket._putObjectAt(pos, subBucket) - - await addLinksToHamtBucket(blockstore, node.Links, subBucket, rootBucket, options) - - await Promise.resolve(); return - } - - await rootBucket.put(linkName.substring(2), { - size: link.Tsize, - cid: link.Hash - }) - }) - ) -} - -export const toPrefix = (position: number): string => { - return position - .toString(16) - .toUpperCase() - .padStart(2, '0') - .substring(0, 2) -} - -export interface HamtPath { - rootBucket: Bucket - path: Array<{ - bucket: Bucket - prefix: string - node?: PBNode - }> -} - -export const generatePath = async (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise => { - // start at the root bucket and descend, loading nodes as we go - const rootBucket = await recreateInitialHamtLevel(parent.node.Links) - const position = await rootBucket._findNewBucketAndPos(name) - - // the path to the root bucket - const path: Array<{ bucket: Bucket, prefix: string, node?: PBNode }> = [{ - bucket: position.bucket, - prefix: toPrefix(position.pos) - }] - let currentBucket = position.bucket - - while (currentBucket !== rootBucket) { - path.push({ - bucket: currentBucket, - prefix: toPrefix(currentBucket._posAtParent) - }) - - // @ts-expect-error - only the root bucket's parent will be undefined - currentBucket = currentBucket._parent - } - - path.reverse() - path[0].node = parent.node - - // load PbNode for each path segment - for (let i = 0; i < path.length; i++) { - const segment = path[i] - - if (segment.node == null) { - throw new Error('Could not generate HAMT path') - } - - // find prefix in links - const link = segment.node.Links - .filter(link => (link.Name ?? '').substring(0, 2) === segment.prefix) - .pop() - - // entry was not in shard - if (link == null) { - // reached bottom of tree, file will be added to the current bucket - log(`Link ${segment.prefix}${name} will be added`) - // return path - continue - } - - // found entry - if (link.Name === `${segment.prefix}${name}`) { - log(`Link ${segment.prefix}${name} will be replaced`) - // file already existed, file will be added to the current bucket - // return path - continue - } - - // found subshard - log(`Found subshard ${segment.prefix}`) - const block = await blockstore.get(link.Hash, options) - const node = dagPB.decode(block) - - // subshard hasn't been loaded, descend to the next level of the HAMT - if (path[i + 1] == null) { - log(`Loaded new subshard ${segment.prefix}`) - - await recreateHamtLevel(blockstore, node.Links, rootBucket, segment.bucket, parseInt(segment.prefix, 16), options) - const position = await rootBucket._findNewBucketAndPos(name) - - // i-- - path.push({ - bucket: position.bucket, - prefix: toPrefix(position.pos), - node - }) - - continue - } - - const nextSegment = path[i + 1] - - // add intermediate links to bucket - await addLinksToHamtBucket(blockstore, node.Links, nextSegment.bucket, rootBucket, options) - - nextSegment.node = node - } - - await rootBucket.put(name, true) - - path.reverse() - - return { - rootBucket, - path - } -} - -export interface CreateShardOptions { - mtime?: Mtime - mode?: number -} - -export const createShard = async (blockstore: Blockstore, contents: Array<{ name: string, size: number, cid: CID }>, options: CreateShardOptions = {}): Promise => { - const shard = new DirSharded({ - root: true, - dir: true, - parent: undefined, - parentKey: undefined, - path: '', - dirty: true, - flat: false, - mtime: options.mtime, - mode: options.mode - }, options) - - for (let i = 0; i < contents.length; i++) { - await shard._bucket.put(contents[i].name, { - size: contents[i].size, - cid: contents[i].cid - }) - } - - const res = await last(shard.flush(blockstore)) - - if (res == null) { - throw new Error('Flushing shard yielded no result') - } - - return res -} diff --git a/packages/unixfs/src/commands/utils/persist.ts b/packages/unixfs/src/commands/utils/persist.ts deleted file mode 100644 index 1e287c6f..00000000 --- a/packages/unixfs/src/commands/utils/persist.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CID, Version } from 'multiformats/cid' -import * as dagPB from '@ipld/dag-pb' -import { sha256 } from 'multiformats/hashes/sha2' -import type { BlockCodec } from 'multiformats/codecs/interface' -import type { AbortOptions } from '@libp2p/interfaces' -import type { Blockstore } from 'interface-blockstore' - -export interface PersistOptions extends AbortOptions { - codec?: BlockCodec - cidVersion?: Version -} - -export const persist = async (buffer: Uint8Array, blockstore: Blockstore, options: PersistOptions = {}): Promise => { - const multihash = await sha256.digest(buffer) - const cid = CID.create(options.cidVersion ?? 1, dagPB.code, multihash) - - await blockstore.put(cid, buffer, { - signal: options.signal - }) - - return cid -} diff --git a/packages/unixfs/src/commands/utils/remove-link.ts b/packages/unixfs/src/commands/utils/remove-link.ts deleted file mode 100644 index b2608690..00000000 --- a/packages/unixfs/src/commands/utils/remove-link.ts +++ /dev/null @@ -1,151 +0,0 @@ - -import * as dagPB from '@ipld/dag-pb' -import { CID } from 'multiformats/cid' -import { logger } from '@libp2p/logger' -import { UnixFS } from 'ipfs-unixfs' -import { - generatePath, - updateHamtDirectory, - UpdateHamtResult -} from './hamt-utils.js' -import type { PBNode, PBLink } from '@ipld/dag-pb' -import type { Blockstore } from 'interface-blockstore' -import { sha256 } from 'multiformats/hashes/sha2' -import type { Bucket } from 'hamt-sharding' -import type { Directory } from './cid-to-directory.js' -import type { AbortOptions } from '@libp2p/interfaces' -import { InvalidPBNodeError } from './errors.js' -import { InvalidParametersError } from '@helia/interface/errors' - -const log = logger('helia:unixfs:utils:remove-link') - -export interface RemoveLinkResult { - node: PBNode - cid: CID -} - -export async function removeLink (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise { - if (parent.node.Data == null) { - throw new InvalidPBNodeError('Parent node had no data') - } - - const meta = UnixFS.unmarshal(parent.node.Data) - - if (meta.type === 'hamt-sharded-directory') { - log(`Removing ${name} from sharded directory`) - - return await removeFromShardedDirectory(parent, name, blockstore, options) - } - - log(`Removing link ${name} regular directory`) - - return await removeFromDirectory(parent, name, blockstore, options) -} - -const removeFromDirectory = async (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise => { - // Remove existing link if it exists - parent.node.Links = parent.node.Links.filter((link) => { - return link.Name !== name - }) - - const parentBlock = dagPB.encode(parent.node) - const hash = await sha256.digest(parentBlock) - const parentCid = CID.create(parent.cid.version, dagPB.code, hash) - - await blockstore.put(parentCid, parentBlock, options) - - log(`Updated regular directory ${parentCid}`) - - return { - node: parent.node, - cid: parentCid - } -} - -const removeFromShardedDirectory = async (parent: Directory, name: string, blockstore: Blockstore, options: AbortOptions): Promise => { - const { - rootBucket, path - } = await generatePath(parent, name, blockstore, options) - - await rootBucket.del(name) - - const { - node - } = await updateShard(parent, blockstore, path, name, options) - - return await updateHamtDirectory(parent, blockstore, node.Links, rootBucket, options) -} - -const updateShard = async (parent: Directory, blockstore: Blockstore, positions: Array<{ bucket: Bucket, prefix: string, node?: PBNode }>, name: string, options: AbortOptions): Promise<{ node: PBNode, cid: CID, size: number }> => { - const last = positions.pop() - - if (last == null) { - throw new InvalidParametersError('Could not find parent') - } - - const { - bucket, - prefix, - node - } = last - - if (node == null) { - throw new InvalidParametersError('Could not find parent') - } - - const link = node.Links - .find(link => (link.Name ?? '').substring(0, 2) === prefix) - - if (link == null) { - throw new InvalidParametersError(`No link found with prefix ${prefix} for file ${name}`) - } - - if (link.Name === `${prefix}${name}`) { - log(`Removing existing link ${link.Name}`) - - const links = node.Links.filter((nodeLink) => { - return nodeLink.Name !== link.Name - }) - - await bucket.del(name) - - parent.node = node - - return await updateHamtDirectory(parent, blockstore, links, bucket, options) - } - - log(`Descending into sub-shard ${link.Name} for ${prefix}${name}`) - - const result = await updateShard(parent, blockstore, positions, name, options) - - const child: Required = { - Hash: result.cid, - Tsize: result.size, - Name: prefix - } - - if (result.node.Links.length === 1) { - log(`Removing subshard for ${prefix}`) - - // convert shard back to normal dir - const link = result.node.Links[0] - - child.Name = `${prefix}${(link.Name ?? '').substring(2)}` - child.Hash = link.Hash - child.Tsize = link.Tsize ?? 0 - } - - log(`Updating shard ${prefix} with name ${child.Name}`) - - return await updateShardParent(parent, child, prefix, blockstore, bucket, options) -} - -const updateShardParent = async (parent: Directory, child: Required, oldName: string, blockstore: Blockstore, bucket: Bucket, options: AbortOptions): Promise => { - // Remove existing link if it exists - const parentLinks = parent.node.Links.filter((link) => { - return link.Name !== oldName - }) - parentLinks.push(child) - - return await updateHamtDirectory(parent, blockstore, parentLinks, bucket, options) -} diff --git a/packages/unixfs/src/commands/utils/resolve.ts b/packages/unixfs/src/commands/utils/resolve.ts deleted file mode 100644 index ef5a76bc..00000000 --- a/packages/unixfs/src/commands/utils/resolve.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { CID } from 'multiformats/cid' -import { Blockstore, exporter } from 'ipfs-unixfs-exporter' -import type { AbortOptions } from '@libp2p/interfaces' -import { InvalidParametersError } from '@helia/interface/errors' -import { logger } from '@libp2p/logger' -import { DoesNotExistError } from './errors.js' -import { addLink } from './add-link.js' -import { cidToDirectory } from './cid-to-directory.js' -import { cidToPBLink } from './cid-to-pblink.js' - -const log = logger('helia:unixfs:components:utils:add-link') - -export interface Segment { - name: string - cid: CID - size: number -} - -export interface ResolveResult { - /** - * The CID at the end of the path - */ - cid: CID - - path?: string - - /** - * If present, these are the CIDs and path segments that were traversed through to reach the final CID - * - * If not present, there was no path passed or the path was an empty string - */ - segments?: Segment[] -} - -export async function resolve (cid: CID, path: string | undefined, blockstore: Blockstore, options: AbortOptions): Promise { - log('resolve "%s" under %c', path, cid) - - if (path == null || path === '') { - return { cid } - } - - const parts = path.split('/').filter(Boolean) - const segments: Segment[] = [{ - name: '', - cid, - size: 0 - }] - - for (let i = 0; i < parts.length; i++) { - const part = parts[i] - const result = await exporter(cid, blockstore, options) - - if (result.type === 'file') { - if (i < parts.length - 1) { - throw new InvalidParametersError('Path was invalid') - } - - cid = result.cid - } else if (result.type === 'directory') { - let dirCid: CID | undefined - - for await (const entry of result.content()) { - if (entry.name === part) { - dirCid = entry.cid - } - } - - if (dirCid == null) { - throw new DoesNotExistError('Could not find path in directory') - } - - cid = dirCid - - segments.push({ - name: part, - cid, - size: result.size - }) - } else { - throw new InvalidParametersError('Could not resolve path') - } - } - - return { - cid, - path, - segments - } -} - -/** - * Where we have descended into a DAG to update a child node, ascend up the DAG creating - * new hashes and blocks for the changed content - */ -export async function updatePathCids (cid: CID, result: ResolveResult, blockstore: Blockstore, options: AbortOptions): Promise { - if (result.segments == null || result.segments.length === 0) { - return cid - } - - let child = result.segments.pop() - - if (child == null) { - throw new Error('Insufficient segments') - } - - child.cid = cid - - result.segments.reverse() - - for (const parent of result.segments) { - const [ - directory, - pblink - ] = await Promise.all([ - cidToDirectory(parent.cid, blockstore, options), - cidToPBLink(child.cid, child.name, blockstore, options) - ]) - - const result = await addLink(directory, pblink, blockstore, { - ...options, - allowOverwriting: true - }) - - cid = result.cid - parent.cid = cid - child = parent - } - - return cid -} diff --git a/packages/unixfs/src/index.ts b/packages/unixfs/src/index.ts deleted file mode 100644 index 832fd159..00000000 --- a/packages/unixfs/src/index.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { CID, Version } from 'multiformats/cid' -import type { Blockstore } from 'interface-blockstore' -import type { AbortOptions } from '@libp2p/interfaces' -import type { ImportCandidate, ImportResult, UserImporterOptions } from 'ipfs-unixfs-importer' -import { add, addStream } from './commands/add.js' -import { cat } from './commands/cat.js' -import { mkdir } from './commands/mkdir.js' -import type { Mtime } from 'ipfs-unixfs' -import { cp } from './commands/cp.js' -import { rm } from './commands/rm.js' -import { stat } from './commands/stat.js' -import { touch } from './commands/touch.js' -import { chmod } from './commands/chmod.js' -import type { UnixFSEntry } from 'ipfs-unixfs-exporter' -import { ls } from './commands/ls.js' - -export interface UnixFSComponents { - blockstore: Blockstore -} - -export interface CatOptions extends AbortOptions { - offset?: number - length?: number - path?: string -} - -export interface ChmodOptions extends AbortOptions { - recursive: boolean - path?: string -} - -export interface CpOptions extends AbortOptions { - force: boolean -} - -export interface LsOptions extends AbortOptions { - path?: string - offset?: number - length?: number -} - -export interface MkdirOptions extends AbortOptions { - cidVersion: Version - force: boolean - mode?: number - mtime?: Mtime -} - -export interface RmOptions extends AbortOptions { - -} - -export interface StatOptions extends AbortOptions { - path?: string -} - -export interface UnixFSStats { - /** - * The file or directory CID - */ - cid: CID - - /** - * The file or directory mode - */ - mode?: number - - /** - * The file or directory mtime - */ - mtime?: Mtime - - /** - * The size of the file in bytes - */ - fileSize: number - - /** - * The size of the DAG that holds the file in bytes - */ - dagSize: number - - /** - * How much of the file is in the local block store - */ - localFileSize: number - - /** - * How much of the DAG that holds the file is in the local blockstore - */ - localDagSize: number - - /** - * How many blocks make up the DAG - nb. this will only be accurate - * if all blocks are present in the local blockstore - */ - blocks: number - - /** - * The type of file - */ - type: 'file' | 'directory' | 'raw' -} - -export interface TouchOptions extends AbortOptions { - mtime?: Mtime - path?: string - recursive: boolean -} - -export interface UnixFS { - add: (source: Uint8Array | Iterator | AsyncIterator | ImportCandidate, options?: Partial) => Promise - addStream: (source: Iterable | AsyncIterable, options?: Partial) => AsyncGenerator - cat: (cid: CID, options?: Partial) => AsyncIterable - chmod: (source: CID, mode: number, options?: Partial) => Promise - cp: (source: CID, target: CID, name: string, options?: Partial) => Promise - ls: (cid: CID, options?: Partial) => AsyncIterable - mkdir: (cid: CID, dirname: string, options?: Partial) => Promise - rm: (cid: CID, path: string, options?: Partial) => Promise - stat: (cid: CID, options?: Partial) => Promise - touch: (cid: CID, options?: Partial) => Promise -} - -class DefaultUnixFS implements UnixFS { - private readonly components: UnixFSComponents - - constructor (components: UnixFSComponents) { - this.components = components - } - - async add (source: Uint8Array | Iterator | AsyncIterator | ImportCandidate, options: Partial = {}): Promise { - return await add(source, this.components.blockstore, options) - } - - async * addStream (source: Iterable | AsyncIterable, options: Partial = {}): AsyncGenerator { - yield * addStream(source, this.components.blockstore, options) - } - - async * cat (cid: CID, options: Partial = {}): AsyncIterable { - yield * cat(cid, this.components.blockstore, options) - } - - async chmod (source: CID, mode: number, options: Partial = {}): Promise { - return await chmod(source, mode, this.components.blockstore, options) - } - - async cp (source: CID, target: CID, name: string, options: Partial = {}): Promise { - return await cp(source, target, name, this.components.blockstore, options) - } - - async * ls (cid: CID, options: Partial = {}): AsyncIterable { - yield * ls(cid, this.components.blockstore, options) - } - - async mkdir (cid: CID, dirname: string, options: Partial = {}): Promise { - return await mkdir(cid, dirname, this.components.blockstore, options) - } - - async rm (cid: CID, path: string, options: Partial = {}): Promise { - return await rm(cid, path, this.components.blockstore, options) - } - - async stat (cid: CID, options: Partial = {}): Promise { - return await stat(cid, this.components.blockstore, options) - } - - async touch (cid: CID, options: Partial = {}): Promise { - return await touch(cid, this.components.blockstore, options) - } -} - -export function unixfs (helia: { blockstore: Blockstore }): UnixFS { - return new DefaultUnixFS(helia) -} diff --git a/packages/unixfs/test/cat.spec.ts b/packages/unixfs/test/cat.spec.ts deleted file mode 100644 index 4aa97216..00000000 --- a/packages/unixfs/test/cat.spec.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import type { CID } from 'multiformats/cid' -import type { Blockstore } from 'interface-blockstore' -import { unixfs, UnixFS } from '../src/index.js' -import { MemoryBlockstore } from 'blockstore-core' -import toBuffer from 'it-to-buffer' -import drain from 'it-drain' - -const smallFile = Uint8Array.from(new Array(13).fill(0).map(() => Math.random() * 100)) - -describe('cat', () => { - let blockstore: Blockstore - let fs: UnixFS - let emptyDirCid: CID - - beforeEach(async () => { - blockstore = new MemoryBlockstore() - - fs = unixfs({ blockstore }) - - emptyDirCid = await fs.add({ path: 'empty' }) - }) - - it('reads a small file', async () => { - const cid = await fs.add(smallFile) - const bytes = await toBuffer(fs.cat(cid)) - - expect(bytes).to.equalBytes(smallFile) - }) - - it('reads a file with an offset', async () => { - const offset = 10 - const cid = await fs.add(smallFile) - const bytes = await toBuffer(fs.cat(cid, { - offset - })) - - expect(bytes).to.equalBytes(smallFile.subarray(offset)) - }) - - it('reads a file with a length', async () => { - const length = 10 - const cid = await fs.add(smallFile) - const bytes = await toBuffer(fs.cat(cid, { - length - })) - - expect(bytes).to.equalBytes(smallFile.subarray(0, length)) - }) - - it('reads a file with an offset and a length', async () => { - const offset = 2 - const length = 5 - const cid = await fs.add(smallFile) - const bytes = await toBuffer(fs.cat(cid, { - offset, - length - })) - - expect(bytes).to.equalBytes(smallFile.subarray(offset, offset + length)) - }) - - it('refuses to read a directory', async () => { - await expect(drain(fs.cat(emptyDirCid))).to.eventually.be.rejected - .with.property('code', 'ERR_NOT_FILE') - }) - -/* - describe('with sharding', () => { - it('reads file from inside a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - const filePath = `${shardedDirPath}/file-${Math.random()}.txt` - const content = Uint8Array.from([0, 1, 2, 3, 4]) - - await ipfs.files.write(filePath, content, { - create: true - }) - - const bytes = uint8ArrayConcat(await all(ipfs.files.read(filePath))) - - expect(bytes).to.deep.equal(content) - }) - }) - */ -}) diff --git a/packages/unixfs/test/chmod.spec.ts b/packages/unixfs/test/chmod.spec.ts deleted file mode 100644 index b787ff5e..00000000 --- a/packages/unixfs/test/chmod.spec.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import type { Blockstore } from 'interface-blockstore' -import { MemoryBlockstore } from 'blockstore-core' -import { UnixFS, unixfs } from '../src/index.js' -import type { CID } from 'multiformats/cid' - -const smallFile = Uint8Array.from(new Array(13).fill(0).map(() => Math.random() * 100)) - -describe('chmod', () => { - let blockstore: Blockstore - let fs: UnixFS - let emptyDirCid: CID - - beforeEach(async () => { - blockstore = new MemoryBlockstore() - - fs = unixfs({ blockstore }) - emptyDirCid = await fs.add({ path: 'empty' }) - }) - - it('should update the mode for a raw node', async () => { - const cid = await fs.add(smallFile) - const originalMode = (await fs.stat(cid)).mode - const updatedCid = await fs.chmod(cid, 0o777) - - const updatedMode = (await fs.stat(updatedCid)).mode - expect(updatedMode).to.not.equal(originalMode) - expect(updatedMode).to.equal(0o777) - }) - - it('should update the mode for a file', async () => { - const cid = await fs.add(smallFile, { - rawLeaves: false - }) - const originalMode = (await fs.stat(cid)).mode - const updatedCid = await fs.chmod(cid, 0o777) - - const updatedMode = (await fs.stat(updatedCid)).mode - expect(updatedMode).to.not.equal(originalMode) - expect(updatedMode).to.equal(0o777) - }) - - it('should update the mode for a directory', async () => { - const path = `foo-${Math.random()}` - - const dirCid = await fs.mkdir(emptyDirCid, path) - const originalMode = (await fs.stat(dirCid, { - path - })).mode - const updatedCid = await fs.chmod(dirCid, 0o777, { - path - }) - - const updatedMode = (await fs.stat(updatedCid, { - path - })).mode - expect(updatedMode).to.not.equal(originalMode) - expect(updatedMode).to.equal(0o777) - }) - - it('should update mode recursively', async () => { - const path = 'path' - const cid = await fs.add(smallFile) - const dirCid = await fs.cp(cid, emptyDirCid, path) - const originalMode = (await fs.stat(dirCid, { - path - })).mode - const updatedCid = await fs.chmod(dirCid, 0o777, { - recursive: true - }) - - const updatedMode = (await fs.stat(updatedCid, { - path - })).mode - expect(updatedMode).to.not.equal(originalMode) - expect(updatedMode).to.equal(0o777) - }) - -/* - it('should update the mode for a hamt-sharded-directory', async () => { - const path = `/foo-${Math.random()}` - - await ipfs.files.mkdir(path) - await ipfs.files.write(`${path}/foo.txt`, uint8ArrayFromString('Hello world'), { - create: true, - shardSplitThreshold: 0 - }) - const originalMode = (await ipfs.files.stat(path)).mode - await ipfs.files.chmod(path, '0777', { - flush: true - }) - - const updatedMode = (await ipfs.files.stat(path)).mode - expect(updatedMode).to.not.equal(originalMode) - expect(updatedMode).to.equal(parseInt('0777', 8)) - }) - */ -}) diff --git a/packages/unixfs/test/cp.spec.ts b/packages/unixfs/test/cp.spec.ts deleted file mode 100644 index b8f42d75..00000000 --- a/packages/unixfs/test/cp.spec.ts +++ /dev/null @@ -1,201 +0,0 @@ -/* eslint-env mocha */ - -import { fromString as uint8ArrayFromString } from 'uint8arrays/from-string' -import { expect } from 'aegir/chai' -import { identity } from 'multiformats/hashes/identity' -import { CID } from 'multiformats/cid' -import type { Blockstore } from 'interface-blockstore' -import { unixfs, UnixFS } from '../src/index.js' -import { MemoryBlockstore } from 'blockstore-core' -import toBuffer from 'it-to-buffer' - -describe('cp', () => { - let blockstore: Blockstore - let fs: UnixFS - let emptyDirCid: CID - - beforeEach(async () => { - blockstore = new MemoryBlockstore() - - fs = unixfs({ blockstore }) - emptyDirCid = await fs.add({ path: 'empty' }) - }) - - it('refuses to copy files without a source', async () => { - // @ts-expect-error invalid args - await expect(fs.cp()).to.eventually.be.rejected.with('Please supply at least one source') - }) - - it('refuses to copy files without a source, even with options', async () => { - // @ts-expect-error invalid args - await expect(fs.cp({})).to.eventually.be.rejected.with('Please supply at least one source') - }) - - it('refuses to copy files without a destination', async () => { - // @ts-expect-error invalid args - await expect(fs.cp('/source')).to.eventually.be.rejected.with('Please supply at least one source') - }) - - it('refuses to copy files without a destination, even with options', async () => { - // @ts-expect-error invalid args - await expect(fs.cp('/source', {})).to.eventually.be.rejected.with('Please supply at least one source') - }) - - it('refuses to copy files to an unreadable node', async () => { - const hash = identity.digest(uint8ArrayFromString('derp')) - const source = await fs.add(Uint8Array.from([0, 1, 3, 4])) - const target = CID.createV1(identity.code, hash) - - await expect(fs.cp(source, target, 'foo')).to.eventually.be.rejected - .with.property('code', 'ERR_NOT_DIRECTORY') - }) - - it('refuses to copy files from an unreadable node', async () => { - const hash = identity.digest(uint8ArrayFromString('derp')) - const source = CID.createV1(identity.code, hash) - - await expect(fs.cp(source, emptyDirCid, 'foo')).to.eventually.be.rejected - .with.property('code', 'ERR_NOT_UNIXFS') - }) - - it('refuses to copy files to an existing file', async () => { - const path = 'path' - const source = await fs.add(Uint8Array.from([0, 1, 3, 4])) - const target = await fs.cp(source, emptyDirCid, path) - - await expect(fs.cp(source, target, path)).to.eventually.be.rejected - .with.property('code', 'ERR_ALREADY_EXISTS') - }) - - it('copies a file to new location', async () => { - const data = Uint8Array.from([0, 1, 3, 4]) - const path = 'path' - const source = await fs.add(data) - const dirCid = await fs.cp(source, emptyDirCid, path) - - const bytes = await toBuffer(fs.cat(dirCid, { - path - })) - - expect(bytes).to.deep.equal(data) - }) - - it('copies directories', async () => { - const path = 'path' - const dirCid = await fs.cp(emptyDirCid, emptyDirCid, path) - - await expect(fs.stat(dirCid, { - path - })).to.eventually.include({ - type: 'directory' - }) - }) - -/* - describe('with sharding', () => { - it('copies a sharded directory to a normal directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - - const normalDir = `dir-${Math.random()}` - const normalDirPath = `/${normalDir}` - - await ipfs.files.mkdir(normalDirPath) - - await ipfs.files.cp(shardedDirPath, normalDirPath) - - const finalShardedDirPath = `${normalDirPath}${shardedDirPath}` - - // should still be a sharded directory - await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory') - - const files = await all(ipfs.files.ls(finalShardedDirPath)) - - expect(files.length).to.be.ok() - }) - - it('copies a normal directory to a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - - const normalDir = `dir-${Math.random()}` - const normalDirPath = `/${normalDir}` - - await ipfs.files.mkdir(normalDirPath) - - await ipfs.files.cp(normalDirPath, shardedDirPath) - - const finalDirPath = `${shardedDirPath}${normalDirPath}` - - // should still be a sharded directory - await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory') - expect((await ipfs.files.stat(finalDirPath)).type).to.equal('directory') - }) - - it('copies a file from a normal directory to a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - - const file = `file-${Math.random()}.txt` - const filePath = `/${file}` - const finalFilePath = `${shardedDirPath}/${file}` - - await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), { - create: true - }) - - await ipfs.files.cp(filePath, finalFilePath) - - // should still be a sharded directory - await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory') - expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file') - }) - - it('copies a file from a sharded directory to a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - const othershardedDirPath = await createShardedDirectory(ipfs) - - const file = `file-${Math.random()}.txt` - const filePath = `${shardedDirPath}/${file}` - const finalFilePath = `${othershardedDirPath}/${file}` - - await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), { - create: true - }) - - await ipfs.files.cp(filePath, finalFilePath) - - // should still be a sharded directory - await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory') - await expect(isShardAtPath(othershardedDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(othershardedDirPath)).type).to.equal('directory') - expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file') - }) - - it('copies a file from a sharded directory to a normal directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - const dir = `dir-${Math.random()}` - const dirPath = `/${dir}` - - const file = `file-${Math.random()}.txt` - const filePath = `${shardedDirPath}/${file}` - const finalFilePath = `${dirPath}/${file}` - - await ipfs.files.write(filePath, Uint8Array.from([0, 1, 2, 3]), { - create: true - }) - - await ipfs.files.mkdir(dirPath) - - await ipfs.files.cp(filePath, finalFilePath) - - // should still be a sharded directory - await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(shardedDirPath)).type).to.equal('directory') - expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') - expect((await ipfs.files.stat(finalFilePath)).type).to.equal('file') - }) - }) - */ -}) diff --git a/packages/unixfs/test/ls.spec.ts b/packages/unixfs/test/ls.spec.ts deleted file mode 100644 index a8fb1160..00000000 --- a/packages/unixfs/test/ls.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import type { CID } from 'multiformats/cid' -import all from 'it-all' -import type { Blockstore } from 'interface-blockstore' -import { unixfs, UnixFS } from '../src/index.js' -import { MemoryBlockstore } from 'blockstore-core' - -describe('ls', () => { - let blockstore: Blockstore - let fs: UnixFS - let emptyDirCid: CID - - beforeEach(async () => { - blockstore = new MemoryBlockstore() - - fs = unixfs({ blockstore }) - emptyDirCid = await fs.add({ path: 'empty' }) - }) - - it('should require a path', async () => { - // @ts-expect-error invalid args - await expect(all(fs.ls())).to.eventually.be.rejected() - }) - - it('lists files in a directory', async () => { - const path = 'path' - const data = Uint8Array.from([0, 1, 2, 3]) - const fileCid = await fs.add(data) - const dirCid = await fs.cp(fileCid, emptyDirCid, path) - const files = await all(fs.ls(dirCid)) - - expect(files).to.have.lengthOf(1).and.to.containSubset([{ - cid: fileCid, - name: path, - size: data.byteLength, - type: 'raw' - }]) - }) - - it('lists a file', async () => { - const path = 'path' - const data = Uint8Array.from([0, 1, 2, 3]) - const fileCid = await fs.add(data, { - rawLeaves: false - }) - const dirCid = await fs.cp(fileCid, emptyDirCid, path) - const files = await all(fs.ls(dirCid, { - path - })) - - expect(files).to.have.lengthOf(1).and.to.containSubset([{ - cid: fileCid, - size: data.byteLength, - type: 'file' - }]) - }) - - it('lists a raw node', async () => { - const path = 'path' - const data = Uint8Array.from([0, 1, 2, 3]) - const fileCid = await fs.add(data) - const dirCid = await fs.cp(fileCid, emptyDirCid, path) - const files = await all(fs.ls(dirCid, { - path - })) - - expect(files).to.have.lengthOf(1).and.to.containSubset([{ - cid: fileCid, - size: data.byteLength, - type: 'raw' - }]) - }) - - /* - describe('with sharding', () => { - it('lists a sharded directory contents', async () => { - const fileCount = 1001 - const dirPath = await createShardedDirectory(ipfs, fileCount) - const files = await all(ipfs.files.ls(dirPath)) - - expect(files.length).to.equal(fileCount) - - files.forEach(file => { - // should be a file - expect(file.type).to.equal('file') - }) - }) - - it('lists a file inside a sharded directory directly', async () => { - const dirPath = await createShardedDirectory(ipfs) - const files = await all(ipfs.files.ls(dirPath)) - const filePath = `${dirPath}/${files[0].name}` - - // should be able to ls new file directly - const file = await all(ipfs.files.ls(filePath)) - - expect(file).to.have.lengthOf(1).and.to.containSubset([files[0]]) - }) - - it('lists the contents of a directory inside a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - const dirPath = `${shardedDirPath}/subdir-${Math.random()}` - const fileName = `small-file-${Math.random()}.txt` - - await ipfs.files.mkdir(`${dirPath}`) - await ipfs.files.write(`${dirPath}/${fileName}`, Uint8Array.from([0, 1, 2, 3]), { - create: true - }) - - const files = await all(ipfs.files.ls(dirPath)) - - expect(files.length).to.equal(1) - expect(files.filter(file => file.name === fileName)).to.be.ok() - }) - }) - */ -}) diff --git a/packages/unixfs/test/mkdir.spec.ts b/packages/unixfs/test/mkdir.spec.ts deleted file mode 100644 index 843bc0fb..00000000 --- a/packages/unixfs/test/mkdir.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import all from 'it-all' -import type { Blockstore } from 'interface-blockstore' -import { unixfs, UnixFS } from '../src/index.js' -import { MemoryBlockstore } from 'blockstore-core' -import type { CID } from 'multiformats/cid' -import type { Mtime } from 'ipfs-unixfs' - -describe('mkdir', () => { - let blockstore: Blockstore - let fs: UnixFS - let emptyDirCid: CID - let emptyDirCidV0: CID - - beforeEach(async () => { - blockstore = new MemoryBlockstore() - - fs = unixfs({ blockstore }) - - emptyDirCid = await fs.add({ path: 'empty' }) - emptyDirCidV0 = await fs.add({ path: 'empty' }, { - cidVersion: 0 - }) - }) - - async function testMode (mode: number | undefined, expectedMode: number): Promise { - const path = 'sub-directory' - const dirCid = await fs.mkdir(emptyDirCid, path, { - mode - }) - - await expect(fs.stat(dirCid, { - path - })).to.eventually.have.property('mode', expectedMode) - } - - async function testMtime (mtime: Mtime, expectedMtime: Mtime): Promise { - const path = 'sub-directory' - const dirCid = await fs.mkdir(emptyDirCid, path, { - mtime - }) - - await expect(fs.stat(dirCid, { - path - })).to.eventually.have.deep.property('mtime', expectedMtime) - } - - it('requires a directory', async () => { - // @ts-expect-error not enough arguments - await expect(fs.mkdir(emptyDirCid)).to.eventually.be.rejected() - }) - - it('creates a directory', async () => { - const path = 'foo' - const dirCid = await fs.mkdir(emptyDirCid, path) - - const stats = await fs.stat(dirCid) - expect(stats.type).to.equal('directory') - - const files = await all(fs.ls(dirCid)) - - expect(files.length).to.equal(1) - expect(files).to.have.nested.property('[0].name', path) - }) - - it('refuses to create a directory that already exists', async () => { - const path = 'qux' - const dirCid = await fs.mkdir(emptyDirCid, path) - - await expect(fs.mkdir(dirCid, path)).to.eventually.be.rejected() - .with.property('code', 'ERR_ALREADY_EXISTS') - }) - - it('creates a nested directory with a different CID version to the parent', async () => { - const subDirectory = 'sub-dir' - - expect(emptyDirCidV0).to.have.property('version', 0) - - const dirCid = await fs.mkdir(emptyDirCidV0, subDirectory, { - cidVersion: 1 - }) - - await expect(fs.stat(dirCid)).to.eventually.have.nested.property('cid.version', 0) - await expect(fs.stat(dirCid, { - path: subDirectory - })).to.eventually.have.nested.property('cid.version', 1) - }) - - it('should make directory and have default mode', async function () { - await testMode(undefined, parseInt('0755', 8)) - }) - - it('should make directory and specify mtime as { nsecs, secs }', async function () { - const mtime = { - secs: 5, - nsecs: 0 - } - await testMtime(mtime, mtime) - }) -/* - describe('with sharding', () => { - it('makes a directory inside a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - const dirPath = `${shardedDirPath}/subdir-${Math.random()}` - - await ipfs.files.mkdir(`${dirPath}`) - - await expect(isShardAtPath(shardedDirPath, ipfs)).to.eventually.be.true() - await expect(ipfs.files.stat(shardedDirPath)).to.eventually.have.property('type', 'directory') - - await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.false() - await expect(ipfs.files.stat(dirPath)).to.eventually.have.property('type', 'directory') - }) - }) - */ -}) diff --git a/packages/unixfs/test/rm.spec.ts b/packages/unixfs/test/rm.spec.ts deleted file mode 100644 index fdd3d246..00000000 --- a/packages/unixfs/test/rm.spec.ts +++ /dev/null @@ -1,177 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import type { Blockstore } from 'interface-blockstore' -import { unixfs, UnixFS } from '../src/index.js' -import { MemoryBlockstore } from 'blockstore-core' -import type { CID } from 'multiformats/cid' - -const smallFile = Uint8Array.from(new Array(13).fill(0).map(() => Math.random() * 100)) - -describe('rm', () => { - let blockstore: Blockstore - let fs: UnixFS - let emptyDirCid: CID - - beforeEach(async () => { - blockstore = new MemoryBlockstore() - - fs = unixfs({ blockstore }) - - emptyDirCid = await fs.add({ path: 'empty' }) - }) - - it('refuses to remove files without arguments', async () => { - // @ts-expect-error invalid args - await expect(fs.rm()).to.eventually.be.rejected() - }) - - it('removes a file', async () => { - const path = 'foo' - const fileCid = await fs.add(smallFile) - const dirCid = await fs.cp(fileCid, emptyDirCid, path) - const updatedDirCid = await fs.rm(dirCid, path) - - await expect(fs.stat(updatedDirCid, { - path - })).to.eventually.be.rejected - .with.property('code', 'ERR_DOES_NOT_EXIST') - }) - - it('removes a directory', async () => { - const path = 'foo' - const dirCid = await fs.cp(emptyDirCid, emptyDirCid, path) - const updatedDirCid = await fs.rm(dirCid, path) - - await expect(fs.stat(updatedDirCid, { - path - })).to.eventually.be.rejected - .with.property('code', 'ERR_DOES_NOT_EXIST') - }) -/* - describe('with sharding', () => { - it('recursively removes a sharded directory inside a normal directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - const dir = `dir-${Math.random()}` - const dirPath = `/${dir}` - - await ipfs.files.mkdir(dirPath) - - await ipfs.files.mv(shardedDirPath, dirPath) - - const finalShardedDirPath = `${dirPath}${shardedDirPath}` - - await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory') - - await ipfs.files.rm(dirPath, { - recursive: true - }) - - await expect(ipfs.files.stat(dirPath)).to.eventually.be.rejectedWith(/does not exist/) - await expect(ipfs.files.stat(shardedDirPath)).to.eventually.be.rejectedWith(/does not exist/) - }) - - it('recursively removes a sharded directory inside a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - const otherDirPath = await createShardedDirectory(ipfs) - - await ipfs.files.mv(shardedDirPath, otherDirPath) - - const finalShardedDirPath = `${otherDirPath}${shardedDirPath}` - - await expect(isShardAtPath(finalShardedDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(finalShardedDirPath)).type).to.equal('directory') - await expect(isShardAtPath(otherDirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(otherDirPath)).type).to.equal('directory') - - await ipfs.files.rm(otherDirPath, { - recursive: true - }) - - await expect(ipfs.files.stat(otherDirPath)).to.eventually.be.rejectedWith(/does not exist/) - await expect(ipfs.files.stat(finalShardedDirPath)).to.eventually.be.rejectedWith(/does not exist/) - }) - }) - - it('results in the same hash as a sharded directory created by the importer when removing a file', async function () { - const { - nextFile, - dirWithAllFiles, - dirWithSomeFiles, - dirPath - } = await createTwoShards(ipfs, 1001) - - await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath) - - await ipfs.files.rm(nextFile.path) - - const stats = await ipfs.files.stat(dirPath) - const updatedDirCid = stats.cid - - await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') - expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString()) - }) - - it('results in the same hash as a sharded directory created by the importer when removing a subshard', async function () { - const { - nextFile, - dirWithAllFiles, - dirWithSomeFiles, - dirPath - } = await createTwoShards(ipfs, 31) - - await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath) - - await ipfs.files.rm(nextFile.path) - - const stats = await ipfs.files.stat(dirPath) - const updatedDirCid = stats.cid - - await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') - expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString()) - }) - - it('results in the same hash as a sharded directory created by the importer when removing a file from a subshard of a subshard', async function () { - const { - nextFile, - dirWithAllFiles, - dirWithSomeFiles, - dirPath - } = await createTwoShards(ipfs, 2187) - - await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath) - - await ipfs.files.rm(nextFile.path) - - const stats = await ipfs.files.stat(dirPath) - const updatedDirCid = stats.cid - - await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') - expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString()) - }) - - it('results in the same hash as a sharded directory created by the importer when removing a subshard of a subshard', async function () { - const { - nextFile, - dirWithAllFiles, - dirWithSomeFiles, - dirPath - } = await createTwoShards(ipfs, 139) - - await ipfs.files.cp(`/ipfs/${dirWithAllFiles}`, dirPath) - - await ipfs.files.rm(nextFile.path) - - const stats = await ipfs.files.stat(dirPath) - const updatedDirCid = stats.cid - - await expect(isShardAtPath(dirPath, ipfs)).to.eventually.be.true() - expect((await ipfs.files.stat(dirPath)).type).to.equal('directory') - expect(updatedDirCid.toString()).to.deep.equal(dirWithSomeFiles.toString()) - }) - */ -}) diff --git a/packages/unixfs/test/stat.spec.ts b/packages/unixfs/test/stat.spec.ts deleted file mode 100644 index 69892f1e..00000000 --- a/packages/unixfs/test/stat.spec.ts +++ /dev/null @@ -1,236 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import type { Blockstore } from 'interface-blockstore' -import { unixfs, UnixFS } from '../src/index.js' -import { MemoryBlockstore } from 'blockstore-core' -import type { CID } from 'multiformats/cid' -import * as dagPb from '@ipld/dag-pb' - -const smallFile = Uint8Array.from(new Array(13).fill(0).map(() => Math.random() * 100)) -const largeFile = Uint8Array.from(new Array(490668).fill(0).map(() => Math.random() * 100)) - -describe('stat', function () { - this.timeout(120 * 1000) - - let blockstore: Blockstore - let fs: UnixFS - let emptyDirCid: CID - - beforeEach(async () => { - blockstore = new MemoryBlockstore() - - fs = unixfs({ blockstore }) - - emptyDirCid = await fs.add({ path: 'empty' }) - }) - - it('stats an empty directory', async () => { - await expect(fs.stat(emptyDirCid)).to.eventually.include({ - fileSize: 0, - dagSize: 2, - blocks: 1, - type: 'directory' - }) - }) - - it('computes how much of the DAG is local', async () => { - const largeFileCid = await fs.add({ content: largeFile }) - const block = await blockstore.get(largeFileCid) - const node = dagPb.decode(block) - - expect(node.Links).to.have.lengthOf(2) - - await expect(fs.stat(largeFileCid)).to.eventually.include({ - fileSize: 490668, - blocks: 3, - localDagSize: 490776 - }) - - // remove one of the blocks so we now have an incomplete DAG - await blockstore.delete(node.Links[0].Hash) - - // block count and local file/dag sizes should be smaller - await expect(fs.stat(largeFileCid)).to.eventually.include({ - fileSize: 490668, - blocks: 2, - localFileSize: 228524, - localDagSize: 228632 - }) - }) - - it('stats a raw node', async () => { - const fileCid = await fs.add({ content: smallFile }) - - await expect(fs.stat(fileCid)).to.eventually.include({ - fileSize: smallFile.length, - dagSize: 13, - blocks: 1, - type: 'raw' - }) - }) - - it('stats a small file', async () => { - const fileCid = await fs.add({ content: smallFile }, { - cidVersion: 0, - rawLeaves: false - }) - - await expect(fs.stat(fileCid)).to.eventually.include({ - fileSize: smallFile.length, - dagSize: 19, - blocks: 1, - type: 'file' - }) - }) - - it('stats a large file', async () => { - const cid = await fs.add({ content: largeFile }) - - await expect(fs.stat(cid)).to.eventually.include({ - fileSize: largeFile.length, - dagSize: 490682, - blocks: 3, - type: 'file' - }) - }) - - it('should stat file with mode', async () => { - const mode = 0o644 - const cid = await fs.add({ - content: smallFile, - mode - }) - - await expect(fs.stat(cid)).to.eventually.include({ - mode - }) - }) - - it('should stat file with mtime', async function () { - const mtime = { - secs: 5, - nsecs: 0 - } - const cid = await fs.add({ - content: smallFile, - mtime - }) - - await expect(fs.stat(cid)).to.eventually.deep.include({ - mtime - }) - }) - - it('should stat a directory', async function () { - await expect(fs.stat(emptyDirCid)).to.eventually.include({ - type: 'directory', - blocks: 1, - fileSize: 0 - }) - }) - - it('should stat dir with mode', async function () { - const mode = 0o755 - const path = 'test-dir' - const dirCid = await fs.mkdir(emptyDirCid, path, { - mode - }) - - await expect(fs.stat(dirCid, { - path - })).to.eventually.include({ - mode - }) - }) - - it('should stat dir with mtime', async function () { - const mtime = { - secs: 5, - nsecs: 0 - } - - const path = 'test-dir' - const dirCid = await fs.mkdir(emptyDirCid, path, { - mtime - }) - - await expect(fs.stat(dirCid, { - path - })).to.eventually.deep.include({ - mtime - }) - }) -/* - it('should stat sharded dir with mode', async function () { - const testDir = `/test-${nanoid()}` - - await ipfs.files.mkdir(testDir, { parents: true }) - await ipfs.files.write(`${testDir}/a`, uint8ArrayFromString('Hello, world!'), { - create: true, - shardSplitThreshold: 0 - }) - - const stat = await ipfs.files.stat(testDir) - - await expect(isShardAtPath(testDir, ipfs)).to.eventually.be.true() - expect(stat).to.have.property('type', 'directory') - expect(stat).to.include({ - mode: 0o755 - }) - }) - - it('should stat sharded dir with mtime', async function () { - const testDir = `/test-${nanoid()}` - - await ipfs.files.mkdir(testDir, { - parents: true, - mtime: { - secs: 5, - nsecs: 0 - } - }) - await ipfs.files.write(`${testDir}/a`, uint8ArrayFromString('Hello, world!'), { - create: true, - shardSplitThreshold: 0 - }) - - const stat = await ipfs.files.stat(testDir) - - await expect(isShardAtPath(testDir, ipfs)).to.eventually.be.true() - expect(stat).to.have.property('type', 'directory') - expect(stat).to.deep.include({ - mtime: { - secs: 5, - nsecs: 0 - } - }) - }) - - describe('with sharding', () => { - it('stats a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - - const stats = await ipfs.files.stat(`${shardedDirPath}`) - - expect(stats.type).to.equal('directory') - expect(stats.size).to.equal(0) - }) - - it('stats a file inside a sharded directory', async () => { - const shardedDirPath = await createShardedDirectory(ipfs) - const files = [] - - for await (const file of ipfs.files.ls(`${shardedDirPath}`)) { - files.push(file) - } - - const stats = await ipfs.files.stat(`${shardedDirPath}/${files[0].name}`) - - expect(stats.type).to.equal('file') - expect(stats.size).to.equal(7) - }) - }) - - */ -}) diff --git a/packages/unixfs/test/touch.spec.ts b/packages/unixfs/test/touch.spec.ts deleted file mode 100644 index df53eb5e..00000000 --- a/packages/unixfs/test/touch.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* eslint-env mocha */ - -import { expect } from 'aegir/chai' -import type { Blockstore } from 'interface-blockstore' -import { unixfs, UnixFS } from '../src/index.js' -import { MemoryBlockstore } from 'blockstore-core' -import type { CID } from 'multiformats/cid' -import delay from 'delay' - -describe('.files.touch', () => { - let blockstore: Blockstore - let fs: UnixFS - let emptyDirCid: CID - - beforeEach(async () => { - blockstore = new MemoryBlockstore() - - fs = unixfs({ blockstore }) - - emptyDirCid = await fs.add({ path: 'empty' }) - }) - - it('should have default mtime', async () => { - const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) - - await expect(fs.stat(cid)).to.eventually.have.property('mtime') - .that.is.undefined() - - const updatedCid = await fs.touch(cid) - - await expect(fs.stat(updatedCid)).to.eventually.have.property('mtime') - .that.is.not.undefined().and.does.not.deep.equal({ - secs: 0, - nsecs: 0 - }) - }) - - it('should update file mtime', async function () { - this.slow(5 * 1000) - const mtime = new Date() - const seconds = Math.floor(mtime.getTime() / 1000) - - const cid = await fs.add({ - content: Uint8Array.from([0, 1, 2, 3, 4]), - mtime: { - secs: seconds - } - }) - - await delay(2000) - const updatedCid = await fs.touch(cid) - - await expect(fs.stat(updatedCid)).to.eventually.have.nested.property('mtime.secs') - .that.is.greaterThan(seconds) - }) - - it('should update directory mtime', async function () { - this.slow(5 * 1000) - const path = 'path' - const mtime = new Date() - const seconds = Math.floor(mtime.getTime() / 1000) - - const cid = await fs.mkdir(emptyDirCid, path, { - mtime: { - secs: seconds - } - }) - await delay(2000) - const updateCid = await fs.touch(cid) - - await expect(fs.stat(updateCid)).to.eventually.have.nested.property('mtime.secs') - .that.is.greaterThan(seconds) - }) - - it('should update mtime recursively', async function () { - this.slow(5 * 1000) - const path = 'path' - const mtime = new Date() - const seconds = Math.floor(mtime.getTime() / 1000) - - const cid = await fs.add(Uint8Array.from([0, 1, 2, 3, 4])) - const dirCid = await fs.cp(cid, emptyDirCid, path) - - await delay(2000) - - const updatedCid = await fs.touch(dirCid, { - recursive: true - }) - - await expect(fs.stat(updatedCid)).to.eventually.have.nested.property('mtime.secs') - .that.is.greaterThan(seconds) - - await expect(fs.stat(updatedCid, { - path - })).to.eventually.have.nested.property('mtime.secs') - .that.is.greaterThan(seconds) - }) -/* - it('should update the mtime for a hamt-sharded-directory', async () => { - const path = `/foo-${Math.random()}` - - await ipfs.files.mkdir(path, { - mtime: new Date() - }) - await ipfs.files.write(`${path}/foo.txt`, uint8ArrayFromString('Hello world'), { - create: true, - shardSplitThreshold: 0 - }) - const originalMtime = (await ipfs.files.stat(path)).mtime - - if (!originalMtime) { - throw new Error('No originalMtime found') - } - - await delay(1000) - await ipfs.files.touch(path, { - flush: true - }) - - const updatedMtime = (await ipfs.files.stat(path)).mtime - - if (!updatedMtime) { - throw new Error('No updatedMtime found') - } - - expect(updatedMtime.secs).to.be.greaterThan(originalMtime.secs) - }) -*/ -}) diff --git a/packages/unixfs/tsconfig.json b/packages/unixfs/tsconfig.json deleted file mode 100644 index 4c0bdf77..00000000 --- a/packages/unixfs/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "aegir/src/config/tsconfig.aegir.json", - "compilerOptions": { - "outDir": "dist" - }, - "include": [ - "src", - "test" - ], - "references": [ - { - "path": "../interface" - } - ] -} From 8bfb5ea737aebf71c04bd089c57a13b954132cfb Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 3 Feb 2023 00:12:17 +0100 Subject: [PATCH 14/18] chore: add comments --- packages/interface/src/index.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/interface/src/index.ts b/packages/interface/src/index.ts index 2417005e..b914e860 100644 --- a/packages/interface/src/index.ts +++ b/packages/interface/src/index.ts @@ -63,13 +63,36 @@ export interface Helia { } export interface InfoOptions extends AbortOptions { + /** + * If passed, return information about this PeerId, defaults + * to the ID of the current node. + */ peerId?: PeerId } export interface InfoResponse { + /** + * The ID of the peer this info is about + */ peerId: PeerId + + /** + * The multiaddrs the peer is listening on + */ multiaddrs: Multiaddr[] + + /** + * The peer's reported agent version + */ agentVersion: string + + /** + * The peer's reported protocol version + */ protocolVersion: string + + /** + * The protocols the peer supports + */ protocols: string[] } From eecdff1bc382aefd35ae6c0e9fbbcf5522a6edc3 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 3 Feb 2023 00:14:27 +0100 Subject: [PATCH 15/18] chore: fix electron tests --- packages/interop/test/fixtures/create-kubo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/interop/test/fixtures/create-kubo.ts b/packages/interop/test/fixtures/create-kubo.ts index 29c01744..1c160b10 100644 --- a/packages/interop/test/fixtures/create-kubo.ts +++ b/packages/interop/test/fixtures/create-kubo.ts @@ -3,12 +3,12 @@ import * as goIpfs from 'go-ipfs' import { Controller, createController } from 'ipfsd-ctl' import * as kuboRpcClient from 'kubo-rpc-client' -import { isNode } from 'wherearewe' +import { isElectronMain, isNode } from 'wherearewe' export async function createKuboNode (): Promise { const controller = await createController({ kuboRpcModule: kuboRpcClient, - ipfsBin: isNode ? goIpfs.path() : undefined, + ipfsBin: isNode || isElectronMain ? goIpfs.path() : undefined, test: true, endpoint: process.env.IPFSD_SERVER, ipfsOptions: { From 020e7d22e28f9f2915239234655763017b529fe8 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Fri, 3 Feb 2023 08:28:04 +0100 Subject: [PATCH 16/18] chore: add logo --- README.md | 2 ++ assets/helia.idraw | Bin 0 -> 127187 bytes assets/helia.png | Bin 0 -> 107566 bytes 3 files changed, 2 insertions(+) create mode 100644 assets/helia.idraw create mode 100644 assets/helia.png diff --git a/README.md b/README.md index cc1cc0d3..c056e41d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +helia logo + # helia [![ipfs.tech](https://img.shields.io/badge/project-IPFS-blue.svg?style=flat-square)](https://ipfs.tech) diff --git a/assets/helia.idraw b/assets/helia.idraw new file mode 100644 index 0000000000000000000000000000000000000000..0afaddbfc62dd537df29ccd6bdd57f8d2ea4ec2a GIT binary patch literal 127187 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 literal 0 HcmV?d00001 diff --git a/assets/helia.png b/assets/helia.png new file mode 100644 index 0000000000000000000000000000000000000000..bfde8c545b33d2b6681d012ba6d4e5cf915527c1 GIT binary patch literal 107566 zcmdRWgf$u)}z(v@5ueIkKbIdWv@}-8FA^|QnE(!_?fwGdk777abk3ZO$;5&9# zFR;KTR7WjES(Jiq+7<9WZjbLPTim^i!U?{{M!`U(M!AIi5%5P6mFD-?w^6U5p#S(j z8VX8~H44T*KcfadBmYwZe~`cP^D}xX>OVgXzL$#j>!;DRQqlkY8u@DtP;xTx3CCVZ z-w_1`4}6VBhKlko@d^rxG>WpkjE)=XdJ;}W?6Mh%q@ik|mu3H|Q{8ZNMPVlSlPosLjex27Ek3Nrua7EPqruCr~jSQlnA zW8Kw13xDcBwDJG=DPpClD3?gsZLNFgS?iK*we9Hcn*ZHz#sZu~j?4r$j%uiu+C_Lx zB>Vroleuu|OB{Xn$|7`(fgW@_t^C*iyQI`)X&;V8i6|FkTxWao)qOY2zHWxcvZETe|L4UY$)>)a=Tp>zfuHfA?+7mWpA7^tr#s)R z&ZgigO}4i5zU!LU|Lo5*)znDCK*V zV8AOMqF#L{_dmNRhkYw*+Cl|)O#13Igdf3`|B0OjS}yz-%D5eVY|9>g6mS0f_oR{1 zk{pie%3^Gnythbioi*UEH$?s?A0ins#*{=ZUX#O6UkQ3Q5MTdqOgN~{=x~fbQXa#+ zE;CH2+8Fnr3lJ+}sNB&vj)a+L^d;K2Dd6-u?I5-XvYlY=BLbl?M z8e%4SD(DYqeI7r45%!R4oF0H6h-|P`TXS2F0pL}vBJR~ z&uCQGn~rCzGgP~7PHD6$i&W7cGy94wQqhc%3I;;{`-&w=m7;36<6&A+c~Ds`tteOe zfp-E=pWR}7wZ|-uwWK~<*>S(Itccg-3m^+9N(mc!Ap&Y9yt7xkj|4Bzi zJhc3U-Fh}_ydy{uA>I1ULMIFLRz;orXvyQ`WZdI4wN%^xLJA?X{i{=P&8R9=oyjU^ z_&~0C0cE<5AK3SE4}k2XH4gyzjFvjfiXVjz(P5JEf;s*aj6?p?Yw4DZacYZ z=4E~eAGgFJqPGbl=a_S7yf~susV0s=Uy!Mc8AWr(a>jOk`S-OP)KlNa*w?W_pw*Dk zl1!rt`}ZN+q47&c+utFdo(%FW`1fqP>iIu=?}ZEZuNqJ#{d>#iqNJTU2KAx_F0BbX z9fD1JA0~a^4NVfVONM|j46m3GHQZFuP>a!fpQKT)(f0RV5L05jQsJPFhp9*9q13u< z&z1RJoC^vF41TG2yu0|dOs~*bP_Mv{$)Rz-g6I&egL2{aaqK9*Grlu{bNJuu;KX4K z8A@W!kIvv^R999mP7-r1qY<>R=T$4VwzhU@2_`9f^d*YKq4BI8>$s9QhJm(0?k$1l zlE$G1*5C1+`a+tl2Lv@i*i25%LvEKFGSWe zjWCV-Z`5b(#o&D9lFpioSM@yK3-n_;a>hT-l-ztt%GJFDcC}1 zd;|`qIAxEk^TV4JPzGVnQi)a0)$4!1Uo+VmC-+q+F~>z!cJynHUhS)7 zr}%ggy!n)4NBD(829?}r-pe5~PS49qAh&rigp&a2F_C)s7GJ4vmT z1RzxzIu;@95-vIRt$6=Scpe_ICX{G(Gx$>-tJtfzRs~mu|6Yn0i+$ZZOg^d%Wz^Vh zHt-0-y$^sesLNncQ2(!HR%?nYvBGRXIkf^zn_JKB!|s4B0hurn&kd~ zHhlg|v{4wvx?TO5MS>tCFTAs{?D_e%ATHDkQRh{wN6$qgs?`5NKWHR&Ow0!?6%`Pn z#N0+zFuGe-k(2el_0ijnRnBWR5%dx^&z_ZVa_f(hEJ(X+a8sg*HP9E`xhIOT=Q!XE zLj_Cyi@4IpN^>8t-Hj4d)}lCC>`oe=@>t7v>5RL<-HPrqUFYr69?4W@)t8o6F+UJ~ zjj|)?Srzffr2}Zvvpj9-0yzWe1nKd=v9o~$<6S%ZE~_=36C~vFjdifAhAkJy$z)em zi`bLh(nPsuM@Sds-HmY8F+2m)4MI0%#-(ZA|AoJPiZd9;hj_}X{stIl(_Xt_AKpx# z?+=?C_~UVFw0$NrNSnA;&jp=~Ayn(T*K6??AmFH^2FIN6Dtof!QeP+My4NLnJRgn3 z1LedBYbzqjqtvglT=AUTKI0?{SXg@B*i5BHNDlT4T-%P{e@%s_68u$xNuNuX#FVCI zd;-x|G;(FOYOSDjZMe{t&#$Q!n?*4kmZ=h}_4)HBx<+(EL&yxOXHx;L(3UiftbuGo z+MjD`;f0oF*wia&329gjeSCX%b~v4cpBlN6p&Sjf9i$U5FynHxf5ukCv;MKjC++Tg z$X|~!7uw*Ul}rKgU=zy$iQM|c){eG|HUay=>PqKvC+&-O{%Sao8@5EwfygEgE>Ilh1 z0}-te_B!E7Iz2{am&gD*gaASaiTE>?QzIKP({`T$jOokBVDBBh?Y}XK||{9Y7zz8VE_{YnSDLNx)?O3_(9qwCtD3QcC@o-NrBps7I*y zEh=7pfvZcMJ?atuj_+)EYlPIWaX2>BIMv7Mp~IVSs}QT`KY0t(kMtUsM=di4GQY{} z1dP4+brX`4H&_WxK0S%8Jzh)@s2kLHtXLy}X1V3Ul*5q24p2+)^MWsg6ha1x`Eyak z#AxpF5n`+mkX{X42||UnCmYpC1*PsAl}on@N;Mz1HLuYP2|%km_aQk#IZ{DVVl(yW zVQKkkU;hGdGo=}E_XB1Ccq-#n&QGhIR>ndrgGmqS-?t?67*H#nxtd=O;r)vYbIQmXBUgPfg;~Lqb&Gbbf zTyJlh`YY6SBNk;p!`R!~#A>Knkek4ix)`hr@b~^TFD3{$1qh77Bf@MyNHrp1ZaLLrlo$VTI zUYidp3m5zgAF-pQbt^ad!HzYq4t^NtHE*qW^%~w3hz9}iI^fV}b!S+#IlL&FOGZN8 z+sTL!>o^_WjP(~8d#03HWO$eZS}C1NehML%KdFmvKc6hOONp*m6&(%9R!dq3 zXl>ZiIE6`JSGyl~10Xpk_t^TCr{1O}Bcp>aSaVWxvU3WHxt|921gZPk`9=H%vb6H4 z#iysGfL$a*`ZHc8NqCk;qmuI_@jZSe0qV*iSZQbKGQMZ?Mghh?#lGOR1p*nOBkUvG zKQfP19EvMDa~JGY)hi0FG0pVbm9L!PC67DTKMfmy2oJe!fT?Ra!m!^En|hu`kM-$P zu@6rYy)NL7mEkBd*2f(AfL0GsNL=!3Gx?UIwVvhCN67LzPLh}#7CrmJ`o1=PDV^8s2mgPkK>Ij^giC7WlrATIh~JI zw+Y0h`0)ZEqUit$Do6*O4%k5qEy5&i+Y1EfHb zaE*uQ0&KbtNY_EP*d}be+|HFGd#u*;aE#K}t?@FajuHhW#3j_#Thmt~LLd2E_M~)FeuY$Q%Ju- zrfWICGr+Ise*kq!i{$Yl^&;uU89D&@^TF-MmXbZYvobS9UP=ksPFQW$ zo!BNsMWww85fsy!Mu!vbrPjt=)08#veqrYy;a~ViZ3HAUBN$UiX^c@> z_aUeu+@9a=3XR@P5UdOqf;@SWM<;0W=7w=i1u{XDy!q{1b6j(LbAq@r;FxeMxYApK znGZ9iGu3~tkqqtYLar~UDN3VRcN4}c9ha(td1}Sk)st=1MORpn&>1DlPY{4DW>#1?kC0fjNmz!AmyHH7_>3%m ziB?3u_>8*OpTq{#2eb$DDE%QLecioNF{8MW`Gu@U!hc-u=r*JBvns8APO$T=w1ysE z1%^jJyHpWOQybth?Q;S#D6`SmHH3}6Mj*yFG)y$@jOcEK9lUVK-b%GT5BO$jm;UEYR;<2vMd)`Y(F-9aw zRQD$dU?LN~x8Y`_RRzjGuR|N1rD8bk3a22fGoGh>*vK`B^Xiz5l6U&}qDG2lp%Km) z%UC8~b<<%ZRuknPg^tkx?Rfsx2`e158Czdcr0TtApjhh#M%9Te0quvm*~ukt;evt# zdU{6TfwdZin)OCy3^4UTI9468BuU-tKa%$?X@!#HnL$()R2Fmwi9YF)9(z_{(>WhX z(_5=3?L@zSC0VWJYE0mP=><;A(9E*@(X(w`V63*QM}wBjR!t0kYw4+YxA?B=&q%@g zaHe1$#xzzaTdhu-G|T_*ui>bs!eY+Ye!zuQ(b@=CVa=S=aPhcp_1*6U=6UNm`BQA~ z>PDQ2;0+4}@m6iPuC>p5o^L)U`nS(wC5BFD4cSJODQQu-eWy*Nm+%mlz_RR3l_|5I zZ9>)=mi@uOjb8d*ZhN&DyvE9VR>QbGrU68OJb~)}UZzC^>X_l>s|@(1@Mg{KdYVFr zVT%Jm2GC4P%swak0iLTL3(p!YtQf4=3ozg`a6{8GKlMPnKQ6)qCCgMbxuZpyk?7Q~ zd~mW^?@CcU3i4;nu}4A=37;9=h%pljK{-#EY6mu}O{>GV5@ItcjcrZC|Gv^WKWR;l zW)$TU)?8fGSCl-qDc%SA3Gd#mvKo)WtfOlIWY_@KpySYIp}6|t!{ow%v6YaOl$9dL zX?i$=4`a6IrFqe#2SPZsSf$AE95`n#vnkWI~9K289vBS_+8 zy%e4(;%L}nZk(*0t@a5B7g|5FpzeV3<=5VV-a12^Q?ye#R?*gF<0G@9N57v<+7CyE zbaE;-gL@?B!Kb{ly>IG9pC7t>iDEm5y2Gvc>ncmxUwc7vgR5Yi2BwHAa_}52szm(@ zT&T<%8cy!=KRS1On-3F(CQ_E;oOaW7laf2_EXbz)_v#GC@!$k-LU>Q$LmHPOg1|QN z2E}H*X3Kv$m6;Nwa`L21zX}izafO$6S3eY%ffD9IV(E^xUk7+?nPKI#-`7in@W7;C z%|JwAhtr;;`(T{2jy(?9a^D)=n%tWGU4fw2$gCK9PBcOP+azc^F>($xMKv_8N21;c zCxdz)@1Oay_SCnyhzgnRTu>>f$xDS%X0NMW!d@c3>j)z*Mn@R8!U93cTb}Q|(~_!4k0_7crpAiVc+Ygsew!WtHw6hn zdoRBE8rj}UGx?sMA_box?fqQ#WVP$~`Ps=h+9li^Bo6@?QjM2a+pE@R+-GZqeTsid zaH^(3_2@mNmzbBhm&EVKYhXu%u%%>1!7gD=skwu0*pYN8EY=hwSjztVYRn>pLIhY0p$UbaLYOk z$*#04z2WN7Uwgy*l8taqiBDf&y?XJDqK?5E<@aMSVxX}tjLAgxgFHg%0HZdfCU>|w zy~)b`pxibIK9rwuS5566l8k`8a><@Fw(7AOwwmU~ILG$EIaj9Q+CyE&>t+9!;D*aG z#-G9kk)q$~)(_Fob<-9p`Y{uh0Bxug$V*W-9>8*b)>`LJ)o!JfV?tvhW7T+2U;h;U zdaYJMZ!`qv9?8EU(Toj?oL*AyA8H)Gm;Ye&65i#CU{BENT%3Cv)OtAo4vB3mewZuF zQxDDeQhjD59Rhog;`c>RnKbV9Ur}Ba-3Nm17-(;YbU&Ie^`zK=Ud8M86<$8%hxs0% z>Y-ierr#zAlf?AKL=gVka#mI-v({=hD?B&n$2Z=KZ!K*Ae~3P^06O3S5O6IIx1M%fN#B%a@UMIRtD@k@FzTy}>i0iT&J*2;i6QrF zCJ-6{tg<@T(&oW~ciw0F!;oC9%-=B9XNlautX=cQCgnBan@ z1k`J{(|Y3}uKd0V1@wF7ExS8f!y-W09Id-JKPfP~jO;RzBaXa(uTfG)b&7c!#08xW zxX`vj-xFAN{Dn5D@1#-t&P|o8S#wt-@hCVSf!F`^aDDVUtJ-L>c~HFXRE@jtFM#ed zem{x_PyzS6C9vUw$@fU^ahXg?KpmDh@vmSVAgPeOb*>kcM<~p}@x|DAP}c=$x7>52 zpcl8D_S}^CsR;a&h3U1-rIe>Qr?{sfcu=Y5CeM9!EV%Y$mVYzVRDWsjzGv@PA#??h zk)ro!E7>WAElkGA{1#nB1k|@QenF~Yf`TnSzy>N16xdR^z4w-*dxd^UCybF89WU=S zvjXLRuy1aBLTJjZd9=KeORoSnY~ownYUijss%cQ0URQPc{Ich4J_*2saJ)tXo})W~ zc7uIF?;~VQ6#l6dP-)_JmIr$=u)9EV7Xvcl0l-klvbu+d8B>Od>-@?@MNfq` z<1Mi*sVzBfjJ1*(saeI?QF3nz#PvO+Uojs{1r^+x{~QH#&N!G0;Q^}kS$W7&Q|B(2 zJe32H=XdP^OYE`OlG{=&!0;hI*Li=p&-MLprYfz2GZ2*>7M)SB`%ybPJn5Eo)Q2}s z=Lh3UhQg+LP8ctOZ_oPIlh~`IKA%7L&$tERyh|%nWi;N{ zvLSo{_AC%t22p)cb8&^A%GLt=1Xo!8vb$b0YpKm-}U2gh? zj3O#Nxe4UnV1a7Jv5YvjPc-hbNv z;u+Ic`BwE-ea9*qLU_-4dG}Yu67xbG*@7y;h6NuaI(8TPp6$nn2sd^*PEkbqXE=^kqJQ*n3GB3b5r}VjRY9yT!^e=NYB@vd{K*&c`sSyVx6xJa+bZuq` zx&fl*|H9=6!EcuQNBr?7qKyn(XZ<+dj0o9Z$u}P?ZCbgi{ExnPusysL!c<9=s*!rA$+opH})6QK~qso!P@G4_K6S z6@xAf2>U$)vmdo7SyNO2!qn^kNCHaU&Y!^UNWw2kVfijSF{eMWK6)BRvzuxs&+xJ? z>K9gBsL!Y#TgM^kJm`2tr~3~FwAFS0&63ZBUuO;t87NRM@{0)H7s{t7|9lE1d$cy? ztm{Qoj%PKQ%vGm5_b!6Vx zex^QSBldj!&OsdPPE=l_Y@`1rNQ$KQ`o?5+(%zbw%X(3SBqHZ)JWt9Wueun1$rrmG z`(jykre}6|b~<)YVDJ6%oxbdUC?u(0!-7`pLX?j4ePw0kGPxi^m(Q;#xDgIWERi~z zLHArQZp-^s(bH?K3Io z0Ji*Y2Kp>u`W4P7J{0GJv8g7~7sLfVyfsALyoi6et#kuUA}hihbitO|)h-Y*pHnwI z@1nEs5mGil+yC=2Z%x2uE}sv@rlv^y&c@eic42w*BdmUAjdTMxIi+`UbPT7mE|zx5 zBjpo-cw;cwE%vjG7X&oxbgmPo+FDwFR8m-crznaqUOnE$nryF0>a_I5YzZyl&TWRU zI%;pheRf0p?X^tbyv?BYGbe&kp>aE1A^>O@%N}>exqwQ@jnL&$S3S$oFU|Tl zB|-&5T-+%a<=@fC;+5$3Af*?o|6;_=uUcU>jtqQvK8tR-d@SDB+p!2^;L1eVZey ziTA{oeS3+lXB;BKm)H81(l`IHQpiiEzMc@%jw%apNcKJ$Jpd)vxlhW&9m#!%@(q4> zj|`C83D&vjTi-#GZ zq&c8X+O&n!A=S-0$W9gn@7p0!lzPI84V7M=_r6-~Sj8x2X#a$qAjZy@X@(e>b`)oj zNvbsJ$yPJbxKXu>$O;zFZ>B7vEtInN&WN1&gBy}OgC>d6=w%lx^x z*kEUrGZaJskc@;^?0O^KOUcz|h4PqTwGaoh{iv~#H!+^wQ3jJzg(%Lpuw5ySgLUh2 z$oOK0+<3KXA)T;&>J7t+qJMEbv_kW0g?n4SxQX*bI0#v@fD&k3-$U|knVXdUy$!8 zeuTdHsaLP!kVli^#{m3^mE+b-d+t3ONQva|(LPVWmj6&?{~3Y&5L*b#D9ilob~?B! z7wY+@-HQm0OUDm_&e6}X5oDD9=R{|W2*7~lyPzZJyx2d$Xn;hCB&=HH0grMldN-Ia z*6t4(B)O={y}<7q;ms{cFY{3JCfX-+op1=6tn;p>q(K^t9+W?^NYGro_*5o`@6=mZ zaXLqI#*9#W97-I(cv(hO_6HnMV7v>7tH0B~Xb-PkO76@h#f4`T7icbZoyi4Y60C!y z;5&Qf-SiyiK{j>nZs5n1x5GWDjxSqvoc5w+mLgyJ1S=CK{D@!1A`H$D3kDUfrAO+* zvlwC?d&{Z;LQ;|~bCV$ZbAPoB0bjSm5b-8HZ{e@ej#|AN{7}8pKlJvLLB@y7ZiWhl zDIHOz6n{lbTl7s=Owi(-u_5jTmdSI>7rCaW*{q)Ltv|MFWzB^yolWf^f zW&((wOsL(9rTa7}wtAA1(m)*nrol~FSy?akk}eHBHw1Lf&^)JQsj0_;?TBW)#TU}V zb9tnd_AH|FyXZr)z&2;bdoo@ABR}uL%M!_|zH!Yu6A(-$LgAW?s$rcEkO}ct3meqbGs};2#pT{mlhUs-ad%BBYf@3 zS#A^hJV`a<3z_RboN>?`QenAdLAj_Zl*KOo9$2Zx*Ny@cqLa;US$9jblU&~Yt_C9? zExx{U0aVwtytvEu6zC%Ega*=Qyjfe1*(+YaMz@Puoob?Rk=Gj@{FwOLkCy_x zgy!=&EXW>Z;bw%`hVaRdfnDXQ+&bu=pNcuJX1sbSNTX$91lh3nG{x-&E~KUPgl**P zK++pwmH32w*oVdZ@N>F|iJ_yL_w1(am>7a%PY)o*b%ep=136WGSyD6FADDQg#+Wic za;KjVycyQ+H23BALF?T~Vo7^6KrJl+&XkH98mWH2^1C_!9>^heIPF_s`YAX_$VOG5 z=~x>#oBDGm(X2G58-((V0&!!{gd5}R2Eu5s9_s|{k`zm;%*TK?KaECp#1%Tvjt!y& zgRFS!Pj{}DYaOKT9vyv;fCES`{FS#vf=?%_J^@(HXZU(!Ui{`@@LIN+iJkB#o&&-5 z4&Ss3S*b-8`xsQj?8jt=nI4i|#)BVL0qGG8wbx?oAeJ%K20vJWiP~zOT@jeCen9GY zzcIAY0y!>6PJxCn<-WKbZOxVeM~qD&@6N)64f7G?${7_xK+pgD%2|o5ucW5a?d$T$ zq=V9uU1QU$W9GOEkWdel!V{pLjh&qwimecBeMNi~nzo6Kj{eOn=xlgY7pTCPqEzsT zZF4Jm7I;f~-*wU}wge{5#g8D=XVeHGJ-f(0o^;>aQr<;98e9BkOfB-kVLx$~4LY$C z52ns>86ADIHKEm;b>p^^RRR;jYdMFeBqu<6wFFMApEDKEfPqMubRIV99x|l;1bwlk zs4B49`yPGV0pCk}O4alo!;mV|tfj3deT`s*58)XdLIN-W>N5cZyxCi+!B^4d`4ybv zpAIZSCi~=5L6a!Kw#EF$`@pq@=exOZ42%}z0HFT>gqt_400HkFOAQI-9n1mGW;2ZI zEqNyIbc%tH*z*O!COD+Q31c^7Yx(Jey*U`z2>e_krO|4lUwjTHT?@Qw{_%6g{+lxR zSl&o-FzT|{O?`O5*NU2XUnN%GEz6gjEj^WNB021Vb3NFp=#77b7kpct$g zIvuB>8QxJkB1LnbL&kwgaSEV_==K_z@s_+vivdLNciqDp=K{t40FbUCVBk_4W8_N` zc1SD#KGgt4PrA$7p+T=51}3oZ2^*+M(3D<(q@4%mi{F~j;;-wc?H5Tqmkr#%$SK{<8uL0o zUg{o_F-__RVQ(2BviOE}(ArDednw6v4B&0mtJxYa5H*0gQfE^y1hcG_)t%oA7Hl@H zURE^1A;h~)OWaaonNDU_@al&a{tAbje3Bs+W)Q`xsFS0?bOL(&5ed%&;q9Igk8&`7 zS3El~py3CDgK(@#<10mbYr{PoTN{kJr=+#u z78Mm`gB;fb25)#|zG<*uKIPFF$75pr$9rxXl`Oa%n{HZYSlMq3%GhQ2*_LGxLd@l) zsg=Cok~S`lz!UP~y7zGr!)vy8UOtlA`><@X9XA@t$Sv(P6Eq!MVniImbXCpG67L|mrWF}MnJZHZ8g=lW?y!~??{H~X^U0x zAaN;Dp98+;!e#sV3^>T+7+@4|mmzXld&YEhaPoyc#c2m#E;t0_`tjhnTH^HyfPmdY z%xOkI=-fo=TAKTJL1Xp_>AyNT+;p9Na4w~;D)3~wPNF_TbLde1gZH*b>zZ#LQM!~1 zQ!vBU0?r)H;~J7uCKow;MX<@iB+!tB$~&yqj*JH-o!q6s8XbXGzq)V??mYtB5WgkV zfUjW9;?)*ksVyP)XNdg+Ago2KTqPJOIRl*E{$6V+5)e(BU+f9to-iW{mydY%+Lq&u z!2Mb%aN#en5Ej!S_xTl=29&nm(OSBtCb6$0=DzbtuWq0_Sz=reGN$uutj1vT!p~w1 zX5Br;SVrJ`ybup5Hv5eBjj&kS`*>%eyCd=S_S3@JEQdVJhwJhwY2UJ+n+fgpEQ8*0 z4zGpk{W5vvQ<85?Wym46Fsp{vj*q_9JJ24aIj!^~9XZ`uV;>F5yQiLY-VhWK(f=L5 zjR#Ewl<^Q^Xx~V&c`X4Kp^vSGPd9Q7d%E2Q2c3lBXM5v1G1mvVy?EETV23o zP_Y$1HRtAqUgx}a5Aco$Vs=yFfamDg;Wi=*9k68Df+3+fpd)U&6R^>@2+V4uDX-nH zYFNa0m#-Irw)b9NYd9cE*+E{G!lJ4IRa;GXx;ITv2wYCW^iIfuB3B2#)7_mXHvw&~ zCJDM`9(w$F>cdUuELK&}^yz2G$$80=g& zdt8bfim${7!g9f+Ox--ytZ@-gVuy|7n}a!ONuqlKxF<#TKaIi$gPJezfJLYLdk=`SkKv`QRWeEG z-;z6HIKJO4b#%UYHLJU3yOoAIyPLMZoA)!e-I}3o6{_zgV?gw^dP3krqHrNi5%VH` z-*d0}sw0*i8AJagc_xz<|6m;CKum;DL%zWMLBFwb9<})8cp_Ti-qEKBmYTF4hSTGv zl!MtIhJ(xBpPH)!O_4(Hpo#pls`cGFcXlhCPvev3K2PW)zA-Dja+^5Vm^?9&p*wvp z!iFHzuvsvh*H!etZ^aSIO&@kUduKSTJ zqrPOdF&LZZyBx`!8mOqes$I!SxLzg}j@4M+2Am4{wR`<{D%}H1s;;ygklaWrsM%4m z%TN6v7%U*AXXG5qIZe)aLZ&9%=mSP91;ppRb#I;wfPr>}1r?{Kap8w)S4bgJrv`Z^ zGM@?7onlx_X%&aZ|o&An$^UXt;@3*EeEn`^L7u?c)bQp^&FLHe#?TMKc> zn(Nqrq@WDmd#^XuZsbsz5hwtYE-B}mf%`mU%9g5kj{&aNCUF^kN&sA2%CtuTxQ^m$ z*9xlFfskuByT9}K!B_SaFRkx7EY4?f4Ckn29V%NwQpt_t;h`@JD&}xc!0%ZGO3w9= z^e`4^w$t;nnknPru#!X3w8rX?e$PRBkT_OI!K2#qa3h#5z7-!hM@>AD1`?!5;PUNW zPmbM?4=N4o9(;UwR~#PV%dKCM;k`eomsB7Yz|*3W4jwH0+e`Eb_PKm* zmnI=?{gVQvGtus*X3<5!G#**LC^`?Xz}zO7OH^_W+c^tnJ5iCfQD%Zxu-j+G*jKXZ z5>TLH;WOSVFrShq zTs^M@vqcx6^mmU~KKkgEr#W#Vr#joIW>4Tf{JO4F{c0PnW2pBDygaEkOffNi4^O)# zE|`~tl{g+lEn4dg@Rw+b@1gwHF$rVja2{I1EK|JW*0RZmtRe6skNs7(9`UhK%ZMK% z!}qd*1IO7`<2yhsEx!|YU1Wet_=o_S)}MRZ!62yUDyLYZKwm-hON4mKRjIQ*ZI`P! zC!4{ZTlj%qBu|+aGa{u8iLY^G{`AEVkWM2)ukbdFmP?v?mitWx6L3%q+T?VUjjfuc zt%h)1<*s*E&_)0`)CPpfeP6k)9{1hFPoTd$5Tv$W3BTFk4cfF@Y67o15iL-iEwd|K zaVry*rKO{M!XZQs#B)rK&xp;`T9SihI9OAor0M$*#>(TYxljDw1f4dZ6A32-t@CXK z>rUOjdEU+vVecJKQbj*d_HqWrGM6#Zg$)?La855zd{J&?zBH(|&=FgT^ds)_B-|Xt z4uMw+@|QV7wNK1CFRXk}?7pRVta4aNlC6~xQ!+`4zN1Gyr7_cJNefm6_kPi!khTIW zuhfaIF8%$PcZ@#UI;3nOOUmaJYKVGLK%a4s$P#U}$Nm#b$YZ4O6o9yfgOAE^IJkGM zZL$wy#Rajq(rpv>nm3DDz^a#VOWfbK(0>2B1zDqA2yliSxbT0q;`h0TIKsA6ePIw} z=Eh*A)sjU{ihTRAmf&kDdID*GHlsi!Fva#_#?&VWVgGxTr`)Yl<1OZ9mF+}k4xMSzxYzOgDYxF-7S z#E}kC4BpJp0#&@5*vkrzm?|<*Zqu3XWkyP;V#g3ejH8h1c5~CpY+=pLtKEEWBh1S( zT{Mm>pqi!Uaa(<;-cI8g8po#-9YD_3^mXZ_d}<_CGNN?`+o0Z|pFR+>o__p1C0s+( zShmK(d~av}x84dI`O_oJbD+9+*@>T=OJU$vt z)7aJYfpibVqs+tK%G2X@nJ&4&)B^gBb%`rz_}~C{<|Zi3@-%_-RkyhBk>YCJhsnnl zJhaA=;Pr%*?*RTxKF86b`xa?nW^z%$54jJH>S?muhl_8kgIEZc0MrV8dc13NM7H=A z2i5E8$KqrSOIHX@&?L7(+4bnLxgJG>a@$X}9{VNkJM)_QhpnLqJL1^wd$=jqAEWQS zyH#9j*>liVr1(W)TZbjqW$->Fnb8upz>^WXy5q%b`PWLMmO;GFBf#gAqJR#!t>Md_WA?!Q6SrEZ0(RN^F5{9tPN? zuEBJ-oy$BUr}@pm`b(3oZ&%_I6K(7&7cN(WhPUWcQO(Z(49vyc0mIc96W}-4EuAUX zRBbjadMKlopxZvo)Gb?+S;Wm)rVx6E36&iK_fEkX8ypOFph!+4CXCeWnZz)rW*DGi zn>E_HwSD(`0zUF&dCfE!=xN{4xKpgD0JcGAxY&@RwL2woUmDl9SwN<2zt1Uv#|H?O zAD^0p=H9+i!IAhRW2j8ARP5q%jbH*GR-Qc)ha-Sm9SwhBh#?z0D(#Vc+P4@x%D~HQ zX4FzcPP!E+9W~#t08W19QDf9y`b>>KJnc_EFLq=uh*3~~?xE|fWROIZRtFCFxDM&3 zSDC(g2!!_UX~vVI0&6lltp*&1Wb$G`X=HWzE+U_=H4~WTX#y#1U>7&=M&aPJWSVs| z-{X#v+2$az@pY_i%DI~@=6%=WiF#zaLPjWgZw6vCUG2TUajG9xhWRjEfJZ95dGSJo z#ZQPd|G_6My_8#J(EDql`3^>E48Uxb4IeH{`I;i-YdDat#TGg#E_DJWFKGN!Tvo`? z6EWm52z=dENl(ZrT@KOD;gAnI^H$@ddibgVBa6mFlEbK1 z&!udX*`}t+;I$=lnmVu==6OVx!d5+1NpXN{a}2A`w%pDVSC zucdsjBwR^3W90mvPhQ}hsR-NsyjPtp;VC*TL!lj(>^6r3;$W_IsIS1l%Xrx*4GbwS zTgp_Cwpa##BOk-$le2&*xqFhw8bo(4!aO#p=yj@;$Ek#)!b3cH!o87qw z*$+inz$tG!e+T5urRx0g&`13PpfrY7dpN4xHUD=0^;jBrcrMv3FG4uPVx-8lyJxRi zCmm2wYoPt<9fW6B(bzq1G&n~d@|w%9$CF8X;YQ$)z~>SiM#92!FDnw9#}RF3?_|x* z8v(=A1EdX%>)uEEzNxp_a%dx?SO?$kE#+*!mYhSH5Rix6?ima{%~cQo^=|=u)c>BO ze7SJ{ag(A5gZ$%~*d~eX`;@$FotxjT z<8-~{Z$p}Xqjy!Uj2!6Sbbbopx-V)O0EV}r{e0|)m z9`1xrCVUNaIqM~zbMGG43{%AQji zgEA-?J^R9jbRfQlsZY^7M7ux2sr#Cv0sjHa%2Y4^!NSMO5J0jHmit&t2{k3MJ{FHt zDo94k(zkPwjP+1iCYoAyy_x+;Xz>U@4)8pwV`sh8u)BZz;!F;CG8jpp`W|^*Fl^J$ z)%lHwqjT^QFQ7mh2s4WA9#lAd_p;0Ax_KYt=I25{BV}CQ;p8$*VS;n2pGU|2z}Q#p zQ9H3gn*7V>b7Idl<^ys<%eC6?7`n@{s`9P^a${8aEK{5PMec2oRD*(p*O*qN(*~Rg z7Hq)T($61B_56=`8rm}L8J!>EH~I!j&tPh?-HC&NitbWJQShO)$4icV3m8uLu(366 zhUQWr>+v^1VK6!J1j7i$$&I@-)CRRG>zMQeY661ZnZD>8^MT^#6mC$qH@A0>fsqHM zF0mi;%{pt#?9G$sD<4&|s1*h=`(Q#T6q5%OxSOE-A{z-yvGTc2!IpqvJh?tD+~H@8 zY>^yT*ccX!fz4FNHdY8SM()9y93W=M9p2OU-CbD_19bOfw-nz-A|7gPhtr9b6yH1Y zH3mu@1ejU7My96W4U86}s^%sPR_n2pGzL}jPH3^LRM&g1e4qY-L_6|RbNxx|MQHp* z=yJ;_S$%E?vpqRFxlu^VX>(zyNDY~G7@8bo#Km$b!Id5iQfp> z(Gzuq$}(R6D)}uuM9to#ZX_7=G6StL|gVyw_{zM(5n)l*k`&(UH1^t7*abQt&2gM~m!TUC zPKyvUguALe1U`QSkNwga=tgQsZ2Qs_)OLDgveFl*F=ItgVzJ}Bm;*x*%XPpU@WX~S zV$~36Gg&n_RX#2JA$UoxGIiZId5-+XqG4N_+SM_BUWGKE#&&0!rMk@AwI_C! zK6z+mW%oj`Ti~EsFRXTj+MJ7l(%W~X$~H~)8V&e2DPHWj;9oadqf|KutgsZQ5lFM> z?z9YD3`-XEq|>*%GVh*{UlV(7#&1mXQE)z9!XAc$|de9L;rF$)fu^>BVwbS3KQm%S71lJpO{=Pb@*4rD@*gp5=eecUpA$`*|yIAmAdXkIC>$>4yWe@y~i_r@;m(ZF)PE$Y$wN%CtkZ(y>7Rfcx?y9TB|}WNFIqP zXqN+4&ev?Ccp6c!pdByoae0ov#kv4vn{BS^8t;O%inKN%vAtxZ=tjl&bzn-{%!7}B z1CQh2Xae82)_~zUa&HW3A=|`UiOi0j-J4e)t~P_SpX*Mq))X1MA8mgQyi9W=d1Lya z?2zGT^areZTUo_r>Mso|GoF-p@qFHDG9Ycko0UuS*>QmP}AGV@6M%=N4vqf-o#H~ zp`q#GgG!k<(v0ki=zji;@Qzo&QfYK%4ew% zDZq4l==q!1EQ-`PSCS-xsG|Mup#^^@?qMpB^HS_n@I&WNIEb>V{~9JVWi{Wc!D7H^ z`JvJVx|S3Y4G*jVmI6_yW#apj{yA?{V{h`Y_0Jd*j6R!q`O3ui?9*!kt}c+!ka<5N z_MYH-SqbIT{A-6>$yx5iv6^#ASlSPF}v7!+-O1`Ctfc_L5 zHG|YZ-%TI4an?I%%lbYO$4}!^q5JZXlh%c*hoJw$^I)A9i2wY^J{o-rLyQVaH7%hZsp@TP1fFAZFY9&Z@NhlE_HkoU%;`L{nF$#;AtZ zcjpCNmgW)(vQBB`Gg)aupi1?gVM&PG;_A40RV*8cAOB310)2aDgdl0hL)@;0?uaE! zUZVA~;mkXf)u3q8!s51gWKNK7Jwg#_$zbt0O?LYB57}qT^zw#R!tEK^^5f?cr5|Ey zB?#G-@hNc3+0}zX6r^gN-z894i%gp_ZJE4!8C9}AjDb2aYL@z4r8R=V4x}nwxlD=M zfLzDHSFuqdvr}sMB&}~FO9<-f#l!U!_bvCkiYHX=OzQLAZlkhi64?P9H}M4Q!iFh1 zb=r}p-i|#fy{}(8^@eE1z@e=BFuSnC97i))(ETwglSmdx`NS))z{A~hymrpRYM*1n z{OD}dGUY9jXDLNq(R5f)O$gpt`r63EXeZyL%$`I}8)%k;GbCk7rvpx0r~+Ah^W7;p z!9S(;67=r3fD*SYagk~v?6T7*uxoyIK%XG=bz{)6cMY%Wylz^@4tAL$wwd4HJ+@OWVsg7Hf999S>+NRM8&b zFmlU#1q;6>rA4Dvy3}^L?ukF-7RR>S46@%$X?m_ny53O!=nJWxqU7-l@le`uA3Xoh zLG&K&10VIjwU0HWo6rXN(g_Ky$ts8j=RN3qQ#dVXk+*lYB-Dct@j1KH(@?slDlSI* z<;%lWtF9(@i{4HBDyQ5=%-aR^j{wjtf_6p#9Pp8-m&WzTO!Wa13voqG<27j@27do! zg`-~pj&rd;stf@q#)>8@u6qdy({$LqULje`FQ3|RQSz93$=)?p>v`t${MG4VmyiV< z!J-@Xh?s3HuB+LVdUr|QoZHIb(Nuv{BmcI|GK!IGY&t_+(+ry#db28?@p6DDooXsL=7VXeq&XKP4M^i$_Dzv% zI9YJhgn8$+p|^P46%Fl8hqMzEV{BmHeDkOYb!q(nsCvt=thT6K81O;5K|#7pX^{?r zO?L<)Eh$QOcSs5XlG30^i6ETwy4HHuTyu_b*C;b? z7OBZt+_K|brU;lYELFIv*^|O=)zQ;^*f^Nxduon>GZ>CPw47F*yP5dI6uYeLuBPSn z)uq+$eEV$BShX?l@6t5`r{w$eMc@*$0@-R2tYEdG1FOIG{%Xi9Ibr&?ARRR}_QsWD zZ8gmz-XGuNP<|@Vmnb0Wflm1jO2Xz4QKMvoTBYNG^bSLbru953Iv)4F044?s+4~~j z()u`Ex+jP0B{Nku}^%(;yAy&AX?#MQxa6Z1ThjAu@P3MJJZkYxyPj(Q+i(i*AE? zh#$H8@6!G!NF+nWFEE#zU2x&leYeEpVMWb}wg+ByA8)X&#$+a0A~_k>&9{~M&A?Qz z0KLo)XN-E8O7Tklsu9^N-NqJwj8{Np;(BpAHwUun6V_=r#g1V!Aen%e0k5<5;^qfy z4o!REi8nkxwg?5c&fmL)tR4GXTu*GuTh8oQuNcw(dU67p8B91Y#et89EQ(OK^v*_oL!iYKs_FeTiB zU!hvAU?+H_&`)e#%Vcj+Z_%@obM{~J^FYNAn6UUl@QXYgEsYz5%He2 zP}fILH?Ba+1s?#-u|kqo3F1N4G=fDh{%lp?Y>*96TfY6wx(k~@2B6AmPy~-{O?~<{ zc>_2=dt@O3m4)TJ$G=Lg`Bx|yT3qYhbPl+APcrLCLh^PKBZ%5=wcS~+oqlnEzr8h5 z_)<~rXl|L#rb9SX;Br(@3g~Qp3!2SqZzh zoB;_qoI)RjD_Hceb~ca`?MnWBxqxMhFhc@&O0mg`f{ZaK7wYrcFJ9cp7RbY~_@zTQ z(o#eHGpbGl0svG$>wB4yBVHxNP>js=wh6^#+;GR~n}03WCmV&2dV;RjYC8~y3`znyFKe3??Qj^?)j6;~SxtlV7|Y}NK(^)3t}oAtVX|FL?;Ww_aNnD?8Zp3PkbZZavg5B#x9SYg&H{AmA{-U(n7izRLPp92C#=aV2 zxb;wbklTCyuA9d#0b?Z{ZL47Q5>odIs_AI4&b!V?8+tz&s&R|R_SX{b((NH%{9PC6Et6`V$ZY(xL+D@{z9G)e_Os$FG~BH@}cSWg=zo&)UTxcIV)*dzInWIFS{hw z;Wqxtw9yC#-ZLec2qzW}8k9KipPs3JU-D+-W&JCBY7z32))v3>J0~*#DwV0!V3Y5R z>T2Ja;7BJ9KwQD@TtdpBm?vp3(*y;7h!k4u`91w#GkJ12qtJhmd~iNB3a4A~@XEN6 zK_fQ%EiG8W!P_zK+9w)8lv}JFOLC!$2eF)$^yv1oQJE<2CDc5ofyXR9kmxrs`*ij7 z7YDzyLtdQQTmr}gh$Mhq7;gmvd>d_Dd5p(yC6 zp>x6Q6!>32R#pB6bft5XtoHtHYJ65Btlhqc>AbJ0o_GE^o@eOocD`>D`rMRp0$VU& zR?QT4w<6#mi{OZ|0>(ShI!z8AAeskA`(2$s$)?c1?sgR9wavuaq(hQV7y=zkAbMn} zOar#7%-eLJhk8(CKZ)tV)GMpJ)e)fUJ4Q_Y5K2a`lf^*!o*gU4|#wata+1KPv-c zK|3|GAD`*=lkQ(Xex%9mqhEZ_TB59C^W5*is*x>FFw4PpOSU)VpTHLky=R8p%yxfb zQZ6B-oNzXZ?&I=*MIFuByM;S?$%A*t9~sh>kcP+`x@2LQ&`3tJS7!R~S| z|8LM9*i{ya45%u)6Gk!i+DzAeK=$>`=+pk2IVGn?L%-65Nn*UVhu2YS!RU z?rNJTs3mKN=cSb1FINxy0C+k1q+RkE!+eCe)5H>d%}GAurqwF$J*AukHql#epP)-!#JnZ{usrZ-|BAiHa6pzBqN)8s|L-SWQ;#$X_bnFw=`?^V6gTeJzp%3Ny7srp|!QkHVD-9MZGSK5yh zuuzvhY?f@?%>R@oDk^^BVR3aep7!m9K+%`w?hD>Q>clYRnPuvlOrsPs!Qch`}2$=YvF4MOj-1b9ME_<=vVE4s| z?sVO|X9FdC7OBvkX&g~au>t-pwrQL3XO{gRjwwHyu+lZU$%K;0tX1d2c_I%!NhfG)I4Y%P6SA7U!xZTh_(Nzqo8qT>^A z>69eYozu;}`ZH10Sq4e7qI2laI*6ZlzPwy_?%g!(Zo01L^|QqI{K6>a>vB@+wMzN#U72>(b@|fj~7e7j?2b~}i7^xVP zvf4Eg^1!y%fJ*ZlQb1+;2PsFF;zrGB&~HZhZEygX3AyjI`$AYX8L1p|P-bROa8@38 zdzb8|jc}KSPV-0WBKa)nCaS6D)&S1!$OL^6pHT0t8@&FNj?g|ft z)(GRK?x3Vy|LJ$efU}zhjDJ^6y2r{Xyo{v>P%UK*E)yJB`^*~6BU*2rCsH0bs-jSy z|8k#xZUy<6D^MOB)v9{zsrXL*g*GNbbo+DXxwGB=(pN5;PBA!F^P~c`&+|}(Q{u_* z+v(h;9kiC0LUGWSxYDoP?|bOzj#V^SFVXn)cURO3s8M3h=-xrh>`J0M4KHA9yc1W= z09_UV`7L}$7M0?r;lHWROCc8t4tbbpCpaQ^T+CQ4Zk_SmcjzG4Qy+-u3r3b47@B7r zD&3@Fa)B#`LXz8AX*UiK8Cy) z!JsNa@%1N>8z%RQ9$Q!#`%L#v#SAHnJEik->V^5|q=;$=CiCi`D)XTKmkb{I5ao)> zpV}YGUqbUji!H|U9u^`IDr4!I=s^*)@&`X(Y8(=#fgPOwnfDz5oB2y*$&mqpU3%)$ z&L!+c)3Fr%-G&b|fUu*QC7ZPL#Q(`YUmqnf*g?BO97WyQYnXiKM^<(8+!p#szC^K#zQC$!#C zCjO&oe&-Kcxz{}2s)WdgmNSzFGlwBAm{0-;S_!V>{UJSxiBJWJ`;Bt7JPZ5CY5r+3 zeJ3pH0M|)_X#k}W4z1?rO%9(I!arxN1R0%F8vFycb8|agFA~ub(`c+*nbV#UBwb|JSxvz^l|p4uRTP@RFBiI9@0D-H@idao{T&7plk@k zn|Vs0RpU>SYdx=eEAfU=dozut^c#3wEYysaA6te#&66KDl#*K1@tF<`hPdZn*6uC# zWI&gDo+hEp8@_cy(p-+O=1tmf&d!#V719(M5E~<_gmND6yMO;seDwS5>(vZD z%5(C@0GEPE8YjuwbVvx8ZaVq>@=#lTXaR$X&Bpe;HHE`j7|DS9TrYZ*G49G#X3!oMK2@EUH+XS8uWfKG{@;DUvZhOLVw!2E6lE@S z;4h`5;q%d}aWpAcxqA(DaVH=b5-D641cODtP`a#xyanYXntS8_Z9tdx)?Jp@7H?H^S}DsIRMtBJ1us5A%mZM;geNp6Gv@H zbRHCPRBt1#KngSqii&QkJ>VdvK~)*|1&mPzic2)%$cLpo9|V4Nx>pV0xZP^+cq<(e zClcB5k*BxA4S7CXIU>#;?#Lz{J(uBT|K0s;hsSDbX=o?*2lde32Ab+@W?b|RGtzDg z&zJw8U0*A0Xe>9Q{=Ak@niFr|Iz6WP=%5{7F|K1x<@Z> zC5^`9D1dy)v1(M;c+teb3U9u>RqgpGW#_Kxoi6)5csTZKiz~95ogq+DJ<%GhxeV)CRteJ0N;Q2K%9h{ z`DY8dK_xj5#8^%|dr)Uufa07?bI(z7U)nzx`>>m|-7~eDZ?w)770^5^2cAzBsl2yc z1y+(Of|arVgON=M(FIKrdZfg?F(@}xDMCuGuj14R6uR56ucr_3taci*tNq`_PPSTG zbu+hInt-wNU?syL9dP0|pxK8)xW0thj^SU6+8|c( z!T1xGSX+pEtwVnyn2Pax5MmaMdcEiIM#vsLp~q-F`&f7fZg-*S4YvGs0uI$8<$&#e z4%z~K(a|=4(WPp2)(IUJaeNHRwU1PCKrq)3R)chp2D1g>Yp1#PGCqmLYbF%5l4r;} z_m|i$+Gt%E3vejqq2%$Kdm`+!;Y^`3V>Y7w=nS7&-UKPcww0&}ld<0#Uvwn-TB;ZMNSeF%=zDb-qTC2-y)`6=MGHq>4R zB2z}*w1+LZ*X3qr7Ke{O+B!@DC^$J+1a;+n2k_#jcf!f5iTHBJIpA?yk}25cZ|R872$6 zzIpo-(QEXV@)GScteATL5>g$B{WmpxSFa};@ljZ#cP{6Jw&P~sk0i6uF$Cvb*qTdM znxC@=Gvw(LY3LBzV{%~Tl|*7klM&fRDv{tsMxt@h$=PDFHA7qq_4VoORsq$c>aQ%5 zOgqxmANdcgtXpQ3XHy>>+|)8oguX;<9zWihq(CsR-;$i)9cfvy_KD`#rTL!}E$Z>Y-s9c3KoBT0Id; zkhOYp4`m@quiy)|?6nAcN7kJS$)EgZJGj7D#0eI^DJ~x6!*v;z#Ne+}3gv*ysOt4$5X#(A$m1+N`W{n9pjiYRYEV zP>#2Ka{O*uNJnfrbr+XkTRjddI#8~pDdXUADE8@391O|5?^2ITpVFR2#o7P)hvHgX zs;Bzu;Qe+NR?%IE?~WIMHP&(E!w?)uXG(xduXVH=y*b5We0|({Jq)Q!WaEpj2lIq2 z7eG!HLPxWEJL6b+;(PNzs{?D6D=`_T6}O&%KXYv8L^FPO&|X(QqIv}w9rRyPFUr?s zbn?-MjbtKkap|?wK8$2WGpY%y9b%cn50lD`4@zJg!$I(mkti}DqIu-`vRSF#NK_pw z=Gcf7@@TqT_Pa_RJw#=B-TtJHL9^w|;CwE(({O5dxVeIjTaS=P8~Za}nsMai(a&1<$~pyL_&a+vd8K{LcfJ0Ou-X#V1#SWR-` zip^@58F&X|4U^?rN7(OhD&{~vi}TL;2?gehCdCiwvVS+x1vyxINy}?o>KN`OMTxkw za?I%`5-&@8?<(f8N6T3bQROduk_0#U+j+|B;TV&;-#isbv``4-++E+ z{_joHYHr(8x+^5t->mfcW8}ipBP);cw0F_z;Ta?VHmYS>s?0Fkf+!~h_ z1{v%Y=c=v(AMTM=2xPvBe&9;#?KxYuO4E|GI}^IYMkEaZb(E-k3d^y|9jgp{znD{> zJcQr_bB*~D;Q5f4=^k(ANgynr}dK1h~corFcTMC`9lORj#&!dGrt$@5(g z*QejmldBFiMN*o_o#?8fPQgQK177jOj;!Zq!+}D(R1DZC=|#vcAneGZ^iYfO%Ipg) zY|@5L9*v216ZJ^MfB>C#EaRAh*PrdB&>L)}$+4=6fS9;#pICf{D@nJ^cM54A=U5dv zWS2o;-KnW|{&&;b=llD1xwv4dCI&iL4&$9NnZ<`F(E!5|Nl3+qGmfRmT*kb%;?YH; zVVlQ_*J*lbWKO!S%J9c{kZaHR{8Ib_yXJCq`!V0ouR3f@*d?f&fWuIWF ze&mf%~w^`hCVm@44;2_%veV zrxC0%k6al-J<48{YA7bx-%~TSnR;ar!)RLn`Xc|sJC(&3`?%_sff@^8eA+LlWrhcZ z8&XED>*Gf1jtevMDRmbIl)&^VRJ{{J!M_G#B==e;TUEkHY50!6%&WfCOTEo)tOCPX zM80Q>i#|4rtltb)z@^{|&EH#i$vV7*OliHp1c4`$?`Z9owbjLc=#e0^-gM{wyv};O zNb>AyA3Jf_`6g-jivYcX093kpnYfq-O5CqTy1sI&TZ5uJo$W|TxFpAI7e}ON{R!Ff zj;%<_;st?J%P#p!2eH>9-QVWRPd1QM2})$4ArvEhCNtabzxWvcRVC60y+ zUJQXIq)6s-mH^vCK;I!Hav!9O6L$nVTIKDu6`u5{bxGhN&da)978DVFdj98^jZ{&$ z#P26Q3wBt3vL;!?rkHo-5U4xd8WQ?}JO;Sf(iflcR}JFWbh}TVgkmd6mBfS=1nB$` z{fkCDHI^N>owL0jVJkAmc$DG2+m7LNsRQy)*W?ZjN-{rbx^6;31~Q8W{dhB9Qahl% zQ$M+X4K=U3OzM*#5im(W6QUn5P3}&-XG=uazVorV+Fm*kg*B0|alBpVXN3uERJq*J zoz)tKY6={tLI2D0&-*S(kqt9ahr6R*dQQ45XOS&=gKIt|Xt+Uzk#s0KNj;@c|KOqH zqkDIq{>=D#z^u!Loho5@EiZpXEfrDcp1*aNL)EG|amySIT;z}0jb<0+? zxpB{gDOyJsS}Wc%ik@k!{S<7;OL73juK*~LIKaD=kW#!PRb|8Az-L656`y`jnJM>V zK14A=KxHQqvF{JruWXEroux$rZd&>6~{H7-(Fgw$b<2-bTvf$cb(1buG%siEZ5r z!dY@AstDmVy-QBg9nNcBM$X?&6N!knq=T?T{ePr<3Vanw{^<-=q{dSndD=HNQV_k# z?p^rJaV3eMqi`T=MAF+d{ITSZAU^l_N1Mf&IeByW@&u1RTH)bQi|BYYzy78tOBiW^ zi2kxS@zjP$))a_KR|p#$g{Y6y&W`{FCTC_+wbRJ1Y5`?Ik4MfE2;tCFS@>OTy1+lX z;D0tNeh-QQQjGh6bOvmdpzG9u4Zw^o&y=1boWcMHRhh2Nr9eDh5t?=p(m zIk;N|@yrxhy)dRKTKk$aK%ai$AC$^nv+MB*7r(;EZo{PdEL`k;AwC;wg*NAMv(`ZW zB0?W+)W|1}^HmH&wk?51HQ+9a&Po*no8~J{yxU|7uS#i&{FI9OXYzl{YVT+$Et@4j z*gN0@Q;rr(HOnZ@;bSX>C_Gex_aNIk1mI2^B)rsror?+z zhH7j_G5<^x9nvmF-d-!j#XG5< zzrtFoQ;yudbA+ooIU-9<=8oxwZ~kMR$xqxAOGWaee{xg#Syd#8m})ff>A<9gRl$Lm zY$TUi-v@!DyE)#8Ba$i2v2pf|v2jJ8ijtM$HMHy`S}PG=oS=1-0}mRi`e|o(3-UcB z+>1)5n^!}O^|VIP&5NM{`JD&c?tz%?#U1nq0b-axgM-+zow_NQm^gMt&$=lc5XO`5 zCTpJ?UZLGQb$U32-!c?pfJ?1%owerjdCF!sbZzF1>;nS2cWyT|W#n+TsU98<-(FzW zBpT%SzU0-Ayl8tb zI0aPJS|eRB3*P_zb1b>=#OGLCEmTkr5wyk-=%%JLW=ND>z1o?SMq#kPaCbkHqhvFy zdzIMlQ;@?-IV)-IhKfPNEg^i{H++^hKqnr8Aa7Xiws521x4i+SkYFi#Ow2?M1Rc9O z4eC}8D1NE3&mt5&K%%%@xv+a!reD=@KV!VyxH%mhL1W>~m zo3~G@+5eH-e$+TQpQb4J;ox>U8^6}!ZAy-pLh)_qIfQ=2KWV}^Y%HdVb88vSf&)n~ zd1B3v5YAoJj)J8K7uMIEUK_FBobs@x8q%!yG6q~i{0D|(lUqLArao)Lu(TGrrjEq? z03353sE%G$QZz==5x&G!sEpEYdgk}FWHuF~+N_y8Ucb*A<=96M&V}_!N}G3c#E#djX|KZZ9a;ikeVbKaC)+M! zP+aznpmx5)ud~azfy3P-1q2rPf~Y?1++@fysxM3Ft-txG_UDVeeKPyJDHx17`;nb0 z_s2)wU)JQO1mcGYD$D!mv+ai<%`1hECQfCwjXQ1Of{bKgVAv#amwxx4!Y+2WQP!`V zVEfLH93}LN&j@*96xG z5X^#x&xiBSD;8e;drT1%1wVZCZT6a!j`w)nx6f>2a(0dHRTJyx1K--|OkgkfC-i1?^xd zDfqCkFvD>jK@o#FkXQsev8yU)#@v=+X`n)aB5LOqVO2OT@#V7w@`rgI6 zu<_pB-g*D8MTub+%THtX>?VITNZxQDa?538!2FDjs`AZZ&r-R5|6~lC%S^=KUDsRx zpY4O|^D8dvy#v;3`f-={xNs4t)O(i%q9{iwTWIjJK8G7|o~BmZy)t#;T2ZJlYD^jD z5dK8*K8S3T1xEGVvm1Jg8o<-(cq z%8bH|P|gEGJU?%AD_av&08kO-du$|l1erxcF!^Tc@M8Xn4=rCRT zeSM{{nT`;}7RWrAv2hhMRnjwhCXUFAwS+!vm#CJrhTU!r3^#cH7GeSiJ_brC zxR=8l6XyM-dhL9L{Gmb`gY1@`;S+#=OF>-Z3IiKPA&P<;ie6kiRF!S4h+cNq3F|)mnC#$Mw`ldn#fd9te8ZfzQj*=O&^Nt%V2uWL2KMc`>Ui8Nw*R75+EV-=ru6!_qpBz+y%KJ{%@-z&^e7<0MW$vF&|ALm^Yc884oY z7{Lci^Al!e-B;z9e(RSkDp!cU1opYAB^EuW+MDOBU58qDtO`8Q>21=1u zvUU)q^(|~Gxj03_AuH5W`hteINCN_86J*i>PMY2;=Q3%@9B-Jv;RXY6zmY{)6LK`fHPz!L`zr!chGjdmg5mGP$T*A|1PTiaHNCw>BDChJEHEOz&QzM+u(Yyjc#0Uo6UBVG zy7rd%Kw{`ICQpzUQJKJZxBtxokZ${q8A#kdim-mwliO8*g&S4(OU@HZErf?2W9M;= zvH9An#!5>a`(j&9vYrzP8TYvM!fX2l!n0feU0pXn)Qvz)@weI@NKwM94Th(|hm$hw zhOTWA(b`BM4%7p2$M)D5Ou!VQ|J|9(xt$T7?`J!jg&9co0m>MY?XBFyJ}ksafV#ja z_EKaG{0}6Ul_6M(Crw?LzpxO|r6j-9e9PnJHR?B3C05x3_ul3B1;$+*vsrHYz0~3) zC|=T{rfU9jvgf`vQ6*-GUfZ!XIZ!d|T+=zCufjkFNjLko^^K#L`+)>^Ltl4y*Qw#$ zdTqX_LwH|yi--t;2s)899kP)11em%iD9~K2KVB-YrXs3Y``v*`q=52%Q$FsKz635S zU96Ivta|AWIU9-*wMThdxT~Gc9r!`wXpM-YC^({+&>j|K%JR2Q10_u*F9S0nnUO48 zTz-|ujxj#5<3?$5AGdN`J|`YF+P2L6;P?M785o$|RGg)vlG*l(^!doMoP-rz0tmtir=pU&wC-42B(l%Lni&`%RuEc#Rt=S1L5MUg0S{&k zcrNeon4*{&B`1L=SqAW+JRn66Fy8jNxf2`NDiabS5UA2^?1+hP1*fl;UyrbEWh)Spy+S~qr(9Pji3e@{H~~gUJ@dHy$ocKP4R5jO6PH^HCM`XSov%4 zLsm0jLjr23 z4hT4;C6=@N}4%XmO15^kR+= zg6^JTC$SRnS1Xl?=ot;y?N;r6`>L-RQTs$D;%Z%fQoimaZ(ZTe5zZF*1ixFd5uEH3 za#E4WMqR{B67EC+KspPXPrZI*ye@PHEOL3EY|(_vo`!ti!%+zNpKAq62@#oU6%=oI zapz-_6zzHO>-4b>)~Otl30|F0i|^K_Z;wZfc*GT12uECMxc){Q;pVk>v;<|r)ezCJ zdrBoL?Ruxp%>ZaHn_~^pe#tOfi;dxgY#UM!R@eeg=tP~DBJ=-1U-sYA1xtAnRW>Pa zoa5#VmWfGX7c!m&-SyneZCT@|J6-U&8xps5NB#WTUY+9HdZRSHY`&jz>(0ajkONx! zV7SLe(rJ*xg2k3m=vxP`SOIKyqW~I}0;UkhxDSoI7IdyBA)7!EIW_s&YW%lyqzExZ z(P%1OJRY2NaL2ihQ|2?-Uc^cMizh=7;D297bTxLRD3|1L;Z zM6~P4BS78J828h9m{GU)(3f4`d{j0vfFXOo4LcCLH1a_nSd}qPNicPU@PJ(^1y$3pO2n?ZXhB7rCXlONjn7 z#3@RG@dah8MFiBC#kSYoF6;+wskPgW>0T2BqPY`oF-|B#Z2^y+EX~j!kBKS*(Sqr` z7B_#u&?+Z^$_!5kKRvmz2B@0z|NUkoIncHYv~a)5q_?Htt-$ut6R4_wqBmQbSBa^DPbRBcd&)3FU=}voEQ!y2Co_(kJ2ThxF#6o`)Fbd@6Udkt& zSQcJOVF7zn^bi+{<7p{Lf?qEjsbEU+2;`_+8LLS<3t-mOz0QAjLONqL~;M!xytbGi3% zfo}L3zmTu8mmRGZ{X(h?h0kn(UqEd>W24C;uDU}{9xsHFdABMe9RDMW$RX1h&S;j6{2S~4Go zCd{*3Qo)|`x_E%T=t*$`V zaAmZa9bgcL0LoW_9IYbwVudA~Wk1y~y;3d#33?4AoBm7C{I>xkP>&hjs`@3+4$%-Y z5i-l5_zns`BC{c0{;RQG73D9qE1FUtgm1sDh*5h~wAC+39S~#ssJ$gP3*ASVi5C{` zYn0=9NNEv(xr=A87nOhjC@$U;m{(VrL#PRb-RX_4|9dA4+$7SdQAlJ|MZ4=PVo9B;e10t!kAE#1>|A{;dZp7o5ZsL3h;x*x;0ygDN8FAB zWMY(pVo@x>V;+=+nVDN_HZIQ2Yfx$|ni%f}IUTNP%1@_u|IZnXq8kiwgWTdD2vYtP~&D+EW085d{bO)(IOj5L5*622XDM2Cl^I|M_%; z5oM+%YjsCLL4vjw$@hEWzXmO*jD%vN56S2bTQ`yO`WfqIVPPQJxEVac!;fvlk$i$6UZFM{;P9Iyr|AVZ%Q^C4`G#0V@}V=aC@XF$dN zhxh-_qnlHpy1xy)P2axMtMp!skoiyUKD}Crg_jY}i(w44TUJCJ+~>UxuBM~9uDd&; zZ0Bowziz_EM83s0VO=>Y`x^uHVkc*AkVr`*!43?s&mfVeru_lAU$zjfKK9wk;)>1@ zO60fyo~iH|5pH6#-9ofzrv0J^Age`xDO@s^Z}p$y+1RL>wXQoQPpX{9?BgdC;9if?mtWZlaR@dZD;j z^t%1GXGEd&|89nKk1+$K*dG>%tEX6hNh%hx#7q&%!_VYuX!&8LZp1bG$8%p+a_dBs zg?dsr?&yUL9C;G8FQQ#ACTuImP3zkIL#SDa8=>uvp7D_frBxo(|Jk!JUG*KzepzHZ zg%qo`t&f8%l7}A*vpn2HXU+cq^DgoZ+7#W9%8ZO;a$MD!l81}bTF!m1x{J>-4@*n<17~SY!wjy z=0dYABoPcLRN6bU&82|t)qvLi|NdlyPGqEfQccg!z49H)Z@#}&4wte$L)Zazdwh50OpW1#?`{O9+TS~a3%?@o2=-X4;Wc zj25@&V`6ZG@mkGl8Xp7v>;L&&iE&Wn0&scF;|gYzzYjje!o@_i zMJi@ECot*CAKuFxleR$BM~Qn)kVePV&b_&NT(dKXvo>FwYj-(DGxGpr&g{A!KU5sE zQKQQbdG$&0AklNM!T$ED2J%FdnVDI5sY=J^pbSnhKgBOOVX)S(4wD^=fw=)Hn?Eq zU+qeQ?=|v0Ty%J0p;A=|HFA?q2(IQsEzo+TF8;}D2br}7P+DHw&-JLYL6t)}}fKUec^v9x$!PVxw9@yX(!>UhO5GuoxHpB8kQbVTK^ ziThHy)fao>iy>~`UZaW z)(w8GbDW4$JK`RmB97Ogn%e_e=@hw;u+=?Cx9G^!{StBK=?I;|-Fh)yI_>%|YmxrG^Qm5r_?G3@=_4GoDz>k; z^YRo6UFMvH<8L4V<#yG6@lfFXAVL~j{BTI7#-bL@8{Cywk_Pe(^s;?K}k6O*J zpfAb#wbT7_(@(i0Y52Vc@OLj;iVSX&Y_4ftRP+1i`%zZp{kCk-{Q6FYjlDkHEnu1O zJ@UNyA!_|sR9rj@f^coU0RX_XMQVBo5rDiBX2Zr<8Bq-E#uhKTc2P_OIr=Lun2F9w z-(`IPPNIwdc|+(dcG@y4 zs+wJv{zP~D*!zzx?ON9Ze+noQoLrv{`fOz>vHL{%(=CY?AzLNwI%N-cfa{v@H{iv` zx6ek*TH3Qftw7)+CrPi$Fp1k_=?&iO;Cvh$RDPcgyK^=216Co))WGVNLXm+$|i3)nu)|0k9~ zg|^+?o?HSNJi2)<#O^a+Si$j(%5>@=BYZVCSCb^(W6g|-B_`q-lx8aP&F}n(R~k*R zgR|lQf*1E{-glhc;0qqi2J^%Ljy6tgAeA53PHP|`=BT@kaWUKgQJn3~0n=C`%VAiA z@ww$0WfY^r!QuKSmNADZl_c7or>Z|b_9hhl6U|h5&Gy6fs|W|5W!`~TlWs}0?=S{o z3~T;tloLx@gMWOkn?f1SUzd4HlHJo2_P-MN?a2hC(EQ5paYauAj}}au>~Qw8g|-=iCWF$I2T{Hk5b?(DlO~ly__NrufLQxX`gB2eI@TMDn^O+|2^*!!*`^ny;*Ff8P(^Z{Wxsa?lM_)+`eJc zf!n*8h=ELe>qf&qvRk`x6L)_787h01Qy~y=w?rJSf&O3Owo-ZXFAllO=m=>z-U>Mb;HG<*V8ZN@(st{!)|8ZWgckt((ebRv`5dGPRMIqKS`??aoLF66{ezs`@@&N%Od z7Gwug4Yyw$2p19i?|r#_2vXBw7(h4iGSyn{wHMelh5(~2fn{*2i^bLoTuRfN*+r-# z2B-OT)ZmGqVgi=i_J=~OWGn4%QOhR12BP8PS0#uJ?o~SKsXeB`9j^!_U7s{9rJU6R z6~z%%Wc@KZv%CHc=BOYi!Gz+G%L$-CdCftJeK!qK4bi{D@XMhFEopd-Tl5+rm0DCF z`N=IzzkAp0TGUe-D1uFTwY2cts?uvakS>z`m89s_Dp{{N7&HW|`pJ#oze!9a`o4W; zM2UcY-XGa-5@9+Rqs7m_$v@bIx%ml=$XAj+Zw^TG?_GR^Sxp;(j*BjS5VYe()?=E@ zcgleiLU(8APpZ+QwlM;AuTv@HiZTS=^WCcJJo!qBZbdult|HcaZHsdF({l zlm+U{o7W<1-xHIiQCyCySaRvwA7heRA~zCK+MtF$wwq2JIJ39y=_uc|`Jl>B%(7fO zXE>uOO03r<)T3U)VtNN4fR{VTrd}0|3wZ3yabDao--ouz8S|h?Bz_(rI5DB|_Q&NK zh{T&n2K5YnmXKLfa?hTm$|{FM_9MyvkE*YZimHwJg<*i9hX#@EPATc`Mo}b10Z}?7 zq`O0;TS8Q9K%@}_l@t&KDJ509!Mo3Uzi+L3|8^73;|?0966YwzWa_`H9P`e7KU!jFkfs@*J>Xta@Gz#4^xWB(00_my zf0EVt^pX43f>~+ODrJP?2ammTCwe{_6d63&#P-=kfGAubexrB-VtVQIvM5J;#@>Jv zDIEbAJAGSE7U4}RN8Ft7-5+-Tc(a{hWMpJ(T`8;vPuExC7TZGtj#+}&3w+R6Upyhq zN9{H{ubsTFlcIB_bv<-M*I%OJEjo(f9xoOvNTF-;%JdKAF#SpWlB%*~pv1@Rh2oX2 z;-w1_)M}Mf2fuo!hMrwQxzT#ZDa$OV1kS+#Gf~)Y3f~8Bb`zj)ieqoI7(Hl?^NZ;q zez?-nU*9AdarUF!+c#uRusO2bJ-Xcs}Q)-3+yb`DlqiepynkSqZFnHF2ca^;Z zmjYGZJV!`&qUiXq%8GVY-2J;O`Jn9(gP1VoXd)aaHYTs)b0OtFd_vX6vjhLo_89o& zN=QbRTsOx)yMX^N3i$2EhkM)sxDOdQOtf3#pHi+DygztJ-H+w9>$Kl$(H^4@{rwn^ z4%?AtIy8vS$>HIS0GZZ*qIWp$(bzqPvLRrZRoOhyVxJxX*kw=*qv`#>Gj2g{tR+dF z3v5OpG$k^gBkpZA=(W2LBKM#({@tNUC2nH(WEm_Vk2QBB2@Ts7CbuIml;-dT6ikfi>Gx`fx#QdbhH(0Cqw=?;M z_Z`bWWXl=fJ_&gkex$7CO@|NjlqRlkQ{{iV)||>LG+LKI0tVvap`;3md=`m=Vlxh8 zx7TRtgD61FL4|En2R%F0$Z&?qT=Ig|W?ih@Cgg)I`RN|+x&Z>_3zQ1ATEDg#HUu^L z^d8JsY9-1Fc9TAW#b`99^L?v>QQ>t=#udVtU)f=xh81Rc`CStj*U z-bP~e0MUjD9tXW8eymZVlf z7W*D7H(gkgR~K1lUnGam-~jnGB4KewnO6;?VpBs-S63bl39qTN$K(IfzzUio&fQ+lOXR zyxnNs78aUHgK=yI8j0M9YM5Me-Eqpg{2fmtpHK+r)FZTU;*G=LG=sVaWP#g&Ycb-V z@*H#sE`^4Llx8?tAhI~igP-$obh2U4sW=aza}O;YaR&^mvxz}tXoq&1XApf!O?TS+ z$i4`>7b0+5fb~6v@%g^^f^qeTX!|1Sc-^q)P~J8(_`xuEnf)&3-U~gAh&LL>+oQ#t zcTQIAsoK?rvM)v}Z`BU!0Hy2+~*|xkm{+`VrrWaWL{b}lzjQ`%VFbs|0rDS9j zCvTgszarTtagldJ=^fNU@4y`1=+4S@aRE~)vL^)-B-H{PXy{7r75{;(S7~p&TGj;l z$x-4{0ltxPCPkNw_V+~-4LTXsKK%og7%ByQTMtypQBlDwOOsKzi1f(#*4f0yp{5pDU8x+ zFQATBI9jeq_G0tnG$qV9Xh_B2auQPo(s;c6Eqys?hZ( zI$oEAjn%(ZvG2o&GU$pdc$ai|DKnCfy)>SJA@gU98t+EoVR7)5N~6?o$L8)`6AWc& zNKP44j;~WWR+`Lvehw0&c+Bz97+Z`Z!YcmuIyHOCvYH~k9S-ht3^j$G_8bn zQwmgtC`uv=G49WXT&tZVqKnH3VDS17fMH~090LN99GmIE%^%N!V$;@B@PDF2$T&-* zpjYf|F*17Q>C}PSt-o`Dg-#^W?#WWGf+(Gycl`hVB;)}^2r@JP6fYcyZGZqLElYx-B-#ZZFS@a&Of9)3R*@`Nih}zxw&D3U-#wnM+_n|M(`A zMuQ30_AB~srF>_7pWcd87MEa2aQ|1OT_JMrqewfILF1RK0ZmG9kLj_X|68rgqj6d& zDzwvnee#X^-3i9gpt=bFsiN+1+}uUfx{yyy;c|~pQYSi<71~2Ua!+!wLAYI1=;Ngkz1V>segp^|1#>Sk6snt!w7t$haXz;X1hNvs0nRpjS~4W{v2 zY1i&1Lx$lln{*}Qcfj!PzOEAmE~C`bWI; zdfcn)?$~sf+r{6&tb}?CbPkK!YzDl%v{uH%(-Anv>Rsn}56Z{xQA?pLK4AH#&65=J zngz0SX&&O{D-7@3vmg8n3;Vv=qAwaBSr9MU_dnz}Iz*yHEwCkuGIp(GYG@`TGUzg|j8wP8nD z^Lf2gO_uGyiWH||LZ2~NJjyJ(lhk`*QhHl|2+-Y}Al70FMevwt6*f7!JawP_wF%Lh zO~)cqok`dlqejvQxv@I+33VXmZtu#I^Wgyzi1I9SzgoRyesF0?sp!@iA8X_bVOd%V`P4!FV$2%CSN}Xq%zYR^%A28LPsqzA&SiaBu!+5n+ zp&e=Oyzmv2e>G)Z-=R#e4IOSH9KnM(FWv_YJdpv^a$)i{*x7I~RvcM1bi!A~V*Pc- zK!mXZxkC?kc6Mr%9Wb9#SK?zbYCwCqm95NfYT9;V?_zs>pP6Lk2Z`7@E60weyW$QK zLBIHp2#DUt>sHV9K3EJ3Yf;e^n8L(s|2MIT!vEGQMFlYSuO!2eH1usD+B6jAg;7Wn zceIu%rnw4;5ya7OL)4?ltV746sCRi{nj%x)Fhz`lAyE?`4WR}>%=I5&_rF4I$)+bF zVlgh#XhO;GZ$nR<$VA(SrKFcrmAMUBm6-q42P)+Jy5hB2qFU}}h}bc`7@xxZqd|&$ zn@+6URP!fEzOnGz!){ezb{v=a1();1hynZ9zHK-!#v!U7a(QrP2 zr%?k71->m)tQoK%d7iXZro_`p;m_~Aa-oVb|5RO1X4#RK9;de&w)T?es=>nsmqg7S z#RD;#NLFRW@$Ku1T;85UD2Z2t+%js-$Xv4-gG0JxuS?Uy2M3Q{AXSX3=f?Z5?52T< z?r^nqPSv>R!%>)rrqe1P8tjN79JTg%NBdi3U{_tOeVww3sY9OcrhZ6Wlu3i?M-Mg& zopyQY9lf0kJdvzezV*Xlz7N4Kk&FNLMYaigc;6eITq16Q%$e>%jR33BZq(5-CA0X4>DnweLD{a@YSoGJOYU6Jr6=;|WF9kkOeyO)2%uOa$~ zdGZMG%tTiI{L0KA{Z;d&w2Xo7i*EtEYYVwn>Ffq;FE}@F0Qnk9)w5cKyv!V>lMQt} zU0wc{p||^!QgSrsadGc#Jg?9t>&1q*X#LVJ40$J3_;=yi*5jpbsUEG%*5Ul&RQSq2 zoK;7LK@nr&yP5ll1P-`Db|h|L6hgjTf-Szi@u-G>d>Cwqch=q)prk*)gTV9KWNrYk zq1O>h)QX%<6QB+)gO}O3h`W)5xchOPE5$m&8QLnw!=u!D7Wu}n2ot-CIMq)lT(QD_ z{i8<~YJz1$dWo&GaZB?DOWMjp8TXohCI%t*X$%pOt;(@R zSz3HwAXsCu54{t+nNS-dKdIE~W(yoela!7qg%%68uC20|fj>rCgVjlG?k43#bSK3E zofjGM8GHmQIg!us2vb>AH4JyE$J=8-f65CyTED)w<)WRvu*od|x4hrU)`mCx#~^?( z@4QOsi-kG;sW5Z3AZOhyWK5zGMWEc<0X1g}P2!`|>3i2AagK?zy#~u3MNdwH%urSS+aEHSws7F7e$7Cali^e29`gF_&=^3Lm2@^9CJ7v zZol#d{XjFASXO2`qF(Qmn+8ElQ}KXf)K}1M1btRFY_~6z_Y-&p5mT4U*#aN<$A1c_ z;D!^^$(TmLEQW}*987@4NHw&TC4ZU&S|z{b^^>1+vr(X$T#?V`3M9L6!NpQ@2fwYe zm^IAA@xX&?rddQoVCGLvG9obINK`R1qw>IU+b@6GdL#fk$f+k+EmlP#kkEsS>fu8< zZUjuRfr_Np1yQ`B`402X=!o6Q8>9uWjL%UwT>$DudfD#AJDPDc4XGsNl$2GX*gTls5Y|4zB1c|4IsBpi z-T53Iz5CjFDMZD*CwN5?t~{o;@z>=SQBSP_vk zZ$IS5^g_eE=p2j&5zn=c_)G^t7b!0IM(Q|R{SF~14v)DvrEFXrfd??1_N9j(e+7H2 ziO*sE`gYmB)4%t!i;9?a{A#HOFnoCQ(HIL%tTL-OOU{^Eu`gq_B?~pru0n^H);Vo} zXEvN@c23xE65@jqd3kX#yCS0dWKhRB*7{oX>orVW2rqv9cIBc>%4Z^unA#NoDE;;< z7}qB>{r++rXm;CPj==22cgiDGC8Fu8X1wE+CInBU!*j~k+}0xn9c~*u5gN>jQ>mqT z@+hsGq7vN}SzKcEPcq@n8A(Rsn~MSq$WM%C_XOvW`EdE3l9p;YRI)NXQ9!y_aQ z%qPy#nT8r)->)}vgQiHNA~2!D$$=9fRXZo)q*FT0jb_i7rNZr^wWB3-?WFvU! zB81n}h?oA)ogH7}%V2R@(*0&A1?7J=p)VSER$$S77A)7hdR^cL6ked*DfStIuO)EN zp$zUFkh$|+uuOYrNQ}qukLM2aVd<8ipC#*;u}AUC!8+Bm)g?(S@WR_K3a=u+=Of{C z^8fpzJt19Ua#enOADBhu*S_D)nqU+!eu8Ftt_VXTBE=AZPSWbfDpX)OE`@4ywAlqA zKS*QMfiQNq9c3(%V`-9c9?#W!wUGfA%ZBmc3q?sXD3>n3GPW&W+^pu4&Uu@t9Dj#p0+>)1@6raCtOh zxx6MWI7H4-I8e~WXZ>`w|C_yev+R7d-p;%3;ZC)UZ6)Oswhsk@SZrFhpnH_B8vI;G zvIzM9Os;Oh4ebgQYVi0izUCU70MhijIrf63m86r57@@RKEBJ74+YHq9ZxFD3Y${qQ z0F5G7kHi&!;aOld0n+UWkPj%{x$zwja!h|D4wJVxp*~@)OX5uWA z4SmC2VR-5M@d6W;Gy@|R`b+GiEu^^D@z)G@@Rb#al?K!PF0ilEAOh#Qjox29;)c$U z28vz~5S4=llqh14!pqj~eflpqn{l_TmZNMq@CWbI_Z!MgFCKbvadJk50ZbVlYX(d8 zIpwer6~9nH2fZI)tO$LArwD7`%W%I$LZ5g)xbFKXTLxq?w~ZE(nw zqerP>K`mN(X&{=k`EVzqLGvm=?y?_)N-2+qly5h*KnAi-faQ}VHS|tCOW^&GZeoE5#Y$OX z*2H=R=A6OAu_b=`P}UsL4Wh0r+&CmvNkB}yZJXjQ8vdROfEmM+7Pl$Lj zg%?QSy0B12Ufu^c4KEr>G%4!+8k(v~HdDLhknY)YOcuM%gU|2(33I~@wiO(9pt`AW zB{N&y~)QY(cks+7rt>y|W?SbeR$G`u!b^|(=OCkIvxEb-DO zW!iu$MMqM(1_7D83>3sOzpu!)3xECgiq20BRh2A`JIfq*x+`C))~`g=x+Ob}T?mpr z$DEZ`v5c3i(Mw?*33C$Pui_^<#*SpT4AdC1V!lk51;gy)bN#Z=_Q~RzEpyV2C5dF! z37H^43c7KFk{~cq8zKV>ezS8B&w?t-)mqdd27%N5wEYXS{uFLt^gyiReyW9$0onjd z6pALuKM#QB#A)gGT$5T!%lH+W*4;b}&sKNi9a@g!c+!NZlCVn~r16cokbl!OC!)iO zK$|?2?Nj+G_zpYp?lZgW){H%ZkeN?~a#?0(W(P-BB!_MPPM;3B-k!X-0vyDy_$}1h z8BpzgG6MGYSIZ_kEtKj*cg{J)ylL|Ip|lFz0MqN3iTmdg5^?&-x)rRCYKSsEF9oy& zt%eIlaH(pPIBeK+9t07|rVqCaPgvJ(X6`Su!?|;Jav%pEKF)heP&F!JSPE3oYhLaU zw5d|3RYk5~3Pd$CATvo%DHh1xBCuO6Vyfti>0)oa;SYw}{++p97Op&+v}Z8Sgth3_ zqoUwHO>uBQI_hf+@QR=gj)&17)@)GcV)+j5M+rS>CJ_JOT}dWR9h#CZs+1645@fW< z5Q@A||5y%oOCiZMY{{DghP*#oIL8$D=K^tN=&xg&TrDtONZp({zzvPo7!fbTs|9)h z;@>x64siutbT-H!kb<=lj=jKcueU?m~(T^1?t01cyTG`r?`O zKCiyKSqTq%>f#Md*7BOO_Qv+ULf%rf&hSh0$%6Oa^cbC{^_gq1wIIV}rv)RTb@Eh3 zVPrP}?I9R54ZI`rcKvzys#YD9k`H9$|IYJ|Zx@g6mxJ^>wYTBvbMb$2v$HK+Y<>_o z62#|=lZFg*<^eYTVg>r>Uic-z?45ReOJ7%jmk;%R9LUy|ax#TwsnBD?MYgQimQH zyt*}OqKTfDeL8wDToFYGZ!o%PV^uguMk#*p7_T;y;0~tYCGrK$vw6~sD^kRY8-27; zQg3NF)=5ypL&u|)w*zZxL61eOW-`O+c%%B{$MJElv;ux~2p>yx$V_+7G zp0IrBK!QMM=fKsuw^Q*#7Vm-TWi+wHtk28;Fi=F<*B)}6qt*`N*@ z<>(^{4M5iopd0=Ml-}%&nHzt#BxzDoaK3qZxisa|1M#R0DoRk=s03#Q>gDU5^Po{M zvfXl`#23ToZYEbS3wbyrWQM>{clk0$^UZPySWSLr*;YIDxXrUudNCp^zrVV7Ks69x zAW&2u24U9HmA3S^sf7qyv_p9S3&#V48#ivyPUS&mVP-5D(-)OjVfjS* zM~i>u3R`O()OUzuRoe+PRwRc{QN!YXe_D^-X97MKYR?CPP-zqpHup8N$q9jCX}UKT z0&CB6p;{OF-ViNZ71Pd+keiE9$aGwuW-?rr_0jj+~2glyy_fl6w4K zrW7#VzU;_H4m9@;Akrd9rdYp@5RBC0-0`rigdbxM;6H&!o^cA&p%H*|zDSW>=Zf)A z0AQKp_H$@A6Ig{Po-j^KS3Ej~muXe4hB(+#0Z2qC_7$o6c~+ zZCb0RSbH}s#-drzWA}>i&E{{)riezoX~4DmZSiLybaNy07L30S zQ)lCkA>uf5Sha$_IO0d=EP{Sdy>i`I9zOs27zz(J-hKu>GBVI!V{sW%^(}{gq2+bI zd)(|?!cMU%CNkm}m%~+C>=fGwT0noSN!FHypL+I7o-Gv|&z;lZ==xi|wI^phqq~0VvBY+a-AvzT%Jhn-We5Ao1+bA^>CCH4fC*u!>`sQ6)yqhV8%OD;mDk) z;5ro(5|$9AHlLgAO=T&6X!we5)ke?gvD;EcRUC55S zW4r@rLc1Iqa`!)(|0)jMyOM^RW4Fe}b8u0*I2}~42d8(7N?5kUclSY^%eJ1j1Gf!j z{0N{{7>7@xU6~bk0;7R(QwN|=q~=4&K!d8NQouU#LvHP>Kj}_1wc+{Am}Z8ZM`t=C-0y_`pe%hz$K`;{5?T061zQXkS1dKO&|zY z8E1N@J=X{>PXomdfX#0NEAp3%Xx%+>5DQF9y-0}@w1wpi%o{S)%#pXx2xT+f;~|$-J=d+bv%t_+Zc(@8XYu+yo1Vb^f6g+>G+V#!BDCch9sYO+gv)I`EQQl) zp=S{)s?_Zh&GeB|F0~zyFdZZggndU2^&UdU;A=?SBC=}h_r(%_{KHeMIX?Ur;8@@Z zd|#Muzu$HQT{*Jn?BUm0w5+9aM**DFV2G~>Up=~{X0-A8v9s2`N;5?a7Puoh)8;3g@jqZNRJ-%dV{OXUpcG# z=R(a~4;n(MPZPpUW^=R6lKLmk>v8wn#aF9wr6S9jw#;Y%-=ySE44108;IrXm4}0L+WCe+9=ltE$n4+T@le zfmi#CdHezW-$=y@)K?R^8L#xsn8@(r_i`~*-LcJlc~7=OK-ChX$Gt{FNbNq;qA3!w#^5!memfcV#Me69OZJ>D8N?Pff-Lj=#-9{#8kF) zUR12qX26qq-TZSdTWEZWvdC(^gxWz~rKR$xx9>CbJasyBzhyo>XHg$$>0|wqAr6Dy ze@q>x(s)B>?g0vq(*+i6MDU8ZO}|T6ti&Pb=XqQMMWiS?YV{`s`gNG%QOb##J++sf z(Nw{n3~_@cx6kxO;4RS9jz__|%_lc6v4~OY)q1G1)EJTTaP&4#kG&~KGcd#A7Jn(Bu z4Cm0c*O%R3qUOoDxmEd2*x+Gf@spZ&mo7o9<;@L!!2ppjwJOLS(zS8u-f~N%jx*miziEEUbtx*Kqcy6CFqp!>B>or}(Vu6*i!1D{ zJK{Hf!JzywftpwT?2)J%A?ICMI@=%6rxD%r3wh7eWij@HHPBFozA*dP`pwqOE>(WQ z+UUVfP)AITgFN5@BGh`ltD%ZG#BG8iOx0tG04(N?l15Z^=ts8@3mxSpE`_#Cj^dQ7I%F`ux*c_?ZWZ0^?zs ziFAVIedhLU4F2cWpva;nt(nI=k2GAR)W}6JIQ9>mH!}HMQ~K0J#vr$-EMl+gK-j3 zsZ@WY=F}HZELbFJm0Cc{r??juTG2-Y-)<~!2<*+lD&8}^YZ$(-T*+>;F#89J8KWTmm- z`LG(e(iV0aS+{s)59Ix$$lr3#b0S8SakTp;;AuWcaCI3rLFGg{_iq`MMOSa-`6OAb zs8E*=R#LHP(zJ0{JU0ulCA55f#JqZG@>_1|TkCeFhtyq4QAGs7!kNQ{B+K_{Vvx`q zEUs^XJV$Hza>^z2vHY>HRb(DMcV$&nS^Mwhy@~;%cug2v_tx%oD&*qxdy-Muf zm$Q6w@qT8eAUd!Y@99kr(Jmzt?Zuz%!5kNgwBRTmjc=}0d_jnN<|5<%eL3(c2(65) z*b?k6!z{WGc>JG-SA!q|LXja7=jUxZAs7+en|{(g&A9$HW|juowdzbdTFatVKFp^O zllu%mj|z8G!eY6KTY7nq{&su65PP;|Sa4Lxdlb%TcEHU&QK^@xVCk`gODpO{Y&Z2m zX40;qOWqeVXj^KuXE{Yo4uez29G&D)-QdxPp*9TkO{{V0;ocJu_0l zEQn(Y>5fGAx;pwGQi&*niGzPb3!nPW`A{USozv*3GX~^xH zf5(j6CZgVzm6G@2D*E*yxaJ8aH?OZ(dpHdfW+9b>%p$j>3yiA-Y~~Ks(ZM96`}X%b z$f*O zIT$+9c*UqVggfq5G~}quDjjnuhWWfhSOl1fHG!8T67Vi_)lN0yEY+cW8iXoAsgBHW z+j8INqgAj}LKA;6lv|5e`jPE9f4Tg-rmxxykD3#r{o37`S|erj$rZd{5@oTjGpYm+ zF|gLaz)uMv!k9p=se`V-!oZhl2=sE6n;u{>ymB1tO#Elr_L zX?`U3N;H#WhA+h*0QsOc#zMe<5|J#?58v1(1ZZ##izi8{R_Fg}dU5YrFqTLAB#BV~5Kfdz89D@EEzr zPxq4P+A<@S+fS2Ett2EQ6mA%CiT!>1KWRD5aODsYgd>NW1}pB#op}#sIk~U?&PY{I zDd(@Rtq;=F_-1Ngt`2-?eJI74r5i}ebm3NasP^JhaF6vY%2Jh$8pMt53uW=`)!Nsw zYEW~xER^J$tb|07l_Z3@9GFVvJihzlD&ed2ev77=ZM^6X*)h&_o=M||ijBNm5B*}N zo-Fs$!QkPz7A80I4S;Hi;f}~Y&)cMNI5beIC`4G0kvJIaWg6o~D?Ca&+1;hRGICB%f8Ds3<6^#+$- zjL^l*yM}%Dds@trupAd@clE20Z)fdx4(}}nT0?;{cE1hgJ*st{6BE+^joWD4pYd3{ zce+a-vFwX5X=2}8Ypvgi0G*NaYp}#v3&7>1>c;JfxvR$_?^mFXG5%ER+>?gzbU@}oaDugJu*U_} zMkE3Gc^#k>wgi3VE5TBWxN{nE>j1+ylH$LE+_2%8_KVkALk@A=f*<{!ew+KJc}>Ja z3vCY1)4o13q&o@+kGh?(8{47e?d?79;Ib)0pq{C~`9i@k>`;K>V><^yh!i=0+g%rp z9Y#InYpK0YU7mSc%yq}r-pu-%ed*WjOI}-Kd)Rf3N$rL#cs3At%^wwgUJ(P4B$C5& zaHS$Vdl^`@jq|7zDl57CY zW_C@A%gTf1hE;q<2I&Cm?P{3!Cbb%^Aj&vb3@Bsf722N{UDA0sc$Yop zwt0u&MA_-pN`475@u~5i&d9c?uNQ4et#sILKZ{^S2g7ITL-z2XGLE1yjQzGDwg~($ z|1&^gV^%?t^ANr+$rV3jhbpCz`CJLZ)@%)2O^7#2pW}flX|J#C0$5`Pdr?6%w?tM3&MAQZ9{u`xbTlNX{p|Gx&uG>o(%ueW;hGvT}9x z{CE8@fg)n%PLRuqN~ z5oq28rnZZ!=@Be7Na*XP)O~2vKCrPiLcTb%Xvi<<_c@Llem}#931T5bT5@aDVu)33 z?gG6k&gz6JdG+AEGul@5WR`2*7M;9AqF}L@Ae_0>-{<8!#bND2=Or zGjn%f6Mt~>8&ZH5y=#u-i?Uz+Z}3CM;XEY6f9pI35mBzNKX=$n;ywm$jqnR`^0L%;2ra@aD@JnwD>@dhjOfn z#U0adp)z+?@kP`cN6@^>%nC)fIV*_E2t?Dv|4x7Wll$++;et}AZ};}T?r~amc=o=0 z?n3nNm|)anho17)hPr{!03bqvcL?6&I6_Le%t!E|Aq(4jd}77cN)8_Si`IpA-e=k2?WmZJ+nn zze);?2H^ev2-H4}%a?`Kp1bKp^$E>O#lZ>-h%_Ej6^DF8hyplEt2bTEoF49McCz|1 zaU~>VGDZzH{j8%3{oNsWsU?<}Lp=6Yp+u3Hp0s$loPYGYNLD`kw)RwbB_k0HXG32? zF)NBq{(zP-_8TM^oc0&WMmX$4#{Q5xg`x(xo@|?b6Ln~|G#XcieFJzKDvs+@Imost zWcy3yIN~>UW1l4%(QDuQT*#PQC+LfaWe$XXE_>8zL)G=%pu%NeBY8?JYKACy)*#=+ zVTGES%kKGNf!08A-6hl&q8gwv&gDD`iz9Y!4cEKeD#7;gw?$+q>LUIg@!7ZrI>H2z z*$PCK|5arJ>wMb|0KyD(*9q3}wSdBoUQHByvT^M)6IM!0F~qf{In*B7hG+N&2tb;i>VP9$oWtL#;2WsSb{fwjh~6#qZGftm)9zGkJ@n}C~D4yIYop(yoR~cEBeRQr#|Mk@5+58vgw&4bK`?PkEh)^UIFahow-r?-m zCs>?e*Xia9(_zQIUxpt0BLgISch!~84mdre&aI118ttf00WcqlQ+~~{Sx@X6A<}=Y zkRz0Tz2oIHHP{ zC%9s#$s@mdr;LZ5J3K7INx=HC%onq{j{}!}msKmu2R;&iN57{9_OZz`u><))B3C`^ z2J)N6i{a`_z){g(9JQK%BYsry_5`Tn&qcJFDZ_g%@ z`Fg$N{QC7P_7p7`@SU{rPa&2|-Rx)Kl8U+*u`iLY;LD1-Tty^sJ(EcnRve}@m1^y(Wll@svF<(MdbaX zPjN1y_R#CZATt?3a1p6_Z$%2!!G-fA^+;$RQnyRAWTXfB-42s$ZDERtXfyiM70#_d zbsQ$W&KC}sLBaO3wEXz%JEPKF^}O-@f9p1c_B1``&e++p7_KsO`u(A|G^UgdZ1(r0 zixZE!36z2_o75_)nxMNA$w(scX;f+W^|Fw-lL;ImAWz>lQDqKH&#X`Xnf|x-?ClH5 ze;-3$e9iOtmv=7=^rR{;o@pxF7D@Fp4b)Zzp&f^?xsm_0JBj@r6HHViUngfx(z6Zz z%W9Q>YB>{G6;~Fzl}wmx^cEN$?P`orgbPj0gi8F6Fyya`m}WLT$`J^FY@T1Fj8>(yT2z4p*;jJpz)jyo^}(m(m8X6zee+Wd-2Kynaw?b zZ!5R4;=3*jh3aZ!Aun-DD!Q7NbslEdLY`MY&;KlD)EPk7>Gua| zE27{<&DK}~dik}JNd$h4r{D=p`h8EWOey#vVU#l5xegpRrK?2rbN|lv+lZK2r!-;7 zG{JzSt_K@rX9;SknJUf$@C&0i3V$T zBfBoch}?zHGL98T(VD^Ni`%SxWiE#F1V`itli{1V^&1SL4Pq#NmGFdY=Bp6{vr#zl zMlgNWKn(o-!0C=$4o?b+p_>lh>S zdfebhXdSDvpbUbZv1iw{19p&13^2(|PlU|hMz?&(c&@dr%&yqwrsNyi0TyMT5h9W_ z-z}FX88}Nq+{>tIS6%~5UWg5*@kR8F4ds7_X?p<$7x9#cE~4sKaSy#N`eO{R(ZE(68JIkg43xqH83aH#o% zUlHc=5Tb(j)-wvsrQODp>lws4^e4esp$=$iHSm{-xWOFZn&Y=p3`@DAFF3)t z9HB+wrtpG}fBnr%I9?j=e=)cTO(SKj_f9g1X$82l!1q^ZZyKFNbbnbDr&7mYQc|Gu z=57-vi?lm*;*Rc1i}MqPyj7x^V+MiqOxizhkv=?#mY8CqJXJgj4O#A%4UeI05JVYN zu1`r^6JbTqo@21{Wd$a?bKZLz@C)jEh+2Tn43QC6S}U$yN55nrg4)XFxmc#ik+@eD z@IkyxTss-R)@$}BrU^E$k#!!!0M=;pc0ussL#M5C6w^B2)Jq!V81XlG44#xTat(yy z)eoq*Tw7T$R#GZEUuDUpLc5k$&e3zug^>C5;!)7f{__462Hdxt?Hc`mgu8!ktKw)> zL(r)AznqUB4N8LLMH<2!U|SypZ;76P!B|5{|6$nJCzle0MFdF@xj=xOZaK(J=1DmeWO+M9< zWH;L0Co3d0dGMC zcu-uqi10BW)T_}~(&9$3lvj=`u%j#c4exsw;t@QxaJN{`RN~(~r&@YeK9ch`{xMx* z<3RNq`c(GFXJ_qF2-_D+qwy}xVk{1I?RS$oF>SVRejGM=pyr-Cu`(KLc7=%^tfti7 z6G~R8#J1lYfw@Als=Au+8oHjflcpUmO@cxmD#~|{FLW&4`}19HB~SQ@xF7DHLieAa-L|e22isl-)fbdQX7`7lLeEs^PBx23sRW-_R>Vv5uW1+uwN|JT z$E%5V!z#C4#hR6M(lf-29f^m@K!N90}@erJJ=*vuXh zCRHQ3x=a)Cf=JR)yZiuRhWdAN>XbZvQpwVTrZUyI=u#bpX$~@+%xj*?W<>YV{%VfB zp_gCXt(qyQq0Vq}pve|GoITJJW|9@nUB^qCtgoi}lKANQ&cJfXZ>4D~x1K9k#yIW& znre)74br|Oj)3)CzZ~!R^_YS=09C15%F28ohxLi&7yEZSlzeur3ea{OfMjvIerf_} z4W?qlKMUq>{qG7nK)v5t)EWJ$SlDvz6@~~G_3#9Xm#_FHdI@o{%#F!m5L$x4Gn|f$ zSOeS-<7V}d9OIYEKfc=`juDc8)O+n#lWP!amx;~gGv)ZvIG2ngNLD-z$`FEVVIfn` z+q2!OZ@;!NUA$7M@&JLr6O)p%e&^)mXoA1)O}7Bc5MD zfn9IT^r9l+1oB@ux=uHM6Pw=mEo`rtf{lo+?KR?l`qPn3K3&f#i&lxBofeUklbem+ z_>AGWO|Qz#WAH!)4u8@b9na>{^X2I5_L~_Wsrc&^pGVnmn^-A-Y1VD=K;TB$>I<0@ zdvCayMN zV0l|lFzr1G_!ddQc`uM}oxAoc3a51{YFX;mSYPI;v%Tg4P_ZtMgXDeLzvSZ~hY+Ua z_|IFYu6$Lx-D%Hoe5P>~o-2=xXU|lP3VM79tRVx6gQx_LUfG+EBS0^n6zDf=fr`?) zktbLU>0;OTC`Rc2?5GCe@Bt9G6z(3obnrH9O&`3xOy4J() zGYeeN#=*2p#PeUeiRL19tLEUhBDnxTP_8z_mFMrB9r7Z({!0MRRLZ|ee+J{YL5}bU z$vD)ogMN_AJ6JlZ3rDtqLEJIA_Q;cCi8I_8hlW@g%i))@Ya-W0^Tlj)Y&daelPG5e z^^D%4>Zqc|bNK31gqCvR*-JPAqt9|;DC4=&4|ICW+||u&@Z`t8Gi_#h z-=WsaSHps={r&+QrTQqADfpyQI8aM0XE4}Kw4NwvF+)2sjVu0GV}3boT;X{CZoJLNAx$OiuI}l6ggi9@zv9^Ws{w99~thH_)jxf zyH#CbLHtISFQUyXCPfoA^d*z7jAs~6f;9PNq)K+i9q&-qAhj#bQ?Z{i; z4h@=qF4O|LN2#DKr{>?UzioZOyU)}(`Dgrbfq!R-$1gf9u?0g(DlX#BT=qoWcjdYB z^75Yl22!jp@_=Cga}dJoL&<%fOa1?l^_BrqZE?Ra3rXB8{Y!(ozDF5+W@f@0#;G_x*71`FtGq?Ad#*|N13icd#@AZeT6HPq&B?%@5Sb zG#D0ZKw>fLJBwHp{^%mj{X&O>X>#&ex}^$2 zZt{1&zHd7(9!s2{dqo)t`Fy%-Ydklpuq0PI$Ev#NHhJ(>L zkdPd10q~y+(?&$#LjE`2cjL-6QZVbLxoT5IK)|^Tr4ctzkw3zCW?g6P}3pN%0_6w3BZShUOiKg7RBYdd``*kg}C?jBPZ11}a zBj9gItRGWP^|Ai{8bl0SpcD^0t0fO^P1aqTdXm#d>Ddk|1d=<%=!8(cL^ zGHE^PZe`R-qDRe?OjDJCd&V#i^G%>j` z>DABAaYtAP%82|`Qt(DPV0vvz`~jrBF!v&v7ikjy+(Kx|&mZv0V zCpNBsM3`&34hbQjJJD zz~7_dQjhZoRr~;mkk`=IfMRC#5E0BV7W$JJ@j)5Ka6-0+Ncv}qNpF=Yw-hY1 z>fMSHV))cq!pXW)+#U#yZ-ZhJuh6dWu5$pjfe?+z&0A!J6WBmcROKG5?e2ZtV%b$d z%84m1^f;U%G!gg{C;6gI&{0W~nPkr7=+H7I=+x!6PXKbG-uzMg`Lkz^JhKo;8K zk$w+$WWsYYI}B<#fYvn4aBfSwiyXC{Tmc$`hfP0&@EnY3LvdcGjRoCQMj+~Qll5c0 zsL9}9k_{?V@Sc}xf@Aun&AN6G0Gv6$j9q}1L-ogXam4P&1J|tlERl@zCrfpr6@E!t za0$59S{J?#vEyUv{4>-Ij@G5Sl$EEPC25D{xk`m}rw4NxN)*n$4}uUlIFE7Sr-b17BSuf$J%dflhO*d*r#5(>A$e4 zU5La{|Fj^J+P*=@FMLdNvvv#cO1H(5i$Sbcf|zbty4hg1(Sx;+C*86$j+G3kC$YKO z-tx_~GRoJ_ReT%t%rz-uc_y62E`9Q^sO6IIByB8cBB+o~G3fboU9sF}wwAHML}1NjdM%>*7Y-V3YVlut7=k2N*X?}pH=4-?-MqoP>j=K_hGj%3_~C9Stc$j>c+hG_F% z|EPIH`LE{#RxYSBnm+ghBioCDruz1&36vbCJFNGNzrN<&5kjP&?C?o+)$Y1;vZG}wn+3?GO zZ%X3@SK)6uoZVwr=%x^PD?pViz*^bEoaidxOm2?fO?VExb=O*tjI6FERKQPG2OD}~ zK(CBOmozNHJhG~lB)-;lsSeqSP8dR6PpZ%Y1z2y+&{;~XqUdWv?lBCrC#?&A*@0yr zK7kk#DFX#^Df#TVXUE6m8lTWJSZ^k{&CA!((czJDfo3Xdr{x`8e4jjAaBbR11dFXXoI5vOxio= z%quO)5WVy}HYnybBenf&GyX<`fN|Hw4lwH5zlNBg(??w(J}r1o$1d6hSVxQ51oxvt z&y68$%$M!Gs+5g><{Xy(v6j%yDRz8jh81v5&iKuSqkBS^PJrpkQpl;sGO{H9BwME> ziR7MZ3hkUWi30TqZ&s$zTHzrq5c7@%i>VtH79YIwWoBZukCHk>4v5uu01eWh?6{_0 z1q86486MwU9Hyf*fWcP%(5WtVhZq}eP2~nS2h*GaPEHNVgx-|x_O7svKYHk3ULu#? zY$`1U&my5_b#5=2xt3o~w=bxBK8dKXabpfV$-X} zQ*+2r_7b3L9>BikN*Uu83FM+0UL)e`dapB%LG_3RYGx;eYJzho(be;H$RxWd@Xj}A?~0ZQpC9s7ShjC7EXY!n z{fM0oi)5XI)57#s=}l6d(jAx0U5`@#FGy)^ui02Hxb|84Zo`}E9+8(#cG`qXNI&&$ zG8;y-INtbAM1d45RY}tB()u=3C-=Hplt0eZE_ns*9f>pOWe|X2q=h<}a3H!J)qNFQ z?Gtdg*4C+~dF#o@xs3rRmrT2wp_J+5jE%I$gGWV(z7b8~xJnP1Wqak2LlqVB0}I!Q zKOFDIPasNgP9o2xG(=im$!r>}iO=O6a&8tIAr^pY+^Bvc$2`yY=aExr3}Y28PNEg7|%QxFBM8E3_24ZR~)=Z(JM}}+EU0~G(fq7USpA}1QSF`gJGTV7usju z9x%?PRAJeglPx|GZ;@5G%4MCGt_N*SQ>IYSiyu7T=s8`J^iKeB!T6`J;*|D?=n~<% zc$90z$fI$NwL@n@j;zux3pV5iUGw<{W&p$G^Nf->6y}%(a;MROB=;ZX`k}Iyt0*I5 zW4ND2s1v|A3H}K^1V+qW@K!DSLU!e02YrIBjwlr-%=uy%wiyccg=>Gojh|x*NYm>e zS?o`>!Zth4$YJqrfJC$|$RO^Q(!SQI!C;KA>GWbq5&?0=~j>F@XK{M+!6vUCl zMi-o<78)&I=~s^Ow#U{@RwbiusmkQ=f^n!6QQQEcFz7pvBsyg+0{{B}a`-7haQc&u z?c=UNzkI~lyB~)jVVZT7G$XY6KgbuA&^p9`jnxp?v?>4!;{lKa2M5|(6tJ7|1Gfm$ z%C`5%jl1{@X~g~}4$SigV-97JDGeYVj9}RKZRw-|Ubv=_5h>yz=eBYSqCC7;Iw0>7 zz}15P9~-&auh)aR|&bSM)|85@{;`TX9jcVp)W;*)Nt_{(H z4Jqd~u7LC6(iTSnX}Ip`7X;0^VmXUDJqu3iE>)NVTF z;hn{x9|6}d&*&*R&p&p(0whGW4WeXhZ=ta)$NmkfRdw?JKGYpBoA1Ocf4skJ$Ru3I zAil;tf=51o^C2=7pYP1!y+ipRa9wMGXl}7MYKbI7lnBoEUeEttiC}^uIMT|Y_xMIY z!iDUA|CkE#k6-6Uro9GlBEffS4v!>WXkHE7_ zcNp~lyaFJO_h%7xA!%>aKX!{2eng|{C$GO|zB`2x-dU>Uc*te{{ruf_(1ymwrr*K4 z0p1TE=;VTe1L6O@3I9V!M$~A8R-l3igwIbhLwK-95`4wg5xL^6M=e1Q_n?5TxO2xm zA%CVenMClvHRBJg0}Rc8SQh`wUjRN1DT4U_S~6;xfp(z?m}VAhEqaJyh&PlT)Grha zC$mihm_`9avM$DSY$&nfn0-!TeVu^I#<+-V7yjQX6BgY4A32YJ{2f6|7K6ix497L< z8Qu=tgf$q^V&ygoA`|=xTA*GktBBZh#DR58`OI82<=24$2&LUUKL^hG4=pX?e+Ojp zQnfdl4~i<<&o!3gG|L>uL;vSXbi>GD0DB8ZSjZ6$N6*8R+@tt84#3oug2a_c#5oy( ziUJ~W=P{kA<<8;Mz`ZFj*r$+_@LA;?ae*J11~9I5o*03k#w2wG8;?$w`g4P;N~~M~ zc*Zb^E6C%-y!K5%=G~)IUdu&SSiute7fTjd**ruh=)=vg2X+yWCw>Cs_osN@mpbzU z7aDSPp69aEPiSl&CtTK>TmH3?(z30fR6?JXsGbnzPPlX>Vx-jtiBzAbhWFiR!+JiL*wMnT-(pJE{JNxQY=LOnjP$}b zm5`?I_B|d_HFhPFl6!(jssL|)mc z23mRwfbS*v{`OQCXwZmcKtwQNDf)X=r=ZeJ2R7u}!oCY;2pISa!PO7wI}~)VH(Nsa zZUNkv419RD!&!~r_8ZSmOaGp=)hX~idOBlH$i|1X87BK%&nmM2x1;GCrKe}CHu2f4 znQcY9Z&TxE2L&XnyS=%Ox&TZo0hH~_kCd~*AM)jcLIc^BLOA@0ShWgT-M6P|npY}4 zB|m&=13*$Z8aBCHw%D6ekdc~-k@~3aGS3KX0FL28zNxnNB37|F_$fqNO`*ixChrl^ zCReB@Kn|7`B1|qR)p#uhLO9<_kA9*r^$!AFr8(?{cI6al?hY ztos;0#?cy}Sy}g3^(HMv^Eo&jeLj0ZM_pTfoIB- z;RUc*{wxrm3;F@a_V!O^68^b#7ixdm-J`1KUyPzW%~%$?{BwKpBbe8tiPU zz7tUn@+ZU8w1Ij$%QS|v3%`Ba0Vd;SbqP$a4BKAYlpJB*o~rSa*;vL?#!h0>_0`<+ zIoNE3d+Z!3qmXcZ<urvK7ce96+2BoEd`?cL?X2u3A?i=xM5r?=r&731s+2&j>!PXryvZ0`;xK?d9=f zAiyt}Mo&Tn2lI9uDeOw4Tv~zN066o8xITiJXLr)$s%iwswN|y65ov`Vamv-~FPKZf zR_-SM7x-d|u`HIKNwCk#26XwNbT&Q}ahCF#(#~hr4^hT6Vjn27$h+Q@NpF-@F84@} z`L{p^|J0_0x*@&w6e}7XtMSDOImF!NTQ3Y6ie!;#Pmhb|@A9w4d^MWpj(}RGAZUtZ zo}JtZW9^86Wgze8OADaFM-=tF1ezwbAH4L}Q=oZs*we)1CS1hSWe|7Sg%RRkPm6tF zL+~_;20SwZI_6FlyCD8a7X*7w7HKvUreJNwq#)XrKp2k-Vh#gQ-{yV-W3abvVCgN+ zv!ndg(TR@3_j^^VcGsU*<=H0I6jYGqz^8wpSQ^=%)E-v6HQzU#4ecidPT_ z8jU1TmH~oZ@LZ%BPU}P`JZ(e0BfMriGu-*8K(K!Y)g&9rUq(%91ri3!gE?|7Qz9aw zT_BlCGm6_YN z!5jYh|B2~LScm-jIv^%st-c>z*u2?4e?yxu1i4~(pP9)Kr30psod~+XAwJ)Ui^)fY z`Mr;&&&;1IX)7yHoYY8m!p2ul@0y2&uhd$psV+3a>!C(CBV@&wj};6}r?` z#e|^_tAVdetOSqA5avfkFZ>jgt*Ed9#h&#=unEKiki5JTW*(ng&`kQVNY~i8fayg8 zRF&|7!!BimI&(DBf29N;TeBt#vT&kxa~j{jT^}r`A*sJct<$mIkDVguX;*kYJ3}y! ztoR1`Yc~LfVW@CGJ!=+C7b#7 z0mNKBr+v*dzza96j9nOlfPXTxVG&-?YcTl#-*zj*7URy-F)60s!H{^C_m4&hUiaYs zy=*|mgS%W*n#*N%nYKq=^IFJL#nUG5$rs2)AJM@QnboE+pXG2R6|b*d=ewU+=byEW zj$G%!<;FN1DrbDct8VWT|}=RqG%N6^dW~kqxJ&G>-E6wyPK#=TL`wk;U*nj>0>Ccm2nnVu6?Mzo~KA+ z)Y&IT%*mw{`=)x;>Mwx|YTok0#z2C9UneUk0|Pt{Fl1&R$kn3*==3C6%)M1}n3po% z=C_AE-(T6zYhydbH&r)d#z=MLJ-x^piMR=|^|9Jq8E`L|GqsxCqK2MOE|vXwt3$JI zGzGo~mQ#Cb{^$lal%F4c65f7>FdnSRT9y^i!AsEYD6hrN+$B({?t#quV`;Upgy+Iq z8_30R+gth1l%zany|CtO|1Cq{zUZivT?q&kh(%wvR6+TCKxa`IA`cQ6%bz-)1N8g7 zbxY@|?aCR*$=n6qGHKQg9>yLE@V*(t>dL-M=hM?jI|;L^w&QZ zDmUkwF|54+YPMOs4q(GMGufTK*IVG)ds}rMgVD?Zq}zu1Oam+Gv>6}!PoH&W9Kzaf zzs$Sdony`Yo+@HCO=`i2N!nB&c=c2B|6~EILrGbx6BIHCbK0xz4tAlD+kBlGqk|dp z1=V$uS3-FWs4SPNJ#^2zC3zz36IR_=W)B_1gwxGG2zVBJ!a{>Dy(VgL{qSgFSd6}U8DXuvQd&Po8aqR6D6^4#iCEI< z7lB|o>e$a2Du2~3cMf*{}EcaL7&4Xpdc$~nMtNN%v!Lw_N1 z-rsZ_zuRwLU+)hsQ(qO$VR;LcD_R}Ew_0*E%MJKt4hoHk9#tE18sp-&o{(`^EJ<&TvIEmjv@ zb#kTQI%!+wl+oXt8unpTQ|#s`d1o42bRUS|c8*Hw&rqt&3ybWQb(^}^zq)bTK&v4H zHVL8#9W@;3Ji9ld7tCfOB9C{{O};Vl+x5c`zT%~zkq%NF3TMP5@R@Y$dNT$|>tueI zn%8nP4aX%V^P`b&*gr5eQemY{*ZrW{8QSIcQS`Lg89WjyPcqqHKB(-{ULjH%trMrX zmcEF^)%!a6e#OvCHs(1rq$v_g_3Pz+-5C3qf0jn+3}yE=0+^zKW4D)C1TYe0akt)x zK44;1XVWR%i)lLBN%s4IXMWrA5o6>?h@qF6f=KnPbfIy0 zrus)3BqIkYXb4R|I@PZ|%=8K_Y24`iTxOHlkhnKxZEmJTSa5)?4>idEV2`f8{h)i1 zbege#rTWEq9-L*>L?8l8sTocu^Huy7)n5?uY1}>wvPiif2N%xf=L$f;Vr%I=2~o`jn*MW{YgE= zb)vj#k-3quI%Jg|nDj*<55OAR>4Wj2=ifmOrt0rtfE2OWdjJ@S#v=gh9}tMA5a5H~ zY<-ZvlEC3tS0}R`ka0+o@KCPyzN5!Ct-C<-%x{bgTxp3L%Spx_$!KO{`^(Yrl%3dO zSQDR&;U#B&n-{8i-bxj_KV>S+hVDZMt7uCdk*j(Y-%>+NzGXkQs6*)O`Ei5hT?=)6 zXg9zpw$N4f{~}V1<}18|MJzwR%FMw8(E=k#hAy8>5bI0zXaAJBb|9y8GqipM1E=d} zf13toI`6U4rw752vuFB+#kHjprV&d<-csS$R$KT9qn2R2SSO~j?-M~;6(PuTw7c%U zSCY#nrH>gn4SFxvmT>JzsTz0p^U7% zsv7qMF^0b!Rdwu~o|@VLt4P`yGiY_M=uA@`e>y4VBsxWPnm*Om=fdIip3Yt}k4@^0 z?`A-4>0gH5SKDhEhrJC*ALTsRdr@|_7@=dJF-`Yi%M z&gYC-pWPGYe-67`+n8kfk3S)gBpk!(eVW6iN&s{J7()p2%a35{WTH&Pa-@VmwGHD* zdd2Hw0SoJ~h;J=BVWnHyZXNtAOU#TppF!{mbukR+r-dMZ^GwZ+A)9R&7#!5(vNRFe z`bHfUyfRtJ#IUL$MFamYyPnTl2e@ep2wD0q=)X7YcQfosyu}Fk5-9X68pliC1A83> z^0-?$4gYyg6;m|a{aI752<&n3v|l>w263AZt>S|J}V=&f7S(k*@)1mN`OS1yB$cn zCZX|K$sh8XD*NeB=EX7d5lT=PG4<&yjm4%aW4Qikm0V;A7!y48^kBf>E0CEvD=f-n zRg1Ychw~!-GU3bn#IM3h^R6rBsz_G19^;#JK{a>j`1aU9!1V~Kb?Gsp%kt|5v|5>7 zL?jhNq5U_&_T1K*>pXon-SYjX!^pX%8)klUpmL3WA8U{A8&|U}3Rx78cY938~v*4AB3CSM!yHxZABPW7pq1p1rH_ zuz1h$PnmjCRcDfD`ozL&c#BwOqYHM1DK-}8sqdwe-+16pBLhnzIq1c5$Z$ehC&XZU zLeKm5?V1Ziyx&6(PLTPQ%6Y`Fok7-Zo=4EW~KN!EGINb(le@Ec0AciqFC|Qvl znG8bNX8tvappF&}$|=AZCI-=!%E+ZQQyCgj(QJbLujyzAJtl=wo?hjvB-;@SKdXHk z?Y`RUWAap`rv#kSEeDIjo$dSngr2Y+@-q}-OWzRGmdBn&3Q^l-3gidM3LdtFn!3#V ztI{8+;9yNqev7I-gDYgEka6ZNR9IiZJ|v4)b=Fl3(dQ(SgJ| z%%lfWJ67Xiv6zt$DJ&Y=gYFS1v7RoYBZZW->w?ikiuEiJ01cM-_17x(SB5%ECCrvJJp^&I?jo z3%9{=MHyI~@<=FZ{sIy*ej*r!(m@M?Du}h3vtl$!e15pXR->od|GP~i<%c>A0WXi9 zjRCFBmS)3aJJ-j%Gn0P*V5dw{CnwNNgizf#DnvxVX2>#M7M$&HCWA+wWC%Hasd!4rq4 z(7B3gW9a%EZ7DTB!QHZ!;MCgMenk#PA{hE6{P_~b@B2+j%17{XMKN9wm3#awlip+T2JYI0NUXQZ6%@1SmRtDji3Z0tvPB2oa%BZ^ z6Gq&z!MXV2Ui%rj(e~L5)Q=ASt^E)!Sr3`VK>n<@P$6Ph&8-|wTnLPZ@m4?>h)7>J zUwn}(qZ;&TS^i73OyO9L5G<)c`!%0`MhCh9GmZhhBJU{iddqMscdD!I*SP4j27_DO z6b6?Qq{;a~e=gK*v~<}us^5n%kLT<@mQo+I_HB_f)Ycu1kac%0MuxV$(7maO%JYj5 z%}sDD`K=UpPtji7(Jyvw=1Dl_y9b^r=Al}wQOH3!6n zUcjassnVb%n&panzCFLoygdpxPnKzzP!L(3Fhwni*^^hD=pgbTwM2axCG*64gJ@en z$%?fxg>{$G+zf*CgXL4oJ22gN@ajpnt;~XCwE4{n9kgDIY_|1=CzhiCOjn*|^e^7CwnE_D2YYb}hf$OqF zJDQorKGoC&7#=djy0=?xGd5pLKp}F?qL)3bt*lxvG64!^!E%NNDlC|d`-)9JNA`*x zrj1GDW89;YMQ(9b;e5?c$fxXdYL~t5U?8pd>${TZSBU`M?$16}`HVrGC`H7fDx7?Z zWm>95;R|qmAOqKVTvu>k;xibA-jyFNby^|irLMtT_Pe!oPhB%vT#Q?X_u@CifKHIb zB#Bvb-P+ObBx{c>Za4Qq$ra~B5sCI78UycbPvqj6=#rmEVQkO)E}L~U!-w1T-%X}< zUpyR8J`K3uuO8yDc9GeFfg<-8Qta?mhRe_u7`7zoh;Sz(*8PCLc+#R>IOZnDCp3#j}x`ft{ZbhjKTr{YcR=B#q9!=l)R z*SESejQEio@0K_dJl0qZSU3qt)Q4+?TO$~Fr>K|HZUU+l6nj#lQl8ZG`g+nMO}Xft zl^OZ(%;e{3UQe0Tdx&)2j!DJzminnpQ7B zIEe;YmgkNOr4zY%VESOrsc`%Pbk+EmOZ%xih z$w=NZ^hP)+(_8J=m^QSvE>>v2`CcNLK7u{TpZwKgH}>(BGwHsr*doLk(Ejrja1O)X zCi(1Kef|+NRU+Wb`=#T>&OpQ=^+_Qmg@J`d*>@drqPGNExJ4&VK`wLe^`B}oVI(gX z?n9GsVJl4iir8)zl+0KHVR$cmbn7_qIp1?w4BGkXJEyU$(rdTK@R7ghN?K#)aQj$! zwmO={IkmI0F>KAD&;xabi`v>k_hZ57P4~~Y{73oo|60-UWJeL?rH_5zTZj@pFK;{o zX#<@S$u|nNmMJv1dpeDe{u9rcprT1Z$Eh7_y${1hrZYjyJ?U{TL{`hIjc ztS85!0^5Kjdl%I6FmMv65&g+gNYqnPth?&n@xS&^A<@EOD-N!I(>-_Gqn z1~UOiJNJG`4$>l!^d*EHD<=o5Ym5rESy%I6aM?;V)_D6H$mPCqX{fDL;n9{7REbT9 z3gqf$B|wD)t94u`;aEN_Ds~~bQTd;QeZFtICv(low4PzGKqeJB$hGJULS5`s>Gh1> zC+jJ__RXdj?UW&kPV1g}r=a)V>n5lB1cSvWrb!_(WCK)=XcAC|rG&&7lHl`7WXISq zHw*R9hEH_L#v}p`S~&eLcl@CqIM>sA<_Y2P4GIy-kuzEKvl}B4`~Z&+cDyn{Psz&4 zvYyFke+0Ig=!AsyG|7vT;0#4L7d1PCHSH3uGHXK_3zBZ(hJgrx>H@8anrbs&nN)X! z+({=VN~(4zcENcIx-SxEz!VbrINC{BZ_uOj2dcIex`RfeFowwf`68$s^^C!Dit8YoPf@VKWgLy z!BIW=22HFV|2=s9ZV_q+I!S>*JicVWpPHTvN_gxE2eiEs>6G;&aF+JfqDWhcvBZHq z6Aoe2rZs>VY8a0Xn8JSBm zi!n8`Tf`bhlbEO?J{#(_^6o6lH=K0gdG!n zRR;r@LU67&JG0$wWIzEBCMzKhs4Qx)q`Y#vNN(h7WWCi+sG0)NRassxS|P9b;1v&M z6h_>9rD3Z5L+#ohl(T{zNb6QnxBUSKl>GkI2nGBh;G=djdi~#!H7H#)tr}oY%&X(^ zyXB2ZxiKmhL24~YC8@6OPRN+MX&yh#i}IV&M*$+p+nC?|`)(}@wHtJ=z6Qq|M)>z# zMibOw!VzLZaQ1nbdYv(qotVgw>v0S6RL{N%jZXkAynW@p#lk5Nu6oL{3XmAIc{n1P zBS2F^=KA_s5UY&WZ|u8{KrHQr&x)(j@?S1{1fXi~Z8VZdTfhE0aO&@sQ7?6wrZ-&g z7vDZBTzi2u{GdVsA+G|b-Nk&w<*FT-VePeg(x!Ca$Kr8dTB`Y> zaC^S?RG{YW5{V>3P^Ohh)h_J1zSH-2 zcH^398A`!r=L1h0lRf@kfn*v)0mx=L=Q8tNwF$b2>!_En&!R$MxmeFsge&n^Lzm1kLO+;G)u2`OG>CfB1S*l@ z#iju^3#_NRT`0l|S{pw-*NdHe^Nd-ivWjb2cw8uQGu776%;0(4rpWdr)nJiNH+7n?`K63oOf_yS4%`%c;Y!8rIL#@uqQ=)C`9zD<4OtkB2E1{Xhd~dTh^W0gU_)r_ zUoUV_4(2hKTx@8a!D`P)0ChAVK}QTMaj)fb(0EK#4!KP<+I1*DQ1l!e5QaU)XjG5KkVi|29&ZST#3Y)l2nj zJs^YaRrMP|L4*F`)EY*O3@g_05{Ut4eF2C>r@S0I^T}oFVkn+|OmL?Bqi+6eUSj%D zn64LUs7X;&%d@?(qz|p1p4SUxk6Ca?JgH{-ufGVyQ)~AH-@gEi{~2&V8z6L92!;^g zr4#!Ljl0$l*JL!0Y8P%|cayR-#et90^dpcRB*s%uz6l%@$J*eDZRfwYi+YXN)?9Da zt7d=!hI{)^7NqeP`=NBXuRoLWywUM>TzqvMa4IUvRm&pgc8BFa!q7e0@&f6#1(T9# zp~c$CYSi$qW#r4R<8s*#anGUOa97q~MK&310ofc?seTq)=N3prHufstX2!K-bKVgk zLBQUbz(Ef*CGsFL9}{F9sQsA+7bef9lXEmF0xX8R%Wpwou|3N0< zTRVu%0e3sgW}O~ytPS4k{9bYF4S}>BrC@VW$)Bmbz*TpJx(l*;3xT%J;p5gGz)`k; zo$khLIqi5Gr7^41%e=mcB7X>#92U`Gd`n&OSG`}qXuVQAN4Dd#!fVz3j6u($T7=VV z%A#{su=8=oi<06WcwN=cr=a;UY^eOmqR;<#+U$iR&F(`P>GA3WJ$I*+buI?mD!T%1 zrqX!KZ&~vn1<5wOT1c9$#_01m`@({8%ese&_YSm06oK@lHX^V@7!%THS>}j3XGvMi zVSKeFkLW3iaHY^ZL%lU7YIP80*yp6+k!gFl@lngYD5`rJB@ zuB31(Dff42^k7cWJGU@D)>6=>tMzr0y*x4w=Iy$)8&rVvXaj$}i`7+q7e>D$2%_`& z>D)~4`H>??t_K4TpMwEHw+FdGbc&3P4!%aF*K}zBGOdr&F$mLo+j=w?Y?|?uDanWP z*d1jW^p$C%cyP!sC!m4U|9cygIld9s+nS}6*Iob+3 zlpw`e5$z;ChHsvBM{Fh~F26_&myE-rr`uPFK1nB6gygK7O;mT~!RC=cJDScF^*DUSm%Rm6Ky$=dcp z+g3xc$^kC!0Rn}H_QLS&K-yJ$|1L-|D#{PMj>4tEbT)RVPQ4xvx@Vd*ppo^Tr(Pv3 z3pqu9p8?c@XemUIi2jeKH|A?5C>uht({f${ukBqCK1Ax3ck~>9G7($*qX+CO*;1*Q8oq&<+^6~chV>Xv#6tOF(SVc)cUK~0)#)TC2A>b)UFZ%m&Zbj z>=QxtHl)@9h*}os#ZMT*6oWBHkZ`4677Z^QD9DH!;W4Q=+NuQ=iVWn1#WsDl(PQfPs$5|AorzAsudU2S7(z+Pds*J9QleZiq+qdLx^WAjPT(wG!zPl$4#D?|SvVJ6E@+CcIt>8;pn|-+lIhT>C{y;W!w&?Ux zy=rP_@`EDhAUMsiOt$VfjKco(dSLy}SBN;Kju9f5m{^u5k5J@*fK>7O(N=$=-tM7*rX%io_F(OjjTj{GT znRXB=Q0$56{%4`H%9OZ9z07_hA!&DN<71j54s>_)DtEv-g!Hm8hz0#uE>oj}j)Vdn2L%YpcY5&wqBw{1HH*RjD~CGjD52Q;n}f{}U(G#aae z{}wenUB+eC)a*n9*hFW)6I;JOnnGIZAy^M^4y$O=B`0D3L#zhh9^CyKoBzs)=b^ik z>wL|m#ij7fw^*5jOCVhCC7OOn>0IcK>=n|9PZhkPzr%T}px2=b;K(X5A?m=CHbnnE zElmXxQ|-Zue6h?u3o_4x0C1Z|d%uc=;%8C#rP~|rPfG~F4zM;S<+MfYdi`fB;RO;H zz6%o;@_^^-v{GJLgt>rIClEdV!9)JbHV56G|G>Wf5yUP=o>Yn{IDiI>=gcyPCi-K4 z2ji#3xaOutP>xnm44{9K(#a$!AVtNI$+2sUrnH15LnLY>WzAxNQt|mIPAdj6J9q1z zpf8wE@)IIwsm}fuWJ+vJ_dSH+AurET?EGvR$~sA}W_V*?iFizFQbq zIF~H~jYsfgJ%%)-m+9=by>o%Grnc9Vv+?V9gGpi#m+K<{D2>qB4GavZYZ`!qF*8Sq zvHUInPh?u;wfh_mo&aL()kadC?Ek2?2`vDUr37xCk6 zigFhPzefJ4N{(V$8WC_TlNVH1{Rs?mxL4LKZwGi!G7bYkBeVwBvmp&r zxHUAo7>4JadaHtQ%_`c7Gf2K|BgiMSGomdU@wf&$FnuD4X@c4C(R(NK$toDV zF9(uS^)k$izyaow#m}GGz3_X1DAHQ?#bM_~o)PAw@Lfrkfh;Sp1#n*Q{~@{Pa~md~^!QIGag@8gZ{CZS`B_B~4OQ+J zbah;D)nsLFr2~H$Xify}%@$X33^%BIJ_z}pzd{r|BcdhRo;1pmK~TeO>-7Yz#WjqW zUYLiWDW^+z!C*t$Gw7)6{;C zQr~FJ)G4mE{RX8H{1NqxnjInI;e(l`tEh2F86YCh4F{+6WWee3XB5U)c36BtCKp7! z2_E`W!UBeI;1o8p8whr~rsA=lcp^k>7-)aUAbAE4y82n8x6hpWDzU-T$yC0`D_>k4 zWt*??-kod`x1LplfVG)rM7anPCRzeZ4pYWw7ISr5+tgvIUEa>IvRiw4J4+|2AoF6Z zY&@-|s881K-R>1iD_B4=p6&s=1WU`|i^&8ewBKb3m-ivd8D#xjWw^`Cm?CqIaRzE; zg+!j?QRw`bDidfDp8QdmXg%HQ>s0|jFTZJg1Go_#Dd0myKIAiL2Kwl3X3o0jQNt!6 z9HrJ$stve#crrgb42TP`QfU~LWU4trrs7`;Ha0jG*U5PjOgFEl;xVG%tg{LzCQd<9msHj{_;-EFL1L{UI@w+~GyD6@D2pjnY2fefa6}MAGh`WY#Co_MC&c zmgzxm7ZNES^sL0C~PcV_S@-;Et-VBD>Db=-5TYUUv;%;&B@5=Y* z8OBl!T`83gso_;GPd6GlDidHD!VerHdQuq7HhLQ=ER+%6htI{)AY!2yfg^#}=1o+f z5#=4MdqC>C9r#0%5Q*?62R(kgD#q=N%A9SB$+OhbO3N?4mFT8dr!Z>}Fu&*ONn2`4 zT@(DKShT54GU&N^hW5NZ@>f@Krm)S5-JX;mva^)$x4Zov$>E_PdjI;qxfL{=FfJj| z8dyeCScW#4##&6wLY{J{i&uDy8qsTkn79Rxwc32clfFT6@1_A_PA)a#rPm1SXx(hM zdWvBlreC@{uphgrr~m>0AqhX{XG}0H|3a(zrOY zIfKt{mjkvlsXSLUKys0O^IC}<`IW`5$|qci3RduD^7NLh?R$b`1P%0Pf*Ds8tvaMAcFc||?i3p0u81_?c&t=7!#u;SY{deTTUQ}$VBOn~&2 z+BB*bnM!$0W7*kA`TkXrWJ3E)e%6~8CjU{gsCYZq@9S_252}1l-m&3$wB-8Qk!%D? zvhApmp|NeD83Xr|Hq_dfdb&Y?c41HvG-N0iXwe9`@IB=PRmVTX@_ zxoVS~D8P`QfBsq8r&=-ht@W%cW8^J6*9?Nh&RM3p=e68{t{AFHZ1k$MqP69-Az5V;>&#gX5`@nibt6RGwqH5R zMrH>U9F~>KGs3b~43p-?`$|`U{2o#GsRwec;y89Fs{;3CIDjoApSs^Jf#z*cxd5Kg z01O&}R0=FL)`)J@o)F+cSQ8gT;J9&=Okawq2~L|H{eLuFWmr{B*QUE0Bn70qySqye z=?-ZGq+7ZLq`O2Aq+7Z09THGkL>0 zi-^l{y&_rEwLitLO&J&PgSE?=s0GZ=56IAdpwK&U#$!XZVINbrWpO-BfwsiuaQaf4`M{x1al;(54@)iEX z7FlgkaKhH2y}-bir}?NDF?Rb>a~+HmPDd)=p)m|j1rHuwz$XUEL<}^nTrM@-_s~$~ zyf-Q-EuDsRg<+yPD7rW4F7BwE9NO+KL(1kNnf&gi2+u)} zI^39?x!^fKsJ}zTkk$kEztd2Yd}wAo7&k{2#msX5XN`IbZBXW#oZeplB_`^hum_0Crbi{-H} zpD@GvP$>}P^aGywI@nn~=N3(ca=ow@c7ZZE#>^@3sBWmiMK=rB;{gxGRq=30i3*Y}x8f%j_Hx_D(>Q~t$MJA|B`%69hRwWZ-s;M%-=HB*KV*ceTlPH- zqVRYP?PAiK0%zY+^hD>lramDc~OT01S<&uuabwdPj+^!j+0b#1ht$m z0e^2N%2BQE+Q@*07SQ=uoZHp@egf3WTHKtPZ$JMh38l)P`9!c#X`97YM9Icp7MF%M zm?q9eHW*Cu@~e6%s}c)=V(RV4gcok8V{N14hQ2%rc;cOH%aMyzw!C(##cJYk2jPjO z)|Fw!?QVdzx^JucT(@l}i10dil`;F&u@szrb*5KhN|8>h-jreSl+4g8!iR5A6BNTV zj8J0q-P|z;vz zXdR!zG}r!{{!+59EiEO98o4KifbFjuX{Vu*&BA<3DDH#tme}K!ct&&Q;DAHi5ki82nT+lOv3y7+W55_3SLTKn?uOGxQu($XWoO>%Upa zp^b*P4@GtZGlfE+uEkqGH>Ca&K6%0hIb{6g`*uQ0?}q7!(d~VNBT=j&}e4c5-iso^OxpsXySW!eHiKtfKI}boVvw+6g@o9mi2IAG!Tt z!Vn@V5fO2bw&J*0;KUz6D^0e6wm)VYy$b<;?5i24=Dg>u zzQqUdz3hH=eWH?8Xx{s6cWT;-E`x%H(*+kOsufDoP zH(LCHy?0+?UUqv7Kl`Cm6VDy_kXcsN_MKU3K{tC{Ekk;op*mU7l8{GWTfTDKtr@ z+Tc=Zl6z<0$E$51?t#WyB;s{6OiIqP2qKx?@sCAL`mL~^>OWlA{8?^f0XlQ-Zuf;@ z6oV+3vOFxHNaZAIkcN&)G$fvQ)jq#0!!zJeEkPK(mwceQc zxX7dCh>U=I)&N%vr1{mkbNP1JZLQlM6%K9gjy0i%J4> ziBwDRwShshM=;1&QeXi#wKHGEB?8_hR9@5ow5`YVRhpvFz71{RCM_kXpYRX0B;zm?@L#fU zX}-@_funcXieyDbn*7rxyp_JU>hUY{e&_&mpF~1b(k4G2|BdbbMJda{ZwXsSedojP z?-dtRf~djmO8_p?0YcUHRW`=2a2Sn61YN@1dSRJozR6<2CcW|*12Ng3XV^eSR?7On z4J=;}K+`t=U0lA08v&5{Y06VIvia52W#)5n2ct=TYNL0qF2HOwD!1^V?pl+@R#?{&&HE>jG+ z1DEsj0kEh8s(Sm4Rk;R*>O=^e0nZh{wIBc}uLRH}c#19O>cvEt~+qA7WxfMI!mdzyA}`z1G%_h69nZnMw;^YT7$Y!Xt~`v8o?ij z&L!S2$hDBmV2><-5TRNk4Ko!}a(eX(Dqtm!2bpd=)NF-&C<3$1^EK-5D@N)QENQ8p zs1%>*G{1>Q=i||?eiN^B#X-8vW)|Oa_Z`#_!~P?5CQ9s^m)|PlJ$sQ_KC4)_%fX}a zTqf@-7dwgV=S;wnXL*)zAGT3!jgm;UR@-qnzI_kthNEF1y(0L0iLD{VlrB|mtRP*EsUbh3g!HyBM86SjmAZ%slD|8 zF#zY-__3eA8c2L%?P3=HphbKCi*RD?u)L>gZbxK50=99ry; z%HehEPjHpJm1G2CW~8s+&C|L2S>k4S0$)o=D5&R_VH2|#>X-jhbHv)e(bjEx_vQc* z^#R_z`2&$7F3LbXfwG~aBO%2ycs909qav!dKUH5E^c|ox7@!%}2I{_jV3RgilMnnX zUejm-3CcZam+Sy?GS}i}dAtPGpK<=}6J~sTs{0A$-CjGr$WPhCZUUe2OoWY%P1%$f z1+l(vrQ%6LYv@0{VrF`jDH|NQ0y<0W5>#20Xi?-!ZCK_Mx1CEgl8}#~@`$k9AK3r7 zeXLZB%MTxt+rRMr$4?=yELJ{MbVJNq2+wf!tj6oKtZNtsoFE;mbSqPQhwmy#Lo|mq z8SVM>X{+XbSVL2}Xuf`j8gBepDOC>{+l6AL>UzCi2tU(70D&tU95CM~1z|@J=rA*| z?tQ%Tk6Wyo8Lsudmc)U7pa^QhlbU*qwA{4geurp)h zH!kyx_#LO6w8m{)z!^v&zAi`r`-Nw?;e=Q~Sans8$}`Lr|2T*nbTY|X1LbE_|pR~A#cV77(hPBpAq)|%!+)i=lYZ)j-E2;mGkVwozyAGN~w zU^ba49qUFu=z)b37tbX>Xh3Rd6Srky0$h2;;eJ-^+Mj))4aE0%S-LOQEL*zQ=D{}}T*_=YLPFi>srlI6af3n#Wt6C{dx|w5QXh9+| zmY3g%j6KZ~G+ClS#8ymeVM?|ZTKjR|2!vBvO52WEz`>q99)5eJL{a0N7_Hs^4zK0i zpoN!DCA2VWY=0;2{Wr&G8&*XheC12l9^>NnlV1cyTY6E^{_#`#9dR8~8gZ%jElalW z4Be_FBJ7s+sEynk2{*A9&;u?4 z_VNgjQ)jDN7I!9&E&Bc+*X9TYd$!cvPiv2VK5iHf)DVEN8@9UBcLK-N^dqLIGtxnO zY6p^LAxTc8K?4%YEj27gm!GkqftT0`idl*8KS4Yb*^sCb`wOOFA*U~UL_Iq&JXKmt zEH0H}V-MzuGBgq0*wxT(7bmKWY=}N{Mb>XEA3E0IVs0|W-v~(yb@t8)at9WQOMJHd zesF00WgoFF&|%v7>A}@Z-~ohnT z#yXzzKZo!^7LxorWy-j4&x^Xl|Pc+#JOsS%GF@h|3=*dZ|JPNJg?L&FW-1bL%aLUMa9&( zP{UKjKaP;hvJgJ%{iw5np7dHdc z>LiSYn@*~H{U9w~2I?u=wj;3L&H4Xc0JOAbehA+LmO)%5$KuK6i9xfOO9Lp%v}+_V zZXWo;+f<3|MPjMi=P}@9;~d|N)4<%w!w+DutB})h8e6*ggU*_j>4g96({rkviv2`L zpR~lP;TN$`jQeoa%?{}|nf~a7iW#xS;DF=6n8H@nMK;z;5-`Wrwsnp6u7+yKtf z%mHAA*KtrmoOPg&!`HFGBH4E^;}XrUF<-!_tlNYC69nH@0)LA1#R|}?%ze@47`xcS zAc3Vo>3IVIo({4k_-|-Vdv1nl6Cr?|QmyF{G5KEl;QQ9i7sAnEV+M_5o?pE_8>jj| zWgZi<^zpmWZi`?05gKqQG;2q1%{{~rU2c^UYrWz6gpkf)9mefY$zP%O9_fd+#8jGO zI5iW1n5qK<6Y@uhc7lY|R$E9la=n=Js?v_W5p$TR0xf={*OR)QfwF(#=;-(Zu|{;7 zK}Gl!w)wvqNd|N+pqfQdr`sL>j8B9&kO@`4!#L)DJ7CKxG>|IdtkL%K7Y>&-KZ@aE z9jsR^QWYO|A9vtpW4NH!l+VG!lTa1+Mi7elSO?6L;-~+~ScQ+a9+}%8aRm}t@ey$M<70>4e1FOQ+j5&C za!(GaEny4NrH*;;y=K^ASs6#hr#zYQtHrIj^iI(DE(({HanCR>nN~p?^k_LId}E`g zz~jXCPh~kY^a|LaS-lZ&WwMFYb*^yDG{)&6LDta#9fw1-x1P_hzDr=+bD#8=&5pGf z=P*w`<`>Ua^HhZWAF7BxdR!gfq+m}}9$|e6ky?QAZ zt$d{u1EJHlQj~ql+WSz#%Ya!j!omEhB1x$4uJ)wsX4Tsm`{ctzhxc{BuYtguJp23r zalib|#Wuf_#;IwDD*Rw(q{(T0@mU`km8PaYGHw%Ip2}W9j@F(}YrCJVy?$jc>@=k- z2V+ZD3i|QFbHY@GS)gjisjmU77VL|B(!0`cQ7k<6%o5^L!4eNwhqfK|>N$d3v!B+2 zsp6+?X+m_=g$B}Csu$CiwWpcwT`k*j<5i|Zz4afm0Hq`+*)<7Kyx zIC7vyf@l(=pq%R== z;{I-T9|Xpcl0<4ByAV22Bgrlqkr9LsBFGAgrS(eUfmRv|0Fd{(Ec;NR5Ot=)VUuOb2k@V4+7M@!dENVKHQVr5n`}k70v5RlnmegF%5Lp}{yvr|qvFd@1~jBoNpIF{Ra_nXfT^3v6}n!iPp~ZA|o#CY=oQ z%ifB-Q`ml+Rey;4k?ZdOys%JG4!P;K3*m zK%?z?xDX@$12h1;ySf7m5wx3~jSnSX*P5__p$;L51Mn5Cr<9%PiXus#I|Ce-*leOem{L0>Qe z#LiZ?ukGf2NQVbsjjKvleYKHWE8GH@QaBQsTan+hkq#^hR6Tys)r_QlIF(C{7*p-Z z7mTLdzJh`6&=S9w3JlzgV&SmAZL=ODvfTGP`Z+zWqtr)#APiVs%uu(&QE)ZWg4`VS z#YRyfArt~8ZCPkq`1cAhzF9d3cQ%PP#6SZ%hA{%ch(;5LBU?eyIZjSr9dG}(V9$&R zc3%t*svVhwAsjKrN96GjGY&87HI#m^N9DO-z>5iZ zVb*Gx7bf|$-$y*EepY6ZP`H@<0r?TW3(&lg!T4*jk*!4tcpc-cf8Mt7rhh4>6A2-2 z^F-M7G0WTofPHoQJ9*w4_S8ut-p-`r{w<$rqcl2L_z`_E zFJJ5nLA+0E1gRl3g(}kJ;m%4UR&ZV_=C2sUQJrs!#bpFAjd}f#doMQhRdvm72`|8K zu|97H&QQx_t|UbDctMFQjfs$_;PJ%|o6sKW9`DB^xNRmI-OU2Z=v-pHo^fXny7PGO zwfNsn!|f0!dj=KNAp7;dY+2~GX#`B#xLekc|Eh@}^(8Oi;UWfnmQ5U6pPv zn_)wRYOb)}>lz~_(1ogAk^l8fl?0n@2Lw0p_d#tN9iC`F-V(78;L9UMlKBcU(QTlG z0CVunyAapN(7cx`a8HQ77|`E5i-2 z`-CiG!Ztsk>8hd6`XU?^KMwi^2=H7ZU_^C#(V?D5*5rjh)demI_t_w(1w&x*Q43#4 zD}>kVhbL4VsQMPt5O%Symne}{7Zyg|^ksB}>4Y~bXm0*!pL@K!u>K|9egb^RmOTyG zD88?5(;eu3t^X`xT@5R!7Dc8xC?z+?$tkAEndj*zk?^p$fxjr5c(<6z5HpC_7q=Lu z!@q5d7P2P6=?PIZeLt@k@;Dr9s-<6TK%-Npnrtx;v!o_!iJk+@)Zyb`--EY}j={Wg z-jA z$zyM_@gFUGS{%3*mR@By&=b^t9AALHOV8$cp502dk^gAe1N zI4PI~0oYqRrW<}1aLG$!eKX@*Uzi~&5mJa0FaR6!7nuU5joMRM83h&Gf97R$bO(rQ zHRYSHFyHcpo1C%;G53S1o$*5T8%wcHJ8FmcM2 zn3r4DtI3?XAn5#fpcPG~Shr3{9!rMybMJ+w%|z9HphR?s$>G(z-l?gJ@!RmiutACo z{U(=R&QIyUWk6sQl^hcotU{o#zzIPeL<(=$-vPZw5-0}CkBdBWu4Ys?NHZofKkh}1 z?Rtt*XsEr0vvUOOZq@Ivm!TO&pbc!iudr(_bdfv>E<_URhYe9jUc(Zm^c0JsMX*y8 zO3^|cF{)W1;So^`!w=g4j?~^dZXjC6P#m!L{;R0Y9`ANW7ua?d*cp^3wuH}2SR6=T zl7@@A+-{=&CLTvmrgF=t2IW4F9Dc{_Hux}H!U1|4ZpwMc&%WX;rXZq|TnjnD)(ACRAu#CqWu&yW5>B|xzBGJcTtl*-b z7bxN9h5OF@+=?4QJ5Oj5T>#ECcM#N+ z?Td18`wsmQb)T?8q0km=u%&`_sgGgo3T|mIm`n;h?CeWEq(gQ+W4HdR|1^W-B7pHb zTPv``MC=}4Nl&fPOZM~iO4f0hPg?yWzLx)B6-WCkY9_l^k$dP7zX&y&)MVP}nX^`( zdMVhHcZKH)UY;N=7@ea-)68gzh(znuun`m%A#QSY3NY;1A2DCpbTB}}x>rBE1a8K}H4M)yThs#EM{rs4*Zf1qlpcyT4*3To26vBeiKc+iM&`-C?+n>w#KJG2 z`R%$(Pe-xVSMK0i=11ZoqxoY2)a52%#sAwy;_op7Op5Q_o5^%1Ml)t9%c7x~OwBN@e&E_3V3*-4IQjtz(RMxj+H6R*lDHWF9G$yJ|AOi1 zB5E4B@*ql%=Zw-huu&f#|IC9Zrrq=#lr<@X@h@Kd_wY@)oZH z1kdvAC;=<=VxtS&wn!6ki+W;4)Y;CBvYb)zgNKsqc$ACQzBgG65~XcV__X zykwxIFigzHO_9_@;JhRl6BPv-KxjG|sKfJM0$`!f4@{D23sR*7!V=__pfyqoBhNM^ zB2iB#nS)(s;SW^etdow5gg3T(z}7zr)X5I@$I6}Dr8^1ExmKFnNZNERdBYt{JgRKI zRD3yc<^)f?>dZxh1lN6e+=F_nUBjFLCN3zlt#=r?+r$%toB?J}_$`QlF#f&geVbEA z>?Jghwng>hq338aYR|*^&PZy!Ko_+Uzsp*i9+dwR(ucN#`Csh~1j3&|sO6ifzu?+# z$a}Ys2*LhFNp*>cjL=5x`7MjAAXP3Xcittfgho<^*}k589{ER+b043XR0OBm+qj* zN&Fc9tEn@IiS=;H!1PZIw zdq~}cVj&sFrIaA}i*YMaB_0S5ci1m8*Yh;3kZ5|hWWj7x?fwsNjNWQE{t#`o zR{TCF9Q23IUbCzrn(7)avhwruvldeACHY-O9A8k|7{=z92pJ3 z44?tWRb$|bhGz)8S%nmbt7L(t)MDr);u>%|1vdcw2P6hj%E#aQGcX!l*?6GHz5o+g zeBC)`4IxJLt<$rSNsy`) zNeMRHU!FYu%-aEat~pZym-Tm%&62G>hH_1%26YzPPRqP+pzKc|j`q4#AMgguyipl! zF*=ZN*Tw6SaDeG$z1H@g8<@w36kdabr{(yZx@5^Pw}g3q*19J4S3YN7tf9D#Z(^~gNm zAT9KHVVh2`FI4VL_wzr$!-Gfx)(&q2U49g~5#LZY`AmtR!0(w{1T7wuLPHkg z$rWCISD8Q#An!VaEjsw#eaSJ|*LG7!Jc0w+qPeoFo_8xom5+l0R{t zsWh~1{J0fg#)$V*nKM&2ZY^BGK}F7+ly>ad8KWX|df<>2(mTPCrJ^3t0VtY-j^su# z$Y-?h7<`Os24Co-3PsGr0tQKKNirsY349V9BAS85IuP#KcW@<8h`jbX`e`l}D6a@7 zg)>}uyp2d~WVS06Ps%fR4>2A0lWtGAra106zMGcfoE9t;k@^_Qj+h&hf(gpJ(a6tv zr^77o+jAqk$O8Ynf>oOoA}G`L@?L(p7>#;8VI6a=InPG%hImjQp@>2=Qe|8|)R^$N zB!=V-?lv032Q6g6j&JQ(h#tn47Zkd!z?((_#}P-)) zCpg7y;^0;^U~UFMm+%Sn{i+~!#7yL#HM$mrN^sjwlD`3_kqs`t)_s8v<|j44gD_b& zsu?oDx11&kG{nC_KZ^D4146@!UAK7!VqK&a3@PdwiZGrh{R8?VVv?CW>Hkm+uLCTD z3$q-g>JO)xkx_$ROnX@?_Ul{^z?2zp_|Gmy)$0^L-}j$*{>-Oz2zihZ?0@K$ppLoZ zmx~WWuwv?iq20SD{}Ax=_D(33JU{Sc=#6bIq2h@GVBby%vxAgT18Q_<8+rDE45zsY zEt|k4a09>V%)smn2L@Dp7i0e`sCgh4lr#*%i0v*dn@B(ysY%&5HOV)_Tf#hTl% zEPnn}wx#4N_@_akW%f^ZMR_R8j<)!2ZcVlU9_=RXNasS?1zYEbU=C+~OaG14o+;_l z&|z^$eNz4igzgbu23egJln2Da{yAWJmw6|*IhD z?}E`tz5BJb_w#{gEX*=X$E>SKn76QLm;Y-j7$>@A+lE=rIf%0eH6)|}U5a6A83D`6 zz=i>|`!@LAhk;|ftxkLx3#VVum7alMPgQ24_Q67Af*x%r@fTv{x!dng+Z9-lbts0A?}d;%6Oct}%9f(oi7nL^vQ6evIR-|I*(g=!;U zd*5D>ok$rVb_aKZz>{;p;XvAo#>pc915?l$=MnUcemZc^mjncU#KX^ zP|7s$%NN^p(4Uy0}bwprcXeymK}h*0SlV~jpc9ro6ZV5xs1dDNo7hk*Wyapu5R z#VL>23#_4q_;XoufTYXY=lN|{8+3}&a`^1oT;qR%^AR`uwi5%Eqn~*us2ut<|7(E( z^VVfotZ1UY2-1Xj%&~0VFx2u99*zRJ4a<{qUz`9>kQIEK9|!uv+z>X(ZyYci`8sf# zB^pepSHhytNaUer3?o3(|Fw*;yTeysLSjYW0q|k4A{gp>?1wG-N;z5M= zEVHkmgZ)6w*&e1`>d4_Xrwvj!;|<+&Pwk{;Ce&x*P51gg#ZS;Q5kSd$C+aVyG^RXK zDdW!3x`Vgv1RT1P7mWE&R+=?zrt+LR=zBF++A~y!t*iqt*6kUR^gCw<{k9s`Qsb_P zj@g$@8~T`Nxo7eQSq1UiFg?llg^jYQ(YbpD-p(Ysb1l~MPe>dbYfQTqMRL^hGugL4 zfG~R1un==G1CODmov}|U?C)kY{~oUyXX!^VCvb*twd+4-%g56O%~I`V)KhduE!yIC zr{yT_x@bCVjFSAFaREl@l7UviES4B9eOWx3%iM1PTtx>39l-3I11Do5mY9QIg|kP~ zqu<)Y#uoO3SR2mZ1y%yxH!Eozj~COEMV%3ak=Ki`A?)zENYG~n8|5<$W+iX&7O-$% zpFg%CtATvuf)>gkN14`664-jNvb?VTmN!@*fi@bX{*GZ=q&b6mAlBiLGlmm+^h7Ph zt@Ocj<<}kq+{!NRb7oz+Pk_z&#l=gJ0O^sCLLGode*kfY)e^qILT-F33joq7v+5!f zzK@l1244;NK7eW34)~bT0X4SFOF8nokn-SHcXy=%r*4uoTn$doL-5U31hEND2LsqW z%**->-+eSdU&szIFBp?7FHDkb1J{zW0JjN_f?0Q9eobLysC zP!Yl4`>!bkgaK%6G%FLEXSfpTMR3fttk+Nrz$?OiR)}#ypJYa1QjZoW4&Lkj^wEL~ z3G#g4#=PR|_z%I#TnUaW1p*Upph-XUc;WuXzfxS-#la?@a;Dhky(OKTNU_C5MI5a4 zzHA&VkBoBneWykod0fO%F^KilFD&R>tq_u;olhrV6_zCLK4`iw| zYVx-|_DFwW|LYT%x*qW)!(n{$IOm+f)ETb!6WBlW%W4hzb_x_Y`*aYsf?`Si!lYnA z&}>zkS<#^)Y&}_CX!ZTM=dknPYk2o<^y>)jog$TInAD@zc1eN`YSq%Gx8n|wimQRH(5dfFX?}SGp473s{V2Rg$ zIURp*sp;bnxc9s}2LwUDEI)`LK3rurYSDyROZ~VPXhM{q1>+))Q^}|+7e)X`!}^T$ z3EO9)M+~$$p)V^mq}p-RIcI_66QK>o6L0?&0p2%o(x>U=#IYv8Ji`2{0`MCmSq@1U zhKmdc6-#}7+>-#)lWV`HY<*Ua+4=Tgt(b=@A*-#lO~F_cF%uuTjoI!Q9Ei#7SQ+IA zG3;ZPaCSv%rx+#R(}T3>VH9uy29}|tA*KgrO1Xc8NS9C*h(V@Q7PNycBR+CO6NTUbG%!oHqa@gLr{&Hj6@}4}1IUzUdkcUs zt{N6zsHRzVebVd*zdM>NbH7+idvkTblv}0G#jo@3A3g@{rNE}nds%-n>q&nB^fvpqL>^uGhJ&cv8}iT+z(Je z&j`?0CvqjpH><#TM(Sn{3lD`hgnT)#;D$g<4k|U-8N~J^LOiBgmTLHTn+$tg3}0_B zi;@f9hU|%hhKc|HD_t`?Qa8AeTF>_Hksf^2N&vB%0M3M&NL2v8tM@$6KD1}e;;1#6 zE-O_%nAiK0&JOI}xBRlrs7uGAVo3A`hiB$`dX%|Z=13#{@<>1wj@ZO#>L(717i>ho z-=6G#!&bxA#8TP-S2^FIy7?^x5X{Y<6e2~1%TJ-$j+lBF5+)vFG>IXKh8ZXnN_4*( zUM1O!EW?u+Mwu&f&H*ULuVMOB7;o4U4w_91tzU^)4X8_1GMWN|8vN{@%_<7TJS7X> zN)83rz~+WY!-W7$h)~dCojRnaugzGY4*^sC@d!$wMJHob6?_K&;8QYeXV1qHrz#nX zj14!jm3W50?I&`}v-~FWGiX9iy`hp7J2@BI5iQ11&X_SG~ib^6OfF37f zw%=ekEe%4(I~zvQ(1RN?VH&HPM zRUuQF=tk6*=j=w*CqL`@d<{ije_`gCGrotRxXutMe|Dg(*m$wsvk~KSro#2*B}zP3 zDbs}nJ|=<*6%7nKr*=SK0)f<_HG45`@31|vrGNbO{FDy@mN`h{{}vo3iu@IvWqA(? z!8q!H(6u-WYC@C!-k#`4kNi3m+PZ zEsWrax=#RMW28%}MDTZF({UNiPL@pTIk^{-lyStQQn-n@w*zU0x+;*0FX`02OCIyfeiK*b%W~P4yI+?1$s9I$pYBEvF)L)w(BB`RyKGSg zTxxxvoBVN3>Ba@K$hh1=4*!)emQZ3JLp&G9SXCRa6+Q)drjXIH*aiS3hHua`)dG+S}zr)C)VM+X){+*h)l;Mnc(P za$(wFJrU4Q!y`d80I@>EPmwhH>YQI)n2N-(nhE07=Xrw-%m_TV-l+r{Q|%Pl#9Nou zNX&Ogf0%{2m&bX>{8O2W7WG&0picE7{ofX$UjjMf47K{A1Y-zhknwDRDH2|2PS+I` zskRFHt1SV?1=WIZ=hYUjMxB;K1ZgE08I3xa>og{v63s&Ch1n=C)MI=Mz6O0Pl;yyE z7zpBmLqU?{K9q7|oX#nQijcm~Pqgr&a#I7qN>D=*8+Q2&cH?ol;wF5f8t53>1NpP7 zwnC+@E-`zA9s%LA<6~{yKXH#3GX0+k%Ld7&wA<>Z-8PGd0SS){Dlp0`0Gfs(*7Yyk znCpIQ{{$Y-aa^B9&Hug(f#6n{c6W7vwmEdO98A@k^SS^qjJ8Zm;$W9gj?-{ucQJj< zF)c|G0>J}^>~ghvFF2_dPrMz2V`txxT|1z3O4`3$ZK=DvIsxEXQ{KA#@3GOf&h`=N z<5K2|PQAb>opF>HL)C|3V;!6BzZX-n8cE$#&%8tRvsYZR+<9o-EC1u{00lu@>PK*! zE=Qe5xivV+=vAMD_!uMbbzm zEDU^L8jkI@rx%JR6jn57LNuXw!k;7aEj*NW3&-YUe;f^Rpkr9TXXnAR2IY?x2mq}2 zt~GwaX`4VU>{69JE_7^lP0)f zkdT(sb`+r|B@%Xxbd0qtQ%K=12`MXfQh1TrlqU_Rj$<~qCCks13`*76<4H1K$6Ik| z(*vFCkWGRuj{3T|nJwacH+jskIq87GJHyvnCHo(tCDy=={DE5hCPAkV27L}b?t6nm z)0abM0>TKKFN~=We_*2{jHWTsq7$)lT6cnRK9Pm!JtKq`aR`6EP+Eg90--)d65L_G zL7oS1pGK^uJ^EPaHR3S_*r6g|hq^ZWwws-9(oyVT0v~ zPso)_f(bQIa|n!Y$Q>RXh#B2+&b59wbggZ&r!9pS|B^%_@YFBYO!!Xz)3ZJMJ|xLT<4k?B5a zC+h8lo#xF|Y0I_hcRpagn~^*}Y>&xl8WN|potc^KXd4f2m>`?bskQIC=!GCLS0$i9HKzyZ53Mnxv z3EX}zg`jHz*D?sH@deh)iF({V0ubkdCrJ?tzbEN0(Q45cPCWm6X0WPIkZ;nKh6>bG z#BBjZks)M(%FqcX>+2Y#W&riu;OeYIv*t;;qD+7dKC^ z~lFga+vI;+y>%i1A!Y;nfFi=&qv$55Awim(|T3P(qIlu zMQD+N+O&OcQ(y4EV}y#I?k@IJ0J9xCh&wSwk_3Y2Mc zXkV>WAHKR!{KaUg=~EiBCiM&|K40FPY>aZr*vN^GG_mgJO%#Q79Fy3JQL5+S1NrR- zK{yimaHV5T=M972vfIONxj>C_4vaK2D*)fdY#sfQvXFSB%&g@X6vXz$0#B~7-XrL;+;A{0>-N(T!d?&{DH3ws3oEq}%TMCOwQPlkN=E_KT`8l~zd#VK z{qn&8)B!%+&OsI3(n13qXCQ(+Jy*orP{VO=yK1C*)^{xU{ylv&mPAO8LCodL(o4x7 zn&%x)by_Zhl!xQO8(BLoCREYx2ym|Efxy?1G-KD*$yo;)lm^8lS=H}HS!zqj_;Tbn zGH!vOEcUo@i8BK+774H%_uRLlZ#Q$K;p}OXz}Vr(so3RYQO0mn0fnSruovuc7;Pv& zu@lF#S;GJUo}w~19#Xj9-f5yD1kNAx@PZ=V9=hH|o3edzkr&{NHRTO<+<$rap!Q@_ zdlp)#S>BaN_M(a31c@fViBpZ+v1&bze|4Jz48eRo9t7S>esj_>8wLsiSt2$=Ixs~4 zM#{IXEDZ-#t~^KtyG%a{>e(uur?CpeP_{nuK^r~DV9{5(?AWFZX*eY{P>*Zda!|2{ z^eD8&M_5HV+=~5o$fdz!z6Y1bS8V%U_>2Y0$U~9Fj1@nxPut37YN=_IOymMcpSZrM zKI^{A%wMWS`sCuexf(^@GSzwbw6{^NvC*=f00z#1OC*;hZB{xU{)GXA0VPq2`Ih(z z1p~uz9jFf+@KNHw(0W&f?v@L}WY3`%Y`{~G&in6bQ?ado0)7t!L|yr5K%K|xo=jjc zBfk}`D?Cy4Yo5TI7OTmv#PAb2v4ImE5$vF)m)UnoZU|<$S}5;@i{!SoYz;d;k>)AkBduf)#k>epiOWKg;H|e+k@C(w$aL$!v_w z`hjWEO2>=a2C-Faq+ifcIRK<9$o-D$NHsVuT@zqdGZM*#W&|Zkg*_{NUTdS?{H|Ek z>MqV}HjadcOK8G(VqDf1;qZ_|wQiupApq+xm2BNngLA8ezx)$r z9?JlZb`Tpu^dy;0N}sUWt?VqdA2_eu^#2!c0V;QjfBsC*g5q`9aNA&}=4D3ZTeU@p zu{cD}@HQwS`4s&8YauX1DEd1L?CmS;$p+zFm}+ z;DMiL0zx#|N5=fr>-p6;X1H7#IbTa1c2=%uR4^uoCqFi({N6Y<;rFLO0x!a~gxV39 zU5h*>%>K%7_~39V!fcaQerv!0&zbN(DwZR}kHnzXzm8OaYS;}uIxNY+U|yxZzZ}Aj z>J#5fShD@LO&>Yjs;6PeMYA0JoV*0aC!gBc)5eCMFnN1hCrQqu$UXa9 zZuis0o5e9BV>qS-v+vSvN0rR=9Qntb&!_2I2~N@~^c@eK6K~mfHCPrB`yrBF9Q)wq zw{}|oNkP-)w>Xc-Pf4QT1zs05n8mz1EV5}iGzXN{#< z6+D0M^!O9g$_lJ9;3k68Sne{RHOdc9z(vic7?e8XP4+7lyKaujaS(VB&)764RZzodgMeH_+U}!-RbOt~d zn$ICS(2R8XO~e`=h=jU$O_zq`64~hejYsIvWoxm# zRqu&a8cL%wDMy`48mNsj87v7#pS{{UF6&%n)>M7@T#k>!ne6a(lPras2Qt@veR&ny zepP}X?gj`MuoauL)>#{fj%7lV+3jDas+?)_0Z8rb90Y3b^DQodf_HcpT^q2Y5OA{S z&JgCDi8-=CE&OvL@o3b!VdjDN#%E}@T#&kkLb+^?97D4Bbqnw+6NW?0a#7S76+sNy z*C!mKgn9JO6Ex5UW|odH{~-Q_sC4Fz>_LWJ-i_LmJ+yQQvGlEZMmN-6Q@l%NZ-zi)eO0`4XRx6`TKtMT}nU4fp}cZy1?fK6rQFDf)Wfcg<=7M>n~PF5?}nbnQ_XtXc-uovN*Jr&Qk`*Wc!?I#!PI zdH<-&^~hi;gu3{Bhyptx489{VRdVp9%4I6rwgTTibY~=bd7dI&w?S?lTJJhQQLwPb zLDe_Zi+SrTb@}Lx-?9k1j^3OM$G1zfeSzV&43`jia-&TMS8-Q2vEpF3Q}KikjZ)1K zONckr1MQ%kj&N9p-kV!RiX|x{qq(^Qf(Gq~b(%bK?iJTaH(9n|ff?_C-pC70(1v^9 zv3X}_%mSJ1Q|bIGgU{<-TuHd*Nk58PP^b{PQKs4cP&4 zHk{yWnA$b=KFQF@j5|V+^%%~E8{yOfY>j!wy}>V{ml|X6P}x3E>>phS{HX8vn=+QH z==$p{?S)ZQ^^{)q%gh4wOP-ONLJb^|5$D2Qj;&`@#K)t}`2FSX^Ug12R(c#X-kKw4 zoPz<@YWY3Ew+1J!5=4d90UBvI)xFp%%Cz*{p(q}rpH>h>wos5!@Y)o0wXQD?sAEBQ zo(u@hDGB=qO3xz31>Y*txi0!rs?u!b69rXJIx-Ilmsv0|AObUa;dlr%te$Q>W7<|jw-becNw0!Yt zMEVl4K8W0^<n-185p zXd1;s2I<^PUg8OjVq+w#4WhiA72rKJ?@7PP9KpWLziKqN5WntwG}iac3rp8mZbTIc zPFIs~pPK5+BH4^?H)6N^_^)pocKetU7;n=1LP8B`=?NibC@EjH(v}CM?{3kOB&0wq zzB=d<^+-myf<(Bar0!U3QB4T8i zlQwiy*m3kqe$5OSoxCeY*XrJy8|NLBQl6qPFFr|%jv zvV=D6==dZt+jeMX$@2C7sr%+k9-Y3(OFuP*g@E)fmEmAXa>t{uH7F_K0&F1R5+bs3 z!@JQN^NH+>cTf^|+u6pAIWaL*;XrJ&+Fj(fOxcTtY1wjXv)=|YWnSf{2G7Oc5vXIK zboA?tIFCM&ubq^dFMVgeWMeR`eSXX3Q`oM<$H-$d&_D1PpJr9FRQ)GAv1Za|wMPTu zE6sx8`G6`{D1LJxlyU<`SW;M#2Q1dhVrvE^29-T#A zhM${q?-%?dG~T+v(JIRK$rRQ<(j}KOa+dIS|MZW6-fNA6!o0pvTv1mEyh5fyRFR#o zi{T!69XgWxJ@>hx5Vu6c2~xDb$kekBo3d>EmpztDPP>Zk8pS5Lb{_5ba5s--?$({! zI#6ibP12fsVR7_&=N_x5dBA7u3o^-KwlM>*uW50i#sls2(mvnh)v*>$f1}7m{TieS zKkd4@FMrjCIT)@<=WQaP)2qpAzm17!cBRnd-4z(Q#)oG%9@yn=BY)$A(0K?#RgMQ& zkUG+a5^EyN;YkQdJLfL}Qu3mDRI|~J#-E>qYKD)wDC9#Q5b@r=$zM_9%BSpYS~a!4 zP}zO|uKqQS%rH7_ek+NL;kMr~kgS(vPAVEuzs{^yWX@R0eam&|6WmxFWcSMIL;RKa|@`)1-YRa{*ol>Vqw=TtGK`LzU0t6 zqRVCGyLF9m{9I2(eg26>xdHFI-yHOqDm#PTiu6Yr;P9;(;4V%geqPBFC{4Jxxo65Q z`oOn{^lrFWdR6@|n3Hhp>cuyleQ^nxr3PLXdzYq<8|_lOrS$Kt zBT_68Idy$~u*qLb>!AE1Aw1YOi9x>xxOytH*%f4BMsI;#ib}<4ZO~Uv|pQ zAlXh!JO%Iuc0As|cV-7c%TQk&p(?Jd>NOa;+4%T46I&dcK~5sRj)c}Z%}q#T(Haxe?$8U+t5U6agE5lf7^U430vn<`<{Y?fZU zZ+iJF9NVD<_tsSP4g^Uhso|3S3ztod^H31&G&k~?^bS8^)Rk7j3u%fOku|G5c${2K zI|@zlkM@4SAUGO&)9}lVsxj@JpM}m(V2;8s<#0=!- zCVLOrb^F)e<1gC~14031A=ii_M^2JmQd7AqxwahEwqB8)=aq5%TV3IZ>kTJ+WL<%J z>^uGWN09|mB&uyvY0;)x!(VNKN=lt?PqSP3Tzt>H;WbPb7x=RtI*ydkP~g*osQ5g6 zf5THtziW7v>15aLcp&j|`w(sePl9<(0la2coG5k2TR~AiK&>poo|9OZ3~T3iRUysJ z?YjXpJBSNtBfC^MjzzHn2e=K$%Z}NJB|mdoI%2>VPaN|cqK8cN7OG42vVn8r>Zlvl zD#47YPu`H~Lg{9v;B9y5Dl@xVem;+jck$|d-(1?D^1Ku3M`P4TngMNh8#XrlX{DWy z{&m3fD>@~A>`~J#9IoS~p7xV)k2esYszHa1jl{8o4;`#r71nJG`7UCm!4&nEQ~=TF zvMd~~bW-{mLcNU7fY?@dfPTShGy*dS2A&$`8+M^`eSn%zqc!~SBZt($&Aj)>SQ&&J zW3OcTBjuYa5SEe?z)|37CFTo+AlrN;_NKTw=~g4Vo6wD)B&$_R1Fx(wN#6`P);Bp{ z8zsK`rS~{chh3{KS++pK@sya4h^5%YBY|jk^?3SkGyP(7mQQ6glniPVy)GI0q=%C; zj(nO5NF{r*G2+^jguxHFFRUr8OzO6dT7pXXG0|}TCjL~^hux|2d!s|FqkxU3ZKY~0 zJ=D<%F@DCnIupF6`_MYwFKF=MfKgs02mQbeJE~s3bVudme=VH?zw|b8zF@p5jT{?s zFYo;iqc@eD34!|LH65>UA}r=3dRCukDmmwoH8!BGuf(gI$%=eC;59)e<)r_j(ejVj zkI{m^tNfaOBr&?&pQS|0>`xx_s?t$#P<>_EpY((1pu;{lg^%^VdK)_-2r1>e7{#W> zLjTNl9W~k%0~9D#KAZ3Ze_FC7fiB?tK^||xH4dE#j;S*cKg9u##5EDQ$=O^J<3+K> zxF|RPC9p-mG6Fj)Y39K|YAc4f-0&dZM0$3b?j8N9{dicnk%aw=J$dauu%gv@g0xm* zl@7jod1)~VAO;ta@9llPE;(|}T2trd>05`bzS6fQ-G4PXy=ND|5W1sJZzVs$;m`d* zgEna@X+1YUu@O25{wj00KDmo!RNw*HG#d2J+JJL!Eq13lk$~`7RB%S(8fB$>{%Cv+ zFp|WV0w#QeS=0xKYaO_KH{<1Diu9E_AJ3&WGL``i(E+5A3?gqyq&J|N+wlngCdh&_ z9V%}1Oo%}fv6g;su+KR^EQO-w!n=htA}pqE`iOR|mGjv)LxQ!|XxVImqHaMSRSHqd zvCqbrb%2@C!YKOAxtpgOGT-P`IY1Y>-efxz*Bai3^Ydw*w(0h#l04?CulfcCgVbUAo74z_RqHPYme5s%<_}8kXu$oV1f7&QIu--}yWZTwOHu zgc_bUD!RKBgJeDDQU{gxA|b?`tsT>-WuQ+;r83F^F?sY80hR)l-gu!D=KHM?{0BkDJx_+cg0*S>8SU5;}7jQ z#+bY&|JY@UX{S_qPhtf%z%R--S4W2z72X_lmD%@qK1arACs*$77iolrKA76ta+z?O9;R20xqN*<04@z(*BwIT4F-})%Y)Q8> zXf;e@@GA#Hspo;)i=$nLFA+|x(C3v|cHz%|cLRayt3xukxg*e3X`h}kF>ZEW5vtr? z75;oJKAL(s>Sg_!0_!#MH`&pFBo3#&bE-<9T;rG_O(rsR0E19FS(UH)vZWTtdXu`~C7u3wzF<Y;)72s zXT%5k*KXj;602kH#{`T)qZ|#ap21yq4VkN+%LbJdol0-hl3%8M4O5fNI`KVw;B{#` zse`h`Gv+TLvfn$!3Y%$&=yi$5^UPtclptxw=Ni${0=_k1%u^<-*Gqpbvw!?~+WeF4 zre6cHalZgT5_9#9@DM)Vn!VF-twsm#;n-JFNdJ0>A}0gV@2PV>s%CeGYO!X$3*iF? z$rcOI9o{%%V!vM7`HYO3FeejKD(KT656y>W{v5KF2hXV%yu_cDJDzpj-A>Fo*3t5E zyX$p?#j#=Efm@<FC1Y{rY*X$uBYV9qPUwi=-y3aHux?6jq|q+w^85*xSV?;>(*vq zGiWd)W$KbOW9_ayzJL_c^RBksc0I+Nr0b1)QBAt7xHJ8@j48=24J6lpqX44+QAQ?c z*o>^}KY6g1UYng-WcnmktKVICkz|05Hu3C0ZC-jtCK7E<^D+>> zsu^@RCAW$gwI_Bg3}m>Z{?PQ2pghl2fBVXR7nKm8OynC@DK4Fv3>1!`%^!KTZ`}mV>=iuwV^Xjw zT2H*F`t3Dg@SAUHx6P#`$3FMe>aZBWGU1tH{m*4^*C1q*N54oVq3H9UzlorrpzgKZO(V5MGrCVKFC@p5E#6vC#1*d$ z_q*%RUoI%}oHB*Z#?&IY8ST2Nt6>Re3m114MXmg{S4S7TxPzqGaB^7h@C_RG(~E|1 zN&LO@$_b3E`8^()VTJ6;`i9-IOG0{s5fm4kI{KvDkLnkLVn+9Lu$B#9Qh($`Hj5c5 zV;kt~*7iH)p01cNR1asn)Idg_mVWL(ucq`UK~;&jF_90MTn1*k<7rRLLe2|JdKlxk ztCX=!gHR9002NZYL7{vjuncJC!&oSq>C}6$q_u3#A$w3*h3%24mC>4Q&5OciE$fO? zwat4eH?C7nC=Mj$$r+dgE|UK9QudD8WyJ1hkQ$*`i=i_X3v-AVA}TTI>mft=>d_V`%@i_Tb?*E6&xpL-X{)1=EMJ zkHNnv|K}5S1$ zJ638UBY%}JPO@dzkK;VH>Xf6Yju;g zI%ijSM#8J<-A^p4|94l9z;t)zV+vE(X^I5o%JG#c&v6LHh;nE>nUm(|fXK-c$o+~}9@w7(*Nr) z@ND?xJR_fs2DX`8_#G@9?=->oiHUm~a2=t5iE}^f=8f;t*Qw*IJ!8tIk9(;O7T)xz znq;3phlxdr^e23YOaA^Yhsd4#zuOr=d|nL^Zswa5Mg!Iu1|0#*zL2X1o{o5R@c?>v zy+DNRf&KiI>J9PhVNItxT(h=PHofmcJM`Ek6Wl&j~amQgt*|^x+`EJmYddYTN+g>7*c;ahbh4#1rK z&x3BHX82?zlbk{+t))sg9e1Z+JaOm{Nnb>ysH*mi-Xo>%F8TU2QW;? z3kY=&!osyYpPKeZZiL)Fh938=_nx4?H6WAcjU>Y(ssH)ZUne{!J`-#AwYY)8M#%qQ z9}D6Bvg25t0{y5P#Qaz&Q(&NRQo5lr(Wm3yg4&LX%mq)%3&?|Yu5n-Ikh6=d0}^?z zhpahHocPAVW1oVCW7(bgAbPv~)hd=GBlEemKX6}gfCjt$wXkLTi|iZh3hmMM#1PGhewt1^ z!!a|;b1zQ|nVWtO4>A3p`S&pjWa*)5!c4&shGJx9S)JC#8?exhPV~tFAWPgR2txdI z|Bg>wt~;eAt1paHMCu07qaDyrXjhGA8LP^iLDhKVby%N*1Q{W1AP)$Q3^p2=-;Pur8y3=8^0HUjP+4KYs$42O57b7kG?+$1x2Ts_C%G1YAn*S?4Q7Zp#w52i=!p$*W3Zxr@;`hsPTxDzY_@%7Lyuu}|t`Ht-! zkLpcrFRpi!nlLMWD@De z9x6}YLX$D7^?f?A?rG#fkRNT-`qXH%T#QG`q21**Pf})kgd?EY6PM25{ouO=)fASz zy+tTi)XdfmJ;YGGjBeupNyAh19o30n1WgY;*l^-l$HU7}t|xwOw~w=P zzIa`nYDL)i4WgIinn=|+;Zx?T?tHkrXfYwTsC|n2Dz<2m7G}3*xoD51!z+nvBR zRpMPNbV+p8;;-(AZm4MiN{<2eFVonVjgcEFJE0UO0=B`}{`pgm2vyO|{OzV1t$$36 zfT8sQ;m#IT%HeBK1DnkT0C4M&&pb_cbHG6Y@dH1whnnu`hH#-hiT|`JGH4_CKKjdM z$hbTv6J@mC{ZH%!7I)#oxUo)4CU_hnDkgz(ooAvA!zET4i0U7p_EQVm!wns2O7F$A zP1@h9W*@^eaPEU`u%M5l(^Rv0LiX10O>$=cV;4s#LeR>`bp~_c`_2Q;XITM?Eh_UP zeBCe^x9vE9=y*q1(^M8Dh620g7Z7>4O|veEy@+{?&%yuuAtUF=wC5<8@PI%l1v+rI zVd?`Hnid~K4LA&yz52_YI+Kinn;@KNzmlx;jCrR%Kxo3n0%dNZq!uab1u17$?$I zq1a&ANj}!|0KMHL7ve#IV>;xJH4!i91&VrPR2#V<*C~daWkWrNW8oKsH-YmTpkA>K zH`(kl5wj5ruB1e#aRT9S#t?*3d~xeor&kU&mJCOY<;18S-nhWKm(z3mN16y_lXrN(%3vw(Z#%U6-?}F)0k&|m|yp5fBx4sV#O0#1t)Oq7f zKm3#d#Iz^kG`3ChEu!G1kH8#xPyBGcpuU}27`W7)Z&q6^CVxJ`1EnkPgbebz;Ag}8 zD9Pt6mIT9ylKSJd^Ew+7+HB1e{^d$GVM^*?4P|=s1Cm{;E}Kd=6L#!#NgCgB|1mFkWy)dX$(csK z3&1s0!3~RrPCwQVNAt;j2(2){@{(>mS&mWNUFT4t_<551N{xr+c1Cn&!9N)={vN7l z58P+(N72Z^>EXz%v>C4t!~&Cwu;f_pa4Agd#%DjeYir+W7A{r?FP0NIIeX*{(%P`w z9Jj&aCI5G|!hZtk^Rh}5!BI_Rz;GWd^z=`zjJy-0555B#C>DqjwLUu=q0cG$!re~P zs*wK#&mebI5&1+vNAU31@Wu%e>-N_819SQ zbD@dAy~zVj4SK?R<76>0(=zRR?E>wm|Fp>q7t_OXW-r3F51K*uU@c51^Ma`_PV_&* zDq;Q&4(PO-c(_(j+nIZWZky&pJq4z|E5k=UN$uMoelAWqkHYQqfNvq}cMSyWSSXQk zgsfOh;=yh_QJQ3bOh*CUz>AzkLL10Th-G?^Im~aALI|hP+)FZ5~ zOlvBxaYcyOouoJ6XxBi?pcT+6hrQSl3h7JfSvk9kG~STlMDI0DfnhOe&c!In7lCjJ z+hpKUFvpXKxt>mohs^C{?X2zW?Hqr;fP^4rU*AY63F}r=Yv+ z;#<^eK@KG6Az63hjtWI8W6u)uAYCX!`%st+R*qPJ#vgu+S)&pd#ZXD!3V7t8Qj1Q@ zhVUB$lJg%!)=ex-wSUsv5ZjR1klP%N_rW&5MJmhq4v|SL4~~bZB!Xm^6HHOWLMuGs zIlP?80-jf92H+?SosZ@5y!O~d{OwRS{Z~BA5u{8o>K_%x38D0lSnx4;XG2x6-TLHWsF`pU ze@J$te!!~I#9k91tJ za~N`-iDfQ_mcLGud&toZ2G2;$ACU|EHnHY7>d~GJO%W@V8DN+gJZ^zkak?%3Q6mI( zRB3gJo@JV6<1n;>_fc^ViM{4)>pqP~uOoAHIdRZo_^>kY{66P82>lMTk$#d_#r|n{ z0mP*d%EGm{M0QDg=jLbgqcxRqQ;ylRMts|`YH{lbWO6M6U|L4UECtNxGea^ZDym(I7-*xDxPr3R?aJj`LJe6!Yp6p+X zBWNL?{=9jh(P#+2zyqB0HR!-~pg!$mIh?T>Eb%N@;a@gXZvj~!EWMv>5)pXJ{y{ezx_ytZMw|$(wAdg8IX7 z#@|2Lr_bh-!E-lE%eaq7(p>FdMve$1BA@Op)DpitK+jCXm)}poY)&Zoucg3Q;0?6& zvVIN-k##xG<7?Y3Z-WVp|8xp?57k9{XCbNBuJyDPU;b*8ZS&hd?`imv@jQynzBBJX z$6X84xjo9#6Jh&rv&Jg{ypwpM4|!CpP$S>l)nMBXZztIPeb#u*QUAFQ*4@Adu0nz{B_@kHAU(c~txZd7^*qEWm0h^=w`(EU=>GvlEX>CM literal 0 HcmV?d00001 From aa3c3d62902ae07c62b0ebc2902ce575fe3e067d Mon Sep 17 00:00:00 2001 From: achingbrain Date: Sat, 4 Feb 2023 19:01:51 +0100 Subject: [PATCH 17/18] chore: rename file --- packages/interop/test/{files.spec.ts => blockstore.spec.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename packages/interop/test/{files.spec.ts => blockstore.spec.ts} (98%) diff --git a/packages/interop/test/files.spec.ts b/packages/interop/test/blockstore.spec.ts similarity index 98% rename from packages/interop/test/files.spec.ts rename to packages/interop/test/blockstore.spec.ts index be314264..31375fdf 100644 --- a/packages/interop/test/files.spec.ts +++ b/packages/interop/test/blockstore.spec.ts @@ -10,7 +10,7 @@ import { sha256 } from 'multiformats/hashes/sha2' import { CID } from 'multiformats/cid' import * as raw from 'multiformats/codecs/raw' -describe('files', () => { +describe('blockstore', () => { let helia: Helia let kubo: Controller From e45faa11f306d3599ec41afd28592a6e5e4ddcd0 Mon Sep 17 00:00:00 2001 From: Nishant Arora <1895906+whizzzkid@users.noreply.github.com> Date: Sun, 5 Feb 2023 13:29:21 -0700 Subject: [PATCH 18/18] fix: :pushpin: Node + NPM Version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a2ce5243..758f5883 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "ipfs" ], "engines": { - "node": ">=16.0.0", - "npm": ">=7.0.0" + "node": ">=18", + "npm": ">=9" }, "private": true, "scripts": {