fix: add cancel button support for extraction; feat: delete progress dialog
- ExtractArchiveToDir now accepts context.Context for cancellation - extractZipArchive/extractTarArchive check ctx.Done() in extraction loops - startExtractJob creates context with cancel (C/c now works for extraction) - Added startDeleteJob method with per-file progress reporting - Local delete (trash/permanent) now shows progress dialog with B/b and C/c - renderArchiveProgressModal handles 'delete' kind (file-based progress, no speed) - archiveDoneMsg handles 'delete' completion (reload panes, clear marks)
This commit is contained in:
parent
42c51f0ef5
commit
3b9eb4afa5
2 changed files with 184 additions and 26 deletions
|
|
@ -26,18 +26,21 @@ func ExtractArchiveToTemp(sourcePath string) (string, error) {
|
||||||
return "", extractErr
|
return "", extractErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use background context for temp extraction (no cancellation needed)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
sourceLower := strings.ToLower(sourcePath)
|
sourceLower := strings.ToLower(sourcePath)
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(sourceLower, ".zip"):
|
case strings.HasSuffix(sourceLower, ".zip"):
|
||||||
if err := extractZipArchive(sourcePath, tempDir, nil, totalFiles, totalBytes); err != nil {
|
if err := extractZipArchive(ctx, sourcePath, tempDir, nil, totalFiles, totalBytes); err != nil {
|
||||||
return cleanupOnErr(err)
|
return cleanupOnErr(err)
|
||||||
}
|
}
|
||||||
case strings.HasSuffix(sourceLower, ".tar"):
|
case strings.HasSuffix(sourceLower, ".tar"):
|
||||||
if err := extractTarArchive(sourcePath, tempDir, false, nil, totalFiles, totalBytes); err != nil {
|
if err := extractTarArchive(ctx, sourcePath, tempDir, false, nil, totalFiles, totalBytes); err != nil {
|
||||||
return cleanupOnErr(err)
|
return cleanupOnErr(err)
|
||||||
}
|
}
|
||||||
case strings.HasSuffix(sourceLower, ".tar.gz"), strings.HasSuffix(sourceLower, ".tgz"):
|
case strings.HasSuffix(sourceLower, ".tar.gz"), strings.HasSuffix(sourceLower, ".tgz"):
|
||||||
if err := extractTarArchive(sourcePath, tempDir, true, nil, totalFiles, totalBytes); err != nil {
|
if err := extractTarArchive(ctx, sourcePath, tempDir, true, nil, totalFiles, totalBytes); err != nil {
|
||||||
return cleanupOnErr(err)
|
return cleanupOnErr(err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
@ -50,17 +53,17 @@ func ExtractArchiveToTemp(sourcePath string) (string, error) {
|
||||||
// ExtractArchiveToDir extracts an archive to the specified target directory.
|
// ExtractArchiveToDir extracts an archive to the specified target directory.
|
||||||
// Unlike ExtractArchiveToTemp, it extracts directly to targetDir without
|
// Unlike ExtractArchiveToTemp, it extracts directly to targetDir without
|
||||||
// creating a temporary directory. The progress callback is called after each
|
// creating a temporary directory. The progress callback is called after each
|
||||||
// file is extracted; it may be nil.
|
// file is extracted; it may be nil. Cancellation is supported via ctx.
|
||||||
func ExtractArchiveToDir(sourcePath, targetDir string, progress func(CopyProgress)) error {
|
func ExtractArchiveToDir(ctx context.Context, sourcePath, targetDir string, progress func(CopyProgress)) error {
|
||||||
totalFiles, totalBytes := countArchiveEntries(sourcePath)
|
totalFiles, totalBytes := countArchiveEntries(sourcePath)
|
||||||
sourceLower := strings.ToLower(sourcePath)
|
sourceLower := strings.ToLower(sourcePath)
|
||||||
switch {
|
switch {
|
||||||
case strings.HasSuffix(sourceLower, ".zip"):
|
case strings.HasSuffix(sourceLower, ".zip"):
|
||||||
return extractZipArchive(sourcePath, targetDir, progress, totalFiles, totalBytes)
|
return extractZipArchive(ctx, sourcePath, targetDir, progress, totalFiles, totalBytes)
|
||||||
case strings.HasSuffix(sourceLower, ".tar"):
|
case strings.HasSuffix(sourceLower, ".tar"):
|
||||||
return extractTarArchive(sourcePath, targetDir, false, progress, totalFiles, totalBytes)
|
return extractTarArchive(ctx, sourcePath, targetDir, false, progress, totalFiles, totalBytes)
|
||||||
case strings.HasSuffix(sourceLower, ".tar.gz"), strings.HasSuffix(sourceLower, ".tgz"):
|
case strings.HasSuffix(sourceLower, ".tar.gz"), strings.HasSuffix(sourceLower, ".tgz"):
|
||||||
return extractTarArchive(sourcePath, targetDir, true, progress, totalFiles, totalBytes)
|
return extractTarArchive(ctx, sourcePath, targetDir, true, progress, totalFiles, totalBytes)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("archive format is not supported: %s", filepath.Ext(sourcePath))
|
return fmt.Errorf("archive format is not supported: %s", filepath.Ext(sourcePath))
|
||||||
}
|
}
|
||||||
|
|
@ -131,7 +134,7 @@ func countTarEntries(sourcePath string) (int64, int64) {
|
||||||
return files, bytes
|
return files, bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractZipArchive(sourcePath string, targetDir string, progress func(CopyProgress), totalFiles, totalBytes int64) error {
|
func extractZipArchive(ctx context.Context, sourcePath string, targetDir string, progress func(CopyProgress), totalFiles, totalBytes int64) error {
|
||||||
reader, err := zip.OpenReader(sourcePath)
|
reader, err := zip.OpenReader(sourcePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -140,6 +143,12 @@ func extractZipArchive(sourcePath string, targetDir string, progress func(CopyPr
|
||||||
|
|
||||||
var filesDone int64
|
var filesDone int64
|
||||||
for _, file := range reader.File {
|
for _, file := range reader.File {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
relPath, ok := safeArchivePath(file.Name)
|
relPath, ok := safeArchivePath(file.Name)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
|
|
@ -180,7 +189,7 @@ func extractZipArchive(sourcePath string, targetDir string, progress func(CopyPr
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractTarArchive(sourcePath string, targetDir string, gzipped bool, progress func(CopyProgress), totalFiles, totalBytes int64) error {
|
func extractTarArchive(ctx context.Context, sourcePath string, targetDir string, gzipped bool, progress func(CopyProgress), totalFiles, totalBytes int64) error {
|
||||||
file, err := os.Open(sourcePath)
|
file, err := os.Open(sourcePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -200,6 +209,12 @@ func extractTarArchive(sourcePath string, targetDir string, gzipped bool, progre
|
||||||
tarReader := tar.NewReader(reader)
|
tarReader := tar.NewReader(reader)
|
||||||
var filesDone int64
|
var filesDone int64
|
||||||
for {
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
header, err := tarReader.Next()
|
header, err := tarReader.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
|
|
|
||||||
|
|
@ -683,15 +683,21 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
_ = m.reloadPane(PaneLeft, activeSelection)
|
_ = m.reloadPane(PaneLeft, activeSelection)
|
||||||
_ = m.reloadPane(PaneRight, activeSelection)
|
_ = m.reloadPane(PaneRight, activeSelection)
|
||||||
if msg.err == context.Canceled {
|
if msg.err == context.Canceled {
|
||||||
if m.archiveJob.kind == "extract" {
|
switch m.archiveJob.kind {
|
||||||
|
case "extract":
|
||||||
m.status = "Extraction cancelled"
|
m.status = "Extraction cancelled"
|
||||||
} else {
|
case "delete":
|
||||||
|
m.status = "Delete cancelled"
|
||||||
|
default:
|
||||||
m.status = "Archiving cancelled"
|
m.status = "Archiving cancelled"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if m.archiveJob.kind == "extract" {
|
switch m.archiveJob.kind {
|
||||||
|
case "extract":
|
||||||
m.status = fmt.Sprintf("Extraction failed: %v", msg.err)
|
m.status = fmt.Sprintf("Extraction failed: %v", msg.err)
|
||||||
} else {
|
case "delete":
|
||||||
|
m.status = fmt.Sprintf("Delete failed: %v", msg.err)
|
||||||
|
default:
|
||||||
m.status = fmt.Sprintf("Archiving failed: %v", msg.err)
|
m.status = fmt.Sprintf("Archiving failed: %v", msg.err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -728,6 +734,37 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
return m, cmd
|
return m, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete completion — reload both panes, clear marks
|
||||||
|
if m.archiveJob.kind == "delete" {
|
||||||
|
log.Printf("[DONE] deleteDoneMsg: job=%d sources=%d", msg.jobID, len(msg.sourcePaths))
|
||||||
|
sourceCount := len(m.archiveJob.sourcePaths)
|
||||||
|
verb := "moved to trash"
|
||||||
|
if m.archiveJob.progress.Stage == "delete permanently" {
|
||||||
|
verb = "deleted"
|
||||||
|
}
|
||||||
|
m.status = fmt.Sprintf("%d entr%s %s", sourceCount, pluralSuffix(sourceCount, "y", "ies"), verb)
|
||||||
|
activeSelection := selectedName(m.activePane())
|
||||||
|
_ = m.reloadPane(PaneLeft, activeSelection)
|
||||||
|
_ = m.reloadPane(PaneRight, activeSelection)
|
||||||
|
background := m.archiveJob.background
|
||||||
|
m.archiveJob = nil
|
||||||
|
m.activePane().ClearMarks()
|
||||||
|
|
||||||
|
cmd := m.loadPreviewCmd()
|
||||||
|
if m.modal.kind == modalArchiveProgress {
|
||||||
|
m.modal = modalState{}
|
||||||
|
}
|
||||||
|
if background {
|
||||||
|
m.modal = modalState{
|
||||||
|
kind: modalNotice,
|
||||||
|
title: "Delete complete",
|
||||||
|
body: fmt.Sprintf("%d entr%s %s.", sourceCount, pluralSuffix(sourceCount, "y", "ies"), verb),
|
||||||
|
note: "Press Esc to close",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, cmd
|
||||||
|
}
|
||||||
|
|
||||||
// Archive creation completion
|
// Archive creation completion
|
||||||
log.Printf("[DONE] archiveDoneMsg: job=%d targetPath=%s sources=%d", msg.jobID, msg.targetPath, len(msg.sourcePaths))
|
log.Printf("[DONE] archiveDoneMsg: job=%d targetPath=%s sources=%d", msg.jobID, msg.targetPath, len(msg.sourcePaths))
|
||||||
m.status = fmt.Sprintf("Archived %d entr%s to %s", len(msg.sourcePaths), pluralSuffix(len(msg.sourcePaths), "y", "ies"), msg.targetPath)
|
m.status = fmt.Sprintf("Archived %d entr%s to %s", len(msg.sourcePaths), pluralSuffix(len(msg.sourcePaths), "y", "ies"), msg.targetPath)
|
||||||
|
|
@ -1438,6 +1475,16 @@ func (m Model) handleModalKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Local delete with progress dialog
|
||||||
|
if (pending.kind == opDelete || pending.kind == opPermanentDelete) && pending.srcClient == nil {
|
||||||
|
if m.archiveJob != nil {
|
||||||
|
m.status = "Delete is already running"
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
log.Printf("[MODAL] Confirm — local %s sources=%d", operationVerb(pending.kind), len(pending.sourcePaths))
|
||||||
|
return m, m.startDeleteJob(pending.kind, pending.sourcePaths)
|
||||||
|
}
|
||||||
|
|
||||||
// Extract archive
|
// Extract archive
|
||||||
if pending.kind == opExtractArchive {
|
if pending.kind == opExtractArchive {
|
||||||
if m.archiveJob != nil {
|
if m.archiveJob != nil {
|
||||||
|
|
@ -3981,12 +4028,13 @@ func renderArchiveProgressModal(job archiveJobState, palette theme.Palette, widt
|
||||||
|
|
||||||
progress := job.progress
|
progress := job.progress
|
||||||
ratio := 0.0
|
ratio := 0.0
|
||||||
if job.kind == "extract" {
|
switch job.kind {
|
||||||
// Extraction: progress by file count (no byte tracking during extraction)
|
case "extract", "delete":
|
||||||
|
// Extraction/delete: progress by file count (no byte tracking)
|
||||||
if progress.FilesTotal > 0 {
|
if progress.FilesTotal > 0 {
|
||||||
ratio = float64(progress.FilesDone) / float64(progress.FilesTotal)
|
ratio = float64(progress.FilesDone) / float64(progress.FilesTotal)
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
if progress.BytesTotal > 0 {
|
if progress.BytesTotal > 0 {
|
||||||
ratio = float64(progress.BytesDone) / float64(progress.BytesTotal)
|
ratio = float64(progress.BytesDone) / float64(progress.BytesTotal)
|
||||||
}
|
}
|
||||||
|
|
@ -3994,16 +4042,24 @@ func renderArchiveProgressModal(job archiveJobState, palette theme.Palette, widt
|
||||||
|
|
||||||
stage := progress.Stage
|
stage := progress.Stage
|
||||||
if stage == "" {
|
if stage == "" {
|
||||||
if job.kind == "extract" {
|
switch job.kind {
|
||||||
|
case "extract":
|
||||||
stage = "Extracting data"
|
stage = "Extracting data"
|
||||||
} else {
|
case "delete":
|
||||||
|
stage = "Deleting files"
|
||||||
|
default:
|
||||||
stage = "Archiving data"
|
stage = "Archiving data"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
title := "Archiving"
|
var title string
|
||||||
if job.kind == "extract" {
|
switch job.kind {
|
||||||
|
case "extract":
|
||||||
title = "Extracting"
|
title = "Extracting"
|
||||||
|
case "delete":
|
||||||
|
title = "Deleting"
|
||||||
|
default:
|
||||||
|
title = "Archiving"
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := []string{
|
lines := []string{
|
||||||
|
|
@ -4017,8 +4073,8 @@ func renderArchiveProgressModal(job archiveJobState, palette theme.Palette, widt
|
||||||
renderProgressStatLine("Files:", fmt.Sprintf("%d / %d", progress.FilesDone, progress.FilesTotal), contentWidth, palette),
|
renderProgressStatLine("Files:", fmt.Sprintf("%d / %d", progress.FilesDone, progress.FilesTotal), contentWidth, palette),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show size and speed for archive creation (not tracked during extraction)
|
// Only show size and speed for archive creation (not tracked during extraction/delete)
|
||||||
if job.kind != "extract" {
|
if job.kind == "archive" {
|
||||||
lines = append(lines,
|
lines = append(lines,
|
||||||
renderProgressStatLine("Size:", fmt.Sprintf("%s / %s", formatSize(progress.BytesDone, true), formatSize(progress.BytesTotal, true)), 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),
|
renderProgressStatLine("Speed:", transferSpeed(progress.BytesDone, job.startedAt), contentWidth, palette),
|
||||||
|
|
@ -4030,9 +4086,12 @@ func renderArchiveProgressModal(job archiveJobState, palette theme.Palette, widt
|
||||||
renderModalNoteLine("Background / b, Cancel / c", contentWidth, palette, mutedStyle),
|
renderModalNoteLine("Background / b, Cancel / c", contentWidth, palette, mutedStyle),
|
||||||
)
|
)
|
||||||
if job.background {
|
if job.background {
|
||||||
if job.kind == "extract" {
|
switch job.kind {
|
||||||
|
case "extract":
|
||||||
lines = append(lines, mutedStyle.Render("Extraction continues in background"))
|
lines = append(lines, mutedStyle.Render("Extraction continues in background"))
|
||||||
} else {
|
case "delete":
|
||||||
|
lines = append(lines, mutedStyle.Render("Delete continues in background"))
|
||||||
|
default:
|
||||||
lines = append(lines, mutedStyle.Render("Archive continues in background"))
|
lines = append(lines, mutedStyle.Render("Archive continues in background"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4654,6 +4713,7 @@ func (m *Model) startArchiveJob(sourcePaths []string, targetDir string, format s
|
||||||
func (m *Model) startExtractJob(sourcePath, targetDir string) tea.Cmd {
|
func (m *Model) startExtractJob(sourcePath, targetDir string) tea.Cmd {
|
||||||
m.nextArchiveJob++
|
m.nextArchiveJob++
|
||||||
jobID := m.nextArchiveJob
|
jobID := m.nextArchiveJob
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
m.archiveJob = &archiveJobState{
|
m.archiveJob = &archiveJobState{
|
||||||
id: jobID,
|
id: jobID,
|
||||||
|
|
@ -4667,6 +4727,7 @@ func (m *Model) startExtractJob(sourcePath, targetDir string) tea.Cmd {
|
||||||
BytesTotal: 0,
|
BytesTotal: 0,
|
||||||
CurrentPath: sourcePath,
|
CurrentPath: sourcePath,
|
||||||
},
|
},
|
||||||
|
cancel: cancel,
|
||||||
startedAt: time.Now(),
|
startedAt: time.Now(),
|
||||||
}
|
}
|
||||||
m.modal = modalState{kind: modalArchiveProgress}
|
m.modal = modalState{kind: modalArchiveProgress}
|
||||||
|
|
@ -4681,7 +4742,7 @@ func (m *Model) startExtractJob(sourcePath, targetDir string) tea.Cmd {
|
||||||
progress: p,
|
progress: p,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := vfs.ExtractArchiveToDir(sourcePath, targetDir, emitProgress)
|
err := vfs.ExtractArchiveToDir(ctx, sourcePath, targetDir, emitProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) {
|
if errors.Is(err, context.Canceled) {
|
||||||
err = context.Canceled
|
err = context.Canceled
|
||||||
|
|
@ -4706,6 +4767,88 @@ func (m *Model) startExtractJob(sourcePath, targetDir string) tea.Cmd {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Model) startDeleteJob(kind fileOpKind, sources []string) tea.Cmd {
|
||||||
|
m.nextArchiveJob++
|
||||||
|
jobID := m.nextArchiveJob
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
m.archiveJob = &archiveJobState{
|
||||||
|
id: jobID,
|
||||||
|
kind: "delete",
|
||||||
|
sourcePaths: append([]string(nil), sources...),
|
||||||
|
targetPath: "",
|
||||||
|
progress: vfs.CopyProgress{
|
||||||
|
FilesDone: 0,
|
||||||
|
FilesTotal: len(sources),
|
||||||
|
BytesDone: 0,
|
||||||
|
BytesTotal: 0,
|
||||||
|
CurrentPath: sources[0],
|
||||||
|
},
|
||||||
|
cancel: cancel,
|
||||||
|
startedAt: time.Now(),
|
||||||
|
}
|
||||||
|
m.modal = modalState{kind: modalArchiveProgress}
|
||||||
|
verb := "Moving to trash"
|
||||||
|
if kind == opPermanentDelete {
|
||||||
|
verb = "Deleting"
|
||||||
|
}
|
||||||
|
m.status = verb + " started"
|
||||||
|
|
||||||
|
return tea.Batch(
|
||||||
|
func() tea.Msg {
|
||||||
|
go func() {
|
||||||
|
verb := "move to trash"
|
||||||
|
if kind == opPermanentDelete {
|
||||||
|
verb = "delete permanently"
|
||||||
|
}
|
||||||
|
for i, sourcePath := range sources {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
m.archiveProgress <- archiveDoneMsg{
|
||||||
|
jobID: jobID,
|
||||||
|
sourcePaths: append([]string(nil), sources...),
|
||||||
|
err: context.Canceled,
|
||||||
|
}
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if kind == opPermanentDelete {
|
||||||
|
err = vfs.DeletePath(sourcePath)
|
||||||
|
} else {
|
||||||
|
err = vfs.MoveToTrash(sourcePath)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
m.archiveProgress <- archiveDoneMsg{
|
||||||
|
jobID: jobID,
|
||||||
|
sourcePaths: append([]string(nil), sources...),
|
||||||
|
err: fmt.Errorf("%s %s: %w", verb, sourcePath, err),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.archiveProgress <- archiveProgressMsg{
|
||||||
|
jobID: jobID,
|
||||||
|
progress: vfs.CopyProgress{
|
||||||
|
FilesDone: i + 1,
|
||||||
|
FilesTotal: len(sources),
|
||||||
|
CurrentPath: sourcePath,
|
||||||
|
Stage: verb,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.archiveProgress <- archiveDoneMsg{
|
||||||
|
jobID: jobID,
|
||||||
|
sourcePaths: append([]string(nil), sources...),
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
waitArchiveProgressCmd(m.archiveProgress),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func moveCmd(sourcePath, targetDir string, overwrite bool) tea.Cmd {
|
func moveCmd(sourcePath, targetDir string, overwrite bool) tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
targetPath, err := vfs.MovePath(sourcePath, targetDir, overwrite)
|
targetPath, err := vfs.MovePath(sourcePath, targetDir, overwrite)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue