Skip to content

Commit f5f7677

Browse files
committed
Added offline download feature
| # | Step | Repo / files (primary) | Effort | Depends on | |---|---|---|---|---| | 0 | Verify pinning chain | (op-run; done) | done | — | | 1.2 | Add `bucket_lookup_h` field + SDK header + master populate-if-missing | `fula-api/crates/fula-core/src/metadata.rs`, `fula-cli/src/handlers/object.rs`, `fula-client/src/encryption.rs:3243`, new `fula-crypto` HKDF helper | ~100 LOC, 2-3 days | Step 0 | | 2.1 | Master-down detection (health gate) | `fula-client/src/encryption.rs` GET, `fula-cli/src/client.rs:319-371` | ~150 LOC, 2-3 days | independent of 1.2 | | 2.2 | Local block cache (redb LRU) | new `fula-client/src/block_cache.rs` | ~200 LOC, 3-4 days | — | | 2.3 | Multi-gateway race + dynamic priority + CID verification | new `fula-client/src/gateway_fetch.rs` | ~300 LOC, 4-5 days | 2.2 | | 2.4 | Wire warm-device offline GET | `fula-client/src/encryption.rs` GET, glue 2.1+2.2+2.3 | ~150 LOC, 2-3 days | 2.1, 2.2, 2.3 |
1 parent aa813b1 commit f5f7677

17 files changed

Lines changed: 4344 additions & 10 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ lru = "0.12"
169169
semver = "1.0"
170170
bitvec = "1.0"
171171

172+
# Embedded persistent KV (block cache, Phase 2.2 of master-independent reads).
173+
# Pinned to 2.6.x to avoid silent file-format drift in routine cargo update.
174+
# A 2.x bump is a deliberate decision (verify file-format compatibility before
175+
# upgrading; cache files in production may need migration handling).
176+
redb = "~2.6"
177+
172178
# Testing
173179
criterion = "0.5"
174180
proptest = "1.5"

crates/fula-cli/src/handlers/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub mod multipart;
88
pub mod object;
99
pub mod service;
1010
pub mod tagging;
11+
pub mod users_index_publisher;
1112

1213
pub use admin::*;
1314
pub use batch::*;

crates/fula-cli/src/handlers/object.rs

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,16 @@ pub async fn put_object(
130130
metadata = metadata.with_content_type(ct);
131131
}
132132

133-
// Extract user metadata (x-amz-meta-*)
133+
// Extract user metadata (x-amz-meta-*).
134+
// Internal Fula control headers (consumed by the handler, not stored as
135+
// object metadata) are filtered out — they would otherwise pollute every
136+
// object's persisted metadata.
137+
const FULA_CONTROL_HEADERS: &[&str] = &["fula-bucket-lookup-h"];
134138
for (name, value) in headers.iter() {
135139
if let Some(key) = name.as_str().strip_prefix("x-amz-meta-") {
140+
if FULA_CONTROL_HEADERS.contains(&key) {
141+
continue;
142+
}
136143
if let Ok(v) = value.to_str() {
137144
metadata = metadata.with_user_metadata(key, v);
138145
}
@@ -145,14 +152,72 @@ pub async fn put_object(
145152
tracing::error!(error = %e, key = %key, "Failed to put object");
146153
e
147154
})?;
148-
155+
149156
tracing::debug!("Flushing bucket");
150157
let bucket_root_cid = bucket.flush().await
151158
.map_err(|e| {
152159
tracing::error!(error = %e, "Failed to flush bucket");
153160
e
154161
})?;
155162

163+
// Phase 1.2 of master-independent reads: if the SDK included
164+
// `x-amz-meta-fula-bucket-lookup-h` (only set on the Phase 2 manifest
165+
// root PUT in `save_sharded_hamt_forest`), populate the bucket-level
166+
// `bucket_lookup_h` field if currently None. Idempotent — never
167+
// overwrites. Gated by env so we can stage the rollout: SDK always
168+
// sends the header (cheap); master only consumes it when ready.
169+
//
170+
// Failures are non-fatal — bad/missing headers must not break uploads.
171+
// Placement: AFTER bucket.flush() (so the flush has already replaced
172+
// the DashMap entry) and BEFORE persist_registry_with_token (so the
173+
// updated field gets serialized into the registry CBOR on this same
174+
// request, no extra IPFS write).
175+
let buckets_index_enabled = std::env::var("FULA_BUCKET_LOOKUP_H_ENABLED")
176+
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
177+
.unwrap_or(false);
178+
if buckets_index_enabled {
179+
if let Some(hex_str) = headers
180+
.get("x-amz-meta-fula-bucket-lookup-h")
181+
.and_then(|v| v.to_str().ok())
182+
{
183+
match hex::decode(hex_str) {
184+
Ok(bytes) if bytes.len() == 16 => {
185+
let mut lookup_h = [0u8; 16];
186+
lookup_h.copy_from_slice(&bytes);
187+
match state.bucket_manager.populate_lookup_h_if_missing(
188+
&session.hashed_user_id,
189+
&bucket_name,
190+
lookup_h,
191+
) {
192+
Ok(true) => tracing::debug!(
193+
bucket = %bucket_name,
194+
"Populated bucket_lookup_h (Phase 1.2)"
195+
),
196+
Ok(false) => { /* already set; idempotent skip */ }
197+
// BucketNotFound on a successful PUT to a real bucket
198+
// is an internal-consistency violation — promote to
199+
// error level so operators notice the signal.
200+
Err(e) => tracing::error!(
201+
error = %e,
202+
bucket = %bucket_name,
203+
user = %session.hashed_user_id,
204+
"populate_lookup_h_if_missing failed on a bucket that just accepted a PUT"
205+
),
206+
}
207+
}
208+
Ok(other) => tracing::warn!(
209+
actual_len = other.len(),
210+
"x-amz-meta-fula-bucket-lookup-h: expected 16-byte hex (32 chars), got {} bytes",
211+
other.len()
212+
),
213+
Err(e) => tracing::warn!(
214+
error = %e,
215+
"Failed to hex-decode x-amz-meta-fula-bucket-lookup-h"
216+
),
217+
}
218+
}
219+
}
220+
156221
// Persist the bucket registry so the new root CID survives restarts.
157222
// This MUST succeed — otherwise the new tree root is lost on restart.
158223
// Use the user's JWT for pinning service authentication.

0 commit comments

Comments
 (0)