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,
}