Skip to main content

hashtree_cli/storage/
upload.rs

1use super::*;
2use futures::io::AllowStdIo;
3use std::collections::HashMap;
4use std::io::Read;
5
6impl HashtreeStore {
7    /// Upload a file as raw plaintext and return its CID, with auto-pin
8    pub fn upload_file<P: AsRef<Path>>(&self, file_path: P) -> Result<String> {
9        self.upload_file_internal(file_path, true)
10    }
11
12    /// Upload a file without pinning (for blossom uploads that can be evicted)
13    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        // Store raw plaintext blobs without CHK encryption, streaming from disk.
23        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        // Only pin if requested (htree add = pin, blossom upload = no pin)
30        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    /// Upload a file from a stream with progress callbacks
40    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        // Use HashTree.put_stream for streaming upload without CHK encryption.
50        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        // Auto-pin on upload
60        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    /// Upload a directory and return its root hash (hex)
68    /// Respects .gitignore and ignores common OS junk files by default.
69    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    /// Upload a directory with options as raw plaintext (no CHK encryption)
74    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        // Build directory structure from flat file list - store full Cid with key
107        let mut dir_contents: HashMap<String, Vec<(String, Cid)>> = HashMap::new();
108        dir_contents.insert(String::new(), Vec::new()); // Root
109
110        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            // Skip the root directory itself
117            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                // Get parent directory path and file name
131                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                // Ensure directory entry exists
143                let dir_path = relative.to_string_lossy().to_string();
144                dir_contents.entry(dir_path).or_default();
145            }
146        }
147
148        // Build directory tree bottom-up
149        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        // Sort directories by depth (deepest first) to build bottom-up
158        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) // Deepest first
163        });
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            // Add subdirectory entries
176            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        // Return root Cid
200        dir_cids
201            .get("")
202            .cloned()
203            .ok_or_else(|| anyhow::anyhow!("No root directory"))
204    }
205
206    /// Upload a file with CHK encryption, returns CID in format "hash:key"
207    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        // Use unified API with encryption enabled (default), streaming from disk.
213        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    /// Upload a directory with CHK encryption, returns CID
229    /// Respects .gitignore and ignores common OS junk files by default.
230    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    /// Upload a directory with CHK encryption and options
235    /// Returns CID as "hash:key" format for encrypted directories
236    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        // Use unified API with encryption enabled (default)
245        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(); // Returns "hash:key" or "hash"
254
255        let mut wtxn = self.env.write_txn()?;
256        // Pin by hash only (the key is for decryption, not identification)
257        self.pins.put(&mut wtxn, root_cid.hash.as_slice(), &())?;
258        wtxn.commit()?;
259
260        Ok(cid_str)
261    }
262}