@taqueria/plugin-ipfs-pinata
Version:
A plugin for Taqueria providing ipfs publishing and pinning using the Pinata service
145 lines (124 loc) • 3.79 kB
text/typescript
import { sendAsyncErr, sendAsyncRes, sendErr, sendJsonRes } from '@taqueria/node-sdk';
import { RequestArgs } from '@taqueria/node-sdk';
import { LoadedConfig, SanitizedAbsPath } from '@taqueria/node-sdk/types';
import path from 'path';
import { processFiles } from './file-processing';
import { PinataAuth, pinHash, publishFileToIpfs } from './pinata-api';
import { createProcessBackoffController } from './utils';
// Load .env for jwt token
// TODO: How should this be stored in a secure way?
import 'dotenv/config';
// TODO: What should this be, it was removed from the sdk
type PluginResponse =
| void
| {
render: 'table';
data: unknown[];
};
interface Opts extends RequestArgs.t {
readonly path?: string;
readonly hash?: string;
readonly task?: string;
}
const publishToIpfs = async (fileOrDirPath: undefined | string, auth: PinataAuth): Promise<PluginResponse> => {
if (!fileOrDirPath) {
throw new Error(`path was not provided`);
}
// Pinata is limited to 180 requests per minute
// So for the first 180 requests they can go fast
const { processWithBackoff } = createProcessBackoffController({
retryCount: 5,
targetRequestsPerMinute: 180,
});
const result = await processFiles({
fileOrDirPath,
parallelCount: 10,
processFile: async filePath => {
// // TEMP: Debug
// console.log(`publishing: ${filePath}`);
return processWithBackoff(() =>
publishFileToIpfs({
auth,
item: { filePath, name: path.basename(filePath) },
})
);
},
onProgress: ({ processedFilesCount, estimateFileCount }) => {
if (estimateFileCount && processedFilesCount % 10) {
let ratio = processedFilesCount / estimateFileCount;
if (ratio > 1) ratio = 1;
// // TODO: Call task sdk progress
// console.log(`Progress: ${(ratio * 100).toFixed(0)}%`);
}
},
});
// // TEMP: DEBUG: Show error
// if (result.failures.length) {
// console.log('❗ Failures:\n' + result.failures.map(f => `${f.filePath}: ${f.error}`).join('\n'));
// }
return {
render: 'table',
data: [
...result.failures.map(x => ({
'?': '❌',
filePath: x.filePath,
ipfsHash: undefined,
error: (x.error as { message?: string })?.message ?? JSON.stringify(x.error),
})),
...result.successes.map(x => ({
'?': '✔',
filePath: x.filePath,
ipfsHash: x.result.ipfsHash,
error: undefined,
})),
],
};
};
const pinToIpfs = async (hash: undefined | string, auth: PinataAuth): Promise<PluginResponse> => {
if (!hash) {
throw new Error(`ipfs hash was not provided`);
}
await pinHash({ ipfsHash: hash, auth });
return {
render: 'table',
data: [{ ipfsHash: hash }],
};
};
const execute = async (opts: Opts): Promise<PluginResponse> => {
const {
task,
path,
hash,
} = opts;
const auth: PinataAuth = {
// TODO: Where should this be stored?
// pinataJwtToken: (config as Record<string, any>).credentials.pinataJwtToken,
pinataJwtToken: process.env['pinataJwtToken'] as string,
};
if (!auth.pinataJwtToken) {
throw new Error(`The 'credentials.pinataJwtToken' was not found in config`);
}
switch (task) {
case 'publish':
return publishToIpfs(path, auth);
case 'pin':
return pinToIpfs(hash, auth);
default:
throw new Error(`${task} is not an understood task by the ipfs-pinata plugin`);
}
};
export default async (args: RequestArgs.t): Promise<PluginResponse> => {
const opts = args as Opts;
try {
const resultRaw = await execute(opts) as Record<string, unknown>;
// TODO: Fix deno parsing
// Without this, `data.reduce is not a function`
const result = ('data' in resultRaw) ? resultRaw.data : resultRaw;
return sendJsonRes(result);
} catch (err) {
const error = err as Error;
if (error.message) {
return sendAsyncErr(error.message);
}
}
};