hashtree_cli/storage/
upload.rs1use super::*;
2use futures::io::AllowStdIo;
3use std::collections::HashMap;
4use std::io::Read;
5
6impl HashtreeStore {
7 pub fn upload_file<P: AsRef<Path>>(&self, file_path: P) -> Result<String> {
9 self.upload_file_internal(file_path, true)
10 }
11
12 pub fn upload_file_no_pin<P: AsRef<Path>>(&self, file_path: P) -> Result<String> {
14 self.upload_file_internal(file_path, false)
15 }
16
17 fn upload_file_internal<P: AsRef<Path>>(&self, file_path: P, pin: bool) -> Result<String> {
18 let file_path = file_path.as_ref();
19 let file = std::fs::File::open(file_path)
20 .with_context(|| format!("Failed to open file {}", file_path.display()))?;
21
22 let store = self.store_arc();
24 let tree = HashTree::new(HashTreeConfig::new(store).public());
25
26 let (cid, _size) = sync_block_on(async { tree.put_stream(AllowStdIo::new(file)).await })
27 .context("Failed to store file")?;
28
29 if pin {
31 let mut wtxn = self.env.write_txn()?;
32 self.pins.put(&mut wtxn, cid.hash.as_slice(), &())?;
33 wtxn.commit()?;
34 }
35
36 Ok(to_hex(&cid.hash))
37 }
38
39 pub fn upload_file_stream<R: Read, F>(
41 &self,
42 reader: R,
43 _file_name: impl Into<String>,
44 mut callback: F,
45 ) -> Result<String>
46 where
47 F: FnMut(&str),
48 {
49 let store = self.store_arc();
51 let tree = HashTree::new(HashTreeConfig::new(store).public());
52
53 let (cid, _size) = sync_block_on(async { tree.put_stream(AllowStdIo::new(reader)).await })
54 .context("Failed to store file")?;
55
56 let root_hex = to_hex(&cid.hash);
57 callback(&root_hex);
58
59 let mut wtxn = self.env.write_txn()?;
61 self.pins.put(&mut wtxn, cid.hash.as_slice(), &())?;
62 wtxn.commit()?;
63
64 Ok(root_hex)
65 }
66
67 pub fn upload_dir<P: AsRef<Path>>(&self, dir_path: P) -> Result<String> {
70 self.upload_dir_with_options(dir_path, true)
71 }
72
73 pub fn upload_dir_with_options<P: AsRef<Path>>(
75 &self,
76 dir_path: P,
77 respect_gitignore: bool,
78 ) -> Result<String> {
79 let dir_path = dir_path.as_ref();
80
81 let store = self.store_arc();
82 let tree = HashTree::new(HashTreeConfig::new(store).public());
83
84 let root_cid = sync_block_on(async {
85 self.upload_dir_recursive(&tree, dir_path, dir_path, respect_gitignore)
86 .await
87 })
88 .context("Failed to upload directory")?;
89
90 let root_hex = to_hex(&root_cid.hash);
91
92 let mut wtxn = self.env.write_txn()?;
93 self.pins.put(&mut wtxn, root_cid.hash.as_slice(), &())?;
94 wtxn.commit()?;
95
96 Ok(root_hex)
97 }
98
99 async fn upload_dir_recursive<S: Store>(
100 &self,
101 tree: &HashTree<S>,
102 _root_path: &Path,
103 current_path: &Path,
104 respect_gitignore: bool,
105 ) -> Result<Cid> {
106 let mut dir_contents: HashMap<String, Vec<(String, Cid)>> = HashMap::new();
108 dir_contents.insert(String::new(), Vec::new()); let walker = crate::ignore_rules::build_content_walker(current_path, respect_gitignore);
111
112 for result in walker {
113 let entry = result?;
114 let path = entry.path();
115
116 if path == current_path {
118 continue;
119 }
120
121 let relative = path.strip_prefix(current_path).unwrap_or(path);
122
123 if path.is_file() {
124 let file = std::fs::File::open(path)
125 .with_context(|| format!("Failed to open file {}", path.display()))?;
126 let (cid, _size) = tree.put_stream(AllowStdIo::new(file)).await.map_err(|e| {
127 anyhow::anyhow!("Failed to upload file {}: {}", path.display(), e)
128 })?;
129
130 let parent = relative
132 .parent()
133 .map(|p| p.to_string_lossy().to_string())
134 .unwrap_or_default();
135 let name = relative
136 .file_name()
137 .map(|n| n.to_string_lossy().to_string())
138 .unwrap_or_default();
139
140 dir_contents.entry(parent).or_default().push((name, cid));
141 } else if path.is_dir() {
142 let dir_path = relative.to_string_lossy().to_string();
144 dir_contents.entry(dir_path).or_default();
145 }
146 }
147
148 self.build_directory_tree(tree, &mut dir_contents).await
150 }
151
152 async fn build_directory_tree<S: Store>(
153 &self,
154 tree: &HashTree<S>,
155 dir_contents: &mut HashMap<String, Vec<(String, Cid)>>,
156 ) -> Result<Cid> {
157 let mut dirs: Vec<String> = dir_contents.keys().cloned().collect();
159 dirs.sort_by(|a, b| {
160 let depth_a = a.matches('/').count() + if a.is_empty() { 0 } else { 1 };
161 let depth_b = b.matches('/').count() + if b.is_empty() { 0 } else { 1 };
162 depth_b.cmp(&depth_a) });
164
165 let mut dir_cids: HashMap<String, Cid> = HashMap::new();
166
167 for dir_path in dirs {
168 let files = dir_contents.get(&dir_path).cloned().unwrap_or_default();
169
170 let mut entries: Vec<hashtree_core::DirEntry> = files
171 .into_iter()
172 .map(|(name, cid)| hashtree_core::DirEntry::from_cid(name, &cid))
173 .collect();
174
175 for (subdir_path, cid) in &dir_cids {
177 let parent = Path::new(subdir_path)
178 .parent()
179 .map(|p| p.to_string_lossy().to_string())
180 .unwrap_or_default();
181
182 if parent == dir_path {
183 let name = Path::new(subdir_path)
184 .file_name()
185 .map(|n| n.to_string_lossy().to_string())
186 .unwrap_or_default();
187 entries.push(hashtree_core::DirEntry::from_cid(name, cid));
188 }
189 }
190
191 let cid = tree
192 .put_directory(entries)
193 .await
194 .map_err(|e| anyhow::anyhow!("Failed to create directory node: {}", e))?;
195
196 dir_cids.insert(dir_path, cid);
197 }
198
199 dir_cids
201 .get("")
202 .cloned()
203 .ok_or_else(|| anyhow::anyhow!("No root directory"))
204 }
205
206 pub fn upload_file_encrypted<P: AsRef<Path>>(&self, file_path: P) -> Result<String> {
208 let file_path = file_path.as_ref();
209 let file = std::fs::File::open(file_path)
210 .with_context(|| format!("Failed to open file {}", file_path.display()))?;
211
212 let store = self.store_arc();
214 let tree = HashTree::new(HashTreeConfig::new(store));
215
216 let (cid, _size) = sync_block_on(async { tree.put_stream(AllowStdIo::new(file)).await })
217 .map_err(|e| anyhow::anyhow!("Failed to encrypt file: {}", e))?;
218
219 let cid_str = cid.to_string();
220
221 let mut wtxn = self.env.write_txn()?;
222 self.pins.put(&mut wtxn, cid.hash.as_slice(), &())?;
223 wtxn.commit()?;
224
225 Ok(cid_str)
226 }
227
228 pub fn upload_dir_encrypted<P: AsRef<Path>>(&self, dir_path: P) -> Result<String> {
231 self.upload_dir_encrypted_with_options(dir_path, true)
232 }
233
234 pub fn upload_dir_encrypted_with_options<P: AsRef<Path>>(
237 &self,
238 dir_path: P,
239 respect_gitignore: bool,
240 ) -> Result<String> {
241 let dir_path = dir_path.as_ref();
242 let store = self.store_arc();
243
244 let tree = HashTree::new(HashTreeConfig::new(store));
246
247 let root_cid = sync_block_on(async {
248 self.upload_dir_recursive(&tree, dir_path, dir_path, respect_gitignore)
249 .await
250 })
251 .context("Failed to upload encrypted directory")?;
252
253 let cid_str = root_cid.to_string(); let mut wtxn = self.env.write_txn()?;
256 self.pins.put(&mut wtxn, root_cid.hash.as_slice(), &())?;
258 wtxn.commit()?;
259
260 Ok(cid_str)
261 }
262}