Skip to main content

syncweb_core/fs/
importer.rs

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/// A local file after it has been added to the blob store and document.
15#[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/// Sequential scanner/blob/document import pipeline.
25#[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    /// Import one file or every file below a directory.
61    /// # Errors
62    ///
63    /// Returns an error if the filesystem or database cannot be accessed.
64    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    /// Import one file or every file below a directory using this pipeline.
70    /// # Errors
71    ///
72    /// Returns an error if the filesystem or database cannot be accessed.
73    pub async fn import(&self, input: impl AsRef<Path>) -> Result<Vec<ImportEntry>> {
74        self.import_path(input).await
75    }
76
77    /// Import pre-scanned entries.
78    /// # Errors
79    ///
80    /// Returns an error if the filesystem or database cannot be accessed.
81    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/// Parallel import pipeline. Hashing and filesystem reads are parallelized by
153/// the scanner; blob/document writes retain their ordering and async safety.
154#[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    /// Import all files under a directory.
188    /// # Errors
189    ///
190    /// Returns an error if the filesystem or database cannot be accessed.
191    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    /// Import one file or every file below a directory using the parallel pipeline.
197    /// # Errors
198    ///
199    /// Returns an error if the filesystem or database cannot be accessed.
200    pub async fn import_parallel(&self, input: impl AsRef<Path>) -> Result<Vec<ImportEntry>> {
201        self.import_path(input).await
202    }
203
204    /// Import pre-scanned entries. This method can be used with a custom
205    /// scanner while preserving the document update semantics.
206    /// # Errors
207    ///
208    /// Returns an error if the filesystem or database cannot be accessed.
209    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}