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