feat: SFTP file transfer progress bar + auto-refresh
- Progress bar: [████████░░░░] 67% 12.3MB/18.5MB - progressWriter wraps io.Writer, reports bytes via callback - Footer shows live progress during upload/download - Auto-refresh destination pane after transfer completes - Success message shown for 1 second before clearing
This commit is contained in:
+67
-10
@@ -425,27 +425,59 @@ func (t *SFTPBrowserTab) Update(msg tea.Msg) (Tab, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (t *SFTPBrowserTab) copyFile(srcType, dstType paneType, srcPath, dstPath, name string) {
|
||||
// Get file size for progress bar
|
||||
var totalSize int64
|
||||
if srcType == paneRemote {
|
||||
t.mu.Lock()
|
||||
client := t.right.sftpClient
|
||||
t.mu.Unlock()
|
||||
if client != nil {
|
||||
if info, err := client.Stat(srcPath); err == nil {
|
||||
totalSize = info.Size()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if info, err := os.Stat(srcPath); err == nil {
|
||||
totalSize = info.Size()
|
||||
}
|
||||
}
|
||||
|
||||
t.mu.Lock()
|
||||
t.transferring = true
|
||||
t.transferMsg = fmt.Sprintf("Copying %s...", name)
|
||||
t.transferMsg = fmt.Sprintf("Copying %s... 0%%", name)
|
||||
t.mu.Unlock()
|
||||
|
||||
progressFn := func(transferred int64) {
|
||||
if totalSize <= 0 {
|
||||
return
|
||||
}
|
||||
pct := int(transferred * 100 / totalSize)
|
||||
barWidth := 20
|
||||
filled := int(float64(barWidth) * float64(transferred) / float64(totalSize))
|
||||
if filled > barWidth {
|
||||
filled = barWidth
|
||||
}
|
||||
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
|
||||
t.mu.Lock()
|
||||
t.transferMsg = fmt.Sprintf("Copying %s... [%s] %d%% %s/%s",
|
||||
name, bar, pct, formatSize(transferred), formatSize(totalSize))
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
var err error
|
||||
if srcType == paneRemote && dstType == paneLocal {
|
||||
// Download
|
||||
t.mu.Lock()
|
||||
client := t.right.sftpClient
|
||||
t.mu.Unlock()
|
||||
if client != nil {
|
||||
err = downloadFile(client, srcPath, dstPath)
|
||||
err = downloadFile(client, srcPath, dstPath, progressFn)
|
||||
}
|
||||
} else if srcType == paneLocal && dstType == paneRemote {
|
||||
// Upload
|
||||
t.mu.Lock()
|
||||
client := t.right.sftpClient
|
||||
t.mu.Unlock()
|
||||
if client != nil {
|
||||
err = uploadFile(client, srcPath, dstPath)
|
||||
err = uploadFile(client, srcPath, dstPath, progressFn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,19 +485,26 @@ func (t *SFTPBrowserTab) copyFile(srcType, dstType paneType, srcPath, dstPath, n
|
||||
if err != nil {
|
||||
t.err = fmt.Errorf("copy %s: %w", name, err)
|
||||
} else {
|
||||
t.transferMsg = ""
|
||||
t.transferMsg = fmt.Sprintf("Done: %s", name)
|
||||
}
|
||||
t.transferring = false
|
||||
t.mu.Unlock()
|
||||
|
||||
// Auto-refresh destination pane
|
||||
if dstType == paneRemote {
|
||||
t.refreshRemote()
|
||||
} else {
|
||||
t.refreshLocal()
|
||||
}
|
||||
|
||||
// Clear success message after brief delay
|
||||
time.Sleep(1 * time.Second)
|
||||
t.mu.Lock()
|
||||
t.transferMsg = ""
|
||||
t.mu.Unlock()
|
||||
}
|
||||
|
||||
func downloadFile(client *sftp.Client, remotePath, localPath string) error {
|
||||
func downloadFile(client *sftp.Client, remotePath, localPath string, progressFn func(int64)) error {
|
||||
src, err := client.Open(remotePath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -478,11 +517,12 @@ func downloadFile(client *sftp.Client, remotePath, localPath string) error {
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
pw := &progressWriter{w: dst, total: 0, fn: progressFn}
|
||||
_, err = io.Copy(pw, src)
|
||||
return err
|
||||
}
|
||||
|
||||
func uploadFile(client *sftp.Client, localPath, remotePath string) error {
|
||||
func uploadFile(client *sftp.Client, localPath, remotePath string, progressFn func(int64)) error {
|
||||
src, err := os.Open(localPath)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -495,10 +535,27 @@ func uploadFile(client *sftp.Client, localPath, remotePath string) error {
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
pw := &progressWriter{w: dst, total: 0, fn: progressFn}
|
||||
_, err = io.Copy(pw, src)
|
||||
return err
|
||||
}
|
||||
|
||||
// progressWriter wraps an io.Writer and reports progress via callback
|
||||
type progressWriter struct {
|
||||
w io.Writer
|
||||
total int64
|
||||
fn func(int64)
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (int, error) {
|
||||
n, err := pw.w.Write(p)
|
||||
pw.total += int64(n)
|
||||
if pw.fn != nil {
|
||||
pw.fn(pw.total)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (t *SFTPBrowserTab) deleteItem(fullPath string, isDir bool, isRemote bool) {
|
||||
t.mu.Lock()
|
||||
t.transferring = true
|
||||
|
||||
Reference in New Issue
Block a user