Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J Tools
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / Server Manager
872 Dateien
core-20261030T220507Z-61e4558d889f
Hinzugefügt: · Erstellt:
1
//! SFTP channels over the shared SSH connection (replaces ssh2 `client.sftp()`).2
//!3
//! Built on `russh_sftp::client::RawSftpSession` so positioned reads/writes can be pipelined4
//! (the Transfer Manager keeps up to 8 requests in flight, like 0.1.0). Errors keep the SFTP5
//! status code so callers can map them exactly like the 0.1.0 `sftpError`/`errorCode` helpers.7
use crate::core::error::{CoreError, CoreResult};8
use crate::core::ssh::session::{not_connected, SessionManager, SessionRecord};9
use russh_sftp::client::error::Error as RawError;10
use russh_sftp::client::RawSftpSession;11
use russh_sftp::protocol::{FileAttributes, OpenFlags, StatusCode};12
use std::sync::Arc;13
use std::time::Duration;15
/// SFTP request timeout (a dead link never answers; the transfer stall detector uses 60 s too).16
const REQUEST_TIMEOUT_SECS: u64 = 60;18
#[derive(Debug, Clone)]19
pub enum SftpError {20
/// SSH_FX_* status code and server message.21
Status(u32, String),22
/// Channel closed / connection lost / timeout.23
Connection(String),24
Other(String),25
}27
impl SftpError {28
pub fn status(&self) -> Option<u32> {29
match self {30
SftpError::Status(code, _) => Some(*code),31
_ => None,32
}33
}35
pub fn message(&self) -> String {36
match self {37
SftpError::Status(_, m) | SftpError::Connection(m) | SftpError::Other(m) => m.clone(),38
}39
}41
/// 0.1.0 `sftpError`: FILE_* codes for the File Manager.42
pub fn file_error(&self, message: &str) -> CoreError {43
let code = match self {44
SftpError::Status(2, _) => "FILE_NOT_FOUND",45
SftpError::Status(3, _) => "FILE_PERMISSION_DENIED",46
SftpError::Status(6 | 7, _) | SftpError::Connection(_) => "SESSION_NOT_CONNECTED",47
SftpError::Status(8, _) => "FILE_OPERATION_UNSUPPORTED",48
_ => "FILE_REMOTE_IO_FAILED",49
};50
let text = if message.is_empty() { self.message() } else { message.to_string() };51
CoreError::new(code, format!("{code}: {text}"))52
}54
/// 0.1.0 Transfer Manager `errorCode`.55
pub fn transfer_code(&self) -> &'static str {56
match self {57
SftpError::Status(6 | 7, _) | SftpError::Connection(_) => "SESSION_NOT_CONNECTED",58
SftpError::Status(3, _) => "FILE_PERMISSION_DENIED",59
SftpError::Status(2, _) => "FILE_NOT_FOUND",60
_ => "TRANSFER_FAILED",61
}62
}63
}65
impl std::fmt::Display for SftpError {66
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {67
f.write_str(&self.message())68
}69
}71
fn convert(error: RawError) -> SftpError {72
match error {73
RawError::Status(status) => SftpError::Status(status_number(status.status_code), status.error_message),74
RawError::Timeout => SftpError::Connection("SFTP request timed out.".into()),75
RawError::UnexpectedBehavior(m) if m.contains("closed") => SftpError::Connection(m),76
RawError::IO(m) => SftpError::Connection(m),77
other => SftpError::Other(other.to_string()),78
}79
}81
fn status_number(code: StatusCode) -> u32 {82
code as u3283
}85
pub type SftpResult<T> = Result<T, SftpError>;87
/// Normalized attributes (ssh2 `Stats` equivalent).88
#[derive(Debug, Clone, Default)]89
pub struct Stat {90
pub size: u64,91
pub mode: u32,92
pub mtime: u32,93
pub uid: Option<u32>,94
pub gid: Option<u32>,95
}97
const S_IFMT: u32 = 0o170000;99
impl Stat {100
fn from(attrs: &FileAttributes) -> Stat {101
Stat { size: attrs.size.unwrap_or(0), mode: attrs.permissions.unwrap_or(0), mtime: attrs.mtime.unwrap_or(0), uid: attrs.uid, gid: attrs.gid }102
}103
pub fn is_dir(&self) -> bool {104
self.mode & S_IFMT == 0o040000105
}106
pub fn is_file(&self) -> bool {107
self.mode & S_IFMT == 0o100000108
}109
pub fn is_symlink(&self) -> bool {110
self.mode & S_IFMT == 0o120000111
}112
}114
#[derive(Debug, Clone)]115
pub struct DirEntry {116
pub name: String,117
pub stat: Stat,118
}120
/// One SFTP subsystem channel. Cheap to share (`Arc`); requests may run concurrently.121
pub struct Sftp {122
raw: RawSftpSession,123
/// Largest data payload per READ/WRITE request (ssh2 rule: OpenSSH 256 KiB packets,124
/// otherwise 34000 bytes; `limits@openssh.com` wins when offered).125
max_read: usize,126
max_write: usize,127
}129
/// Open file handle (closed explicitly; dropping without close leaks only the server handle130
/// until the channel closes).131
#[derive(Debug, Clone)]132
pub struct FileHandle(pub String);134
pub fn mode_attrs(mode: u32) -> FileAttributes {135
let mut attrs = FileAttributes::empty();136
attrs.permissions = Some(mode);137
attrs138
}140
impl Sftp {141
/// `openSftp`: new SFTP channel on the connected session.142
pub async fn open(manager: &SessionManager, session_id: &str) -> CoreResult<Arc<Sftp>> {143
let record = manager.require_connected(session_id)?;144
Self::open_record(&record).await145
}147
pub async fn open_record(record: &Arc<SessionRecord>) -> CoreResult<Arc<Sftp>> {148
let handle = record.handle()?;149
let failed = |e: String| if record.is_connected() { CoreError::new("SFTP_OPEN_FAILED", e) } else { not_connected() };150
let opened = tokio::time::timeout(Duration::from_secs(30), async {151
let channel = handle.channel_open_session().await.map_err(|e| e.to_string())?;152
channel.request_subsystem(true, "sftp").await.map_err(|e| e.to_string())?;153
let raw = RawSftpSession::new(channel.into_stream());154
raw.set_timeout(REQUEST_TIMEOUT_SECS);155
let version = raw.init().await.map_err(|e| e.to_string())?;156
// Large requests only when the server announces its limits; Win32-OpenSSH 8.1157
// (Windows Server 2022) fails 255 KiB writes. Small pieces are sent concurrently.158
let base = 34_000 - 1024;159
let (mut max_read, mut max_write) = (base, base);160
if version.extensions.contains_key("limits@openssh.com") {161
if let Ok(limits) = raw.limits().await {162
let large = 256 * 1024 - 1024;163
max_read = large;164
max_write = large;165
if limits.max_read_len > 0 {166
max_read = max_read.min(limits.max_read_len as usize);167
}168
if limits.max_write_len > 0 {169
max_write = max_write.min(limits.max_write_len as usize);170
}171
}172
}173
Ok::<_, String>((raw, max_read, max_write))174
})175
.await176
.map_err(|_| failed("SFTP subsystem did not answer.".into()))?177
.map_err(failed)?;178
let (raw, max_read, max_write) = opened;179
Ok(Arc::new(Sftp { raw, max_read, max_write }))180
}182
pub fn close(&self) {183
let _ = self.raw.close_session();184
}186
pub async fn stat(&self, path: &str) -> SftpResult<Stat> {187
self.raw.stat(path).await.map(|a| Stat::from(&a.attrs)).map_err(convert)188
}190
pub async fn lstat(&self, path: &str) -> SftpResult<Stat> {191
self.raw.lstat(path).await.map(|a| Stat::from(&a.attrs)).map_err(convert)192
}194
pub async fn exists(&self, path: &str) -> bool {195
self.lstat(path).await.is_ok()196
}198
pub async fn read_dir(&self, path: &str) -> SftpResult<Vec<DirEntry>> {199
let handle = self.raw.opendir(path).await.map_err(convert)?.handle;200
let mut entries = Vec::new();201
let result = loop {202
match self.raw.readdir(handle.as_str()).await {203
Ok(name) => {204
entries.extend(name.files.into_iter().map(|f| DirEntry { stat: Stat::from(&f.attrs), name: f.filename }));205
if entries.len() > 1_000_000 {206
break Err(SftpError::Other("Directory listing is too large.".into()));207
}208
}209
Err(RawError::Status(status)) if status.status_code == StatusCode::Eof => break Ok(()),210
Err(error) => break Err(convert(error)),211
}212
};213
let _ = self.raw.close(handle).await;214
result.map(|_| entries)215
}217
pub async fn mkdir(&self, path: &str, mode: u32) -> SftpResult<()> {218
self.raw.mkdir(path, mode_attrs(mode)).await.map(|_| ()).map_err(convert)219
}221
pub async fn rmdir(&self, path: &str) -> SftpResult<()> {222
self.raw.rmdir(path).await.map(|_| ()).map_err(convert)223
}225
pub async fn unlink(&self, path: &str) -> SftpResult<()> {226
self.raw.remove(path).await.map(|_| ()).map_err(convert)227
}229
pub async fn rename(&self, from: &str, to: &str) -> SftpResult<()> {230
self.raw.rename(from, to).await.map(|_| ()).map_err(convert)231
}233
/// ssh2 flag strings used by 0.1.0: "r", "r+", "wx".234
pub async fn open_file(&self, path: &str, flags: &str, mode: Option<u32>) -> SftpResult<FileHandle> {235
let flags = match flags {236
"r" => OpenFlags::READ,237
"r+" => OpenFlags::READ | OpenFlags::WRITE,238
"wx" => OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::EXCLUDE | OpenFlags::TRUNCATE,239
_ => OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE,240
};241
let attrs = mode.map(mode_attrs).unwrap_or_else(FileAttributes::empty);242
self.raw.open(path, flags, attrs).await.map(|h| FileHandle(h.handle)).map_err(convert)243
}245
pub async fn close_file(&self, handle: &FileHandle) -> SftpResult<()> {246
self.raw.close(handle.0.as_str()).await.map(|_| ()).map_err(convert)247
}249
pub async fn fstat(&self, handle: &FileHandle) -> SftpResult<Stat> {250
self.raw.fstat(handle.0.as_str()).await.map(|a| Stat::from(&a.attrs)).map_err(convert)251
}253
pub async fn truncate(&self, handle: &FileHandle, size: u64) -> SftpResult<()> {254
let mut attrs = FileAttributes::empty();255
attrs.size = Some(size);256
self.raw.fsetstat(handle.0.as_str(), attrs).await.map(|_| ()).map_err(convert)257
}259
pub async fn fsync(&self, handle: &FileHandle) -> SftpResult<()> {260
self.raw.fsync(handle.0.as_str()).await.map(|_| ()).map_err(convert)261
}263
/// Reads up to `len` bytes at `offset`; an empty result means end of file.264
pub async fn read_at(&self, handle: &FileHandle, offset: u64, len: u32) -> SftpResult<Vec<u8>> {265
match self.raw.read(handle.0.as_str(), offset, len).await {266
Ok(data) => Ok(data.data),267
Err(RawError::Status(status)) if status.status_code == StatusCode::Eof => Ok(Vec::new()),268
Err(error) => Err(convert(error)),269
}270
}272
/// Fills `len` bytes from `offset` (servers may answer short reads), stops at EOF.273
pub async fn read_full(&self, handle: &FileHandle, offset: u64, len: usize) -> SftpResult<Vec<u8>> {274
// All pieces in parallel; a short piece (EOF or a server limit) falls back to sequential reads.275
let pieces: Vec<(u64, usize)> = (0..len).step_by(self.max_read.max(1)).map(|start| (offset + start as u64, (len - start).min(self.max_read))).collect();276
let results = futures::future::join_all(pieces.iter().map(|(at, want)| self.read_at(handle, *at, *want as u32))).await;277
let mut out = Vec::with_capacity(len);278
for ((_, want), result) in pieces.iter().zip(results) {279
let data = result?;280
let full = data.len() == *want;281
out.extend_from_slice(&data);282
if !full {283
break;284
}285
}286
while out.len() < len {287
let want = (len - out.len()).min(self.max_read) as u32;288
let chunk = self.read_at(handle, offset + out.len() as u64, want).await?;289
if chunk.is_empty() {290
break;291
}292
out.extend_from_slice(&chunk);293
}294
Ok(out)295
}297
/// Writes `data` at `offset` in protocol-sized pieces.298
pub async fn write_at(&self, handle: &FileHandle, offset: u64, data: &[u8]) -> SftpResult<()> {299
let writes = data.chunks(self.max_write.max(1)).enumerate().map(|(i, piece)| self.raw.write(handle.0.as_str(), offset + (i * self.max_write) as u64, piece.to_vec()));300
for result in futures::future::join_all(writes).await {301
result.map_err(convert)?;302
}303
Ok(())304
}306
/// Whole-file helpers for small files (editor temp copies, probes).307
pub async fn write_new_file(&self, path: &str, data: &[u8], mode: u32) -> SftpResult<()> {308
let handle = self.open_file(path, "wx", Some(mode)).await?;309
let result = self.write_at(&handle, 0, data).await;310
let closed = self.close_file(&handle).await;311
result.and(closed)312
}313
}315
impl Drop for Sftp {316
fn drop(&mut self) {317
let _ = self.raw.close_session();318
}319
}SHA-256
c24d0e563f7646bfb6181662acc2f93df26cf5b737fba7c6c959b72793943878CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df