iris_chat_core/
direct_file_destination.rs1use crate::DirectFileSnapshot;
4use std::{
5 fmt,
6 fs::{self, File, OpenOptions},
7 io::Write,
8 path::PathBuf,
9 sync::{Arc, Mutex},
10};
11
12#[derive(Debug, Clone, uniffi::Error)]
13pub enum DirectFileDestinationError {
14 Failed { reason: String },
15}
16impl fmt::Display for DirectFileDestinationError {
17 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18 match self {
19 Self::Failed { reason } => f.write_str(reason),
20 }
21 }
22}
23impl std::error::Error for DirectFileDestinationError {}
24impl From<uniffi::UnexpectedUniFFICallbackError> for DirectFileDestinationError {
25 fn from(_: uniffi::UnexpectedUniFFICallbackError) -> Self {
26 failure("Could not save received files.")
27 }
28}
29
30impl From<std::io::Error> for DirectFileDestinationError {
31 fn from(_: std::io::Error) -> Self {
32 failure("Could not save received files.")
33 }
34}
35fn failure(message: &str) -> DirectFileDestinationError {
36 DirectFileDestinationError::Failed {
37 reason: message.into(),
38 }
39}
40type Result<T> = std::result::Result<T, DirectFileDestinationError>;
41
42#[uniffi::export(with_foreign)]
47pub trait DirectFileDestination: Send + Sync {
48 fn prepare(&self, transfer_id: String, files: Vec<DirectFileSnapshot>) -> Result<()>;
49 fn write(&self, file_index: u32, bytes: Vec<u8>) -> Result<()>;
50 fn finish_file(&self, file_index: u32) -> Result<()>;
51 fn commit(&self) -> Result<Vec<String>>;
52 fn abort(&self);
53}
54impl fmt::Debug for dyn DirectFileDestination {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 f.write_str("Selected file destination")
57 }
58}
59
60#[uniffi::export]
63pub fn direct_file_directory_destination(directory: String) -> Arc<dyn DirectFileDestination> {
64 Arc::new(DirectoryDestination {
65 directory: PathBuf::from(directory),
66 state: Mutex::new(None),
67 })
68}
69struct DirectoryDestination {
70 directory: PathBuf,
71 state: Mutex<Option<DirectoryState>>,
72}
73struct DirectoryState {
74 directory: PathBuf,
75 partial: Vec<PathBuf>,
76 paths: Vec<PathBuf>,
77 handles: Vec<Option<File>>,
78 committed: bool,
79}
80impl Drop for DirectoryState {
81 fn drop(&mut self) {
82 if self.committed {
83 return;
84 }
85 self.handles.clear();
86 for path in self.partial.iter().chain(&self.paths) {
87 let _ = fs::remove_file(path);
88 }
89 let _ = fs::remove_dir(&self.directory);
90 }
91}
92impl DirectFileDestination for DirectoryDestination {
93 fn prepare(&self, id: String, files: Vec<DirectFileSnapshot>) -> Result<()> {
94 if !crate::direct_files::valid_transfer_id(&id)
95 || files.is_empty()
96 || files.len() > 32
97 || files
98 .iter()
99 .any(|f| !crate::direct_files::valid_filename(&f.filename))
100 {
101 return Err(failure("Invalid file destination."));
102 }
103 let mut slot = self
104 .state
105 .lock()
106 .map_err(|_| failure("File destination is unavailable."))?;
107 if slot.is_some() {
108 return Err(failure("File destination is already in use."));
109 }
110 if !self.directory.is_dir() {
111 return Err(failure("Choose a folder to save these files."));
112 }
113 let directory = self.directory.join(format!("Iris files {id}"));
114 fs::create_dir(&directory)?;
115 let mut state = DirectoryState {
116 directory,
117 partial: vec![],
118 paths: vec![],
119 handles: vec![],
120 committed: false,
121 };
122 for (i, file) in files.iter().enumerate() {
123 let path = state.directory.join(format!("{}.part", i));
124 let handle = OpenOptions::new()
125 .write(true)
126 .create_new(true)
127 .open(&path)?;
128 state.partial.push(path);
129 state
130 .paths
131 .push(state.directory.join(format!("{}-{}", i + 1, file.filename)));
132 state.handles.push(Some(handle));
133 }
134 *slot = Some(state);
135 Ok(())
136 }
137 fn write(&self, index: u32, bytes: Vec<u8>) -> Result<()> {
138 if bytes.len() > 32 * 1024 {
139 return Err(failure("File chunk is too large."));
140 }
141 let mut state = self
142 .state
143 .lock()
144 .map_err(|_| failure("File destination is unavailable."))?;
145 state
146 .as_mut()
147 .and_then(|s| s.handles.get_mut(index as usize))
148 .and_then(Option::as_mut)
149 .ok_or_else(|| failure("File destination is closed."))?
150 .write_all(&bytes)?;
151 Ok(())
152 }
153 fn finish_file(&self, index: u32) -> Result<()> {
154 let mut state = self
155 .state
156 .lock()
157 .map_err(|_| failure("File destination is unavailable."))?;
158 state
159 .as_mut()
160 .and_then(|s| s.handles.get_mut(index as usize))
161 .and_then(Option::take)
162 .ok_or_else(|| failure("File destination is closed."))?
163 .sync_all()?;
164 Ok(())
165 }
166 fn commit(&self) -> Result<Vec<String>> {
167 let mut slot = self
168 .state
169 .lock()
170 .map_err(|_| failure("File destination is unavailable."))?;
171 let state = slot
172 .as_mut()
173 .ok_or_else(|| failure("File destination is closed."))?;
174 if state.committed || state.handles.iter().any(Option::is_some) {
175 return Err(failure("Files are not ready to save."));
176 }
177 for (partial, path) in state.partial.iter().zip(&state.paths) {
178 fs::rename(partial, path)?;
179 }
180 #[cfg(unix)]
181 {
182 File::open(&state.directory)?.sync_all()?;
183 File::open(&self.directory)?.sync_all()?;
184 }
185 state.committed = true;
186 Ok(state
187 .paths
188 .iter()
189 .map(|p| p.to_string_lossy().into_owned())
190 .collect())
191 }
192 fn abort(&self) {
193 if let Ok(mut state) = self.state.lock() {
194 state.take();
195 }
196 }
197}