From 0f5872b1e04140c90d2ecd103f6470665920a40b Mon Sep 17 00:00:00 2001 From: Christian Stefanescu Date: Thu, 5 Mar 2026 16:03:01 +0100 Subject: [PATCH 1/2] New export format: ftm stub entities --- Cargo.lock | 180 ++++++++++++++++++++++++++++++- Cargo.toml | 4 + src/main.rs | 298 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 465 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7611cb..6811a16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d13a61f2963b88eef9c1be03df65d42f6996dfeac1054870d950fcf66686f83" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d314cc62af2b6b0c65780555abb4d02a03dd3b799cd42419044f0c38d99738c0" +dependencies = [ + "darling", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -363,6 +388,40 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "deflate64" version = "0.1.11" @@ -483,6 +542,23 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "ftm-types" +version = "0.1.0" +dependencies = [ + "anyhow", + "bon", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "serde_json", + "serde_yaml", + "syn", + "thiserror 1.0.69", + "zstd", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -528,6 +604,12 @@ dependencies = [ "digest", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "indexmap" version = "2.13.0" @@ -584,6 +666,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "jobserver" version = "0.1.34" @@ -767,6 +855,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.105" @@ -839,6 +937,22 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -859,6 +973,32 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sevenz-rust" version = "0.6.1" @@ -927,14 +1067,18 @@ name = "sumdir" version = "0.3.0" dependencies = [ "anyhow", + "chrono", "clap", "flate2", + "ftm-types", "indicatif", "infer", "itertools", "jwalk", "rayon", + "serde_json", "sevenz-rust", + "sha1", "tar", "zip", ] @@ -961,13 +1105,33 @@ dependencies = [ "xattr", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1029,6 +1193,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "utf8parse" version = "0.2.2" @@ -1279,7 +1449,7 @@ dependencies = [ "memchr", "pbkdf2", "sha1", - "thiserror", + "thiserror 2.0.18", "time", "xz2", "zeroize", @@ -1287,6 +1457,12 @@ dependencies = [ "zstd", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zopfli" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index aace93a..3cad080 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,18 @@ description = "summarize a directory by file type frequency" [dependencies] anyhow = "1.0" +chrono = { version = "0.4", default-features = false, features = ["std"] } clap = { version = "4.5", features = ["derive"] } flate2 = "1" +ftm-types = { path = "../ftm-types", features = ["builder"] } indicatif = "0.17" infer = "0.19" itertools = "0.14.0" jwalk = "0.8" rayon = "1" +serde_json = "1.0" sevenz-rust = "0.6" +sha1 = "0.10" tar = "0.4" zip = "2" diff --git a/src/main.rs b/src/main.rs index 8371326..29b0545 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ use flate2::read::GzDecoder; +use ftm_types::generated::ftm_entity::FtmEntity; use jwalk::WalkDir; use rayon::prelude::*; +use sha1::{Digest, Sha1}; use std::fs::File; use std::io::Read; use std::path::Path; @@ -8,6 +10,7 @@ use std::{collections::BTreeMap, path::PathBuf}; use tar::Archive as TarArchive; use anyhow::{Context, Result}; +use chrono::{DateTime, Utc}; use clap::Parser; use indicatif::{HumanBytes, ProgressBar, ProgressStyle}; use itertools::Itertools; @@ -18,6 +21,7 @@ enum OutputFormat { Text, Csv, Json, + Ftm, } #[derive(Parser)] @@ -45,6 +49,23 @@ struct ScanError { message: String, } +#[derive(Debug, Clone)] +struct ScanConfig { + collect_file_entries: bool, + progress_bar: bool, +} + +#[derive(Debug, Clone)] +struct FileEntry { + path: PathBuf, + mime_type: String, + size: u64, + created_at: Option, + modified_at: Option, + content_hash: Option, + is_dir: bool, +} + #[derive(Debug, Default)] struct Report { extensions: BTreeMap, @@ -52,6 +73,7 @@ struct Report { folders: Vec, size: u64, errors: Vec, + file_entries: Vec, } impl Report { @@ -65,6 +87,7 @@ impl Report { OutputFormat::Text => self.display_text(data), OutputFormat::Csv => self.display_csv(data, use_mime), OutputFormat::Json => self.display_json(data, use_mime), + OutputFormat::Ftm => self.display_ftm(), } } @@ -124,6 +147,18 @@ impl Report { println!(" ]"); println!("}}"); } + + fn display_ftm(&self) { + for entry in &self.file_entries { + match file_entry_to_ftm_entity(entry) { + Ok(entity) => match serde_json::to_string(&entity) { + Ok(json) => println!("{json}"), + Err(e) => eprintln!("error: failed to serialize entity: {e}"), + }, + Err(e) => eprintln!("error: failed to convert entry {:?}: {e}", entry.path), + } + } + } } fn detect_mimetype(path: &std::path::Path) -> Result { @@ -138,6 +173,81 @@ fn detect_mimetype(path: &std::path::Path) -> Result { Ok("application/octet-stream".to_string()) } +fn compute_sha1(path: &Path) -> Result { + let data = std::fs::read(path) + .with_context(|| format!("failed to read {:?} for SHA1 computation", path))?; + let mut hasher = Sha1::new(); + hasher.update(&data); + Ok(format!("{:x}", hasher.finalize())) +} + +fn system_time_to_iso8601(t: std::time::SystemTime) -> String { + let dt: DateTime = t.into(); + dt.to_rfc3339() +} + +fn mime_to_ftm_schema(mime: &str) -> &'static str { + if mime.starts_with("image/") { + "Image" + } else if mime.starts_with("audio/") { + "Audio" + } else if mime.starts_with("video/") { + "Video" + } else { + match mime { + "application/pdf" => "Pages", + "application/zip" + | "application/x-tar" + | "application/gzip" + | "application/x-7z-compressed" => "Package", + "message/rfc822" => "Email", + "text/html" => "HyperText", + "text/plain" => "PlainText", + _ => "Document", + } + } +} + +fn file_entry_to_ftm_entity(entry: &FileEntry) -> Result { + let schema = if entry.is_dir { + "Folder" + } else { + mime_to_ftm_schema(&entry.mime_type) + }; + + let filename = entry + .path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); + + let mut obj = serde_json::json!({ + "id": entry.path.to_string_lossy().as_ref(), + "schema": schema, + "fileName": [&filename], + "name": [&filename], + }); + + if !entry.is_dir { + obj["mimeType"] = serde_json::json!([&entry.mime_type]); + obj["fileSize"] = serde_json::json!([entry.size as f64]); + } + + if let Some(ref hash) = entry.content_hash { + obj["contentHash"] = serde_json::json!([hash]); + } + if let Some(ref t) = entry.created_at { + obj["createdAt"] = serde_json::json!([t]); + } + if let Some(ref t) = entry.modified_at { + obj["modifiedAt"] = serde_json::json!([t]); + } + + let json_str = serde_json::to_string(&obj).context("failed to serialize entity to JSON")?; + FtmEntity::from_ftm_json(&json_str) + .map_err(|e| anyhow::anyhow!("failed to parse FTM entity: {e}")) +} + fn is_archive_extension(ext: &str) -> bool { matches!(ext, "zip" | "tar" | "gz" | "tgz" | "7z") } @@ -294,7 +404,7 @@ fn scan_archive_contents(path: &Path, report: &mut Report) -> Result<()> { } } -fn process_entry(path: &std::path::Path, report: &mut Report) -> Result<()> { +fn process_entry(path: &std::path::Path, report: &mut Report, config: &ScanConfig) -> Result<()> { let ext = path .extension() .unwrap_or_default() @@ -318,10 +428,25 @@ fn process_entry(path: &std::path::Path, report: &mut Report) -> Result<()> { report .mimetypes - .entry(mimetype) + .entry(mimetype.clone()) .and_modify(|e| *e += 1) .or_insert(1); + if config.collect_file_entries { + let content_hash = compute_sha1(path).ok(); + let created_at = metadata.created().ok().map(system_time_to_iso8601); + let modified_at = metadata.modified().ok().map(system_time_to_iso8601); + report.file_entries.push(FileEntry { + path: path.to_path_buf(), + mime_type: mimetype, + size: metadata.len(), + created_at, + modified_at, + content_hash, + is_dir: false, + }); + } + if is_archive_extension(&ext) { scan_archive_contents(path, report) .with_context(|| format!("failed to scan archive contents of {:?}", path))?; @@ -346,6 +471,7 @@ fn merge_reports(mut a: Report, b: Report) -> Report { a.folders.extend(b.folders); a.size += b.size; a.errors.extend(b.errors); + a.file_entries.extend(b.file_entries); a } @@ -363,12 +489,12 @@ fn make_progress_bar(enabled: bool) -> Option { Some(progress) } -fn scan(target: PathBuf, progress_bar: bool) -> Report { - let pb = make_progress_bar(progress_bar); +fn scan(target: PathBuf, config: &ScanConfig) -> Report { + let pb = make_progress_bar(config.progress_bar); if target.is_file() { let mut report = Report::default(); - if let Err(e) = process_entry(&target, &mut report) { + if let Err(e) = process_entry(&target, &mut report, config) { report.errors.push(ScanError { path: target, message: e.to_string(), @@ -381,6 +507,7 @@ fn scan(target: PathBuf, progress_bar: bool) -> Report { } let pb_fold = pb.clone(); + let config_clone = config.clone(); let report = WalkDir::new(target) .into_iter() .skip(1) @@ -397,12 +524,23 @@ fn scan(target: PathBuf, progress_bar: bool) -> Report { return partial; } if entry.file_type().is_dir() { + if config_clone.collect_file_entries { + partial.file_entries.push(FileEntry { + path: path.clone(), + mime_type: "application/x-directory".to_string(), + size: 0, + created_at: None, + modified_at: None, + content_hash: None, + is_dir: true, + }); + } partial.folders.push(path); } else { if let Some(ref pb) = pb_fold { pb.tick(); } - if let Err(e) = process_entry(&path, &mut partial) { + if let Err(e) = process_entry(&path, &mut partial, &config_clone) { partial.errors.push(ScanError { path, message: e.to_string(), @@ -438,7 +576,11 @@ fn main() { ); std::process::exit(1); } - let report = scan(cli.target, cli.progress_bar); + let config = ScanConfig { + collect_file_entries: matches!(cli.output, OutputFormat::Ftm), + progress_bar: cli.progress_bar, + }; + let report = scan(cli.target, &config); report.display(&cli.output, cli.mime); } @@ -446,9 +588,19 @@ fn main() { mod tests { use super::*; + fn scan_default(target: PathBuf) -> Report { + scan( + target, + &ScanConfig { + collect_file_entries: false, + progress_bar: false, + }, + ) + } + #[test] fn test_with_testdata_folder() { - let report = scan("testdata".into(), false); + let report = scan_default("testdata".into()); let num_files: i32 = report.extensions.values().sum(); // 27 top-level files + 4 inner files (one archived.txt per archive) assert_eq!(num_files, 31); @@ -465,7 +617,7 @@ mod tests { #[test] fn test_scan_zip_archive() { - let report = scan("testdata/archives/sample.zip".into(), false); + let report = scan_default("testdata/archives/sample.zip".into()); let num_files: i32 = report.extensions.values().sum(); assert_eq!(num_files, 2); // 1 zip + 1 txt inside assert_eq!(report.extensions.get("zip"), Some(&1)); @@ -475,7 +627,7 @@ mod tests { #[test] fn test_scan_tar_archive() { - let report = scan("testdata/archives/sample.tar".into(), false); + let report = scan_default("testdata/archives/sample.tar".into()); let num_files: i32 = report.extensions.values().sum(); assert_eq!(num_files, 2); // 1 tar + 1 txt inside assert_eq!(report.extensions.get("tar"), Some(&1)); @@ -485,7 +637,7 @@ mod tests { #[test] fn test_scan_gz_archive() { - let report = scan("testdata/archives/sample.gz".into(), false); + let report = scan_default("testdata/archives/sample.gz".into()); let num_files: i32 = report.extensions.values().sum(); assert_eq!(num_files, 2); // 1 gz + 1 txt inside assert_eq!(report.extensions.get("gz"), Some(&1)); @@ -495,7 +647,7 @@ mod tests { #[test] fn test_scan_7z_archive() { - let report = scan("testdata/archives/sample.7z".into(), false); + let report = scan_default("testdata/archives/sample.7z".into()); let num_files: i32 = report.extensions.values().sum(); assert_eq!(num_files, 2); // 1 7z + 1 txt inside assert_eq!(report.extensions.get("7z"), Some(&1)); @@ -588,7 +740,7 @@ mod tests { .write_all(b"Hello") .expect("failed to write txt"); - let report = scan(dir.clone(), false); + let report = scan_default(dir.clone()); assert_eq!(report.mimetypes.get("image/png"), Some(&1)); assert_eq!(report.mimetypes.get("application/pdf"), Some(&1)); @@ -601,7 +753,7 @@ mod tests { #[test] fn test_testdata_mimetypes() { - let report = scan("testdata".into(), false); + let report = scan_default("testdata".into()); // Verify various MIME types are detected correctly assert_eq!(report.mimetypes.get("image/png"), Some(&1)); assert_eq!(report.mimetypes.get("image/jpeg"), Some(&1)); @@ -645,7 +797,7 @@ mod tests { let readable_file = dir.join("readable.txt"); std::fs::write(&readable_file, "hello").expect("failed to write readable file"); - let report = scan(dir.clone(), false); + let report = scan_default(dir.clone()); // Should have scanned the readable file assert_eq!(report.extensions.get("txt"), Some(&1)); @@ -697,4 +849,120 @@ mod tests { assert_eq!(report.errors[0].path, PathBuf::from("/path/to/file1.txt")); assert_eq!(report.errors[1].path, PathBuf::from("/path/to/file2.txt")); } + + #[test] + fn test_mime_to_ftm_schema() { + assert_eq!(mime_to_ftm_schema("image/png"), "Image"); + assert_eq!(mime_to_ftm_schema("image/jpeg"), "Image"); + assert_eq!(mime_to_ftm_schema("audio/mpeg"), "Audio"); + assert_eq!(mime_to_ftm_schema("audio/ogg"), "Audio"); + assert_eq!(mime_to_ftm_schema("video/mp4"), "Video"); + assert_eq!(mime_to_ftm_schema("video/quicktime"), "Video"); + assert_eq!(mime_to_ftm_schema("application/pdf"), "Pages"); + assert_eq!(mime_to_ftm_schema("application/zip"), "Package"); + assert_eq!(mime_to_ftm_schema("application/x-tar"), "Package"); + assert_eq!(mime_to_ftm_schema("message/rfc822"), "Email"); + assert_eq!(mime_to_ftm_schema("text/html"), "HyperText"); + assert_eq!(mime_to_ftm_schema("text/plain"), "PlainText"); + assert_eq!(mime_to_ftm_schema("application/octet-stream"), "Document"); + assert_eq!(mime_to_ftm_schema("application/x-ole-storage"), "Document"); + } + + #[test] + fn test_compute_sha1() { + use std::io::Write; + let dir = std::env::temp_dir().join("sumdir_test_sha1"); + std::fs::create_dir_all(&dir).expect("failed to create test dir"); + let file_path = dir.join("test.bin"); + let mut file = File::create(&file_path).expect("failed to create test file"); + file.write_all(b"abc").expect("failed to write test file"); + + // SHA1("abc") = a9993e364706816aba3e25717850c26c9cd0d89d + let hash = compute_sha1(&file_path).expect("failed to compute SHA1"); + assert_eq!(hash, "a9993e364706816aba3e25717850c26c9cd0d89d"); + + std::fs::remove_dir_all(&dir).expect("failed to cleanup test dir"); + } + + #[test] + fn test_scan_ftm_collects_file_entries() { + let config = ScanConfig { + collect_file_entries: true, + progress_bar: false, + }; + let report = scan("testdata".into(), &config); + assert!( + !report.file_entries.is_empty(), + "expected file entries to be populated" + ); + // All entries should have a non-empty path + for entry in &report.file_entries { + assert!(entry.path.to_string_lossy().len() > 0); + } + // File entries should have mime types set + let file_entries: Vec<_> = report.file_entries.iter().filter(|e| !e.is_dir).collect(); + assert!(!file_entries.is_empty()); + for entry in &file_entries { + assert!(!entry.mime_type.is_empty()); + } + } + + #[test] + fn test_scan_no_entries_when_not_ftm() { + let config = ScanConfig { + collect_file_entries: false, + progress_bar: false, + }; + let report = scan("testdata".into(), &config); + assert!( + report.file_entries.is_empty(), + "expected no file entries when collect_file_entries is false" + ); + } + + #[test] + fn test_file_entry_to_ftm_entity_image() { + use std::io::Write; + let dir = std::env::temp_dir().join("sumdir_test_ftm_image"); + std::fs::create_dir_all(&dir).expect("failed to create test dir"); + let file_path = dir.join("photo.png"); + let png_header: [u8; 8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + File::create(&file_path) + .expect("failed to create file") + .write_all(&png_header) + .expect("failed to write file"); + + let entry = FileEntry { + path: file_path.clone(), + mime_type: "image/png".to_string(), + size: 8, + created_at: None, + modified_at: None, + content_hash: Some("abc123".to_string()), + is_dir: false, + }; + + let entity = file_entry_to_ftm_entity(&entry).expect("failed to create FTM entity"); + assert_eq!(entity.schema(), "Image"); + assert_eq!(entity.id(), file_path.to_string_lossy().as_ref()); + + std::fs::remove_dir_all(&dir).expect("failed to cleanup test dir"); + } + + #[test] + fn test_file_entry_to_ftm_entity_folder() { + let entry = FileEntry { + path: PathBuf::from("/some/dir"), + mime_type: "application/x-directory".to_string(), + size: 0, + created_at: None, + modified_at: None, + content_hash: None, + is_dir: true, + }; + + let entity = file_entry_to_ftm_entity(&entry).expect("failed to create FTM entity"); + assert_eq!(entity.schema(), "Folder"); + assert_eq!(entity.id(), "/some/dir"); + } } From d144276bb206f5c71df1039bfaad19fec249dec3 Mon Sep 17 00:00:00 2001 From: Christian Stefanescu Date: Thu, 5 Mar 2026 19:58:42 +0100 Subject: [PATCH 2/2] Use ftm-export 0.1.0 --- Cargo.lock | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 6811a16..b4da628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -545,6 +545,8 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "ftm-types" version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef2da6f8a49fef4780179ddc69afc3cd3d238b2042bcf72e5e47538a97a945b" dependencies = [ "anyhow", "bon", diff --git a/Cargo.toml b/Cargo.toml index 3cad080..dacdc40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ anyhow = "1.0" chrono = { version = "0.4", default-features = false, features = ["std"] } clap = { version = "4.5", features = ["derive"] } flate2 = "1" -ftm-types = { path = "../ftm-types", features = ["builder"] } +ftm-types = { version = "0.1", features = ["builder"] } indicatif = "0.17" infer = "0.19" itertools = "0.14.0"