diff --git a/frontend/src/hooks/useGlobalEvents.tsx b/frontend/src/hooks/useGlobalEvents.tsx index 6f312f1..f5013ec 100644 --- a/frontend/src/hooks/useGlobalEvents.tsx +++ b/frontend/src/hooks/useGlobalEvents.tsx @@ -21,7 +21,7 @@ export function useGlobalEvents({ setProgressUpdate: (v: number) => void; setMaxProgressUpdate: (v: number) => void; setProgressDownload: (v: number) => void; - setDownloadSpeed: (v: number) => void; + setDownloadSpeed: (v: string) => void; setMessageUpdate: (v: string) => void; setStageType: (v: string) => void, }) { @@ -33,7 +33,7 @@ export function useGlobalEvents({ const onDownload = (event: any) => { const { percent, speed } = event.data[0]; setProgressDownload(Number(percent)); - setDownloadSpeed(Number(speed)); + setDownloadSpeed(speed); }; const onUpdateProgress = (event: any) => { diff --git a/frontend/src/pages/launcher/index.tsx b/frontend/src/pages/launcher/index.tsx index c31a622..616e1a6 100644 --- a/frontend/src/pages/launcher/index.tsx +++ b/frontend/src/pages/launcher/index.tsx @@ -405,7 +405,7 @@ export default function LauncherPage() {
{downloadType}
- {downloadSpeed.toFixed(1)} MB/s + {downloadSpeed} {progressDownload.toFixed(1)}%
@@ -414,7 +414,7 @@ export default function LauncherPage() { className="h-full bg-gradient-to-r from-cyan-400 to-blue-500 rounded-full" initial={{ width: 0 }} animate={{ width: `${progressDownload}%` }} - transition={{ duration: 0.3 }} + transition={{ type: "tween", ease: "linear", duration: 0.03 }} />
diff --git a/frontend/src/stores/launcherStore.ts b/frontend/src/stores/launcherStore.ts index 4f5f394..80e041d 100644 --- a/frontend/src/stores/launcherStore.ts +++ b/frontend/src/stores/launcherStore.ts @@ -11,7 +11,7 @@ interface LauncherState { isLoading: boolean; gameRunning: boolean; progressDownload: number; - downloadSpeed: number; + downloadSpeed: string; launcherVersion: string; updateData: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>; setDownloadType: (value: string) => void; @@ -24,7 +24,7 @@ interface LauncherState { setGameRunning: (value: boolean) => void; setProgressDownload: (value: number) => void; setLauncherVersion: (value: string) => void; - setDownloadSpeed: (value: number) => void; + setDownloadSpeed: (value: string) => void; setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => void; } @@ -38,7 +38,7 @@ const useLauncherStore = create((set, get) => ({ proxyRunning: false, gameRunning: false, progressDownload: 0, - downloadSpeed: 0, + downloadSpeed: "", launcherVersion: "", updateData: { server: { isUpdate: false, isExists: false, version: "" }, @@ -55,7 +55,7 @@ const useLauncherStore = create((set, get) => ({ setGameRunning: (value: boolean) => set({ gameRunning: value }), setProgressDownload: (value: number) => set({ progressDownload: value }), setLauncherVersion: (value: string) => set({ launcherVersion: value }), - setDownloadSpeed: (value: number) => set({ downloadSpeed: value }), + setDownloadSpeed: (value: string) => set({ downloadSpeed: value }), setUpdateData: (value: Record<'server' | 'proxy' | 'launcher', { isUpdate: boolean, isExists: boolean, version: string }>) => set({ updateData: value }), })); diff --git a/go.mod b/go.mod index c01fbf7..cf199f4 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( require ( aead.dev/minisign v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect + golang.org/x/crypto v0.41.0 // indirect golang.org/x/text v0.28.0 // indirect ) @@ -49,7 +50,6 @@ require ( github.com/wailsapp/go-webview2 v1.0.21 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.41.0 // indirect golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b golang.org/x/net v0.43.0 // indirect google.golang.org/protobuf v1.36.8 diff --git a/internal/git-service/launcher.go b/internal/git-service/launcher.go index 73730d1..34216db 100644 --- a/internal/git-service/launcher.go +++ b/internal/git-service/launcher.go @@ -38,56 +38,20 @@ func (g *GitService) GetLatestLauncherVersion() (bool, string, string) { func (g *GitService) UpdateLauncherProgress(version string) (bool, string) { - resp, err := http.Get(constant.LauncherGitUrl) - if err != nil { - panic(err) - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - - var releases []*models.ReleaseType - err = json.Unmarshal(body, &releases) - if err != nil { - return false, err.Error() - } - - if len(releases) == 0 { - return false, "no releases found" - } - - var releaseData *models.ReleaseType - for _, release := range releases { - if release.TagName == version { - releaseData = release - break - } - } - - if releaseData == nil || releaseData.TagName == "" { + asset, ok := g.getReleaseAsset(version, constant.LauncherGitUrl, constant.LauncherFile) + if !ok { return false, "no release found" } - var assetWin models.AssetType - for _, asset := range releaseData.Assets { - if asset.Name == constant.LauncherFile { - assetWin = asset - break - } - } - - if assetWin.Name == "" { - return false, "no assets found" - } - - resp, err = http.Get(assetWin.BrowserDownloadURL) + resp, err := http.Get(asset.BrowserDownloadURL) if err != nil { return false, err.Error() } defer resp.Body.Close() + err = selfupdate.Apply(resp.Body, selfupdate.Options{}) if err != nil { return false, err.Error() } return true, "" -} +} \ No newline at end of file diff --git a/internal/git-service/proxy.go b/internal/git-service/proxy.go index 9cde54e..77cfda3 100644 --- a/internal/git-service/proxy.go +++ b/internal/git-service/proxy.go @@ -1,6 +1,7 @@ package gitService import ( + "encoding/json" "firefly-launcher/pkg/constant" "firefly-launcher/pkg/models" "fmt" @@ -8,7 +9,8 @@ import ( "net/http" "os" "path/filepath" - "encoding/json" + "time" + "github.com/wailsapp/wails/v3/pkg/application" ) @@ -35,72 +37,35 @@ func (g *GitService) GetLatestProxyVersion() (bool, string, string) { } func (g *GitService) DownloadProxyProgress(version string) (bool, string) { - resp, err := http.Get(constant.ProxyGitUrl) - if err != nil { - panic(err) - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - - var releases []*models.ReleaseType - err = json.Unmarshal(body, &releases) - if err != nil { - return false, err.Error() - } - - if len(releases) == 0 { - return false, "no releases found" - } - - var releaseData *models.ReleaseType - for _, release := range releases { - if release.TagName == version { - releaseData = release - break - } - } - - if releaseData == nil || releaseData.TagName == "" { + asset, ok := g.getReleaseAsset(version, constant.ProxyGitUrl, constant.ProxyZipFile) + if !ok { return false, "no release found" } - var assetWin models.AssetType - for _, asset := range releaseData.Assets { - if asset.Name == constant.ProxyZipFile { - assetWin = asset - break - } + if err := os.MkdirAll(constant.ProxyStorageUrl, 0755); err != nil { + return false, err.Error() } - if assetWin.Name == "" { - return false, "no assets found" - } - - if err := os.Mkdir(constant.ProxyStorageUrl, 0755); err != nil { - if !os.IsExist(err) { - return false, err.Error() - } - } - saveFile := filepath.Join(constant.ProxyStorageUrl, assetWin.Name) - - resp, err = http.Get(assetWin.BrowserDownloadURL) + saveFile := filepath.Join(constant.ProxyStorageUrl, asset.Name) + tmpPath, err := g.downloadFileParallel(saveFile, asset.BrowserDownloadURL, 4, func(percent float64, speed string) { + application.Get().Event.Emit("download:proxy", map[string]interface{}{ + "percent": fmt.Sprintf("%.2f", percent), + "speed": speed, + }) + }) if err != nil { return false, err.Error() } - defer resp.Body.Close() - - DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) { - application.Get().Event.Emit("download:proxy", map[string]interface{}{ - "percent": fmt.Sprintf("%.2f", percent), - "speed": fmt.Sprintf("%.2f", speed), - }) - - }) - return true, "" + for i := 0; i < 3; i++ { + if err := os.Rename(tmpPath, saveFile); err == nil { + return true, "" + } + time.Sleep(300 * time.Millisecond) + } + return false, "failed to rename tmp file after retries" } func (g *GitService) UnzipProxy() { - unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl) + g.unzipParallel(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile), constant.ProxyStorageUrl) os.Remove(filepath.Join(constant.ProxyStorageUrl, constant.ProxyZipFile)) -} \ No newline at end of file +} diff --git a/internal/git-service/server.go b/internal/git-service/server.go index 4f88523..e3d43f7 100644 --- a/internal/git-service/server.go +++ b/internal/git-service/server.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "path/filepath" + "time" "github.com/wailsapp/wails/v3/pkg/application" ) @@ -36,72 +37,35 @@ func (g *GitService) GetLatestServerVersion() (bool, string, string) { } func (g *GitService) DownloadServerProgress(version string) (bool, string) { - resp, err := http.Get(constant.ServerGitUrl) - if err != nil { - panic(err) - } - defer resp.Body.Close() - - body, _ := io.ReadAll(resp.Body) - - var releases []*models.ReleaseType - err = json.Unmarshal(body, &releases) - if err != nil { - return false, err.Error() - } - - if len(releases) == 0 { - return false, "no releases found" - } - - var releaseData *models.ReleaseType - for _, release := range releases { - if release.TagName == version { - releaseData = release - break - } - } - - if releaseData == nil || releaseData.TagName == "" { + asset, ok := g.getReleaseAsset(version, constant.ServerGitUrl, constant.ServerZipFile) + if !ok { return false, "no release found" } - var assetWin models.AssetType - for _, asset := range releaseData.Assets { - if asset.Name == constant.ServerZipFile { - assetWin = asset - break - } + if err := os.MkdirAll(constant.ServerStorageUrl, 0755); err != nil { + return false, err.Error() } - if assetWin.Name == "" { - return false, "no assets found" - } - - if err := os.Mkdir(constant.ServerStorageUrl, 0755); err != nil { - if !os.IsExist(err) { - return false, err.Error() - } - } - - saveFile := filepath.Join(constant.ServerStorageUrl, assetWin.Name) - - resp, err = http.Get(assetWin.BrowserDownloadURL) + saveFile := filepath.Join(constant.ServerStorageUrl, asset.Name) + tmpPath, err := g.downloadFileParallel(saveFile, asset.BrowserDownloadURL, 4, func(percent float64, speed string) { + application.Get().Event.Emit("download:server", map[string]interface{}{ + "percent": fmt.Sprintf("%.2f", percent), + "speed": speed, + }) + }) if err != nil { return false, err.Error() } - defer resp.Body.Close() - - DownloadFile(saveFile, assetWin.BrowserDownloadURL, func(percent float64, speed float64) { - application.Get().Event.Emit("download:server", map[string]interface{}{ - "percent": fmt.Sprintf("%.2f", percent), - "speed": fmt.Sprintf("%.2f", speed), - }) - }) - return true, "" + for i := 0; i < 3; i++ { + if err := os.Rename(tmpPath, saveFile); err == nil { + return true, "" + } + time.Sleep(300 * time.Millisecond) + } + return false, "failed to rename tmp file after retries" } func (g *GitService) UnzipServer() { - unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl) + g.unzipParallel(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile), constant.ServerStorageUrl) os.Remove(filepath.Join(constant.ServerStorageUrl, constant.ServerZipFile)) } \ No newline at end of file diff --git a/internal/git-service/util.go b/internal/git-service/util.go index a942ea3..bceb5f2 100644 --- a/internal/git-service/util.go +++ b/internal/git-service/util.go @@ -2,6 +2,8 @@ package gitService import ( "archive/zip" + "encoding/json" + "firefly-launcher/pkg/models" "fmt" "io" "math" @@ -13,39 +15,37 @@ import ( "time" ) -func humanFormat(bytes int64) string { - n := float64(bytes) +func HumanFormat(bytes float64) string { for _, unit := range []string{"", "Ki", "Mi", "Gi"} { - if math.Abs(n) < 1024.0 { - return fmt.Sprintf("%3.1f%sB", n, unit) + if math.Abs(bytes) < 1024.0 { + return fmt.Sprintf("%3.1f%sB", bytes, unit) } - n /= 1024.0 + bytes /= 1024.0 } - return fmt.Sprintf("%.1fTiB", n) + return fmt.Sprintf("%.1fTiB", bytes) } type WriteCounter struct { - Total uint64 - StartTime time.Time - OnEmit func(percent float64, speedMBps float64) - TotalSize int64 - lastLoggedPercent int + Total uint64 + StartTime time.Time + OnEmit func(percent float64, speed string) + TotalSize int64 + mu sync.Mutex } -func NewWriteCounter(total int64, onEmit func(percent float64, speedMBps float64)) *WriteCounter { +func NewWriteCounter(total int64, onEmit func(percent float64, speed string)) *WriteCounter { return &WriteCounter{ - StartTime: time.Now(), - TotalSize: total, - lastLoggedPercent: -1, - OnEmit: onEmit, + StartTime: time.Now(), + TotalSize: total, + OnEmit: onEmit, } } -func (wc *WriteCounter) Write(p []byte) (int, error) { - n := len(p) +func (wc *WriteCounter) Add(n int) { + wc.mu.Lock() + defer wc.mu.Unlock() wc.Total += uint64(n) wc.PrintProgress() - return n, nil } func (wc *WriteCounter) PrintProgress() { @@ -53,63 +53,109 @@ func (wc *WriteCounter) PrintProgress() { if elapsed < 0.001 { elapsed = 0.001 } - - speed := float64(wc.Total) / 1024 / 1024 / elapsed // MB/s + speed := float64(wc.Total) / 1024 / 1024 / elapsed percent := float64(wc.Total) / float64(wc.TotalSize) * 100 if wc.OnEmit != nil { - wc.OnEmit(percent, speed) + wc.OnEmit(percent, fmt.Sprintf("%s/s", HumanFormat(speed))) } } -func DownloadFile(filepath string, url string, onEmit func(percent float64, speed float64)) error { - tmpPath := filepath + ".tmp" - - resp, err := http.Get(url) +// --- DownloadFileParallel --- +func (g *GitService) downloadFileParallel(filePath, url string, numParts int, onEmit func(percent float64, speed string)) (tmpPath string, err error) { + resp, err := http.Head(url) if err != nil { - return fmt.Errorf("failed to get file: %w", err) + return "", fmt.Errorf("failed to get head: %w", err) } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("bad status: %s", resp.Status) + return "", fmt.Errorf("bad status: %s", resp.Status) } + size := resp.ContentLength + tmpPath = filePath + ".tmp" + out, err := os.Create(tmpPath) if err != nil { - return fmt.Errorf("failed to create tmp file: %w", err) + return "", fmt.Errorf("failed to create tmp file: %w", err) } + defer out.Close() - counter := NewWriteCounter(resp.ContentLength, onEmit) - _, err = io.Copy(out, io.TeeReader(resp.Body, counter)) - if closeErr := out.Close(); closeErr != nil { - return fmt.Errorf("failed to close tmp file: %w", closeErr) - } - if err != nil { - return fmt.Errorf("failed to download file: %w", err) - } + counter := NewWriteCounter(size, onEmit) + partSize := size / int64(numParts) + var wg sync.WaitGroup + var mu sync.Mutex - // Delete destination file if it exists - if _, err := os.Stat(filepath); err == nil { - if err := os.Remove(filepath); err != nil { - return fmt.Errorf("failed to remove existing file: %w", err) + for i := 0; i < numParts; i++ { + start := int64(i) * partSize + end := start + partSize - 1 + if i == numParts-1 { + end = size - 1 } + + _ = start + _ = end + + wg.Go(func() { + req, _ := http.NewRequest("GET", url, nil) + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + buf := make([]byte, 32*1024) + var written int64 + for { + n, err := resp.Body.Read(buf) + if n > 0 { + mu.Lock() + out.Seek(start+written, 0) + out.Write(buf[:n]) + mu.Unlock() + written += int64(n) + counter.Add(n) + } + if err == io.EOF { + break + } + if err != nil { + break + } + } + }) } - for i := 0; i < 3; i++ { - err = os.Rename(tmpPath, filepath) - if err == nil { - break - } - time.Sleep(300 * time.Millisecond) - } - if err != nil { - return fmt.Errorf("failed to rename after retries: %w", err) - } - - return nil + wg.Wait() + return tmpPath, nil } -func unzipParallel(src string, dest string) error { +// --- Helper getReleaseAsset --- +func (g *GitService) getReleaseAsset(version, url, fileName string) (models.AssetType, bool) { + resp, err := http.Get(url) + if err != nil { + return models.AssetType{}, false + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var releases []*models.ReleaseType + if err := json.Unmarshal(body, &releases); err != nil || len(releases) == 0 { + return models.AssetType{}, false + } + + for _, release := range releases { + if release.TagName == version { + for _, asset := range release.Assets { + if asset.Name == fileName { + return asset, true + } + } + } + } + return models.AssetType{}, false +} + +func (g *GitService) unzipParallel(src string, dest string) error { numCPU := runtime.NumCPU() reserved := 1 @@ -137,11 +183,11 @@ func unzipParallel(src string, dest string) error { jobs := make(chan job) var wg sync.WaitGroup - // Worker pool for i := 0; i < maxWorkers; i++ { + wg.Go(func() { for j := range jobs { - err := extractFile(j.f, dest) + err := g.extractFile(j.f, dest) if err != nil { fmt.Printf("Error extracting %s: %v\n", j.f.Name, err) } @@ -149,17 +195,17 @@ func unzipParallel(src string, dest string) error { }) } - // Feed jobs for _, f := range r.File { jobs <- job{f} } close(jobs) + wg.Wait() return nil } -func extractFile(f *zip.File, dest string) error { +func (g *GitService) extractFile(f *zip.File, dest string) error { fp := filepath.Join(dest, f.Name) if f.FileInfo().IsDir() { diff --git a/pkg/constant/constant.go b/pkg/constant/constant.go index fa34343..b04d517 100644 --- a/pkg/constant/constant.go +++ b/pkg/constant/constant.go @@ -10,7 +10,7 @@ const ProxyZipFile = "64bit.zip" const LauncherFile = "firefly-launcher.exe" const TempUrl = "./temp" -const CurrentLauncherVersion = "1.5.1" +const CurrentLauncherVersion = "1.6.0" type ToolFile string