diff --git a/kittens/dnd/drop.go b/kittens/dnd/drop.go index b90e7e6e2..309269aae 100644 --- a/kittens/dnd/drop.go +++ b/kittens/dnd/drop.go @@ -2,9 +2,11 @@ package dnd import ( "bytes" + "context" "errors" "fmt" "io" + "io/fs" "net/url" "os" "path/filepath" @@ -12,6 +14,7 @@ import ( "strconv" "strings" + "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/streaming_base64" ) @@ -124,6 +127,77 @@ func uniqify_child_names(names []string, is_case_sensitive_filesystem bool) []st return names } +func do_local_copy(ctx context.Context, dest_dir *os.File, uri_list []string) (err error) { + var src_file *os.File + defer func() { + if src_file != nil { + src_file.Close() + } + }() + for _, path := range uri_list { + if path == "" { + continue + } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + if src_file != nil { + src_file.Close() + } + if src_file, err = os.Open(path); err != nil { + return err + } + st, err := src_file.Stat() + if err != nil { + return err + } + if st.IsDir() { + d, err := utils.CreateDirAt(dest_dir, filepath.Base(src_file.Name()), st.Mode().Perm()) + if err != nil { + return err + } + err = utils.CopyFolderContents(ctx, src_file, dest_dir, utils.CopyFolderOptions{ + Filter_files: func(parent *os.File, child os.FileInfo) bool { + return child.IsDir() || child.Mode().IsRegular() || child.Mode()&fs.ModeSymlink != 0 + }, + }) + d.Close() + if err != nil { + return err + } + } else if st.Mode().IsRegular() { + // First try a hard link + if err = os.Link(src_file.Name(), filepath.Join(dest_dir.Name(), filepath.Base(src_file.Name()))); err == nil { + continue + } + d, err := utils.CreateAt(dest_dir, filepath.Base(src_file.Name()), st.Mode().Perm()) + if err != nil { + return err + } + err = utils.CopyFileAndClose(ctx, src_file, d) + src_file = nil // already closed + if err != nil { + return err + } + } + } + return +} + +func do_local_copy_in_goroutine(ctx context.Context, dest_dir *os.File, completion chan error, uri_list []string, wakeup func()) { + var err error + defer func() { + if r := recover(); r != nil { + err = parallel.Format_stacktrace_on_panic(r, 1) + } + completion <- err + wakeup() + }() + err = do_local_copy(ctx, dest_dir, uri_list) +} + func (d *remote_dir_entry) add_remote_data(data []byte, output_buf []byte, has_more bool, is_case_sensitive_filesystem bool) error { if len(data) > 0 { for chunk, derr := range d.b64_decoder.Decode(data, output_buf) { @@ -198,13 +272,28 @@ type drop_status struct { reading_data bool is_remote_client bool + dropping_to *dir_handle root_remote_dir *remote_dir_entry open_remote_dir *remote_dir_entry current_remote_entry *remote_dir_entry // used for m=1 only pending_remote_dirs []*remote_dir_entry + + local_copy struct { + ctx context.Context + dest_dir *os.File + cancel_ctx context.CancelFunc + completion chan error + } } func (d *drop_status) reset() { + if d.local_copy.ctx != nil { + d.local_copy.cancel_ctx() + <-d.local_copy.completion + } + if d.dropping_to != nil { + d.dropping_to = d.dropping_to.unref() + } *d = drop_status{cell_x: -1, cell_y: -1} } @@ -268,9 +357,8 @@ func (dnd *dnd) all_mime_data_dropped() (err error) { if err != nil { return err } + dnd.drop_status.dropping_to = new_dir_handle(f) if drop_status.is_remote_client { - rd := new_dir_handle(f) - defer rd.unref() drop_status.root_remote_dir = &remote_dir_entry{} seen := utils.NewSet[string](len(drop_status.uri_list)) idx := slices.Index(drop_status.offered_mimes, "text/uri-list") @@ -288,14 +376,17 @@ func (dnd *dnd) all_mime_data_dropped() (err error) { } seen.Add(key) } - c = &remote_dir_entry{base_dir: rd.newref(), name: name} + c = &remote_dir_entry{base_dir: dnd.drop_status.dropping_to.newref(), name: name} dnd.lp.QueueDnDData(DC{Type: 'r', X: idx + 1, Y: i + 1}) } drop_status.root_remote_dir.children = append(drop_status.root_remote_dir.children, c) } drop_status.open_remote_dir = drop_status.root_remote_dir } else { - // TODO: Implement this + drop_status.local_copy.dest_dir = f + drop_status.local_copy.ctx, drop_status.local_copy.cancel_ctx = context.WithCancel(context.Background()) + drop_status.local_copy.completion = make(chan error, 1) + go do_local_copy_in_goroutine(drop_status.local_copy.ctx, f, drop_status.local_copy.completion, slices.Clone(drop_status.uri_list), func() { dnd.lp.WakeupMainThread() }) } return } diff --git a/kittens/dnd/main.go b/kittens/dnd/main.go index ff674d632..e099e7d68 100644 --- a/kittens/dnd/main.go +++ b/kittens/dnd/main.go @@ -372,6 +372,7 @@ func dnd_main(cmd *cli.Command, opts *Options, args []string) (rc int, err error } } dnd := dnd{opts: opts, drop_dests: drop_dests, drag_sources: drag_sources} + defer dnd.reset_drop() if err = dnd.run_loop(); err != nil { return 1, err } diff --git a/tools/utils/file_at_fd.go b/tools/utils/file_at_fd.go index 1c2fa8a40..1f3049023 100644 --- a/tools/utils/file_at_fd.go +++ b/tools/utils/file_at_fd.go @@ -396,7 +396,9 @@ type CopyFolderOptions struct { Filter_files func(parent *os.File, child os.FileInfo) bool } -func copy_file_and_close(ctx context.Context, src *os.File, dest *os.File) (err error) { +// Copy the file objects as efficiently as possible with cancellation. The +// files are always closed before this function returns. +func CopyFileAndClose(ctx context.Context, src *os.File, dest *os.File) (err error) { err_chan := make(chan error) go func() { defer func() { @@ -537,7 +539,7 @@ func CopyFolderContents(ctx context.Context, src_folder *os.File, dest_folder *o sf.Close() return fail(err) } - if err = copy_file_and_close(ctx, sf, df); err != nil { + if err = CopyFileAndClose(ctx, sf, df); err != nil { UnlinkAt(dest.File(), child.Name()) // dont leave partially copied files around return fail(err) }