package main import ( "archive/zip" "bytes" "fmt" "io" "log" "net/http" "os" "path/filepath" "regexp" "strings" ) type Addon struct { Name string `json:"name"` URL string `json:"url"` } var versionRegex = regexp.MustCompile(`(\d+\.\d+\.\d+)`) func (a *Addon) GetRemoteTocURL() string { return fmt.Sprintf("%s/raw/branch/master/%s.toc", a.URL, a.Name) } func (a *Addon) GetRemoteReleaseURL() string { return fmt.Sprintf("%s/media/branch/master/%s.zip", a.URL, a.Name) } func (a *Addon) GetRemoteRelease() (body []byte, err error) { url := a.GetRemoteReleaseURL() response, err := http.Get(url) if err != nil { return nil, fmt.Errorf("error getting remote release: %w", err) } return io.ReadAll(response.Body) } func (a *Addon) HasBreakingChanges(lhsToc, rhsToc string) bool { lhsLines := strings.Split(lhsToc, "\n") rhsLines := strings.Split(rhsToc, "\n") if len(lhsLines) != len(rhsLines) { return true } for i, lhsLine := range lhsLines { rhsLine := rhsLines[i] lhsLine = strings.TrimSpace(lhsLine) rhsLine = strings.TrimSpace(rhsLine) // We don't care about ## lines if strings.HasPrefix(lhsLine, "#") && strings.HasPrefix(rhsLine, "#") { continue } // Nor do we care about empty lines if lhsLine == "" && rhsLine == "" { continue } log.Printf("%s %s", lhsLine, rhsLine) if i >= len(rhsLines) || rhsLine != lhsLine { return true } } return false } func UpdateFile(localPath string, data []byte) (err error) { err = os.MkdirAll(filepath.Dir(localPath), 0755) if err != nil { return fmt.Errorf("error creating directory: %w", err) } fileHandle, err := os.OpenFile(localPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return fmt.Errorf("error opening file: %w", err) } defer fileHandle.Close() _, err = fileHandle.Write(data) if err != nil { return fmt.Errorf("error writing file: %w", err) } return nil } func (a *Addon) Update(body []byte) (hasBreakingChanges bool, err error) { log.Printf("Updating %s", a.Name) zipReader, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) if err != nil { return false, fmt.Errorf("error creating zip reader: %w", err) } log.Printf("Found %d files", len(zipReader.File)) // Yes it is quite ugly iterating twice doing the same work // Maybe I fix it later but definitely not now for _, file := range zipReader.File { if strings.HasSuffix(file.Name, ".toc") { localToc, err := os.ReadFile(a.GetLocalTocPath()) if err != nil { if os.IsNotExist(err) { localToc = []byte{} } else { return false, fmt.Errorf("error reading local toc: %w", err) } } fileHandle, err := file.Open() if err != nil { return false, fmt.Errorf("error opening file: %w", err) } fileData, err := io.ReadAll(fileHandle) if err != nil { return false, fmt.Errorf("error reading file: %w", err) } if a.HasBreakingChanges(string(localToc), string(fileData)) { Warning.Printf("Has breaking changes") hasBreakingChanges = true } } } for _, file := range zipReader.File { if file.FileInfo().IsDir() { continue } log.Printf("Found file %s", file.Name) fileHandle, err := file.Open() if err != nil { return false, fmt.Errorf("error opening file: %w", err) } fileData, err := io.ReadAll(fileHandle) if err != nil { return false, fmt.Errorf("error reading file: %w", err) } localPath := filepath.Join(settings.GamePath, "Interface", "AddOns", file.Name) log.Printf("Updating file %s", localPath) err = UpdateFile(localPath, fileData) if err != nil { return false, fmt.Errorf("error updating file: %w", err) } } if hasBreakingChanges { Warning.Printf("Has breaking changes") } return hasBreakingChanges, nil } func (a *Addon) GetLocalTocPath() string { return filepath.Join(settings.GamePath, "Interface", "AddOns", a.Name, a.Name+".toc") } func (a *Addon) GetRemoteVersion() (version string, err error) { url := a.GetRemoteTocURL() log.Printf("Fetching remote version from %s", url) response, err := http.Get(url) if err != nil { return "", fmt.Errorf("error getting remote version: %w", err) } if response.StatusCode != http.StatusOK { return "", fmt.Errorf("error getting remote version with status: %s", response.Status) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { return "", fmt.Errorf("error reading remote version: %w", err) } return GetVersion(string(body)) } func (a *Addon) GetLocalVersion() (version string, err error) { file, err := os.Open(a.GetLocalTocPath()) if err != nil { if os.IsNotExist(err) { return "0.0.0", nil } return "", fmt.Errorf("error opening local toc: %w", err) } defer file.Close() body, err := io.ReadAll(file) if err != nil { return "", fmt.Errorf("error reading local toc: %w", err) } return GetVersion(string(body)) } func (a *Addon) IsUpToDate() bool { remoteVersion, err := a.GetRemoteVersion() if err != nil { return false } localVersion, err := a.GetLocalVersion() if err != nil { return false } return remoteVersion == localVersion }