Skip to content

Commit 32730a8

Browse files
authored
feat: Enhance file and folder creation with visibility options in GNFD services (#3)
1 parent 4038c26 commit 32730a8

5 files changed

Lines changed: 105 additions & 28 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ The project is organized into several core modules:
2323
- **Wallet**: Wallet operations and management
2424
- **Common**: Shared utilities and types
2525
- **Greenfield**: Support file management operations on Greenfield network including, uploading, downloading, and managing files and buckets
26-
- Additional features coming soon (Greenfield, Swap, Bridge, etc.)
26+
- Additional features coming soon (Swap, Bridge, etc.)
2727

2828
## Integration with Cursor
2929

src/gnfd/services/bucket.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ export const createBucket = async (
3535
network: "testnet" | "mainnet",
3636
{
3737
privateKey,
38-
bucketName
38+
bucketName,
39+
visibility
3940
}: {
4041
privateKey: Hex
4142
bucketName?: string
43+
visibility?: "public" | "private"
4244
}
4345
): Promise<ApiResponse<BucketData>> => {
4446
const client = getClient(network)
@@ -64,7 +66,10 @@ export const createBucket = async (
6466
const createBucketTx = await client.bucket.createBucket({
6567
bucketName: _bucketName,
6668
creator: account.address,
67-
visibility: VisibilityType.VISIBILITY_TYPE_PUBLIC_READ,
69+
visibility:
70+
visibility === "public"
71+
? VisibilityType.VISIBILITY_TYPE_PUBLIC_READ
72+
: VisibilityType.VISIBILITY_TYPE_PRIVATE,
6873
chargedReadQuota: Long.fromString("0"),
6974
paymentAddress: account.address,
7075
primarySpAddress: spInfo.primarySpAddress

src/gnfd/services/index.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ test("list objects", async () => {
8787
expect(res.status).toBe("success")
8888
})
8989

90-
test("download object", async () => {
90+
test("download private object", async () => {
9191
const res = await downloadObject("testnet", {
9292
privateKey: process.env.PRIVATE_KEY as Hex,
9393
bucketName,
@@ -99,6 +99,27 @@ test("download object", async () => {
9999
expect(res.status).toBe("success")
100100
})
101101

102+
test("download public object", async () => {
103+
await createFile("testnet", {
104+
privateKey: process.env.PRIVATE_KEY as Hex,
105+
filePath: fileName,
106+
bucketName: "created-by-bnbchain-mcp",
107+
visibility: "public"
108+
})
109+
110+
const res = await downloadObject("testnet", {
111+
bucketName: "created-by-bnbchain-mcp",
112+
objectName
113+
})
114+
// remove the file after test
115+
await deleteObject("testnet", {
116+
privateKey: process.env.PRIVATE_KEY as Hex,
117+
bucketName: "created-by-bnbchain-mcp",
118+
objectName
119+
})
120+
expect(res.data?.file.startsWith("https://")).toBeTrue()
121+
})
122+
102123
test("delete object", async () => {
103124
const res = await deleteObject("testnet", {
104125
privateKey: process.env.PRIVATE_KEY as Hex,

src/gnfd/services/object.ts

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
1-
import {
2-
existsSync,
3-
fstatSync,
4-
readFileSync,
5-
statSync,
6-
writeFileSync
7-
} from "fs"
1+
import { existsSync, readFileSync, statSync, writeFileSync } from "fs"
82
import path from "path"
3+
import { ObjectInfo } from "@bnb-chain/greenfield-cosmos-types/greenfield/storage/types"
94
import {
105
bytesFromBase64,
116
Long,
127
RedundancyType,
138
VisibilityType
149
} from "@bnb-chain/greenfield-js-sdk"
15-
import { ObjectInfo } from "@bnb-chain/greenfield-js-sdk/dist/esm/types/sp/Common"
1610
import { NodeAdapterReedSolomon } from "@bnb-chain/reed-solomon/node.adapter"
1711
import type { Hex } from "viem"
1812

@@ -54,11 +48,13 @@ export const createFile = async (
5448
{
5549
privateKey,
5650
filePath,
57-
bucketName
51+
bucketName,
52+
visibility
5853
}: {
5954
privateKey: Hex
6055
filePath: string
6156
bucketName?: string
57+
visibility?: "public" | "private"
6258
}
6359
): Promise<ApiResponse<FileData>> => {
6460
try {
@@ -101,7 +97,10 @@ export const createFile = async (
10197
bucketName: _bucketName,
10298
objectName: objectName,
10399
creator: account.address,
104-
visibility: VisibilityType.VISIBILITY_TYPE_PRIVATE,
100+
visibility:
101+
visibility === "public"
102+
? VisibilityType.VISIBILITY_TYPE_PUBLIC_READ
103+
: VisibilityType.VISIBILITY_TYPE_PRIVATE,
105104
contentType: fileObj.type as string,
106105
redundancyType: RedundancyType.REDUNDANCY_EC_TYPE,
107106
payloadSize: Long.fromInt(fileObj.content.byteLength),
@@ -161,11 +160,13 @@ export const createFolder = async (
161160
{
162161
privateKey,
163162
folderName,
164-
bucketName
163+
bucketName,
164+
visibility
165165
}: {
166166
privateKey: Hex
167167
folderName?: string
168168
bucketName?: string
169+
visibility?: "public" | "private"
169170
}
170171
): Promise<ApiResponse<{ bucketName: string; folderName: string }>> => {
171172
try {
@@ -197,7 +198,10 @@ export const createFolder = async (
197198
bucketName: _bucketName,
198199
objectName: formattedFolderName,
199200
creator: account.address,
200-
visibility: VisibilityType.VISIBILITY_TYPE_PRIVATE,
201+
visibility:
202+
visibility === "public"
203+
? VisibilityType.VISIBILITY_TYPE_PUBLIC_READ
204+
: VisibilityType.VISIBILITY_TYPE_PRIVATE,
201205
redundancyType: RedundancyType.REDUNDANCY_EC_TYPE
202206
})
203207

@@ -240,7 +244,7 @@ export const getObjectInfo = async (
240244
const client = getClient(network)
241245
const res = await client.object.headObject(bucketName, objectName)
242246

243-
return response.success(res.objectInfo as {} as ObjectInfo)
247+
return response.success(res.objectInfo as ObjectInfo)
244248
} catch (error) {
245249
Logger.error(`Get object info operation failed: ${error}`)
246250
return response.fail(`Get object info operation failed: ${error}`)
@@ -311,7 +315,9 @@ export const listObjects = async (
311315
network: "testnet" | "mainnet",
312316
bucketName: string
313317
): Promise<
314-
ApiResponse<{ objects: Array<{ objectName: string; createAt: number }> }>
318+
ApiResponse<{
319+
objects: Array<{ objectName: string; createAt: number; visibility: string }>
320+
}>
315321
> => {
316322
try {
317323
const client = getClient(network)
@@ -339,7 +345,11 @@ export const listObjects = async (
339345
objectsRes.body?.GfSpListObjectsByBucketNameResponse?.Objects || []
340346
const objects = res.map((it) => ({
341347
objectName: it.ObjectInfo.ObjectName,
342-
createAt: it.ObjectInfo.CreateAt
348+
createAt: it.ObjectInfo.CreateAt,
349+
visibility:
350+
it.ObjectInfo.Visibility === VisibilityType.VISIBILITY_TYPE_PUBLIC_READ
351+
? "public"
352+
: "private"
343353
}))
344354

345355
return response.success({ objects })
@@ -352,18 +362,46 @@ export const listObjects = async (
352362
export const downloadObject = async (
353363
network: "testnet" | "mainnet",
354364
{
355-
privateKey,
356365
bucketName,
357366
objectName,
367+
privateKey,
358368
targetPath
359369
}: {
360-
privateKey: Hex
361370
bucketName: string
362371
objectName: string
372+
privateKey?: Hex
363373
targetPath?: string
364374
}
365375
): Promise<ApiResponse<{ file: string }>> => {
366376
try {
377+
const objectInfo = await getObjectInfo(network, {
378+
bucketName,
379+
objectName
380+
})
381+
if (objectInfo.status === "error") {
382+
return response.fail(objectInfo.message || "Object does not exist")
383+
}
384+
385+
const objectInfoData = objectInfo.data as ObjectInfo
386+
Logger.debug(`Object info: ${JSON.stringify(objectInfoData)}`)
387+
// for public object, use sp endpoint to download
388+
if (
389+
objectInfoData.visibility === VisibilityType.VISIBILITY_TYPE_PUBLIC_READ
390+
) {
391+
const sp = await selectSp(network)
392+
return response.success({
393+
file: sp.endpoint + "/view/" + bucketName + "/" + objectName
394+
})
395+
}
396+
397+
// for private object, private key is required
398+
if (
399+
objectInfoData.visibility === VisibilityType.VISIBILITY_TYPE_PRIVATE &&
400+
!privateKey
401+
) {
402+
return response.fail("Object is private and private key is not provided")
403+
}
404+
367405
let filePath = ""
368406
if (!targetPath || !existsSync(targetPath)) {
369407
Logger.debug(
@@ -383,7 +421,7 @@ export const downloadObject = async (
383421
},
384422
{
385423
type: "ECDSA",
386-
privateKey: privateKey
424+
privateKey: privateKey || ""
387425
}
388426
)
389427
if (res.code !== 0) {

src/gnfd/tools.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,19 @@ export function registerGnfdTools(server: McpServer) {
149149
.describe(
150150
"Absolute path to the file to upload. The file must exist on the machine."
151151
),
152-
bucketName: bucketNameParam
152+
bucketName: bucketNameParam,
153+
visibility: z
154+
.enum(["public", "private"])
155+
.optional()
156+
.default("private")
157+
.describe("The visibility of the file. Defaults to 'private'.")
153158
},
154159
async ({
155160
network,
156161
privateKey,
157162
filePath,
158-
bucketName = "created-by-bnbchain-mcp"
163+
bucketName = "created-by-bnbchain-mcp",
164+
visibility
159165
}) => {
160166
try {
161167
// Ensure absolute path is used
@@ -166,7 +172,8 @@ export function registerGnfdTools(server: McpServer) {
166172
const result = await services.createFile(network, {
167173
privateKey: privateKey as Hex,
168174
filePath: absoluteFilePath,
169-
bucketName
175+
bucketName,
176+
visibility
170177
})
171178
return formatResponse(result)
172179
} catch (error) {
@@ -187,14 +194,20 @@ export function registerGnfdTools(server: McpServer) {
187194
.optional()
188195
.default("created-by-bnbchain-mcp")
189196
.describe("Optional folder name. Default is 'created-by-bnbchain-mcp'"),
190-
bucketName: bucketNameParam
197+
bucketName: bucketNameParam,
198+
visibility: z
199+
.enum(["public", "private"])
200+
.optional()
201+
.default("private")
202+
.describe("The visibility of the folder. Defaults to 'private'.")
191203
},
192-
async ({ network, privateKey, folderName, bucketName }) => {
204+
async ({ network, privateKey, folderName, bucketName, visibility }) => {
193205
try {
194206
const result = await services.createFolder(network, {
195207
privateKey: privateKey as Hex,
196208
folderName,
197-
bucketName
209+
bucketName,
210+
visibility
198211
})
199212
return formatResponse(result)
200213
} catch (error) {

0 commit comments

Comments
 (0)