u
This commit is contained in:
65
README.md
65
README.md
@@ -1,59 +1,8 @@
|
||||
# Welcome to Your New Wails3 Project!
|
||||
TODO
|
||||
|
||||
Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Navigate to your project directory in the terminal.
|
||||
|
||||
2. To run your application in development mode, use the following command:
|
||||
|
||||
```
|
||||
wails3 dev
|
||||
```
|
||||
|
||||
This will start your application and enable hot-reloading for both frontend and backend changes.
|
||||
|
||||
3. To build your application for production, use:
|
||||
|
||||
```
|
||||
wails3 build
|
||||
```
|
||||
|
||||
This will create a production-ready executable in the `build` directory.
|
||||
|
||||
## Exploring Wails3 Features
|
||||
|
||||
Now that you have your project set up, it's time to explore the features that Wails3 offers:
|
||||
|
||||
1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications.
|
||||
|
||||
2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
|
||||
|
||||
```
|
||||
go run .
|
||||
```
|
||||
|
||||
Note: Some examples may be under development during the alpha phase.
|
||||
|
||||
3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references.
|
||||
|
||||
4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions).
|
||||
|
||||
## Project Structure
|
||||
|
||||
Take a moment to familiarize yourself with your project structure:
|
||||
|
||||
- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript)
|
||||
- `main.go`: The entry point of your Go backend
|
||||
- `app.go`: Define your application structure and methods here
|
||||
- `wails.json`: Configuration file for your Wails project
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Modify the frontend in the `frontend/` directory to create your desired UI.
|
||||
2. Add backend functionality in `main.go`.
|
||||
3. Use `wails3 dev` to see your changes in real-time.
|
||||
4. When ready, build your application with `wails3 build`.
|
||||
|
||||
Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community.
|
||||
- [ ] 断点续传
|
||||
- [x] 加密传输
|
||||
- [x] 剪辑板传输
|
||||
- [ ] 文件夹传输
|
||||
- [x] 多样化图标
|
||||
- [ ] 取消传输
|
||||
@@ -35,6 +35,10 @@ export function SendFile(target: discovery$0.Peer | null, targetIP: string, file
|
||||
return $Call.ByID(2954589433, target, targetIP, filePath);
|
||||
}
|
||||
|
||||
export function SendFolder(target: discovery$0.Peer | null, targetIP: string, folderPath: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3258308403, target, targetIP, folderPath);
|
||||
}
|
||||
|
||||
export function SendText(target: discovery$0.Peer | null, targetIP: string, text: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(1497421440, target, targetIP, text);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
GetTransferList,
|
||||
SendFile,
|
||||
SendText,
|
||||
SendFolder,
|
||||
} from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
|
||||
@@ -115,8 +116,6 @@ const pendingCount = computed(() => {
|
||||
|
||||
// --- 操作 ---
|
||||
|
||||
const dialog = useDialog();
|
||||
|
||||
const handleSendFile = async (ip: string) => {
|
||||
try {
|
||||
const filePath = await Dialogs.OpenFile({
|
||||
@@ -134,9 +133,21 @@ const handleSendFile = async (ip: string) => {
|
||||
};
|
||||
|
||||
const handleSendFolder = async (ip: string) => {
|
||||
// TODO
|
||||
const opts: Dialogs.OpenFileDialogOptions = {
|
||||
Title: "Select folder to send",
|
||||
CanChooseDirectories: true,
|
||||
CanChooseFiles: false,
|
||||
AllowsMultipleSelection: false,
|
||||
};
|
||||
const folderPath = await Dialogs.OpenFile(opts);
|
||||
if (!folderPath) return;
|
||||
const peer = await GetPeerByIP(ip);
|
||||
if (!peer) return;
|
||||
await SendFolder(peer, ip, folderPath as string);
|
||||
activeKey.value = "transfers";
|
||||
};
|
||||
|
||||
const dialog = useDialog();
|
||||
const handleSendText = (ip: string) => {
|
||||
const textContent = ref("");
|
||||
const d = dialog.create({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { computed, h } from "vue";
|
||||
import {
|
||||
NCard,
|
||||
NButton,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
NText,
|
||||
NTag,
|
||||
useMessage,
|
||||
NInput,
|
||||
} from "naive-ui";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
@@ -16,12 +17,17 @@ import {
|
||||
faArrowDown,
|
||||
faCircleExclamation,
|
||||
faUser,
|
||||
faFile,
|
||||
faFileLines,
|
||||
faFolder,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Transfer } from "../../bindings/mesh-drop/internal/transfer";
|
||||
import { ResolvePendingRequest } from "../../bindings/mesh-drop/internal/transfer/service";
|
||||
import { Dialogs, Clipboard } from "@wailsio/runtime";
|
||||
|
||||
import { useDialog } from "naive-ui";
|
||||
|
||||
const props = defineProps<{
|
||||
transfer: Transfer;
|
||||
}>();
|
||||
@@ -86,6 +92,20 @@ const handleCopy = async () => {
|
||||
message.error("Failed to copy to clipboard");
|
||||
});
|
||||
};
|
||||
|
||||
const dialog = useDialog();
|
||||
const handleOpen = async () => {
|
||||
const d = dialog.create({
|
||||
title: "Text Content",
|
||||
content: () =>
|
||||
h(NInput, {
|
||||
value: props.transfer.text,
|
||||
readonly: true,
|
||||
type: "textarea",
|
||||
rows: 10,
|
||||
}),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -114,15 +134,27 @@ const handleCopy = async () => {
|
||||
v-if="props.transfer.content_type === 'file'"
|
||||
strong
|
||||
class="filename"
|
||||
:title="props.transfer.file_name"
|
||||
>{{ props.transfer.file_name }}</n-text
|
||||
>
|
||||
:title="props.transfer.file_name">
|
||||
<n-icon>
|
||||
<FontAwesomeIcon :icon="faFile" />
|
||||
</n-icon>
|
||||
{{ props.transfer.file_name }}
|
||||
</n-text>
|
||||
<n-text
|
||||
v-else-if="props.transfer.content_type === 'text'"
|
||||
strong
|
||||
class="filename"
|
||||
title="Text"
|
||||
>Text</n-text
|
||||
title="Text">
|
||||
<n-icon> <FontAwesomeIcon :icon="faFileLines" /> </n-icon>
|
||||
Text</n-text
|
||||
>
|
||||
<n-text
|
||||
v-else-if="props.transfer.content_type === 'folder'"
|
||||
strong
|
||||
class="filename"
|
||||
title="Folder">
|
||||
<n-icon> <FontAwesomeIcon :icon="faFolder" /> </n-icon>
|
||||
{{ props.transfer.file_name || "Folder" }}</n-text
|
||||
>
|
||||
<n-tag
|
||||
size="small"
|
||||
@@ -208,7 +240,7 @@ const handleCopy = async () => {
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 复制按钮 -->
|
||||
<!-- 文本传输按钮 -->
|
||||
<div
|
||||
class="actions-wrapper"
|
||||
v-if="
|
||||
@@ -217,6 +249,9 @@ const handleCopy = async () => {
|
||||
props.transfer.content_type === 'text'
|
||||
">
|
||||
<n-space>
|
||||
<n-button size="small" type="success" @click="handleOpen"
|
||||
>Open</n-button
|
||||
>
|
||||
<n-button size="small" type="success" @click="handleCopy"
|
||||
>Copy</n-button
|
||||
>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"math"
|
||||
"mesh-drop/internal/discovery"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -44,6 +46,39 @@ func (s *Service) SendFile(target *discovery.Peer, targetIP string, filePath str
|
||||
s.processTransfer(target, targetIP, task, file)
|
||||
}
|
||||
|
||||
func (s *Service) SendFolder(target *discovery.Peer, targetIP string, folderPath string) {
|
||||
size, err := calculateTarSize(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(),
|
||||
FileName: filepath.Base(folderPath),
|
||||
FileSize: size,
|
||||
Sender: Sender{
|
||||
ID: s.discoveryService.GetID(),
|
||||
Name: s.discoveryService.GetName(),
|
||||
},
|
||||
Type: TransferTypeSend,
|
||||
Status: TransferStatusPending,
|
||||
ContentType: ContentTypeFolder,
|
||||
}
|
||||
|
||||
s.processTransfer(target, targetIP, task, r)
|
||||
}
|
||||
|
||||
func (s *Service) SendText(target *discovery.Peer, targetIP string, text string) {
|
||||
reader := bytes.NewReader([]byte(text))
|
||||
task := Transfer{
|
||||
@@ -62,6 +97,116 @@ func (s *Service) SendText(target *discovery.Peer, targetIP string, text string)
|
||||
s.processTransfer(target, targetIP, task, reader)
|
||||
}
|
||||
|
||||
type countWriter struct {
|
||||
n int64
|
||||
}
|
||||
|
||||
func (w *countWriter) Write(p []byte) (int, error) {
|
||||
w.n += int64(len(p))
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func calculateTarSize(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
|
||||
}
|
||||
|
||||
// 计算相对路径
|
||||
relPath, err := filepath.Rel(srcPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 使用 tar.FileInfoHeader 计算 header
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 保持与 streamFolderToTar 一致
|
||||
header.Name = filepath.ToSlash(relPath)
|
||||
if info.IsDir() {
|
||||
header.Name += "/"
|
||||
}
|
||||
|
||||
cw := &countWriter{}
|
||||
tw := tar.NewWriter(cw)
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
// tw.WriteHeader 写入 header blocks(包括扩展头)
|
||||
size += cw.n
|
||||
|
||||
if !info.IsDir() {
|
||||
// 文件内容大小 + 填充
|
||||
fileSize := info.Size()
|
||||
blocks := math.Ceil(float64(fileSize) / 512)
|
||||
size += int64(blocks) * 512
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// 两个 512 字节的空块作为结束标记
|
||||
size += 1024
|
||||
|
||||
return size, err
|
||||
}
|
||||
|
||||
func streamFolderToTar(w io.Writer, srcPath string) error {
|
||||
tw := tar.NewWriter(w)
|
||||
defer tw.Close()
|
||||
|
||||
return filepath.Walk(srcPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(srcPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if relPath == "." {
|
||||
return nil
|
||||
}
|
||||
slog.Debug("Processing file", "path", path, "relPath", relPath, "component", "transfer-client")
|
||||
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// tar 文件名使用正斜杠
|
||||
header.Name = filepath.ToSlash(relPath)
|
||||
if info.IsDir() {
|
||||
header.Name += "/"
|
||||
}
|
||||
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if _, err := io.Copy(tw, file); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package transfer
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -215,7 +217,7 @@ func (s *Service) handleUpload(c *gin.Context) {
|
||||
s.transferList.Store(task.ID, task)
|
||||
s.app.Event.Emit("transfer:refreshList")
|
||||
case ContentTypeFolder:
|
||||
// s.receiveFolder(c, savePath, task)
|
||||
s.receiveFolder(c, savePath, &task)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +232,7 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
|
||||
Total: total,
|
||||
Speed: speed,
|
||||
}
|
||||
s.transferList.Store(task.ID, task)
|
||||
s.transferList.Store(task.ID, *task)
|
||||
s.app.Event.Emit("transfer:refreshList")
|
||||
},
|
||||
}
|
||||
@@ -245,7 +247,7 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
|
||||
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.transferList.Store(task.ID, *task)
|
||||
// 通知前端传输失败
|
||||
s.app.Event.Emit("transfer:refreshList")
|
||||
return
|
||||
@@ -257,7 +259,91 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer) {
|
||||
})
|
||||
// 传输成功,任务结束
|
||||
task.Status = TransferStatusCompleted
|
||||
s.transferList.Store(task.ID, task)
|
||||
s.transferList.Store(task.ID, *task)
|
||||
s.app.Event.Emit("transfer:refreshList")
|
||||
}
|
||||
|
||||
func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer) {
|
||||
// 创建根目录
|
||||
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",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 包装 reader,用于计算进度
|
||||
reader := &PassThroughReader{
|
||||
Reader: c.Request.Body,
|
||||
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")
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
target := filepath.Join(destPath, header.Name)
|
||||
// 确保路径没有越界
|
||||
if !strings.HasPrefix(target, filepath.Clean(destPath)+string(os.PathSeparator)) {
|
||||
// 非法路径
|
||||
continue
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(target, 0755); err != nil {
|
||||
slog.Error("Failed to create dir", "path", target, "error", err)
|
||||
}
|
||||
case tar.TypeReg:
|
||||
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
|
||||
if err != nil {
|
||||
slog.Error("Failed to create file", "path", target, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, TransferUploadResponse{
|
||||
ID: task.ID,
|
||||
Message: "Folder received successfully",
|
||||
})
|
||||
task.Progress.Total = task.FileSize
|
||||
task.Progress.Current = task.FileSize
|
||||
task.Status = TransferStatusCompleted
|
||||
s.transferList.Store(task.ID, *task)
|
||||
s.app.Event.Emit("transfer:refreshList")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user