lb_rs/service/
import_export.rs1use 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}