From e76bcd709cf16544bd541108782dec6895918c06 Mon Sep 17 00:00:00 2001 From: nite Date: Sat, 7 Feb 2026 19:39:14 +0800 Subject: [PATCH] if the file being received already exists locally, it will be renamed --- .goreleaser.yaml | 4 +- .vscode/launch.json | 5 +- .../mesh-drop/internal/config/models.ts | 12 ---- .../mesh-drop/internal/transfer/models.ts | 1 + .../mesh-drop/internal/transfer/service.ts | 4 +- goreleaser/info.json | 6 +- internal/config/config.go | 19 ++--- internal/transfer/history.go | 3 +- internal/transfer/model.go | 29 ++++---- internal/transfer/server.go | 27 ++++++- internal/transfer/service.go | 8 +-- internal/transfer/writer.go | 16 +++++ main.go | 70 +++++++++++-------- 13 files changed, 114 insertions(+), 90 deletions(-) create mode 100644 internal/transfer/writer.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index e237bf5..4fd5fd4 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -42,8 +42,8 @@ builds: hooks: pre: - "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" - post: "rm -f 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 wails_windows_amd64.syso" archives: - formats: ["tar.gz"] diff --git a/.vscode/launch.json b/.vscode/launch.json index 90a2d5a..1db4543 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,10 +10,7 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}", - "args": [ - "-tags", - "gtk4" - ], + "buildFlags": "-tags=gtk4", "preLaunchTask": "build frontend" } ] diff --git a/frontend/bindings/mesh-drop/internal/config/models.ts b/frontend/bindings/mesh-drop/internal/config/models.ts index bb01db2..4dea928 100644 --- a/frontend/bindings/mesh-drop/internal/config/models.ts +++ b/frontend/bindings/mesh-drop/internal/config/models.ts @@ -21,9 +21,6 @@ export enum Language { export class WindowState { "Width": number; "Height": number; - "X": number; - "Y": number; - "Maximised": boolean; /** Creates a new WindowState instance. */ constructor($$source: Partial = {}) { @@ -33,15 +30,6 @@ export class WindowState { if (!("Height" in $$source)) { 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); } diff --git a/frontend/bindings/mesh-drop/internal/transfer/models.ts b/frontend/bindings/mesh-drop/internal/transfer/models.ts index c5c4553..b6f5d80 100644 --- a/frontend/bindings/mesh-drop/internal/transfer/models.ts +++ b/frontend/bindings/mesh-drop/internal/transfer/models.ts @@ -83,6 +83,7 @@ export class Transfer { "sender": discovery$0.Peer; /** + * FileName 如果 ContentType 为 file,文件名;如果 ContentType 为 folder,文件夹名;如果 ContentType 为 text,空 * 文件名 */ "file_name": string; diff --git a/frontend/bindings/mesh-drop/internal/transfer/service.ts b/frontend/bindings/mesh-drop/internal/transfer/service.ts index 2c26a43..5afc439 100644 --- a/frontend/bindings/mesh-drop/internal/transfer/service.ts +++ b/frontend/bindings/mesh-drop/internal/transfer/service.ts @@ -61,8 +61,8 @@ export function ResolvePendingRequest(id: string, accept: boolean, savePath: str return $Call.ByID(207902967, id, accept, savePath); } -export function SaveHistory(): $CancellablePromise { - return $Call.ByID(713135400); +export function SaveHistory(transfers: ($models.Transfer | null)[]): $CancellablePromise { + return $Call.ByID(713135400, transfers); } export function SendFile(target: discovery$0.Peer | null, targetIP: string, filePath: string): $CancellablePromise { diff --git a/goreleaser/info.json b/goreleaser/info.json index 00437b6..06b9294 100644 --- a/goreleaser/info.json +++ b/goreleaser/info.json @@ -6,10 +6,10 @@ "0000": { "ProductVersion": "0.1.0", "CompanyName": "Nite", - "FileDescription": "A mesh-drop application", + "FileDescription": "MeshDrop - A cross-platform file transfer application", "LegalCopyright": "© 2026, Nite", - "ProductName": "Nite", - "Comments": "A mesh-drop application" + "ProductName": "MeshDrop", + "Comments": "A cross-platform file transfer application" } } } \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index fdb6adb..2620d85 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -13,11 +13,8 @@ import ( // WindowState 定义窗口状态 type WindowState struct { - Width int `mapstructure:"width"` - Height int `mapstructure:"height"` - X int `mapstructure:"x"` - Y int `mapstructure:"y"` - Maximised bool `mapstructure:"maximised"` + Width int `mapstructure:"width"` + Height int `mapstructure:"height"` } var Version = "next" @@ -50,14 +47,6 @@ type Config struct { data configData } -// 默认窗口配置 -var defaultWindowState = WindowState{ - Width: 1024, - Height: 768, - X: -1, - Y: -1, -} - func GetConfigDir() string { configPath, err := os.UserConfigDir() if err != nil { @@ -75,7 +64,7 @@ func GetUserHomeDir() string { } // New 读取配置 -func Load() *Config { +func Load(defaultState WindowState) *Config { v := viper.New() configDir := GetConfigDir() err := os.MkdirAll(configDir, 0755) @@ -86,7 +75,7 @@ func Load() *Config { // 设置默认值 defaultSavePath := filepath.Join(GetUserHomeDir(), "Downloads") - v.SetDefault("window_state", defaultWindowState) + v.SetDefault("window_state", defaultState) v.SetDefault("save_path", defaultSavePath) defaultHostName, err := os.Hostname() if err != nil { diff --git a/internal/transfer/history.go b/internal/transfer/history.go index 9b51057..b43f763 100644 --- a/internal/transfer/history.go +++ b/internal/transfer/history.go @@ -8,11 +8,10 @@ import ( "path/filepath" ) -func (s *Service) SaveHistory() { +func (s *Service) SaveHistory(transfers []*Transfer) { if !s.config.GetSaveHistory() { return } - transfers := s.GetTransferList() configDir := config.GetConfigDir() historyPath := filepath.Join(configDir, "history.json") historyJson, err := json.Marshal(transfers) diff --git a/internal/transfer/model.go b/internal/transfer/model.go index 62652f7..f84a0d8 100644 --- a/internal/transfer/model.go +++ b/internal/transfer/model.go @@ -34,20 +34,21 @@ const ( // Transfer type Transfer struct { - ID string `json:"id" binding:"required"` // 传输会话 ID - CreateTime int64 `json:"create_time"` // 创建时间 - Sender discovery.Peer `json:"sender" binding:"required"` // 发送者 - FileName string `json:"file_name"` // 文件名 - FileSize int64 `json:"file_size"` // 文件大小 (字节) - SavePath string `json:"savePath"` // 保存路径 - Status TransferStatus `json:"status"` // 传输状态 - Progress Progress `json:"progress"` // 传输进度 - Type TransferType `json:"type"` // 进度类型 - ContentType ContentType `json:"content_type"` // 内容类型 - Text string `json:"text"` // 文本内容 - ErrorMsg string `json:"error_msg"` // 错误信息 - Token string `json:"token"` // 用于上传的凭证 - DecisionChan chan Decision `json:"-"` // 用户决策通道 + ID string `json:"id" binding:"required"` // 传输会话 ID + CreateTime int64 `json:"create_time"` // 创建时间 + Sender discovery.Peer `json:"sender" binding:"required"` // 发送者 + // FileName 如果 ContentType 为 file,文件名;如果 ContentType 为 folder,文件夹名;如果 ContentType 为 text,空 + FileName string `json:"file_name"` // 文件名 + FileSize int64 `json:"file_size"` // 文件大小 (字节) + SavePath string `json:"savePath"` // 保存路径 + Status TransferStatus `json:"status"` // 传输状态 + Progress Progress `json:"progress"` // 传输进度 + Type TransferType `json:"type"` // 进度类型 + ContentType ContentType `json:"content_type"` // 内容类型 + Text string `json:"text"` // 文本内容 + ErrorMsg string `json:"error_msg"` // 错误信息 + Token string `json:"token"` // 用于上传的凭证 + DecisionChan chan Decision `json:"-"` // 用户决策通道 } type TransferOption func(*Transfer) diff --git a/internal/transfer/server.go b/internal/transfer/server.go index ba522f7..3123bbf 100644 --- a/internal/transfer/server.go +++ b/internal/transfer/server.go @@ -175,6 +175,14 @@ func (s *Service) handleUpload(c *gin.Context) { switch task.ContentType { case ContentTypeFile: 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) if err != nil { // 接收方无法创建文件,直接报错,任务结束 @@ -189,17 +197,17 @@ func (s *Service) handleUpload(c *gin.Context) { return } defer file.Close() - s.receive(c, task, file, ctxReader) + s.receive(c, task, Writer{w: file, filePath: destPath}, ctxReader) case ContentTypeText: var buf bytes.Buffer - s.receive(c, task, &buf, ctxReader) + s.receive(c, task, Writer{w: &buf, filePath: ""}, ctxReader) task.Text = buf.String() case ContentTypeFolder: 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 := &PassThroughReader{ 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") task.Status = TransferStatusError task.ErrorMsg = fmt.Errorf("failed to write file: %v", err).Error() + + // 删除文件 + if task.ContentType == ContentTypeFile && writer.GetFilePath() != "" { + _ = os.Remove(writer.GetFilePath()) + } return } @@ -265,6 +278,14 @@ func (s *Service) receiveFolder(c *gin.Context, savePath string, task *Transfer, // 创建根目录 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 { c.JSON(http.StatusInternalServerError, TransferUploadResponse{ ID: task.ID, diff --git a/internal/transfer/service.go b/internal/transfer/service.go index 4e85485..19bb6b6 100644 --- a/internal/transfer/service.go +++ b/internal/transfer/service.go @@ -128,13 +128,13 @@ func (s *Service) StoreTransfersToList(transfers []*Transfer) { for _, transfer := range transfers { s.transferList.Store(transfer.ID, transfer) } - s.SaveHistory() + s.SaveHistory(transfers) s.NotifyTransferListUpdate() } func (s *Service) StoreTransferToList(transfer *Transfer) { s.transferList.Store(transfer.ID, transfer) - s.SaveHistory() + s.SaveHistory([]*Transfer{transfer}) s.NotifyTransferListUpdate() } @@ -154,12 +154,12 @@ func (s *Service) CleanFinishedTransferList() { } return true }) - s.SaveHistory() + s.SaveHistory(s.GetTransferList()) s.NotifyTransferListUpdate() } func (s *Service) DeleteTransfer(transferID string) { s.transferList.Delete(transferID) - s.SaveHistory() + s.SaveHistory(s.GetTransferList()) s.NotifyTransferListUpdate() } diff --git a/internal/transfer/writer.go b/internal/transfer/writer.go new file mode 100644 index 0000000..29c96c9 --- /dev/null +++ b/internal/transfer/writer.go @@ -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 +} diff --git a/main.go b/main.go index 701500f..3b5e573 100644 --- a/main.go +++ b/main.go @@ -42,9 +42,8 @@ func init() { } func NewApp() *App { - conf := config.Load() app := application.New(application.Options{ - Name: "mesh-drop", + Name: "MeshDrop", Assets: application.AssetOptions{ Handler: application.AssetFileServerFS(assets), }, @@ -54,12 +53,28 @@ func NewApp() *App { 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{ - Title: "mesh drop", + Title: "MeshDrop", Width: conf.GetWindowState().Width, Height: conf.GetWindowState().Height, - X: conf.GetWindowState().X, - Y: conf.GetWindowState().Y, EnableFileDrop: true, Linux: application.LinuxWindow{ WebviewGpuPolicy: application.WebviewGpuPolicyAlways, @@ -114,7 +129,7 @@ func (a *App) registerCustomEvents() { application.RegisterEvent[application.Void]("transfer:refreshList") } -func (a *App) setupWindowEvents() { +func (a *App) setupEvents() { // 窗口文件拖拽事件 a.mainWindows.OnWindowEvent(events.Common.WindowFilesDropped, func(event *application.WindowEvent) { files := event.Context().DroppedFiles() @@ -125,17 +140,26 @@ func (a *App) setupWindowEvents() { }) }) - // 应用关闭事件 - a.app.OnShutdown(func() { - x, y := a.mainWindows.Position() - width, height := a.mainWindows.Size() + // 窗口关闭事件 + a.mainWindows.OnWindowEvent(events.Common.WindowClosing, func(event *application.WindowEvent) { + if a.conf.GetCloseToSystray() { + event.Cancel() + a.mainWindows.Hide() + return + } + + w, h := a.mainWindows.Size() + a.conf.SetWindowState(config.WindowState{ - X: x, - Y: y, - Width: width, - Height: height, + Width: w, + Height: h, }) + slog.Info("Window closed", "width", w, "height", h) + }) + + // 应用关闭事件 + a.app.OnShutdown(func() { // 保存传输历史 if a.conf.GetSaveHistory() { // 将 pending 状态的任务改为 canceled @@ -145,13 +169,8 @@ func (a *App) setupWindowEvents() { task.Status = transfer.TransferStatusCanceled } } - a.transferService.SaveHistory() - } - - // 保存配置 - err := a.conf.Save() - if err != nil { - slog.Error("Failed to save config", "error", err) + // 保存传输历史 + a.transferService.SaveHistory(t) } }) } @@ -176,20 +195,13 @@ func (a *App) setupSystray() { }) 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() { a.registerServices() a.setupSystray() a.registerCustomEvents() - a.setupWindowEvents() + a.setupEvents() err := a.app.Run() if err != nil { panic(err)