Skip to content

Commit c85fd17

Browse files
authored
Bucket methods (#154)
* bucket methods * delete file * fix tests
1 parent 24e084d commit c85fd17

6 files changed

Lines changed: 363 additions & 12 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"@oasisprotocol/sapphire-paratime": "^1.3.2",
4848
"@oceanprotocol/contracts": "^2.5.0",
4949
"@oceanprotocol/ddo-js": "^0.3.0",
50-
"@oceanprotocol/lib": "^8.0.4",
50+
"@oceanprotocol/lib": "^8.0.6",
5151
"commander": "^13.1.0",
5252
"cross-fetch": "^3.1.5",
5353
"crypto-js": "^4.1.1",

src/cli.ts

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,31 +74,45 @@ export async function createCLI() {
7474
chalk.cyan("libp2p node started. Waiting for peer connections...")
7575
);
7676

77-
// Wait for at least one active P2P connection before running commands
77+
// Wait for the TARGET peer (the one in NODE_URL) to be connected,
78+
// not just any bootstrap peer — otherwise signed commands fail with
79+
// "Cannot reach peer ...".
80+
const targetPeerId = isFullMultiaddr
81+
? nodeUrl.split("/p2p/").pop()!
82+
: nodeUrl;
7883
const maxWait = 20_000;
7984
const interval = 500;
8085
let waited = 0;
8186
const libp2p = (ProviderInstance as any).p2pProvider?.libp2pNode;
87+
const isTargetConnected = () =>
88+
(libp2p?.getPeers() ?? []).some(
89+
(p: { toString(): string }) => p.toString() === targetPeerId
90+
);
8291
while (waited < maxWait) {
83-
const conns = libp2p?.getConnections()?.length ?? 0;
84-
if (conns > 0) {
92+
if (isTargetConnected()) {
93+
const total = libp2p?.getConnections()?.length ?? 0;
8594
console.log(
86-
chalk.green(`Connected to ${conns} peer(s) in ${waited}ms`)
95+
chalk.green(
96+
`Connected to target peer ${targetPeerId.slice(0, 12)}… in ${waited}ms (total peers: ${total})`
97+
)
8798
);
8899
break;
89100
}
90101
await new Promise((r) => setTimeout(r, interval));
91102
waited += interval;
92103
if (waited % 3000 === 0) {
104+
const total = libp2p?.getConnections()?.length ?? 0;
93105
console.log(
94-
chalk.yellow(` Still waiting for peers... (${waited / 1000}s)`)
106+
chalk.yellow(
107+
` Waiting for target peer ${targetPeerId.slice(0, 12)}… (${waited / 1000}s, ${total} other peer(s))`
108+
)
95109
);
96110
}
97111
}
98-
if ((libp2p?.getConnections()?.length ?? 0) === 0) {
112+
if (!isTargetConnected()) {
99113
console.error(
100114
chalk.red(
101-
`No P2P peers connected after ${maxWait / 1000}s. Commands may fail.`
115+
`Target peer ${targetPeerId} not reachable after ${maxWait / 1000}s. Commands will fail.`
102116
)
103117
);
104118
}
@@ -802,5 +816,69 @@ export async function createCLI() {
802816
]);
803817
});
804818

819+
program
820+
.command("createBucket")
821+
.description("Create a new persistent-storage bucket gated by a single access list (chain inferred from RPC)")
822+
.argument("<accessListAddress>", "Access list contract address (0x…)")
823+
.action(async (accessListAddress) => {
824+
const { signer, chainId } = await initializeSigner();
825+
const commands = new Commands(signer, chainId);
826+
await commands.createBucket([null, accessListAddress]);
827+
});
828+
829+
program
830+
.command("addFileToBucket")
831+
.description("Upload a local file into a bucket")
832+
.argument("<bucketId>", "Bucket id")
833+
.argument("<filePath>", "Path to local file")
834+
.argument("[fileName]", "Name under which to store the file (defaults to basename)")
835+
.action(async (bucketId, filePath, fileName) => {
836+
const { signer, chainId } = await initializeSigner();
837+
const commands = new Commands(signer, chainId);
838+
await commands.addFileToBucket([null, bucketId, filePath, fileName]);
839+
});
840+
841+
program
842+
.command("listBuckets")
843+
.description("List buckets owned by an address (defaults to signer)")
844+
.option("-o, --owner <address>", "Owner address")
845+
.action(async (options) => {
846+
const { signer, chainId } = await initializeSigner();
847+
const commands = new Commands(signer, chainId);
848+
await commands.listBuckets([null, options.owner]);
849+
});
850+
851+
program
852+
.command("listFilesInBucket")
853+
.description("List files in a bucket")
854+
.argument("<bucketId>", "Bucket id")
855+
.action(async (bucketId) => {
856+
const { signer, chainId } = await initializeSigner();
857+
const commands = new Commands(signer, chainId);
858+
await commands.listFilesInBucket([null, bucketId]);
859+
});
860+
861+
program
862+
.command("getFileObject")
863+
.description("Get the file-object descriptor for a file in a bucket")
864+
.argument("<bucketId>", "Bucket id")
865+
.argument("<fileName>", "File name")
866+
.action(async (bucketId, fileName) => {
867+
const { signer, chainId } = await initializeSigner();
868+
const commands = new Commands(signer, chainId);
869+
await commands.getFileObject([null, bucketId, fileName]);
870+
});
871+
872+
program
873+
.command("deleteFile")
874+
.description("Delete a file from a bucket")
875+
.argument("<bucketId>", "Bucket id")
876+
.argument("<fileName>", "File name")
877+
.action(async (bucketId, fileName) => {
878+
const { signer, chainId } = await initializeSigner();
879+
const commands = new Commands(signer, chainId);
880+
await commands.deleteFile([null, bucketId, fileName]);
881+
});
882+
805883
return program;
806884
}

src/commands.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1795,4 +1795,134 @@ export class Commands {
17951795
console.error(chalk.red("Error downloading node logs: "), error);
17961796
}
17971797
}
1798+
1799+
public async createBucket(args: string[]): Promise<void> {
1800+
try {
1801+
const accessListAddress = args[1];
1802+
if (!accessListAddress) {
1803+
console.error(chalk.red("accessListAddress is required"));
1804+
return;
1805+
}
1806+
if (!/^0x[a-fA-F0-9]{40}$/.test(accessListAddress)) {
1807+
console.error(chalk.red(`Invalid access list address: ${accessListAddress}`));
1808+
return;
1809+
}
1810+
1811+
const { chainId } = await this.signer.provider.getNetwork();
1812+
const accessLists = [{ [String(chainId)]: [accessListAddress] }];
1813+
const result = await ProviderInstance.createPersistentStorageBucket(
1814+
this.oceanNodeUrl,
1815+
this.signer,
1816+
{ accessLists }
1817+
);
1818+
console.log(chalk.green("Bucket created."));
1819+
console.log(util.inspect(result, false, null, true));
1820+
} catch (error) {
1821+
console.error(chalk.red("Error creating bucket: "), error);
1822+
}
1823+
}
1824+
1825+
public async addFileToBucket(args: string[]): Promise<void> {
1826+
try {
1827+
const bucketId = args[1];
1828+
const filePath = args[2];
1829+
const fileName = args[3] || (filePath ? path.basename(filePath) : undefined);
1830+
if (!bucketId || !filePath) {
1831+
console.error(chalk.red("bucketId and filePath are required"));
1832+
return;
1833+
}
1834+
if (!fs.existsSync(filePath)) {
1835+
console.error(chalk.red(`File not found: ${filePath}`));
1836+
return;
1837+
}
1838+
1839+
const stream = fs.createReadStream(filePath);
1840+
const result = await ProviderInstance.uploadPersistentStorageFile(
1841+
this.oceanNodeUrl,
1842+
this.signer,
1843+
bucketId,
1844+
fileName,
1845+
stream as unknown as AsyncIterable<Uint8Array>
1846+
);
1847+
console.log(chalk.green(`File '${fileName}' uploaded to bucket ${bucketId}.`));
1848+
console.log(util.inspect(result, false, null, true));
1849+
} catch (error) {
1850+
console.error(chalk.red("Error uploading file: "), error);
1851+
}
1852+
}
1853+
1854+
public async listBuckets(args: string[]): Promise<void> {
1855+
try {
1856+
const owner = args[1] || (await this.signer.getAddress());
1857+
const buckets = await ProviderInstance.getPersistentStorageBuckets(
1858+
this.oceanNodeUrl,
1859+
this.signer,
1860+
owner
1861+
);
1862+
console.log(chalk.cyan(`Buckets owned by ${owner}:`));
1863+
console.log(util.inspect(buckets, false, null, true));
1864+
} catch (error) {
1865+
console.error(chalk.red("Error listing buckets: "), error);
1866+
}
1867+
}
1868+
1869+
public async listFilesInBucket(args: string[]): Promise<void> {
1870+
try {
1871+
const bucketId = args[1];
1872+
if (!bucketId) {
1873+
console.error(chalk.red("bucketId is required"));
1874+
return;
1875+
}
1876+
const files = await ProviderInstance.listPersistentStorageFiles(
1877+
this.oceanNodeUrl,
1878+
this.signer,
1879+
bucketId
1880+
);
1881+
console.log(chalk.cyan(`Files in bucket ${bucketId}:`));
1882+
console.log(util.inspect(files, false, null, true));
1883+
} catch (error) {
1884+
console.error(chalk.red("Error listing files: "), error);
1885+
}
1886+
}
1887+
1888+
public async getFileObject(args: string[]): Promise<void> {
1889+
try {
1890+
const bucketId = args[1];
1891+
const fileName = args[2];
1892+
if (!bucketId || !fileName) {
1893+
console.error(chalk.red("bucketId and fileName are required"));
1894+
return;
1895+
}
1896+
const fileObject = await ProviderInstance.getPersistentStorageFileObject(
1897+
this.oceanNodeUrl,
1898+
this.signer,
1899+
bucketId,
1900+
fileName
1901+
);
1902+
console.log(JSON.stringify(fileObject, null, 2));
1903+
} catch (error) {
1904+
console.error(chalk.red("Error getting file object: "), error);
1905+
}
1906+
}
1907+
1908+
public async deleteFile(args: string[]): Promise<void> {
1909+
try {
1910+
const bucketId = args[1];
1911+
const fileName = args[2];
1912+
if (!bucketId || !fileName) {
1913+
console.error(chalk.red("bucketId and fileName are required"));
1914+
return;
1915+
}
1916+
const result = await ProviderInstance.deletePersistentStorageFile(
1917+
this.oceanNodeUrl,
1918+
this.signer,
1919+
bucketId,
1920+
fileName
1921+
);
1922+
console.log(chalk.green(`File '${fileName}' deleted from bucket ${bucketId}.`));
1923+
console.log(util.inspect(result, false, null, true));
1924+
} catch (error) {
1925+
console.error(chalk.red("Error deleting file: "), error);
1926+
}
1927+
}
17981928
}

0 commit comments

Comments
 (0)