CH-J Toolsspráva serverů přes SSH
Menu
Vydané zdroje

CH-J Tools

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / Server Manager 872 souborů
core-20261030T220507Z-61e4558d889f Přidáno: · Sestaveno:
sftp.rs Přidáno: Datum není evidováno 12,5 KB · 319 řádků
Kód Stáhnout soubor
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 pipelined
4//! (the Transfer Manager keeps up to 8 requests in flight, like 0.1.0). Errors keep the SFTP
5//! status code so callers can map them exactly like the 0.1.0 `sftpError`/`errorCode` helpers.
7use crate::core::error::{CoreError, CoreResult};
8use crate::core::ssh::session::{not_connected, SessionManager, SessionRecord};
9use russh_sftp::client::error::Error as RawError;
10use russh_sftp::client::RawSftpSession;
11use russh_sftp::protocol::{FileAttributes, OpenFlags, StatusCode};
12use std::sync::Arc;
13use std::time::Duration;
15/// SFTP request timeout (a dead link never answers; the transfer stall detector uses 60 s too).
16const REQUEST_TIMEOUT_SECS: u64 = 60;
18#[derive(Debug, Clone)]
19pub 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),
27impl 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 }
65impl std::fmt::Display for SftpError {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 f.write_str(&self.message())
68 }
71fn 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 }
81fn status_number(code: StatusCode) -> u32 {
82 code as u32
85pub type SftpResult<T> = Result<T, SftpError>;
87/// Normalized attributes (ssh2 `Stats` equivalent).
88#[derive(Debug, Clone, Default)]
89pub struct Stat {
90 pub size: u64,
91 pub mode: u32,
92 pub mtime: u32,
93 pub uid: Option<u32>,
94 pub gid: Option<u32>,
97const S_IFMT: u32 = 0o170000;
99impl 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 == 0o040000
105 }
106 pub fn is_file(&self) -> bool {
107 self.mode & S_IFMT == 0o100000
108 }
109 pub fn is_symlink(&self) -> bool {
110 self.mode & S_IFMT == 0o120000
111 }
114#[derive(Debug, Clone)]
115pub struct DirEntry {
116 pub name: String,
117 pub stat: Stat,
120/// One SFTP subsystem channel. Cheap to share (`Arc`); requests may run concurrently.
121pub 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,
129/// Open file handle (closed explicitly; dropping without close leaks only the server handle
130/// until the channel closes).
131#[derive(Debug, Clone)]
132pub struct FileHandle(pub String);
134pub fn mode_attrs(mode: u32) -> FileAttributes {
135 let mut attrs = FileAttributes::empty();
136 attrs.permissions = Some(mode);
137 attrs
140impl 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).await
145 }
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.1
157 // (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 .await
176 .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 }
315impl Drop for Sftp {
316 fn drop(&mut self) {
317 let _ = self.raw.close_session();
318 }
SHA-256c24d0e563f7646bfb6181662acc2f93df26cf5b737fba7c6c959b72793943878

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu77380d4805485ffa4027fbe7858b5836f197a4aa9d52358a43e2d12d018744df
Soubory