mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-11 18:32:12 +02:00
more work on transfer kitten
This commit is contained in:
@@ -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
|
||||
|
||||
274
kittens/transfer/ftc.go
Normal file
274
kittens/transfer/ftc.go
Normal file
@@ -0,0 +1,274 @@
|
||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user