diff --git a/app.go b/app.go index cd48afc..6a03dfa 100644 --- a/app.go +++ b/app.go @@ -167,7 +167,7 @@ func (a *App) Delete(connId, path string) error { return exp.Delete(path) } -func (a *App) QueueTransfer(id string, srcConnId, dstConnId, srcPath, dstPath, filename string, size int64, verify bool) error { +func (a *App) QueueTransfer(id string, srcConnId, dstConnId, srcPath, dstPath, filename string, size int64, verify bool, limitMBps int) error { srcExp, err := a.getExplorerForConnection(srcConnId) if err != nil { return err @@ -185,7 +185,7 @@ func (a *App) QueueTransfer(id string, srcConnId, dstConnId, srcPath, dstPath, f return err } - return a.transferManager.QueueTransfer(id, srcPath, dstPath, filename, size, srcExp, dstExp, verify) + return a.transferManager.QueueTransfer(id, srcPath, dstPath, filename, size, srcExp, dstExp, verify, limitMBps) } func (a *App) GetTransfers() []*transfer.Transfer { @@ -232,7 +232,7 @@ func (a *App) PromptUploadFiles(connId, destPath string) error { remotePath = destPath + fileName } - a.QueueTransfer(id, "local", connId, localPath, remotePath, fileName, stat.Size(), false) + a.QueueTransfer(id, "local", connId, localPath, remotePath, fileName, stat.Size(), false, 0) } return nil } @@ -272,7 +272,7 @@ func (a *App) PromptUploadDirectory(connId, destPath string) error { } id := uuid.New().String() - a.QueueTransfer(id, "local", connId, path, remotePath, info.Name(), info.Size(), false) + a.QueueTransfer(id, "local", connId, path, remotePath, info.Name(), info.Size(), false, 0) return nil }) } @@ -304,7 +304,7 @@ func (a *App) PromptDownload(connId, remotePath string) error { size = stat.Size } - return a.QueueTransfer(id, connId, "local", remotePath, localPath, fileName, size, false) + return a.QueueTransfer(id, connId, "local", remotePath, localPath, fileName, size, false, 0) } type TransferItem struct { @@ -314,7 +314,7 @@ type TransferItem struct { Size int64 `json:"size"` } -func (a *App) TransferItems(srcConnId, dstConnId, dstPath string, items []TransferItem, verify bool) error { +func (a *App) TransferItems(srcConnId, dstConnId, dstPath string, items []TransferItem, verify bool, limitMBps int) error { for _, item := range items { if !item.IsDir { id := uuid.New().String() @@ -328,18 +328,18 @@ func (a *App) TransferItems(srcConnId, dstConnId, dstPath string, items []Transf remotePath = dstPath + item.Name } - if err := a.QueueTransfer(id, srcConnId, dstConnId, item.Path, remotePath, item.Name, item.Size, verify); err != nil { + if err := a.QueueTransfer(id, srcConnId, dstConnId, item.Path, remotePath, item.Name, item.Size, verify, limitMBps); err != nil { return err } } else { // Run remote walk in goroutine to not block UI - go a.transferRemoteDirectory(srcConnId, dstConnId, item.Path, dstPath, verify) + go a.transferRemoteDirectory(srcConnId, dstConnId, item.Path, dstPath, verify, limitMBps) } } return nil } -func (a *App) transferRemoteDirectory(srcConnId, dstConnId, srcDirPath, dstBasePath string, verify bool) { +func (a *App) transferRemoteDirectory(srcConnId, dstConnId, srcDirPath, dstBasePath string, verify bool, limitMBps int) { baseName := filepath.Base(srcDirPath) newDstPath := dstBasePath @@ -360,11 +360,11 @@ func (a *App) transferRemoteDirectory(srcConnId, dstConnId, srcDirPath, dstBaseP for _, entry := range entries { if entry.IsDir { - a.transferRemoteDirectory(srcConnId, dstConnId, entry.Path, newDstPath, verify) + a.transferRemoteDirectory(srcConnId, dstConnId, entry.Path, newDstPath, verify, limitMBps) } else { id := uuid.New().String() itemDstPath := newDstPath + "/" + entry.Name - a.QueueTransfer(id, srcConnId, dstConnId, entry.Path, itemDstPath, entry.Name, entry.Size, verify) + a.QueueTransfer(id, srcConnId, dstConnId, entry.Path, itemDstPath, entry.Name, entry.Size, verify, limitMBps) } } } diff --git a/frontend/index.html b/frontend/index.html index 81e6a68..fd034e3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -185,6 +185,12 @@ +
+ + + (0 = Unlimited) +
+
diff --git a/frontend/src/main.js b/frontend/src/main.js index 29d6be4..50b03d1 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -529,6 +529,7 @@ window.openTransferModal = () => { window.closeTransferModal = () => { document.getElementById('transfer-modal').style.display = 'none'; document.getElementById('transfer-verify').checked = false; + document.getElementById('transfer-limit').value = "0"; }; window.loadTransferDestRoot = () => { @@ -583,7 +584,9 @@ window.loadTransferDestDirectory = async (connId, path) => { window.executeTransfer = async () => { if (selectedItems.length === 0) return; try { - await TransferItems(currentConn, transferDestConn, transferDestPath, selectedItems); + const verify = document.getElementById('transfer-verify').checked; + const limit = parseInt(document.getElementById('transfer-limit').value, 10) || 0; + await TransferItems(currentConn, transferDestConn, transferDestPath, selectedItems, verify, limit); closeTransferModal(); showTransfers(); selectedItems = []; diff --git a/go.mod b/go.mod index 124f57e..0cd290c 100644 --- a/go.mod +++ b/go.mod @@ -64,6 +64,7 @@ require ( golang.org/x/net v0.55.0 // indirect golang.org/x/sys v0.46.0 // indirect golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.15.0 // indirect ) // replace github.com/wailsapp/wails/v2 v2.12.0 => /home/jamie/go/pkg/mod diff --git a/go.sum b/go.sum index 1b59b9d..1b3ce69 100644 --- a/go.sum +++ b/go.sum @@ -148,6 +148,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= diff --git a/internal/transfer/transfer.go b/internal/transfer/transfer.go index d0a0432..2a09aa2 100644 --- a/internal/transfer/transfer.go +++ b/internal/transfer/transfer.go @@ -1,6 +1,7 @@ package transfer import ( + "context" "crypto/md5" "encoding/hex" "errors" @@ -10,6 +11,8 @@ import ( "sync" "time" + "golang.org/x/time/rate" + "goexplore/internal/explorer" ) @@ -34,6 +37,7 @@ type Transfer struct { Status Status `json:"status"` Error string `json:"error,omitempty"` Verify bool `json:"verify"` + LimitMBps int `json:"limit_mbps"` srcExp explorer.Explorer dstExp explorer.Explorer @@ -89,8 +93,19 @@ func (m *Manager) doTransfer(t *Transfer) error { hash := md5.New() tr := io.TeeReader(r, hash) + var streamReader io.Reader = tr + if t.LimitMBps > 0 { + limitBytes := t.LimitMBps * 1024 * 1024 + // Set burst to limitBytes to allow typical io.Copy chunk sizes without erroring + limiter := rate.NewLimiter(rate.Limit(limitBytes), limitBytes) + streamReader = &throttledReader{ + r: tr, + limiter: limiter, + } + } + pr := &progressReader{ - r: tr, + r: streamReader, t: t, } @@ -118,6 +133,21 @@ func (m *Manager) doTransfer(t *Transfer) error { return nil } +type throttledReader struct { + r io.Reader + limiter *rate.Limiter +} + +func (tr *throttledReader) Read(p []byte) (int, error) { + n, err := tr.r.Read(p) + if n > 0 && tr.limiter != nil { + if errWait := tr.limiter.WaitN(context.Background(), n); errWait != nil { + return n, errWait + } + } + return n, err +} + type progressReader struct { r io.Reader t *Transfer @@ -138,7 +168,7 @@ func (pr *progressReader) Read(p []byte) (int, error) { return n, err } -func (m *Manager) QueueTransfer(id, srcPath, dstPath, filename string, size int64, srcExp, dstExp explorer.Explorer, verify bool) error { +func (m *Manager) QueueTransfer(id, srcPath, dstPath, filename string, size int64, srcExp, dstExp explorer.Explorer, verify bool, limitMBps int) error { if srcExp == nil || dstExp == nil { return errors.New("invalid explorers") } @@ -151,6 +181,7 @@ func (m *Manager) QueueTransfer(id, srcPath, dstPath, filename string, size int6 BytesTotal: size, Status: StatusQueued, Verify: verify, + LimitMBps: limitMBps, srcExp: srcExp, dstExp: dstExp, }