*Add first version of the ipfs rpc methods
This commit is contained in:
parent
44e1366856
commit
c90d207b7c
|
@ -0,0 +1,268 @@
|
|||
import { rpcError, RpcMethodList } from "./index.js";
|
||||
import type { IPFS } from "ipfs-core";
|
||||
import type { UnixFSEntry } from "ipfs-core/dist/src/utils";
|
||||
import { dynImport } from "../util.js";
|
||||
import { exporter } from "ipfs-unixfs-exporter";
|
||||
// @ts-ignore
|
||||
import { CID } from "multiformats/cid";
|
||||
import { MemoryDatastore } from "datastore-core";
|
||||
import { MemoryBlockstore } from "blockstore-core";
|
||||
import { createRepo } from "ipfs-repo";
|
||||
// @ts-ignore
|
||||
import { MemoryLock } from "ipfs-repo/locks/memory";
|
||||
// @ts-ignore
|
||||
import * as rawCodec from "multiformats/codecs/raw";
|
||||
import last from "it-last";
|
||||
// @ts-ignore
|
||||
import toStream from "it-to-stream";
|
||||
import { addStream } from "../streams.js";
|
||||
import { ERR_INVALID_CHAIN } from "../error.js";
|
||||
|
||||
let client: IPFS | Promise<any>;
|
||||
let resolver: typeof import("ipfs-http-response").resolver;
|
||||
let utils: typeof import("ipfs-http-response").utils;
|
||||
let detectContentType: typeof import("ipfs-http-response").utils.detectContentType;
|
||||
let normalizeCidPath: typeof import("ipfs-core/dist/src/utils.js").normalizeCidPath;
|
||||
|
||||
const repo = createRepo(
|
||||
"",
|
||||
async () => rawCodec,
|
||||
{
|
||||
blocks: new MemoryBlockstore(),
|
||||
datastore: new MemoryDatastore(),
|
||||
keys: new MemoryDatastore(),
|
||||
pins: new MemoryDatastore(),
|
||||
root: new MemoryDatastore(),
|
||||
},
|
||||
{ autoMigrate: false, repoLock: MemoryLock, repoOwner: true }
|
||||
);
|
||||
|
||||
interface StatFileResponse {
|
||||
exists: boolean;
|
||||
contentType: string | null;
|
||||
error: any;
|
||||
directory: boolean;
|
||||
files: string[];
|
||||
timeout: boolean;
|
||||
}
|
||||
|
||||
async function initIpfs() {
|
||||
if (client) {
|
||||
if (client instanceof Promise) {
|
||||
await client;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const IPFS: typeof import("ipfs-core") = await dynImport("ipfs-core");
|
||||
|
||||
const ipfsHttpResponse: typeof import("ipfs-http-response") = await dynImport(
|
||||
"ipfs-http-response"
|
||||
);
|
||||
normalizeCidPath = (await dynImport("ipfs-core/src/utils.js"))
|
||||
.normalizeCidPath;
|
||||
resolver = ipfsHttpResponse.resolver;
|
||||
utils = ipfsHttpResponse.utils;
|
||||
detectContentType = utils.detectContentType;
|
||||
|
||||
client = IPFS.create({
|
||||
relay: { hop: { enabled: false } },
|
||||
silent: true,
|
||||
repo,
|
||||
});
|
||||
client = await client;
|
||||
}
|
||||
|
||||
function joinURLParts(...urls: string[]) {
|
||||
urls = urls.filter((url) => url.length > 0);
|
||||
urls = [""].concat(urls.map((url) => removeSlashFromBothEnds(url)));
|
||||
|
||||
return urls.join("/");
|
||||
}
|
||||
|
||||
function removeSlashFromBothEnds(url: string): string {
|
||||
url = removeLeadingSlash(url);
|
||||
url = removeTrailingSlash(url);
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
function removeLeadingSlash(url: string): string {
|
||||
if (url[0] === "/") {
|
||||
url = url.substring(1);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
export function removeTrailingSlash(url: string): string {
|
||||
if (url.endsWith("/")) {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
initIpfs();
|
||||
|
||||
function normalizePath(
|
||||
hash?: string,
|
||||
path?: string,
|
||||
fullPath?: string
|
||||
): string {
|
||||
if (!fullPath) {
|
||||
if (!path) {
|
||||
path = "/";
|
||||
}
|
||||
|
||||
fullPath = `${hash}/${path}`;
|
||||
}
|
||||
return normalizeCidPath(fullPath);
|
||||
}
|
||||
|
||||
async function fetchFile(hash?: string, path?: string, fullPath?: string) {
|
||||
let data = await fileExists(hash, path, fullPath);
|
||||
|
||||
if (data instanceof Error) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const streamId = addStream(data.content());
|
||||
|
||||
return { streamId };
|
||||
}
|
||||
|
||||
async function statFile(hash?: string, path?: string, fullPath?: string) {
|
||||
let stats: StatFileResponse = {
|
||||
exists: false,
|
||||
contentType: null,
|
||||
error: null,
|
||||
directory: false,
|
||||
files: [],
|
||||
timeout: false,
|
||||
};
|
||||
|
||||
client = client as IPFS;
|
||||
|
||||
let exists = await fileExists(hash, path, fullPath);
|
||||
fullPath = normalizePath(hash, path, fullPath);
|
||||
|
||||
if (exists instanceof Error) {
|
||||
stats.error = exists.toString();
|
||||
|
||||
if (exists.message.includes("aborted")) {
|
||||
stats.timeout = true;
|
||||
}
|
||||
|
||||
return stats;
|
||||
}
|
||||
stats.exists = true;
|
||||
|
||||
if (exists?.type === "directory") {
|
||||
stats.directory = true;
|
||||
stats.files = exists.node.Links.map((item) => item.Name) as string[];
|
||||
return stats;
|
||||
}
|
||||
|
||||
const { contentType } = await detectContentType(
|
||||
fullPath,
|
||||
client.cat(exists.cid)
|
||||
);
|
||||
stats.contentType = contentType ?? null;
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
async function fileExists(
|
||||
hash?: string,
|
||||
path?: string,
|
||||
fullPath?: string
|
||||
): Promise<Error | UnixFSEntry> {
|
||||
await initIpfs();
|
||||
client = client as IPFS;
|
||||
let ipfsPath = normalizePath(hash, path, fullPath);
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 5000);
|
||||
return await exporter(ipfsPath, repo.blocks, { signal: controller.signal });
|
||||
} catch (err: any) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveIpns(hash: string, path: string): Promise<string> {
|
||||
let fullPath = `${hash}/${path}`;
|
||||
|
||||
client = client as IPFS;
|
||||
|
||||
return (
|
||||
(await last(client.name.resolve(fullPath, { recursive: true }))) || path
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
stat_ipfs: async (args: any, context: object) => {
|
||||
// @ts-ignore
|
||||
if ("ipfs" !== context.chain) {
|
||||
return rpcError(ERR_INVALID_CHAIN);
|
||||
}
|
||||
|
||||
try {
|
||||
return await statFile(args?.hash, args?.path);
|
||||
} catch (e: any) {
|
||||
return rpcError((e as Error).message);
|
||||
}
|
||||
},
|
||||
stat_ipns: async (args: any, context: object) => {
|
||||
// @ts-ignore
|
||||
if ("ipfs" !== context.chain) {
|
||||
return rpcError(ERR_INVALID_CHAIN);
|
||||
}
|
||||
|
||||
let ipfsPath;
|
||||
|
||||
try {
|
||||
ipfsPath = await resolveIpns(args.hash, args.path);
|
||||
|
||||
return statFile(undefined, undefined, ipfsPath);
|
||||
} catch (e: any) {
|
||||
return rpcError((e as Error).message);
|
||||
}
|
||||
},
|
||||
|
||||
fetch_ipfs: async (args: any, context: object) => {
|
||||
// @ts-ignore
|
||||
if ("ipfs" !== context.chain) {
|
||||
return rpcError(ERR_INVALID_CHAIN);
|
||||
}
|
||||
try {
|
||||
const ret = await fetchFile(args?.hash, args?.path);
|
||||
if (ret instanceof Error) {
|
||||
throw ret;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} catch (e: any) {
|
||||
return rpcError((e as Error).message);
|
||||
}
|
||||
},
|
||||
fetch_ipns: async (args: any, context: object) => {
|
||||
// @ts-ignore
|
||||
if ("ipfs" !== context.chain) {
|
||||
return rpcError(ERR_INVALID_CHAIN);
|
||||
}
|
||||
let ipfsPath;
|
||||
try {
|
||||
ipfsPath = await resolveIpns(args.hash, args.path);
|
||||
const ret = await fetchFile(undefined, undefined, ipfsPath);
|
||||
if (ret instanceof Error) {
|
||||
throw ret;
|
||||
}
|
||||
|
||||
return ret;
|
||||
} catch (e: any) {
|
||||
return rpcError((e as Error).message);
|
||||
}
|
||||
},
|
||||
} as RpcMethodList;
|
Loading…
Reference in New Issue