10
SFTP Dual-Pane dan Editor Remote
Halaman ini membahas panel SFTP dua kolom di aplikasi dan editor teks yang membaca serta menulis berkas langsung ke server. Seluruh IO remote ditangani modul src-tauri/src/sftp/mod.rs lewat satu SftpSession dari russh-sftp per session_id, sementara komponen React di src/components/sftp/ hanya memanggil command Tauri.
Sesi SFTP per session_id
SftpManager menyimpan Mutex<HashMap<String, Arc<SftpConnection>>>, dan setiap SftpConnection memegang session_id, host_id, sftp, serta _handle yang menjaga Handle<SshClientHandler> tetap hidup agar koneksi tidak tertutup src-tauri/src/sftp/mod.rs:27-371. Helper connection meng-clone Arc keluar dari map sebelum IO dimulai; komentar di kode menyebut alasan eksplisitnya, yaitu agar transfer lambat tidak menahan lock manager dan menyerialkan semua panggilan SFTP lain src-tauri/src/sftp/mod.rs:46-552. Bila session_id tidak dikenal, hasilnya error "SFTP session not found. Please connect first." src-tauri/src/sftp/mod.rs:46-552.
connect membuka TcpStream, memanggil client::connect_stream, mengautentikasi password atau private key, membuka channel, lalu request_subsystem(true, "sftp") dan SftpSession::new(channel.into_stream()). Path awal diambil dari canonicalize(".") dan jatuh ke / bila gagal src-tauri/src/sftp/mod.rs:100-1303. Di sisi command, sftp_connect mengambil Host dari database dan menyelesaikan auth lewat resolve_host_auth sebelum meneruskan ke SftpManager::connect src-tauri/src/commands/mod.rs:962-9884.
graph LR
SftpView["SftpView (dual pane)"] --> FilePane["FilePane"]
SftpView --> FileEditorModal["FileEditorModal"]
FilePane --> Api["api.ts invoke"]
FileEditorModal --> Api
Api --> Cmd["commands: sftp_*"]
Cmd --> Mgr["SftpManager.connections"]
Mgr --> Sess["SftpConnection + SftpSession"]
Sess --> Remote["SFTP server"]
style Mgr fill:#336791,color:#fff
style Sess fill:#336791,color:#fff
style Api fill:#f59e0b,color:#000
style FilePane fill:#8b5cf6,color:#fff
style FileEditorModal fill:#8b5cf6,color:#fff
style Remote fill:#2d7d46,color:#fff
style Cmd fill:#2d7d46,color:#fff
%% fig: Lapisan SFTP dari pane sampai server
%% desc: Komponen React memanggil command lewat api.ts, command meneruskan ke SftpManager, dan sesi russh-sftp menyelesaikan IO ke server.Frontend membuat sendiri sessionId berformat sftp-<timestamp>-<random> pada connectRemote, memanggil api.connectSftp(host.id, sessionId), lalu mengambil listing direktori awal dengan api.listSftp src/stores/useSftpStore.ts:558-5895. Pemetaan nama command ke binding invoke ada di api.ts, termasuk sftp_stat_many dan pasangan lokalnya local_stat_many src/lib/api.ts:291-3156.
Daftar isi direktori dan operasi metadata
list membaca read_dir, melewati entri . dan .., lalu membentuk FileEntry berisi name, path, is_dir, size, modified, dan permissions. Hasilnya diurutkan dengan direktori lebih dulu, kemudian alfabetis tanpa membedakan huruf besar-kecil src-tauri/src/sftp/mod.rs:132-1827. Operasi tulis metadata lainnya adalah mkdir, delete (memilih remove_dir atau remove_file berdasarkan is_dir), dan rename src-tauri/src/sftp/mod.rs:184-2138. disconnect hanya menghapus entri dari map tanpa menutup channel secara eksplisit; Arc yang tersisa menentukan kapan koneksi dilepas src-tauri/src/sftp/mod.rs:430-4349.
stat_many melakukan stat per path untuk pemeriksaan konflik transfer. Path yang tidak ada atau gagal di-stat dipetakan ke None, bukan menggagalkan seluruh batch, sehingga satu entri tidak terbaca tidak memblokir transfer src-tauri/src/sftp/mod.rs:218-24010.
Upload dan download berkas
upload_file membaca ukuran berkas lokal, membuka berkas remote dengan OpenFlags::CREATE | TRUNCATE | WRITE, lalu menyalin per chunk 64 * 1024 byte (64 KB). Setiap chunk menaikkan bytes_done, dan on_progress hanya dipanggil bila sudah lewat Duration::from_millis(100); komentar di kode menyebut target sekitar 10 pembaruan per detik agar channel IPC tidak banjir. Di akhir ada flush, shutdown, dan satu panggilan progres penutup on_progress(total_bytes, total_bytes) src-tauri/src/sftp/mod.rs:242-30411.
download_file adalah cerminannya: ukuran total diambil dari metadata remote (jatuh ke 0 bila tidak tersedia), berkas remote dibuka dengan OpenFlags::READ, berkas lokal dibuat lewat tokio::fs::File::create, dan chunk 64 KB ditulis dengan ambang emisi 100 ms yang sama src-tauri/src/sftp/mod.rs:306-36212.
⚠️
upload_filemembuka berkas tujuan denganTRUNCATE. Berkas remote dengan nama sama akan dipotong lebih dulu, jadi pemeriksaan konflik di frontend adalah satu-satunya pengaman sebelum isi lama hilang src-tauri/src/sftp/mod.rs:260-26713.
Editor teks remote
read_text_file menolak berkas di atas 5 * 1024 * 1024 byte dengan pesan "File is too large to open in the inline editor (max 5 MB)", lalu membaca seluruh isi dan mengubahnya ke String. Isi biner ditolak dengan pesan "File contains binary data and cannot be displayed as plain text" src-tauri/src/sftp/mod.rs:364-39614. write_text_file membuka berkas dengan CREATE | TRUNCATE | WRITE, menulis seluruh content, lalu flush dan shutdown src-tauri/src/sftp/mod.rs:398-42815.
FileEditorModal memuat isi lewat api.readSftpFile(sessionId, file.path) untuk berkas remote atau api.readLocalFile untuk berkas lokal, dan menyimpan salinan awal di originalContent src/components/sftp/FileEditorModal.tsx:31-6616. Status isDirty dihitung dari content !== originalContent; handleSave memanggil api.writeSftpFile atau api.writeLocalFile, mengubah originalContent, menyalakan saveSuccess selama 2500 ms, dan memicu onSaved src/components/sftp/FileEditorModal.tsx:68-9017. Menutup editor saat isDirty memunculkan confirm("You have unsaved changes. Are you sure you want to close?") src/components/sftp/FileEditorModal.tsx:117-12418.
stateDiagram-v2
[*] --> loading
loading --> ready: readSftpFile / readLocalFile
loading --> error: String(e)
ready --> dirty: content != originalContent
dirty --> saving: handleSave (Ctrl+S)
saving --> saved: writeSftpFile sukses
saving --> error: String(e)
saved --> ready: setTimeout 2500 ms
error --> saving: percobaan ulang
style loading fill:#f59e0b,color:#000
style ready fill:#2d7d46,color:#fff
style dirty fill:#8b5cf6,color:#fff
style saved fill:#2d7d46,color:#fff
style error fill:#dc2626,color:#fff
style saving fill:#336791,color:#fff
%% fig: Siklus keadaan FileEditorModal
%% desc: Modal memuat isi berkas, menandai perubahan yang belum disimpan, lalu berpindah ke saved atau error setelah handleSave.Sisi lokal dan batas angka
Pane kiri memakai helper lokal di modul yang sama: list_local_directory menyusun FileEntry dengan permissions: None dan urutan direktori-dulu yang identik dengan versi remote src-tauri/src/sftp/mod.rs:438-47619, sedangkan read_local_text_file memakai batas 5 MB yang sama dan write_local_text_file menulis lewat fs::write src-tauri/src/sftp/mod.rs:510-51620. Command local_stat_many, local_home_dir, local_list, local_read_file, dan local_write_file membungkus helper itu; yang menyentuh disk dijalankan lewat run_blocking agar tidak menahan thread utama src-tauri/src/commands/mod.rs:1126-113021 dan src-tauri/src/commands/mod.rs:1146-116822.
| Nilai | Lokasi | Arti |
|---|---|---|
64 * 1024 | upload_file, download_file | ukuran buffer chunk 64 KB |
Duration::from_millis(100) | upload_file, download_file | jeda minimum antar event progres |
5 * 1024 * 1024 | read_text_file, read_local_text_file | batas berkas untuk editor inline |
MAX_TRAVERSAL_DEPTH = 32 | useSftpStore | kedalaman maksimum penelusuran plan transfer |
ROW_HEIGHT = 30, OVERSCAN_ROWS = 8 | FilePane | ukuran baris dan overscan virtual window |
Math.min(lineCount, 1000) | FileEditorModal | jumlah nomor baris yang dirender |
Sumber baris konstanta transfer ada di src-tauri/src/sftp/mod.rs:271-27423 dan src-tauri/src/sftp/mod.rs:335-33624, sedangkan kedalaman penelusuran didefinisikan di src/stores/useSftpStore.ts:85-15025 dan geometri baris di src/components/sftp/FilePane.tsx:364-37226.
Pelaporan progres sftp-transfer-progress
Command sftp_upload dan sftp_download membungkus upload_file/download_file dengan closure yang memancarkan event sftp-transfer-progress berisi TransferProgressEvent src-tauri/src/commands/mod.rs:1028-103727 dan src-tauri/src/commands/mod.rs:1039-108528. Struct itu memakai #[serde(rename_all = "camelCase")], sehingga frontend menerima sessionId, path, bytesDone, dan bytesTotal src-tauri/src/commands/mod.rs:1028-103727.
| Field event | Tipe | Isi |
|---|---|---|
sessionId | string | session_id sesi SFTP yang memancarkan progres |
path | string | local_path untuk upload, remote_path untuk download |
bytesDone | number | byte yang sudah dipindahkan |
bytesTotal | number | ukuran total berkas yang sedang dipindah |
Di frontend, listener didaftarkan sekali di level modul store dan meneruskan payload ke applyTransferBytes src/stores/useSftpStore.ts:742-74629. Fungsi itu mengabaikan event dari sesi lain, mengabaikan event untuk berkas yang bukan currentFile, lalu memakai Math.max(prog.currentFile.size, event.bytesTotal) karena stat langsung dianggap lebih baru daripada ukuran dari listing src/stores/useSftpStore.ts:719-73830.
Plan transfer, konflik, dan dual pane
buildTransferPlan menelusuri pohon yang dipilih secara rekursif untuk arah upload (memakai api.listLocal) atau download (memakai api.listSftp), mencatat direktori dan berkas ke daftar datar, dan menyimpan path yang sudah dikunjungi agar dua root yang tumpang tindih tidak menulis tujuan yang sama dua kali src/stores/useSftpStore.ts:85-15025. runTransfer lalu memanggil resolveTransferConflicts sebelum menghitung total byte, sehingga bilah progres hanya menghitung berkas yang benar-benar akan ditulis src/stores/useSftpStore.ts:329-34231. Berkas dipindahkan berurutan; setiap berkas mengatur currentFile di store, memanggil api.uploadSftp atau api.downloadSftp, dan menambah doneCount serta completedBytes src/stores/useSftpStore.ts:378-40232.
Urutan lengkap satu upload, termasuk pelaporan progres per chunk, tampak pada diagram berikut.
sequenceDiagram
participant U as User
participant P as FilePane
participant St as useSftpStore
participant C as commands::sftp_upload
participant M as SftpManager
participant S as SFTP server
participant O as TransferProgressOverlay
U->>P: klik ArrowRight (transferLocal)
P->>St: uploadSelected(entries)
St->>St: buildTransferPlan + resolveTransferConflicts
loop setiap berkas (berurutan)
St->>C: invoke sftp_upload
C->>M: upload_file(local_path, remote_path)
M->>S: open_with_flags CREATE|TRUNCATE|WRITE
loop chunk 64 KB
M->>S: write_all
M-->>C: on_progress bila sudah 100 ms
C-->>St: emit sftp-transfer-progress
St-->>O: applyTransferBytes
end
M->>S: flush lalu shutdown
end
St->>P: navigateRemote(remotePath)
%% fig: Alur upload beserta pelaporan progres byte
%% desc: Store menyusun plan lalu command sftp_upload meneruskan ke SftpManager; setiap chunk memicu event sftp-transfer-progress yang memperbarui overlay.Tampilan dua kolom dirakit SftpView: FilePane kiri berjudul Local Machine, dua tombol tengah untuk upload dan download, dan FilePane kanan berlabel host remote. Tombol upload dinonaktifkan bila tidak ada berkas lokal terpilih, remoteSessionId kosong, atau transferring bernilai benar src/components/sftp/SftpView.tsx:222-29333. Klik ganda pada baris memanggil onNavigate untuk direktori dan onOpenFile untuk berkas, sehingga berkas langsung dibuka di FileEditorModal src/components/sftp/FilePane.tsx:337-34634. Modal editor dipasang di akhir SftpView dengan sessionId sesi remote dan onSaved yang menyegarkan pane asal src/components/sftp/SftpView.tsx:298-31335.
⚠️
local_deleteuntuk folder memakaistd::fs::remove_dir_all, jadi satu konfirmasi yang salah bisa menghapus seluruh isi direktori lokal tanpa jalan kembali src-tauri/src/commands/mod.rs:1178-118836.