diff --git a/README.md b/README.md index cd264db..0bc37fa 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,23 @@ # trzsz-go -[trzsz](https://github.com/trzsz/trzsz) ( trz / tsz ) is a simple file transfer tools, similar to lrzsz ( rz / sz ), and compatible with tmux. +[trzsz](https://trzsz.github.io/) ( trz / tsz ) is a simple file transfer tools, similar to lrzsz ( rz / sz ), and compatible with tmux. + +Website: [https://trzsz.github.io/go](https://trzsz.github.io/go)  中文文档:[https://trzsz.github.io/cn/go](https://trzsz.github.io/cn/go) [![MIT License](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://choosealicense.com/licenses/mit/) [![GitHub Release](https://img.shields.io/github/v/release/trzsz/trzsz-go)](https://github.com/trzsz/trzsz-go/releases) -**_Please check [https://github.com/trzsz/trzsz](https://github.com/trzsz/trzsz) for more information of `trzsz`._** +**_Please check [https://trzsz.github.io](https://trzsz.github.io) for more information of `trzsz`._** `trzsz-go` is the `go` version of `trzsz`, supports native terminals that support a local shell. +⭐ It's recommended to use the `go` version of `trzsz` on the server. + ## Installation -- Install with apt on Ubuntu
sudo apt install trzsz +- Install with apt on Ubuntu + +
sudo apt install trzsz ```sh sudo apt update && sudo apt install software-properties-common @@ -22,7 +28,9 @@
-- Install with apt on Debian
sudo apt install trzsz +- Install with apt on Debian + +
sudo apt install trzsz ```sh sudo apt install curl gpg @@ -37,21 +45,35 @@
-- Install with yum on Linux
sudo yum install trzsz +- Install with yum on Linux - ```sh - echo '[trzsz] - name=Trzsz Repo - baseurl=https://yum.fury.io/trzsz/ - enabled=1 - gpgcheck=0' | sudo tee /etc/yum.repos.d/trzsz.repo +
sudo yum install trzsz - sudo yum install trzsz - ``` + - Install with [gemfury](https://gemfury.com/) repository. + + ```sh + echo '[trzsz] + name=Trzsz Repo + baseurl=https://yum.fury.io/trzsz/ + enabled=1 + gpgcheck=0' | sudo tee /etc/yum.repos.d/trzsz.repo + + sudo yum install trzsz + ``` + + - Install with [wlnmp](https://www.wlnmp.com/install) repository. It's not necessary to configure the epel repository for trzsz, take CentOS as an example: + + ```sh + sudo rpm -ivh https://mirrors.wlnmp.com/centos/wlnmp-release-centos.noarch.rpm + + sudo yum install trzsz + ```
-- Install with [yay](https://github.com/Jguer/yay) on ArchLinux
yay -S trzsz +- Install with [yay](https://github.com/Jguer/yay) on ArchLinux + +
yay -S trzsz ```sh yay -Syu @@ -60,7 +82,9 @@
-- Install with [homebrew](https://brew.sh/) on MacOS
brew install trzsz-go +- Install with [homebrew](https://brew.sh/) on MacOS + +
brew install trzsz-go ```sh brew update @@ -69,7 +93,9 @@
-- Install with [scoop](https://scoop.sh/) on Windows
scoop install trzsz +- Install with [scoop](https://scoop.sh/) on Windows + +
scoop install trzsz ```sh scoop bucket add extras @@ -79,7 +105,9 @@
-- Install with Go ( Requires go 1.20 or later )
go install github.com/trzsz/trzsz-go/cmd/...@latest +- Install with Go ( Requires go 1.20 or later ) + +
go install github.com/trzsz/trzsz-go/cmd/...@latest ```sh go install github.com/trzsz/trzsz-go/cmd/trz@latest @@ -91,7 +119,9 @@
-- Download from the [Releases](https://github.com/trzsz/trzsz-go/releases)
Or build and install from the source code ( Requires go 1.20 or later ) +- Download from the [Releases](https://github.com/trzsz/trzsz-go/releases) + +
Or build and install from the source code ( Requires go 1.20 or later ) ```sh git clone https://github.com/trzsz/trzsz-go.git @@ -126,9 +156,9 @@ - If using `tmux` on the jump server, use `trzsz --relay ssh` to login to the remote server, e.g.: ```sh + trzsz ssh jump_server tmux - trzsz -r ssh x.x.x.x - trzsz --relay ssh x.x.x.x + trzsz --relay ssh remote_server ``` ### Use on the remote server @@ -139,13 +169,13 @@ ## Suggestion -- It is recommended to set `alias ssh="trzsz ssh"` for convenience, `alias ssh="trzsz -d ssh"` for dragging files. +- It is recommended to set `alias ssh="trzsz ssh"` for convenience, `alias ssh="trzsz -d ssh"` for dragging files to upload. - If using `tmux` on the local computer, run `tmux` ( without `trzsz` ) first, then `trzsz ssh` to login. ## Configuration -`trzsz` looks for configuration at `~/.trzsz.conf`. The path have to end with `/`, e.g.: +`trzsz` looks for configuration at `~/.trzsz.conf` ( `C:\Users\your_name\.trzsz.conf` on Windows ). The path have to end with `/`, e.g.: ``` DefaultUploadPath = @@ -168,7 +198,7 @@ DefaultDownloadPath = /Users/username/Downloads/ - In `MSYS2`, e.g.: `winpty trzsz /c/Windows/System32/OpenSSH/ssh.exe x.x.x.x`. - In `Cygwin`, e.g.: `trzsz "C:\Windows\System32\OpenSSH\ssh.exe" x.x.x.x`. - - Or use [trzsz-ssh](https://github.com/trzsz/trzsz-ssh) instead, e.g.: `tssh alias` ( The `tssh` includes `trzsz ssh` ). + - ⭐ Recommended to use [trzsz-ssh](https://trzsz.github.io/ssh) ( tssh ) instead, `tssh` is same as `trzsz ssh`. - Dragging files doesn't upload? - Don't forget the `--dragfile` option. e.g.: `trzsz -d ssh x.x.x.x`. @@ -179,7 +209,7 @@ DefaultDownloadPath = /Users/username/Downloads/ ## Development -Want to write your own ssh client that supports trzsz? Please check the [go ssh client example](examples/ssh_client.go). +Want to write your own ssh client that supports trzsz? Please check the [go ssh client example](https://github.com/trzsz/trzsz-go/blob/main/examples/ssh_client.go). ## Screenshot @@ -197,7 +227,7 @@ Want to write your own ssh client that supports trzsz? Please check the [go ssh ## Contact -Feel free to email the author . Welcome to join the QQ group: 318578930. +Feel free to email the author , or create an [issue](https://github.com/trzsz/trzsz-go/issues). Welcome to join the QQ group: 318578930. ## Sponsor diff --git a/debian/changelog b/debian/changelog index e21c685..e619e82 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +trzsz (1.1.4) trusty; urgency=medium + + * trzsz-go v1.1.4 + + -- Lonny Wong Sun, 16 Jul 2023 13:21:30 +0800 + trzsz (1.1.3) trusty; urgency=medium * trzsz-go v1.1.3 diff --git a/go.mod b/go.mod index 84dfc67..6495e70 100644 --- a/go.mod +++ b/go.mod @@ -5,23 +5,25 @@ go 1.20 require ( github.com/UserExistsError/conpty v0.1.1 github.com/creack/pty v1.1.18 - github.com/klauspost/compress v1.16.6 + github.com/klauspost/compress v1.16.7 + github.com/manifoldco/promptui v0.9.0 github.com/ncruces/zenity v0.10.9 github.com/stretchr/testify v1.8.4 github.com/trzsz/go-arg v1.5.2 - golang.org/x/sys v0.9.0 - golang.org/x/term v0.9.0 - golang.org/x/text v0.10.0 + golang.org/x/sys v0.10.0 + golang.org/x/term v0.10.0 + golang.org/x/text v0.11.0 ) require ( github.com/akavel/rsrc v0.10.2 // indirect github.com/alexflint/go-scalar v1.2.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f // indirect github.com/josephspurrier/goversioninfo v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/randall77/makefat v0.0.0-20210315173500-7ddd0e42c844 // indirect - golang.org/x/image v0.8.0 // indirect + golang.org/x/image v0.9.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 5d2c622..7488179 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,15 @@ github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -13,8 +22,10 @@ github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f h1:OGqDDftRTwrvUoL6pO github.com/dchest/jsmin v0.0.0-20220218165748-59f39799265f/go.mod h1:Dv9D0NUlAsaQcGQZa5kc5mqR9ua72SmA8VXi4cd+cBw= github.com/josephspurrier/goversioninfo v1.4.0 h1:Puhl12NSHUSALHSuzYwPYQkqa2E1+7SrtAPJorKK0C8= github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= -github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= -github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/ncruces/zenity v0.10.9 h1:TYdNwEj9HiDDcpdsIUecBMsQw7L80Aiu/IJMM0Tao1E= github.com/ncruces/zenity v0.10.9/go.mod h1:FzjqP1loicusCFJTdIt5Oqbmoj2zySHpM0RsgJeeCbk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -32,8 +43,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg= -golang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM= +golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g= +golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -43,26 +54,28 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/trzsz/comm.go b/trzsz/comm.go index 4949368..39fa246 100644 --- a/trzsz/comm.go +++ b/trzsz/comm.go @@ -84,6 +84,7 @@ type progressCallback interface { onStep(step int64) onDone() setPreSize(size int64) + setPause(pausing bool) } type bufferSize struct { @@ -209,6 +210,7 @@ type trzszError struct { var ( errStopped = simpleTrzszError("Stopped") + errStoppedAndDeleted = simpleTrzszError("Stopped and deleted") errReceiveDataTimeout = simpleTrzszError("Receive data timeout") ) @@ -253,6 +255,13 @@ func (e *trzszError) isRemoteFail() bool { return e.errType == "fail" || e.errType == "FAIL" } +func (e *trzszError) isStopAndDelete() bool { + if e == nil || e.errType != "fail" { + return false + } + return e.message == errStoppedAndDeleted.message +} + func checkPathWritable(path string) error { info, err := os.Stat(path) if os.IsNotExist(err) { @@ -468,6 +477,12 @@ func checkTmux() (tmuxModeType, *os.File, int32, error) { return tmuxNormalMode, tmuxStdout, int32(tmuxPaneWidth), nil } +func tmuxRefreshClient() { + cmd := exec.Command("tmux", "refresh-client") + cmd.Stdout = os.Stdout + _ = cmd.Run() +} + func getTerminalColumns() int { cmd := exec.Command("stty", "size") cmd.Stdin = os.Stdin @@ -495,7 +510,7 @@ func wrapStdinInput(transfer *trzszTransfer) { buffer = make([]byte, bufSize) } if err == io.EOF { - transfer.stopTransferringFiles() + transfer.stopTransferringFiles(false) } } } @@ -505,7 +520,7 @@ func handleServerSignal(transfer *trzszTransfer) { signal.Notify(sigstop, os.Interrupt, syscall.SIGTERM) go func() { <-sigstop - transfer.stopTransferringFiles() + transfer.stopTransferringFiles(false) }() } @@ -834,26 +849,32 @@ func isCompressionProfitable(file *os.File, size int64) (bool, error) { return compressedCount < 2, nil } -func formatSavedFileNames(fileNames []string, dstPath string) string { - var msg strings.Builder - msg.WriteString("Saved ") - msg.WriteString(strconv.Itoa(len(fileNames))) +func formatSavedFiles(fileNames []string, destPath string) string { + var builder strings.Builder + builder.WriteString("Saved ") + builder.WriteString(strconv.Itoa(len(fileNames))) if len(fileNames) > 1 { - msg.WriteString(" files/directories") + builder.WriteString(" files/directories") } else { - msg.WriteString(" file/directory") + builder.WriteString(" file/directory") } - if len(dstPath) != 0 { - msg.WriteString(" to ") - msg.WriteString(dstPath) + if len(destPath) > 0 { + builder.WriteString(" to ") + builder.WriteString(destPath) } - msg.WriteString("\r\n") - for i, name := range fileNames { - if i > 0 { - msg.WriteString("\r\n") - } - msg.WriteString("- ") - msg.WriteString(name) + for _, name := range fileNames { + builder.WriteString("\r\n- ") + builder.WriteString(name) + } + return builder.String() +} + +func joinFileNames(header string, fileNames []string) string { + var builder strings.Builder + builder.WriteString(header) + for _, name := range fileNames { + builder.WriteString("\r\n- ") + builder.WriteString(name) } - return msg.String() + return builder.String() } diff --git a/trzsz/comm_test.go b/trzsz/comm_test.go index f59dbe3..6dbb2ac 100644 --- a/trzsz/comm_test.go +++ b/trzsz/comm_test.go @@ -324,7 +324,7 @@ func TestFormatSavedFileNames(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := formatSavedFileNames(tt.args.files, tt.args.dstPath) + got := formatSavedFiles(tt.args.files, tt.args.dstPath) assert.Equal(tt.want, got) }) } diff --git a/trzsz/filter.go b/trzsz/filter.go index 665df74..a5c2051 100644 --- a/trzsz/filter.go +++ b/trzsz/filter.go @@ -35,6 +35,7 @@ import ( "sync/atomic" "time" + "github.com/manifoldco/promptui" "github.com/ncruces/zenity" ) @@ -59,6 +60,7 @@ type TrzszFilter struct { options TrzszOptions transfer atomic.Pointer[trzszTransfer] progress atomic.Pointer[textProgressBar] + promptPipe atomic.Pointer[io.PipeWriter] serverVersion *trzszVersion remoteIsWindows bool dragging atomic.Bool @@ -110,9 +112,9 @@ func (filter *TrzszFilter) IsTransferringFiles() bool { } // StopTransferringFiles tell trzsz to stop if it is transferring files. -func (filter *TrzszFilter) StopTransferringFiles() { +func (filter *TrzszFilter) StopTransferringFiles(stopAndDelete bool) { if transfer := filter.transfer.Load(); transfer != nil { - transfer.stopTransferringFiles() + transfer.stopTransferringFiles(stopAndDelete) } } @@ -177,6 +179,7 @@ var parentWindowID = getParentWindowID() func (filter *TrzszFilter) chooseDownloadPath() (string, error) { savePath := filter.getTrzszConfig("DefaultDownloadPath") if savePath != nil { + time.Sleep(50 * time.Millisecond) // wait for all output to show return *savePath, nil } options := []zenity.Option{ @@ -238,6 +241,10 @@ func (filter *TrzszFilter) downloadFiles(transfer *trzszTransfer) error { return err } + if !filter.transfer.CompareAndSwap(nil, transfer) { + return simpleTrzszError("Swap transfer failed") + } + if err := transfer.sendAction(true, filter.serverVersion, filter.remoteIsWindows); err != nil { return err } @@ -257,7 +264,7 @@ func (filter *TrzszFilter) downloadFiles(transfer *trzszTransfer) error { return err } - return transfer.clientExit(formatSavedFileNames(localNames, path)) + return transfer.clientExit(formatSavedFiles(localNames, path)) } func (filter *TrzszFilter) uploadFiles(transfer *trzszTransfer, directory bool) error { @@ -273,6 +280,10 @@ func (filter *TrzszFilter) uploadFiles(transfer *trzszTransfer, directory bool) return err } + if !filter.transfer.CompareAndSwap(nil, transfer) { + return simpleTrzszError("Swap transfer failed") + } + if err := transfer.sendAction(true, filter.serverVersion, filter.remoteIsWindows); err != nil { return err } @@ -297,15 +308,14 @@ func (filter *TrzszFilter) uploadFiles(transfer *trzszTransfer, directory bool) if err != nil { return err } - return transfer.clientExit(formatSavedFileNames(remoteNames, "")) + return transfer.clientExit(formatSavedFiles(remoteNames, "")) } func (filter *TrzszFilter) handleTrzsz(mode byte) { transfer := newTransfer(filter.serverIn, nil, isWindowsEnvironment() || filter.remoteIsWindows, filter.logger) - filter.transfer.Store(transfer) defer func() { - filter.transfer.Store(nil) + filter.transfer.CompareAndSwap(transfer, nil) }() defer func() { @@ -379,13 +389,109 @@ func (filter *TrzszFilter) uploadDragFiles() { filter.resetDragFiles() } +func (filter *TrzszFilter) transformPromptInput(promptPipe *io.PipeWriter, buf []byte) { + const keyPrev = '\x10' + const keyNext = '\x0E' + n := len(buf) + for i := 0; i < n; i++ { + c := buf[i] + if c == '\x1b' && n-i > 2 && buf[i+1] == '[' { + switch buf[i+2] { + case '\x42': // ↓ to Next + c = keyNext + case '\x41', '\x5A': // ↑ Shift-TAB to Prev + c = keyPrev + } + i += 2 + } else { + switch c { + case '\x03': // Ctrl-C to Stop + _, _ = promptPipe.Write([]byte{keyPrev, keyPrev, '\r'}) + return + case 'q', 'Q', '\x11': // q Ctrl-C Ctrl-Q to Quit + _, _ = promptPipe.Write([]byte{keyNext, keyNext, '\r'}) + return + case '\t', '\x0E', 'j', 'J', '\x0A': // Tab ↓ j Ctrl-J to Next + c = keyNext + case '\x10', 'k', 'K', '\x0B': // ↑ k Ctrl-K to Prev + c = keyPrev + case '\r': // Enter + default: + continue + } + } + _, _ = promptPipe.Write([]byte{c}) + } +} + +func (filter *TrzszFilter) confirmStopTransfer(transfer *trzszTransfer) { + pipeIn, pipeOut := io.Pipe() + if !filter.promptPipe.CompareAndSwap(nil, pipeOut) { + pipeIn.Close() + pipeOut.Close() + return + } + + transfer.pauseTransferringFiles() + + go func() { + defer pipeIn.Close() + defer pipeOut.Close() + defer filter.promptPipe.Store(nil) + + if progress := filter.progress.Load(); progress != nil { + progress.setPause(true) + defer func() { + progress.setTerminalColumns(filter.options.TerminalColumns) + progress.setPause(false) + }() + time.Sleep(50 * time.Millisecond) // wait for the progress bar output + _, _ = filter.clientOut.Write([]byte("\r\n")) // keep the progress bar displayed + } + + prompt := promptui.Select{ + Label: "Are you sure you want to stop transferring files", + Items: []string{ + "Stop and keep transferred files", + "Stop and delete transferred files", + "Continue to transfer remaining files", + }, + Stdin: pipeIn, + Stdout: filter.clientOut, + Templates: &promptui.SelectTemplates{ + Help: `{{ "Use ↓ ↑ j k to navigate" | faint }}`, + }, + } + + idx, _, err := prompt.Run() + + if transfer := filter.transfer.Load(); transfer != nil { + if err != nil || idx == 2 { + transfer.resumeTransferringFiles() + } else if idx == 0 { + transfer.stopTransferringFiles(false) + } else if idx == 1 { + transfer.stopTransferringFiles(true) + } + } + }() +} + func (filter *TrzszFilter) sendInput(buf []byte) { if filter.logger != nil { filter.logger.writeTraceLog(buf, "stdin") } + if promptPipe := filter.promptPipe.Load(); promptPipe != nil { + filter.transformPromptInput(promptPipe, buf) + return + } if transfer := filter.transfer.Load(); transfer != nil { if buf[0] == '\x03' { // `ctrl + c` to stop transferring files - transfer.stopTransferringFiles() + if filter.serverVersion.compare(&trzszVersion{1, 1, 3}) > 0 { + filter.confirmStopTransfer(transfer) + } else { + transfer.stopTransferringFiles(false) + } } return } diff --git a/trzsz/progress.go b/trzsz/progress.go index 1eee8e8..a992093 100644 --- a/trzsz/progress.go +++ b/trzsz/progress.go @@ -153,6 +153,7 @@ type textProgressBar struct { speedIdx int timeArray [kSpeedArraySize]*time.Time stepArray [kSpeedArraySize]int64 + pausing atomic.Bool } func newTextProgressBar(writer io.Writer, columns int32, tmuxPaneColumns int32) *textProgressBar { @@ -215,7 +216,9 @@ func (p *textProgressBar) onStep(step int64) { return } p.fileStep = step - p.showProgress() + if !p.pausing.Load() { + p.showProgress() + } } func (p *textProgressBar) onDone() { @@ -239,6 +242,13 @@ func (p *textProgressBar) setPreSize(size int64) { p.preSize = size } +func (p *textProgressBar) setPause(pausing bool) { + if p == nil { + return + } + p.pausing.Store(pausing) +} + func (p *textProgressBar) showProgress() { now := timeNowFunc() if p.lastUpdateTime != nil && now.Sub(*p.lastUpdateTime) < 200*time.Millisecond { diff --git a/trzsz/relay.go b/trzsz/relay.go index 974a71e..79924d2 100644 --- a/trzsz/relay.go +++ b/trzsz/relay.go @@ -281,6 +281,12 @@ func (r *trzszRelay) handshake() { confirm = true } +func (r *trzszRelay) resetToStandby() { + if r.relayStatus.CompareAndSwap(kRelayTransferring, kRelayStandBy) { + tmuxRefreshClient() + } +} + func (r *trzszRelay) wrapInput() { defer close(r.osStdinChan) for { @@ -304,11 +310,11 @@ func (r *trzszRelay) wrapInput() { r.osStdinChan <- buf if status == kRelayTransferring { if buf[0] == '\x03' { // `ctrl + c` to stop - r.relayStatus.Store(kRelayStandBy) + r.resetToStandby() } else if bytes.Contains(buf, []byte("#EXIT:")) { // transfer exit - r.relayStatus.Store(kRelayStandBy) + r.resetToStandby() } else if bytes.Contains(buf, []byte("#FAIL:")) || bytes.Contains(buf, []byte("#fail:")) { // transfer error - r.relayStatus.Store(kRelayStandBy) + r.resetToStandby() } } } @@ -343,9 +349,9 @@ func (r *trzszRelay) wrapOutput() { if status == kRelayTransferring { r.bypassTmuxChan <- buf if bytes.Contains(buf, []byte("#EXIT:")) { // transfer exit - r.relayStatus.Store(kRelayStandBy) + r.resetToStandby() } else if bytes.Contains(buf, []byte("#FAIL:")) || bytes.Contains(buf, []byte("#fail:")) { // transfer error - r.relayStatus.Store(kRelayStandBy) + r.resetToStandby() } continue } diff --git a/trzsz/transfer.go b/trzsz/transfer.go index 9691bb1..0956a28 100644 --- a/trzsz/transfer.go +++ b/trzsz/transfer.go @@ -80,6 +80,7 @@ type trzszTransfer struct { buffer *trzszBuffer writer io.Writer stopped atomic.Bool + stopAndDelete atomic.Bool pausing atomic.Bool pauseIdx atomic.Uint32 pauseBeginTime atomic.Int64 @@ -98,6 +99,7 @@ type trzszTransfer struct { savedSteps atomic.Int64 transferConfig transferConfig logger *traceLogger + createdFiles []string } func maxDuration(a, b time.Duration) time.Duration { @@ -148,10 +150,11 @@ func (t *trzszTransfer) addReceivedData(buf []byte) { t.lastInputTime.Store(time.Now().UnixMilli()) } -func (t *trzszTransfer) stopTransferringFiles() { +func (t *trzszTransfer) stopTransferringFiles(stopAndDelete bool) { if t.stopped.Load() { return } + t.stopAndDelete.Store(stopAndDelete) t.stopped.Store(true) t.buffer.stopBuffer() @@ -186,6 +189,9 @@ func (t *trzszTransfer) resumeTransferringFiles() { } func (t *trzszTransfer) checkStop() error { + if t.stopAndDelete.Load() { + return errStoppedAndDeleted + } if t.stopped.Load() { return errStopped } @@ -554,6 +560,22 @@ func (t *trzszTransfer) serverExit(msg string) { } os.Stdout.WriteString(msg) os.Stdout.WriteString("\r\n") + if t.transferConfig.TmuxOutputJunk { + tmuxRefreshClient() + } +} + +func (t *trzszTransfer) deleteCreatedFiles() []string { + var deletedFiles []string + for _, path := range t.createdFiles { + if _, err := os.Stat(path); os.IsNotExist(err) { + continue + } + if err := os.RemoveAll(path); err == nil { + deletedFiles = append(deletedFiles, path) + } + } + return deletedFiles } func (t *trzszTransfer) clientError(err error) { @@ -567,6 +589,14 @@ func (t *trzszTransfer) clientError(err error) { } } + if t.stopAndDelete.Load() { + deletedFiles := t.deleteCreatedFiles() + if len(deletedFiles) > 0 { + _ = t.sendString("fail", joinFileNames(err.Error()+":", deletedFiles)) + return + } + } + typ := "fail" if trace { typ = "FAIL" @@ -579,6 +609,13 @@ func (t *trzszTransfer) serverError(err error) { trace := true if e, ok := err.(*trzszError); ok { + if e.isStopAndDelete() { + deletedFiles := t.deleteCreatedFiles() + if len(deletedFiles) > 0 { + t.serverExit(joinFileNames(err.Error()+":", deletedFiles)) + return + } + } trace = e.isTraceBack() if e.isRemoteExit() || e.isRemoteFail() { t.serverExit(e.Error()) @@ -788,6 +825,10 @@ func (t *trzszTransfer) recvFileNum(progress progressCallback) (int64, error) { return num, nil } +func (t *trzszTransfer) addCreatedFiles(path string) { + t.createdFiles = append(t.createdFiles, path) +} + func (t *trzszTransfer) doCreateFile(path string, truncate bool) (*os.File, error) { flag := os.O_RDWR | os.O_CREATE if truncate { @@ -806,13 +847,19 @@ func (t *trzszTransfer) doCreateFile(path string, truncate bool) (*os.File, erro } return nil, simpleTrzszError("Create file [%s] failed: %v", path, err) } + t.addCreatedFiles(path) return file, nil } func (t *trzszTransfer) doCreateDirectory(path string) error { stat, err := os.Stat(path) if os.IsNotExist(err) { - return os.MkdirAll(path, 0755) + err := os.MkdirAll(path, 0755) + if err != nil { + return err + } + t.addCreatedFiles(path) + return nil } else if err != nil { return err } diff --git a/trzsz/trz.go b/trzsz/trz.go index 01a8c79..364c6aa 100644 --- a/trzsz/trz.go +++ b/trzsz/trz.go @@ -101,7 +101,7 @@ func recvFiles(transfer *trzszTransfer, args *trzArgs, tmuxMode tmuxModeType, tm return err } - transfer.serverExit(formatSavedFileNames(localNames, args.Path)) + transfer.serverExit(formatSavedFiles(localNames, args.Path)) return nil } diff --git a/trzsz/trzsz.go b/trzsz/trzsz.go index ed89ce7..7ddca51 100644 --- a/trzsz/trzsz.go +++ b/trzsz/trzsz.go @@ -103,7 +103,7 @@ func handleSignal(pty *trzszPty, filter *TrzszFilter) { signal.Notify(sigint, os.Interrupt) go func() { for range sigint { - filter.StopTransferringFiles() + filter.StopTransferringFiles(false) } }() } diff --git a/trzsz/version.go b/trzsz/version.go index a07c917..8c99c0e 100644 --- a/trzsz/version.go +++ b/trzsz/version.go @@ -24,4 +24,4 @@ SOFTWARE. package trzsz -const kTrzszVersion = "1.1.3" +const kTrzszVersion = "1.1.4"