add: cancel transfer
This commit is contained in:
@@ -4,4 +4,5 @@ TODO
|
|||||||
- [x] 文件夹传输
|
- [x] 文件夹传输
|
||||||
- [x] 多样化图标
|
- [x] 多样化图标
|
||||||
- [ ] 加密传输
|
- [ ] 加密传输
|
||||||
- [ ] 取消传输
|
- [x] 取消传输
|
||||||
|
- [ ] 多文件发送
|
||||||
@@ -13,10 +13,21 @@ import * as discovery$0 from "../discovery/models.js";
|
|||||||
// @ts-ignore: Unused imports
|
// @ts-ignore: Unused imports
|
||||||
import * as $models from "./models.js";
|
import * as $models from "./models.js";
|
||||||
|
|
||||||
|
export function CancelTransfer(transferID: string): $CancellablePromise<void> {
|
||||||
|
return $Call.ByID(900002248, transferID);
|
||||||
|
}
|
||||||
|
|
||||||
export function GetPort(): $CancellablePromise<number> {
|
export function GetPort(): $CancellablePromise<number> {
|
||||||
return $Call.ByID(4195335736);
|
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[]> {
|
export function GetTransferList(): $CancellablePromise<$models.Transfer[]> {
|
||||||
return $Call.ByID(584162076).then(($result: any) => {
|
return $Call.ByID(584162076).then(($result: any) => {
|
||||||
return $$createType1($result);
|
return $$createType1($result);
|
||||||
|
|||||||
@@ -51,9 +51,10 @@ const showMobileMenu = ref(false);
|
|||||||
const isMobile = ref(false);
|
const isMobile = ref(false);
|
||||||
|
|
||||||
// 监听窗口大小变化更新 isMobile
|
// 监听窗口大小变化更新 isMobile
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
checkMobile();
|
checkMobile();
|
||||||
window.addEventListener("resize", checkMobile);
|
window.addEventListener("resize", checkMobile);
|
||||||
|
transferList.value = await GetTransferList();
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
@@ -83,7 +84,12 @@ const menuOptions = computed<MenuOption[]>(() => [
|
|||||||
[
|
[
|
||||||
"Transfers",
|
"Transfers",
|
||||||
pendingCount.value > 0 ?
|
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,
|
: null,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -124,8 +130,8 @@ const handleSendFile = async (ip: string) => {
|
|||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
const peer = await GetPeerByIP(ip);
|
const peer = await GetPeerByIP(ip);
|
||||||
if (!peer) return;
|
if (!peer) return;
|
||||||
await SendFile(peer, ip, filePath);
|
|
||||||
activeKey.value = "transfers";
|
activeKey.value = "transfers";
|
||||||
|
await SendFile(peer, ip, filePath);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Failed to send file: " + e);
|
alert("Failed to send file: " + e);
|
||||||
@@ -143,8 +149,8 @@ const handleSendFolder = async (ip: string) => {
|
|||||||
if (!folderPath) return;
|
if (!folderPath) return;
|
||||||
const peer = await GetPeerByIP(ip);
|
const peer = await GetPeerByIP(ip);
|
||||||
if (!peer) return;
|
if (!peer) return;
|
||||||
await SendFolder(peer, ip, folderPath as string);
|
|
||||||
activeKey.value = "transfers";
|
activeKey.value = "transfers";
|
||||||
|
await SendFolder(peer, ip, folderPath as string);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
@@ -167,8 +173,8 @@ const handleSendText = (ip: string) => {
|
|||||||
try {
|
try {
|
||||||
const peer = await GetPeerByIP(ip);
|
const peer = await GetPeerByIP(ip);
|
||||||
if (!peer) return;
|
if (!peer) return;
|
||||||
await SendText(peer, ip, textContent.value);
|
|
||||||
activeKey.value = "transfers";
|
activeKey.value = "transfers";
|
||||||
|
await SendText(peer, ip, textContent.value);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
alert("Failed to send text: " + e);
|
alert("Failed to send text: " + e);
|
||||||
@@ -185,8 +191,8 @@ const handleSendClipboard = async (ip: string) => {
|
|||||||
}
|
}
|
||||||
const peer = await GetPeerByIP(ip);
|
const peer = await GetPeerByIP(ip);
|
||||||
if (!peer) return;
|
if (!peer) return;
|
||||||
await SendText(peer, ip, text);
|
|
||||||
activeKey.value = "transfers";
|
activeKey.value = "transfers";
|
||||||
|
await SendText(peer, ip, text);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeTransfer = (id: string) => {
|
const removeTransfer = (id: string) => {
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ import {
|
|||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
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 { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||||
|
|
||||||
import { useDialog } from "naive-ui";
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -177,19 +201,31 @@ const handleOpen = async () => {
|
|||||||
<!-- 状态文本(进行中/已完成) -->
|
<!-- 状态文本(进行中/已完成) -->
|
||||||
<span>
|
<span>
|
||||||
<n-text depth="3" v-if="props.transfer.status === 'active'">
|
<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
|
<n-text
|
||||||
depth="3"
|
depth="3"
|
||||||
v-if="props.transfer.status === 'completed'"
|
v-if="props.transfer.status === 'completed'"
|
||||||
type="success">
|
type="success">
|
||||||
- Completed</n-text
|
- Completed</n-text
|
||||||
>
|
>
|
||||||
<n-text
|
<n-text
|
||||||
depth="3"
|
depth="3"
|
||||||
v-if="props.transfer.status === 'error'"
|
v-if="props.transfer.status === 'error'"
|
||||||
type="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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,17 +294,16 @@ const handleOpen = async () => {
|
|||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 发送方取消按钮 -->
|
<!-- 取消按钮 -->
|
||||||
<div
|
<div class="actions-wrapper" v-if="canCancel">
|
||||||
class="actions-wrapper"
|
|
||||||
v-if="
|
|
||||||
props.transfer.type === 'send' &&
|
|
||||||
props.transfer.status !== 'completed'
|
|
||||||
">
|
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button size="small" type="error" ghost @click="">
|
<n-button
|
||||||
Cancel
|
size="small"
|
||||||
</n-button>
|
type="error"
|
||||||
|
ghost
|
||||||
|
@click="CancelTransfer(props.transfer.id)"
|
||||||
|
>Cancel</n-button
|
||||||
|
>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package transfer
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -18,6 +20,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath string) {
|
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)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to open file", "path", filePath, "error", err, "component", "transfer-client")
|
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{
|
task := Transfer{
|
||||||
ID: uuid.New().String(),
|
ID: taskID,
|
||||||
FileName: filepath.Base(filePath),
|
FileName: filepath.Base(filePath),
|
||||||
FileSize: stat.Size(),
|
FileSize: stat.Size(),
|
||||||
Sender: Sender{
|
Sender: Sender{
|
||||||
@@ -43,28 +55,52 @@ func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath str
|
|||||||
ContentType: ContentTypeFile,
|
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) {
|
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 {
|
if err != nil {
|
||||||
slog.Error("Failed to calculate folder size", "path", folderPath, "error", err, "component", "transfer-client")
|
slog.Error("Failed to calculate folder size", "path", folderPath, "error", err, "component", "transfer-client")
|
||||||
return
|
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{
|
task := Transfer{
|
||||||
ID: uuid.New().String(),
|
ID: taskID,
|
||||||
FileName: filepath.Base(folderPath),
|
FileName: filepath.Base(folderPath),
|
||||||
FileSize: size,
|
FileSize: size,
|
||||||
Sender: Sender{
|
Sender: Sender{
|
||||||
@@ -76,13 +112,51 @@ func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath
|
|||||||
ContentType: ContentTypeFolder,
|
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) {
|
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{
|
task := Transfer{
|
||||||
ID: uuid.New().String(),
|
ID: taskID,
|
||||||
FileName: "",
|
FileName: "",
|
||||||
FileSize: int64(len(text)),
|
FileSize: int64(len(text)),
|
||||||
Sender: Sender{
|
Sender: Sender{
|
||||||
@@ -94,7 +168,135 @@ func (s *Service) SendText(target *discovery.Peer, targetIP string, text string)
|
|||||||
ContentType: ContentTypeText,
|
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 {
|
type countWriter struct {
|
||||||
@@ -106,12 +308,15 @@ func (w *countWriter) Write(p []byte) (int, error) {
|
|||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculateTarSize(srcPath string) (int64, error) {
|
func calculateTarSize(ctx context.Context, srcPath string) (int64, error) {
|
||||||
var size int64
|
var size int64
|
||||||
err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
// 计算相对路径
|
// 计算相对路径
|
||||||
relPath, err := filepath.Rel(srcPath, path)
|
relPath, err := filepath.Rel(srcPath, path)
|
||||||
@@ -158,7 +363,7 @@ func calculateTarSize(srcPath string) (int64, error) {
|
|||||||
return size, err
|
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)
|
tw := tar.NewWriter(w)
|
||||||
defer tw.Close()
|
defer tw.Close()
|
||||||
|
|
||||||
@@ -166,6 +371,9 @@ func streamFolderToTar(w io.Writer, srcPath string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
relPath, err := filepath.Rel(srcPath, path)
|
relPath, err := filepath.Rel(srcPath, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -206,98 +414,3 @@ func streamFolderToTar(w io.Writer, srcPath string) error {
|
|||||||
return nil
|
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")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -75,4 +75,5 @@ type TransferAskResponse struct {
|
|||||||
type TransferUploadResponse struct {
|
type TransferUploadResponse struct {
|
||||||
ID string `json:"id"` // 传输会话 ID
|
ID string `json:"id"` // 传输会话 ID
|
||||||
Message string `json:"message"`
|
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 (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"mesh-drop/internal/discovery"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"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 处理接收文件请求
|
// handleAsk 处理接收文件请求
|
||||||
func (s *Service) handleAsk(c *gin.Context) {
|
func (s *Service) handleAsk(c *gin.Context) {
|
||||||
var task Transfer
|
var task Transfer
|
||||||
|
|
||||||
// Gin 的 BindJSON 自动处理 JSON 解析
|
|
||||||
if err := c.ShouldBindJSON(&task); err != nil {
|
if err := c.ShouldBindJSON(&task); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, TransferAskResponse{
|
c.JSON(http.StatusBadRequest, TransferAskResponse{
|
||||||
ID: task.ID,
|
ID: task.ID,
|
||||||
@@ -113,7 +63,8 @@ func (s *Service) handleAsk(c *gin.Context) {
|
|||||||
Accepted: decision.Accepted,
|
Accepted: decision.Accepted,
|
||||||
Token: task.Token,
|
Token: task.Token,
|
||||||
})
|
})
|
||||||
case <-c.Done():
|
s.app.Event.Emit("transfer:refreshList")
|
||||||
|
case <-c.Request.Context().Done():
|
||||||
// 发送端放弃
|
// 发送端放弃
|
||||||
task.Status = TransferStatusCanceled
|
task.Status = TransferStatusCanceled
|
||||||
s.transferList.Store(task.ID, task)
|
s.transferList.Store(task.ID, task)
|
||||||
@@ -147,6 +98,7 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
c.JSON(http.StatusBadRequest, TransferUploadResponse{
|
c.JSON(http.StatusBadRequest, TransferUploadResponse{
|
||||||
ID: id,
|
ID: id,
|
||||||
Message: "Invalid request: missing id or token",
|
Message: "Invalid request: missing id or token",
|
||||||
|
Status: TransferStatusError,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -157,16 +109,24 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
c.JSON(http.StatusUnauthorized, TransferUploadResponse{
|
c.JSON(http.StatusUnauthorized, TransferUploadResponse{
|
||||||
ID: id,
|
ID: id,
|
||||||
Message: "Invalid request: task not found",
|
Message: "Invalid request: task not found",
|
||||||
|
Status: TransferStatusError,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
task := val.(Transfer)
|
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
|
// 校验 token
|
||||||
if task.Token != token {
|
if task.Token != token {
|
||||||
c.JSON(http.StatusUnauthorized, TransferUploadResponse{
|
c.JSON(http.StatusUnauthorized, TransferUploadResponse{
|
||||||
ID: id,
|
ID: id,
|
||||||
Message: "Token mismatch",
|
Message: "Token mismatch",
|
||||||
|
Status: TransferStatusError,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -176,6 +136,7 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
c.JSON(http.StatusForbidden, TransferUploadResponse{
|
c.JSON(http.StatusForbidden, TransferUploadResponse{
|
||||||
ID: id,
|
ID: id,
|
||||||
Message: "Invalid task status",
|
Message: "Invalid task status",
|
||||||
|
Status: TransferStatusError,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -190,6 +151,11 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
savePath = s.savePath
|
savePath = s.savePath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctxReader := &ContextReader{
|
||||||
|
ctx: ctx,
|
||||||
|
r: c.Request.Body,
|
||||||
|
}
|
||||||
|
|
||||||
switch task.ContentType {
|
switch task.ContentType {
|
||||||
case ContentTypeFile:
|
case ContentTypeFile:
|
||||||
destPath := filepath.Join(savePath, task.FileName)
|
destPath := filepath.Join(savePath, task.FileName)
|
||||||
@@ -199,6 +165,7 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
||||||
ID: task.ID,
|
ID: task.ID,
|
||||||
Message: "Receiver failed to create file",
|
Message: "Receiver failed to create file",
|
||||||
|
Status: TransferStatusError,
|
||||||
})
|
})
|
||||||
slog.Error("Failed to create file", "error", err, "component", "transfer")
|
slog.Error("Failed to create file", "error", err, "component", "transfer")
|
||||||
task.Status = TransferStatusError
|
task.Status = TransferStatusError
|
||||||
@@ -209,22 +176,22 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
s.receive(c, &task, file)
|
s.receive(c, &task, file, ctxReader)
|
||||||
case ContentTypeText:
|
case ContentTypeText:
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
s.receive(c, &task, &buf)
|
s.receive(c, &task, &buf, ctxReader)
|
||||||
task.Text = buf.String()
|
task.Text = buf.String()
|
||||||
s.transferList.Store(task.ID, task)
|
s.transferList.Store(task.ID, task)
|
||||||
s.app.Event.Emit("transfer:refreshList")
|
s.app.Event.Emit("transfer:refreshList")
|
||||||
case ContentTypeFolder:
|
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,用于计算进度
|
||||||
reader := &PassThroughReader{
|
reader := &PassThroughReader{
|
||||||
Reader: c.Request.Body,
|
Reader: ctxReader,
|
||||||
total: task.FileSize,
|
total: task.FileSize,
|
||||||
callback: func(current, total int64, speed float64) {
|
callback: func(current, total int64, speed float64) {
|
||||||
task.Progress = Progress{
|
task.Progress = Progress{
|
||||||
@@ -239,16 +206,42 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
|
|||||||
|
|
||||||
_, err := io.Copy(writer, reader)
|
_, err := io.Copy(writer, reader)
|
||||||
if err != nil {
|
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{
|
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
||||||
ID: task.ID,
|
ID: task.ID,
|
||||||
Message: "Failed to write file",
|
Message: "Failed to write file",
|
||||||
|
Status: TransferStatusError,
|
||||||
})
|
})
|
||||||
slog.Error("Failed to write file", "error", err, "component", "transfer")
|
slog.Error("Failed to write file", "error", err, "component", "transfer")
|
||||||
task.Status = TransferStatusError
|
task.Status = TransferStatusError
|
||||||
task.ErrorMsg = fmt.Errorf("failed to write file: %v", err).Error()
|
task.ErrorMsg = fmt.Errorf("failed to write file: %v", err).Error()
|
||||||
s.transferList.Store(task.ID, *task)
|
s.transferList.Store(task.ID, *task)
|
||||||
// 通知前端传输失败
|
|
||||||
s.app.Event.Emit("transfer:refreshList")
|
s.app.Event.Emit("transfer:refreshList")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -256,6 +249,7 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
|
|||||||
c.JSON(http.StatusOK, TransferUploadResponse{
|
c.JSON(http.StatusOK, TransferUploadResponse{
|
||||||
ID: task.ID,
|
ID: task.ID,
|
||||||
Message: "File received successfully",
|
Message: "File received successfully",
|
||||||
|
Status: TransferStatusCompleted,
|
||||||
})
|
})
|
||||||
// 传输成功,任务结束
|
// 传输成功,任务结束
|
||||||
task.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")
|
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)
|
destPath := filepath.Join(savePath, task.FileName)
|
||||||
if err := os.MkdirAll(destPath, 0755); err != nil {
|
if err := os.MkdirAll(destPath, 0755); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
||||||
ID: task.ID,
|
ID: task.ID,
|
||||||
Message: "Receiver failed to create folder",
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 包装 reader,用于计算进度
|
// 包装 reader,用于计算进度
|
||||||
reader := &PassThroughReader{
|
reader := &PassThroughReader{
|
||||||
Reader: c.Request.Body,
|
Reader: ctxReader,
|
||||||
total: task.FileSize,
|
total: task.FileSize,
|
||||||
callback: func(current, total int64, speed float64) {
|
callback: func(current, total int64, speed float64) {
|
||||||
task.Progress = Progress{
|
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)
|
tr := tar.NewReader(reader)
|
||||||
for {
|
for {
|
||||||
header, err := tr.Next()
|
header, err := tr.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if handleError(err, "read_tar_header") {
|
||||||
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
|
||||||
ID: task.ID,
|
|
||||||
Message: "Stream error",
|
|
||||||
})
|
|
||||||
slog.Error("Tar stream error", "error", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,13 +363,10 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer)
|
|||||||
|
|
||||||
if _, err := io.Copy(f, tr); err != nil {
|
if _, err := io.Copy(f, tr); err != nil {
|
||||||
f.Close()
|
f.Close()
|
||||||
slog.Error("Failed to write file", "path", target, "error", err)
|
if handleError(err, "write_file_content") {
|
||||||
c.JSON(http.StatusInternalServerError, TransferUploadResponse{
|
|
||||||
ID: task.ID,
|
|
||||||
Message: "Write error",
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,12 +381,3 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer)
|
|||||||
s.transferList.Store(task.ID, *task)
|
s.transferList.Store(task.ID, *task)
|
||||||
s.app.Event.Emit("transfer:refreshList")
|
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