feat(i18n): add multi-language support (ja/ko/vi/ru/zh-TW) with system auto-detection
- Extend AppLanguage to 7 locales: zh-CN, zh-TW, en, ja, ko, vi, ru - Auto-detect system language via navigator.languages; fallback to English - Add BCP47 locale mapping for date formatting and sorting - Create 50 translation JSON files (5 languages × 10 namespaces, 1137 keys each) - Update language selector with native language names - Extend locale key parity test to cover all 7 languages (70 assertions) - Add VNC/RDP desktop, serial terminal, AI agent, port forwarding, and SFTP features - Add credential encryption, host metrics, session backup crypto, and zmodem bridge - Update Rust backend: desktop services, host metrics, SSH/SFTP enhancements
This commit is contained in:
1 parent
fea6d8db04
commit
b0d0e3a5f3
301 files changed
+28002
-2705
No files matched your search
@@ -75,6 +75,36 @@ impl SshService {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sftp_upload_dir(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
sftp_id: &str,
|
||||
local_dir: &str,
|
||||
remote_dir: &str,
|
||||
) -> Result<(), String> {
|
||||
let sftp = self.sftp(sftp_id)?;
|
||||
upload_dir(
|
||||
&sftp,
|
||||
&expand_home(local_dir),
|
||||
remote_dir,
|
||||
&mut |transferred, total| emit_progress(app, sftp_id, transferred, total),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sftp_download_dir(
|
||||
&self,
|
||||
app: &AppHandle,
|
||||
sftp_id: &str,
|
||||
remote_dir: &str,
|
||||
local_dir: &str,
|
||||
) -> Result<(), String> {
|
||||
let sftp = self.sftp(sftp_id)?;
|
||||
let local_dir = expand_home(local_dir);
|
||||
download_dir(&sftp, remote_dir, &local_dir, &mut |transferred, total| {
|
||||
emit_progress(app, sftp_id, transferred, total)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sftp_rename(
|
||||
&self,
|
||||
sftp_id: &str,
|
||||
@@ -311,6 +341,140 @@ pub(super) fn download_file(
|
||||
fs::rename(&partial_path, local_path).map_err(|error| format!("提交下载文件失败: {error}"))
|
||||
}
|
||||
|
||||
fn join_remote(parent: &str, name: &str) -> String {
|
||||
if parent.is_empty() || parent == "/" {
|
||||
format!("/{name}")
|
||||
} else {
|
||||
format!("{}/{name}", parent.trim_end_matches('/'))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dir_mode(mode: u32) -> bool {
|
||||
mode & 0o170000 == 0o040000
|
||||
}
|
||||
|
||||
fn ensure_remote_dir(sftp: &ssh2::Sftp, remote_dir: &str) -> Result<(), String> {
|
||||
let normalized = if remote_dir.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
remote_dir.trim_end_matches('/').to_string()
|
||||
};
|
||||
if normalized.is_empty() || normalized == "/" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut parts = Vec::new();
|
||||
for part in normalized.trim_start_matches('/').split('/') {
|
||||
if part.is_empty() || part == "." {
|
||||
continue;
|
||||
}
|
||||
if part == ".." {
|
||||
return Err("远程目录路径不能包含 ..".into());
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
|
||||
let mut current = String::new();
|
||||
for part in parts {
|
||||
current = if current.is_empty() {
|
||||
format!("/{part}")
|
||||
} else {
|
||||
format!("{current}/{part}")
|
||||
};
|
||||
match sftp.lstat(Path::new(¤t)) {
|
||||
Ok(stat) if is_dir_mode(stat.perm.unwrap_or(0)) => continue,
|
||||
Ok(_) => return Err(format!("远程路径已存在且不是目录: {current}")),
|
||||
Err(_) => {
|
||||
sftp.mkdir(Path::new(¤t), 0o755)
|
||||
.map_err(|error| format!("创建远程目录失败 ({current}): {error}"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn upload_dir(
|
||||
sftp: &ssh2::Sftp,
|
||||
local_dir: &Path,
|
||||
remote_dir: &str,
|
||||
progress: &mut dyn FnMut(u64, u64),
|
||||
) -> Result<(), String> {
|
||||
let meta = fs::metadata(local_dir)
|
||||
.map_err(|error| format!("读取本地目录失败 ({}): {error}", local_dir.display()))?;
|
||||
if !meta.is_dir() {
|
||||
return Err(format!("本地路径不是目录: {}", local_dir.display()));
|
||||
}
|
||||
ensure_remote_dir(sftp, remote_dir)?;
|
||||
|
||||
let mut entries = fs::read_dir(local_dir)
|
||||
.map_err(|error| format!("遍历本地目录失败 ({}): {error}", local_dir.display()))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|error| format!("读取本地目录项失败: {error}"))?;
|
||||
entries.sort_by_key(|entry| entry.file_name());
|
||||
|
||||
for entry in entries {
|
||||
let file_type = entry
|
||||
.file_type()
|
||||
.map_err(|error| format!("读取本地文件类型失败: {error}"))?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if name == "." || name == ".." {
|
||||
continue;
|
||||
}
|
||||
let local_child = entry.path();
|
||||
let remote_child = join_remote(remote_dir, &name);
|
||||
if file_type.is_dir() {
|
||||
upload_dir(sftp, &local_child, &remote_child, progress)?;
|
||||
} else if file_type.is_file() {
|
||||
upload_file(sftp, &local_child, &remote_child, |transferred, total| {
|
||||
progress(transferred, total)
|
||||
})?;
|
||||
}
|
||||
// skip symlinks / special files for safety
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn download_dir(
|
||||
sftp: &ssh2::Sftp,
|
||||
remote_dir: &str,
|
||||
local_dir: &Path,
|
||||
progress: &mut dyn FnMut(u64, u64),
|
||||
) -> Result<(), String> {
|
||||
let stat = sftp
|
||||
.lstat(Path::new(remote_dir))
|
||||
.map_err(|error| format!("读取远程目录失败 ({remote_dir}): {error}"))?;
|
||||
if !is_dir_mode(stat.perm.unwrap_or(0)) {
|
||||
return Err(format!("远程路径不是目录: {remote_dir}"));
|
||||
}
|
||||
fs::create_dir_all(local_dir)
|
||||
.map_err(|error| format!("创建本地目录失败 ({}): {error}", local_dir.display()))?;
|
||||
|
||||
let mut entries = sftp
|
||||
.readdir(Path::new(remote_dir))
|
||||
.map_err(|error| format!("遍历远程目录失败 ({remote_dir}): {error}"))?;
|
||||
entries.sort_by_key(|(path, _)| path.file_name().map(|name| name.to_os_string()));
|
||||
|
||||
for (path, entry_stat) in entries {
|
||||
let name = match path.file_name() {
|
||||
Some(value) => value.to_string_lossy().to_string(),
|
||||
None => continue,
|
||||
};
|
||||
if name == "." || name == ".." {
|
||||
continue;
|
||||
}
|
||||
let remote_child = join_remote(remote_dir, &name);
|
||||
let local_child = local_dir.join(&name);
|
||||
if is_dir_mode(entry_stat.perm.unwrap_or(0)) {
|
||||
download_dir(sftp, &remote_child, &local_child, progress)?;
|
||||
} else {
|
||||
download_file(sftp, &remote_child, &local_child, |transferred, total| {
|
||||
progress(transferred, total)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn copy_with_progress(
|
||||
reader: &mut impl Read,
|
||||
writer: &mut impl Write,
|
||||
|
||||
Reference in new issue
Block a user