Skip to content

Commit bf29ae6

Browse files
committed
fix: fix ic_object_store with integration test
1 parent 1da3273 commit bf29ae6

File tree

12 files changed

+381
-223
lines changed

12 files changed

+381
-223
lines changed

Cargo.lock

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

examples/upload_js/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"license": "ISC",
1212
"description": "",
1313
"dependencies": {
14-
"@dfinity/agent": "^2.0.0",
15-
"@dfinity/identity": "^2.0.0",
16-
"@dfinity/principal": "^2.0.0",
17-
"@dfinity/utils": "^2.4.0",
18-
"@ldclabs/ic_oss_ts": "^0.6.5"
14+
"@dfinity/agent": "^2.4.1",
15+
"@dfinity/identity": "^2.4.1",
16+
"@dfinity/principal": "^2.4.1",
17+
"@dfinity/utils": "^2.13.0",
18+
"@ldclabs/ic_oss_ts": "^1.1.0"
1919
}
2020
}

src/ic_object_store/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ ic_cose_types = { workspace = true }
2727
[dev-dependencies]
2828
tokio = { workspace = true }
2929
ed25519-consensus = { workspace = true }
30+
object_store = { workspace = true, features = ["integration", "rand"] }

src/ic_object_store/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,68 @@
99

1010
`ic_object_store` is the Rust version of the client SDK for the IC Object Store canister.
1111

12+
## Overview
13+
14+
This library provides a Rust client SDK for interacting with the IC Object Store canister, which implements the Apache [Object Store](https://github.com/apache/arrow-rs-object-store) interface on the Internet Computer. It allows developers to seamlessly integrate with the decentralized storage capabilities of the Internet Computer using familiar Apache Object Store APIs.
15+
16+
## Features
17+
18+
- Full implementation of Apache Arrow object store APIs
19+
- Secure data storage with AES256-GCM encryption
20+
- Asynchronous stream operations for efficient data handling
21+
- Seamless integration with the Internet Computer ecosystem
22+
- Compatible with the broader Apache ecosystem
23+
24+
## Installation
25+
26+
Add this to your `Cargo.toml`:
27+
28+
```toml
29+
[dependencies]
30+
ic_object_store = "1.1"
31+
```
32+
33+
## Usage
34+
35+
```rust
36+
use ic_object_store::{Client, ObjectStoreClient, build_agent};
37+
use object_store::ObjectStore;
38+
39+
let secret = [8u8; 32];
40+
// backend: IC Object Store Canister
41+
let canister = Principal::from_text("6at64-oyaaa-aaaap-anvza-cai").unwrap();
42+
let sk = SigningKey::from(secret);
43+
let id = BasicIdentity::from_signing_key(sk);
44+
println!("id: {:?}", id.sender().unwrap().to_text());
45+
// jjn6g-sh75l-r3cxb-wxrkl-frqld-6p6qq-d4ato-wske5-op7s5-n566f-bqe
46+
47+
let agent = build_agent("https://ic0.app", Arc::new(id))
48+
.await
49+
.unwrap();
50+
let client = Arc::new(Client::new(Arc::new(agent), canister, Some(secret)));
51+
let storage = ObjectStoreClient::new(client);
52+
53+
let path = Path::from("test/hello.txt");
54+
let payload = "Hello Anda!".as_bytes().to_vec();
55+
let res = storage
56+
.put_opts(&path, payload.into(), Default::default())
57+
.await
58+
.unwrap();
59+
println!("put result: {:?}", res);
60+
61+
let res = storage.get_opts(&path, Default::default()).await.unwrap();
62+
println!("get result: {:?}", res);
63+
```
64+
1265
## Documentation
1366

1467
For detailed documentation, please visit: https://docs.rs/ic_object_store
1568

69+
## Related Projects
70+
71+
- [IC Object Store Canister](https://github.com/ldclabs/ic-oss/tree/main/src/ic_object_store_canister) - The canister implementation
72+
- [IC-OSS](https://github.com/ldclabs/ic-oss) - A decentralized Object Storage Service on the Internet Computer
73+
1674
## License
1775

1876
Copyright © 2024-2025 [LDC Labs](https://github.com/ldclabs).

src/ic_object_store/src/agent.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,17 @@ pub async fn build_agent(host: &str, identity: Arc<dyn Identity>) -> Result<Agen
1818
let agent = Agent::builder()
1919
.with_url(host)
2020
.with_arc_identity(identity)
21-
.with_verify_query_signatures(false)
22-
.with_background_dynamic_routing()
23-
.build()
24-
.map_err(format_error)?;
21+
.with_verify_query_signatures(false);
22+
23+
let agent = if host.starts_with("https://") {
24+
agent
25+
.with_background_dynamic_routing()
26+
.build()
27+
.map_err(format_error)?
28+
} else {
29+
agent.build().map_err(format_error)?
30+
};
31+
2532
if host.starts_with("http://") {
2633
agent.fetch_root_key().await.map_err(format_error)?;
2734
}

src/ic_object_store/src/client.rs

Lines changed: 98 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use candid::{
66
CandidType, Decode, Principal,
77
};
88
use chrono::DateTime;
9-
use futures::{stream::BoxStream, StreamExt, TryStreamExt};
9+
use futures::{stream::BoxStream, StreamExt};
1010
use ic_agent::Agent;
1111
use ic_cose_types::{BoxError, CanisterCaller};
1212
use ic_oss_types::{format_error, object_store::*};
@@ -321,7 +321,7 @@ pub trait ObjectStoreSDK: CanisterCaller + Sized {
321321
.await
322322
.map_err(|error| Error::Generic {
323323
error: format_error(error),
324-
})
324+
})?
325325
}
326326

327327
/// Lists objects under a prefix
@@ -829,17 +829,12 @@ impl ObjectStore for ObjectStoreClient {
829829
) -> BoxStream<'static, object_store::Result<object_store::ObjectMeta>> {
830830
let prefix = prefix.cloned();
831831
let client = self.client.clone();
832-
futures::stream::once(async move {
833-
let res = client.list(prefix.as_ref()).await;
834-
let values: Vec<object_store::Result<object_store::ObjectMeta, object_store::Error>> =
835-
match res {
836-
Ok(res) => res.into_iter().map(|v| Ok(from_object_meta(v))).collect(),
837-
Err(err) => vec![Err(from_error(err))],
838-
};
839-
840-
Ok::<_, object_store::Error>(futures::stream::iter(values))
841-
})
842-
.try_flatten()
832+
try_stream! {
833+
let res = client.list(prefix.as_ref()).await.map_err(from_error)?;
834+
for object in res {
835+
yield from_object_meta(object);
836+
}
837+
}
843838
.boxed()
844839
}
845840

@@ -852,17 +847,12 @@ impl ObjectStore for ObjectStoreClient {
852847
let prefix = prefix.cloned();
853848
let offset = offset.clone();
854849
let client = self.client.clone();
855-
futures::stream::once(async move {
856-
let res = client.list_with_offset(prefix.as_ref(), &offset).await;
857-
let values: Vec<object_store::Result<object_store::ObjectMeta, object_store::Error>> =
858-
match res {
859-
Ok(res) => res.into_iter().map(|v| Ok(from_object_meta(v))).collect(),
860-
Err(err) => vec![Err(from_error(err))],
861-
};
862-
863-
Ok::<_, object_store::Error>(futures::stream::iter(values))
864-
})
865-
.try_flatten()
850+
try_stream! {
851+
let res = client.list_with_offset(prefix.as_ref(), &offset).await.map_err(from_error)?;
852+
for object in res {
853+
yield from_object_meta(object);
854+
}
855+
}
866856
.boxed()
867857
}
868858

@@ -879,7 +869,11 @@ impl ObjectStore for ObjectStoreClient {
879869

880870
Ok(object_store::ListResult {
881871
objects: res.objects.into_iter().map(from_object_meta).collect(),
882-
common_prefixes: res.common_prefixes.into_iter().map(Path::from).collect(),
872+
common_prefixes: res
873+
.common_prefixes
874+
.into_iter()
875+
.map(|p| Path::parse(p).unwrap())
876+
.collect(),
883877
})
884878
}
885879

@@ -995,7 +989,7 @@ fn create_decryption_stream(
995989
let data = data?;
996990
buf.extend_from_slice(&data);
997991

998-
while buf.len() >= CHUNK_SIZE as usize {
992+
while buf.len() > CHUNK_SIZE as usize {
999993
let mut chunk = buf.drain(..CHUNK_SIZE as usize).collect::<Vec<u8>>();
1000994

1001995
let tag = aes_tags.get(idx).ok_or_else(|| object_store::Error::Generic {
@@ -1004,8 +998,8 @@ fn create_decryption_stream(
1004998
})?;
1005999

10061000
decrypt_chunk(&cipher, nonce_ref, &mut chunk, tag, &location)?;
1007-
if idx == start_idx {
1008-
chunk = chunk[start_offset..].to_vec();
1001+
if idx == start_idx && start_offset > 0 {
1002+
chunk.drain(..start_offset);
10091003
}
10101004

10111005
remaining = remaining.saturating_sub(chunk.len());
@@ -1021,9 +1015,10 @@ fn create_decryption_stream(
10211015
source: format!("missing AES256 tag for chunk {idx} for path {location}").into(),
10221016
})?;
10231017
decrypt_chunk(&cipher, nonce_ref, &mut buf, tag, &location)?;
1024-
if idx == start_idx {
1025-
buf = buf[start_offset..].to_vec();
1018+
if idx == start_idx && start_offset > 0 {
1019+
buf.drain(..start_offset);
10261020
}
1021+
10271022
buf.truncate(remaining);
10281023
yield bytes::Bytes::from(buf);
10291024
}
@@ -1091,7 +1086,7 @@ pub fn from_error(err: Error) -> object_store::Error {
10911086
/// Converted object_store::ObjectMeta with equivalent fields
10921087
pub fn from_object_meta(val: ObjectMeta) -> object_store::ObjectMeta {
10931088
object_store::ObjectMeta {
1094-
location: val.location.into(),
1089+
location: Path::parse(val.location).unwrap(),
10951090
last_modified: DateTime::from_timestamp_millis(val.last_modified as i64)
10961091
.expect("invalid timestamp"),
10971092
size: val.size,
@@ -1227,6 +1222,7 @@ mod tests {
12271222
use ed25519_consensus::SigningKey;
12281223
use ic_agent::{identity::BasicIdentity, Identity};
12291224
use ic_cose_types::cose::sha3_256;
1225+
use object_store::integration::*;
12301226

12311227
#[tokio::test(flavor = "current_thread")]
12321228
#[ignore]
@@ -1253,23 +1249,13 @@ mod tests {
12531249
println!("put result: {:?}", res);
12541250

12551251
let res = oc.get_opts(&path, Default::default()).await.unwrap();
1256-
println!("get result: {:?}", res);
12571252
assert_eq!(res.meta.size as usize, payload.len());
1258-
let res = match res.payload {
1259-
object_store::GetResultPayload::Stream(mut stream) => {
1260-
let mut buf = Vec::new();
1261-
while let Some(data) = stream.next().await {
1262-
buf.extend_from_slice(&data.unwrap());
1263-
}
1264-
buf
1265-
}
1266-
};
1267-
assert_eq!(res, payload);
1253+
let res = res.bytes().await.unwrap();
1254+
assert_eq!(res.to_vec(), payload);
12681255

12691256
let res = cli.get_opts(&path, Default::default()).await.unwrap();
1270-
println!("get result: {:?}", res);
12711257
assert_eq!(res.meta.size as usize, payload.len());
1272-
assert_eq!(&res.payload, &payload);
1258+
assert_ne!(&res.payload, &payload);
12731259
let aes_nonce = res.meta.aes_nonce.unwrap();
12741260
assert_eq!(aes_nonce.len(), 12);
12751261
let aes_tags = res.meta.aes_tags.unwrap();
@@ -1299,44 +1285,92 @@ mod tests {
12991285
}
13001286
let res = oc.get_opts(&path, Default::default()).await.unwrap();
13011287
assert_eq!(res.meta.size as usize, payload.len());
1302-
let res = match res.payload {
1303-
object_store::GetResultPayload::Stream(mut stream) => {
1304-
let mut buf = bytes::BytesMut::new();
1305-
while let Some(data) = stream.next().await {
1306-
buf.extend_from_slice(&data.unwrap());
1307-
}
1308-
buf.freeze() // Convert to immutable Bytes
1309-
}
1310-
};
1311-
assert_eq!(res, payload);
1288+
let res = res.bytes().await.unwrap();
1289+
assert_eq!(res.to_vec(), payload);
13121290

13131291
let res = cli.get_opts(&path, Default::default()).await.unwrap();
13141292
assert_eq!(res.meta.size as usize, payload.len());
1315-
assert_eq!(&res.payload, &payload);
1293+
assert_ne!(&res.payload, &payload);
13161294
let aes_nonce = res.meta.aes_nonce.unwrap();
13171295
assert_eq!(aes_nonce.len(), 12);
13181296
let aes_tags = res.meta.aes_tags.unwrap();
13191297
assert_eq!(aes_tags.len(), len.div_ceil(CHUNK_SIZE) as usize);
13201298

1321-
let ranges = vec![(0u64, 1000), (100, 100000), (len - CHUNK_SIZE - 1, len)];
1299+
let ranges = vec![0u64..1000, 100..100000, len - CHUNK_SIZE - 1..len];
13221300

1323-
let rt = cli.get_ranges(&path, &ranges).await.unwrap();
1301+
let rt = oc.get_ranges(&path, &ranges).await.unwrap();
13241302
assert_eq!(rt.len(), ranges.len());
1325-
for (i, (start, end)) in ranges.into_iter().enumerate() {
1326-
let res = cli
1303+
1304+
for (i, Range { start, end }) in ranges.into_iter().enumerate() {
1305+
let res = oc
13271306
.get_opts(
13281307
&path,
1329-
GetOptions {
1330-
range: Some(GetRange::Bounded(start, end)),
1308+
object_store::GetOptions {
1309+
range: Some(object_store::GetRange::Bounded(start..end)),
13311310
..Default::default()
13321311
},
13331312
)
13341313
.await
13351314
.unwrap();
1336-
assert_eq!(rt[i], &res.payload);
1337-
assert_eq!(&res.payload, &payload[start as usize..end as usize]);
1338-
assert_eq!(res.meta.location, path.as_ref());
1315+
assert_eq!(res.meta.location, path);
13391316
assert_eq!(res.meta.size as usize, payload.len());
1317+
let data = res.bytes().await.unwrap();
1318+
assert_eq!(rt[i].len(), data.len());
1319+
assert_eq!(&data, &payload[start as usize..end as usize]);
1320+
}
1321+
}
1322+
1323+
const NON_EXISTENT_NAME: &str = "nonexistentname";
1324+
1325+
#[tokio::test]
1326+
#[ignore]
1327+
async fn integration_test() {
1328+
// Should be run in a clean environment
1329+
// dfx canister call ic_object_store_canister admin_clear '()'
1330+
let secret = [8u8; 32];
1331+
let canister = Principal::from_text("6at64-oyaaa-aaaap-anvza-cai").unwrap();
1332+
let sk = SigningKey::from(secret);
1333+
let id = BasicIdentity::from_signing_key(sk);
1334+
println!("id: {:?}", id.sender().unwrap().to_text());
1335+
// jjn6g-sh75l-r3cxb-wxrkl-frqld-6p6qq-d4ato-wske5-op7s5-n566f-bqe
1336+
1337+
let agent = build_agent("http://localhost:4943", Arc::new(id))
1338+
.await
1339+
.unwrap();
1340+
let cli = Arc::new(Client::new(Arc::new(agent), canister, Some(secret)));
1341+
let storage = ObjectStoreClient::new(cli.clone());
1342+
1343+
let location = Path::from(NON_EXISTENT_NAME);
1344+
1345+
let err = get_nonexistent_object(&storage, Some(location))
1346+
.await
1347+
.unwrap_err();
1348+
if let object_store::Error::NotFound { path, .. } = err {
1349+
assert!(path.ends_with(NON_EXISTENT_NAME));
1350+
} else {
1351+
panic!("unexpected error type: {err:?}");
1352+
}
1353+
1354+
put_get_delete_list(&storage).await;
1355+
put_get_attributes(&storage).await;
1356+
get_opts(&storage).await;
1357+
put_opts(&storage, true).await;
1358+
list_uses_directories_correctly(&storage).await;
1359+
list_with_delimiter(&storage).await;
1360+
rename_and_copy(&storage).await;
1361+
copy_if_not_exists(&storage).await;
1362+
copy_rename_nonexistent_object(&storage).await;
1363+
// multipart_race_condition(&storage, true).await; // TODO: fix this test?
1364+
multipart_out_of_order(&storage).await;
1365+
1366+
let objs = storage.list(None).collect::<Vec<_>>().await;
1367+
for obj in objs {
1368+
let obj = obj.unwrap();
1369+
storage
1370+
.delete(&obj.location)
1371+
.await
1372+
.expect("failed to delete object");
13401373
}
1374+
stream_get(&storage).await;
13411375
}
13421376
}

0 commit comments

Comments
 (0)