Skip to content

Commit 6a9247a

Browse files
committed
Add fula-flutter crate for Flutter bindings
Introduces the new fula-flutter crate providing Flutter/Dart bindings for Fula decentralized storage, supporting Android and Web via flutter_rust_bridge. Implements API modules for client, encrypted, chunked, forest, sharing, rotation, and multipart operations, along with error handling and type definitions. Updates workspace configuration and documentation for Flutter and WASM integration.
1 parent 07f94b5 commit 6a9247a

21 files changed

Lines changed: 4045 additions & 34 deletions

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"crates/fula-core",
77
"crates/fula-cli",
88
"crates/fula-client",
9+
"crates/fula-flutter",
910
]
1011

1112
# Root package for examples

crates/fula-flutter/Cargo.toml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
[package]
2+
name = "fula-flutter"
3+
description = "Flutter bindings for Fula decentralized storage - works on Android and Web"
4+
version.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
repository.workspace = true
8+
authors.workspace = true
9+
rust-version.workspace = true
10+
11+
[lib]
12+
crate-type = ["cdylib", "staticlib", "lib"]
13+
14+
[dependencies]
15+
# flutter_rust_bridge
16+
flutter_rust_bridge = "2"
17+
18+
# Error handling
19+
anyhow = { workspace = true }
20+
thiserror = { workspace = true }
21+
22+
# Async
23+
futures = { workspace = true }
24+
25+
# Serialization
26+
serde = { workspace = true }
27+
serde_json = { workspace = true }
28+
29+
# Time
30+
chrono = { workspace = true }
31+
32+
# Utilities
33+
bytes = { workspace = true }
34+
35+
# Thread safety
36+
parking_lot = { workspace = true }
37+
38+
# Platform-specific dependencies
39+
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
40+
# Native: fula-client with default features (includes tokio)
41+
fula-client = { path = "../fula-client" }
42+
fula-crypto = { path = "../fula-crypto" }
43+
44+
[target.'cfg(target_arch = "wasm32")'.dependencies]
45+
# WASM: fula-client without tokio
46+
fula-client = { path = "../fula-client", default-features = false }
47+
fula-crypto = { path = "../fula-crypto", default-features = false, features = ["wasm"] }
48+
wasm-bindgen-futures = "0.4"
49+
50+
[dev-dependencies]
51+
# For WASM tests
52+
wasm-bindgen-test = "0.3"
53+
54+
[features]
55+
default = []
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//! Chunked/streaming operations
2+
//!
3+
//! Functions for handling large files using chunked encryption.
4+
//! Files above a certain threshold are automatically split into chunks.
5+
6+
use crate::api::types::*;
7+
use crate::api::error::FulaResult;
8+
9+
/// Default chunk size (256 KB)
10+
pub const DEFAULT_CHUNK_SIZE: u32 = 256 * 1024;
11+
12+
/// Threshold for using chunked encryption (5 MB)
13+
pub const CHUNKED_THRESHOLD: u64 = 5 * 1024 * 1024;
14+
15+
// ============================================================================
16+
// Chunked Operations
17+
// ============================================================================
18+
19+
/// Upload a large file using chunked encryption
20+
///
21+
/// The file is split into chunks, each encrypted separately.
22+
/// This is more efficient for large files and enables partial downloads.
23+
pub async fn put_chunked(
24+
client: &EncryptedClientHandle,
25+
bucket: String,
26+
key: String,
27+
data: Vec<u8>,
28+
chunk_size: Option<u32>,
29+
) -> FulaResult<PutResult> {
30+
let guard = client.inner.read();
31+
let chunk_size = chunk_size.map(|s| s as usize);
32+
let result = guard.put_object_chunked(&bucket, &key, &data, chunk_size).await?;
33+
Ok(result.into())
34+
}
35+
36+
/// Download and decrypt a chunked file
37+
///
38+
/// Automatically handles reassembly of chunks.
39+
pub async fn get_chunked(
40+
client: &EncryptedClientHandle,
41+
bucket: String,
42+
key: String,
43+
) -> FulaResult<Vec<u8>> {
44+
let guard = client.inner.read();
45+
let data = guard.get_object_chunked(&bucket, &key).await?;
46+
Ok(data.to_vec())
47+
}
48+
49+
/// Download a range of bytes from a file
50+
///
51+
/// For chunked files, only the necessary chunks are downloaded.
52+
/// This is efficient for large files when you only need part of the content.
53+
pub async fn get_range(
54+
client: &EncryptedClientHandle,
55+
bucket: String,
56+
key: String,
57+
offset: u64,
58+
length: u64,
59+
) -> FulaResult<Vec<u8>> {
60+
let guard = client.inner.read();
61+
let data = guard.get_object_range(&bucket, &key, offset, length).await?;
62+
Ok(data.to_vec())
63+
}
64+
65+
/// Check if a file size warrants chunked upload
66+
///
67+
/// Returns true if the size is above the chunked threshold.
68+
pub fn should_use_chunked(size: u64) -> bool {
69+
size > CHUNKED_THRESHOLD
70+
}
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
//! Core FulaClient wrapper operations
2+
//!
3+
//! These functions wrap the underlying FulaClient for plain (unencrypted) operations.
4+
5+
use std::sync::Arc;
6+
use std::time::Duration;
7+
use bytes::Bytes;
8+
9+
use crate::api::types::*;
10+
use crate::api::error::{FulaError, FulaResult};
11+
12+
// ============================================================================
13+
// Client Creation
14+
// ============================================================================
15+
16+
/// Create a new Fula client with the given configuration
17+
pub fn create_client(config: FulaConfig) -> FulaResult<FulaClientHandle> {
18+
let inner_config = fula_client::Config::new(&config.endpoint)
19+
.with_timeout(Duration::from_secs(config.timeout_seconds));
20+
21+
let inner_config = if let Some(token) = config.access_token {
22+
inner_config.with_token(token)
23+
} else {
24+
inner_config
25+
};
26+
27+
let client = fula_client::FulaClient::new(inner_config)?;
28+
29+
Ok(FulaClientHandle {
30+
inner: Arc::new(client),
31+
})
32+
}
33+
34+
/// Create a new encrypted client with the given configuration
35+
pub fn create_encrypted_client(
36+
config: FulaConfig,
37+
encryption: EncryptionConfig,
38+
) -> FulaResult<EncryptedClientHandle> {
39+
let inner_config = fula_client::Config::new(&config.endpoint)
40+
.with_timeout(Duration::from_secs(config.timeout_seconds));
41+
42+
let inner_config = if let Some(token) = config.access_token {
43+
inner_config.with_token(token)
44+
} else {
45+
inner_config
46+
};
47+
48+
// Create encryption config
49+
let enc_config = if let Some(secret_key) = encryption.secret_key {
50+
if secret_key.len() != 32 {
51+
return Err(FulaError::InvalidConfig(
52+
"Secret key must be exactly 32 bytes".to_string()
53+
));
54+
}
55+
let mut key_bytes = [0u8; 32];
56+
key_bytes.copy_from_slice(&secret_key);
57+
let secret = fula_crypto::SecretKey::from_bytes(&key_bytes)
58+
.map_err(|e| FulaError::Encryption(e.to_string()))?;
59+
fula_client::EncryptionConfig::from_secret_key(secret)
60+
} else {
61+
fula_client::EncryptionConfig::new()
62+
};
63+
64+
let enc_config = enc_config.with_metadata_privacy(encryption.enable_metadata_privacy);
65+
let enc_config = match encryption.obfuscation_mode {
66+
ObfuscationMode::Deterministic => {
67+
enc_config.with_obfuscation_mode(fula_client::KeyObfuscation::DeterministicHash)
68+
}
69+
ObfuscationMode::Random => {
70+
enc_config.with_obfuscation_mode(fula_client::KeyObfuscation::RandomUuid)
71+
}
72+
};
73+
74+
let client = fula_client::EncryptedClient::new(inner_config, enc_config)?;
75+
76+
Ok(EncryptedClientHandle {
77+
inner: Arc::new(parking_lot::RwLock::new(client)),
78+
})
79+
}
80+
81+
/// Create encrypted client with pinning support
82+
pub fn create_encrypted_client_with_pinning(
83+
config: FulaConfig,
84+
encryption: EncryptionConfig,
85+
pinning: PinningConfig,
86+
) -> FulaResult<EncryptedClientHandle> {
87+
let inner_config = fula_client::Config::new(&config.endpoint)
88+
.with_timeout(Duration::from_secs(config.timeout_seconds));
89+
90+
let inner_config = if let Some(token) = config.access_token {
91+
inner_config.with_token(token)
92+
} else {
93+
inner_config
94+
};
95+
96+
// Create encryption config
97+
let enc_config = if let Some(secret_key) = encryption.secret_key {
98+
if secret_key.len() != 32 {
99+
return Err(FulaError::InvalidConfig(
100+
"Secret key must be exactly 32 bytes".to_string()
101+
));
102+
}
103+
let mut key_bytes = [0u8; 32];
104+
key_bytes.copy_from_slice(&secret_key);
105+
let secret = fula_crypto::SecretKey::from_bytes(&key_bytes)
106+
.map_err(|e| FulaError::Encryption(e.to_string()))?;
107+
fula_client::EncryptionConfig::from_secret_key(secret)
108+
} else {
109+
fula_client::EncryptionConfig::new()
110+
};
111+
112+
let enc_config = enc_config.with_metadata_privacy(encryption.enable_metadata_privacy);
113+
114+
let pinning_creds = fula_client::PinningCredentials::new(
115+
pinning.endpoint,
116+
pinning.token,
117+
);
118+
119+
let client = fula_client::EncryptedClient::new_with_pinning(
120+
inner_config,
121+
enc_config,
122+
pinning_creds,
123+
)?;
124+
125+
Ok(EncryptedClientHandle {
126+
inner: Arc::new(parking_lot::RwLock::new(client)),
127+
})
128+
}
129+
130+
// ============================================================================
131+
// Bucket Operations
132+
// ============================================================================
133+
134+
/// List all buckets
135+
pub async fn list_buckets(client: &FulaClientHandle) -> FulaResult<Vec<BucketInfo>> {
136+
let result = client.inner.list_buckets().await?;
137+
Ok(result.buckets.into_iter().map(|b| b.into()).collect())
138+
}
139+
140+
/// Create a new bucket
141+
pub async fn create_bucket(client: &FulaClientHandle, name: String) -> FulaResult<()> {
142+
client.inner.create_bucket(&name).await?;
143+
Ok(())
144+
}
145+
146+
/// Delete a bucket
147+
pub async fn delete_bucket(client: &FulaClientHandle, name: String) -> FulaResult<()> {
148+
client.inner.delete_bucket(&name).await?;
149+
Ok(())
150+
}
151+
152+
/// Check if a bucket exists
153+
pub async fn bucket_exists(client: &FulaClientHandle, name: String) -> FulaResult<bool> {
154+
Ok(client.inner.bucket_exists(&name).await?)
155+
}
156+
157+
// ============================================================================
158+
// Object Operations
159+
// ============================================================================
160+
161+
/// Upload an object
162+
pub async fn put_object(
163+
client: &FulaClientHandle,
164+
bucket: String,
165+
key: String,
166+
data: Vec<u8>,
167+
) -> FulaResult<PutResult> {
168+
let result = client.inner.put_object(&bucket, &key, Bytes::from(data)).await?;
169+
Ok(result.into())
170+
}
171+
172+
/// Upload an object with metadata
173+
pub async fn put_object_with_metadata(
174+
client: &FulaClientHandle,
175+
bucket: String,
176+
key: String,
177+
data: Vec<u8>,
178+
metadata: ObjectMetadata,
179+
) -> FulaResult<PutResult> {
180+
let result = client.inner.put_object_with_metadata(
181+
&bucket,
182+
&key,
183+
Bytes::from(data),
184+
Some(metadata.into()),
185+
).await?;
186+
Ok(result.into())
187+
}
188+
189+
/// Download an object
190+
pub async fn get_object(
191+
client: &FulaClientHandle,
192+
bucket: String,
193+
key: String,
194+
) -> FulaResult<Vec<u8>> {
195+
let data = client.inner.get_object(&bucket, &key).await?;
196+
Ok(data.to_vec())
197+
}
198+
199+
/// Download an object with metadata
200+
pub async fn get_object_with_metadata(
201+
client: &FulaClientHandle,
202+
bucket: String,
203+
key: String,
204+
) -> FulaResult<GetObjectResult> {
205+
let result = client.inner.get_object_with_metadata(&bucket, &key).await?;
206+
Ok(result.into())
207+
}
208+
209+
/// Get object metadata without downloading content
210+
pub async fn head_object(
211+
client: &FulaClientHandle,
212+
bucket: String,
213+
key: String,
214+
) -> FulaResult<HeadResult> {
215+
let result = client.inner.head_object(&bucket, &key).await?;
216+
Ok(result.into())
217+
}
218+
219+
/// Delete an object
220+
pub async fn delete_object(
221+
client: &FulaClientHandle,
222+
bucket: String,
223+
key: String,
224+
) -> FulaResult<()> {
225+
client.inner.delete_object(&bucket, &key).await?;
226+
Ok(())
227+
}
228+
229+
/// Check if an object exists
230+
pub async fn object_exists(
231+
client: &FulaClientHandle,
232+
bucket: String,
233+
key: String,
234+
) -> FulaResult<bool> {
235+
Ok(client.inner.object_exists(&bucket, &key).await?)
236+
}
237+
238+
/// Copy an object
239+
pub async fn copy_object(
240+
client: &FulaClientHandle,
241+
src_bucket: String,
242+
src_key: String,
243+
dst_bucket: String,
244+
dst_key: String,
245+
) -> FulaResult<CopyResult> {
246+
let result = client.inner.copy_object(&src_bucket, &src_key, &dst_bucket, &dst_key).await?;
247+
Ok(result.into())
248+
}
249+
250+
/// List objects in a bucket
251+
pub async fn list_objects(
252+
client: &FulaClientHandle,
253+
bucket: String,
254+
options: ListOptions,
255+
) -> FulaResult<ListObjectsResult> {
256+
let result = client.inner.list_objects(&bucket, Some(options.into())).await?;
257+
Ok(result.into())
258+
}

0 commit comments

Comments
 (0)