···1515use jacquard::oauth::client::OAuthClient;
1616use jacquard::oauth::loopback::LoopbackConfig;
1717use jacquard::prelude::IdentityResolver;
1818-use jacquard_common::types::string::{Datetime, Rkey, RecordKey};
1818+use jacquard_common::types::string::{Datetime, Rkey, RecordKey, AtUri};
1919use jacquard_common::types::blob::MimeType;
2020use miette::IntoDiagnostic;
2121use std::path::{Path, PathBuf};
···356356 println!(" Split #{}: {} ({} files, {:.1}KB)",
357357 attempts, largest_dir.path, largest_dir.file_count, largest_dir.size as f64 / 1024.0);
358358359359- // Create a subfs record for this directory
360360- use jacquard_common::types::string::Tid;
361361- let subfs_tid = Tid::now_0();
362362- let subfs_rkey = subfs_tid.to_string();
359359+ // Check if this directory is itself too large for a single subfs record
360360+ const MAX_SUBFS_SIZE: usize = 75 * 1024; // 75KB soft limit for safety
361361+ let mut subfs_uri = String::new();
362362+363363+ if largest_dir.size > MAX_SUBFS_SIZE {
364364+ // Need to split this directory into multiple chunks
365365+ println!(" → Directory too large, splitting into chunks...");
366366+ let chunks = subfs_utils::split_directory_into_chunks(&largest_dir.directory, MAX_SUBFS_SIZE);
367367+ println!(" → Created {} chunks", chunks.len());
368368+369369+ // Upload each chunk as a subfs record
370370+ let mut chunk_uris = Vec::new();
371371+ for (i, chunk) in chunks.iter().enumerate() {
372372+ use jacquard_common::types::string::Tid;
373373+ let chunk_tid = Tid::now_0();
374374+ let chunk_rkey = chunk_tid.to_string();
375375+376376+ let chunk_file_count = subfs_utils::count_files_in_directory(chunk);
377377+ let chunk_size = subfs_utils::estimate_directory_size(chunk);
363378364364- let subfs_manifest = crate::place_wisp::subfs::SubfsRecord::new()
365365- .root(convert_fs_dir_to_subfs_dir(largest_dir.directory.clone()))
366366- .file_count(Some(largest_dir.file_count as i64))
367367- .created_at(Datetime::now())
368368- .build();
379379+ let chunk_manifest = crate::place_wisp::subfs::SubfsRecord::new()
380380+ .root(convert_fs_dir_to_subfs_dir(chunk.clone()))
381381+ .file_count(Some(chunk_file_count as i64))
382382+ .created_at(Datetime::now())
383383+ .build();
384384+385385+ println!(" → Uploading chunk {}/{} ({} files, {:.1}KB)...",
386386+ i + 1, chunks.len(), chunk_file_count, chunk_size as f64 / 1024.0);
387387+388388+ let chunk_output = agent.put_record(
389389+ RecordKey::from(Rkey::new(&chunk_rkey).into_diagnostic()?),
390390+ chunk_manifest
391391+ ).await.into_diagnostic()?;
369392370370- // Upload subfs record
371371- let subfs_output = agent.put_record(
372372- RecordKey::from(Rkey::new(&subfs_rkey).into_diagnostic()?),
373373- subfs_manifest
374374- ).await.into_diagnostic()?;
393393+ let chunk_uri = chunk_output.uri.to_string();
394394+ chunk_uris.push((chunk_uri.clone(), format!("{}#{}", largest_dir.path, i)));
395395+ new_subfs_uris.push((chunk_uri.clone(), format!("{}#{}", largest_dir.path, i)));
396396+ }
375397376376- let subfs_uri = subfs_output.uri.to_string();
377377- println!(" ✅ Created subfs: {}", subfs_uri);
398398+ // Create a parent subfs record that references all chunks
399399+ // Each chunk reference MUST have flat: true to merge chunk contents
400400+ println!(" → Creating parent subfs with {} chunk references...", chunk_uris.len());
401401+ use jacquard_common::CowStr;
402402+ use crate::place_wisp::fs::{Subfs};
378403379379- // Replace directory with subfs node (flat: false to preserve structure)
404404+ // Convert to fs::Subfs (which has the 'flat' field) instead of subfs::Subfs
405405+ let parent_entries_fs: Vec<Entry> = chunk_uris.iter().enumerate().map(|(i, (uri, _))| {
406406+ let uri_string = uri.clone();
407407+ let at_uri = AtUri::new_cow(CowStr::from(uri_string)).expect("valid URI");
408408+ Entry::new()
409409+ .name(CowStr::from(format!("chunk{}", i)))
410410+ .node(EntryNode::Subfs(Box::new(
411411+ Subfs::new()
412412+ .r#type(CowStr::from("subfs"))
413413+ .subject(at_uri)
414414+ .flat(Some(true)) // EXPLICITLY TRUE - merge chunk contents
415415+ .build()
416416+ )))
417417+ .build()
418418+ }).collect();
419419+420420+ let parent_root_fs = Directory::new()
421421+ .r#type(CowStr::from("directory"))
422422+ .entries(parent_entries_fs)
423423+ .build();
424424+425425+ // Convert to subfs::Directory for the parent subfs record
426426+ let parent_root_subfs = convert_fs_dir_to_subfs_dir(parent_root_fs);
427427+428428+ use jacquard_common::types::string::Tid;
429429+ let parent_tid = Tid::now_0();
430430+ let parent_rkey = parent_tid.to_string();
431431+432432+ let parent_manifest = crate::place_wisp::subfs::SubfsRecord::new()
433433+ .root(parent_root_subfs)
434434+ .file_count(Some(largest_dir.file_count as i64))
435435+ .created_at(Datetime::now())
436436+ .build();
437437+438438+ let parent_output = agent.put_record(
439439+ RecordKey::from(Rkey::new(&parent_rkey).into_diagnostic()?),
440440+ parent_manifest
441441+ ).await.into_diagnostic()?;
442442+443443+ subfs_uri = parent_output.uri.to_string();
444444+ println!(" ✅ Created parent subfs with chunks (flat=true on each chunk): {}", subfs_uri);
445445+ } else {
446446+ // Directory fits in a single subfs record
447447+ use jacquard_common::types::string::Tid;
448448+ let subfs_tid = Tid::now_0();
449449+ let subfs_rkey = subfs_tid.to_string();
450450+451451+ let subfs_manifest = crate::place_wisp::subfs::SubfsRecord::new()
452452+ .root(convert_fs_dir_to_subfs_dir(largest_dir.directory.clone()))
453453+ .file_count(Some(largest_dir.file_count as i64))
454454+ .created_at(Datetime::now())
455455+ .build();
456456+457457+ // Upload subfs record
458458+ let subfs_output = agent.put_record(
459459+ RecordKey::from(Rkey::new(&subfs_rkey).into_diagnostic()?),
460460+ subfs_manifest
461461+ ).await.into_diagnostic()?;
462462+463463+ subfs_uri = subfs_output.uri.to_string();
464464+ println!(" ✅ Created subfs: {}", subfs_uri);
465465+ }
466466+467467+ // Replace directory with subfs node (flat: false to preserve directory structure)
380468 working_directory = subfs_utils::replace_directory_with_subfs(
381469 working_directory,
382470 &largest_dir.path,
383471 &subfs_uri,
384384- false // Preserve directory structure
472472+ false // Preserve directory - the chunks inside have flat=true
385473 )?;
386474387475 new_subfs_uris.push((subfs_uri, largest_dir.path.clone()));
···729817 }
730818731819 return Ok((file_builder.build(), true));
820820+ } else {
821821+ // CID mismatch - file changed
822822+ println!(" → File changed: {} (old CID: {}, new CID: {})", file_path_key, existing_cid, file_cid);
823823+ }
824824+ } else {
825825+ // File not in existing blob map
826826+ if file_path_key.starts_with("imgs/") {
827827+ println!(" → New file (not in blob map): {}", file_path_key);
732828 }
733829 }
734830
+30-32
cli/src/pull.rs
···3535 let pds_url = resolver.pds_for_did(&did).await.into_diagnostic()?;
3636 println!("Resolved PDS: {}", pds_url);
37373838- // Fetch the place.wisp.fs record
3939-3838+ // Create a temporary agent for fetching records (no auth needed for public reads)
4039 println!("Fetching record from PDS...");
4140 let client = reqwest::Client::new();
4242-4141+4342 // Use com.atproto.repo.getRecord
4443 use jacquard::api::com_atproto::repo::get_record::GetRecord;
4544 use jacquard_common::types::string::Rkey as RkeyType;
4645 let rkey_parsed = RkeyType::new(&rkey).into_diagnostic()?;
4747-4646+4847 use jacquard_common::types::ident::AtIdentifier;
4948 use jacquard_common::types::string::RecordKey;
5049 let request = GetRecord::new()
···7069 println!("Found site '{}' with {} files (in main record)", fs_record.site, file_count);
71707271 // Check for and expand subfs nodes
7373- let expanded_root = expand_subfs_in_pull(&fs_record.root, &pds_url, did.as_str()).await?;
7272+ // Note: We use a custom expand function for pull since we don't have an Agent
7373+ let expanded_root = expand_subfs_in_pull_with_client(&fs_record.root, &client, &pds_url).await?;
7474 let total_file_count = subfs_utils::count_files_in_directory(&expanded_root);
75757676 if total_file_count as i64 != fs_record.file_count.unwrap_or(0) {
···402402}
403403404404/// Expand subfs nodes in a directory tree by fetching and merging subfs records (RECURSIVELY)
405405-async fn expand_subfs_in_pull<'a>(
405405+/// Uses reqwest client directly for pull command (no agent needed)
406406+async fn expand_subfs_in_pull_with_client<'a>(
406407 directory: &Directory<'a>,
408408+ client: &reqwest::Client,
407409 pds_url: &Url,
408408- _did: &str,
409410) -> miette::Result<Directory<'static>> {
411411+ use jacquard_common::IntoStatic;
412412+ use jacquard_common::types::value::from_data;
410413 use crate::place_wisp::subfs::SubfsRecord;
411411- use jacquard_common::types::value::from_data;
412412- use jacquard_common::IntoStatic;
413414414414- // Recursively fetch ALL subfs records (including nested ones)
415415 let mut all_subfs_map: HashMap<String, crate::place_wisp::subfs::Directory> = HashMap::new();
416416 let mut to_fetch = subfs_utils::extract_subfs_uris(directory, String::new());
417417···420420 }
421421422422 println!("Found {} subfs records, fetching recursively...", to_fetch.len());
423423- let client = reqwest::Client::new();
424423425425- // Keep fetching until we've resolved all subfs (including nested ones)
426424 let mut iteration = 0;
427427- const MAX_ITERATIONS: usize = 10; // Prevent infinite loops
425425+ const MAX_ITERATIONS: usize = 10;
428426429427 while !to_fetch.is_empty() && iteration < MAX_ITERATIONS {
430428 iteration += 1;
···437435 let pds_url = pds_url.clone();
438436439437 fetch_tasks.push(async move {
438438+ // Parse URI
440439 let parts: Vec<&str> = uri.trim_start_matches("at://").split('/').collect();
441440 if parts.len() < 3 {
442441 return Err(miette::miette!("Invalid subfs URI: {}", uri));
443442 }
444443445445- let _did = parts[0];
444444+ let did_str = parts[0];
446445 let collection = parts[1];
447447- let rkey = parts[2];
446446+ let rkey_str = parts[2];
448447449448 if collection != "place.wisp.subfs" {
450449 return Err(miette::miette!("Expected place.wisp.subfs collection, got: {}", collection));
451450 }
452451452452+ // Fetch using GetRecord
453453 use jacquard::api::com_atproto::repo::get_record::GetRecord;
454454- use jacquard_common::types::string::Rkey as RkeyType;
454454+ use jacquard_common::types::string::{Rkey as RkeyType, Did as DidType, RecordKey};
455455 use jacquard_common::types::ident::AtIdentifier;
456456- use jacquard_common::types::string::{RecordKey, Did as DidType};
457456458458- let rkey_parsed = RkeyType::new(rkey).into_diagnostic()?;
459459- let did_parsed = DidType::new(_did).into_diagnostic()?;
457457+ let rkey_parsed = RkeyType::new(rkey_str).into_diagnostic()?;
458458+ let did_parsed = DidType::new(did_str).into_diagnostic()?;
460459461460 let request = GetRecord::new()
462461 .repo(AtIdentifier::Did(did_parsed))
···472471473472 let record_output = response.into_output().into_diagnostic()?;
474473 let subfs_record: SubfsRecord = from_data(&record_output.value).into_diagnostic()?;
475475- let subfs_record_static = subfs_record.into_static();
476474477477- Ok::<_, miette::Report>((path, subfs_record_static))
475475+ Ok::<_, miette::Report>((path, subfs_record.into_static()))
478476 });
479477 }
480478481479 let results: Vec<_> = futures::future::join_all(fetch_tasks).await;
482480483481 // Process results and find nested subfs
484484- let mut newly_fetched = Vec::new();
482482+ let mut newly_found_uris = Vec::new();
485483 for result in results {
486484 match result {
487485 Ok((path, record)) => {
488486 println!(" ✓ Fetched subfs at {}", path);
489487490490- // Check for nested subfs in this record
491491- let nested_subfs = extract_subfs_from_subfs_dir(&record.root, path.clone());
492492- newly_fetched.extend(nested_subfs);
488488+ // Extract nested subfs URIs
489489+ let nested_uris = extract_subfs_uris_from_subfs_dir(&record.root, path.clone());
490490+ newly_found_uris.extend(nested_uris);
493491494492 all_subfs_map.insert(path, record.root);
495493 }
···499497 }
500498 }
501499502502- // Update to_fetch with only the NEW subfs we haven't fetched yet
503503- to_fetch = newly_fetched
500500+ // Filter out already-fetched paths
501501+ to_fetch = newly_found_uris
504502 .into_iter()
505505- .filter(|(uri, _)| !all_subfs_map.iter().any(|(k, _)| k == uri))
503503+ .filter(|(_, path)| !all_subfs_map.contains_key(path))
506504 .collect();
507505 }
508506509507 if iteration >= MAX_ITERATIONS {
510510- return Err(miette::miette!("Max iterations reached while fetching nested subfs"));
508508+ eprintln!("⚠️ Max iterations reached while fetching nested subfs");
511509 }
512510513511 println!(" Total subfs records fetched: {}", all_subfs_map.len());
···516514 Ok(replace_subfs_with_content(directory.clone(), &all_subfs_map, String::new()))
517515}
518516519519-/// Extract subfs URIs from a subfs::Directory
520520-fn extract_subfs_from_subfs_dir(
517517+/// Extract subfs URIs from a subfs::Directory (helper for pull)
518518+fn extract_subfs_uris_from_subfs_dir(
521519 directory: &crate::place_wisp::subfs::Directory,
522520 current_path: String,
523521) -> Vec<(String, String)> {
···535533 uris.push((subfs_node.subject.to_string(), full_path.clone()));
536534 }
537535 crate::place_wisp::subfs::EntryNode::Directory(subdir) => {
538538- let nested = extract_subfs_from_subfs_dir(subdir, full_path);
536536+ let nested = extract_subfs_uris_from_subfs_dir(subdir, full_path);
539537 uris.extend(nested);
540538 }
541539 _ => {}
+195-34
cli/src/subfs_utils.rs
···7272 Ok(record_output.value.into_static())
7373}
74747575-/// Merge blob maps from subfs records into the main blob map
7676-/// Returns the total number of blobs merged from all subfs records
7777-pub async fn merge_subfs_blob_maps(
7575+/// Recursively fetch all subfs records (including nested ones)
7676+/// Returns a list of (mount_path, SubfsRecord) tuples
7777+/// Note: Multiple records can have the same mount_path (for flat-merged chunks)
7878+pub async fn fetch_all_subfs_records_recursive(
7879 agent: &Agent<impl AgentSession + IdentityResolver>,
7979- subfs_uris: Vec<(String, String)>,
8080- main_blob_map: &mut HashMap<String, (BlobRef<'static>, String)>,
8181-) -> miette::Result<usize> {
8282- let mut total_merged = 0;
8080+ initial_uris: Vec<(String, String)>,
8181+) -> miette::Result<Vec<(String, SubfsRecord<'static>)>> {
8282+ use futures::stream::{self, StreamExt};
83838484- println!("Fetching {} subfs records for blob reuse...", subfs_uris.len());
8484+ let mut all_subfs: Vec<(String, SubfsRecord<'static>)> = Vec::new();
8585+ let mut fetched_uris: std::collections::HashSet<String> = std::collections::HashSet::new();
8686+ let mut to_fetch = initial_uris;
85878686- // Fetch all subfs records in parallel (but with some concurrency limit)
8787- use futures::stream::{self, StreamExt};
8888+ if to_fetch.is_empty() {
8989+ return Ok(all_subfs);
9090+ }
9191+9292+ println!("Found {} subfs records, fetching recursively...", to_fetch.len());
9393+9494+ let mut iteration = 0;
9595+ const MAX_ITERATIONS: usize = 10;
88968989- let subfs_results: Vec<_> = stream::iter(subfs_uris)
9090- .map(|(uri, mount_path)| async move {
9191- match fetch_subfs_record(agent, &uri).await {
9292- Ok(record) => Some((record, mount_path)),
9393- Err(e) => {
9494- eprintln!(" ⚠️ Failed to fetch subfs {}: {}", uri, e);
9595- None
9797+ while !to_fetch.is_empty() && iteration < MAX_ITERATIONS {
9898+ iteration += 1;
9999+ println!(" Iteration {}: fetching {} subfs records...", iteration, to_fetch.len());
100100+101101+ let subfs_results: Vec<_> = stream::iter(to_fetch.clone())
102102+ .map(|(uri, mount_path)| async move {
103103+ match fetch_subfs_record(agent, &uri).await {
104104+ Ok(record) => Some((mount_path, record, uri)),
105105+ Err(e) => {
106106+ eprintln!(" ⚠️ Failed to fetch subfs {}: {}", uri, e);
107107+ None
108108+ }
96109 }
110110+ })
111111+ .buffer_unordered(5)
112112+ .collect()
113113+ .await;
114114+115115+ // Process results and find nested subfs
116116+ let mut newly_found_uris = Vec::new();
117117+ for result in subfs_results {
118118+ if let Some((mount_path, record, uri)) = result {
119119+ println!(" ✓ Fetched subfs at {}", mount_path);
120120+121121+ // Extract nested subfs URIs from this record
122122+ let nested_uris = extract_subfs_uris_from_subfs_dir(&record.root, mount_path.clone());
123123+ newly_found_uris.extend(nested_uris);
124124+125125+ all_subfs.push((mount_path, record));
126126+ fetched_uris.insert(uri);
97127 }
9898- })
9999- .buffer_unordered(5)
100100- .collect()
101101- .await;
128128+ }
102129103103- // Convert subfs Directory to fs Directory for blob extraction
104104- // Note: We need to extract blobs from the subfs record's root
105105- for result in subfs_results {
106106- if let Some((subfs_record, mount_path)) = result {
107107- // Extract blobs from this subfs record's root
108108- // The blob_map module works with fs::Directory, but subfs::Directory has the same structure
109109- // We need to convert or work directly with the entries
130130+ // Filter out already-fetched URIs (based on URI, not path)
131131+ to_fetch = newly_found_uris
132132+ .into_iter()
133133+ .filter(|(uri, _)| !fetched_uris.contains(uri))
134134+ .collect();
135135+ }
110136111111- let subfs_blob_map = extract_subfs_blobs(&subfs_record.root, mount_path.clone());
112112- let count = subfs_blob_map.len();
137137+ if iteration >= MAX_ITERATIONS {
138138+ eprintln!("⚠️ Max iterations reached while fetching nested subfs");
139139+ }
113140114114- for (path, blob_info) in subfs_blob_map {
115115- main_blob_map.insert(path, blob_info);
141141+ println!(" Total subfs records fetched: {}", all_subfs.len());
142142+143143+ Ok(all_subfs)
144144+}
145145+146146+/// Extract subfs URIs from a subfs::Directory
147147+fn extract_subfs_uris_from_subfs_dir(
148148+ directory: &crate::place_wisp::subfs::Directory,
149149+ current_path: String,
150150+) -> Vec<(String, String)> {
151151+ let mut uris = Vec::new();
152152+153153+ for entry in &directory.entries {
154154+ match &entry.node {
155155+ crate::place_wisp::subfs::EntryNode::Subfs(subfs_node) => {
156156+ // Check if this is a chunk entry (chunk0, chunk1, etc.)
157157+ // Chunks should be flat-merged, so use the parent's path
158158+ let mount_path = if entry.name.starts_with("chunk") &&
159159+ entry.name.chars().skip(5).all(|c| c.is_ascii_digit()) {
160160+ // This is a chunk - use parent's path for flat merge
161161+ println!(" → Found chunk {} at {}, will flat-merge to {}", entry.name, current_path, current_path);
162162+ current_path.clone()
163163+ } else {
164164+ // Normal subfs - append name to path
165165+ if current_path.is_empty() {
166166+ entry.name.to_string()
167167+ } else {
168168+ format!("{}/{}", current_path, entry.name)
169169+ }
170170+ };
171171+172172+ uris.push((subfs_node.subject.to_string(), mount_path));
116173 }
174174+ crate::place_wisp::subfs::EntryNode::Directory(subdir) => {
175175+ let full_path = if current_path.is_empty() {
176176+ entry.name.to_string()
177177+ } else {
178178+ format!("{}/{}", current_path, entry.name)
179179+ };
180180+ let nested = extract_subfs_uris_from_subfs_dir(subdir, full_path);
181181+ uris.extend(nested);
182182+ }
183183+ _ => {}
184184+ }
185185+ }
117186118118- total_merged += count;
119119- println!(" ✓ Merged {} blobs from subfs at {}", count, mount_path);
187187+ uris
188188+}
189189+190190+/// Merge blob maps from subfs records into the main blob map (RECURSIVE)
191191+/// Returns the total number of blobs merged from all subfs records
192192+pub async fn merge_subfs_blob_maps(
193193+ agent: &Agent<impl AgentSession + IdentityResolver>,
194194+ subfs_uris: Vec<(String, String)>,
195195+ main_blob_map: &mut HashMap<String, (BlobRef<'static>, String)>,
196196+) -> miette::Result<usize> {
197197+ // Fetch all subfs records recursively
198198+ let all_subfs = fetch_all_subfs_records_recursive(agent, subfs_uris).await?;
199199+200200+ let mut total_merged = 0;
201201+202202+ // Extract blobs from all fetched subfs records
203203+ // Skip parent records that only contain chunk references (no actual files)
204204+ for (mount_path, subfs_record) in all_subfs {
205205+ // Check if this record only contains chunk subfs references (no files)
206206+ let only_has_chunks = subfs_record.root.entries.iter().all(|e| {
207207+ matches!(&e.node, crate::place_wisp::subfs::EntryNode::Subfs(_)) &&
208208+ e.name.starts_with("chunk") &&
209209+ e.name.chars().skip(5).all(|c| c.is_ascii_digit())
210210+ });
211211+212212+ if only_has_chunks && !subfs_record.root.entries.is_empty() {
213213+ // This is a parent containing only chunks - skip it, blobs are in the chunks
214214+ println!(" → Skipping parent subfs at {} ({} chunks, no files)", mount_path, subfs_record.root.entries.len());
215215+ continue;
216216+ }
217217+218218+ let subfs_blob_map = extract_subfs_blobs(&subfs_record.root, mount_path.clone());
219219+ let count = subfs_blob_map.len();
220220+221221+ for (path, blob_info) in subfs_blob_map {
222222+ main_blob_map.insert(path, blob_info);
120223 }
224224+225225+ total_merged += count;
226226+ println!(" ✓ Merged {} blobs from subfs at {}", count, mount_path);
121227 }
122228123229 Ok(total_merged)
···334440335441 Ok(())
336442}
443443+444444+/// Split a large directory into multiple smaller chunks
445445+/// Returns a list of chunk directories, each small enough to fit in a subfs record
446446+pub fn split_directory_into_chunks(
447447+ directory: &FsDirectory,
448448+ max_size: usize,
449449+) -> Vec<FsDirectory<'static>> {
450450+ use jacquard_common::CowStr;
451451+452452+ let mut chunks = Vec::new();
453453+ let mut current_chunk_entries = Vec::new();
454454+ let mut current_chunk_size = 100; // Base size for directory structure
455455+456456+ for entry in &directory.entries {
457457+ // Estimate the size of this entry
458458+ let entry_size = estimate_entry_size(entry);
459459+460460+ // If adding this entry would exceed the max size, start a new chunk
461461+ if !current_chunk_entries.is_empty() && (current_chunk_size + entry_size > max_size) {
462462+ // Create a chunk from current entries
463463+ let chunk = FsDirectory::new()
464464+ .r#type(CowStr::from("directory"))
465465+ .entries(current_chunk_entries.clone())
466466+ .build();
467467+468468+ chunks.push(chunk);
469469+470470+ // Start new chunk
471471+ current_chunk_entries.clear();
472472+ current_chunk_size = 100;
473473+ }
474474+475475+ current_chunk_entries.push(entry.clone().into_static());
476476+ current_chunk_size += entry_size;
477477+ }
478478+479479+ // Add the last chunk if it has any entries
480480+ if !current_chunk_entries.is_empty() {
481481+ let chunk = FsDirectory::new()
482482+ .r#type(CowStr::from("directory"))
483483+ .entries(current_chunk_entries)
484484+ .build();
485485+ chunks.push(chunk);
486486+ }
487487+488488+ chunks
489489+}
490490+491491+/// Estimate the JSON size of a single entry
492492+fn estimate_entry_size(entry: &crate::place_wisp::fs::Entry) -> usize {
493493+ match serde_json::to_string(entry) {
494494+ Ok(json) => json.len(),
495495+ Err(_) => 500, // Conservative estimate if serialization fails
496496+ }
497497+}