perf: skip plan phases for copy/delete, remove size tracking, show file count only
- Delete: skip remoteDeletePlanCmd and trashPlanCmd, show dialog immediately - Copy: skip copyPlanCmd and remoteCopyPlanCmd, show dialog immediately - CopyStats: no lstat per file, count files via WalkDir only - Copy: two-phase (count first, then copy with known total + progress bar) - Progress: file-based ratio, remove Size/Speed display - Stage: Counting files... → Coping files... (no empty stage)
This commit is contained in:
parent
bd157b41b0
commit
8352441bda
2 changed files with 145 additions and 102 deletions
|
|
@ -33,6 +33,16 @@ type copyProgressState struct {
|
||||||
stats TransferStats
|
stats TransferStats
|
||||||
callback func(CopyProgress)
|
callback func(CopyProgress)
|
||||||
lastEmit time.Time
|
lastEmit time.Time
|
||||||
|
stage string
|
||||||
|
discover bool // if true, count files during copy for progress total
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *copyProgressState) discoverFiles(count int, dirPath string) {
|
||||||
|
if count == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.stats.FilesTotal += count
|
||||||
|
s.emit(dirPath, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyPath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
func CopyPath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
||||||
|
|
@ -73,15 +83,15 @@ func CopyPathWithProgressContext(ctx context.Context, srcPath string, dstDir str
|
||||||
if progress == nil {
|
if progress == nil {
|
||||||
progress = func(CopyProgress) {}
|
progress = func(CopyProgress) {}
|
||||||
}
|
}
|
||||||
if stats.FilesTotal == 0 && stats.BytesTotal == 0 {
|
tracker := copyProgressState{
|
||||||
resolved, err := CopyStats(srcPath)
|
ctx: ctx,
|
||||||
if err != nil {
|
stats: stats,
|
||||||
return "", err
|
callback: progress,
|
||||||
}
|
stage: "Scanning files...",
|
||||||
stats = resolved
|
discover: stats.FilesTotal == 0,
|
||||||
}
|
}
|
||||||
tracker := copyProgressState{ctx: ctx, stats: stats, callback: progress}
|
|
||||||
tracker.emit(srcPath, true)
|
tracker.emit(srcPath, true)
|
||||||
|
tracker.stage = "Copying files..."
|
||||||
|
|
||||||
cleanupOnErr := func(copyErr error) (string, error) {
|
cleanupOnErr := func(copyErr error) (string, error) {
|
||||||
if copyErr != nil {
|
if copyErr != nil {
|
||||||
|
|
@ -144,16 +154,7 @@ func CopyStats(srcPath string) (TransferStats, error) {
|
||||||
if d.IsDir() {
|
if d.IsDir() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Lstat(current)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.FilesTotal++
|
stats.FilesTotal++
|
||||||
if info.Mode()&os.ModeSymlink == 0 {
|
|
||||||
stats.BytesTotal += info.Size()
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -204,14 +205,6 @@ func MovePathWithProgressContext(ctx context.Context, srcPath string, dstDir str
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if stats.FilesTotal == 0 && stats.BytesTotal == 0 {
|
|
||||||
resolved, err := CopyStats(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
stats = resolved
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Rename(srcPath, targetPath); err == nil {
|
if err := os.Rename(srcPath, targetPath); err == nil {
|
||||||
progress(CopyProgress{
|
progress(CopyProgress{
|
||||||
FilesDone: stats.FilesTotal,
|
FilesDone: stats.FilesTotal,
|
||||||
|
|
@ -373,6 +366,17 @@ func copyDir(srcDir string, dstDir string, tracker *copyProgressState) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Count files in this directory so progress total converges
|
||||||
|
if tracker != nil && tracker.discover {
|
||||||
|
fileCount := 0
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
fileCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracker.discoverFiles(fileCount, srcDir)
|
||||||
|
}
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if tracker != nil && tracker.ctx != nil {
|
if tracker != nil && tracker.ctx != nil {
|
||||||
if err := tracker.ctx.Err(); err != nil {
|
if err := tracker.ctx.Err(); err != nil {
|
||||||
|
|
@ -496,13 +500,17 @@ func (s *copyProgressState) emit(currentPath string, force bool) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.lastEmit = time.Now()
|
s.lastEmit = time.Now()
|
||||||
|
stage := s.stage
|
||||||
|
if stage == "" {
|
||||||
|
stage = "Transferring data"
|
||||||
|
}
|
||||||
s.callback(CopyProgress{
|
s.callback(CopyProgress{
|
||||||
FilesDone: s.filesDone,
|
FilesDone: s.filesDone,
|
||||||
FilesTotal: s.stats.FilesTotal,
|
FilesTotal: s.stats.FilesTotal,
|
||||||
BytesDone: s.bytesDone,
|
BytesDone: s.bytesDone,
|
||||||
BytesTotal: s.stats.BytesTotal,
|
BytesTotal: s.stats.BytesTotal,
|
||||||
CurrentPath: currentPath,
|
CurrentPath: currentPath,
|
||||||
Stage: "Transferring data",
|
Stage: stage,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2355,22 +2355,7 @@ func (m *Model) handleTransfer(kind fileOpKind) (tea.Model, tea.Cmd) {
|
||||||
log.Printf("[ACTION] Transfer: kind=%d active=%s srcPath=%s dstPath=%s srcRemote=%v dstRemote=%v sources=%v",
|
log.Printf("[ACTION] Transfer: kind=%d active=%s srcPath=%s dstPath=%s srcRemote=%v dstRemote=%v sources=%v",
|
||||||
kind, m.active, srcPane.Path, dstPane.Path, srcHasRemote, dstHasRemote, sources)
|
kind, m.active, srcPane.Path, dstPane.Path, srcHasRemote, dstHasRemote, sources)
|
||||||
|
|
||||||
// Remote-involved transfer (Local↔Remote or Remote→Remote)
|
// Check for existing targets (fast — one Stat per top-level item)
|
||||||
if srcHasRemote || dstHasRemote {
|
|
||||||
if m.copyJob != nil {
|
|
||||||
log.Printf("[SKIP] Transfer: already running (remote)")
|
|
||||||
m.status = "Transfer is already running"
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("[ACTION] Transfer: remote copyPlan — targetDir=%s", targetDir)
|
|
||||||
m.busy = true
|
|
||||||
m.status = fmt.Sprintf("Calculating %s size", operationVerb(kind))
|
|
||||||
return m, m.remoteCopyPlanCmd(kind, sources, targetDir,
|
|
||||||
srcHasRemote, dstHasRemote, srcRemote.Client, dstRemote.Client)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Local-only transfer (existing logic)
|
|
||||||
existingTargets := 0
|
existingTargets := 0
|
||||||
for _, sourcePath := range sources {
|
for _, sourcePath := range sources {
|
||||||
targetPath := filepath.Join(targetDir, filepath.Base(sourcePath))
|
targetPath := filepath.Join(targetDir, filepath.Base(sourcePath))
|
||||||
|
|
@ -2384,23 +2369,45 @@ func (m *Model) handleTransfer(kind fileOpKind) (tea.Model, tea.Cmd) {
|
||||||
existingTargets++
|
existingTargets++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
overwrite := existingTargets > 0
|
||||||
if kind == opCopy || kind == opMove {
|
if existingTargets > 0 && !m.cfg.Behavior.ConfirmOverwrite {
|
||||||
if m.copyJob != nil {
|
overwrite = true
|
||||||
log.Printf("[SKIP] Transfer: already running (local)")
|
|
||||||
m.status = "Transfer is already running"
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
overwrite := existingTargets > 0
|
|
||||||
if existingTargets > 0 && !m.cfg.Behavior.ConfirmOverwrite {
|
|
||||||
overwrite = true
|
|
||||||
}
|
|
||||||
log.Printf("[ACTION] Transfer: local copyPlan — targetDir=%s existingTargets=%d overwrite=%v", targetDir, existingTargets, overwrite)
|
|
||||||
m.busy = true
|
|
||||||
m.status = fmt.Sprintf("Calculating %s size", operationVerb(kind))
|
|
||||||
return m, copyPlanCmd(kind, sources, targetDir, overwrite, existingTargets)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show confirm dialog immediately (no plan — skip walking & counting files)
|
||||||
|
verb := strings.Title(operationVerb(kind))
|
||||||
|
title := fmt.Sprintf("%s selected entry?", verb)
|
||||||
|
if srcHasRemote || dstHasRemote {
|
||||||
|
title = fmt.Sprintf("%s selected entry via SFTP?", verb)
|
||||||
|
}
|
||||||
|
bodyLines := []string{
|
||||||
|
fmt.Sprintf("Items: %d", len(sources)),
|
||||||
|
fmt.Sprintf("Source: %s", srcPane.Path),
|
||||||
|
fmt.Sprintf("Target: %s", targetDir),
|
||||||
|
}
|
||||||
|
if existingTargets > 0 {
|
||||||
|
bodyLines = append(bodyLines, fmt.Sprintf("Overwrite: %d existing target(s)", existingTargets))
|
||||||
|
}
|
||||||
|
if srcHasRemote || dstHasRemote {
|
||||||
|
bodyLines = append(bodyLines, "")
|
||||||
|
if srcHasRemote {
|
||||||
|
hostName := srcRemote.Host.Name
|
||||||
|
bodyLines = append(bodyLines, fmt.Sprintf("Source host: %s", hostName))
|
||||||
|
}
|
||||||
|
if dstHasRemote {
|
||||||
|
hostName := dstRemote.Host.Name
|
||||||
|
bodyLines = append(bodyLines, fmt.Sprintf("Target host: %s", hostName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.openConfirmModal(title, strings.Join(bodyLines, "\n"), "confirm-actions", pendingOperation{
|
||||||
|
kind: kind,
|
||||||
|
sourcePaths: append([]string(nil), sources...),
|
||||||
|
targetDir: targetDir,
|
||||||
|
overwrite: overwrite,
|
||||||
|
existingTargets: existingTargets,
|
||||||
|
srcClient: srcRemote.Client,
|
||||||
|
dstClient: dstRemote.Client,
|
||||||
|
})
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2444,35 +2451,34 @@ func (m *Model) handleDelete() (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[ACTION] Delete: sources=%v remote=%v", sources, m.activePane().InRemote())
|
log.Printf("[ACTION] Delete: sources=%v remote=%v", sources, m.activePane().InRemote())
|
||||||
|
m.busy = false
|
||||||
|
|
||||||
// Remote delete via SFTP (no trash on remote — always permanent)
|
// Remote delete via SFTP (no trash on remote — always permanent)
|
||||||
if mount, ok := m.activePane().CurrentRemote(); ok {
|
if mount, ok := m.activePane().CurrentRemote(); ok {
|
||||||
log.Printf("[ACTION] Delete: remote — sources=%v", sources)
|
log.Printf("[ACTION] Delete: remote — sources=%v", sources)
|
||||||
m.busy = true
|
title := "Delete selected entr" + pluralSuffix(len(sources), "y", "ies") + " from remote?"
|
||||||
m.status = "Calculating delete size"
|
body := fmt.Sprintf("Items: %d", len(sources))
|
||||||
return m, m.remoteDeletePlanCmd(sources, mount.Client)
|
note := "Enter / y to confirm, Esc / n to cancel"
|
||||||
|
m.openConfirmModal(title, body, note, pendingOperation{
|
||||||
|
kind: opDelete,
|
||||||
|
sourcePaths: append([]string(nil), sources...),
|
||||||
|
srcClient: mount.Client,
|
||||||
|
})
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to permanent delete
|
// Default to permanent delete
|
||||||
m.deleteKind = "permanent"
|
m.deleteKind = "permanent"
|
||||||
|
|
||||||
if !m.cfg.Behavior.ConfirmDelete {
|
// Show confirm dialog with mode toggle (no plan — skip counting files)
|
||||||
// No plan needed — show a simple confirm dialog with mode toggle
|
title := "Permanently delete selected entr" + pluralSuffix(len(sources), "y", "ies") + "?"
|
||||||
title := "Permanently delete selected entr" + pluralSuffix(len(sources), "y", "ies") + "?"
|
body := fmt.Sprintf("Items: %d", len(sources))
|
||||||
body := fmt.Sprintf("Items: %d", len(sources))
|
note := fmt.Sprintf("Mode: %s (D/d to change)\nEnter / y to confirm, Esc / n to cancel", m.deleteKind)
|
||||||
note := fmt.Sprintf("Mode: %s (D/d to change)\nEnter / y to confirm, Esc / n to cancel", m.deleteKind)
|
m.openConfirmModal(title, body, note, pendingOperation{
|
||||||
pending := pendingOperation{
|
kind: opDelete,
|
||||||
kind: opPermanentDelete,
|
sourcePaths: append([]string(nil), sources...),
|
||||||
sourcePaths: append([]string(nil), sources...),
|
})
|
||||||
}
|
return m, nil
|
||||||
m.openConfirmModal(title, body, note, pending)
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute plan first, then show confirm modal with mode toggle
|
|
||||||
m.busy = true
|
|
||||||
m.status = "Calculating delete size"
|
|
||||||
return m, trashPlanCmd(sources)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleUnpack() (tea.Model, tea.Cmd) {
|
func (m *Model) handleUnpack() (tea.Model, tea.Cmd) {
|
||||||
|
|
@ -4318,26 +4324,33 @@ func renderCopyProgressModal(job copyJobState, palette theme.Palette, width int)
|
||||||
BorderBackground(palette.Panel)
|
BorderBackground(palette.Panel)
|
||||||
|
|
||||||
progress := job.progress
|
progress := job.progress
|
||||||
|
filesLabel := fmt.Sprintf("%d / ?", progress.FilesDone)
|
||||||
|
if progress.FilesTotal > 0 {
|
||||||
|
filesLabel = fmt.Sprintf("%d / %d", progress.FilesDone, progress.FilesTotal)
|
||||||
|
}
|
||||||
|
|
||||||
ratio := 0.0
|
ratio := 0.0
|
||||||
if progress.BytesTotal > 0 {
|
if progress.FilesTotal > 0 {
|
||||||
ratio = float64(progress.BytesDone) / float64(progress.BytesTotal)
|
ratio = float64(progress.FilesDone) / float64(progress.FilesTotal)
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := []string{
|
lines := []string{
|
||||||
titleStyle.Render(progressTitle(job.kind)),
|
titleStyle.Render(progressTitle(job.kind)),
|
||||||
spacer,
|
spacer,
|
||||||
renderProgressBarLine(ratio, contentWidth, palette),
|
|
||||||
spacer,
|
|
||||||
renderProgressPercentLine(ratio, contentWidth, palette),
|
|
||||||
renderProgressStatLine("Stage:", progressStageLabel(progress, job.kind), contentWidth, palette),
|
renderProgressStatLine("Stage:", progressStageLabel(progress, job.kind), contentWidth, palette),
|
||||||
renderProgressStatLine("Target:", job.targetDir, contentWidth, palette),
|
renderProgressStatLine("Target:", job.targetDir, contentWidth, palette),
|
||||||
spacer,
|
spacer,
|
||||||
renderProgressStatLine("Files:", fmt.Sprintf("%d / %d", progress.FilesDone, progress.FilesTotal), contentWidth, palette),
|
renderProgressStatLine("Files:", filesLabel, contentWidth, palette),
|
||||||
renderProgressStatLine("Size:", fmt.Sprintf("%s / %s", formatSize(progress.BytesDone, true), formatSize(progress.BytesTotal, true)), contentWidth, palette),
|
|
||||||
renderProgressStatLine("Speed:", transferSpeed(progress.BytesDone, job.startedAt), contentWidth, palette),
|
|
||||||
spacer,
|
|
||||||
renderModalNoteLine("Background / b, Cancel / c", contentWidth, palette, mutedStyle),
|
|
||||||
}
|
}
|
||||||
|
if progress.FilesTotal > 0 {
|
||||||
|
barAndPct := []string{
|
||||||
|
spacer,
|
||||||
|
renderProgressBarLine(ratio, contentWidth, palette),
|
||||||
|
renderProgressPercentLine(ratio, contentWidth, palette),
|
||||||
|
}
|
||||||
|
lines = append(lines, barAndPct...)
|
||||||
|
}
|
||||||
|
lines = append(lines, spacer, renderModalNoteLine("Cancel / c", contentWidth, palette, mutedStyle))
|
||||||
if job.background {
|
if job.background {
|
||||||
lines = append(lines, mutedStyle.Render("Transfer continues in background"))
|
lines = append(lines, mutedStyle.Render("Transfer continues in background"))
|
||||||
}
|
}
|
||||||
|
|
@ -4932,6 +4945,7 @@ func (m *Model) startCopyJob(kind fileOpKind, sourcePaths []string, targetDir st
|
||||||
BytesDone: 0,
|
BytesDone: 0,
|
||||||
BytesTotal: stats.BytesTotal,
|
BytesTotal: stats.BytesTotal,
|
||||||
CurrentPath: sourcePaths[0],
|
CurrentPath: sourcePaths[0],
|
||||||
|
Stage: "Counting files...",
|
||||||
},
|
},
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
startedAt: time.Now(),
|
startedAt: time.Now(),
|
||||||
|
|
@ -4942,30 +4956,50 @@ func (m *Model) startCopyJob(kind fileOpKind, sourcePaths []string, targetDir st
|
||||||
return tea.Batch(
|
return tea.Batch(
|
||||||
func() tea.Msg {
|
func() tea.Msg {
|
||||||
go func() {
|
go func() {
|
||||||
|
var statErr error
|
||||||
doneFiles := 0
|
doneFiles := 0
|
||||||
var doneBytes int64
|
totalFiles := 0
|
||||||
for _, sourcePath := range sourcePaths {
|
|
||||||
entryStats, statErr := vfs.CopyStats(sourcePath)
|
// Phase 1: count files across all sources
|
||||||
if statErr != nil {
|
entriesStats := make([]vfs.TransferStats, len(sourcePaths))
|
||||||
|
for i, sourcePath := range sourcePaths {
|
||||||
|
entryStats, err := vfs.CopyStats(sourcePath)
|
||||||
|
if err != nil {
|
||||||
m.copyProgress <- copyDoneMsg{
|
m.copyProgress <- copyDoneMsg{
|
||||||
jobID: jobID,
|
jobID: jobID,
|
||||||
kind: kind,
|
kind: kind,
|
||||||
sourcePaths: append([]string(nil), sourcePaths...),
|
sourcePaths: append([]string(nil), sourcePaths...),
|
||||||
targetDir: targetDir,
|
targetDir: targetDir,
|
||||||
err: statErr,
|
err: err,
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
entriesStats[i] = entryStats
|
||||||
|
totalFiles += entryStats.FilesTotal
|
||||||
|
m.copyProgress <- copyProgressMsg{
|
||||||
|
jobID: jobID,
|
||||||
|
progress: vfs.CopyProgress{
|
||||||
|
FilesDone: 0,
|
||||||
|
FilesTotal: totalFiles,
|
||||||
|
CurrentPath: sourcePath,
|
||||||
|
Stage: fmt.Sprintf("Counting files... %d found", totalFiles),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 2: copy with known total
|
||||||
|
for i, sourcePath := range sourcePaths {
|
||||||
|
entryStats := entriesStats[i]
|
||||||
progressFn := func(progress vfs.CopyProgress) {
|
progressFn := func(progress vfs.CopyProgress) {
|
||||||
m.copyProgress <- copyProgressMsg{
|
m.copyProgress <- copyProgressMsg{
|
||||||
jobID: jobID,
|
jobID: jobID,
|
||||||
progress: vfs.CopyProgress{
|
progress: vfs.CopyProgress{
|
||||||
FilesDone: doneFiles + progress.FilesDone,
|
FilesDone: doneFiles + progress.FilesDone,
|
||||||
FilesTotal: stats.FilesTotal,
|
FilesTotal: totalFiles,
|
||||||
BytesDone: doneBytes + progress.BytesDone,
|
BytesDone: 0,
|
||||||
BytesTotal: stats.BytesTotal,
|
BytesTotal: 0,
|
||||||
CurrentPath: progress.CurrentPath,
|
CurrentPath: progress.CurrentPath,
|
||||||
Stage: progress.Stage,
|
Stage: "Copying files...",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4986,7 +5020,6 @@ func (m *Model) startCopyJob(kind fileOpKind, sourcePaths []string, targetDir st
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
doneFiles += entryStats.FilesTotal
|
doneFiles += entryStats.FilesTotal
|
||||||
doneBytes += entryStats.BytesTotal
|
|
||||||
}
|
}
|
||||||
m.copyProgress <- copyDoneMsg{
|
m.copyProgress <- copyDoneMsg{
|
||||||
jobID: jobID,
|
jobID: jobID,
|
||||||
|
|
@ -5440,13 +5473,14 @@ func formatSize(size int64, human bool) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatCopyStatus(kind fileOpKind, progress vfs.CopyProgress) string {
|
func formatCopyStatus(kind fileOpKind, progress vfs.CopyProgress) string {
|
||||||
|
filesLabel := fmt.Sprintf("%d/?", progress.FilesDone)
|
||||||
|
if progress.FilesTotal > 0 {
|
||||||
|
filesLabel = fmt.Sprintf("%d/%d", progress.FilesDone, progress.FilesTotal)
|
||||||
|
}
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"%s in background: %d/%d files, %s/%s",
|
"%s in background: %s files",
|
||||||
strings.Title(operationVerb(kind)),
|
strings.Title(operationVerb(kind)),
|
||||||
progress.FilesDone,
|
filesLabel,
|
||||||
progress.FilesTotal,
|
|
||||||
formatSize(progress.BytesDone, true),
|
|
||||||
formatSize(progress.BytesTotal, true),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -6263,6 +6297,7 @@ func (m *Model) startRemoteCopyJob(kind fileOpKind, sources []string, targetDir
|
||||||
BytesDone: 0,
|
BytesDone: 0,
|
||||||
BytesTotal: stats.BytesTotal,
|
BytesTotal: stats.BytesTotal,
|
||||||
CurrentPath: sources[0],
|
CurrentPath: sources[0],
|
||||||
|
Stage: "Counting files...",
|
||||||
},
|
},
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
startedAt: time.Now(),
|
startedAt: time.Now(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue