From 587114e827066fefdaa278de51f21d072f70d6f6 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 27 Feb 2025 18:45:06 -0700 Subject: [PATCH] feat(GET /ipfs/download/:cid): Download CID as readable stream --- config/system-user-dev.json | 6 +-- src/controllers/rest-api/ipfs/controller.js | 30 +++++++++++--- src/controllers/rest-api/ipfs/index.js | 1 + src/use-cases/ipfs-use-cases.js | 46 ++++++++++++++++++++- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/config/system-user-dev.json b/config/system-user-dev.json index e26eff9..4e675ec 100644 --- a/config/system-user-dev.json +++ b/config/system-user-dev.json @@ -1,6 +1,6 @@ { - "password": "yKm2qMy2LkRNFJsCiD2B", + "password": "BJdugaoiq10ZJnA7Uskd", "email": "system@system.com", - "id": "67bb5a07da0eb4a2e7fc7bde", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YmI1YTA3ZGEwZWI0YTJlN2ZjN2JkZSIsImlhdCI6MTc0MDMzMTUyN30.o-MDjJpoq2MNElzWQsrWjy-kL_wji2hxPrXLPomrw7I" + "id": "67c114fd4935ba05e48f6bb2", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YzExNGZkNDkzNWJhMDVlNDhmNmJiMiIsImlhdCI6MTc0MDcwNzA2OX0.R5nU8J5yhGorp7I5kc8_dRESbo4hwCIrTtqJiVYVhLA" } \ No newline at end of file diff --git a/src/controllers/rest-api/ipfs/controller.js b/src/controllers/rest-api/ipfs/controller.js index 0856026..c296d5b 100644 --- a/src/controllers/rest-api/ipfs/controller.js +++ b/src/controllers/rest-api/ipfs/controller.js @@ -40,6 +40,7 @@ class IpfsRESTControllerLib { this.getFileInfo = this.getFileInfo.bind(this) this.getPins = this.getPins.bind(this) this.cid2json = this.cid2json.bind(this) + this.downloadFile = this.downloadFile.bind(this) } /** @@ -165,12 +166,34 @@ class IpfsRESTControllerLib { ) 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/download/:cid Download a file via its IPFS CID + * @apiPermission public + * @apiName GetCidDownload + * @apiGroup REST IPFS + * @apiDescription Download a file via its IPFS CID. Returns a readable stream. + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5001/ipfs/download/bafkreieaqtdhfywyddomswogynzymukosqqgqo7lkt5lch2zwfnc55m6om + */ + async downloadFile (ctx) { + try { + const { cid } = ctx.params + + const { readStream } = await this.useCases.ipfs.downloadCid({ cid }) + + ctx.body = readStream + } catch (err) { + console.log('Error in ipfs/controller.js/downloadFile(): ', err) + this.handleError(ctx, err) + } + } + /** * @api {get} /ipfs/service Get the IPFS ID for the File Pin service * @apiPermission public @@ -192,7 +215,7 @@ class IpfsRESTControllerLib { selectedIpfsFileProvider } } catch (err) { - // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) + // wlogger.error('Error in ipfs/controller.js/getService(): ', err) console.log('Error in ipfs/controller.js/getService(): ', err) this.handleError(ctx, err) } @@ -220,7 +243,6 @@ class IpfsRESTControllerLib { ctx.body = metadata } catch (err) { - // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) console.log('Error in ipfs/controller.js/getFileInfo(): ', err) this.handleError(ctx, err) } @@ -248,7 +270,6 @@ class IpfsRESTControllerLib { ctx.body = pinData } catch (err) { - // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) console.log('Error in ipfs/controller.js/getPins(): ', err) this.handleError(ctx, err) } @@ -274,7 +295,6 @@ class IpfsRESTControllerLib { ctx.body = json } catch (err) { - // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) console.log('Error in ipfs/controller.js/cid2json(): ', err.message) this.handleError(ctx, err) } diff --git a/src/controllers/rest-api/ipfs/index.js b/src/controllers/rest-api/ipfs/index.js index 599e14f..016f321 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('/download/:cid', this.ipfsRESTController.downloadFile) this.router.get('/service', this.ipfsRESTController.getService) this.router.get('/file-info/:cid', this.ipfsRESTController.getFileInfo) this.router.get('/pins', this.ipfsRESTController.getPins) diff --git a/src/use-cases/ipfs-use-cases.js b/src/use-cases/ipfs-use-cases.js index 7497061..8611c70 100644 --- a/src/use-cases/ipfs-use-cases.js +++ b/src/use-cases/ipfs-use-cases.js @@ -9,6 +9,7 @@ // import { exporter } from 'ipfs-unixfs-exporter' import { Duplex } from 'stream' import PSFFPP from 'psffpp' +import { CID } from 'multiformats' // Local libraries // import PinEntity from '../entities/pin.js' @@ -28,11 +29,13 @@ class IpfsUseCases { // Encapsulate dependencies this.psffpp = null // placeholder + this.CID = CID // Bind 'this' object to all class subfunctions. this.downloadCid = this.downloadCid.bind(this) // this.downloadCid2 = this.downloadCid2.bind(this) this.getWritePrice = this.getWritePrice.bind(this) + this.getCidMetadata = this.getCidMetadata.bind(this) this.cid2json = this.cid2json.bind(this) // State @@ -43,7 +46,7 @@ class IpfsUseCases { // Returns a readable stream. async downloadCid (inObj = {}) { try { - const { cid } = inObj + let { cid } = inObj if (!cid) throw new Error('CID is undefined') @@ -64,6 +67,47 @@ class IpfsUseCases { const helia = this.adapters.ipfs.ipfs + const cidClass = this.CID.parse(cid) + + // Download the file from other nodes. + await helia.blockstore.get(cidClass) + + // list cid content, to dermine if this is a file-based or directory-based CID. + const contentArray = [] + for await (const file of helia.fs.ls(cid)) { + contentArray.push(file) + } + + // Handle directory-based CIDs. This is used by Token Tiger for mutable and immutable token data. + /** + * If the cid is a directory + * The next block of code sends a html page with a list of links with the file names into the directory. + * Skipping this code delivers directly the first file detected in the directory. + */ + const isDir = contentArray[0].path.match('/') // TODO : looking for a better way to detect if is a directory + // 'listDir' is a flag to ignore this code on /download endpoint. + if (isDir) { + console.log('This CID is a directory. contentArray[0]: ', contentArray[0]) + cid = `${cid}/${contentArray[0].name}` + + // throw new Error('CID is a directory. Not supported yet.') + // const stream = new Stream.Readable({ read () { } }) + // for (let i = 0; i < contentArray.length; i++) { + // const cont = contentArray[i] + // // List all paths excluding root path. + // if (cont.path !== cid) { + // // Add links to the gateway with the format cid/:filename + // stream.push(`/${cont.name} ( CID: ${cont.cid} )
`) + // } + // } + + // stream.push(null) + // // return fileName as html because the controller the library sends it as html + // return { readStream: stream } + } else { + console.log('This CID is not a directory.') + } + // Convert the file to a Buffer. const fileChunks = [] for await (const chunk of helia.fs.cat(cid)) {