From c2f13637f90b7ba2d94485bfdbbde90b9468b9f0 Mon Sep 17 00:00:00 2001 From: Evan Burkey Date: Sun, 20 Jul 2025 13:13:45 -0700 Subject: [PATCH] move downloader into own package --- config.go | 6 +- main.go | 134 +-------------------------- manifest.go => pkg/epoch/manifest.go | 8 +- pkg/epoch/update.go | 127 +++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 133 deletions(-) rename manifest.go => pkg/epoch/manifest.go (85%) create mode 100644 pkg/epoch/update.go diff --git a/config.go b/config.go index 38f24b4..90f9745 100644 --- a/config.go +++ b/config.go @@ -20,8 +20,10 @@ type Config struct { } const ( - configDirName = "epochcli" - configName = "config.toml" + configDirName = "epochcli" + configName = "config.toml" + defaultWowPath = "/path/to/wow" + defaultLaunchCmd = "not configured" ) var ( diff --git a/main.go b/main.go index cf093af..40bf22c 100644 --- a/main.go +++ b/main.go @@ -1,27 +1,15 @@ package main import ( - "crypto/md5" - "encoding/hex" "flag" "fmt" - "io" - "io/fs" + "git.burkey.co/eburk/epochcli/pkg/epoch" "log" - "net/http" "os" "os/exec" - "path/filepath" - "regexp" "strings" ) -const ( - manifestUrl = "https://updater.project-epoch.net/api/v2/manifest" - defaultWowPath = "/path/to/wow" - defaultLaunchCmd = "not configured" -) - func main() { outOfDate, err := needUpdate() if err != nil { @@ -61,14 +49,14 @@ func main() { log.Fatalf("WowDir in %s is still the default setting, exiting", cfgPath) } - stats, err := downloadUpdate(config, forceFlag) + stats, err := epoch.DownloadUpdate(config.WowDir, forceFlag, config.RemoveUnknownPatches) if err != nil { log.Fatal(err) } - fmt.Printf("%d files updated\n\n", stats.updated) - if stats.current > 0 { - fmt.Printf("%d files are already up to date\n\n", stats.current) + fmt.Printf("%d files updated\n\n", stats.Updated) + if stats.Current > 0 { + fmt.Printf("%d files are already up to date\n\n", stats.Current) } if configRun { @@ -102,115 +90,3 @@ func main() { } } } - -type DownloadStats struct { - updated int - current int -} - -func downloadUpdate(config *Config, force bool) (DownloadStats, error) { - var stats DownloadStats - - manifest, err := getManifest() - if err != nil { - log.Fatalf("Failed to get manifest: %v\n", err) - } - - for _, file := range manifest.Files { - path := strings.ReplaceAll(file.Path, `\`, `/`) - path = strings.TrimLeft(path, `\`) - - localPath := filepath.Join(config.WowDir, path) - localDir := filepath.Dir(localPath) - if _, err = os.Stat(localDir); os.IsNotExist(err) { - err = os.MkdirAll(localDir, 0755) - if err != nil { - return stats, fmt.Errorf("failed to create directory %s: %v", localDir, err) - } - } - - if !force { - if _, err = os.Stat(localPath); err == nil { - data, err := os.ReadFile(localPath) - if err != nil { - return stats, err - } - hashBytes := md5.Sum(data) - hash := hex.EncodeToString(hashBytes[:]) - if hash == file.Hash { - fmt.Printf("File %s is up to date\n", localPath) - stats.current += 1 - continue - } - } - } - - fmt.Printf("Updating %s...\n", localPath) - - outFile, err := os.Create(localPath) - if err != nil { - return stats, err - } - - for _, url := range []string{file.Urls.Cloudflare, file.Urls.Digitalocean, file.Urls.None} { - resp, err := http.Get(url) - if err != nil { - outFile.Close() - return stats, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - outFile.Close() - return stats, fmt.Errorf("failed to download update from %s, status code: %d", url, resp.StatusCode) - } - - _, err = io.Copy(outFile, resp.Body) - if err != nil { - outFile.Close() - return stats, err - } - - break - } - - outFile.Close() - stats.updated += 1 - } - - if config.RemoveUnknownPatches { - patches := make([]string, 0) - patchreg := regexp.MustCompile(`patch-[A-Za-z].MPQ`) - - for _, file := range manifest.Files { - if patchreg.MatchString(file.Path) { - patches = append(patches, strings.Split(file.Path, "Data\\")[1]) - } - } - - err = filepath.WalkDir(filepath.Join(config.WowDir, "Data"), func(path string, d fs.DirEntry, err error) error { - if !d.IsDir() && patchreg.MatchString(d.Name()) { - del := true - for _, patch := range patches { - if patch == d.Name() { - del = false - break - } - } - if del { - err = os.Remove(path) - if err != nil { - return err - } - fmt.Println("Removed unknown patch", d.Name()) - } - } - return nil - }) - - if err != nil { - log.Fatalf("failed to delete unknown patches: %s", err) - } - } - - return stats, nil -} diff --git a/manifest.go b/pkg/epoch/manifest.go similarity index 85% rename from manifest.go rename to pkg/epoch/manifest.go index 8882ea6..326a904 100644 --- a/manifest.go +++ b/pkg/epoch/manifest.go @@ -1,4 +1,4 @@ -package main +package epoch import ( "encoding/json" @@ -6,6 +6,10 @@ import ( "net/http" ) +const ( + manifestUrl = "https://updater.project-epoch.net/api/v2/manifest" +) + type File struct { Path string `json:"Path"` Hash string `json:"Hash"` @@ -25,7 +29,7 @@ type Manifest struct { CheckedAt string `json:"checked_at"` } -func getManifest() (*Manifest, error) { +func GetManifest() (*Manifest, error) { resp, err := http.Get(manifestUrl) if err != nil { return nil, err diff --git a/pkg/epoch/update.go b/pkg/epoch/update.go new file mode 100644 index 0000000..ec23f8b --- /dev/null +++ b/pkg/epoch/update.go @@ -0,0 +1,127 @@ +package epoch + +import ( + "crypto/md5" + "encoding/hex" + "fmt" + "io" + "io/fs" + "log" + "net/http" + "os" + "path/filepath" + "regexp" + "strings" +) + +type DownloadStats struct { + Updated int + Current int +} + +func DownloadUpdate(wowdir string, force bool, removeUnknown bool) (DownloadStats, error) { + var stats DownloadStats + + manifest, err := GetManifest() + if err != nil { + log.Fatalf("Failed to get manifest: %v\n", err) + } + + for _, file := range manifest.Files { + path := strings.ReplaceAll(file.Path, `\`, `/`) + path = strings.TrimLeft(path, `\`) + + localPath := filepath.Join(wowdir, path) + localDir := filepath.Dir(localPath) + if _, err = os.Stat(localDir); os.IsNotExist(err) { + err = os.MkdirAll(localDir, 0755) + if err != nil { + return stats, fmt.Errorf("failed to create directory %s: %v", localDir, err) + } + } + + if !force { + if _, err = os.Stat(localPath); err == nil { + data, err := os.ReadFile(localPath) + if err != nil { + return stats, err + } + hashBytes := md5.Sum(data) + hash := hex.EncodeToString(hashBytes[:]) + if hash == file.Hash { + fmt.Printf("File %s is up to date\n", localPath) + stats.Current += 1 + continue + } + } + } + + fmt.Printf("Updating %s...\n", localPath) + + outFile, err := os.Create(localPath) + if err != nil { + return stats, err + } + + for _, url := range []string{file.Urls.Cloudflare, file.Urls.Digitalocean, file.Urls.None} { + resp, err := http.Get(url) + if err != nil { + outFile.Close() + return stats, err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + outFile.Close() + return stats, fmt.Errorf("failed to download update from %s, status code: %d", url, resp.StatusCode) + } + + _, err = io.Copy(outFile, resp.Body) + if err != nil { + outFile.Close() + return stats, err + } + + break + } + + outFile.Close() + stats.Updated += 1 + } + + if removeUnknown { + patches := make([]string, 0) + patchreg := regexp.MustCompile(`patch-[A-Za-z].MPQ`) + + for _, file := range manifest.Files { + if patchreg.MatchString(file.Path) { + patches = append(patches, strings.Split(file.Path, "Data\\")[1]) + } + } + + err = filepath.WalkDir(filepath.Join(wowdir, "Data"), func(path string, d fs.DirEntry, err error) error { + if !d.IsDir() && patchreg.MatchString(d.Name()) { + del := true + for _, patch := range patches { + if patch == d.Name() { + del = false + break + } + } + if del { + err = os.Remove(path) + if err != nil { + return err + } + fmt.Println("Removed unknown patch", d.Name()) + } + } + return nil + }) + + if err != nil { + log.Fatalf("failed to delete unknown patches: %s", err) + } + } + + return stats, nil +}