1pub mod car;
22pub mod cid;
23pub mod folder_hash;
24mod proto;
25pub mod verify;
26
27use std::path::{Path, PathBuf};
28
29#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
30pub enum CidVersion {
31 V0,
32 #[default]
33 V1,
34}
35
36#[non_exhaustive]
37#[derive(Debug, Clone)]
38pub struct UploadFolderOptions {
39 pub cid_version: CidVersion,
40 pub pin: bool,
41 pub follow_symlinks: bool,
42}
43
44impl Default for UploadFolderOptions {
45 fn default() -> Self {
46 Self {
47 cid_version: CidVersion::V1,
48 pin: true,
49 follow_symlinks: true,
50 }
51 }
52}
53
54#[non_exhaustive]
55#[derive(Debug)]
56pub struct FolderEntry {
57 pub relative_path: String,
59 pub absolute_path: PathBuf,
60}
61
62#[non_exhaustive]
63#[derive(Debug, thiserror::Error)]
64pub enum CollectError {
65 #[error("empty folder: {0}")]
66 Empty(PathBuf),
67 #[error("non-UTF-8 path: {0}")]
68 NonUtf8(PathBuf),
69 #[error("walk failed at {path}: {source}")]
70 Walk {
71 path: PathBuf,
72 #[source]
73 source: walkdir::Error,
74 },
75}
76
77pub fn collect_folder_files(
83 root: &Path,
84 follow_symlinks: bool,
85) -> Result<Vec<FolderEntry>, CollectError> {
86 let mut out = Vec::new();
87 let walker = walkdir::WalkDir::new(root)
88 .follow_links(follow_symlinks)
89 .min_depth(1);
90
91 for entry in walker {
92 let entry = entry.map_err(|e| {
93 let path = e
94 .path()
95 .map(Path::to_path_buf)
96 .unwrap_or_else(|| root.to_path_buf());
97 CollectError::Walk { path, source: e }
98 })?;
99 if !entry.file_type().is_file() {
100 continue;
101 }
102 let abs = entry.path().to_path_buf();
103 let rel = entry
104 .path()
105 .strip_prefix(root)
106 .expect("walkdir entries are descendants of root");
107 let rel_str = rel
108 .to_str()
109 .ok_or_else(|| CollectError::NonUtf8(abs.clone()))?
110 .replace(std::path::MAIN_SEPARATOR, "/");
111 out.push(FolderEntry {
112 relative_path: rel_str,
113 absolute_path: abs,
114 });
115 }
116
117 if out.is_empty() {
118 return Err(CollectError::Empty(root.to_path_buf()));
119 }
120 Ok(out)
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use std::fs;
127 use tempfile::TempDir;
128
129 #[test]
130 fn default_options_are_v1_pinned_follow_symlinks() {
131 let opts = UploadFolderOptions::default();
132 assert_eq!(opts.cid_version, CidVersion::V1);
133 assert!(opts.pin);
134 assert!(opts.follow_symlinks);
135 }
136
137 fn make_tree(tmp: &TempDir, files: &[(&str, &str)]) {
138 for (rel, content) in files {
139 let abs = tmp.path().join(rel);
140 if let Some(parent) = abs.parent() {
141 fs::create_dir_all(parent).unwrap();
142 }
143 fs::write(&abs, content).unwrap();
144 }
145 }
146
147 #[test]
148 fn collect_files_flat_directory() {
149 let tmp = TempDir::new().unwrap();
150 make_tree(&tmp, &[("a.txt", "a"), ("b.txt", "b")]);
151 let mut entries = collect_folder_files(tmp.path(), true).unwrap();
152 entries.sort_by(|x, y| x.relative_path.cmp(&y.relative_path));
153 assert_eq!(entries.len(), 2);
154 assert_eq!(entries[0].relative_path, "a.txt");
155 assert_eq!(entries[1].relative_path, "b.txt");
156 }
157
158 #[test]
159 fn collect_files_nested_directory() {
160 let tmp = TempDir::new().unwrap();
161 make_tree(
162 &tmp,
163 &[
164 ("a.txt", "a"),
165 ("sub/b.txt", "b"),
166 ("sub/deeper/c.txt", "c"),
167 ],
168 );
169 let mut paths: Vec<String> = collect_folder_files(tmp.path(), true)
170 .unwrap()
171 .into_iter()
172 .map(|e| e.relative_path)
173 .collect();
174 paths.sort();
175 assert_eq!(paths, vec!["a.txt", "sub/b.txt", "sub/deeper/c.txt"]);
176 }
177
178 #[test]
179 fn collect_files_empty_directory_errors() {
180 let tmp = TempDir::new().unwrap();
181 let err = collect_folder_files(tmp.path(), true).unwrap_err();
182 assert!(matches!(err, CollectError::Empty(_)));
183 }
184
185 #[test]
186 fn collect_files_uses_forward_slashes() {
187 let tmp = TempDir::new().unwrap();
188 make_tree(&tmp, &[("sub/x.txt", "x")]);
189 let entries = collect_folder_files(tmp.path(), true).unwrap();
190 assert_eq!(entries[0].relative_path, "sub/x.txt");
191 assert!(!entries[0].relative_path.contains('\\'));
192 }
193}