+8
-15
cli/src/blob_map.rs
+8
-15
cli/src/blob_map.rs
···
34
34
let blob_ref = &file_node.blob;
35
35
let cid_string = blob_ref.blob().r#ref.to_string();
36
36
37
-
// Store both normalized and full paths
38
-
// Normalize by removing base folder prefix (e.g., "cobblemon/index.html" -> "index.html")
39
-
let normalized_path = normalize_path(&full_path);
40
-
37
+
// Store with full path (mirrors TypeScript implementation)
41
38
blob_map.insert(
42
-
normalized_path.clone(),
43
-
(blob_ref.clone().into_static(), cid_string.clone())
39
+
full_path,
40
+
(blob_ref.clone().into_static(), cid_string)
44
41
);
45
-
46
-
// Also store the full path for matching
47
-
if normalized_path != full_path {
48
-
blob_map.insert(
49
-
full_path,
50
-
(blob_ref.clone().into_static(), cid_string)
51
-
);
52
-
}
53
42
}
54
43
EntryNode::Directory(subdir) => {
55
44
let sub_map = extract_blob_map_recursive(subdir, full_path);
···
67
56
/// Normalize file path by removing base folder prefix
68
57
/// Example: "cobblemon/index.html" -> "index.html"
69
58
///
70
-
/// Mirrors TypeScript implementation at src/routes/wisp.ts line 291
59
+
/// Note: This function is kept for reference but is no longer used in production code.
60
+
/// The TypeScript server has a similar normalization (src/routes/wisp.ts line 291) to handle
61
+
/// uploads that include a base folder prefix, but our CLI doesn't need this since we
62
+
/// track full paths consistently.
63
+
#[allow(dead_code)]
71
64
pub fn normalize_path(path: &str) -> String {
72
65
// Remove base folder prefix (everything before first /)
73
66
if let Some(idx) = path.find('/') {
+24
-13
cli/src/main.rs
+24
-13
cli/src/main.rs
···
152
152
};
153
153
154
154
// Build directory tree
155
-
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map).await?;
155
+
let (root_dir, total_files, reused_count) = build_directory(agent, &path, &existing_blob_map, String::new()).await?;
156
156
let uploaded_count = total_files - reused_count;
157
157
158
158
// Create the Fs record
···
182
182
}
183
183
184
184
/// Recursively build a Directory from a filesystem path
185
+
/// current_path is the path from the root of the site (e.g., "" for root, "config" for config dir)
185
186
fn build_directory<'a>(
186
187
agent: &'a Agent<impl jacquard::client::AgentSession + IdentityResolver + 'a>,
187
188
dir_path: &'a Path,
188
189
existing_blobs: &'a HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
190
+
current_path: String,
189
191
) -> std::pin::Pin<Box<dyn std::future::Future<Output = miette::Result<(Directory<'static>, usize, usize)>> + 'a>>
190
192
{
191
193
Box::pin(async move {
···
214
216
let metadata = entry.metadata().into_diagnostic()?;
215
217
216
218
if metadata.is_file() {
217
-
file_tasks.push((name_str, path));
219
+
// Construct full path for this file (for blob map lookup)
220
+
let full_path = if current_path.is_empty() {
221
+
name_str.clone()
222
+
} else {
223
+
format!("{}/{}", current_path, name_str)
224
+
};
225
+
file_tasks.push((name_str, path, full_path));
218
226
} else if metadata.is_dir() {
219
227
dir_tasks.push((name_str, path));
220
228
}
···
222
230
223
231
// Process files concurrently with a limit of 5
224
232
let file_results: Vec<(Entry<'static>, bool)> = stream::iter(file_tasks)
225
-
.map(|(name, path)| async move {
226
-
let (file_node, reused) = process_file(agent, &path, &name, existing_blobs).await?;
233
+
.map(|(name, path, full_path)| async move {
234
+
let (file_node, reused) = process_file(agent, &path, &full_path, existing_blobs).await?;
227
235
let entry = Entry::new()
228
236
.name(CowStr::from(name))
229
237
.node(EntryNode::File(Box::new(file_node)))
···
251
259
// Process directories recursively (sequentially to avoid too much nesting)
252
260
let mut dir_entries = Vec::new();
253
261
for (name, path) in dir_tasks {
254
-
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs).await?;
262
+
// Construct full path for subdirectory
263
+
let subdir_path = if current_path.is_empty() {
264
+
name.clone()
265
+
} else {
266
+
format!("{}/{}", current_path, name)
267
+
};
268
+
let (subdir, sub_total, sub_reused) = build_directory(agent, &path, existing_blobs, subdir_path).await?;
255
269
dir_entries.push(Entry::new()
256
270
.name(CowStr::from(name))
257
271
.node(EntryNode::Directory(Box::new(subdir)))
···
275
289
276
290
/// Process a single file: gzip -> base64 -> upload blob (or reuse existing)
277
291
/// Returns (File, reused: bool)
292
+
/// file_path_key is the full path from the site root (e.g., "config/file.json") for blob map lookup
278
293
async fn process_file(
279
294
agent: &Agent<impl jacquard::client::AgentSession + IdentityResolver>,
280
295
file_path: &Path,
281
-
file_name: &str,
296
+
file_path_key: &str,
282
297
existing_blobs: &HashMap<String, (jacquard_common::types::blob::BlobRef<'static>, String)>,
283
298
) -> miette::Result<(File<'static>, bool)>
284
299
{
···
301
316
// Compute CID for this file (CRITICAL: on base64-encoded gzipped content)
302
317
let file_cid = cid::compute_cid(&base64_bytes);
303
318
304
-
// Normalize the file path for comparison
305
-
let normalized_path = blob_map::normalize_path(file_name);
306
-
307
319
// Check if we have an existing blob with the same CID
308
-
let existing_blob = existing_blobs.get(&normalized_path)
309
-
.or_else(|| existing_blobs.get(file_name));
320
+
let existing_blob = existing_blobs.get(file_path_key);
310
321
311
322
if let Some((existing_blob_ref, existing_cid)) = existing_blob {
312
323
if existing_cid == &file_cid {
313
324
// CIDs match - reuse existing blob
314
-
println!(" ✓ Reusing blob for {} (CID: {})", file_name, file_cid);
325
+
println!(" ✓ Reusing blob for {} (CID: {})", file_path_key, file_cid);
315
326
return Ok((
316
327
File::new()
317
328
.r#type(CowStr::from("file"))
···
326
337
}
327
338
328
339
// File is new or changed - upload it
329
-
println!(" ↑ Uploading {} ({} bytes, CID: {})", file_name, base64_bytes.len(), file_cid);
340
+
println!(" ↑ Uploading {} ({} bytes, CID: {})", file_path_key, base64_bytes.len(), file_cid);
330
341
let blob = agent.upload_blob(
331
342
base64_bytes,
332
343
MimeType::new_static("application/octet-stream"),