add: cancel transfer

This commit is contained in:
2026-02-04 15:06:41 +08:00
parent c2f3c2c3df
commit 68533dad31
9 changed files with 529 additions and 221 deletions

View File

@@ -4,4 +4,5 @@ TODO
- [x] 文件夹传输
- [x] 多样化图标
- [ ] 加密传输
- [ ] 取消传输
- [x] 取消传输
- [ ] 多文件发送

View File

@@ -13,10 +13,21 @@ import * as discovery$0 from "../discovery/models.js";
// @ts-ignore: Unused imports
import * as $models from "./models.js";
export function CancelTransfer(transferID: string): $CancellablePromise<void> {
return $Call.ByID(900002248, transferID);
}
export function GetPort(): $CancellablePromise<number> {
return $Call.ByID(4195335736);
}
export function GetTransfer(transferID: string): $CancellablePromise<[$models.Transfer, boolean]> {
return $Call.ByID(1198637268, transferID).then(($result: any) => {
$result[0] = $$createType0($result[0]);
return $result;
});
}
export function GetTransferList(): $CancellablePromise<$models.Transfer[]> {
return $Call.ByID(584162076).then(($result: any) => {
return $$createType1($result);

View File

@@ -51,9 +51,10 @@ const showMobileMenu = ref(false);
const isMobile = ref(false);
// 监听窗口大小变化更新 isMobile
onMounted(() => {
onMounted(async () => {
checkMobile();
window.addEventListener("resize", checkMobile);
transferList.value = await GetTransferList();
});
const checkMobile = () => {
@@ -83,7 +84,12 @@ const menuOptions = computed<MenuOption[]>(() => [
[
"Transfers",
pendingCount.value > 0 ?
h(NBadge, { value: pendingCount.value, max: 99, type: "error" })
h(NBadge, {
style: "display: inline-flex; align-items: center",
value: pendingCount.value,
max: 99,
type: "error",
})
: null,
],
),
@@ -124,8 +130,8 @@ const handleSendFile = async (ip: string) => {
if (!filePath) return;
const peer = await GetPeerByIP(ip);
if (!peer) return;
await SendFile(peer, ip, filePath);
activeKey.value = "transfers";
await SendFile(peer, ip, filePath);
} catch (e: any) {
console.error(e);
alert("Failed to send file: " + e);
@@ -143,8 +149,8 @@ const handleSendFolder = async (ip: string) => {
if (!folderPath) return;
const peer = await GetPeerByIP(ip);
if (!peer) return;
await SendFolder(peer, ip, folderPath as string);
activeKey.value = "transfers";
await SendFolder(peer, ip, folderPath as string);
};
const dialog = useDialog();
@@ -167,8 +173,8 @@ const handleSendText = (ip: string) => {
try {
const peer = await GetPeerByIP(ip);
if (!peer) return;
await SendText(peer, ip, textContent.value);
activeKey.value = "transfers";
await SendText(peer, ip, textContent.value);
} catch (e: any) {
console.error(e);
alert("Failed to send text: " + e);
@@ -185,8 +191,8 @@ const handleSendClipboard = async (ip: string) => {
}
const peer = await GetPeerByIP(ip);
if (!peer) return;
await SendText(peer, ip, text);
activeKey.value = "transfers";
await SendText(peer, ip, text);
};
const removeTransfer = (id: string) => {

View File

@@ -23,7 +23,10 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
import { ResolvePendingRequest } from "../../bindings/mesh-drop/internal/transfer/service";
import {
ResolvePendingRequest,
CancelTransfer,
} from "../../bindings/mesh-drop/internal/transfer/service";
import { Dialogs, Clipboard } from "@wailsio/runtime";
import { useDialog } from "naive-ui";
@@ -106,6 +109,27 @@ const handleOpen = async () => {
}),
});
};
const canCancel = computed(() => {
if (
props.transfer.status === "completed" ||
props.transfer.status === "error" ||
props.transfer.status === "canceled" ||
props.transfer.status === "rejected"
) {
return false;
}
if (props.transfer.type === "send") {
return true;
} else if (props.transfer.type === "receive") {
// 接收端在 pending 状态只能拒绝不能取消
if (props.transfer.status === "pending") {
return false;
}
return true;
}
return false;
});
</script>
<template>
@@ -177,19 +201,31 @@ const handleOpen = async () => {
<!-- 状态文本进行中/已完成 -->
<span>
<n-text depth="3" v-if="props.transfer.status === 'active'">
- {{ formatSpeed(props.transfer.progress.speed) }}</n-text
&nbsp;- {{ formatSpeed(props.transfer.progress.speed) }}</n-text
>
<n-text
depth="3"
v-if="props.transfer.status === 'completed'"
type="success">
- Completed</n-text
&nbsp;- Completed</n-text
>
<n-text
depth="3"
v-if="props.transfer.status === 'error'"
type="error">
- {{ props.transfer.error_msg || "Error" }}</n-text
&nbsp;- {{ props.transfer.error_msg || "Error" }}</n-text
>
<n-text
depth="3"
v-if="props.transfer.status === 'canceled'"
type="error">
&nbsp;- Canceled</n-text
>
<n-text
depth="3"
v-if="props.transfer.status === 'rejected'"
type="error">
&nbsp;- Rejected</n-text
>
</span>
</div>
@@ -258,17 +294,16 @@ const handleOpen = async () => {
</n-space>
</div>
<!-- 发送方取消按钮 -->
<div
class="actions-wrapper"
v-if="
props.transfer.type === 'send' &&
props.transfer.status !== 'completed'
">
<!-- 取消按钮 -->
<div class="actions-wrapper" v-if="canCancel">
<n-space>
<n-button size="small" type="error" ghost @click="">
Cancel
</n-button>
<n-button
size="small"
type="error"
ghost
@click="CancelTransfer(props.transfer.id)"
>Cancel</n-button
>
</n-space>
</div>
</div>

View File

@@ -3,7 +3,9 @@ package transfer
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
@@ -18,6 +20,16 @@ import (
)
func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath string) {
taskID := uuid.New().String()
ctx, cancel := context.WithCancel(context.Background())
s.cancelMap.Store(taskID, cancel)
// 任务结束后清理 ctx
defer func() {
s.cancelMap.Delete(taskID)
cancel()
}()
file, err := os.Open(filePath)
if err != nil {
slog.Error("Failed to open file", "path", filePath, "error", err, "component", "transfer-client")
@@ -31,7 +43,7 @@ func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath str
}
task := Transfer{
ID: uuid.New().String(),
ID: taskID,
FileName: filepath.Base(filePath),
FileSize: stat.Size(),
Sender: Sender{
@@ -43,28 +55,52 @@ func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath str
ContentType: ContentTypeFile,
}
s.processTransfer(target, targetIP, task, file)
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
askResp, err := s.ask(ctx, target, targetIP, task)
if err != nil {
if errors.Is(err, context.Canceled) {
task.Status = TransferStatusCanceled
} else {
// 如果请求发送失败,更新状态为 Error
task.Status = TransferStatusError
task.ErrorMsg = fmt.Sprintf("Failed to connect to receiver: %v", err)
}
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
if askResp.Accepted {
s.processTransfer(ctx, askResp, target, targetIP, task, file)
} else {
// 接收方拒绝
task.Status = TransferStatusRejected
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
}
func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath string) {
size, err := calculateTarSize(folderPath)
taskID := uuid.New().String()
ctx, cancel := context.WithCancel(context.Background())
s.cancelMap.Store(taskID, cancel)
// 任务结束后清理 ctx
defer func() {
s.cancelMap.Delete(taskID)
cancel()
}()
size, err := calculateTarSize(ctx, folderPath)
if err != nil {
slog.Error("Failed to calculate folder size", "path", folderPath, "error", err, "component", "transfer-client")
return
}
r, w := io.Pipe()
go func() {
defer w.Close()
if err := streamFolderToTar(w, folderPath); err != nil {
slog.Error("Failed to stream folder to tar", "error", err, "component", "transfer-client")
w.CloseWithError(err)
}
}()
task := Transfer{
ID: uuid.New().String(),
ID: taskID,
FileName: filepath.Base(folderPath),
FileSize: size,
Sender: Sender{
@@ -76,13 +112,51 @@ func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath
ContentType: ContentTypeFolder,
}
s.processTransfer(target, targetIP, task, r)
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
askResp, err := s.ask(ctx, target, targetIP, task)
if err != nil {
// 如果请求发送失败,更新状态为 Error
task.Status = TransferStatusError
task.ErrorMsg = fmt.Sprintf("Failed to connect to receiver: %v", err)
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
if askResp.Accepted {
r, w := io.Pipe()
go func(ctx context.Context) {
defer w.Close()
if err := streamFolderToTar(ctx, w, folderPath); err != nil {
slog.Error("Failed to stream folder to tar", "error", err, "component", "transfer-client")
w.CloseWithError(err)
}
}(ctx)
s.processTransfer(ctx, askResp, target, targetIP, task, r)
} else {
// 接收方拒绝
task.Status = TransferStatusRejected
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
}
func (s *Service) SendText(target *discovery.Peer, targetIP string, text string) {
reader := bytes.NewReader([]byte(text))
taskID := uuid.New().String()
ctx, cancel := context.WithCancel(context.Background())
s.cancelMap.Store(taskID, cancel)
// 任务结束后清理 ctx
defer func() {
s.cancelMap.Delete(taskID)
cancel()
}()
r := bytes.NewReader([]byte(text))
task := Transfer{
ID: uuid.New().String(),
ID: taskID,
FileName: "",
FileSize: int64(len(text)),
Sender: Sender{
@@ -94,7 +168,135 @@ func (s *Service) SendText(target *discovery.Peer, targetIP string, text string)
ContentType: ContentTypeText,
}
s.processTransfer(target, targetIP, task, reader)
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
askResp, err := s.ask(ctx, target, targetIP, task)
if err != nil {
// 如果请求发送失败,更新状态为 Error
task.Status = TransferStatusError
task.ErrorMsg = fmt.Sprintf("Failed to connect to receiver: %v", err)
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
if askResp.Accepted {
s.processTransfer(ctx, askResp, target, targetIP, task, r)
} else {
// 接收方拒绝
task.Status = TransferStatusRejected
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
}
// ask 向接收端发送传输请求
func (s *Service) ask(ctx context.Context, target *discovery.Peer, targetIP string, task Transfer) (TransferAskResponse, error) {
if err := ctx.Err(); err != nil {
return TransferAskResponse{}, err
}
// 发送请求
askBody, _ := json.Marshal(task)
askUrl := fmt.Sprintf("http://%s:%d/transfer/ask", targetIP, target.Port)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, askUrl, bytes.NewReader(askBody))
req.Header.Set("Content-Type", "application/json")
if err != nil {
return TransferAskResponse{}, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return TransferAskResponse{}, err
}
defer resp.Body.Close()
var askResp TransferAskResponse
if err := json.NewDecoder(resp.Body).Decode(&askResp); err != nil {
return TransferAskResponse{}, err
}
if resp.StatusCode != http.StatusOK {
return TransferAskResponse{}, errors.New(askResp.Message)
}
return askResp, nil
}
// processTransfer 传输数据
func (s *Service) processTransfer(ctx context.Context, askResp TransferAskResponse, target *discovery.Peer, targetIP string, task Transfer, payload io.Reader) {
if err := ctx.Err(); err != nil {
return
}
uploadUrl, _ := url.Parse(fmt.Sprintf("http://%s:%d/transfer/upload/%s", targetIP, target.Port, task.ID))
query := uploadUrl.Query()
query.Add("token", askResp.Token)
uploadUrl.RawQuery = query.Encode()
reader := &PassThroughReader{
Reader: payload,
total: task.FileSize,
callback: func(current, total int64, speed float64) {
task.Progress = Progress{
Current: current,
Total: total,
Speed: speed,
}
task.Status = TransferStatusActive
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
},
}
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadUrl.String(), reader)
if err != nil {
return
}
req.ContentLength = task.FileSize
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
if errors.Is(err, context.Canceled) {
task.Status = TransferStatusCanceled
} else {
task.Status = TransferStatusError
task.ErrorMsg = fmt.Sprintf("Failed to upload file: %v", err)
slog.Error("Failed to upload file", "url", uploadUrl.String(), "error", err, "component", "transfer-client")
}
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
defer resp.Body.Close()
var uploadResp TransferUploadResponse
if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
return
}
if resp.StatusCode != http.StatusOK {
task.Status = TransferStatusError
task.ErrorMsg = uploadResp.Message
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
// 判断任务完成还是被接收端取消
if uploadResp.Status == TransferStatusCanceled {
task.Status = TransferStatusCanceled
task.ErrorMsg = uploadResp.Message
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
// 传输成功,任务结束
task.Status = TransferStatusCompleted
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
}
type countWriter struct {
@@ -106,12 +308,15 @@ func (w *countWriter) Write(p []byte) (int, error) {
return len(p), nil
}
func calculateTarSize(srcPath string) (int64, error) {
func calculateTarSize(ctx context.Context, srcPath string) (int64, error) {
var size int64
err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
// 计算相对路径
relPath, err := filepath.Rel(srcPath, path)
@@ -158,7 +363,7 @@ func calculateTarSize(srcPath string) (int64, error) {
return size, err
}
func streamFolderToTar(w io.Writer, srcPath string) error {
func streamFolderToTar(ctx context.Context, w io.Writer, srcPath string) error {
tw := tar.NewWriter(w)
defer tw.Close()
@@ -166,6 +371,9 @@ func streamFolderToTar(w io.Writer, srcPath string) error {
if err != nil {
return err
}
if ctx.Err() != nil {
return ctx.Err()
}
relPath, err := filepath.Rel(srcPath, path)
if err != nil {
@@ -206,98 +414,3 @@ func streamFolderToTar(w io.Writer, srcPath string) error {
return nil
})
}
func (s *Service) processTransfer(target *discovery.Peer, targetIP string, task Transfer, payload io.Reader) {
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
// 发送请求
askBody, _ := json.Marshal(task)
askUrl := fmt.Sprintf("http://%s:%d/transfer/ask", targetIP, target.Port)
resp, err := http.Post(askUrl, "application/json", bytes.NewReader(askBody))
if err != nil {
slog.Error("Failed to send ask request", "url", askUrl, "error", err, "component", "transfer-client")
// 如果请求发送失败,更新状态为 Error
task.Status = TransferStatusError
task.ErrorMsg = fmt.Sprintf("Failed to connect to receiver: %v", err)
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
defer resp.Body.Close()
var askResp TransferAskResponse
if err := json.NewDecoder(resp.Body).Decode(&askResp); err != nil {
return
}
if resp.StatusCode != http.StatusOK {
task.Status = TransferStatusError
task.ErrorMsg = askResp.Message
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
if !askResp.Accepted {
// 接收方拒绝
task.Status = TransferStatusRejected
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
// 上传
uploadUrl, _ := url.Parse(fmt.Sprintf("http://%s:%d/transfer/upload/%s", targetIP, target.Port, task.ID))
query := uploadUrl.Query()
query.Add("token", askResp.Token)
uploadUrl.RawQuery = query.Encode()
reader := &PassThroughReader{
Reader: payload,
total: task.FileSize,
callback: func(current, total int64, speed float64) {
task.Progress = Progress{
Current: current,
Total: total,
Speed: speed,
}
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
},
}
req, err := http.NewRequest(http.MethodPut, uploadUrl.String(), reader)
if err != nil {
return
}
req.ContentLength = task.FileSize
req.Header.Set("Content-Type", "application/octet-stream")
resp, err = http.DefaultClient.Do(req)
if err != nil {
slog.Error("Failed to upload file", "url", uploadUrl.String(), "error", err, "component", "transfer-client")
return
}
defer resp.Body.Close()
var uploadResp TransferUploadResponse
if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
return
}
if resp.StatusCode != http.StatusOK {
task.Status = TransferStatusError
task.ErrorMsg = uploadResp.Message
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
return
}
// 传输成功,任务结束
task.Status = TransferStatusCompleted
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
}

View File

@@ -73,6 +73,7 @@ type TransferAskResponse struct {
// TransferUploadResponse 上传回应
type TransferUploadResponse struct {
ID string `json:"id"` // 传输会话 ID
Message string `json:"message"`
ID string `json:"id"` // 传输会话 ID
Message string `json:"message"`
Status TransferStatus `json:"status"`
}

View File

@@ -0,0 +1,21 @@
package transfer
import (
"context"
"io"
)
// ContextReader 带有 Context 的 Reader
type ContextReader struct {
ctx context.Context
r io.Reader
}
func (cr *ContextReader) Read(p []byte) (n int, err error) {
select {
case <-cr.ctx.Done():
return 0, cr.ctx.Err() // 返回 context.Canceled 错误
default:
return cr.r.Read(p)
}
}

View File

@@ -3,74 +3,24 @@ package transfer
import (
"archive/tar"
"bytes"
"context"
"errors"
"fmt"
"io"
"log/slog"
"mesh-drop/internal/discovery"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/wailsapp/wails/v3/pkg/application"
)
type Service struct {
app *application.App
port int
savePath string // 默认下载目录
// pendingRequests 存储等待用户确认的通道
// Key: TransferID, Value: Transfer
transferList sync.Map
discoveryService *discovery.Service
}
func NewService(app *application.App, port int, defaultSavePath string, discoveryService *discovery.Service) *Service {
gin.SetMode(gin.ReleaseMode)
return &Service{
app: app,
port: port,
savePath: defaultSavePath,
discoveryService: discoveryService,
}
}
func init() {
application.RegisterEvent[application.Void]("transfer:refreshList")
}
func (s *Service) GetPort() int {
return s.port
}
func (s *Service) Start() {
r := gin.Default()
transfer := r.Group("/transfer")
{
transfer.POST("/ask", s.handleAsk)
transfer.PUT("/upload/:id", s.handleUpload)
}
go func() {
addr := fmt.Sprintf(":%d", s.port)
slog.Info("Transfer service listening", "address", addr, "component", "transfer")
if err := r.Run(addr); err != nil {
slog.Error("Transfer service error", "error", err, "component", "transfer")
}
}()
}
// handleAsk 处理接收文件请求
func (s *Service) handleAsk(c *gin.Context) {
var task Transfer
// Gin 的 BindJSON 自动处理 JSON 解析
if err := c.ShouldBindJSON(&task); err != nil {
c.JSON(http.StatusBadRequest, TransferAskResponse{
ID: task.ID,
@@ -113,7 +63,8 @@ func (s *Service) handleAsk(c *gin.Context) {
Accepted: decision.Accepted,
Token: task.Token,
})
case <-c.Done():
s.app.Event.Emit("transfer:refreshList")
case <-c.Request.Context().Done():
// 发送端放弃
task.Status = TransferStatusCanceled
s.transferList.Store(task.ID, task)
@@ -147,6 +98,7 @@ func (s *Service) handleUpload(c *gin.Context) {
c.JSON(http.StatusBadRequest, TransferUploadResponse{
ID: id,
Message: "Invalid request: missing id or token",
Status: TransferStatusError,
})
return
}
@@ -157,16 +109,24 @@ func (s *Service) handleUpload(c *gin.Context) {
c.JSON(http.StatusUnauthorized, TransferUploadResponse{
ID: id,
Message: "Invalid request: task not found",
Status: TransferStatusError,
})
return
}
task := val.(Transfer)
ctx, cancel := context.WithCancel(c.Request.Context())
s.cancelMap.Store(task.ID, cancel)
defer func() {
s.cancelMap.Delete(task.ID)
cancel()
}()
// 校验 token
if task.Token != token {
c.JSON(http.StatusUnauthorized, TransferUploadResponse{
ID: id,
Message: "Token mismatch",
Status: TransferStatusError,
})
return
}
@@ -176,6 +136,7 @@ func (s *Service) handleUpload(c *gin.Context) {
c.JSON(http.StatusForbidden, TransferUploadResponse{
ID: id,
Message: "Invalid task status",
Status: TransferStatusError,
})
return
}
@@ -190,6 +151,11 @@ func (s *Service) handleUpload(c *gin.Context) {
savePath = s.savePath
}
ctxReader := &ContextReader{
ctx: ctx,
r: c.Request.Body,
}
switch task.ContentType {
case ContentTypeFile:
destPath := filepath.Join(savePath, task.FileName)
@@ -199,6 +165,7 @@ func (s *Service) handleUpload(c *gin.Context) {
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
ID: task.ID,
Message: "Receiver failed to create file",
Status: TransferStatusError,
})
slog.Error("Failed to create file", "error", err, "component", "transfer")
task.Status = TransferStatusError
@@ -209,22 +176,22 @@ func (s *Service) handleUpload(c *gin.Context) {
return
}
defer file.Close()
s.receive(c, &task, file)
s.receive(c, &task, file, ctxReader)
case ContentTypeText:
var buf bytes.Buffer
s.receive(c, &task, &buf)
s.receive(c, &task, &buf, ctxReader)
task.Text = buf.String()
s.transferList.Store(task.ID, task)
s.app.Event.Emit("transfer:refreshList")
case ContentTypeFolder:
s.receiveFolder(c, savePath, &task)
s.receiveFolder(c, savePath, &task, ctxReader)
}
}
func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer, ctxReader io.Reader) {
// 包装 reader用于计算进度
reader := &PassThroughReader{
Reader: c.Request.Body,
Reader: ctxReader,
total: task.FileSize,
callback: func(current, total int64, speed float64) {
task.Progress = Progress{
@@ -239,16 +206,42 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
_, err := io.Copy(writer, reader)
if err != nil {
// 文件写入失败,直接报错,任务结束
// 发送端断线,任务取消
if c.Request.Context().Err() != nil {
slog.Info("Sender canceled transfer (Network/Context disconnected)", "id", task.ID, "raw_err", err)
task.ErrorMsg = "Sender disconnected"
task.Status = TransferStatusCanceled
s.transferList.Store(task.ID, *task)
s.app.Event.Emit("transfer:refreshList")
return
}
// 用户取消传输
if errors.Is(err, context.Canceled) {
slog.Info("User canceled transfer", "component", "transfer")
task.ErrorMsg = "User canceled transfer"
task.Status = TransferStatusCanceled
// 通知发送端
c.JSON(http.StatusOK, TransferUploadResponse{
ID: task.ID,
Message: "File transfer canceled",
Status: TransferStatusCanceled,
})
s.transferList.Store(task.ID, *task)
s.app.Event.Emit("transfer:refreshList")
return
}
// 接收端写文件失败
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
ID: task.ID,
Message: "Failed to write file",
Status: TransferStatusError,
})
slog.Error("Failed to write file", "error", err, "component", "transfer")
task.Status = TransferStatusError
task.ErrorMsg = fmt.Errorf("failed to write file: %v", err).Error()
s.transferList.Store(task.ID, *task)
// 通知前端传输失败
s.app.Event.Emit("transfer:refreshList")
return
}
@@ -256,6 +249,7 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
c.JSON(http.StatusOK, TransferUploadResponse{
ID: task.ID,
Message: "File received successfully",
Status: TransferStatusCompleted,
})
// 传输成功,任务结束
task.Status = TransferStatusCompleted
@@ -263,20 +257,26 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
s.app.Event.Emit("transfer:refreshList")
}
func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer) {
func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer, ctxReader io.Reader) {
// 创建根目录
destPath := filepath.Join(savePath, task.FileName)
if err := os.MkdirAll(destPath, 0755); err != nil {
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
ID: task.ID,
Message: "Receiver failed to create folder",
Status: TransferStatusError,
})
slog.Error("Failed to create folder", "error", err, "component", "transfer")
task.Status = TransferStatusError
task.ErrorMsg = fmt.Errorf("receiver failed to create folder: %v", err).Error()
s.transferList.Store(task.ID, *task)
s.app.Event.Emit("transfer:refreshList")
return
}
// 包装 reader用于计算进度
reader := &PassThroughReader{
Reader: c.Request.Body,
Reader: ctxReader,
total: task.FileSize,
callback: func(current, total int64, speed float64) {
task.Progress = Progress{
@@ -289,18 +289,56 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer)
},
}
handleError := func(err error, stage string) bool {
if err == nil {
return false
}
if c.Request.Context().Err() != nil {
slog.Info("Transfer canceled by sender (Network disconnect)", "id", task.ID, "stage", stage)
task.Status = TransferStatusCanceled
task.ErrorMsg = "Sender disconnected"
// 发送端已断开,无需也不应再发送 c.JSON
s.transferList.Store(task.ID, *task)
s.app.Event.Emit("transfer:refreshList")
return true
}
if errors.Is(err, context.Canceled) {
slog.Info("Transfer canceled by user", "id", task.ID, "stage", stage)
task.Status = TransferStatusCanceled
task.ErrorMsg = "User canceled transfer"
// 通知发送端(虽然此时连接可能即将关闭,但尽力通知)
c.JSON(http.StatusOK, TransferUploadResponse{
ID: task.ID,
Message: "File transfer canceled",
Status: TransferStatusCanceled,
})
s.transferList.Store(task.ID, *task)
s.app.Event.Emit("transfer:refreshList")
return true
}
slog.Error("Transfer failed", "error", err, "stage", stage)
task.Status = TransferStatusError
task.ErrorMsg = fmt.Sprintf("Failed at %s: %v", stage, err)
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
ID: task.ID,
Message: fmt.Sprintf("Transfer failed: %v", err),
Status: TransferStatusError,
})
s.transferList.Store(task.ID, *task)
s.app.Event.Emit("transfer:refreshList")
return true
}
tr := tar.NewReader(reader)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
ID: task.ID,
Message: "Stream error",
})
slog.Error("Tar stream error", "error", err)
if handleError(err, "read_tar_header") {
return
}
@@ -325,12 +363,9 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer)
if _, err := io.Copy(f, tr); err != nil {
f.Close()
slog.Error("Failed to write file", "path", target, "error", err)
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
ID: task.ID,
Message: "Write error",
})
return
if handleError(err, "write_file_content") {
return
}
}
f.Close()
}
@@ -346,12 +381,3 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer)
s.transferList.Store(task.ID, *task)
s.app.Event.Emit("transfer:refreshList")
}
func (s *Service) GetTransferList() []Transfer {
var requests []Transfer
s.transferList.Range(func(key, value any) bool {
requests = append(requests, value.(Transfer))
return true
})
return requests
}

View File

@@ -0,0 +1,94 @@
package transfer
import (
"context"
"fmt"
"log/slog"
"mesh-drop/internal/discovery"
"sync"
"github.com/gin-gonic/gin"
"github.com/wailsapp/wails/v3/pkg/application"
)
type Service struct {
app *application.App
port int
savePath string // 默认下载目录
// pendingRequests 存储等待用户确认的通道
// Key: TransferID, Value: Transfer
transferList sync.Map
discoveryService *discovery.Service
// cancelMap 存储取消操作的通道
// Key: TransferID, Value: context.CancelFunc
cancelMap sync.Map
}
func NewService(app *application.App, port int, defaultSavePath string, discoveryService *discovery.Service) *Service {
gin.SetMode(gin.ReleaseMode)
return &Service{
app: app,
port: port,
savePath: defaultSavePath,
discoveryService: discoveryService,
}
}
func init() {
application.RegisterEvent[application.Void]("transfer:refreshList")
}
func (s *Service) GetPort() int {
return s.port
}
func (s *Service) Start() {
r := gin.Default()
transfer := r.Group("/transfer")
{
transfer.POST("/ask", s.handleAsk)
transfer.PUT("/upload/:id", s.handleUpload)
}
go func() {
addr := fmt.Sprintf(":%d", s.port)
slog.Info("Transfer service listening", "address", addr, "component", "transfer")
if err := r.Run(addr); err != nil {
slog.Error("Transfer service error", "error", err, "component", "transfer")
}
}()
}
func (s *Service) GetTransferList() []Transfer {
var requests []Transfer
s.transferList.Range(func(key, value any) bool {
requests = append(requests, value.(Transfer))
return true
})
return requests
}
func (s *Service) GetTransfer(transferID string) (Transfer, bool) {
val, ok := s.transferList.Load(transferID)
if !ok {
return Transfer{}, false
}
return val.(Transfer), true
}
func (s *Service) CancelTransfer(transferID string) {
if cancel, ok := s.cancelMap.Load(transferID); ok {
cancel.(context.CancelFunc)()
s.cancelMap.Delete(transferID)
t, ok := s.GetTransfer(transferID)
if ok {
t.Status = TransferStatusCanceled
s.transferList.Store(transferID, t)
s.app.Event.Emit("transfer:refreshList")
}
}
}