syncweb_core/fs/
importer.rs1use 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(&self, input: impl AsRef<Path>) -> Result<Vec<ImportEntry>> {
74 self.import_path(input).await
75 }
76
77 pub async fn import_entries(&self, source_entries: Vec<FileEntry>) -> Result<Vec<ImportEntry>> {
82 let mut imported = Vec::with_capacity(source_entries.len());
83 for entry in source_entries {
84 imported.push(self.import_one(entry).await?);
85 }
86 Ok(imported)
87 }
88
89 async fn import_one(&self, entry: FileEntry) -> Result<ImportEntry> {
90 let before = fs::metadata(&entry.path)?;
91 if !metadata_matches_entry(&before, &entry) {
92 return Err(file_changed_error(&entry.path));
93 }
94 let hash = self.blob_store.add_file_ref(&entry.path).await?;
95 let after = fs::metadata(&entry.path)?;
96 if !metadata_matches_entry(&after, &entry) || hash != Hash::from_bytes(*entry.hash.as_bytes()) {
97 return Err(file_changed_error(&entry.path));
98 }
99 self.docs_engine
100 .set_blob(
101 &self.doc,
102 self.author,
103 entry.relative_path.as_os_str().as_encoded_bytes(),
104 hash,
105 entry.size,
106 )
107 .await?;
108 Ok(ImportEntry {
109 path: entry.path,
110 relative_path: entry.relative_path,
111 hash,
112 size: entry.size,
113 })
114 }
115
116 fn scan_input(&self, input: &Path, threads: Option<usize>) -> Result<Vec<FileEntry>> {
117 if !input.exists() {
118 return Err(SyncwebError::Io(std::io::Error::new(
119 std::io::ErrorKind::NotFound,
120 format!("input path does not exist: {}", input.display()),
121 )));
122 }
123 let root = self.root.clone().unwrap_or_else(|| {
124 if input.is_dir() {
125 input.to_path_buf()
126 } else {
127 input.parent().unwrap_or_else(|| Path::new(".")).to_path_buf()
128 }
129 });
130 let mut entries = match threads {
131 Some(threads_count) => ParallelScanner::new(&root, self.ignore_patterns.clone(), threads_count).scan()?,
132 None => Scanner::new(&root, self.ignore_patterns.clone()).scan()?,
133 };
134 let target = fs::canonicalize(input)?;
135 if input.is_dir() {
136 entries.retain(|entry| fs::canonicalize(&entry.path).is_ok_and(|path| path.starts_with(&target)));
137 } else {
138 entries.retain(|entry| fs::canonicalize(&entry.path).is_ok_and(|path| path == target));
139 }
140 Ok(entries)
141 }
142}
143
144fn metadata_matches_entry(metadata: &fs::Metadata, entry: &FileEntry) -> bool {
145 metadata.len() == entry.size && metadata.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH) == entry.modified
146}
147
148fn file_changed_error(path: &Path) -> SyncwebError {
149 SyncwebError::operation("file changed during import", path.display())
150}
151
152#[derive(Clone)]
155pub struct ParallelImporter {
156 importer: Arc<Importer>,
157 threads: usize,
158}
159
160impl ParallelImporter {
161 #[must_use]
162 pub fn new(blob_store: BlobStore, docs_engine: DocsEngine, doc: Doc, author: AuthorId) -> Self {
163 Self {
164 importer: Arc::new(Importer::new(blob_store, docs_engine, doc, author)),
165 threads: 0,
166 }
167 }
168
169 #[must_use]
170 pub const fn with_threads(mut self, threads: usize) -> Self {
171 self.threads = threads;
172 self
173 }
174
175 #[must_use]
176 pub fn with_root(mut self, root: impl Into<std::path::PathBuf>) -> Self {
177 self.importer = Arc::new((*self.importer).clone().with_root(root));
178 self
179 }
180
181 #[must_use]
182 pub fn with_ignore_patterns(mut self, patterns: Vec<String>) -> Self {
183 self.importer = Arc::new((*self.importer).clone().with_ignore_patterns(patterns));
184 self
185 }
186
187 pub async fn import_path(&self, input: impl AsRef<Path>) -> Result<Vec<ImportEntry>> {
192 let entries = self.importer.scan_input(input.as_ref(), Some(self.threads))?;
193 self.import_entries(entries).await
194 }
195
196 pub async fn import_parallel(&self, input: impl AsRef<Path>) -> Result<Vec<ImportEntry>> {
201 self.import_path(input).await
202 }
203
204 pub async fn import_entries(&self, source_entries: Vec<FileEntry>) -> Result<Vec<ImportEntry>> {
210 let concurrency = if self.threads == 0 {
211 std::thread::available_parallelism().map_or(1, std::num::NonZeroUsize::get)
212 } else {
213 self.threads
214 };
215 let shared_importer = self.importer.clone();
216 let mut imported = stream::iter(source_entries.into_iter().enumerate().map(|(index, entry)| {
217 let task_importer = shared_importer.clone();
218 async move {
219 task_importer
220 .import_one(entry)
221 .await
222 .map(|imported_entry| (index, imported_entry))
223 }
224 }))
225 .buffered_unordered(concurrency)
226 .try_collect::<Vec<_>>()
227 .await?;
228 imported.sort_by_key(|(index, _)| *index);
229 Ok(imported.into_iter().map(|(_, entry)| entry).collect())
230 }
231}