From 9ae5e890aea4f2c12f31745204c8272afe67132b Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 9 Feb 2025 05:52:48 -0700 Subject: [PATCH 1/3] feat(cid2json): Got basic functionality in place, no tests yet, known issues --- config/system-user-dev.json | 6 +- src/controllers/rest-api/ipfs/controller.js | 32 +++++++++- src/controllers/rest-api/ipfs/index.js | 1 + src/use-cases/ipfs-use-cases.js | 65 +++++++++++++++++++++ 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/config/system-user-dev.json b/config/system-user-dev.json index 5eb21c7..87318d5 100644 --- a/config/system-user-dev.json +++ b/config/system-user-dev.json @@ -1,6 +1,6 @@ { - "password": "jxR0kPRm6GI7RETZ36KW", + "password": "YksrA6beZTqSioEpHxZd", "email": "system@system.com", - "id": "6796f3e8e473169ab68aa9b5", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3OTZmM2U4ZTQ3MzE2OWFiNjhhYTliNSIsImlhdCI6MTczNzk0NjA4OH0.q1Dy0WXO2E2Q5y2eNB2INq_YD3AnNirgX1vrk9KxkH0" + "id": "67a8a4578de3cb65f3bbdbc0", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YThhNDU3OGRlM2NiNjVmM2JiZGJjMCIsImlhdCI6MTczOTEwNTM2N30.YA9hkc0dUyNvKCLjWnW-L1J1UvCBvGKCVC8qkrKe4Xw" } \ 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 af45bb8..b6b9ac8 100644 --- a/src/controllers/rest-api/ipfs/controller.js +++ b/src/controllers/rest-api/ipfs/controller.js @@ -39,6 +39,7 @@ class IpfsRESTControllerLib { this.getService = this.getService.bind(this) this.getFileInfo = this.getFileInfo.bind(this) this.getPins = this.getPins.bind(this) + this.cid2json = this.cid2json.bind(this) } /** @@ -220,7 +221,7 @@ class IpfsRESTControllerLib { ctx.body = metadata } catch (err) { // wlogger.error('Error in ipfs/controller.js/viewFile(): ', err) - console.log('Error in ipfs/controller.js/getService(): ', err) + console.log('Error in ipfs/controller.js/getFileInfo(): ', err) this.handleError(ctx, err) } } @@ -253,8 +254,36 @@ class IpfsRESTControllerLib { } } + /** + * @api {get} /ipfs/cid2json/:cid Given a CID, retrieve a JSON object + * @apiPermission public + * @apiName GetCid2Json + * @apiGroup REST IPFS + * @apiDescription Given a CID, retrieve a JSON object. + * If the CID does not resolves to a JSON file, then an error is thrown. + * + * @apiExample Example usage: + * curl -H "Content-Type: application/json" -X GET localhost:5015/ipfs/cid2json/bafkreigbgrvpagnmrqz2vhofifrqobigsxkdvnvikf5iqrkrbwrzirazhm + * + */ + async cid2json (ctx) { + try { + const { cid } = ctx.params + + const json = await this.useCases.ipfs.cid2json({ cid }) + + 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) + } + } + // DRY error handler handleError (ctx, err) { + console.log('handleError() err.status: ', err.status) + console.log('handleError() err.message: ', err.message) // If an HTTP status is specified by the buisiness logic, use that. if (err.status) { if (err.message) { @@ -263,6 +292,7 @@ class IpfsRESTControllerLib { ctx.throw(err.status) } } else { + console.log(`handleError() err.message: ${err.message}`) // By default use a 422 error if the HTTP status is not specified. ctx.throw(422, err.message) } diff --git a/src/controllers/rest-api/ipfs/index.js b/src/controllers/rest-api/ipfs/index.js index 645ffef..599e14f 100644 --- a/src/controllers/rest-api/ipfs/index.js +++ b/src/controllers/rest-api/ipfs/index.js @@ -60,6 +60,7 @@ class IpfsRouter { this.router.get('/service', this.ipfsRESTController.getService) this.router.get('/file-info/:cid', this.ipfsRESTController.getFileInfo) this.router.get('/pins', this.ipfsRESTController.getPins) + this.router.get('/cid2json/:cid', this.ipfsRESTController.cid2json) // Attach the Controller routes to the Koa app. app.use(this.router.routes()) diff --git a/src/use-cases/ipfs-use-cases.js b/src/use-cases/ipfs-use-cases.js index 35a032a..cf5c3b4 100644 --- a/src/use-cases/ipfs-use-cases.js +++ b/src/use-cases/ipfs-use-cases.js @@ -33,6 +33,7 @@ class IpfsUseCases { this.downloadCid = this.downloadCid.bind(this) // this.downloadCid2 = this.downloadCid2.bind(this) this.getWritePrice = this.getWritePrice.bind(this) + this.cid2json = this.cid2json.bind(this) // State this.lastWritePriceUpdate = null // Used to periodically update write price. @@ -151,6 +152,70 @@ class IpfsUseCases { } } + // Given a CID, this function will retrieve a JSON object from the + // ipfs-file-pin-service, using the IPFS JSON-RPC. + async cid2json (inObj = {}) { + try { + const { cid } = inObj + console.log('cid2json() cid: ', cid) + + // Throw an error if ipfs-bch-wallet-consumer has not yet connected to an instance of ipfs-file-pin-service. + const ipfsFileProvider = this.adapters.ipfs.ipfsCoordAdapter.state.selectedIpfsFileProvider + if (!ipfsFileProvider) { + throw new Error('No IPFS File Provider Service is available yet. Try again in a few seconds.') + } + + // Throw an error if ipfs-bch-wallet-consumer can not communicate with the ipfs-file-pin-service. + const ipfsFiles = this.adapters.ipfsFiles + const metadata = await ipfsFiles.getFileMetadata({ cid }) + if (metadata.success === false) { + throw new Error(`Could not communicate with instance of ipfs-file-pin-service ${ipfsFileProvider}. Try again in a few seconds.`) + } + + // Throw an error if the file is not pinned. + const fileMetadata = metadata.fileMetadata + if (!fileMetadata.dataPinned) { + throw new Error(`CID ${cid} has not been pinned by ipfs-file-pin-service instance ${ipfsFileProvider}`) + } + console.log('fileMetadata: ', fileMetadata) + + // Throw an error if this is not a JSON file + const filename = fileMetadata.filename + if (!filename.endsWith('.json')) { + throw new Error(`CID ${cid} does not resolve to a JSON file.`) + } + console.log('ping01') + // Retrieve the CID content and store it in a Buffer. + const helia = this.adapters.ipfs.ipfs + const fileChunks = [] + console.log('ping02') + for await (const chunk of helia.fs.cat(cid)) { + fileChunks.push(chunk) + } + console.log('ping03') + const fileBuf = Buffer.concat(fileChunks) + console.log('ping04') + // Convert the Buffer into a string. + const jsonStr = fileBuf.toString() + console.log('ping05') + // Parse the string into a JSON object. + let json = null + try { + json = JSON.parse(jsonStr) + } catch (err) { + throw new Error(`CID ${cid} does not resolve to a valid JSON object.`) + } + + return { + success: true, + json + } + } catch (err) { + console.error('Error in ipfs-use-cases.js/cid2json(): ', err.message) + throw err + } + } + // async downloadCid2 (inObj = {}) { // try { // const { cid } = inObj From 8a70d6a16991cc2618320eef12f19b7bb82f1a5e Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 9 Feb 2025 06:12:47 -0700 Subject: [PATCH 2/3] fix(cid2json): Works with CID files and directories --- config/system-user-dev.json | 6 +++--- src/use-cases/ipfs-use-cases.js | 30 ++++++++++++++++++++++-------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/config/system-user-dev.json b/config/system-user-dev.json index 87318d5..69826dc 100644 --- a/config/system-user-dev.json +++ b/config/system-user-dev.json @@ -1,6 +1,6 @@ { - "password": "YksrA6beZTqSioEpHxZd", + "password": "spksihHDsyk3JUUkH3xt", "email": "system@system.com", - "id": "67a8a4578de3cb65f3bbdbc0", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YThhNDU3OGRlM2NiNjVmM2JiZGJjMCIsImlhdCI6MTczOTEwNTM2N30.YA9hkc0dUyNvKCLjWnW-L1J1UvCBvGKCVC8qkrKe4Xw" + "id": "67a8a8e25a3e45751f654a13", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YThhOGUyNWEzZTQ1NzUxZjY1NGExMyIsImlhdCI6MTczOTEwNjUzMH0.ViWmWvfJF2UnM46pRXvl0sXThfP2dNlzrmzSxpWChew" } \ No newline at end of file diff --git a/src/use-cases/ipfs-use-cases.js b/src/use-cases/ipfs-use-cases.js index cf5c3b4..fe669a5 100644 --- a/src/use-cases/ipfs-use-cases.js +++ b/src/use-cases/ipfs-use-cases.js @@ -177,27 +177,41 @@ class IpfsUseCases { if (!fileMetadata.dataPinned) { throw new Error(`CID ${cid} has not been pinned by ipfs-file-pin-service instance ${ipfsFileProvider}`) } - console.log('fileMetadata: ', fileMetadata) + // console.log('fileMetadata: ', fileMetadata) // Throw an error if this is not a JSON file const filename = fileMetadata.filename if (!filename.endsWith('.json')) { throw new Error(`CID ${cid} does not resolve to a JSON file.`) } - console.log('ping01') // Retrieve the CID content and store it in a Buffer. const helia = this.adapters.ipfs.ipfs const fileChunks = [] - console.log('ping02') - for await (const chunk of helia.fs.cat(cid)) { - fileChunks.push(chunk) + + // list cid content. This is used to determine if the CID is a file or a directory. + const contentArray = [] + for await (const file of helia.fs.ls(cid)) { + contentArray.push(file) } - console.log('ping03') + // console.log('contentArray: ', contentArray) + + try { + // Handle CIDs that are files. + for await (const chunk of helia.fs.cat(cid)) { + fileChunks.push(chunk) + } + } catch (err) { + // Handle CIDs that are directorys containing a JSON file. (TokenTiger.com tokens) + for await (const chunk of helia.fs.cat(`${cid}/${contentArray[0].name}`)) { + fileChunks.push(chunk) + } + } + const fileBuf = Buffer.concat(fileChunks) - console.log('ping04') + // Convert the Buffer into a string. const jsonStr = fileBuf.toString() - console.log('ping05') + // Parse the string into a JSON object. let json = null try { From 33a447cd617e69069bc54916edb3ba29fe2f02f0 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 9 Feb 2025 06:39:07 -0700 Subject: [PATCH 3/3] fix(cid2json): Added error handler for corner case --- config/system-user-dev.json | 6 +++--- src/controllers/rest-api/ipfs/controller.js | 7 ++++--- src/use-cases/ipfs-use-cases.js | 9 ++++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/config/system-user-dev.json b/config/system-user-dev.json index 69826dc..53fd88a 100644 --- a/config/system-user-dev.json +++ b/config/system-user-dev.json @@ -1,6 +1,6 @@ { - "password": "spksihHDsyk3JUUkH3xt", + "password": "eYrzProqwsAyO3CT0Ftf", "email": "system@system.com", - "id": "67a8a8e25a3e45751f654a13", - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YThhOGUyNWEzZTQ1NzUxZjY1NGExMyIsImlhdCI6MTczOTEwNjUzMH0.ViWmWvfJF2UnM46pRXvl0sXThfP2dNlzrmzSxpWChew" + "id": "67a8af30c958c587f890cd8f", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY3YThhZjMwYzk1OGM1ODdmODkwY2Q4ZiIsImlhdCI6MTczOTEwODE0NH0.OFdVwH8ikbChHV0wjRpYvnL7vu46HDYS-od3FRtm9Nc" } \ 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 b6b9ac8..0856026 100644 --- a/src/controllers/rest-api/ipfs/controller.js +++ b/src/controllers/rest-api/ipfs/controller.js @@ -282,8 +282,9 @@ class IpfsRESTControllerLib { // DRY error handler handleError (ctx, err) { - console.log('handleError() err.status: ', err.status) - console.log('handleError() err.message: ', err.message) + // console.log('handleError() err.status: ', err.status) + // console.log('handleError() err.message: ', err.message) + // If an HTTP status is specified by the buisiness logic, use that. if (err.status) { if (err.message) { @@ -292,7 +293,7 @@ class IpfsRESTControllerLib { ctx.throw(err.status) } } else { - console.log(`handleError() err.message: ${err.message}`) + // console.log(`handleError() err.message: ${err.message}`) // By default use a 422 error if the HTTP status is not specified. ctx.throw(422, err.message) } diff --git a/src/use-cases/ipfs-use-cases.js b/src/use-cases/ipfs-use-cases.js index fe669a5..7497061 100644 --- a/src/use-cases/ipfs-use-cases.js +++ b/src/use-cases/ipfs-use-cases.js @@ -201,8 +201,15 @@ class IpfsUseCases { fileChunks.push(chunk) } } catch (err) { + // Extra logic here to look at the filename of the first file and make sure + // it ends in .json. + const name = contentArray[0].name + if (!name.endsWith('.json')) { + throw new Error(`CID ${cid} is a directory and its first file does not resolve to a valid JSON file.`) + } + // Handle CIDs that are directorys containing a JSON file. (TokenTiger.com tokens) - for await (const chunk of helia.fs.cat(`${cid}/${contentArray[0].name}`)) { + for await (const chunk of helia.fs.cat(`${cid}/${name}`)) { fileChunks.push(chunk) } }