Skip to content

Commit

Permalink
feat: use HTTP to pin to Pinata
Browse files Browse the repository at this point in the history
This eliminates the need for running an ipfs peer,
so no need to listen on any port, forward ports,
do NAT traversal or any of that.

It also means we can now upload from Travis CI!

refs #11
  • Loading branch information
agentofuser committed Jun 7, 2019
1 parent 2d4e2c9 commit 14e7453
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 172 deletions.
7 changes: 0 additions & 7 deletions bin/ipfs-deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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,
Expand Down
170 changes: 9 additions & 161 deletions index.js
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -311,7 +166,6 @@ async function deploy({
publicDirPath,
copyHttpGatewayUrlToClipboard = false,
open = false,
port = '4002',
remotePinners = ['infura'],
dnsProviders = [],
siteDomain,
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,23 @@
"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",
"neat-frame": "https://github.com/agentofuser/neat-frame#wrap-ansi-options",
"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"
Expand Down
41 changes: 41 additions & 0 deletions src/logging.js
Original file line number Diff line number Diff line change
@@ -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 }
62 changes: 62 additions & 0 deletions src/pinata.js
Original file line number Diff line number Diff line change
@@ -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
}
}
}

0 comments on commit 14e7453

Please sign in to comment.