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 pre-scanned entries.
70    /// # Errors
71    ///
72    /// Returns an error if the filesystem or database cannot be accessed.
73    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/// Parallel import pipeline. Hashing and filesystem reads are parallelized by
145/// the scanner; blob/document writes retain their ordering and async safety.
146#[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    /// Import all files under a directory.
180    /// # Errors
181    ///
182    /// Returns an error if the filesystem or database cannot be accessed.
183    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    /// Import pre-scanned entries. This method can be used with a custom
189    /// scanner while preserving the document update semantics.
190    /// # Errors
191    ///
192    /// Returns an error if the filesystem or database cannot be accessed.
193    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}