@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
JavaScript
;
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(new Uint8Array(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