lb_rs/service/
import_export.rs

1use crate::Lb;
2use crate::model::ValidationFailure;
3use crate::model::errors::{LbErr, LbErrKind, LbResult};
4use crate::model::file::File;
5use crate::model::file_metadata::FileType;
6use futures::StreamExt;
7use futures::stream::FuturesUnordered;
8use std::fs;
9use std::fs::OpenOptions;
10use std::io::Write;
11use std::path::{Path, PathBuf};
12use uuid::Uuid;
13
14pub enum ImportStatus {
15    CalculatedTotal(usize),
16    StartingItem(String),
17    FinishedItem(File),
18}
19
20impl Lb {
21    #[instrument(level = "debug", skip(self, update_status), err(Debug))]
22    pub async fn import_files<F: Fn(ImportStatus)>(
23        &self, sources: &[PathBuf], dest: Uuid, update_status: &F,
24    ) -> LbResult<()> {
25        update_status(ImportStatus::CalculatedTotal(get_total_child_count(sources)?));
26
27        let parent = self.get_file_by_id(dest).await?;
28        if !parent.is_folder() {
29            return Err(LbErrKind::Validation(ValidationFailure::NonFolderWithChildren(dest)))?;
30        }
31
32        let import_file_futures = FuturesUnordered::new();
33
34        for source in sources {
35            let lb = self.clone();
36
37            import_file_futures.push(async move {
38                lb.import_file_recursively(source, dest, update_status)
39                    .await
40            });
41        }
42
43        import_file_futures
44            .collect::<Vec<LbResult<()>>>()
45            .await
46            .into_iter()
47            .collect::<LbResult<()>>()
48    }
49
50    async fn import_file_recursively<F: Fn(ImportStatus)>(
51        &self, disk_path: &Path, dest: Uuid, update_status: &F,
52    ) -> LbResult<()> {
53        update_status(ImportStatus::StartingItem(format!("{}", disk_path.display())));
54
55        if !disk_path.exists() {
56            return Err(LbErrKind::DiskPathInvalid.into());
57        }
58
59        let name = disk_path
60            .file_name()
61            .and_then(|name| name.to_str())
62            .ok_or(LbErrKind::DiskPathInvalid)?
63            .to_string();
64
65        let file_type = if disk_path.is_file() { FileType::Document } else { FileType::Folder };
66
67        let mut tries = 0;
68        let mut retry_name = name.clone();
69        let file: File;
70
71        loop {
72            match self.create_file(&retry_name, &dest, file_type).await {
73                Ok(new_file) => {
74                    file = new_file;
75                    break;
76                }
77                Err(err)
78                    if matches!(
79                        err.kind,
80                        LbErrKind::Validation(ValidationFailure::PathConflict(_))
81                    ) =>
82                {
83                    tries += 1;
84                    retry_name = format!("{name}-{tries}");
85                }
86                Err(err) => return Err(err),
87            }
88        }
89
90        match file_type {
91            FileType::Document => {
92                let content = fs::read(disk_path).map_err(LbErr::from)?;
93                self.write_document(file.id, content.as_slice()).await?;
94
95                update_status(ImportStatus::FinishedItem(file));
96            }
97            FileType::Folder => {
98                let id = file.id;
99                update_status(ImportStatus::FinishedItem(file));
100
101                let disk_children = fs::read_dir(disk_path).map_err(LbErr::from)?;
102
103                let import_file_futures = FuturesUnordered::new();
104
105                for disk_child in disk_children {
106                    let child_path = disk_child.map_err(LbErr::from)?.path();
107                    let lb = self.clone();
108
109                    import_file_futures.push(async move {
110                        lb.import_file_recursively(&child_path, id, update_status)
111                            .await
112                    });
113                }
114
115                import_file_futures
116                    .collect::<Vec<LbResult<()>>>()
117                    .await
118                    .into_iter()
119                    .collect::<LbResult<()>>()?;
120            }
121
122            FileType::Link { .. } => {
123                error!("links should not be interpreted!")
124            }
125        }
126
127        Ok(())
128    }
129
130    #[instrument(level = "debug", skip(self, update_status), err(Debug))]
131    pub async fn export_file<F: Fn(ExportFileInfo)>(
132        &self, id: Uuid, dest: PathBuf, edit: bool, update_status: &Option<F>,
133    ) -> LbResult<()> {
134        if dest.is_file() {
135            return Err(LbErrKind::DiskPathInvalid.into());
136        }
137
138        self.export_file_recursively(id, &dest, edit, update_status)
139            .await
140    }
141
142    pub async fn export_file_recursively<F: Fn(ExportFileInfo)>(
143        &self, id: Uuid, disk_path: &Path, edit: bool, update_status: &Option<F>,
144    ) -> LbResult<()> {
145        let file = self.get_file_by_id(id).await?;
146
147        let new_dest = disk_path.join(&file.name);
148
149        if let Some(update_status) = update_status {
150            update_status(ExportFileInfo {
151                disk_path: disk_path.to_path_buf(),
152                lockbook_path: self.get_path_by_id(file.id).await?,
153            });
154        }
155
156        match file.file_type {
157            FileType::Document => {
158                let mut disk_file = if edit {
159                    OpenOptions::new()
160                        .write(true)
161                        .create(true)
162                        .truncate(true)
163                        .open(new_dest)
164                } else {
165                    OpenOptions::new()
166                        .write(true)
167                        .create_new(true)
168                        .open(new_dest)
169                }
170                .map_err(LbErr::from)?;
171
172                disk_file
173                    .write(self.read_document(file.id, true).await?.as_slice())
174                    .map_err(LbErr::from)?;
175            }
176            FileType::Folder => {
177                fs::create_dir(new_dest.clone()).map_err(LbErr::from)?;
178                let export_file_futures = FuturesUnordered::new();
179
180                for child in self.get_children(&file.id).await? {
181                    let lb = self.clone();
182                    let new_dest = &new_dest;
183
184                    export_file_futures.push(async move {
185                        lb.export_file_recursively(child.id, new_dest, edit, update_status)
186                            .await
187                    });
188                }
189
190                export_file_futures
191                    .collect::<Vec<LbResult<()>>>()
192                    .await
193                    .into_iter()
194                    .collect::<LbResult<()>>()?;
195            }
196            FileType::Link { target } => {
197                let export_file_futures = FuturesUnordered::new();
198                let lb = self.clone();
199
200                export_file_futures.push(async move {
201                    lb.export_file_recursively(target, disk_path, edit, update_status)
202                        .await
203                });
204
205                export_file_futures
206                    .collect::<Vec<LbResult<()>>>()
207                    .await
208                    .into_iter()
209                    .collect::<LbResult<()>>()?;
210            }
211        }
212
213        Ok(())
214    }
215}
216
217fn get_total_child_count(paths: &[PathBuf]) -> LbResult<usize> {
218    let mut count = 0;
219    for p in paths {
220        count += get_child_count(p)?;
221    }
222    Ok(count)
223}
224
225fn get_child_count(path: &Path) -> LbResult<usize> {
226    let mut count = 1;
227    if path.is_dir() {
228        let children = fs::read_dir(path).map_err(LbErr::from)?;
229        for maybe_child in children {
230            let child_path = maybe_child.map_err(LbErr::from)?.path();
231
232            count += get_child_count(&child_path)?;
233        }
234    }
235    Ok(count)
236}
237
238#[derive(Debug, Clone)]
239pub struct ExportFileInfo {
240    pub disk_path: PathBuf,
241    pub lockbook_path: String,
242}