feat: enable changelog generation in release workflow #2
@@ -167,7 +167,7 @@ func (a *App) Delete(connId, path string) error {
|
|||||||
return exp.Delete(path)
|
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)
|
srcExp, err := a.getExplorerForConnection(srcConnId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -185,7 +185,7 @@ func (a *App) QueueTransfer(id string, srcConnId, dstConnId, srcPath, dstPath, f
|
|||||||
return err
|
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 {
|
func (a *App) GetTransfers() []*transfer.Transfer {
|
||||||
@@ -232,7 +232,7 @@ func (a *App) PromptUploadFiles(connId, destPath string) error {
|
|||||||
remotePath = destPath + fileName
|
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
|
return nil
|
||||||
}
|
}
|
||||||
@@ -272,7 +272,7 @@ func (a *App) PromptUploadDirectory(connId, destPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
id := uuid.New().String()
|
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
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -304,7 +304,7 @@ func (a *App) PromptDownload(connId, remotePath string) error {
|
|||||||
size = stat.Size
|
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 {
|
type TransferItem struct {
|
||||||
@@ -314,7 +314,7 @@ type TransferItem struct {
|
|||||||
Size int64 `json:"size"`
|
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 {
|
for _, item := range items {
|
||||||
if !item.IsDir {
|
if !item.IsDir {
|
||||||
id := uuid.New().String()
|
id := uuid.New().String()
|
||||||
@@ -328,18 +328,18 @@ func (a *App) TransferItems(srcConnId, dstConnId, dstPath string, items []Transf
|
|||||||
remotePath = dstPath + item.Name
|
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
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Run remote walk in goroutine to not block UI
|
// 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
|
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)
|
baseName := filepath.Base(srcDirPath)
|
||||||
|
|
||||||
newDstPath := dstBasePath
|
newDstPath := dstBasePath
|
||||||
@@ -360,11 +360,11 @@ func (a *App) transferRemoteDirectory(srcConnId, dstConnId, srcDirPath, dstBaseP
|
|||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir {
|
if entry.IsDir {
|
||||||
a.transferRemoteDirectory(srcConnId, dstConnId, entry.Path, newDstPath, verify)
|
a.transferRemoteDirectory(srcConnId, dstConnId, entry.Path, newDstPath, verify, limitMBps)
|
||||||
} else {
|
} else {
|
||||||
id := uuid.New().String()
|
id := uuid.New().String()
|
||||||
itemDstPath := newDstPath + "/" + entry.Name
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -185,6 +185,12 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem; flex-direction: row; margin-bottom: 1.5rem;">
|
||||||
|
<label style="margin: 0; width: 120px;">Max Speed (MB/s)</label>
|
||||||
|
<input type="number" id="transfer-limit" value="0" min="0" style="width: 80px;">
|
||||||
|
<span style="font-size: 0.75rem; color: var(--text-secondary);">(0 = Unlimited)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 1rem;">
|
<div style="display: flex; justify-content: flex-end; gap: 1rem;">
|
||||||
<button class="btn" style="background: transparent; color: var(--text-primary); border-color: var(--border);" onclick="closeTransferModal()">Cancel</button>
|
<button class="btn" style="background: transparent; color: var(--text-primary); border-color: var(--border);" onclick="closeTransferModal()">Cancel</button>
|
||||||
<button class="btn btn-primary" onclick="executeTransfer()">Transfer Here</button>
|
<button class="btn btn-primary" onclick="executeTransfer()">Transfer Here</button>
|
||||||
|
|||||||
@@ -529,6 +529,7 @@ window.openTransferModal = () => {
|
|||||||
window.closeTransferModal = () => {
|
window.closeTransferModal = () => {
|
||||||
document.getElementById('transfer-modal').style.display = 'none';
|
document.getElementById('transfer-modal').style.display = 'none';
|
||||||
document.getElementById('transfer-verify').checked = false;
|
document.getElementById('transfer-verify').checked = false;
|
||||||
|
document.getElementById('transfer-limit').value = "0";
|
||||||
};
|
};
|
||||||
|
|
||||||
window.loadTransferDestRoot = () => {
|
window.loadTransferDestRoot = () => {
|
||||||
@@ -583,7 +584,9 @@ window.loadTransferDestDirectory = async (connId, path) => {
|
|||||||
window.executeTransfer = async () => {
|
window.executeTransfer = async () => {
|
||||||
if (selectedItems.length === 0) return;
|
if (selectedItems.length === 0) return;
|
||||||
try {
|
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();
|
closeTransferModal();
|
||||||
showTransfers();
|
showTransfers();
|
||||||
selectedItems = [];
|
selectedItems = [];
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ require (
|
|||||||
golang.org/x/net v0.55.0 // indirect
|
golang.org/x/net v0.55.0 // indirect
|
||||||
golang.org/x/sys v0.46.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.38.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
|
// replace github.com/wailsapp/wails/v2 v2.12.0 => /home/jamie/go/pkg/mod
|
||||||
|
|||||||
@@ -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.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 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
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=
|
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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package transfer
|
package transfer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -10,6 +11,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"goexplore/internal/explorer"
|
"goexplore/internal/explorer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,6 +37,7 @@ type Transfer struct {
|
|||||||
Status Status `json:"status"`
|
Status Status `json:"status"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Verify bool `json:"verify"`
|
Verify bool `json:"verify"`
|
||||||
|
LimitMBps int `json:"limit_mbps"`
|
||||||
|
|
||||||
srcExp explorer.Explorer
|
srcExp explorer.Explorer
|
||||||
dstExp explorer.Explorer
|
dstExp explorer.Explorer
|
||||||
@@ -89,8 +93,19 @@ func (m *Manager) doTransfer(t *Transfer) error {
|
|||||||
hash := md5.New()
|
hash := md5.New()
|
||||||
tr := io.TeeReader(r, hash)
|
tr := io.TeeReader(r, hash)
|
||||||
|
|
||||||
pr := &progressReader{
|
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,
|
r: tr,
|
||||||
|
limiter: limiter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pr := &progressReader{
|
||||||
|
r: streamReader,
|
||||||
t: t,
|
t: t,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +133,21 @@ func (m *Manager) doTransfer(t *Transfer) error {
|
|||||||
return nil
|
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 {
|
type progressReader struct {
|
||||||
r io.Reader
|
r io.Reader
|
||||||
t *Transfer
|
t *Transfer
|
||||||
@@ -138,7 +168,7 @@ func (pr *progressReader) Read(p []byte) (int, error) {
|
|||||||
return n, err
|
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 {
|
if srcExp == nil || dstExp == nil {
|
||||||
return errors.New("invalid explorers")
|
return errors.New("invalid explorers")
|
||||||
}
|
}
|
||||||
@@ -151,6 +181,7 @@ func (m *Manager) QueueTransfer(id, srcPath, dstPath, filename string, size int6
|
|||||||
BytesTotal: size,
|
BytesTotal: size,
|
||||||
Status: StatusQueued,
|
Status: StatusQueued,
|
||||||
Verify: verify,
|
Verify: verify,
|
||||||
|
LimitMBps: limitMBps,
|
||||||
srcExp: srcExp,
|
srcExp: srcExp,
|
||||||
dstExp: dstExp,
|
dstExp: dstExp,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user