Skip to content

Commit 3bd88c0

Browse files
committed
build(deps): replace ipfs-http-client with kubo-rpc-client
- Replace deprecated ipfs-http-client with kubo-rpc-client. - kubo-rpc-client must be imported dynamically since it's ESM-only and we still use CJS. Depends on: #2821 Signed-off-by: Michal Bajer <michal.bajer@fujitsu.com>
1 parent a86adc9 commit 3bd88c0

File tree

11 files changed

+305
-307
lines changed

11 files changed

+305
-307
lines changed

.cspell.json

+4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
"ipaddress",
7171
"ipfs",
7272
"IPFSHTTP",
73+
"IPLD",
74+
"ipld",
7375
"Iroha",
7476
"Irohad",
7577
"isready",
@@ -83,6 +85,7 @@
8385
"KEYUTIL",
8486
"KJUR",
8587
"Knetic",
88+
"kubo",
8689
"LEDGERBLOCKACK",
8790
"leveldb",
8891
"lmify",
@@ -146,6 +149,7 @@
146149
"txqueue",
147150
"Uisrs",
148151
"undici",
152+
"unixfs",
149153
"Unmarshal",
150154
"uuidv",
151155
"vscc",

extensions/cactus-plugin-object-store-ipfs/package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -59,17 +59,19 @@
5959
"@hyperledger/cactus-core": "2.0.0-alpha.2",
6060
"@hyperledger/cactus-core-api": "2.0.0-alpha.2",
6161
"axios": "1.5.1",
62-
"ipfs-http-client": "60.0.1",
62+
"kubo-rpc-client": "3.0.1",
6363
"run-time-error": "1.4.0",
6464
"typescript-optional": "2.0.1",
6565
"uuid": "8.3.2"
6666
},
6767
"devDependencies": {
6868
"@hyperledger/cactus-test-tooling": "2.0.0-alpha.2",
69+
"@multiformats/multiaddr": "11.6.1",
6970
"@types/express": "4.17.19",
7071
"express": "4.18.2",
7172
"ipfs-core-types": "0.14.1",
72-
"multiformats": "9.4.9"
73+
"ipfs-unixfs": "9.0.1",
74+
"multiformats": "11.0.2"
7375
},
7476
"engines": {
7577
"node": ">=10",

extensions/cactus-plugin-object-store-ipfs/src/main/typescript/i-ipfs-http-client.ts

-48
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/**
2+
* Since kubo-rpc-client uses ESM only, we can't import it to get types (since we use CJS).
3+
* To fix this we define required types here, based on their counterparts in kubo-rpc-client.
4+
*/
5+
6+
import type { Multiaddr } from "@multiformats/multiaddr";
7+
import type { MultihashHasher } from "multiformats/hashes/interface";
8+
import type { Agent as HttpAgent } from "http";
9+
import type { Agent as HttpsAgent } from "https";
10+
import type { CID } from "multiformats/cid";
11+
import type { Mtime } from "ipfs-unixfs";
12+
13+
/////////////////////////////////////
14+
// Types from kubo-rpc-client
15+
/////////////////////////////////////
16+
// Some are simplified when details are not needed
17+
18+
export type MultibaseCodec<Prefix extends string = any> =
19+
import("multiformats/bases/interface").MultibaseCodec<Prefix>;
20+
export type BlockCodec<
21+
T1 = any,
22+
T2 = any,
23+
> = import("multiformats/codecs/interface").BlockCodec<T1, T2>;
24+
25+
export interface LoadBaseFn {
26+
(codeOrName: number | string): Promise<MultibaseCodec<any>>;
27+
}
28+
export interface LoadCodecFn {
29+
(codeOrName: number | string): Promise<BlockCodec<any, any>>;
30+
}
31+
export interface LoadHasherFn {
32+
(codeOrName: number | string): Promise<MultihashHasher>;
33+
}
34+
35+
export interface IPLDOptions {
36+
loadBase: LoadBaseFn;
37+
loadCodec: LoadCodecFn;
38+
loadHasher: LoadHasherFn;
39+
bases: Array<MultibaseCodec<any>>;
40+
codecs: Array<BlockCodec<any, any>>;
41+
hashers: MultihashHasher[];
42+
}
43+
44+
export interface Options {
45+
host?: string;
46+
port?: number;
47+
protocol?: string;
48+
headers?: Headers | Record<string, string>;
49+
timeout?: number | string;
50+
apiPath?: string;
51+
url?: URL | string | Multiaddr;
52+
ipld?: Partial<IPLDOptions>;
53+
agent?: HttpAgent | HttpsAgent;
54+
}
55+
56+
export type IPFSPath = CID | string;
57+
58+
export interface StatResult {
59+
cid: CID;
60+
size: number;
61+
cumulativeSize: number;
62+
type: "directory" | "file";
63+
blocks: number;
64+
withLocality: boolean;
65+
local?: boolean;
66+
sizeLocal?: number;
67+
mode?: number;
68+
mtime?: Mtime;
69+
}
70+
71+
/////////////////////////////////////////////////////////
72+
// LikeIpfsHttpClient instead of full IpfsHttpClient
73+
/////////////////////////////////////////////////////////
74+
75+
/**
76+
* Connector only needs these methods to work.
77+
* More methods can be added in the future.
78+
*/
79+
export interface LikeIpfsHttpClientFile {
80+
read: (
81+
ipfsPath: IPFSPath,
82+
options?: Record<string, unknown>,
83+
) => AsyncIterable<Uint8Array>;
84+
85+
write: (
86+
ipfsPath: string,
87+
content:
88+
| string
89+
| Uint8Array
90+
| Blob
91+
| AsyncIterable<Uint8Array>
92+
| Iterable<Uint8Array>,
93+
options?: Record<string, unknown>,
94+
) => Promise<void>;
95+
96+
stat: (
97+
ipfsPath: IPFSPath,
98+
options?: Record<string, unknown>,
99+
) => Promise<StatResult>;
100+
}
101+
102+
export function isLikeIpfsHttpClientFile(
103+
x: unknown,
104+
): x is LikeIpfsHttpClientFile {
105+
if (!x) {
106+
return false;
107+
}
108+
return (
109+
typeof (x as LikeIpfsHttpClientFile).read === "function" &&
110+
typeof (x as LikeIpfsHttpClientFile).write === "function" &&
111+
typeof (x as LikeIpfsHttpClientFile).stat === "function"
112+
);
113+
}
114+
115+
/**
116+
* Only files API is used
117+
*/
118+
export interface LikeIpfsHttpClient {
119+
files: LikeIpfsHttpClientFile;
120+
}
121+
122+
export function isLikeIpfsHttpClient(x: unknown): x is LikeIpfsHttpClient {
123+
if (!x) {
124+
return false;
125+
}
126+
return isLikeIpfsHttpClientFile((x as LikeIpfsHttpClient).files);
127+
}

extensions/cactus-plugin-object-store-ipfs/src/main/typescript/plugin-object-store-ipfs.ts

+49-21
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import path from "path";
22
import type { Express } from "express";
3-
import { create, IPFSHTTPClient } from "ipfs-http-client";
4-
import type { Options } from "ipfs-http-client";
53
import { RuntimeError } from "run-time-error";
64
import { Logger, Checks, LoggerProvider } from "@hyperledger/cactus-common";
75
import type { LogLevelDesc } from "@hyperledger/cactus-common";
@@ -22,21 +20,25 @@ import OAS from "../json/openapi.json";
2220
import { GetObjectEndpointV1 } from "./web-services/get-object-endpoint-v1";
2321
import { SetObjectEndpointV1 } from "./web-services/set-object-endpoint-v1";
2422
import { HasObjectEndpointV1 } from "./web-services/has-object-endpoint-v1";
25-
import { isIpfsHttpClientOptions } from "./i-ipfs-http-client";
23+
import {
24+
LikeIpfsHttpClient,
25+
isLikeIpfsHttpClient,
26+
Options,
27+
} from "./kubo-rpc-client-types";
2628

2729
export const K_IPFS_JS_HTTP_ERROR_FILE_DOES_NOT_EXIST =
2830
"HTTPError: file does not exist";
2931

3032
export interface IPluginObjectStoreIpfsOptions extends ICactusPluginOptions {
3133
readonly logLevel?: LogLevelDesc;
3234
readonly parentDir: string;
33-
readonly ipfsClientOrOptions: Options | IPFSHTTPClient;
35+
readonly ipfsClientOrOptions: Options | LikeIpfsHttpClient;
3436
}
3537

3638
export class PluginObjectStoreIpfs implements IPluginObjectStore {
3739
public static readonly CLASS_NAME = "PluginObjectStoreIpfs";
3840

39-
private readonly ipfs: IPFSHTTPClient;
41+
private ipfs: LikeIpfsHttpClient | undefined;
4042
private readonly log: Logger;
4143
private readonly instanceId: string;
4244
private readonly parentDir: string;
@@ -45,25 +47,48 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore {
4547
return PluginObjectStoreIpfs.CLASS_NAME;
4648
}
4749

50+
/**
51+
* We use dynamic import for kubo-rpc-client since it's ESM and we can't import it normally.
52+
* This methods will load the module and initialize local IPFS client based on ctor arguments.
53+
*/
54+
private async initIpfs(): Promise<void> {
55+
if (isLikeIpfsHttpClient(this.opts.ipfsClientOrOptions)) {
56+
this.ipfs = this.opts.ipfsClientOrOptions;
57+
} else if (this.opts.ipfsClientOrOptions) {
58+
// Force no transpilation (see https://github.com/microsoft/TypeScript/issues/43329)
59+
const kuboRpcModule = await (eval('import("kubo-rpc-client")') as Promise<
60+
typeof import("kubo-rpc-client")
61+
>);
62+
this.ipfs = kuboRpcModule.create(this.opts.ipfsClientOrOptions);
63+
} else {
64+
const errorMessage = `initIpfs Need either "ipfsClient" or "ipfsClientOptions" to construct ${this.className} Neither was provided.`;
65+
throw new RuntimeError(errorMessage);
66+
}
67+
}
68+
69+
/**
70+
* Get IPFS client or initialize it from constructor args.
71+
* @returns `LikeIpfsHttpClient` or exception
72+
*/
73+
private async getIpfs(): Promise<LikeIpfsHttpClient> {
74+
if (!this.ipfs) {
75+
await this.initIpfs();
76+
}
77+
78+
if (!this.ipfs) {
79+
throw new Error("Could not instantiate ipfs http client");
80+
}
81+
82+
return this.ipfs;
83+
}
84+
4885
constructor(public readonly opts: IPluginObjectStoreIpfsOptions) {
4986
const fnTag = `${this.className}#constructor()`;
5087
Checks.truthy(opts, `${fnTag} arg options`);
5188
Checks.nonBlankString(opts.instanceId, `${fnTag} options.instanceId`);
5289
Checks.nonBlankString(opts.parentDir, `${fnTag} options.parentDir`);
5390
Checks.truthy(opts.ipfsClientOrOptions, `${fnTag} ipfsClientOrOptions`);
5491

55-
if (isIpfsHttpClientOptions(opts.ipfsClientOrOptions)) {
56-
this.ipfs = opts.ipfsClientOrOptions;
57-
} else if (opts.ipfsClientOrOptions) {
58-
this.ipfs = create({
59-
...(this.opts.ipfsClientOrOptions as Options),
60-
});
61-
} else {
62-
const errorMessage = `${fnTag} Need either "ipfsClient" or "ipfsClientOptions" to construct ${this.className} Neither was provided.`;
63-
throw new RuntimeError(errorMessage);
64-
}
65-
Checks.truthy(this.ipfs, `${fnTag} arg options.backend`);
66-
6792
const level = this.opts.logLevel || "INFO";
6893
const label = this.className;
6994
this.log = LoggerProvider.getOrCreate({ level, label });
@@ -79,7 +104,7 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore {
79104
}
80105

81106
public async onPluginInit(): Promise<unknown> {
82-
return; // no-op
107+
return this.initIpfs();
83108
}
84109

85110
public async registerWebServices(
@@ -130,7 +155,8 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore {
130155

131156
public async get(req: GetObjectRequestV1): Promise<GetObjectResponseV1> {
132157
const keyPath = this.getKeyPath(req);
133-
const chunksIterable = this.ipfs.files.read(keyPath);
158+
const ipfs = await this.getIpfs();
159+
const chunksIterable = ipfs.files.read(keyPath);
134160
const chunks = [];
135161
for await (const chunk of chunksIterable) {
136162
chunks.push(chunk);
@@ -151,7 +177,8 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore {
151177
const checkedAt = new Date().toJSON();
152178
const keyPath = this.getKeyPath(req);
153179
try {
154-
const statResult = await this.ipfs.files.stat(keyPath);
180+
const ipfs = await this.getIpfs();
181+
const statResult = await ipfs.files.stat(keyPath);
155182
this.log.debug(`StatResult for ${req.key}: %o`, statResult);
156183
return { key: req.key, checkedAt, isPresent: true };
157184
} catch (ex) {
@@ -170,7 +197,8 @@ export class PluginObjectStoreIpfs implements IPluginObjectStore {
170197
try {
171198
this.log.debug(`Seting object ${keyPath} in IPFS...`);
172199
const buffer = Buffer.from(req.value, "base64");
173-
await this.ipfs.files.write(keyPath, buffer, {
200+
const ipfs = await this.getIpfs();
201+
await ipfs.files.write(keyPath, buffer, {
174202
create: true,
175203
parents: true,
176204
});

extensions/cactus-plugin-object-store-ipfs/src/main/typescript/public-api.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
export * from "./generated/openapi/typescript-axios/index";
2-
export { IIpfsHttpClient } from "./i-ipfs-http-client";
2+
export {
3+
Options,
4+
LikeIpfsHttpClientFile,
5+
LikeIpfsHttpClient,
6+
} from "./kubo-rpc-client-types";
37
import { IPluginFactoryOptions } from "@hyperledger/cactus-core-api";
48
export {
59
PluginObjectStoreIpfs,

0 commit comments

Comments
 (0)