package update import ( "crypto/sha256" "encoding/hex" "errors" "io" "net/url" "os" "strings" "time" ) const ( StateDownloading = "downloading" StateVerifying = "verifying" StateReady = "ready" StateFailed = "failed" ErrorKindDownload = "download" ErrorKindVerification = "verification" ChecksumsAssetName = "checksums.txt" SignatureAssetName = "checksums.txt.sig" windowsOS = "windows" ) var ( ErrNotPrepared = errors.New("this server cannot apply updates") ErrJobInProgress = errors.New("an update job is already running or ready") ErrNothingToUpdate = errors.New("there is nothing to update") ErrPreflightRefused = errors.New("the update cannot be downloaded") ) type Platform struct { OS string Arch string } type Job struct { Tag string `json:"tag"` State string `json:"state"` Error string `json:"error"` StartedAt time.Time `json:"started_at"` DownloadedBytes int64 `json:"downloaded_bytes"` TotalBytes int64 `json:"total_bytes"` StagedBinary string `json:"staged_binary"` } type ProgressFunc func(done int64, total int64) type AssetFetcher interface { Fetch(asset Asset, destination string, report ProgressFunc) error } type SignatureVerifier interface { Verify(checksumsPath string, signaturePath string) error } type StagedBinaryRunner interface { Version(binaryPath string) (string, error) } type DatabaseBackup interface { Backup() (string, error) } type StagingArea interface { Prepare(tag string) error PathFor(tag string, name string) string Size(path string) (int64, error) ReadChecksums(path string) (string, error) MakeExecutable(path string) error Remove(tag string) error } type HelperDetector interface { Installed() bool } type JobStore interface { Load() (Job, bool) Save(job Job) error } type Logger interface { Printf(format string, args ...any) } func (j Job) IsRunning() bool { return j.State == StateDownloading || j.State == StateVerifying } func (j Job) Occupies() bool { return j.IsRunning() || j.State == StateReady } func (j Job) Progress() *Progress { if j.State != StateDownloading || j.TotalBytes <= 0 { return nil } return &Progress{ Percent: int(j.DownloadedBytes * 100 / j.TotalBytes), DownloadedBytes: j.DownloadedBytes, TotalBytes: j.TotalBytes, } } func (s UpdateStatus) WithJob(job Job) UpdateStatus { s.State = job.State s.Progress = job.Progress() s.Error = nil if job.Error != "" { kind := job.Error s.Error = &kind } startedAt := job.StartedAt s.StartedAt = &startedAt return s } func AssetName(tag string, platform Platform) string { name := "renewcms_" + tag + "_" + platform.OS + "_" + platform.Arch if platform.OS == windowsOS { name += ".exe" } return name } func FindAsset(assets []Asset, name string) (Asset, bool) { for _, asset := range assets { if asset.Name == name { return asset, true } } return Asset{}, false } func HostOf(rawURL string) string { parsed, err := url.Parse(rawURL) if err != nil { return "" } return parsed.Host } func IsAllowedAssetURL(rawURL string, allowedHost string) bool { parsed, err := url.Parse(rawURL) if err != nil || parsed.Scheme != "https" { return false } return allowedHost != "" && parsed.Host == allowedHost } func ChecksumFor(checksums string, assetName string) (string, bool) { for _, line := range strings.Split(checksums, "\n") { fields := strings.Fields(line) if len(fields) == 2 && strings.TrimPrefix(fields[1], "*") == assetName { return strings.ToLower(fields[0]), true } } return "", false } func FileSHA256(path string) (string, error) { file, err := os.Open(path) if err != nil { return "", err } defer file.Close() hash := sha256.New() if _, err := io.Copy(hash, file); err != nil { return "", err } return hex.EncodeToString(hash.Sum(nil)), nil }