Files
Blizzard bf3b0048fd fix(desktop): 原生下载失败会在用户选定路径上留半截文件
桌面端 Go 侧此前零测试。补 5 个(download/ReadLocalFile/Ping),
逮到一个真 bug:io.Copy 中途断开时,用户选定的路径上会留下一个半截的
.docx —— 带着用户自己起的名字躺在那儿,虽然前端会弹错误,但以后双击打不开,
而用户会以为是导出功能坏了。现在失败一律 os.Remove 不留残file。

同批修的两处(同一段代码,都没测试盖到):
- Close 的错误被 defer 吞掉。写文件时 io.Copy 成功不代表数据落盘,
  flush 失败只在 Close 上报——吞掉就是静默截断,且 download 返回 nil(成功)。
- http.Get 用默认 client,没有超时。上游卡住的话「另存为」会永远转,
  用户只能强杀 app。改用带 3 分钟超时的 client。

顺带核实过一个可疑点、结论是不用改:download 走裸 http.Get 不带鉴权头,
但报告导出路由 `/reports/:id/export` 是故意公开的(router.go 注释:
"EventSource/下载无法带 Bearer"),所以能通。

对话框本身(application.Get().Dialog)要真窗口,自动化盖不到,仍需手点。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 12:06:21 +08:00

116 lines
3.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
goruntime "runtime"
"time"
"github.com/wailsapp/wails/v3/pkg/application"
)
// App 经 Wails v3 的 Service 绑定暴露给前端,承载只有桌面端能做的原生能力:
// 文件读写、系统"另存为"框、用系统默认应用打开、原生通知。
// v3 中不再需要注入 ctx——对话框经 application.Get().Dialog 获取。
type App struct{}
// Ping 供前端探活 Go 桥是否就绪。
func (a *App) Ping() string { return "sundynix-desktop ok" }
// ReadLocalFile 读取本地文件内容(本地文件系统 I/O)。
func (a *App) ReadLocalFile(path string) (string, error) {
b, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(b), nil
}
// SaveReportAs 弹原生"另存为"对话框,把 url 指向的报告(.docx)下载到用户选定路径。
// 返回保存路径;用户取消则返回空串。
func (a *App) SaveReportAs(url, filename string) (string, error) {
if filename == "" {
filename = "report.docx"
}
path, err := application.Get().Dialog.SaveFile().
SetFilename(filename).
AddFilter("Word 文档 (*.docx)", "*.docx").
PromptForSingleSelection()
if err != nil || path == "" {
return "", err
}
if err := download(url, path); err != nil {
return "", err
}
return path, nil
}
// OpenReport 把报告下载到临时目录,并用系统默认应用(Word/Pages/WPS)打开。
func (a *App) OpenReport(url, filename string) error {
if filename == "" {
filename = "report.docx"
}
dst := filepath.Join(os.TempDir(), "sundynix-open-"+filename)
if err := download(url, dst); err != nil {
return err
}
return openInSystem(dst)
}
// Notify 弹一条系统通知(best-effortmacOS 用 osascript,其它平台暂静默)。
func (a *App) Notify(title, body string) {
if goruntime.GOOS == "darwin" {
script := fmt.Sprintf("display notification %q with title %q", body, title)
_ = exec.Command("osascript", "-e", script).Start()
}
}
// downloadClient 给下载加超时:默认 http.Get 用的 client 没有超时,
// 上游卡住的话「另存为」会永远转下去,用户只能强杀 app。
var downloadClient = &http.Client{Timeout: 3 * time.Minute}
// download 把 url 下载到 dst。失败一律不留残file:
// 中途断开会在用户选定的路径上留个半截 .docx,带着用户起的名字,以后双击打不开。
func download(url, dst string) (err error) {
resp, err := downloadClient.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return fmt.Errorf("下载失败 HTTP %d", resp.StatusCode)
}
f, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
// Close 的错误不能吞:写文件时 io.Copy 成功不代表数据落了盘,
// flush 失败只会在 Close 上报出来——吞掉就是静默截断。
cerr := f.Close()
if err == nil {
err = cerr
}
if err != nil {
_ = os.Remove(dst) // 失败不留残file
}
}()
_, err = io.Copy(f, resp.Body)
return err
}
func openInSystem(path string) error {
switch goruntime.GOOS {
case "darwin":
return exec.Command("open", path).Start()
case "windows":
return exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start()
default:
return exec.Command("xdg-open", path).Start()
}
}