1use crate::{
2 config::AppConfig,
3 database::{Database, History, QueueItem},
4 notification::notification,
5 utils, *,
6};
7use anyhow::{Context, Result};
8use ssh2::Session;
9use std::{fs::File, io::BufReader, net::TcpStream, path::Path, sync::Arc, time::Duration};
10use tokio::{sync::mpsc, time};
11
12#[derive(Debug)]
13pub struct SftpManager {
14 config: Arc<AppConfig>,
15 database: Arc<Database>,
16}
17
18
19impl SftpManager {
20 pub fn new(
21 config: Arc<AppConfig>,
22 database: Arc<Database>,
23 ) -> (Self, mpsc::UnboundedReceiver<()>) {
24 let (_tx, rx) = mpsc::unbounded_channel();
25 (
26 SftpManager {
27 config,
28 database,
29 },
30 rx,
31 )
32 }
33
34
35 pub async fn start(self: Arc<Self>) {
36 let mut interval =
37 time::interval(Duration::from_millis(self.config.fs_check_interval));
38
39 info!(
40 "Starting SFTP queue processor with check interval: {}ms",
41 self.config.fs_check_interval
42 );
43
44 loop {
45 interval.tick().await;
46 if let Err(e) = self.process_queue().await {
47 error!("Error processing queue: {e:?}");
48 }
49 }
50 }
51
52
53 async fn process_queue(&self) -> Result<()> {
54 let queue = self.database.get_queue()?;
55 if !queue.is_empty() {
56 self.build_clipboard(&queue)?;
58
59 for item in &queue {
61 if let Err(e) = self.process_element(item).await {
62 error!("Error processing queue element: {e:?}");
63 }
64 }
65 }
66 Ok(())
67 }
68
69
70 fn build_clipboard(&self, queue: &[QueueItem]) -> Result<()> {
71 if let Some(item) = queue.last() {
72 let config = self
73 .config
74 .select_config()
75 .expect("One of configs should always be selected!");
76 let content = format!(
77 "{}{}{}",
78 config.address,
79 item.uuid,
80 file_extension(&item.local_file)
81 );
82
83 put_to_clipboard(&content)?;
84
85 if self.config.notifications.clipboard {
86 let _ = notification(
87 "Link copied to clipboard",
88 "clipboard",
89 &self.config.notifications,
90 &self.config.sounds,
91 );
92 }
93 }
94
95 Ok(())
96 }
97
98
99 async fn process_element(&self, item: &QueueItem) -> Result<()> {
100 let path = Path::new(&item.local_file);
101
102 if !path.exists() || !path.is_file() {
104 debug!(
105 "Local file not found or not a regular file: {}. Skipping.",
106 item.local_file
107 );
108 self.database.remove_from_queue(&item.uuid)?;
109 return Ok(());
110 }
111
112 if TEMP_PATTERN.is_match(&item.local_file) {
114 debug!("File matches temp pattern, skipping: {}", item.local_file);
115 self.database.remove_from_queue(&item.uuid)?;
116 return Ok(());
117 }
118
119 let remote_file = format!("{}{}", item.remote_file, file_extension(&item.local_file));
121 self.send_file(&item.local_file, &remote_file).await?;
122
123 self.add_to_history(item)?;
125
126 self.database.remove_from_queue(&item.uuid)?;
128
129 Ok(())
130 }
131
132
133 async fn send_file(&self, local_file: &str, remote_file: &str) -> Result<()> {
134 let config = &self
135 .config
136 .select_config()
137 .expect("One of configs should always be selected!");
138
139 let tcp = TcpStream::connect(format!("{}:{}", config.hostname, config.ssh_port))
141 .context("Failed to connect to SSH server")?;
142 tcp.set_read_timeout(Some(Duration::from_millis(
143 self.config.ssh_connection_timeout,
144 )))?;
145 tcp.set_write_timeout(Some(Duration::from_millis(
146 self.config.ssh_connection_timeout,
147 )))?;
148
149 let mut sess = Session::new()?;
150 sess.set_tcp_stream(tcp);
151 sess.handshake()?;
152
153 let ssh_private_key = if config.ssh_key.is_empty() {
155 ".ssh/id_ed25519"
156 } else {
157 &config.ssh_key
158 };
159
160 sess.userauth_pubkey_file(
161 &config.username,
162 None,
163 Path::new(&home::home_dir().expect("Home dir has to be set!"))
164 .join(ssh_private_key)
165 .as_path(),
166 if config.ssh_key_pass.is_empty() {
167 None
168 } else {
169 Some(&config.ssh_key_pass)
170 },
171 )?;
172
173 if !sess.authenticated() {
174 anyhow::bail!("SSH authentication failed");
175 }
176
177 debug!("SSH connection established");
178
179 let sftp = sess.sftp()?;
181 debug!("SFTP session started");
182
183 let local_size = local_file_size(local_file)?;
185 let remote_size = sftp
186 .stat(Path::new(remote_file))
187 .map(|stat| stat.size.unwrap_or(0))
188 .unwrap_or(0);
189
190 debug!(
191 "Local file: {local_file} ({local_size}); Remote file: {remote_file} ({remote_size})"
192 );
193
194 if remote_size > 0 && remote_size == local_size {
195 info!("Found file of same size already uploaded. Skipping");
196 return Ok(());
197 }
198
199 let mut local = BufReader::new(File::open(local_file)?);
201 let mut remote = sftp.create(Path::new(remote_file))?;
202
203 stream_file_to_remote(
204 &mut local,
205 &mut remote,
206 self.config.sftp_buffer_size,
207 local_size,
208 )?;
209
210 if self.config.notifications.upload {
211 let _ = notification(
212 "Uploaded successfully.",
213 "upload",
214 &self.config.notifications,
215 &self.config.sounds,
216 );
217 }
218 Ok(())
219 }
220
221
222 fn add_to_history(&self, queue_item: &QueueItem) -> Result<()> {
223 let config = self
224 .config
225 .select_config()
226 .expect("One of configs should always be selected!");
227 let content = format!(
228 "{}{}{}",
229 config.address,
230 queue_item.uuid,
231 file_extension(&queue_item.local_file)
232 );
233
234 let history = self.database.get_history(None)?;
236 let exists = history.iter().any(|h| h.content.contains(&content));
237
238 if !exists {
239 let history_item = History {
240 content,
241 timestamp: chrono::Local::now().timestamp(),
242 file: queue_item.local_file.clone(),
243 uuid: uuid::Uuid::new_v4().to_string(),
244 };
245 self.database.add_history(&history_item)?;
246 }
247
248 Ok(())
249 }
250}