From 6b93610b6abf687399e250998e6d0c906c68b08a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 26 May 2023 20:40:41 +0530 Subject: [PATCH] more work on transfer kitten --- docs/file-transfer-protocol.rst | 2 +- kittens/transfer/ftc.go | 274 ++++++++++++++++++++++++++++++++ kittens/transfer/send.go | 87 ++++------ 3 files changed, 302 insertions(+), 61 deletions(-) create mode 100644 kittens/transfer/ftc.go diff --git a/docs/file-transfer-protocol.rst b/docs/file-transfer-protocol.rst index 86e64fa79..a765816fb 100644 --- a/docs/file-transfer-protocol.rst +++ b/docs/file-transfer-protocol.rst @@ -513,7 +513,7 @@ safe_string integer A base-10 number composed of the characters ``[0-9]`` with a possible - leading ``-`` sign + leading ``-`` sign. When missing the value is zero. base64_string A base64 encoded UTF-8 string using the standard base64 encoding diff --git a/kittens/transfer/ftc.go b/kittens/transfer/ftc.go new file mode 100644 index 000000000..3708fb1f1 --- /dev/null +++ b/kittens/transfer/ftc.go @@ -0,0 +1,274 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package transfer + +import ( + "encoding/base64" + "fmt" + "io/fs" + "kitty" + "kitty/tools/utils" + "kitty/tools/wcswidth" + "reflect" + "strconv" + "strings" + "time" +) + +var _ = fmt.Print + +type Field interface { + IsDefault() bool + Serialize() string +} + +type Action int + +func (self Action) IsDefault() bool { return self == Action_invalid } +func (self Action) Serialize() string { + switch self { + default: + return "invalid" + case Action_file: + return "file" + case Action_data: + return "data" + case Action_end_data: + return "end_data" + case Action_receive: + return "receive" + case Action_send: + return "send" + case Action_cancel: + return "cancel" + case Action_status: + return "status" + case Action_finish: + return "finish" + } +} + +const ( + Action_invalid Action = iota + Action_file + Action_data + Action_end_data + Action_receive + Action_send + Action_cancel + Action_status + Action_finish +) + +type Compression int + +const ( + Compression_none Compression = iota + Compression_zlib +) + +func (self Compression) IsDefault() bool { return self == Compression_none } +func (self Compression) Serialize() string { + switch self { + default: + return "none" + case Compression_zlib: + return "zlib" + } +} + +type FileType int + +const ( + FileType_regular FileType = iota + FileType_symlink + FileType_directory + FileType_link +) + +func (self FileType) ShortText() string { + switch self { + case FileType_regular: + return "fil" + case FileType_directory: + return "dir" + case FileType_symlink: + return "sym" + case FileType_link: + return "lnk" + } + return "und" +} + +func (self FileType) Color() string { + switch self { + case FileType_regular: + return "yellow" + case FileType_directory: + return "magenta" + case FileType_symlink: + return "blue" + case FileType_link: + return "green" + } + return "" +} + +func (self FileType) String() string { + switch self { + case FileType_regular: + return "FileType.Regular" + case FileType_directory: + return "FileType.Directory" + case FileType_symlink: + return "FileType.SymbolicLink" + case FileType_link: + return "FileType.Link" + } + return "FileType.Unknown" +} + +func (self FileType) IsDefault() bool { return self == FileType_regular } +func (self FileType) Serialize() string { + switch self { + default: + return "regular" + case FileType_directory: + return "directory" + case FileType_symlink: + return "symlink" + case FileType_link: + return "link" + } +} + +type TransmissionType int + +const ( + TransmissionType_simple TransmissionType = iota + TransmissionType_rsync +) + +func (self TransmissionType) IsDefault() bool { return self == TransmissionType_simple } +func (self TransmissionType) Serialize() string { + switch self { + default: + return "simple" + case TransmissionType_rsync: + return "rsync" + } +} + +type QuietLevel int + +const ( + Quiet_none QuietLevel = iota + Quiet_acknowledgements + Quiet_errors +) + +func (self QuietLevel) IsDefault() bool { return self == Quiet_none } +func (self QuietLevel) Serialize() string { + switch self { + default: + return "0" + case Quiet_acknowledgements: + return "1" + case Quiet_errors: + return "2" + } +} + +type FileTransmissionCommand struct { + Action Action `name:"ac"` + Compression Compression `name:"zip"` + Ftype FileType `name:"ft"` + Ttype TransmissionType `name:"tt"` + Quiet QuietLevel `name:"q"` + + Id string `name:"id"` + File_id string `name:"fid"` + Bypass string `name:"pw" encoding:"base64"` + Name string `name:"n" encoding:"base64"` + Status string `name:"st" encoding:"base64"` + Parent string `name:"pr"` + Mtime time.Duration `name:"mod"` + Permissions fs.FileMode `name:"prm"` + Size uint64 `name:"sz"` + + Data []byte `name:"d"` +} + +func escape_semicolons(x string) string { + return strings.ReplaceAll(x, ";", ";;") +} + +func (self *FileTransmissionCommand) Serialize(prefix_with_osc_code ...bool) string { + ans := strings.Builder{} + v := reflect.ValueOf(*self) + typ := v.Type() + fields := reflect.VisibleFields(typ) + found := false + if len(prefix_with_osc_code) > 0 && prefix_with_osc_code[0] { + ans.WriteString(strconv.Itoa(kitty.FileTransferCode)) + found = true + } + for _, field := range fields { + if name := field.Tag.Get("name"); name != "" { + val := v.FieldByIndex(field.Index) + encoded_val := "" + switch val.Kind() { + case reflect.String: + sval := val.String() + if sval != "" { + enc := field.Tag.Get("encoding") + switch enc { + case "base64": + encoded_val = base64.StdEncoding.EncodeToString(utils.UnsafeStringToBytes(sval)) + default: + encoded_val = escape_semicolons(wcswidth.StripEscapeCodes(sval)) + } + } + case reflect.Slice: + switch val.Elem().Type().Kind() { + case reflect.Uint8: + bval := val.Bytes() + if len(bval) > 0 { + encoded_val = base64.StdEncoding.EncodeToString(bval) + } + } + case reflect.Uint64: + encoded_val = strconv.FormatUint(val.Uint(), 10) + default: + if val.CanInterface() { + i := val.Interface() + if field, ok := i.(Field); ok { + if !field.IsDefault() { + encoded_val = field.Serialize() + } + } else if field, ok := i.(time.Duration); ok { + if field != 0 { + encoded_val = strconv.FormatInt(int64(field), 10) + } + } else if field, ok := i.(fs.FileMode); ok { + field = field.Perm() + if field != 0 { + encoded_val = strconv.FormatInt(int64(field), 10) + } + } + } + } + if encoded_val != "" { + if found { + ans.WriteString(";") + } else { + found = true + } + ans.WriteString(name) + ans.WriteString("=") + ans.WriteString(encoded_val) + } + } + } + return ans.String() +} diff --git a/kittens/transfer/send.go b/kittens/transfer/send.go index 2ac828cef..217116506 100644 --- a/kittens/transfer/send.go +++ b/kittens/transfer/send.go @@ -20,57 +20,6 @@ import ( var _ = fmt.Print -type FileType int - -const ( - REGULAR_FILE FileType = iota - SYMLINK_FILE - DIRECTORY_FILE - LINK_FILE -) - -func (self FileType) ShortText() string { - switch self { - case REGULAR_FILE: - return "fil" - case DIRECTORY_FILE: - return "dir" - case SYMLINK_FILE: - return "sym" - case LINK_FILE: - return "lnk" - } - return "und" -} - -func (self FileType) Color() string { - switch self { - case REGULAR_FILE: - return "yellow" - case DIRECTORY_FILE: - return "magenta" - case SYMLINK_FILE: - return "blue" - case LINK_FILE: - return "green" - } - return "" -} - -func (self FileType) String() string { - switch self { - case REGULAR_FILE: - return "FileType.Regular" - case DIRECTORY_FILE: - return "FileType.Directory" - case SYMLINK_FILE: - return "FileType.SymbolicLink" - case LINK_FILE: - return "FileType.Link" - } - return "FileType.Unknown" -} - type FileState int const ( @@ -125,8 +74,8 @@ func NewFile(local_path, expanded_local_path string, file_id int, stat_result fs file_hash: FileHash{stat.Dev, stat.Ino}, mtime: stat_result.ModTime(), file_size: stat_result.Size(), bytes_to_transmit: stat_result.Size(), permissions: stat_result.Mode().Perm(), remote_path: filepath.ToSlash(get_remote_path(local_path, remote_base)), - rsync_capable: file_type == REGULAR_FILE && stat_result.Size() > 4096, - compression_capable: file_type == REGULAR_FILE && stat_result.Size() > 4096 && should_be_compressed(expanded_local_path), + rsync_capable: file_type == FileType_regular && stat_result.Size() > 4096, + compression_capable: file_type == FileType_regular && stat_result.Size() > 4096 && should_be_compressed(expanded_local_path), remote_initial_size: -1, } return &ans @@ -141,7 +90,7 @@ func process(opts *Options, paths []string, remote_base string, counter *int) (a } if s.IsDir() { *counter += 1 - ans = append(ans, NewFile(x, expanded, *counter, s, remote_base, DIRECTORY_FILE)) + ans = append(ans, NewFile(x, expanded, *counter, s, remote_base, FileType_directory)) new_remote_base := remote_base if new_remote_base != "" { new_remote_base = strings.TrimRight(new_remote_base, "/") + "/" + filepath.Base(x) + "/" @@ -163,10 +112,10 @@ func process(opts *Options, paths []string, remote_base string, counter *int) (a ans = append(ans, new_ans...) } else if s.Mode()&fs.ModeSymlink == fs.ModeSymlink { *counter += 1 - ans = append(ans, NewFile(x, expanded, *counter, s, remote_base, SYMLINK_FILE)) + ans = append(ans, NewFile(x, expanded, *counter, s, remote_base, FileType_symlink)) } else if s.Mode().IsRegular() { *counter += 1 - ans = append(ans, NewFile(x, expanded, *counter, s, remote_base, REGULAR_FILE)) + ans = append(ans, NewFile(x, expanded, *counter, s, remote_base, FileType_regular)) } } return @@ -219,7 +168,7 @@ func files_for_send(opts *Options, args []string) (files []*File, err error) { for _, group := range groups { if len(group) > 1 { for _, lf := range group[1:] { - lf.file_type = LINK_FILE + lf.file_type = FileType_link lf.hard_link_target = group[0].file_id } } @@ -228,7 +177,7 @@ func files_for_send(opts *Options, args []string) (files []*File, err error) { remove := make([]int, 0, len(files)) // detect symlinks to other transferred files for i, f := range files { - if f.file_type == SYMLINK_FILE { + if f.file_type == FileType_symlink { link_dest, err := os.Readlink(f.local_path) if err != nil { remove = append(remove, i) @@ -389,10 +338,22 @@ type SendHandler struct { failed_files, done_files []*File done_file_ids *utils.Set[string] transmit_ok_checked bool + progress_update_timer loop.IdType +} + +func (self *SendHandler) schedule_progress_update(delay time.Duration) { + if self.progress_update_timer != 0 { + self.lp.RemoveTimer(self.progress_update_timer) + self.progress_update_timer = 0 + } + timer_id, err := self.lp.AddTimer(delay, false, self.refresh_progress) + if err == nil { + self.progress_update_timer = timer_id + } } func (self *SendHandler) on_file_progress(f *File, change int) { - self.schedule_progress_update() + self.schedule_progress_update(100 * time.Millisecond) } func (self *SendHandler) on_file_done(f *File) { @@ -400,7 +361,13 @@ func (self *SendHandler) on_file_done(f *File) { if f.err_msg != "" { self.failed_files = append(self.failed_files, f) } - self.schedule_progress_update() + self.schedule_progress_update(100 * time.Millisecond) +} + +func (self *SendHandler) send_payload(payload string) { + self.lp.QueueWriteString(self.manager.prefix) + self.lp.QueueWriteString(payload) + self.lp.QueueWriteString(self.manager.suffix) } func (self *SendHandler) initialize() error {