if the file being received already exists locally, it will be renamed
This commit is contained in:
@@ -42,8 +42,8 @@ builds:
|
|||||||
hooks:
|
hooks:
|
||||||
pre:
|
pre:
|
||||||
- "wails3 generate icons -input goreleaser/icon.png -windowsfilename goreleaser/icon.ico"
|
- "wails3 generate icons -input goreleaser/icon.png -windowsfilename goreleaser/icon.ico"
|
||||||
- "wails3 generate syso -arch amd64 -icon goreleaser/icon.ico -manifest goreleaser/wails.exe.manifest -info goreleaser/info.json -out goreleaser/wails_windows_amd64.syso"
|
- "wails3 generate syso -arch amd64 -icon goreleaser/icon.ico -manifest goreleaser/wails.exe.manifest -info goreleaser/info.json -out wails_windows_amd64.syso"
|
||||||
post: "rm -f goreleaser/wails_windows_amd64.syso"
|
post: "rm -f wails_windows_amd64.syso"
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- formats: ["tar.gz"]
|
- formats: ["tar.gz"]
|
||||||
|
|||||||
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@@ -10,10 +10,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"mode": "auto",
|
"mode": "auto",
|
||||||
"program": "${workspaceFolder}",
|
"program": "${workspaceFolder}",
|
||||||
"args": [
|
"buildFlags": "-tags=gtk4",
|
||||||
"-tags",
|
|
||||||
"gtk4"
|
|
||||||
],
|
|
||||||
"preLaunchTask": "build frontend"
|
"preLaunchTask": "build frontend"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,9 +21,6 @@ export enum Language {
|
|||||||
export class WindowState {
|
export class WindowState {
|
||||||
"Width": number;
|
"Width": number;
|
||||||
"Height": number;
|
"Height": number;
|
||||||
"X": number;
|
|
||||||
"Y": number;
|
|
||||||
"Maximised": boolean;
|
|
||||||
|
|
||||||
/** Creates a new WindowState instance. */
|
/** Creates a new WindowState instance. */
|
||||||
constructor($$source: Partial<WindowState> = {}) {
|
constructor($$source: Partial<WindowState> = {}) {
|
||||||
@@ -33,15 +30,6 @@ export class WindowState {
|
|||||||
if (!("Height" in $$source)) {
|
if (!("Height" in $$source)) {
|
||||||
this["Height"] = 0;
|
this["Height"] = 0;
|
||||||
}
|
}
|
||||||
if (!("X" in $$source)) {
|
|
||||||
this["X"] = 0;
|
|
||||||
}
|
|
||||||
if (!("Y" in $$source)) {
|
|
||||||
this["Y"] = 0;
|
|
||||||
}
|
|
||||||
if (!("Maximised" in $$source)) {
|
|
||||||
this["Maximised"] = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(this, $$source);
|
Object.assign(this, $$source);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ export class Transfer {
|
|||||||
"sender": discovery$0.Peer;
|
"sender": discovery$0.Peer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* FileName 如果 ContentType 为 file,文件名;如果 ContentType 为 folder,文件夹名;如果 ContentType 为 text,空
|
||||||
* 文件名
|
* 文件名
|
||||||
*/
|
*/
|
||||||
"file_name": string;
|
"file_name": string;
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ export function ResolvePendingRequest(id: string, accept: boolean, savePath: str
|
|||||||
return $Call.ByID(207902967, id, accept, savePath);
|
return $Call.ByID(207902967, id, accept, savePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SaveHistory(): $CancellablePromise<void> {
|
export function SaveHistory(transfers: ($models.Transfer | null)[]): $CancellablePromise<void> {
|
||||||
return $Call.ByID(713135400);
|
return $Call.ByID(713135400, transfers);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SendFile(target: discovery$0.Peer | null, targetIP: string, filePath: string): $CancellablePromise<void> {
|
export function SendFile(target: discovery$0.Peer | null, targetIP: string, filePath: string): $CancellablePromise<void> {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
"0000": {
|
"0000": {
|
||||||
"ProductVersion": "0.1.0",
|
"ProductVersion": "0.1.0",
|
||||||
"CompanyName": "Nite",
|
"CompanyName": "Nite",
|
||||||
"FileDescription": "A mesh-drop application",
|
"FileDescription": "MeshDrop - A cross-platform file transfer application",
|
||||||
"LegalCopyright": "© 2026, Nite",
|
"LegalCopyright": "© 2026, Nite",
|
||||||
"ProductName": "Nite",
|
"ProductName": "MeshDrop",
|
||||||
"Comments": "A mesh-drop application"
|
"Comments": "A cross-platform file transfer application"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,9 +15,6 @@ import (
|
|||||||
type WindowState struct {
|
type WindowState struct {
|
||||||
Width int `mapstructure:"width"`
|
Width int `mapstructure:"width"`
|
||||||
Height int `mapstructure:"height"`
|
Height int `mapstructure:"height"`
|
||||||
X int `mapstructure:"x"`
|
|
||||||
Y int `mapstructure:"y"`
|
|
||||||
Maximised bool `mapstructure:"maximised"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var Version = "next"
|
var Version = "next"
|
||||||
@@ -50,14 +47,6 @@ type Config struct {
|
|||||||
data configData
|
data configData
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认窗口配置
|
|
||||||
var defaultWindowState = WindowState{
|
|
||||||
Width: 1024,
|
|
||||||
Height: 768,
|
|
||||||
X: -1,
|
|
||||||
Y: -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetConfigDir() string {
|
func GetConfigDir() string {
|
||||||
configPath, err := os.UserConfigDir()
|
configPath, err := os.UserConfigDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,7 +64,7 @@ func GetUserHomeDir() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New 读取配置
|
// New 读取配置
|
||||||
func Load() *Config {
|
func Load(defaultState WindowState) *Config {
|
||||||
v := viper.New()
|
v := viper.New()
|
||||||
configDir := GetConfigDir()
|
configDir := GetConfigDir()
|
||||||
err := os.MkdirAll(configDir, 0755)
|
err := os.MkdirAll(configDir, 0755)
|
||||||
@@ -86,7 +75,7 @@ func Load() *Config {
|
|||||||
|
|
||||||
// 设置默认值
|
// 设置默认值
|
||||||
defaultSavePath := filepath.Join(GetUserHomeDir(), "Downloads")
|
defaultSavePath := filepath.Join(GetUserHomeDir(), "Downloads")
|
||||||
v.SetDefault("window_state", defaultWindowState)
|
v.SetDefault("window_state", defaultState)
|
||||||
v.SetDefault("save_path", defaultSavePath)
|
v.SetDefault("save_path", defaultSavePath)
|
||||||
defaultHostName, err := os.Hostname()
|
defaultHostName, err := os.Hostname()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Service) SaveHistory() {
|
func (s *Service) SaveHistory(transfers []*Transfer) {
|
||||||
if !s.config.GetSaveHistory() {
|
if !s.config.GetSaveHistory() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
transfers := s.GetTransferList()
|
|
||||||
configDir := config.GetConfigDir()
|
configDir := config.GetConfigDir()
|
||||||
historyPath := filepath.Join(configDir, "history.json")
|
historyPath := filepath.Join(configDir, "history.json")
|
||||||
historyJson, err := json.Marshal(transfers)
|
historyJson, err := json.Marshal(transfers)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type Transfer struct {
|
|||||||
ID string `json:"id" binding:"required"` // 传输会话 ID
|
ID string `json:"id" binding:"required"` // 传输会话 ID
|
||||||
CreateTime int64 `json:"create_time"` // 创建时间
|
CreateTime int64 `json:"create_time"` // 创建时间
|
||||||
Sender discovery.Peer `json:"sender" binding:"required"` // 发送者
|
Sender discovery.Peer `json:"sender" binding:"required"` // 发送者
|
||||||
|
// FileName 如果 ContentType 为 file,文件名;如果 ContentType 为 folder,文件夹名;如果 ContentType 为 text,空
|
||||||
FileName string `json:"file_name"` // 文件名
|
FileName string `json:"file_name"` // 文件名
|
||||||
FileSize int64 `json:"file_size"` // 文件大小 (字节)
|
FileSize int64 `json:"file_size"` // 文件大小 (字节)
|
||||||
SavePath string `json:"savePath"` // 保存路径
|
SavePath string `json:"savePath"` // 保存路径
|
||||||
|
|||||||
@@ -175,6 +175,14 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
switch task.ContentType {
|
switch task.ContentType {
|
||||||
case ContentTypeFile:
|
case ContentTypeFile:
|
||||||
destPath := filepath.Join(savePath, task.FileName)
|
destPath := filepath.Join(savePath, task.FileName)
|
||||||
|
// 如果文件已存在则在文件名后追加序号
|
||||||
|
_, err := os.Stat(destPath)
|
||||||
|
counter := 1
|
||||||
|
for err == nil {
|
||||||
|
destPath = filepath.Join(savePath, fmt.Sprintf("%s (%d)%s", strings.TrimSuffix(task.FileName, filepath.Ext(task.FileName)), counter, filepath.Ext(task.FileName)))
|
||||||
|
counter++
|
||||||
|
_, err = os.Stat(destPath)
|
||||||
|
}
|
||||||
file, err := os.Create(destPath)
|
file, err := os.Create(destPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// 接收方无法创建文件,直接报错,任务结束
|
// 接收方无法创建文件,直接报错,任务结束
|
||||||
@@ -189,17 +197,17 @@ func (s *Service) handleUpload(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
s.receive(c, task, file, ctxReader)
|
s.receive(c, task, Writer{w: file, filePath: destPath}, ctxReader)
|
||||||
case ContentTypeText:
|
case ContentTypeText:
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
s.receive(c, task, &buf, ctxReader)
|
s.receive(c, task, Writer{w: &buf, filePath: ""}, ctxReader)
|
||||||
task.Text = buf.String()
|
task.Text = buf.String()
|
||||||
case ContentTypeFolder:
|
case ContentTypeFolder:
|
||||||
s.receiveFolder(c, savePath, task, ctxReader)
|
s.receiveFolder(c, savePath, task, ctxReader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer, ctxReader io.Reader) {
|
func (s *Service) receive(c *gin.Context, task *Transfer, writer Writer, ctxReader io.Reader) {
|
||||||
// 包装 reader,用于计算进度
|
// 包装 reader,用于计算进度
|
||||||
reader := &PassThroughReader{
|
reader := &PassThroughReader{
|
||||||
Reader: ctxReader,
|
Reader: ctxReader,
|
||||||
@@ -248,6 +256,11 @@ func (s *Service) receive(c *gin.Context, task *Transfer, writer io.Writer, ctxR
|
|||||||
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()
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
if task.ContentType == ContentTypeFile && writer.GetFilePath() != "" {
|
||||||
|
_ = os.Remove(writer.GetFilePath())
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +278,14 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer,
|
|||||||
|
|
||||||
// 创建根目录
|
// 创建根目录
|
||||||
destPath := filepath.Join(savePath, task.FileName)
|
destPath := filepath.Join(savePath, task.FileName)
|
||||||
|
// 如果文件已存在则在文件名后追加序号
|
||||||
|
_, err := os.Stat(destPath)
|
||||||
|
counter := 1
|
||||||
|
for err == nil {
|
||||||
|
destPath = filepath.Join(savePath, fmt.Sprintf("%s (%d)", task.FileName, counter))
|
||||||
|
counter++
|
||||||
|
_, err = os.Stat(destPath)
|
||||||
|
}
|
||||||
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,
|
||||||
|
|||||||
@@ -128,13 +128,13 @@ func (s *Service) StoreTransfersToList(transfers []*Transfer) {
|
|||||||
for _, transfer := range transfers {
|
for _, transfer := range transfers {
|
||||||
s.transferList.Store(transfer.ID, transfer)
|
s.transferList.Store(transfer.ID, transfer)
|
||||||
}
|
}
|
||||||
s.SaveHistory()
|
s.SaveHistory(transfers)
|
||||||
s.NotifyTransferListUpdate()
|
s.NotifyTransferListUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) StoreTransferToList(transfer *Transfer) {
|
func (s *Service) StoreTransferToList(transfer *Transfer) {
|
||||||
s.transferList.Store(transfer.ID, transfer)
|
s.transferList.Store(transfer.ID, transfer)
|
||||||
s.SaveHistory()
|
s.SaveHistory([]*Transfer{transfer})
|
||||||
s.NotifyTransferListUpdate()
|
s.NotifyTransferListUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,12 +154,12 @@ func (s *Service) CleanFinishedTransferList() {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
s.SaveHistory()
|
s.SaveHistory(s.GetTransferList())
|
||||||
s.NotifyTransferListUpdate()
|
s.NotifyTransferListUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) DeleteTransfer(transferID string) {
|
func (s *Service) DeleteTransfer(transferID string) {
|
||||||
s.transferList.Delete(transferID)
|
s.transferList.Delete(transferID)
|
||||||
s.SaveHistory()
|
s.SaveHistory(s.GetTransferList())
|
||||||
s.NotifyTransferListUpdate()
|
s.NotifyTransferListUpdate()
|
||||||
}
|
}
|
||||||
|
|||||||
16
internal/transfer/writer.go
Normal file
16
internal/transfer/writer.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package transfer
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type Writer struct {
|
||||||
|
w io.Writer
|
||||||
|
filePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Writer) Write(p []byte) (n int, err error) {
|
||||||
|
return w.w.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w Writer) GetFilePath() string {
|
||||||
|
return w.filePath
|
||||||
|
}
|
||||||
70
main.go
70
main.go
@@ -42,9 +42,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewApp() *App {
|
func NewApp() *App {
|
||||||
conf := config.Load()
|
|
||||||
app := application.New(application.Options{
|
app := application.New(application.Options{
|
||||||
Name: "mesh-drop",
|
Name: "MeshDrop",
|
||||||
Assets: application.AssetOptions{
|
Assets: application.AssetOptions{
|
||||||
Handler: application.AssetFileServerFS(assets),
|
Handler: application.AssetFileServerFS(assets),
|
||||||
},
|
},
|
||||||
@@ -54,12 +53,28 @@ func NewApp() *App {
|
|||||||
Icon: icon,
|
Icon: icon,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 获取默认屏幕大小
|
||||||
|
defaultWidth := 1024
|
||||||
|
defaultHeight := 768
|
||||||
|
|
||||||
|
screen := app.Screen.GetPrimary()
|
||||||
|
if screen != nil {
|
||||||
|
defaultWidth = int(float64(screen.Size.Width) * 0.8)
|
||||||
|
defaultHeight = int(float64(screen.Size.Height) * 0.8)
|
||||||
|
slog.Info("Primary screen found", "width", screen.Size.Width, "height", screen.Size.Height, "defaultWidth", defaultWidth, "defaultHeight", defaultHeight)
|
||||||
|
} else {
|
||||||
|
slog.Info("No primary screen found, using defaults")
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := config.Load(config.WindowState{
|
||||||
|
Width: defaultWidth,
|
||||||
|
Height: defaultHeight,
|
||||||
|
})
|
||||||
|
|
||||||
win := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
win := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||||
Title: "mesh drop",
|
Title: "MeshDrop",
|
||||||
Width: conf.GetWindowState().Width,
|
Width: conf.GetWindowState().Width,
|
||||||
Height: conf.GetWindowState().Height,
|
Height: conf.GetWindowState().Height,
|
||||||
X: conf.GetWindowState().X,
|
|
||||||
Y: conf.GetWindowState().Y,
|
|
||||||
EnableFileDrop: true,
|
EnableFileDrop: true,
|
||||||
Linux: application.LinuxWindow{
|
Linux: application.LinuxWindow{
|
||||||
WebviewGpuPolicy: application.WebviewGpuPolicyAlways,
|
WebviewGpuPolicy: application.WebviewGpuPolicyAlways,
|
||||||
@@ -114,7 +129,7 @@ func (a *App) registerCustomEvents() {
|
|||||||
application.RegisterEvent[application.Void]("transfer:refreshList")
|
application.RegisterEvent[application.Void]("transfer:refreshList")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) setupWindowEvents() {
|
func (a *App) setupEvents() {
|
||||||
// 窗口文件拖拽事件
|
// 窗口文件拖拽事件
|
||||||
a.mainWindows.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
|
a.mainWindows.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) {
|
||||||
files := event.Context().DroppedFiles()
|
files := event.Context().DroppedFiles()
|
||||||
@@ -125,17 +140,26 @@ func (a *App) setupWindowEvents() {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 应用关闭事件
|
// 窗口关闭事件
|
||||||
a.app.OnShutdown(func() {
|
a.mainWindows.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
|
||||||
x, y := a.mainWindows.Position()
|
if a.conf.GetCloseToSystray() {
|
||||||
width, height := a.mainWindows.Size()
|
event.Cancel()
|
||||||
|
a.mainWindows.Hide()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w, h := a.mainWindows.Size()
|
||||||
|
|
||||||
a.conf.SetWindowState(config.WindowState{
|
a.conf.SetWindowState(config.WindowState{
|
||||||
X: x,
|
Width: w,
|
||||||
Y: y,
|
Height: h,
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
slog.Info("Window closed", "width", w, "height", h)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 应用关闭事件
|
||||||
|
a.app.OnShutdown(func() {
|
||||||
// 保存传输历史
|
// 保存传输历史
|
||||||
if a.conf.GetSaveHistory() {
|
if a.conf.GetSaveHistory() {
|
||||||
// 将 pending 状态的任务改为 canceled
|
// 将 pending 状态的任务改为 canceled
|
||||||
@@ -145,13 +169,8 @@ func (a *App) setupWindowEvents() {
|
|||||||
task.Status = transfer.TransferStatusCanceled
|
task.Status = transfer.TransferStatusCanceled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.transferService.SaveHistory()
|
// 保存传输历史
|
||||||
}
|
a.transferService.SaveHistory(t)
|
||||||
|
|
||||||
// 保存配置
|
|
||||||
err := a.conf.Save()
|
|
||||||
if err != nil {
|
|
||||||
slog.Error("Failed to save config", "error", err)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -176,20 +195,13 @@ func (a *App) setupSystray() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
systray.SetMenu(menu)
|
systray.SetMenu(menu)
|
||||||
|
|
||||||
a.mainWindows.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) {
|
|
||||||
if a.conf.GetCloseToSystray() {
|
|
||||||
event.Cancel()
|
|
||||||
a.mainWindows.Hide()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Run() {
|
func (a *App) Run() {
|
||||||
a.registerServices()
|
a.registerServices()
|
||||||
a.setupSystray()
|
a.setupSystray()
|
||||||
a.registerCustomEvents()
|
a.registerCustomEvents()
|
||||||
a.setupWindowEvents()
|
a.setupEvents()
|
||||||
err := a.app.Run()
|
err := a.app.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
Reference in New Issue
Block a user