Skip to content

Commit 9085661

Browse files
committed
feat: implement storage command functionalities and enhance CLI operations
- Added new commands for storage operations including `cp`, `ls`, `cat`, `mv`, and `stat`, allowing users to manage files and objects in storage. - Introduced a `client` module for HTTP interactions with the storage gateway API, facilitating blob uploads and downloads. - Enhanced `StorageConfig` to support dynamic gateway URL retrieval from various sources. - Implemented path parsing for storage URIs, improving the handling of storage paths. - Updated `Cargo.toml` to include new dependencies for enhanced functionality. - Added comprehensive error handling and user feedback for storage operations.
1 parent c14d314 commit 9085661

16 files changed

Lines changed: 2132 additions & 31 deletions

File tree

Cargo.lock

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

ipc/cli/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,13 @@ fendermint_app_settings = { path = "../../fendermint/app/settings" }
8888
fendermint_vm_actor_interface = { path = "../../fendermint/vm/actor_interface" }
8989
fendermint_vm_message = { path = "../../fendermint/vm/message" }
9090
fendermint_actor_blobs_shared = { path = "../../fendermint/actors/blobs/shared" }
91+
fendermint_actor_bucket = { path = "../../fendermint/actors/bucket" }
9192
fendermint_app = { path = "../../fendermint/app" }
9293
fendermint_crypto = { path = "../../fendermint/crypto" }
9394
ipc_ipld_resolver = { path = "../../ipld/resolver" }
9495
contracts-artifacts = { path = "../../contracts-artifacts" }
9596
ipc_actors_abis = { path = "../../contract-bindings" }
97+
walkdir = "2.4"
98+
dirs = "5.0"
99+
blake3 = "1.5"
100+
iroh-blobs = { workspace = true }
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// Copyright 2022-2024 Protocol Labs
2+
// SPDX-License-Identifier: MIT
3+
4+
//! Bucket operations for on-chain storage management
5+
//!
6+
//! This module provides functions to interact with bucket smart contracts.
7+
8+
use anyhow::{anyhow, Context, Result};
9+
use fendermint_actor_bucket::{
10+
AddParams, DeleteParams, GetParams, ListObjectsReturn, ListParams, Object, UpdateObjectMetadataParams,
11+
Method as BucketMethod,
12+
};
13+
use fendermint_actor_blobs_shared::bytes::B256;
14+
use fendermint_rpc::{message::GasParams, tx::{BoundClient, TxClient, TxCommit}, QueryClient};
15+
use fendermint_vm_message::query::FvmQueryHeight;
16+
use fvm_ipld_encoding::RawBytes;
17+
use fvm_shared::{address::Address, econ::TokenAmount};
18+
use std::collections::HashMap;
19+
20+
/// Default gas parameters for bucket transactions
21+
fn default_gas_params() -> GasParams {
22+
GasParams {
23+
gas_limit: 10_000_000_000,
24+
gas_fee_cap: TokenAmount::from_atto(100),
25+
gas_premium: TokenAmount::from_atto(100),
26+
}
27+
}
28+
29+
/// Add an object to a bucket
30+
///
31+
/// This registers an object's metadata on-chain after the blob has been uploaded
32+
/// to the gateway and distributed to storage nodes.
33+
pub async fn add_object<C>(
34+
client: &mut C,
35+
bucket_address: Address,
36+
source: B256,
37+
key: String,
38+
hash: B256,
39+
recovery_hash: B256,
40+
size: u64,
41+
metadata: HashMap<String, String>,
42+
data_shards: u16,
43+
parity_shards: u16,
44+
) -> Result<()>
45+
where
46+
C: BoundClient + TxClient<TxCommit> + Send + Sync,
47+
{
48+
let params = AddParams {
49+
source,
50+
key: key.into_bytes(),
51+
hash,
52+
recovery_hash,
53+
size,
54+
ttl: None, // Use default TTL
55+
metadata,
56+
overwrite: false,
57+
data_shards,
58+
parity_shards,
59+
};
60+
61+
let params_bytes = RawBytes::serialize(params).context("Failed to serialize AddParams")?;
62+
63+
let res = TxClient::<TxCommit>::transaction(
64+
client,
65+
bucket_address,
66+
BucketMethod::AddObject as u64,
67+
params_bytes,
68+
TokenAmount::zero(),
69+
default_gas_params(),
70+
)
71+
.await
72+
.context("Failed to send AddObject transaction")?;
73+
74+
if res.response.check_tx.code.is_err() {
75+
return Err(anyhow!(
76+
"AddObject check_tx failed: {}",
77+
res.response.check_tx.log
78+
));
79+
}
80+
81+
if res.response.deliver_tx.code.is_err() {
82+
return Err(anyhow!(
83+
"AddObject deliver_tx failed: {}",
84+
res.response.deliver_tx.log
85+
));
86+
}
87+
88+
Ok(())
89+
}
90+
91+
/// Get an object from a bucket
92+
pub async fn get_object<C>(
93+
client: &mut C,
94+
bucket_address: Address,
95+
key: String,
96+
) -> Result<Option<Object>>
97+
where
98+
C: QueryClient + Send + Sync,
99+
{
100+
let params = GetParams(key.into_bytes());
101+
let gas_params = GasParams {
102+
gas_limit: Default::default(),
103+
gas_fee_cap: Default::default(),
104+
gas_premium: Default::default(),
105+
};
106+
107+
client
108+
.os_get_call(
109+
bucket_address,
110+
params,
111+
TokenAmount::default(),
112+
gas_params,
113+
FvmQueryHeight::default(),
114+
)
115+
.await
116+
.context("Failed to query object")
117+
}
118+
119+
/// List objects in a bucket
120+
pub async fn list_objects<C>(
121+
client: &C,
122+
bucket_address: Address,
123+
prefix: Option<String>,
124+
delimiter: Option<String>,
125+
start_key: Option<String>,
126+
limit: u64,
127+
) -> Result<ListObjectsReturn>
128+
where
129+
C: QueryClient + Send + Sync,
130+
{
131+
let params = ListParams {
132+
prefix: prefix.unwrap_or_default().into_bytes(),
133+
delimiter: delimiter.unwrap_or_default().into_bytes(),
134+
start_key: start_key.map(|s| s.into_bytes()),
135+
limit,
136+
};
137+
138+
let params_bytes = RawBytes::serialize(params).context("Failed to serialize ListParams")?;
139+
140+
let msg = fvm_shared::message::Message {
141+
version: Default::default(),
142+
from: fendermint_vm_actor_interface::system::SYSTEM_ACTOR_ADDR,
143+
to: bucket_address,
144+
sequence: 0,
145+
value: TokenAmount::zero(),
146+
method_num: BucketMethod::ListObjects as u64,
147+
params: params_bytes,
148+
gas_limit: 10_000_000_000,
149+
gas_fee_cap: TokenAmount::zero(),
150+
gas_premium: TokenAmount::zero(),
151+
};
152+
153+
let response = client
154+
.call(msg, FvmQueryHeight::default())
155+
.await
156+
.context("Failed to execute ListObjects call")?;
157+
158+
if response.value.code.is_err() {
159+
return Err(anyhow!("ListObjects query failed: {}", response.value.info));
160+
}
161+
162+
let return_data = fendermint_rpc::response::decode_data(&response.value.data)
163+
.context("Failed to decode response data")?;
164+
165+
let result = fvm_ipld_encoding::from_slice::<ListObjectsReturn>(&return_data)
166+
.context("Failed to decode ListObjects response")?;
167+
168+
Ok(result)
169+
}
170+
171+
/// Delete an object from a bucket
172+
pub async fn delete_object<C>(
173+
client: &mut C,
174+
bucket_address: Address,
175+
key: String,
176+
) -> Result<()>
177+
where
178+
C: BoundClient + TxClient<TxCommit> + Send + Sync,
179+
{
180+
let params = DeleteParams(key.into_bytes());
181+
let params_bytes = RawBytes::serialize(params).context("Failed to serialize DeleteParams")?;
182+
183+
let res = TxClient::<TxCommit>::transaction(
184+
client,
185+
bucket_address,
186+
BucketMethod::DeleteObject as u64,
187+
params_bytes,
188+
TokenAmount::zero(),
189+
default_gas_params(),
190+
)
191+
.await
192+
.context("Failed to send DeleteObject transaction")?;
193+
194+
if res.response.check_tx.code.is_err() {
195+
return Err(anyhow!(
196+
"DeleteObject check_tx failed: {}",
197+
res.response.check_tx.log
198+
));
199+
}
200+
201+
if res.response.deliver_tx.code.is_err() {
202+
return Err(anyhow!(
203+
"DeleteObject deliver_tx failed: {}",
204+
res.response.deliver_tx.log
205+
));
206+
}
207+
208+
Ok(())
209+
}
210+
211+
/// Update object metadata
212+
pub async fn update_object_metadata<C>(
213+
client: &mut C,
214+
bucket_address: Address,
215+
key: String,
216+
metadata: HashMap<String, Option<String>>,
217+
) -> Result<()>
218+
where
219+
C: BoundClient + TxClient<TxCommit> + Send + Sync,
220+
{
221+
let params = UpdateObjectMetadataParams {
222+
key: key.into_bytes(),
223+
metadata,
224+
};
225+
let params_bytes =
226+
RawBytes::serialize(params).context("Failed to serialize UpdateObjectMetadataParams")?;
227+
228+
let res = TxClient::<TxCommit>::transaction(
229+
client,
230+
bucket_address,
231+
BucketMethod::UpdateObjectMetadata as u64,
232+
params_bytes,
233+
TokenAmount::zero(),
234+
default_gas_params(),
235+
)
236+
.await
237+
.context("Failed to send UpdateObjectMetadata transaction")?;
238+
239+
if res.response.check_tx.code.is_err() {
240+
return Err(anyhow!(
241+
"UpdateObjectMetadata check_tx failed: {}",
242+
res.response.check_tx.log
243+
));
244+
}
245+
246+
if res.response.deliver_tx.code.is_err() {
247+
return Err(anyhow!(
248+
"UpdateObjectMetadata deliver_tx failed: {}",
249+
res.response.deliver_tx.log
250+
));
251+
}
252+
253+
Ok(())
254+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2022-2024 Protocol Labs
2+
// SPDX-License-Identifier: MIT
3+
4+
//! Cat command for displaying file contents from storage
5+
6+
use anyhow::{anyhow, Context, Result};
7+
use clap::Args;
8+
use std::io::{self, Write};
9+
use std::path::PathBuf;
10+
11+
use crate::commands::storage::{client::GatewayClient, config::StorageConfig, path};
12+
use crate::{CommandLineHandler, GlobalArguments};
13+
14+
#[derive(Debug, Args)]
15+
pub struct CatArgs {
16+
/// Storage path (ipc://bucket_address/path/to/file)
17+
#[arg(value_name = "PATH")]
18+
pub path: String,
19+
20+
/// Gateway URL (overrides config and env var)
21+
#[arg(long)]
22+
pub gateway: Option<String>,
23+
24+
/// Storage config file
25+
#[arg(long)]
26+
pub config: Option<PathBuf>,
27+
}
28+
29+
pub struct CatStorage;
30+
31+
impl CommandLineHandler for CatStorage {
32+
type Arguments = CatArgs;
33+
34+
async fn handle(_global: &GlobalArguments, args: &Self::Arguments) -> Result<()> {
35+
let storage_path = path::StoragePath::parse(&args.path)?;
36+
37+
if storage_path.is_bucket_root() {
38+
return Err(anyhow!("Path must include a file key, not just bucket address"));
39+
}
40+
41+
// Load config
42+
let config_path = args.config.clone().unwrap_or_else(|| {
43+
dirs::home_dir()
44+
.unwrap()
45+
.join(".ipc")
46+
.join("storage_default.yaml")
47+
});
48+
49+
let mut config = if config_path.exists() {
50+
StorageConfig::load(&config_path)?
51+
} else {
52+
return Err(anyhow!(
53+
"Storage config not found at {}. Run 'ipc-cli storage init' first.",
54+
config_path.display()
55+
));
56+
};
57+
58+
// Get gateway URL
59+
let gateway_url = config.get_gateway_url(
60+
args.gateway.as_deref(),
61+
Some(&config_path),
62+
false, // not interactive (avoid prompting when piping)
63+
)?;
64+
65+
// Download from gateway
66+
let gateway_client = GatewayClient::new(gateway_url)?;
67+
let data = gateway_client
68+
.download_object(&storage_path.bucket_address, &storage_path.key, None)
69+
.await
70+
.context("Failed to download object from gateway")?;
71+
72+
// Write to stdout
73+
io::stdout().write_all(&data)?;
74+
io::stdout().flush()?;
75+
76+
Ok(())
77+
}
78+
}

0 commit comments

Comments
 (0)