UNPKG

@taqueria/plugin-ipfs-pinata

Version:

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

380 lines (372 loc) • 11.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // index.ts var index_exports = {}; __export(index_exports, { getFileIPFSHash: () => getFileIPFSHash }); module.exports = __toCommonJS(index_exports); var import_node_sdk2 = require("@taqueria/node-sdk"); // src/ipfsHash.ts var import_crypto = require("crypto"); var import_promises = require("fs/promises"); async function getFileIPFSHash(filePath) { const fileContent = await (0, import_promises.readFile)(filePath); const hash = (0, import_crypto.createHash)("sha256").update(fileContent).digest("hex"); return hash; } // src/proxy.ts var import_node_sdk = require("@taqueria/node-sdk"); var import_path2 = __toESM(require("path"), 1); // src/file-processing.ts var import_promises2 = __toESM(require("fs/promises"), 1); var import_path = __toESM(require("path"), 1); async function* getFiles(fileOrDirPath) { const dirInfo = await import_promises2.default.stat(fileOrDirPath); if (dirInfo.isFile()) { yield fileOrDirPath; return; } const dirents = await import_promises2.default.readdir(fileOrDirPath, { withFileTypes: true }); for (const dirent of dirents) { const res = import_path.default.resolve(fileOrDirPath, dirent.name); if (dirent.isDirectory()) { yield* getFiles(res); } else { yield res; } } } var createFileProvider = async ({ fileOrDirPath, filter, shouldEstimateFileCount }) => { fileOrDirPath = import_path.default.resolve(fileOrDirPath); const pathInfo = await import_promises2.default.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 var import_form_data = __toESM(require("form-data"), 1); var import_fs = __toESM(require("fs"), 1); var import_node_fetch = __toESM(require("node-fetch"), 1); var publishFileToIpfs = async ({ auth, item }) => { const data = new import_form_data.default(); data.append("file", import_fs.default.createReadStream(item.filePath)); data.append( "pinataMetadata", JSON.stringify({ name: item.name }) ); const response = await (0, import_node_fetch.default)(`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 (0, import_node_fetch.default)(`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 var import_config = require("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: import_path2.default.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 (0, import_node_sdk.sendJsonRes)(result); } catch (err) { const error = err; if (error.message) { return (0, import_node_sdk.sendAsyncErr)(error.message); } } }; // index.ts import_node_sdk2.Plugin.create(() => ({ schema: "0.1", version: "0.4.0", alias: "pinata", tasks: [ import_node_sdk2.Task.create({ task: "publish", command: "publish [path]", description: "Upload and pin files using your pinata account.", aliases: [], handler: "proxy", positionals: [ import_node_sdk2.PositionalArg.create({ placeholder: "path", description: "Directory or file path to publish", type: "string" }) ], encoding: "json" }), import_node_sdk2.Task.create({ task: "pin", command: "pin [hash]", description: "Pin a file already on ipfs with your pinata account.", aliases: [], handler: "proxy", positionals: [ import_node_sdk2.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); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { getFileIPFSHash }); //# sourceMappingURL=index.cjs.map