diff --git a/config/env/common.js b/config/env/common.js index c74d9f6..dc31aa6 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', @@ -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/package-lock.json b/package-lock.json index a9b2a16..4ede96b 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", @@ -43,6 +43,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..29bd76a 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,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/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..bc1e92a --- /dev/null +++ b/src/adapters/ipfs-files/index.js @@ -0,0 +1,232 @@ +/* + 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 + } + } + + // 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..53502a5 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 @@ -278,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 } @@ -308,6 +320,96 @@ 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 + ) { + if (_this.state.selectedIpfsFileProvider !== thisPeer) { + console.log(`---->IPFS File service switched to preferred peer: ${thisPeer}`) + } + + _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/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 d883288..90227c9 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,13 +35,16 @@ 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) + this.getService = this.getService.bind(this) + this.getFileInfo = this.getFileInfo.bind(this) } /** * @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 @@ -107,7 +111,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 @@ -125,6 +129,101 @@ class IpfsRESTControllerLib { } } + /** + * @api {get} /ipfs/view/:cid Retrieve and display a file via its IPFS CID + * @apiPermission public + * @apiName GetCidView + * @apiGroup REST IPFS + * + * @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) + } + } + + /** + * @api {get} /ipfs/service Get the IPFS ID for the File Pin service + * @apiPermission public + * @apiName GetService + * @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:5015/ipfs/service + * + */ + 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) + } + } + + /** + * @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 d787f73..6c602a9 100644 --- a/src/controllers/rest-api/ipfs/index.js +++ b/src/controllers/rest-api/ipfs/index.js @@ -56,6 +56,9 @@ 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) + 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()) 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') 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..3670070 --- /dev/null +++ b/src/use-cases/ipfs-use-cases.js @@ -0,0 +1,160 @@ +/* + 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 filename = await this.getCidMetadata({ cid }) + + 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 + } + } + + // 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) + + const filename = metadata.fileMetadata.filename + + return filename + } catch (err) { + console.error('Error in getCidMetadata(): ', err) + 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