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