add: cancel transfer
This commit is contained in:
@@ -4,4 +4,5 @@ TODO
|
||||
- [x] 文件夹传输
|
||||
- [x] 多样化图标
|
||||
- [ ] 加密传输
|
||||
- [ ] 取消传输
|
||||
- [x] 取消传输
|
||||
- [ ] 多文件发送
|
||||
@@ -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);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
- {{ formatSpeed(props.transfer.progress.speed) }}</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'completed'"
|
||||
type="success">
|
||||
- Completed</n-text
|
||||
- Completed</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'error'"
|
||||
type="error">
|
||||
- {{ props.transfer.error_msg || "Error" }}</n-text
|
||||
- {{ props.transfer.error_msg || "Error" }}</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'canceled'"
|
||||
type="error">
|
||||
- Canceled</n-text
|
||||
>
|
||||
<n-text
|
||||
depth="3"
|
||||
v-if="props.transfer.status === 'rejected'"
|
||||
type="error">
|
||||
- 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>
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
21
internal/transfer/reader.go
Normal file
21
internal/transfer/reader.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
94
internal/transfer/service.go
Normal file
94
internal/transfer/service.go
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user