mirror of
https://github.com/rust-kotlin/ashell.git
synced 2026-10-03 16:00:18 +00:00
This commit introduces a robust logging system and optimizes remote metric collection over SSH: - Logging Infrastructure: - Replaced ad-hoc `eprintln!` and `println!` with the `tracing` crate. - Implemented a rolling file appender in `~/.config/ashell/log/` keeping the last 6 minutes of logs. - Restricted stdout logging to debug builds only, keeping release builds clean. - SSH Multiplexing & Performance: - Shared `russh::client::Handle` across tasks using `Arc<Mutex<>>`. - Replaced the 5-second polling of new SSH connections with `SampleMetrics` over the existing channel. - Eliminated authentication log spam and vastly improved polling latency. - Optimized terminal `resize` events to only trigger when dimensions actually change, further reducing UI layout spam and backend load. - Application & Network Auditing: - Log window lifecycles (open, close). - Log UI actions such as session connects and tab closures. - Detailed connection logs distinguishing between graceful EOFs, network drops, and abrupt closures. - Granular SFTP tracking including directory navigation, creation, batch uploads/downloads, and online file editing.
338 lines
12 KiB
Rust
338 lines
12 KiB
Rust
use gpui::{Context, PathPromptOptions, Pixels, Point, Window};
|
|
|
|
use crate::{
|
|
Ashell, SftpContextMenuState,
|
|
sftp::{RemoteEntry, SftpHandle},
|
|
terminal,
|
|
};
|
|
|
|
pub(crate) fn is_editable_text_file(filename: &str) -> bool {
|
|
let lower = filename.to_lowercase();
|
|
let ext = std::path::Path::new(&lower).extension().and_then(|s| s.to_str()).unwrap_or("");
|
|
let known_exts = ["txt", "conf", "json", "yaml", "yml", "xml", "ini", "sh", "py", "rs", "js", "ts", "html", "css", "md", "toml", "csv", "log", "cfg"];
|
|
if known_exts.contains(&ext) {
|
|
return true;
|
|
}
|
|
let known_names = ["dockerfile", "makefile", ".gitignore", ".env"];
|
|
if known_names.contains(&lower.as_str()) {
|
|
return true;
|
|
}
|
|
false
|
|
}
|
|
|
|
impl Ashell {
|
|
pub(crate) fn active_sftp(&self) -> Option<&terminal::SftpUiState> {
|
|
self.active_group.as_ref()
|
|
.and_then(|id| self.tab_groups.iter().find(|g| &g.id == id))
|
|
.and_then(|g| g.sftp.as_ref())
|
|
}
|
|
|
|
pub(crate) fn active_sftp_mut(&mut self) -> Option<&mut terminal::SftpUiState> {
|
|
let active_id = self.active_group.clone()?;
|
|
self.tab_groups.iter_mut().find(|g| g.id == active_id)
|
|
.and_then(|g| g.sftp.as_mut())
|
|
}
|
|
|
|
pub(crate) fn active_sftp_handle(&self) -> Option<&SftpHandle> {
|
|
self.active_group.as_ref()
|
|
.and_then(|id| self.sftp_handles.get(id))
|
|
}
|
|
|
|
pub(crate) fn navigate_sftp(&mut self, path: String, cx: &mut Context<Self>) {
|
|
if let Some(handle) = self.active_sftp_handle() {
|
|
tracing::info!("[sftp] navigating to directory: '{}'", path);
|
|
handle.list_dir(path.clone());
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
sftp.current_path = path;
|
|
self.pending_sftp_path_sync = Some(sftp.current_path.clone());
|
|
}
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn select_sftp_entry(&mut self, entry: RemoteEntry, cx: &mut Context<Self>) {
|
|
if entry.is_dir {
|
|
self.navigate_sftp(entry.full_path, cx);
|
|
return;
|
|
}
|
|
self.mark_sftp_entry_selected(&entry.full_path, cx);
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
if !sftp.selected_entries.remove(&entry.full_path) {
|
|
sftp.selected_entries.insert(entry.full_path);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn mark_sftp_entry_selected(&mut self, path: &str, cx: &mut Context<Self>) {
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
sftp.selected_path = Some(path.to_string());
|
|
}
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn sftp_parent_path(path: &str) -> String {
|
|
if path == "/" {
|
|
return "/".to_string();
|
|
}
|
|
path.trim_end_matches('/')
|
|
.rsplit_once('/')
|
|
.map(|(parent, _)| {
|
|
if parent.is_empty() {
|
|
"/".to_string()
|
|
} else {
|
|
parent.to_string()
|
|
}
|
|
})
|
|
.unwrap_or_else(|| "/".to_string())
|
|
}
|
|
|
|
pub(crate) fn refresh_sftp(&mut self, cx: &mut Context<Self>) {
|
|
if let Some(path) = self.active_sftp().map(|sftp| sftp.current_path.clone()) {
|
|
self.navigate_sftp(path, cx);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn sync_sftp_path_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(path) = self.pending_sftp_path_sync.take() else {
|
|
return;
|
|
};
|
|
self.sftp_path_input.update(cx, |state, cx| {
|
|
state.set_value(path, window, cx);
|
|
});
|
|
}
|
|
|
|
pub(crate) fn open_sftp_context_menu(
|
|
&mut self,
|
|
remote_path: String,
|
|
is_dir: bool,
|
|
position: Point<Pixels>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.sftp_context_menu = Some(SftpContextMenuState {
|
|
remote_path,
|
|
is_dir,
|
|
position,
|
|
});
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn dismiss_sftp_context_menu(&mut self, cx: &mut Context<Self>) {
|
|
if self.sftp_context_menu.take().is_some() {
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn trigger_sftp_context_download(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(menu) = self.sftp_context_menu.take() else {
|
|
return;
|
|
};
|
|
self.download_sftp_entry(menu.remote_path, window, cx);
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn trigger_sftp_context_edit(&mut self, cx: &mut Context<Self>) {
|
|
let Some(menu) = self.sftp_context_menu.take() else {
|
|
return;
|
|
};
|
|
if let Some(handle) = self.active_sftp_handle() {
|
|
tracing::info!("[sftp] triggering edit for file: '{}'", menu.remote_path);
|
|
handle.edit_file(menu.remote_path);
|
|
}
|
|
cx.notify();
|
|
}
|
|
|
|
pub(crate) fn download_sftp_entry(
|
|
&mut self,
|
|
remote_path: String,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
|
return;
|
|
};
|
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
|
files: false,
|
|
directories: true,
|
|
multiple: false,
|
|
prompt: Some("Select Download Folder".into()),
|
|
});
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
match path_prompt.await {
|
|
Ok(Ok(Some(mut paths))) => {
|
|
if let Some(folder) = paths.pop() {
|
|
let local_path = folder.to_string_lossy().to_string();
|
|
tracing::info!("[sftp] initiating download of '{}' to '{}'", remote_path, local_path);
|
|
handle.download(remote_path, local_path);
|
|
}
|
|
}
|
|
Ok(Err(err)) => {
|
|
this.update(cx, |this, cx| {
|
|
this.status = format!("download picker failed: {err}").into();
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub(crate) fn upload_sftp_files(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
|
return;
|
|
};
|
|
let remote_dir = self
|
|
.active_sftp()
|
|
.map(|sftp| sftp.current_path.clone())
|
|
.unwrap_or_else(|| "/".into());
|
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
|
files: true,
|
|
directories: false,
|
|
multiple: false,
|
|
prompt: Some("Select File to Upload".into()),
|
|
});
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
match path_prompt.await {
|
|
Ok(Ok(Some(mut paths))) => {
|
|
if let Some(file) = paths.pop() {
|
|
let local_path = file.to_string_lossy().to_string();
|
|
tracing::info!("[sftp] initiating upload of file '{}' to '{}'", local_path, remote_dir);
|
|
handle.upload_paths(vec![local_path], remote_dir);
|
|
}
|
|
}
|
|
Ok(Err(err)) => {
|
|
this.update(cx, |this, cx| {
|
|
this.status = format!("upload picker failed: {err}").into();
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub(crate) fn upload_sftp_folder(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
|
return;
|
|
};
|
|
let remote_dir = self
|
|
.active_sftp()
|
|
.map(|sftp| sftp.current_path.clone())
|
|
.unwrap_or_else(|| "/".into());
|
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
|
files: false,
|
|
directories: true,
|
|
multiple: false,
|
|
prompt: Some("Select Folder to Upload".into()),
|
|
});
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
match path_prompt.await {
|
|
Ok(Ok(Some(mut paths))) => {
|
|
if let Some(folder) = paths.pop() {
|
|
let local_path = folder.to_string_lossy().to_string();
|
|
tracing::info!("[sftp] initiating upload of folder '{}' to '{}'", local_path, remote_dir);
|
|
handle.upload_paths(vec![local_path], remote_dir);
|
|
}
|
|
}
|
|
Ok(Err(err)) => {
|
|
this.update(cx, |this, cx| {
|
|
this.status = format!("upload picker failed: {err}").into();
|
|
cx.notify();
|
|
})?;
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub(crate) fn toggle_sftp_entry(&mut self, path: String, checked: bool, cx: &mut Context<Self>) {
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
if checked {
|
|
sftp.selected_entries.insert(path);
|
|
} else {
|
|
sftp.selected_entries.remove(&path);
|
|
}
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn toggle_all_sftp_entries(&mut self, checked: bool, cx: &mut Context<Self>) {
|
|
if let Some(sftp) = self.active_sftp_mut() {
|
|
if checked {
|
|
let paths: Vec<String> = sftp.entries.iter().map(|e| e.full_path.clone()).collect();
|
|
for path in paths {
|
|
sftp.selected_entries.insert(path);
|
|
}
|
|
} else {
|
|
sftp.selected_entries.clear();
|
|
}
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub(crate) fn download_selected_sftp_entries(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
let Some(sftp) = self.active_sftp() else {
|
|
return;
|
|
};
|
|
let selected: Vec<String> = sftp.selected_entries.iter().cloned().collect();
|
|
if selected.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let Some(handle) = self.active_sftp_handle().cloned() else {
|
|
return;
|
|
};
|
|
|
|
let path_prompt = cx.prompt_for_paths(PathPromptOptions {
|
|
files: false,
|
|
directories: true,
|
|
multiple: false,
|
|
prompt: Some("Select Download Folder".into()),
|
|
});
|
|
|
|
cx.spawn_in(window, async move |this, cx| {
|
|
if let Ok(Ok(Some(mut paths))) = path_prompt.await {
|
|
if let Some(folder) = paths.pop() {
|
|
let local_dir = folder.to_string_lossy().to_string();
|
|
tracing::info!("[sftp] initiating batch download of {} entries to '{}'", selected.len(), local_dir);
|
|
for remote in selected {
|
|
let _ = handle.commands.send(crate::sftp::SftpCommand::Download {
|
|
remote,
|
|
local_dir: local_dir.clone(),
|
|
});
|
|
}
|
|
|
|
let _ = this.update(cx, |this, cx| {
|
|
if let Some(sftp_mut) = this.active_sftp_mut() {
|
|
sftp_mut.selected_entries.clear();
|
|
}
|
|
cx.notify();
|
|
});
|
|
}
|
|
}
|
|
Ok::<(), anyhow::Error>(())
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
pub(crate) fn upload_sftp_files_batch(&mut self, paths: Vec<String>, _cx: &mut Context<Self>) {
|
|
if paths.is_empty() {
|
|
return;
|
|
}
|
|
if let Some(sftp) = self.active_sftp() {
|
|
if let Some(handle) = self.active_sftp_handle() {
|
|
tracing::info!("[sftp] initiating batch upload of {} files to '{}'", paths.len(), sftp.current_path);
|
|
let _ = handle.commands.send(crate::sftp::SftpCommand::UploadPaths {
|
|
locals: paths,
|
|
remote_dir: sftp.current_path.clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|