Skip to main content

small_bin/
sftp.rs

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            // Build clipboard content
57            self.build_clipboard(&queue)?;
58
59            // Process each queue item
60            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        // Check if file exists and is regular
103        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        // Check for temp file pattern
113        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        // Upload file
120        let remote_file = format!("{}{}", item.remote_file, file_extension(&item.local_file));
121        self.send_file(&item.local_file, &remote_file).await?;
122
123        // Add to history
124        self.add_to_history(item)?;
125
126        // Remove from queue
127        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        // Connect via SSH
140        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        // Authenticate
154        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        // Start SFTP session
180        let sftp = sess.sftp()?;
181        debug!("SFTP session started");
182
183        // Check remote file
184        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        // Upload file
200        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        // Check if already in history
235        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}