From 14c724bf9c5102dd5ea40a2497abab062ab88951 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 9 Apr 2024 06:36:23 -0700 Subject: [PATCH 1/9] Prototyping IPFS file download --- package-lock.json | 6 +- package.json | 2 + src/controllers/rest-api/ipfs/controller.js | 42 ++++++ src/controllers/rest-api/ipfs/index.js | 1 + src/use-cases/index.js | 3 + src/use-cases/ipfs-use-cases.js | 136 ++++++++++++++++++++ 6 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 src/use-cases/ipfs-use-cases.js diff --git a/package-lock.json b/package-lock.json index a9b2a16..1188898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ipfs-bch-wallet-consumer", - "version": "3.0.0", + "version": "3.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ipfs-bch-wallet-consumer", - "version": "3.0.0", + "version": "3.1.1", "license": "MIT", "dependencies": { "@chainsafe/libp2p-gossipsub": "11.0.1", @@ -27,6 +27,7 @@ "glob": "7.1.6", "helia": "2.1.0", "helia-coord": "1.5.6", + "ipfs-unixfs-exporter": "13.5.0", "jsonrpc-lite": "2.2.0", "jsonwebtoken": "8.5.1", "jwt-bch-lib": "1.3.0", @@ -43,6 +44,7 @@ "koa2-ratelimit": "0.9.1", "libp2p": "1.2.1", "line-reader": "0.4.0", + "mime-types": "2.1.35", "minimal-slp-wallet": "5.11.2", "mongoose": "5.13.14", "node-fetch": "npm:@achingbrain/node-fetch@2.6.7", diff --git a/package.json b/package.json index 800ca49..01ea6e2 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "glob": "7.1.6", "helia": "2.1.0", "helia-coord": "1.5.6", + "ipfs-unixfs-exporter": "13.5.0", "jsonrpc-lite": "2.2.0", "jsonwebtoken": "8.5.1", "jwt-bch-lib": "1.3.0", @@ -58,6 +59,7 @@ "koa2-ratelimit": "0.9.1", "libp2p": "1.2.1", "line-reader": "0.4.0", + "mime-types": "2.1.35", "minimal-slp-wallet": "5.11.2", "mongoose": "5.13.14", "node-fetch": "npm:@achingbrain/node-fetch@2.6.7", diff --git a/src/controllers/rest-api/ipfs/controller.js b/src/controllers/rest-api/ipfs/controller.js index d883288..22e6618 100644 --- a/src/controllers/rest-api/ipfs/controller.js +++ b/src/controllers/rest-api/ipfs/controller.js @@ -3,6 +3,7 @@ */ // Global npm libraries +import mime from 'mime-types' // Local libraries import wlogger from '../../../adapters/wlogger.js' @@ -34,6 +35,7 @@ class IpfsRESTControllerLib { this.handleError = this.handleError.bind(this) this.connect = this.connect.bind(this) this.getThisNode = this.getThisNode.bind(this) + this.viewFile = this.viewFile.bind(this) } /** @@ -125,6 +127,46 @@ class IpfsRESTControllerLib { } } + /** + * @api {get} /ipfs/view/:cid Retrieve and display a file via its IPFS CID + * @apiPermission public + * @apiName GetCidView + * @apiGroup REST BCH + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/view/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om + * + */ + async viewFile (ctx) { + try { + const { cid } = ctx.params + + // const file = await this.adapters.ipfs.ipfs.blockstore.get(cid) + // return file + + // const cid = ctx.params.cid + + const { filename, readStream } = await this.useCases.ipfs.downloadCid({ cid }) + + // ctx.body = ctx.req.pipe(readStream) + + // Lookup the mime type from the filename. + const contentType = mime.lookup(filename) + + ctx.set('Content-Type', contentType) + ctx.set( + 'Content-Disposition', + // 'inline; filename="' + filename + '"' + `inline; filename="${filename}"` + ) + ctx.body = readStream + } catch (err) { + // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) + console.log('Error in ipfs/controller.js/viewFile(): ', err) + this.handleError(ctx, err) + } + } + // DRY error handler handleError (ctx, err) { // If an HTTP status is specified by the buisiness logic, use that. diff --git a/src/controllers/rest-api/ipfs/index.js b/src/controllers/rest-api/ipfs/index.js index d787f73..842dacc 100644 --- a/src/controllers/rest-api/ipfs/index.js +++ b/src/controllers/rest-api/ipfs/index.js @@ -56,6 +56,7 @@ class IpfsRouter { this.router.post('/relays', this.ipfsRESTController.getRelays) this.router.post('/connect', this.ipfsRESTController.connect) this.router.get('/node', this.ipfsRESTController.getThisNode) + this.router.get('/view/:cid', this.ipfsRESTController.viewFile) // Attach the Controller routes to the Koa app. app.use(this.router.routes()) diff --git a/src/use-cases/index.js b/src/use-cases/index.js index f6a87a6..514314e 100644 --- a/src/use-cases/index.js +++ b/src/use-cases/index.js @@ -4,7 +4,9 @@ https://troutsblog.com/blog/clean-architecture */ +// Local libraries import UserUseCases from './user.js' +import IpfsUseCases from './ipfs-use-cases.js' class UseCases { constructor (localConfig = {}) { @@ -23,6 +25,7 @@ class UseCases { // console.log('use-cases/index.js localConfig: ', localConfig) this.user = new UserUseCases(localConfig) + this.ipfs = new IpfsUseCases(localConfig) } // Run any startup Use Cases at the start of the app. diff --git a/src/use-cases/ipfs-use-cases.js b/src/use-cases/ipfs-use-cases.js new file mode 100644 index 0000000..8c40c07 --- /dev/null +++ b/src/use-cases/ipfs-use-cases.js @@ -0,0 +1,136 @@ +/* + Use cases for working with IPFS. +*/ + +// Global npm libraries +// import Wallet from 'minimal-slp-wallet' +// import { CID } from 'multiformats' +// import RetryQueue from '@chris.troutner/retry-queue' +import { exporter } from 'ipfs-unixfs-exporter' +import { Duplex } from 'stream' + +// Local libraries +// import PinEntity from '../entities/pin.js' +// import config from '../../config/index.js' + +// const PSF_TOKEN_ID = '38e97c5d7d3585a2cbf3f9580c82ca33985f9cb0845d4dcce220cb709f9538b0' + +class IpfsUseCases { + constructor (localConfig = {}) { + // console.log('User localConfig: ', localConfig) + this.adapters = localConfig.adapters + if (!this.adapters) { + throw new Error( + 'Instance of adapters must be passed in when instantiating IPFS Use Cases library.' + ) + } + + // Encapsulate dependencies + this.exporter = exporter + + // Bind 'this' object to all class subfunctions. + this.downloadCid = this.downloadCid.bind(this) + this.downloadCid2 = this.downloadCid2.bind(this) + } + + // Download a pinned file, given its CID. + // Returns a readable stream. + async downloadCid (inObj = {}) { + try { + const { cid } = inObj + + if (!cid) throw new Error('CID is undefined') + + // const Pins = this.adapters.localdb.Pins + // let existingModel = await Pins.find({ cid }) + // existingModel = existingModel[0] + // console.log('existingModel: ', existingModel) + + // if (!existingModel) { + // throw new Error(`Database model for CID ${cid} does not exist.`) + // } + // + // if (!existingModel.dataPinned) { + // throw new Error('File has not been pinned. Not available.') + // } + + const helia = this.adapters.ipfs.ipfs + + // Convert the file to a Buffer. + const fileChunks = [] + for await (const chunk of helia.fs.cat(cid)) { + fileChunks.push(chunk) + } + const fileBuf = Buffer.concat(fileChunks) + + // Convert the Buffer into a readable stream + const bufferToStream = (myBuffer) => { + const tmp = new Duplex() + tmp.push(myBuffer) + tmp.push(null) + return tmp + } + const readStream = bufferToStream(fileBuf) + + const filename = 'test.jpg' + + return { filename, readStream } + } catch (err) { + console.error('Error in use-cases/ipfs.js/dowloadCid()') + throw err + } + } + + async downloadCid2 (inObj = {}) { + try { + const { cid } = inObj + + // console.log(`downloadFile() retrieving this CID: ${cid}, with fileName: ${fileName}, and path: ${path}`) + + const blockstore = this.adapters.ipfs.ipfs.blockstore + const entry = await this.exporter(cid, blockstore) + + console.info(entry.cid) // Qmqux + console.log('entry: ', entry) + // console.info(entry.unixfs.fileSize()) // 4 + + // const filePath = `${path}/${fileName}` + // console.log(`filePath: ${filePath}`) + // const writableStream = this.fs.createWriteStream(filePath) + // + // writableStream.on('error', this.writeStreamError) + // + // writableStream.on('finish', this.writeStreamFinished) + // + + const fileChunks = [] + for await (const buf of entry.content()) { + fileChunks.push(buf) + } + const fileBuf = Buffer.concat(fileChunks) + + // + // writableStream.end() + + // Convert the Buffer into a readable stream + const bufferToStream = (myBuffer) => { + const tmp = new Duplex() + tmp.push(myBuffer) + tmp.push(null) + return tmp + } + const readStream = bufferToStream(fileBuf) + + const filename = 'test.jpg' + + return { filename, readStream } + + // return { cid } + } catch (err) { + console.error('Error in ipfs-use-cases.js/downloadCid()') + throw err + } + } +} + +export default IpfsUseCases From 895696802744a3987ead3b709a74628b22c7f757 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 9 Apr 2024 08:08:53 -0700 Subject: [PATCH 2/9] Commenting out downloadCid2() --- config/env/common.js | 2 +- package-lock.json | 1 - package.json | 1 - src/use-cases/ipfs-use-cases.js | 106 ++++++++++++++++---------------- 4 files changed, 54 insertions(+), 56 deletions(-) diff --git a/config/env/common.js b/config/env/common.js index c74d9f6..1f79500 100644 --- a/config/env/common.js +++ b/config/env/common.js @@ -27,7 +27,7 @@ const ipfsCoordName = process.env.COORD_NAME export default { // Configure TCP port. - port: process.env.PORT || 5005, + port: process.env.PORT || 5015, // Password for HTML UI that displays logs. logPass: 'test', diff --git a/package-lock.json b/package-lock.json index 1188898..4ede96b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,6 @@ "glob": "7.1.6", "helia": "2.1.0", "helia-coord": "1.5.6", - "ipfs-unixfs-exporter": "13.5.0", "jsonrpc-lite": "2.2.0", "jsonwebtoken": "8.5.1", "jwt-bch-lib": "1.3.0", diff --git a/package.json b/package.json index 01ea6e2..29bd76a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "glob": "7.1.6", "helia": "2.1.0", "helia-coord": "1.5.6", - "ipfs-unixfs-exporter": "13.5.0", "jsonrpc-lite": "2.2.0", "jsonwebtoken": "8.5.1", "jwt-bch-lib": "1.3.0", diff --git a/src/use-cases/ipfs-use-cases.js b/src/use-cases/ipfs-use-cases.js index 8c40c07..391827b 100644 --- a/src/use-cases/ipfs-use-cases.js +++ b/src/use-cases/ipfs-use-cases.js @@ -6,7 +6,7 @@ // import Wallet from 'minimal-slp-wallet' // import { CID } from 'multiformats' // import RetryQueue from '@chris.troutner/retry-queue' -import { exporter } from 'ipfs-unixfs-exporter' +// import { exporter } from 'ipfs-unixfs-exporter' import { Duplex } from 'stream' // Local libraries @@ -26,11 +26,11 @@ class IpfsUseCases { } // Encapsulate dependencies - this.exporter = exporter + // this.exporter = exporter // Bind 'this' object to all class subfunctions. this.downloadCid = this.downloadCid.bind(this) - this.downloadCid2 = this.downloadCid2.bind(this) + // this.downloadCid2 = this.downloadCid2.bind(this) } // Download a pinned file, given its CID. @@ -81,56 +81,56 @@ class IpfsUseCases { } } - async downloadCid2 (inObj = {}) { - try { - const { cid } = inObj - - // console.log(`downloadFile() retrieving this CID: ${cid}, with fileName: ${fileName}, and path: ${path}`) - - const blockstore = this.adapters.ipfs.ipfs.blockstore - const entry = await this.exporter(cid, blockstore) - - console.info(entry.cid) // Qmqux - console.log('entry: ', entry) - // console.info(entry.unixfs.fileSize()) // 4 - - // const filePath = `${path}/${fileName}` - // console.log(`filePath: ${filePath}`) - // const writableStream = this.fs.createWriteStream(filePath) - // - // writableStream.on('error', this.writeStreamError) - // - // writableStream.on('finish', this.writeStreamFinished) - // - - const fileChunks = [] - for await (const buf of entry.content()) { - fileChunks.push(buf) - } - const fileBuf = Buffer.concat(fileChunks) - - // - // writableStream.end() - - // Convert the Buffer into a readable stream - const bufferToStream = (myBuffer) => { - const tmp = new Duplex() - tmp.push(myBuffer) - tmp.push(null) - return tmp - } - const readStream = bufferToStream(fileBuf) - - const filename = 'test.jpg' - - return { filename, readStream } - - // return { cid } - } catch (err) { - console.error('Error in ipfs-use-cases.js/downloadCid()') - throw err - } - } + // async downloadCid2 (inObj = {}) { + // try { + // const { cid } = inObj + // + // // console.log(`downloadFile() retrieving this CID: ${cid}, with fileName: ${fileName}, and path: ${path}`) + // + // const blockstore = this.adapters.ipfs.ipfs.blockstore + // const entry = await this.exporter(cid, blockstore) + // + // console.info(entry.cid) // Qmqux + // console.log('entry: ', entry) + // // console.info(entry.unixfs.fileSize()) // 4 + // + // // const filePath = `${path}/${fileName}` + // // console.log(`filePath: ${filePath}`) + // // const writableStream = this.fs.createWriteStream(filePath) + // // + // // writableStream.on('error', this.writeStreamError) + // // + // // writableStream.on('finish', this.writeStreamFinished) + // // + // + // const fileChunks = [] + // for await (const buf of entry.content()) { + // fileChunks.push(buf) + // } + // const fileBuf = Buffer.concat(fileChunks) + // + // // + // // writableStream.end() + // + // // Convert the Buffer into a readable stream + // const bufferToStream = (myBuffer) => { + // const tmp = new Duplex() + // tmp.push(myBuffer) + // tmp.push(null) + // return tmp + // } + // const readStream = bufferToStream(fileBuf) + // + // const filename = 'test.jpg' + // + // return { filename, readStream } + // + // // return { cid } + // } catch (err) { + // console.error('Error in ipfs-use-cases.js/downloadCid()') + // throw err + // } + // } } export default IpfsUseCases From 3c9bbb097e97767fcb712939536ac01a3ba49fce Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 9 Apr 2024 20:56:40 -0700 Subject: [PATCH 3/9] Successfully recieving result from file pin JSON RPC call --- config/env/common.js | 5 + src/adapters/index.js | 2 + src/adapters/ipfs-files/index.js | 325 ++++++++++++++++++++++++++++++ src/adapters/ipfs/ipfs-coord.js | 98 ++++++++- src/controllers/json-rpc/index.js | 6 + src/use-cases/ipfs-use-cases.js | 20 ++ 6 files changed, 454 insertions(+), 2 deletions(-) create mode 100644 src/adapters/ipfs-files/index.js diff --git a/config/env/common.js b/config/env/common.js index 1f79500..dc31aa6 100644 --- a/config/env/common.js +++ b/config/env/common.js @@ -159,6 +159,11 @@ export default { // Preferred P2WDB provider preferredP2wdbProvider: process.env.PREFERRED_P2WDB_PROVIDER ? process.env.PREFERRED_P2WDB_PROVIDER + : '', + + // Preferred P2WDB provider + preferredIpfsFileProvider: process.env.PREFERRED_IPFS_FILE_PROVIDER + ? process.env.PREFERRED_IPFS_FILE_PROVIDER : '' } diff --git a/src/adapters/index.js b/src/adapters/index.js index 3abad47..38bd72f 100644 --- a/src/adapters/index.js +++ b/src/adapters/index.js @@ -15,6 +15,7 @@ import Passport from './passport.js' import Nodemailer from './nodemailer.js' import BCH from './bch/index.js' import P2WDB from './p2wdb/index.js' +import IpfsFiles from './ipfs-files/index.js' // const { wlogger } = require('./wlogger') import JSONFiles from './json-files.js' @@ -45,6 +46,7 @@ class Adapters { this.bch = new BCH(localConfig) this.p2wdb = new P2WDB(localConfig) this.wallet = new Wallet(localConfig) + this.ipfsFiles = new IpfsFiles(localConfig) // Get a valid JWT API key and instance bch-js. this.fullStackJwt = new FullStackJWT(config) diff --git a/src/adapters/ipfs-files/index.js b/src/adapters/ipfs-files/index.js new file mode 100644 index 0000000..543b03b --- /dev/null +++ b/src/adapters/ipfs-files/index.js @@ -0,0 +1,325 @@ +/* + This library contains code for interfacing to the ipfs-file-pin-service using + JSON RPC over IPFS. Most of these functions are called by the /files REST API + endpoints and the IPFS Use Cases library. +*/ + +// Public npm libraries +import { v4 as uid } from 'uuid' +import jsonrpc from 'jsonrpc-lite' + +// Local libraries +import wlogger from '../wlogger.js' + +// let _this + +class IpfsFilesAdapter { + constructor (localConfig = {}) { + // console.log('BCH localConfig: ', localConfig) + this.ipfs = localConfig.ipfs + if (!this.ipfs) { + throw new Error( + 'An instance of IPFS must be passed when instantiating the IPFS Files Adapter library.' + ) + } + // this.eventEmitter = localConfig.eventEmitter + // if (!this.eventEmitter) { + // throw new Error( + // 'An instance of an EventEmitter must be passed when instantiating the adapters.' + // ) + // } + + // Connect the RPC handler when the event fires with new data. + // this.eventEmitter.on('rpcData', this.rpcHandler) + + // Encapsulate dependencies + this.uid = uid + this.jsonrpc = jsonrpc + + // A queue for holding RPC data that has arrived. + this.rpcDataQueue = [] + + // _this = this // Global handle on instance of this Class. + + // Bind 'this' object to all subfunctions for this Class + this.rpcHandler = this.rpcHandler.bind(this) + this.getStatus = this.getStatus.bind(this) + this.selectProvider = this.selectProvider.bind(this) + } + + // This handler is triggered when RPC data comes in over IPFS. + // Handle RPC input, and add the response to the RPC queue. + // Once in the queue, it will get processed by waitForRPCResponse() + rpcHandler (data) { + try { + // Convert string input into an object. + // const jsonData = JSON.parse(data) + + // console.log(`JSON RPC response for ID ${data.payload.id} received.`) + + this.rpcDataQueue.push(data) + } catch (err) { + console.error('Error in files/rpcHandler(): ', err) + // Do not throw error. This is a top-level function. + } + } + + // Get the status of BCH wallet services this node can talk to. Returns an + // array of BCH wallet service providers. + async getStatus () { + try { + const peerData = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode.peerData + // console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`) + + // const status = { + // state: this.ipfs.ipfsCoordAdapter.state + // } + + // Add names to the IPFS IDs for each provider. + const initialServiceProviders = + this.ipfs.ipfsCoordAdapter.state.ipfsFileProviders + const serviceProviders = [] + for (let i = 0; i < initialServiceProviders.length; i++) { + const thisProvider = initialServiceProviders[i] + + const providerData = peerData.filter((x) => x.from === thisProvider) + + if (providerData.length) { + const provObj = { + ipfsId: thisProvider, + name: providerData[0].data.jsonLd.name + } + + serviceProviders.push(provObj) + } + } + + // console.log( + // `serviceProviders: ${JSON.stringify(serviceProviders, null, 2)}` + // ) + + const outObj = { + serviceProviders, + selectedProvider: this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider + } + // console.log('outObj: ', outObj) + + return outObj + } catch (err) { + // console.log('createUser() error: ', err) + wlogger.error('Error in adapters/files/getStatus()') + throw err + } + } + + // Choose the ipfs-file-pin-service to use. + async selectProvider (providerId) { + try { + this.ipfs.ipfsCoordAdapter.config.selectedIpfsFileProvider = providerId + + return true + } catch (err) { + // console.log('createUser() error: ', err) + wlogger.error('Error in adapters/files/getStatus()') + throw err + } + } + + // Given a CID, query the ipfs-file-pin-service to get the file metadata, like + // the file name. + async getFileMetadata (inObj = {}) { + try { + const { cid } = inObj + + // Input validation. + if (!cid || typeof cid !== 'string') { + throw new Error('getFileMetadata() cid input hash must be a string.') + } + + // Throw an error if this IPFS node has not yet made a connection to a + // wallet service provider. + const selectedProvider = + this.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider + if (!selectedProvider) { + throw new Error('No IPFS File Pin Service provider available yet.') + } + + const rpcData = { + endpoint: 'getFileMetadata', + cid + } + + // Generate a UUID for the call. + const rpcId = this.uid() + + // Generate a JSON RPC command. + const cmd = this.jsonrpc.request(rpcId, 'file-pin', rpcData) + const cmdStr = JSON.stringify(cmd) + // console.log('cmdStr: ', cmdStr) + + // Send the RPC command to selected wallet service. + const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode + await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage( + selectedProvider, + cmdStr, + thisNode + ) + + // Wait for data to come back from the wallet service. + const data = await this.waitForRPCResponse(rpcId) + // console.log('getFileMetadata() data: ', data) + + return data + } catch (err) { + wlogger.error('Error in adapters/files/getFileMetadata()') + throw err + } + } + + // Read an entry from the P2WDB, given an entry hash. + async getEntryByHash (hash) { + try { + // Input validation. + if (!hash || typeof hash !== 'string') { + throw new Error('getEntry() input hash must be a string.') + } + + // Throw an error if this IPFS node has not yet made a connection to a + // wallet service provider. + const selectedProvider = + this.ipfs.ipfsCoordAdapter.state.selectedP2wdbProvider + if (!selectedProvider) { + throw new Error('No P2WDB Service provider available yet.') + } + + const rpcData = { + endpoint: 'getByHash', + hash + } + + // Generate a UUID for the call. + const rpcId = this.uid() + + // Generate a JSON RPC command. + const cmd = this.jsonrpc.request(rpcId, 'p2wdb', rpcData) + const cmdStr = JSON.stringify(cmd) + // console.log('cmdStr: ', cmdStr) + + // Send the RPC command to selected wallet service. + const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode + await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage( + selectedProvider, + cmdStr, + thisNode + ) + + // Wait for data to come back from the wallet service. + const data = await this.waitForRPCResponse(rpcId) + + return data + } catch (err) { + console.error('Error in getEntryByHash()') + throw err + } + } + + async writeEntry (writeObj) { + try { + const { txid, signature, message, data } = writeObj + + // Throw an error if this IPFS node has not yet made a connection to a + // wallet service provider. + const selectedProvider = + this.ipfs.ipfsCoordAdapter.state.selectedP2wdbProvider + if (!selectedProvider) { + throw new Error('No P2WDB Service provider available yet.') + } + + const rpcData = { + endpoint: 'write', + txid, + signature, + message, + data + } + + // Generate a UUID for the call. + const rpcId = this.uid() + + // Generate a JSON RPC command. + const cmd = this.jsonrpc.request(rpcId, 'p2wdb', rpcData) + const cmdStr = JSON.stringify(cmd) + // console.log('cmdStr: ', cmdStr) + + // Send the RPC command to selected wallet service. + const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode + await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage( + selectedProvider, + cmdStr, + thisNode + ) + + // Wait for data to come back from the wallet service. + const result = await this.waitForRPCResponse(rpcId) + + return result + } catch (err) { + console.error('Error in adapters/p2wdb/writeEntry()') + throw err + } + } + + // Returns a promise that resolves to data when the RPC response is recieved. + async waitForRPCResponse (rpcId) { + try { + // Initialize variables for tracking the return data. + let dataFound = false + let cnt = 0 + + // Default return value, if the remote computer does not respond in time. + let data = { + success: false, + message: 'request timed out', + data: '' + } + + // Loop that waits for a response from the service provider. + do { + // console.log(`this.rpcDataQueue.length: ${this.rpcDataQueue.length}`) + for (let i = 0; i < this.rpcDataQueue.length; i++) { + const rawData = this.rpcDataQueue[i] + // console.log(`rawData: ${JSON.stringify(rawData, null, 2)}`) + + if (rawData.payload.id === rpcId) { + dataFound = true + // console.log('data was found in the queue') + + data = rawData.payload.result.value + + // Remove the data from the queue + this.rpcDataQueue.splice(i, 1) + + break + } + } + + // Wait between loops. + // await this.sleep(1000) + await this.ipfs.ipfsCoordAdapter.wallet.bchjs.Util.sleep(2000) + + cnt++ + + // Exit if data was returned, or the window for a response expires. + } while (!dataFound && cnt < 10) + // console.log(`dataFound: ${dataFound}, cnt: ${cnt}`) + + return data + } catch (err) { + console.error('Error in waitForRPCResponse()') + throw err + } + } +} + +// module.exports = P2wdbAdapter +export default IpfsFilesAdapter diff --git a/src/adapters/ipfs/ipfs-coord.js b/src/adapters/ipfs/ipfs-coord.js index 9e9451f..e07b025 100644 --- a/src/adapters/ipfs/ipfs-coord.js +++ b/src/adapters/ipfs/ipfs-coord.js @@ -23,6 +23,8 @@ const MIN_BCH_WALLET_VERSION = '1.11.11' const WALLET_PROTOCOL = 'bch-wallet' const MIN_P2WDB_VERSION = '1.4.0' const P2WDB_PROTOCOL = 'p2wdb' +const MIN_FILE_PIN_VERSION = '1.0.0' +const FILE_PIN_PROTOCOL = 'ipfs-file-pin-service' let _this @@ -56,9 +58,13 @@ class IpfsCoordAdapter { // Periodically poll services for available wallet service providers. this.pollBchServiceInterval = setInterval(this.pollForBchServices, 10000) + this.pollIpfsFileServiceInterval = setInterval( + this.pollForIpfsFileServices, + 11000 + ) this.pollP2wdbServiceInterval = setInterval( this.pollForP2wdbServices, - 11000 + 12000 ) // State object. TODO: Make this more robust. @@ -66,7 +72,9 @@ class IpfsCoordAdapter { serviceProviders: [], selectedServiceProvider: '', p2wdbProviders: [], - selectedP2wdbProvider: '' + selectedP2wdbProvider: '', + ipfsFileProviders: [], + selectedIpfsFileProvider: '' } _this = this @@ -308,6 +316,92 @@ class IpfsCoordAdapter { } } + // Poll the ipfs-coord coordination channel for available ipfs-file-pin-service + // providers. This method is called periodically by a timer-interval. + pollForIpfsFileServices () { + try { + // console.log('pollForIpfsFileServices() polling for BCH service') + + // An array of IPFS IDs of other nodes in the coordination pubsub channel. + const peers = _this.ipfsCoord.thisNode.peerList + // console.log(`peers: ${JSON.stringify(peers, null, 2)}`) + + // Array of objects. Each object is the IPFS ID of the peer and contains + // data about that peer. + const peerData = _this.ipfsCoord.thisNode.peerData + // console.log(`peerData: ${JSON.stringify(peerData, null, 2)}`) + + for (let i = 0; i < peers.length; i++) { + const thisPeer = peers[i] + const thisData = peerData.filter((x) => x.from === thisPeer) + const thisPeerData = thisData[0] + + // Create a 'fingerprint' that defines the wallet service. + const protocol = thisPeerData.data.jsonLd.protocol + const version = thisPeerData.data.jsonLd.version + // console.log( + // `debug: peer ${thisPeer} uses protocol: ${protocol} v${version}`, + // ) + + let versionMatches = false + if (version) { + // versionMatches = _this.semver.gt(version, MIN_FILE_PIN_VERSION) + versionMatches = _this.semver.satisfies(version, `>=${MIN_FILE_PIN_VERSION}`) + } + + // Ignore any peers that don't match the fingerprint for a BCH wallet + // service. + if (protocol && protocol.includes(FILE_PIN_PROTOCOL) && versionMatches) { + // console.log('Matching peer: ', thisPeerData) + + // Temporary business logic. + // Use the first available wallet service detected. + if (_this.state.ipfsFileProviders.length === 0) { + _this.state.selectedIpfsFileProvider = thisPeer + + // Persist the config setting, so it can be used by other commands. + // _this.conf.set('selectedService', thisPeer) + console.log(`---->IPFS File service selected: ${thisPeer}`) + } + + // console.log('preferredProvider: ', _this.config.preferredProvider) + + // If a preferred provider is set in the config file, then connect + // to the preferred provider when it's discovered. + if ( + _this.config.preferredIpfsFileProvider && + thisPeer === _this.config.preferredIpfsFileProvider + ) { + _this.state.selectedIpfsFileProvider = thisPeer + } + + // console.log('selectedServiceProvider: ', _this.state.selectedServiceProvider) + + // Check if the peer has already been added to the list of providers. + const alreadyExists = _this.state.ipfsFileProviders.filter( + (x) => x === thisPeer + ) + + // Add the peer to the list of serviceProviders if it doesn't already + // exist in the list. + if (!alreadyExists.length) { + _this.state.ipfsFileProviders.push(thisPeer) + } + } + } + } catch (err) { + // catch and handle known failure mode. + if ( + err.message.includes("Cannot read property 'peerList' of undefined") + ) { + return + } + + console.error('Error in pollForIpfsFileServices(): ', err) + // Do not throw error. This is a top-level function. + } + } + // This method handles input coming in from other IPFS peers. // It passes the data on to the REST API library by emitting an event. // peerInputHandler (data) { diff --git a/src/controllers/json-rpc/index.js b/src/controllers/json-rpc/index.js index 7d341bf..eb7b005 100644 --- a/src/controllers/json-rpc/index.js +++ b/src/controllers/json-rpc/index.js @@ -97,6 +97,7 @@ class JSONRPC { wlogger.info( `JSON RPC received from ${from}, ID: ${parsedData.payload.id}, type: ${parsedData.type}` ) + // console.log('parsedData: ', JSON.stringify(parsedData, null, 2)) } // console.log(`parsedData: ${JSON.stringify(parsedData, null, 2)}`) @@ -113,6 +114,11 @@ class JSONRPC { console.log('routing to BCH adapter') retObj = await _this.adapters.bch.rpcHandler(parsedData) } + + if (parsedData.payload.result.method === 'file-pin') { + console.log('routing to IPFS Files adapter') + retObj = await _this.adapters.ipfsFiles.rpcHandler(parsedData) + } } catch (err) { /* exit quietly */ } diff --git a/src/use-cases/ipfs-use-cases.js b/src/use-cases/ipfs-use-cases.js index 391827b..d5e7211 100644 --- a/src/use-cases/ipfs-use-cases.js +++ b/src/use-cases/ipfs-use-cases.js @@ -54,6 +54,8 @@ class IpfsUseCases { // throw new Error('File has not been pinned. Not available.') // } + await this.getCidMetadata({ cid }) + const helia = this.adapters.ipfs.ipfs // Convert the file to a Buffer. @@ -81,6 +83,24 @@ class IpfsUseCases { } } + // Given a CID, this function will retrieve file metadata from the + // ipfs-file-pin-service, using the IPFS JSON-RPC. This metadata includes + // the filename, which can be used to infer mime-type, so that it can be + // delivered to a web browser. + async getCidMetadata (inObj = {}) { + try { + const { cid } = inObj + + const ipfsFiles = this.adapters.ipfsFiles + + const metadata = await ipfsFiles.getFileMetadata({ cid }) + console.log('getCidMetadata() metadata: ', metadata) + } catch (err) { + console.error('Error in getCidMetadata(): ', err) + throw err + } + } + // async downloadCid2 (inObj = {}) { // try { // const { cid } = inObj From b5c7e2efb34e5829b5a364df1e31478c5817c7f4 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Tue, 9 Apr 2024 22:00:58 -0700 Subject: [PATCH 4/9] Using filename from file metadata --- src/use-cases/ipfs-use-cases.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/use-cases/ipfs-use-cases.js b/src/use-cases/ipfs-use-cases.js index d5e7211..3670070 100644 --- a/src/use-cases/ipfs-use-cases.js +++ b/src/use-cases/ipfs-use-cases.js @@ -54,7 +54,7 @@ class IpfsUseCases { // throw new Error('File has not been pinned. Not available.') // } - await this.getCidMetadata({ cid }) + const filename = await this.getCidMetadata({ cid }) const helia = this.adapters.ipfs.ipfs @@ -74,7 +74,7 @@ class IpfsUseCases { } const readStream = bufferToStream(fileBuf) - const filename = 'test.jpg' + // const filename = 'test.jpg' return { filename, readStream } } catch (err) { @@ -95,6 +95,10 @@ class IpfsUseCases { const metadata = await ipfsFiles.getFileMetadata({ cid }) console.log('getCidMetadata() metadata: ', metadata) + + const filename = metadata.fileMetadata.filename + + return filename } catch (err) { console.error('Error in getCidMetadata(): ', err) throw err From 98dd904fa579cf7706f56e178dd64d7c5633620a Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 10 Apr 2024 10:52:07 -0700 Subject: [PATCH 5/9] feat(/ipfs/service): GET endpoint to retrieve File Pin service used by this app --- src/adapters/ipfs-files/index.js | 93 --------------------- src/adapters/ipfs/ipfs-coord.js | 1 + src/controllers/rest-api/ipfs/controller.js | 28 +++++++ src/controllers/rest-api/ipfs/index.js | 1 + 4 files changed, 30 insertions(+), 93 deletions(-) diff --git a/src/adapters/ipfs-files/index.js b/src/adapters/ipfs-files/index.js index 543b03b..bc1e92a 100644 --- a/src/adapters/ipfs-files/index.js +++ b/src/adapters/ipfs-files/index.js @@ -176,99 +176,6 @@ class IpfsFilesAdapter { } } - // Read an entry from the P2WDB, given an entry hash. - async getEntryByHash (hash) { - try { - // Input validation. - if (!hash || typeof hash !== 'string') { - throw new Error('getEntry() input hash must be a string.') - } - - // Throw an error if this IPFS node has not yet made a connection to a - // wallet service provider. - const selectedProvider = - this.ipfs.ipfsCoordAdapter.state.selectedP2wdbProvider - if (!selectedProvider) { - throw new Error('No P2WDB Service provider available yet.') - } - - const rpcData = { - endpoint: 'getByHash', - hash - } - - // Generate a UUID for the call. - const rpcId = this.uid() - - // Generate a JSON RPC command. - const cmd = this.jsonrpc.request(rpcId, 'p2wdb', rpcData) - const cmdStr = JSON.stringify(cmd) - // console.log('cmdStr: ', cmdStr) - - // Send the RPC command to selected wallet service. - const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode - await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage( - selectedProvider, - cmdStr, - thisNode - ) - - // Wait for data to come back from the wallet service. - const data = await this.waitForRPCResponse(rpcId) - - return data - } catch (err) { - console.error('Error in getEntryByHash()') - throw err - } - } - - async writeEntry (writeObj) { - try { - const { txid, signature, message, data } = writeObj - - // Throw an error if this IPFS node has not yet made a connection to a - // wallet service provider. - const selectedProvider = - this.ipfs.ipfsCoordAdapter.state.selectedP2wdbProvider - if (!selectedProvider) { - throw new Error('No P2WDB Service provider available yet.') - } - - const rpcData = { - endpoint: 'write', - txid, - signature, - message, - data - } - - // Generate a UUID for the call. - const rpcId = this.uid() - - // Generate a JSON RPC command. - const cmd = this.jsonrpc.request(rpcId, 'p2wdb', rpcData) - const cmdStr = JSON.stringify(cmd) - // console.log('cmdStr: ', cmdStr) - - // Send the RPC command to selected wallet service. - const thisNode = this.ipfs.ipfsCoordAdapter.ipfsCoord.thisNode - await this.ipfs.ipfsCoordAdapter.ipfsCoord.useCases.peer.sendPrivateMessage( - selectedProvider, - cmdStr, - thisNode - ) - - // Wait for data to come back from the wallet service. - const result = await this.waitForRPCResponse(rpcId) - - return result - } catch (err) { - console.error('Error in adapters/p2wdb/writeEntry()') - throw err - } - } - // Returns a promise that resolves to data when the RPC response is recieved. async waitForRPCResponse (rpcId) { try { diff --git a/src/adapters/ipfs/ipfs-coord.js b/src/adapters/ipfs/ipfs-coord.js index e07b025..86ae74d 100644 --- a/src/adapters/ipfs/ipfs-coord.js +++ b/src/adapters/ipfs/ipfs-coord.js @@ -373,6 +373,7 @@ class IpfsCoordAdapter { thisPeer === _this.config.preferredIpfsFileProvider ) { _this.state.selectedIpfsFileProvider = thisPeer + console.log(`---->IPFS File service switched to preferred peer: ${thisPeer}`) } // console.log('selectedServiceProvider: ', _this.state.selectedServiceProvider) diff --git a/src/controllers/rest-api/ipfs/controller.js b/src/controllers/rest-api/ipfs/controller.js index 22e6618..8ceb542 100644 --- a/src/controllers/rest-api/ipfs/controller.js +++ b/src/controllers/rest-api/ipfs/controller.js @@ -36,6 +36,7 @@ class IpfsRESTControllerLib { this.connect = this.connect.bind(this) this.getThisNode = this.getThisNode.bind(this) this.viewFile = this.viewFile.bind(this) + this.getService = this.getService.bind(this) } /** @@ -167,6 +168,33 @@ class IpfsRESTControllerLib { } } + /** + * @api {get} /ipfs/service Get the IPFS ID for the File Pin service + * @apiPermission public + * @apiName GetService + * @apiGroup REST BCH + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5020/ipfs/service + * + */ + // Reports the ipfs-file-pin-service node that this app is using to retrieve + // file information. + async getService (ctx) { + try { + const selectedIpfsFileProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider + + ctx.body = { + success: true, + selectedIpfsFileProvider + } + } catch (err) { + // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) + console.log('Error in ipfs/controller.js/getService(): ', err) + this.handleError(ctx, err) + } + } + // DRY error handler handleError (ctx, err) { // If an HTTP status is specified by the buisiness logic, use that. diff --git a/src/controllers/rest-api/ipfs/index.js b/src/controllers/rest-api/ipfs/index.js index 842dacc..6f0c3ab 100644 --- a/src/controllers/rest-api/ipfs/index.js +++ b/src/controllers/rest-api/ipfs/index.js @@ -57,6 +57,7 @@ class IpfsRouter { this.router.post('/connect', this.ipfsRESTController.connect) this.router.get('/node', this.ipfsRESTController.getThisNode) this.router.get('/view/:cid', this.ipfsRESTController.viewFile) + this.router.get('/service', this.ipfsRESTController.getService) // Attach the Controller routes to the Koa app. app.use(this.router.routes()) From 71a6de03b32b9edfb433bd7c1ce647573e174f39 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 10 Apr 2024 11:05:46 -0700 Subject: [PATCH 6/9] feat(/bch/service): GET endpoint to get ipfs-bch-wallet-service consumed by this app --- src/adapters/ipfs/ipfs-coord.js | 9 ++++++- src/controllers/rest-api/bch/controller.js | 27 +++++++++++++++++++++ src/controllers/rest-api/bch/index.js | 5 ++++ src/controllers/rest-api/ipfs/controller.js | 14 +++++------ 4 files changed, 47 insertions(+), 8 deletions(-) diff --git a/src/adapters/ipfs/ipfs-coord.js b/src/adapters/ipfs/ipfs-coord.js index 86ae74d..2074c70 100644 --- a/src/adapters/ipfs/ipfs-coord.js +++ b/src/adapters/ipfs/ipfs-coord.js @@ -286,6 +286,10 @@ class IpfsCoordAdapter { _this.config.preferredProvider && thisPeer === _this.config.preferredProvider ) { + if(_this.state.selectedServiceProvider !== thisPeer) { + console.log(`---->BCH wallet service switched to preferred peer: ${thisPeer}`) + } + _this.state.selectedServiceProvider = thisPeer } @@ -372,8 +376,11 @@ class IpfsCoordAdapter { _this.config.preferredIpfsFileProvider && thisPeer === _this.config.preferredIpfsFileProvider ) { + if(_this.state.selectedIpfsFileProvider !== thisPeer) { + console.log(`---->IPFS File service switched to preferred peer: ${thisPeer}`) + } + _this.state.selectedIpfsFileProvider = thisPeer - console.log(`---->IPFS File service switched to preferred peer: ${thisPeer}`) } // console.log('selectedServiceProvider: ', _this.state.selectedServiceProvider) diff --git a/src/controllers/rest-api/bch/controller.js b/src/controllers/rest-api/bch/controller.js index ebccfa0..0e00412 100644 --- a/src/controllers/rest-api/bch/controller.js +++ b/src/controllers/rest-api/bch/controller.js @@ -723,6 +723,33 @@ class BchRESTControllerLib { } } + /** + * @api {get} /bch/service Get the IPFS ID for the Wallet service + * @apiPermission public + * @apiName getService + * @apiGroup REST BCH + * @apiDescription Get the IPFS ID for the ipfs-bch-wallet-service node that + * this app is using to retrieve blockchain data from. + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5015/bch/service + * + */ + async getService (ctx) { + try { + const selectedServiceProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedServiceProvider + + ctx.body = { + success: true, + selectedServiceProvider + } + } catch (err) { + // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) + console.log('Error in ipfs/controller.js/getService(): ', err) + this.handleError(ctx, err) + } + } + // DRY error handler handleError (ctx, err) { // If an HTTP status is specified by the buisiness logic, use that. diff --git a/src/controllers/rest-api/bch/index.js b/src/controllers/rest-api/bch/index.js index 4a80b40..4958087 100644 --- a/src/controllers/rest-api/bch/index.js +++ b/src/controllers/rest-api/bch/index.js @@ -63,6 +63,7 @@ class BchRouter { this.router.post('/utxoIsValid', this.utxoIsValid) this.router.post('/getTokenData', this.getTokenData) this.router.post('/getTokenData2', this.getTokenData2) + this.router.get('/service', this.getService) // Attach the Controller routes to the Koa app. app.use(this.router.routes()) @@ -116,6 +117,10 @@ class BchRouter { async getTokenData2 (ctx, next) { await _this.bchRESTController.getTokenData2(ctx, next) } + + async getService (ctx, next) { + await _this.bchRESTController.getService(ctx, next) + } } // module.exports = BchRouter diff --git a/src/controllers/rest-api/ipfs/controller.js b/src/controllers/rest-api/ipfs/controller.js index 8ceb542..0aede4e 100644 --- a/src/controllers/rest-api/ipfs/controller.js +++ b/src/controllers/rest-api/ipfs/controller.js @@ -43,7 +43,7 @@ class IpfsRESTControllerLib { * @api {get} /ipfs Get status on IPFS infrastructure * @apiPermission public * @apiName GetIpfsStatus - * @apiGroup REST BCH + * @apiGroup REST IPFS * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs @@ -110,7 +110,7 @@ class IpfsRESTControllerLib { * @api {get} /ipfs/node Get a copy of the thisNode object from helia-coord * @apiPermission public * @apiName GetThisNode - * @apiGroup REST BCH + * @apiGroup REST IPFS * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/node @@ -132,7 +132,7 @@ class IpfsRESTControllerLib { * @api {get} /ipfs/view/:cid Retrieve and display a file via its IPFS CID * @apiPermission public * @apiName GetCidView - * @apiGroup REST BCH + * @apiGroup REST IPFS * * @apiExample Example usage: * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/view/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om @@ -172,14 +172,14 @@ class IpfsRESTControllerLib { * @api {get} /ipfs/service Get the IPFS ID for the File Pin service * @apiPermission public * @apiName GetService - * @apiGroup REST BCH + * @apiGroup REST IPFS + * @apiDescription Get the IPFS ID for the ipfs-file-pin-service node that + * this app is using to retrieve file data from. * * @apiExample Example usage: - * curl -H "Content-Type: application/json" -X GET localhost:5020/ipfs/service + * curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/service * */ - // Reports the ipfs-file-pin-service node that this app is using to retrieve - // file information. async getService (ctx) { try { const selectedIpfsFileProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider From 971474d4932f87cfd945f5393cc410f43dbf04d7 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 10 Apr 2024 11:05:55 -0700 Subject: [PATCH 7/9] linting --- src/adapters/ipfs/ipfs-coord.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapters/ipfs/ipfs-coord.js b/src/adapters/ipfs/ipfs-coord.js index 2074c70..53502a5 100644 --- a/src/adapters/ipfs/ipfs-coord.js +++ b/src/adapters/ipfs/ipfs-coord.js @@ -286,7 +286,7 @@ class IpfsCoordAdapter { _this.config.preferredProvider && thisPeer === _this.config.preferredProvider ) { - if(_this.state.selectedServiceProvider !== thisPeer) { + if (_this.state.selectedServiceProvider !== thisPeer) { console.log(`---->BCH wallet service switched to preferred peer: ${thisPeer}`) } @@ -376,7 +376,7 @@ class IpfsCoordAdapter { _this.config.preferredIpfsFileProvider && thisPeer === _this.config.preferredIpfsFileProvider ) { - if(_this.state.selectedIpfsFileProvider !== thisPeer) { + if (_this.state.selectedIpfsFileProvider !== thisPeer) { console.log(`---->IPFS File service switched to preferred peer: ${thisPeer}`) } From efb29104d72809ebe0c233e7dbb576d5d3f54cb2 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Wed, 10 Apr 2024 19:41:43 -0700 Subject: [PATCH 8/9] feat(/ipfs/file-info/:cid): GET endpoint to get CID pin metadata from ipfs-file-pin-server over JSON RPC --- src/controllers/rest-api/ipfs/controller.js | 29 +++++++++++++++++++++ src/controllers/rest-api/ipfs/index.js | 1 + 2 files changed, 30 insertions(+) diff --git a/src/controllers/rest-api/ipfs/controller.js b/src/controllers/rest-api/ipfs/controller.js index 0aede4e..90227c9 100644 --- a/src/controllers/rest-api/ipfs/controller.js +++ b/src/controllers/rest-api/ipfs/controller.js @@ -37,6 +37,7 @@ class IpfsRESTControllerLib { this.getThisNode = this.getThisNode.bind(this) this.viewFile = this.viewFile.bind(this) this.getService = this.getService.bind(this) + this.getFileInfo = this.getFileInfo.bind(this) } /** @@ -195,6 +196,34 @@ class IpfsRESTControllerLib { } } + /** + * @api {get} /ipfs/file-info/:cid Get file pin info about a CID + * @apiPermission public + * @apiName GetFileInfo + * @apiGroup REST IPFS + * @apiDescription Get file metadata and pin status information give a CID. + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/file-info/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om + * + */ + async getFileInfo (ctx) { + try { + const { cid } = ctx.params + + const ipfsFiles = this.adapters.ipfsFiles + + const metadata = await ipfsFiles.getFileMetadata({ cid }) + console.log('getCidMetadata() metadata: ', metadata) + + ctx.body = metadata + } catch (err) { + // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) + console.log('Error in ipfs/controller.js/getService(): ', err) + this.handleError(ctx, err) + } + } + // DRY error handler handleError (ctx, err) { // If an HTTP status is specified by the buisiness logic, use that. diff --git a/src/controllers/rest-api/ipfs/index.js b/src/controllers/rest-api/ipfs/index.js index 6f0c3ab..6c602a9 100644 --- a/src/controllers/rest-api/ipfs/index.js +++ b/src/controllers/rest-api/ipfs/index.js @@ -58,6 +58,7 @@ class IpfsRouter { this.router.get('/node', this.ipfsRESTController.getThisNode) this.router.get('/view/:cid', this.ipfsRESTController.viewFile) this.router.get('/service', this.ipfsRESTController.getService) + this.router.get('/file-info/:cid', this.ipfsRESTController.getFileInfo) // Attach the Controller routes to the Koa app. app.use(this.router.routes()) From 7ea17fb57c9ef1e3aaa5c187b16ecc0b5f717eb5 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 11 Apr 2024 05:34:25 -0700 Subject: [PATCH 9/9] Removing example timer controller --- src/controllers/timer-controllers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/timer-controllers.js b/src/controllers/timer-controllers.js index daa0c64..e6d0bc0 100644 --- a/src/controllers/timer-controllers.js +++ b/src/controllers/timer-controllers.js @@ -48,7 +48,7 @@ class TimerControllers { // Replace this example function with your own timer handler. exampleTimerFunc (negativeTest) { try { - console.log('Example timer controller executed.') + // console.log('Example timer controller executed.') if (negativeTest) throw new Error('test error')