1use std::{fs, path::Path, sync::Arc};
2
3use iroh_blobs::Hash;
4use iroh_docs::{AuthorId, api::Doc};
5use n0_future::{BufferedStreamExt, TryStreamExt, stream};
6
7use crate::{
8 error::{Result, SyncwebError},
9 node::{blob_store::BlobStore, docs_engine::DocsEngine},
10};
11
12use super::{FileEntry, ParallelScanner, Scanner};
13
14#[derive(Clone, Debug)]
16#[non_exhaustive]
17pub struct ImportEntry {
18 pub path: std::path::PathBuf,
19 pub relative_path: std::path::PathBuf,
20 pub hash: Hash,
21 pub size: u64,
22}
23
24#[derive(Clone)]
26pub struct Importer {
27 blob_store: BlobStore,
28 docs_engine: DocsEngine,
29 doc: Doc,
30 author: AuthorId,
31 root: Option<std::path::PathBuf>,
32 ignore_patterns: Vec<String>,
33}
34
35impl Importer {
36 #[must_use]
37 pub const fn new(blob_store: BlobStore, docs_engine: DocsEngine, doc: Doc, author: AuthorId) -> Self {
38 Self {
39 blob_store,
40 docs_engine,
41 doc,
42 author,
43 root: None,
44 ignore_patterns: Vec::new(),
45 }
46 }
47
48 #[must_use]
49 pub fn with_root(mut self, root: impl Into<std::path::PathBuf>) -> Self {
50 self.root = Some(root.into());
51 self
52 }
53
54 #[must_use]
55 pub fn with_ignore_patterns(mut self, patterns: Vec<String>) -> Self {
56 self.ignore_patterns = patterns;
57 self
58 }
59
60 pub async fn import_path(&self, input: impl AsRef<Path>) -> Result<Vec<ImportEntry>> {
65 let files = self.scan_input(input.as_ref(), None)?;
66 self.import_entries(files).await
67 }
68
69 pub async fn import_entries(&self, source_entries: Vec<FileEntry>) -> Result<Vec<ImportEntry>> {
74 let mut imported = Vec::with_capacity(source_entries.len());
75 for entry in source_entries {
76 imported.push(self.import_one(entry).await?);
77 }
78 Ok(imported)
79 }
80
81 async fn import_one(&self, entry: FileEntry) -> Result<ImportEntry> {
82 if entry
86 .relative_path
87 .to_string_lossy()
88 .contains(crate::fs::CONFLICT_COPY_MARKER)
89 {
90 return Ok(ImportEntry {
91 path: entry.path,
92 relative_path: entry.relative_path,
93 hash: Hash::from_bytes(*entry.hash.as_bytes()),
94 size: entry.size,
95 });
96 }
97 let before = fs::metadata(&entry.path)?;
98 if !metadata_matches_entry(&before, &entry) {
99 return Err(file_changed_error(&entry.path));
100 }
101 let hash = self.blob_store.add_file_ref(&entry.path).await?;
102 let after = fs::metadata(&entry.path)?;
103 if !metadata_matches_entry(&after, &entry) || hash != Hash::from_bytes(*entry.hash.as_bytes()) {
104 return Err(file_changed_error(&entry.path));
105 }
106 let key = entry.relative_path.as_os_str().as_encoded_bytes();
107 if self
114 .docs_engine
115 .get_any(&self.doc, key)
116 .await?
117 .is_some_and(|existing| existing.content_hash() == hash)
118 {
119 return Ok(ImportEntry {
120 path: entry.path,
121 relative_path: entry.relative_path,
122 hash,
123 size: entry.size,
124 });
125 }
126 self.docs_engine
127 .set_blob(&self.doc, self.author, key, hash, entry.size)
128 .await?;
129 Ok(ImportEntry {
130 path: entry.path,
131 relative_path: entry.relative_path,
132 hash,
133 size: entry.size,
134 })
135 }
136
137 fn scan_input(&self, input: &Path, threads: Option<usize>) -> Result<Vec<FileEntry>> {
138 if !input.exists() {
139 return Err(SyncwebError::Io(std::io::Error::new(
140 std::io::ErrorKind::NotFound,
141 format!("input path does not exist: {}", input.display()),
142 )));
143 }
144 let root = self.root.clone().unwrap_or_else(|| {
145 if input.is_dir() {
146 input.to_path_buf()
147 } else {
148 input.parent().unwrap_or_else(|| Path::new(".")).to_path_buf()
149 }
150 });
151 let mut entries = match threads {
152 Some(threads_count) => ParallelScanner::new(&root, self.ignore_patterns.clone(), threads_count).scan()?,
153 None => Scanner::new(&root, self.ignore_patterns.clone()).scan()?,
154 };
155 let target = fs::canonicalize(input)?;
156 if input.is_dir() {
157 entries.retain(|entry| fs::canonicalize(&entry.path).is_ok_and(|path| path.starts_with(&target)));
158 } else {
159 entries.retain(|entry| fs::canonicalize(&entry.path).is_ok_and(|path| path == target));
160 }
161 Ok(entries)
162 }
163}
164
165fn metadata_matches_entry(metadata: &fs::Metadata, entry: &FileEntry) -> bool {
166 metadata.len() == entry.size && metadata.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH) == entry.modified
167}
168
169fn file_changed_error(path: &Path) -> SyncwebError {
170 SyncwebError::operation("file changed during import", path.display())
171}
172
173#[derive(Clone)]
176pub struct ParallelImporter {
177 importer: Arc<Importer>,
178 threads: usize,
179}
180
181impl ParallelImporter {
182 #[must_use]
183 pub fn new(blob_store: BlobStore, docs_engine: DocsEngine, doc: Doc, author: AuthorId) -> Self {
184 Self {
185 importer: Arc::new(Importer::new(blob_store, docs_engine, doc, author)),
186 threads: 0,
187 }
188 }
189
190 #[must_use]
191 pub const fn with_threads(mut self, threads: usize) -> Self {
192 self.threads = threads;
193 self
194 }
195
196 #[must_use]
197 pub fn with_root(mut self, root: impl Into<std::path::PathBuf>) -> Self {
198 self.importer = Arc::new((*self.importer).clone().with_root(root));
199 self
200 }
201
202 #[must_use]
203 pub fn with_ignore_patterns(mut self, patterns: Vec<String>) -> Self {
204 self.importer = Arc::new((*self.importer).clone().with_ignore_patterns(patterns));
205 self
206 }
207
208 pub async fn import_path(&self, input: impl AsRef<Path>) -> Result<Vec<ImportEntry>> {
213 let entries = self.importer.scan_input(input.as_ref(), Some(self.threads))?;
214 self.import_entries(entries).await
215 }
216
217 pub async fn import_entries(&self, source_entries: Vec<FileEntry>) -> Result<Vec<ImportEntry>> {
223 let concurrency = if self.threads == 0 {
224 std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get)
225 } else {
226 self.threads
227 };
228 let shared_importer = self.importer.clone();
229 let mut imported = stream::iter(source_entries.into_iter().enumerate().map(|(index, entry)| {
230 let task_importer = shared_importer.clone();
231 async move {
232 task_importer
233 .import_one(entry)
234 .await
235 .map(|imported_entry| (index, imported_entry))
236 }
237 }))
238 .buffered_unordered(concurrency)
239 .try_collect::<Vec<_>>()
240 .await?;
241 imported.sort_by_key(|(index, _)| *index);
242 Ok(imported.into_iter().map(|(_, entry)| entry).collect())
243 }
244}