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