if the file being received already exists locally, it will be renamed

This commit is contained in:
2026-02-07 19:39:14 +08:00
parent e76ada9b4b
commit e76bcd709c
13 changed files with 114 additions and 90 deletions

View File

@@ -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
View File

@@ -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"
} }
] ]

View File

@@ -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);
} }

View File

@@ -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;

View File

@@ -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> {

View File

@@ -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"
} }
} }
} }

View File

@@ -13,11 +13,8 @@ import (
// WindowState 定义窗口状态 // WindowState 定义窗口状态
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 {

View File

@@ -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)

View File

@@ -34,20 +34,21 @@ const (
// Transfer // Transfer
type Transfer struct { 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 string `json:"file_name"` // 文件名 // FileName 如果 ContentType 为 file文件名如果 ContentType 为 folder文件夹名如果 ContentType 为 text
FileSize int64 `json:"file_size"` // 文件大小 (字节) FileName string `json:"file_name"` // 文件
SavePath string `json:"savePath"` // 保存路径 FileSize int64 `json:"file_size"` // 文件大小 (字节)
Status TransferStatus `json:"status"` // 传输状态 SavePath string `json:"savePath"` // 保存路径
Progress Progress `json:"progress"` // 传输进度 Status TransferStatus `json:"status"` // 传输状态
Type TransferType `json:"type"` // 进度类型 Progress Progress `json:"progress"` // 传输进度
ContentType ContentType `json:"content_type"` // 内容类型 Type TransferType `json:"type"` // 进度类型
Text string `json:"text"` // 文本内容 ContentType ContentType `json:"content_type"` // 内容类型
ErrorMsg string `json:"error_msg"` // 错误信息 Text string `json:"text"` // 文本内容
Token string `json:"token"` // 用于上传的凭证 ErrorMsg string `json:"error_msg"` // 错误信息
DecisionChan chan Decision `json:"-"` // 用户决策通道 Token string `json:"token"` // 用于上传的凭证
DecisionChan chan Decision `json:"-"` // 用户决策通道
} }
type TransferOption func(*Transfer) type TransferOption func(*Transfer)

View File

@@ -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,

View File

@@ -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()
} }

View 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
View File

@@ -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)