diff --git a/bin/ipfs-deploy.js b/bin/ipfs-deploy.js index bf6eeb0..1e64451 100755 --- a/bin/ipfs-deploy.js +++ b/bin/ipfs-deploy.js @@ -49,12 +49,6 @@ const parser = yargs 'path' )} will be uploaded`, }, - port: { - default: '4002', - describe: - 'Externally reachable port for pinners to connect to ' + - 'local IPFS node', - }, }) .example( '$0', @@ -100,7 +94,6 @@ async function main() { const deployOptions = { publicDirPath: argv.path, copyHttpGatewayUrlToClipboard: !argv.noClipboard, - port: argv.port, open: !argv.noOpen, remotePinners: argv.pinner, dnsProviders: argv.dns, diff --git a/index.js b/index.js index cc3a0df..48cb65c 100644 --- a/index.js +++ b/index.js @@ -1,69 +1,23 @@ -const util = require('util') const { existsSync } = require('fs') -const stringify = require('json-stringify-safe') -const prettier = require('prettier') -const jsonifyError = require('jsonify-error') +const util = require('util') const trammel = util.promisify(require('trammel')) const byteSize = require('byte-size') const clipboardy = require('clipboardy') -const publicIp = require('public-ip') -const isPortReachable = require('is-port-reachable') const ipfsClient = require('ipfs-http-client') -const IPFS = require('ipfs') -const pinataSDK = require('@pinata/sdk') const updateCloudflareDnslink = require('dnslink-cloudflare') const ora = require('ora') const chalk = require('chalk') const doOpen = require('open') const _ = require('lodash') const fp = require('lodash/fp') -const neatFrame = require('neat-frame') -const { stripIndent } = require('common-tags') -const httpGatewayUrl = require('./src/gateway') -// # Pure functions +const { logError } = require('./src/logging') -function formatError(e) { - const prettierJson = obj => - prettier.format(stringify(obj), { - parser: 'json', - printWidth: 72, - tabWidth: 2, - }) - const beautifyStr = fp.pipe( - stripIndent, - str => neatFrame(str, { trim: false }) - ) - if (_.isError(e)) { - eStr = prettierJson(jsonifyError(e)) - } else if (_.isString(e)) { - eStr = e - } else if (_.isObjectLike(e)) { - eStr = prettierJson(e) - } - const beautifulErrorString = '\n' + beautifyStr(eStr) - return beautifulErrorString -} +const httpGatewayUrl = require('./src/gateway') +const { setupPinata } = require('./src/pinata') const white = chalk.whiteBright -// Effectful functions - -function logError(e) { - const errorString = formatError(e) - console.error(errorString) - return errorString -} - -async function isNodeReachable(port) { - const isIpv4Reachable = await isPortReachable(port, { - host: await publicIp.v4(), - timeout: 5000, - }) - - return isIpv4Reachable -} - function guessedPath() { // prettier-ignore const guesses = [ @@ -173,105 +127,6 @@ async function showSize(path) { } } -function startIpfsNode(port) { - return new Promise((resolve, reject) => { - const spinner = ora() - - spinner.start('♻️️ Starting temporary local IPFS node…\n') - const node = new IPFS({ - silent: true, - config: { - Addresses: { - Swarm: [`/ip4/0.0.0.0/tcp/${port}`], - }, - }, - }) - - node.on('ready', async () => { - spinner.succeed('☎️ Connected to temporary local IPFS node.') - spinner.start(`🔌 Checking if port ${port} is externally reachable…`) - const isReachable = await isNodeReachable(port) - if (isReachable) { - spinner.succeed(`📶 Port ${port} is externally reachable.`) - node.port = port - resolve(node) - } else { - spinner.fail(`💔 Could not reach port ${port} from the outside :(`) - spinner.info( - '💡 Please forward it or try a different one with the --port option.' - ) - reject(new Error(`Could not reach port ${port} from the outside`)) - } - }) - }) -} - -async function stopIpfsNode(node) { - const spinner = ora() - spinner.start('✋️ Stopping temporary local IPFS node…') - try { - await node.stop() - spinner.succeed('✋️ Stopped temporary local IPFS node.') - } catch (e) { - spinner.fail("🚂 Couldn't stop temporary local IPFS node.") - logError(e) - } -} - -async function pinToTmpIpfsNode(ipfsNode, publicDirPath) { - const spinner = ora() - - spinner.start('🔗 Pinning to temporary local IPFS node…') - const localPinResult = await ipfsNode.addFromFs(publicDirPath, { - recursive: true, - }) - const { hash } = localPinResult[localPinResult.length - 1] - spinner.succeed('📌 Pinned to temporary local IPFS node with hash:') - spinner.info(`🔗 ${hash}`) - return hash -} - -async function pinToPinata(ipfsNode, credentials, metadata = {}, hash) { - const spinner = ora() - - spinner.start(`📠 Requesting remote pin to ${white('pinata.cloud')}…`) - - if (fp.some(_.isEmpty)([credentials.apiKey, credentials.secretApiKey])) { - spinner.fail('💔 Missing credentials for Pinata API.') - spinner.warn('🧐 Check if these environment variables are present:') - logError(` - IPFS_DEPLOY_PINATA__API_KEY - IPFS_DEPLOY_PINATA__SECRET_API_KEY - - You can put them in a .env file if you want and they will be picked up. - `) - } else { - const nodeId = util.promisify(ipfsNode.id.bind(ipfsNode)) - const nodeInfo = await nodeId() - - const pinataOptions = { - host_nodes: [ - `/ip4/${await publicIp.v4()}/tcp/${ipfsNode.port}/ipfs/${nodeInfo.id}`, - ], - pinataMetadata: metadata, - } - - try { - const pinata = pinataSDK(credentials.apiKey, credentials.secretApiKey) - - await pinata.pinHashToIPFS(hash, pinataOptions) - - spinner.succeed("📌 It's pinned to Pinata now with hash:") - spinner.info(`🔗 ${hash}`) - return hash - } catch (e) { - spinner.fail("💔 Pinning to Pinata didn't work.") - logError(e) - return undefined - } - } -} - async function addToInfura(publicDirPath) { const spinner = ora() @@ -311,7 +166,6 @@ async function deploy({ publicDirPath, copyHttpGatewayUrlToClipboard = false, open = false, - port = '4002', remotePinners = ['infura'], dnsProviders = [], siteDomain, @@ -350,21 +204,15 @@ async function deploy({ } if (remotePinners.includes('pinata')) { - const ipfsNode = await startIpfsNode(port) - const localHash = await pinToTmpIpfsNode(ipfsNode, publicDirPath) - const pinataHash = await pinToPinata( - ipfsNode, - credentials.pinata, - { name: siteDomain || __dirname }, - localHash - ) + const addToPinata = setupPinata(credentials.pinata) + const pinataHash = await addToPinata(publicDirPath, { + name: siteDomain || __dirname, + }) if (pinataHash) { successfulRemotePinners = successfulRemotePinners.concat(['pinata']) - Object.assign(pinnedHashes, { localHash, pinataHash }) + Object.assign(pinnedHashes, { pinataHash }) } - - await stopIpfsNode(ipfsNode) } if (successfulRemotePinners.length > 0) { diff --git a/package.json b/package.json index 869eac5..3f13776 100644 --- a/package.json +++ b/package.json @@ -35,15 +35,15 @@ "node": ">=10.15.3" }, "dependencies": { - "@pinata/sdk": "^1.0.18", + "axios": "^0.19.0", "byte-size": "^5.0.1", "chalk": "^2.4.2", "clipboardy": "^2.0.0", "common-tags": "^2.0.0-alpha.1", "dnslink-cloudflare": "^2.0.1", "dotenv": "^8.0.0", - "ipfs": "^0.35.0", - "is-port-reachable": "^2.0.1", + "form-data": "^2.3.3", + "ipfs-http-client": "^32.0.1", "json-stringify-safe": "^5.0.1", "jsonify-error": "^1.4.5", "lodash": "^4.17.11", @@ -51,7 +51,7 @@ "open": "^6.2.0", "ora": "^3.4.0", "prettier": "^1.17.0", - "public-ip": "^3.1.0", + "recursive-fs": "^1.1.2", "trammel": "^2.1.0", "update-notifier": "^3.0.0", "yargs": "^13.2.2" diff --git a/src/logging.js b/src/logging.js new file mode 100644 index 0000000..273cfca --- /dev/null +++ b/src/logging.js @@ -0,0 +1,41 @@ +const _ = require('lodash') +const fp = require('lodash/fp') +const stringify = require('json-stringify-safe') +const prettier = require('prettier') +const jsonifyError = require('jsonify-error') +const neatFrame = require('neat-frame') +const { stripIndent } = require('common-tags') + +// # Pure functions + +function formatError(e) { + const prettierJson = obj => + prettier.format(stringify(obj), { + parser: 'json', + printWidth: 72, + tabWidth: 2, + }) + const beautifyStr = fp.pipe( + stripIndent, + str => neatFrame(str, { trim: false }) + ) + if (_.isError(e)) { + eStr = prettierJson(jsonifyError(e)) + } else if (_.isString(e)) { + eStr = e + } else if (_.isObjectLike(e)) { + eStr = prettierJson(e) + } + const beautifulErrorString = '\n' + beautifyStr(eStr) + return beautifulErrorString +} + +// Effectful functions + +function logError(e) { + const errorString = formatError(e) + console.error(errorString) + return errorString +} + +module.exports = { formatError, logError } diff --git a/src/pinata.js b/src/pinata.js new file mode 100644 index 0000000..fcbec72 --- /dev/null +++ b/src/pinata.js @@ -0,0 +1,62 @@ +const axios = require('axios') +const fs = require('fs') +const FormData = require('form-data') +const recursive = require('recursive-fs') +const ora = require('ora') +const { logError } = require('./logging') + +const chalk = require('chalk') +const white = chalk.whiteBright + +module.exports.setupPinata = ({ apiKey, secretApiKey }) => { + const url = 'https://api.pinata.cloud/pinning/pinFileToIPFS' + + // we gather the files from a local directory in this example, but a valid + // readStream is all that's needed for each file in the directory. + return async (publicDirPath, pinataMetadata = {}) => { + const spinner = ora() + spinner.start( + `📠 Uploading and pinning via https to ${white('pinata.cloud')}…` + ) + + try { + const response = await new Promise(resolve => { + recursive.readdirr(publicDirPath, (_err, _dirs, files) => { + let data = new FormData() + files.forEach(file => { + data.append('file', fs.createReadStream(file), { + // for each file stream, we need to include the correct + // relative file path + filepath: file, + }) + }) + + const metadata = JSON.stringify(pinataMetadata) + data.append('pinataMetadata', metadata) + + axios + .post(url, data, { + // Infinity is needed to prevent axios from erroring out with + // large directories + maxContentLength: 'Infinity', + headers: { + 'Content-Type': `multipart/form-data; boundary=${data._boundary}`, + pinata_api_key: apiKey, + pinata_secret_api_key: secretApiKey, + }, + }) + .then(resolve) + }) + }) + + spinner.succeed("📌 It's pinned to Pinata now with hash:") + const hash = response.data.IpfsHash + spinner.info(`🔗 ${hash}`) + return hash + } catch (e) { + spinner.fail("💔 Uploading to Pinata didn't work.") + logError(e) + return undefined + } + } +}