UNPKG

@taqueria/plugin-ipfs-pinata

Version:

A plugin for Taqueria providing ipfs publishing and pinning using the Pinata service

340 lines (333 loc) • 8.86 kB
import { getFileIPFSHash } from "./chunk-6OD7MDAL.js"; // index.ts import { Plugin, PositionalArg, Task } from "@taqueria/node-sdk"; // src/proxy.ts import { sendAsyncErr, sendJsonRes } from "@taqueria/node-sdk"; import path2 from "path"; // src/file-processing.ts import fs from "fs/promises"; import path from "path"; async function* getFiles(fileOrDirPath) { const dirInfo = await fs.stat(fileOrDirPath); if (dirInfo.isFile()) { yield fileOrDirPath; return; } const dirents = await fs.readdir(fileOrDirPath, { withFileTypes: true }); for (const dirent of dirents) { const res = path.resolve(fileOrDirPath, dirent.name); if (dirent.isDirectory()) { yield* getFiles(res); } else { yield res; } } } var createFileProvider = async ({ fileOrDirPath, filter, shouldEstimateFileCount }) => { fileOrDirPath = path.resolve(fileOrDirPath); const pathInfo = await fs.stat(fileOrDirPath); if (!pathInfo.isFile() && !pathInfo.isDirectory()) { throw new Error(`The path '${fileOrDirPath}' is not a file or directory`); } let estimateFileCount = void 0; if (shouldEstimateFileCount) { estimateFileCount = 0; for await (const filePath of getFiles(fileOrDirPath)) { if (filter && !filter(filePath)) { continue; } estimateFileCount++; } } const fileGenerator = getFiles(fileOrDirPath); const getNextFile = async () => { let nextFile = (await fileGenerator.next()).value; if (!filter) { return nextFile; } while (nextFile && !filter(nextFile)) { nextFile = await getNextFile(); } return nextFile; }; return { getNextFile, estimateFileCount }; }; var processFiles = async ({ fileOrDirPath, processFile, filter, parallelCount = 10, onProgress }) => { const { getNextFile, estimateFileCount } = await createFileProvider({ fileOrDirPath, filter, shouldEstimateFileCount: true }); const successes = []; const failures = []; onProgress == null ? void 0 : onProgress({ processedFilesCount: 0, estimateFileCount }); await Promise.all([...new Array(parallelCount)].map(async (x) => { let fileToProcess = await getNextFile(); while (fileToProcess) { const progressInfo = { processedFilesCount: successes.length + failures.length, estimateFileCount }; onProgress == null ? void 0 : onProgress(progressInfo); try { const result = await processFile(fileToProcess, progressInfo); successes.push({ filePath: fileToProcess, result }); } catch (err) { failures.push({ filePath: fileToProcess, error: err }); } fileToProcess = await getNextFile(); } })); onProgress == null ? void 0 : onProgress({ processedFilesCount: successes.length + failures.length, estimateFileCount }); return { successes, failures }; }; // src/pinata-api.ts import FormData from "form-data"; import fs2 from "fs"; import fetch from "node-fetch"; var publishFileToIpfs = async ({ auth, item }) => { const data = new FormData(); data.append("file", fs2.createReadStream(item.filePath)); data.append( "pinataMetadata", JSON.stringify({ name: item.name }) ); const response = await fetch(`https://api.pinata.cloud/pinning/pinFileToIPFS`, { headers: { Authorization: `Bearer ${auth.pinataJwtToken}`, "Content-Type": `multipart/form-data; boundary=${data._boundary}` }, body: data, method: "post" }); if (!response.ok) { throw new Error(`Failed to upload '${item.name}' to ipfs ${response.statusText}`); } const uploadResult = await response.json(); return { ipfsHash: uploadResult.IpfsHash }; }; var pinHash = async ({ auth, ipfsHash }) => { const response = await fetch(`https://api.pinata.cloud/pinning/pinByHash`, { headers: { Authorization: `Bearer ${auth.pinataJwtToken}`, "Content-Type": "application/json" }, method: "post", body: JSON.stringify({ hashToPin: ipfsHash }) }); if (!response.ok) { throw new Error(`Failed to pin '${ipfsHash}' with pinata: ${response.statusText}`); } return; }; // src/utils.ts async function delay(timeout) { return await new Promise((resolve) => { setTimeout(resolve, timeout); }); } var createProcessBackoffController = ({ retryCount = 5, targetRequestsPerMinute = 180 }) => { let averageTimePerRequest = 5e3; let targetTimePerRequest = 6e4 / targetRequestsPerMinute; let lastTime = Date.now(); const processWithBackoff = async (process2) => { let attempt = 0; let lastError = void 0; while (attempt < retryCount) { try { let delayTimeMs = Math.max(10, targetTimePerRequest - averageTimePerRequest); await delay(Math.floor(delayTimeMs * (1 + 0.5 * Math.random()))); const result = await process2(); const timeNow = Date.now(); const timeElapsed = timeNow - lastTime; lastTime = timeNow; averageTimePerRequest = averageTimePerRequest * 0.97 + timeElapsed * 0.03; return result; } catch (err) { lastError = err; } averageTimePerRequest -= (attempt + 1) * 1e3; attempt++; } throw lastError; }; return { processWithBackoff }; }; // src/proxy.ts import "dotenv/config"; var publishToIpfs = async (fileOrDirPath, auth) => { if (!fileOrDirPath) { throw new Error(`path was not provided`); } const { processWithBackoff } = createProcessBackoffController({ retryCount: 5, targetRequestsPerMinute: 180 }); const result = await processFiles({ fileOrDirPath, parallelCount: 10, processFile: async (filePath) => { return processWithBackoff( () => publishFileToIpfs({ auth, item: { filePath, name: path2.basename(filePath) } }) ); }, onProgress: ({ processedFilesCount, estimateFileCount }) => { if (estimateFileCount && processedFilesCount % 10) { let ratio = processedFilesCount / estimateFileCount; if (ratio > 1) ratio = 1; } } }); return { render: "table", data: [ ...result.failures.map((x) => { var _a; return { "?": "\u274C", filePath: x.filePath, ipfsHash: void 0, error: ((_a = x.error) == null ? void 0 : _a.message) ?? JSON.stringify(x.error) }; }), ...result.successes.map((x) => ({ "?": "\u2714", filePath: x.filePath, ipfsHash: x.result.ipfsHash, error: void 0 })) ] }; }; var pinToIpfs = async (hash, auth) => { if (!hash) { throw new Error(`ipfs hash was not provided`); } await pinHash({ ipfsHash: hash, auth }); return { render: "table", data: [{ ipfsHash: hash }] }; }; var execute = async (opts) => { const { task, path: path3, hash } = opts; const auth = { // TODO: Where should this be stored? // pinataJwtToken: (config as Record<string, any>).credentials.pinataJwtToken, pinataJwtToken: process.env["pinataJwtToken"] }; if (!auth.pinataJwtToken) { throw new Error(`The 'credentials.pinataJwtToken' was not found in config`); } switch (task) { case "publish": return publishToIpfs(path3, auth); case "pin": return pinToIpfs(hash, auth); default: throw new Error(`${task} is not an understood task by the ipfs-pinata plugin`); } }; var proxy_default = async (args) => { const opts = args; try { const resultRaw = await execute(opts); const result = "data" in resultRaw ? resultRaw.data : resultRaw; return sendJsonRes(result); } catch (err) { const error = err; if (error.message) { return sendAsyncErr(error.message); } } }; // index.ts Plugin.create(() => ({ schema: "0.1", version: "0.4.0", alias: "pinata", tasks: [ Task.create({ task: "publish", command: "publish [path]", description: "Upload and pin files using your pinata account.", aliases: [], handler: "proxy", positionals: [ PositionalArg.create({ placeholder: "path", description: "Directory or file path to publish", type: "string" }) ], encoding: "json" }), Task.create({ task: "pin", command: "pin [hash]", description: "Pin a file already on ipfs with your pinata account.", aliases: [], handler: "proxy", positionals: [ PositionalArg.create({ placeholder: "hash", description: "Ipfs hash of the file or directory that is already on the ipfs network.", type: "string" }) ] }) ], proxy: proxy_default }), process.argv); export { getFileIPFSHash }; //# sourceMappingURL=index.js.map