node build fixed
This commit is contained in:
161
seanime-2.9.10/internal/updater/announcement.go
Normal file
161
seanime-2.9.10/internal/updater/announcement.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"seanime/internal/constants"
|
||||
"seanime/internal/database/models"
|
||||
"seanime/internal/events"
|
||||
"slices"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type AnnouncementType string
|
||||
|
||||
const (
|
||||
AnnouncementTypeToast AnnouncementType = "toast"
|
||||
AnnouncementTypeDialog AnnouncementType = "dialog"
|
||||
AnnouncementTypeBanner AnnouncementType = "banner"
|
||||
)
|
||||
|
||||
type AnnouncementSeverity string
|
||||
|
||||
const (
|
||||
AnnouncementSeverityInfo AnnouncementSeverity = "info"
|
||||
AnnouncementSeverityWarning AnnouncementSeverity = "warning"
|
||||
AnnouncementSeverityError AnnouncementSeverity = "error"
|
||||
AnnouncementSeverityCritical AnnouncementSeverity = "critical"
|
||||
)
|
||||
|
||||
type AnnouncementAction struct {
|
||||
Label string `json:"label"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type AnnouncementConditions struct {
|
||||
OS []string `json:"os,omitempty"` // ["windows", "darwin", "linux"]
|
||||
Platform []string `json:"platform,omitempty"` // ["tauri", "web", "denshi"]
|
||||
// FeatureFlags []string `json:"featureFlags,omitempty"` // Required feature flags
|
||||
VersionConstraint string `json:"versionConstraint,omitempty"` // e.g. "<= 2.9.0", "2.9.0"
|
||||
UserSettingsPath string `json:"userSettingsPath,omitempty"` // JSON path to check in user settings
|
||||
UserSettingsValue []string `json:"userSettingsValue,omitempty"` // Expected values at that path
|
||||
}
|
||||
|
||||
type Announcement struct {
|
||||
ID string `json:"id"` // Unique identifier for tracking
|
||||
Title string `json:"title,omitempty"` // Title for dialogs/banners
|
||||
Message string `json:"message"` // The message to display
|
||||
Type AnnouncementType `json:"type"` // The type of announcement
|
||||
Severity AnnouncementSeverity `json:"severity"` // Severity level
|
||||
Date interface{} `json:"date"` // Date of the announcement
|
||||
|
||||
NotDismissible bool `json:"notDismissible"` // Can user dismiss it
|
||||
|
||||
Conditions *AnnouncementConditions `json:"conditions,omitempty"` // Advanced targeting
|
||||
|
||||
Actions []AnnouncementAction `json:"actions,omitempty"` // Action buttons
|
||||
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
func (u *Updater) GetAnnouncements(version string, platform string, settings *models.Settings) []Announcement {
|
||||
var filteredAnnouncements []Announcement
|
||||
if !u.checkForUpdate {
|
||||
return filteredAnnouncements
|
||||
}
|
||||
// filter out
|
||||
for _, announcement := range u.announcements {
|
||||
if announcement.Conditions == nil {
|
||||
filteredAnnouncements = append(filteredAnnouncements, announcement)
|
||||
continue
|
||||
}
|
||||
|
||||
conditions := announcement.Conditions
|
||||
|
||||
if len(conditions.OS) > 0 && !slices.Contains(conditions.OS, runtime.GOOS) {
|
||||
continue
|
||||
}
|
||||
|
||||
if conditions.Platform != nil && !slices.Contains(conditions.Platform, platform) {
|
||||
continue
|
||||
}
|
||||
|
||||
if conditions.VersionConstraint != "" {
|
||||
versionConstraint, err := semver.NewConstraint(conditions.VersionConstraint)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msgf("updater: Failed to parse version constraint")
|
||||
continue
|
||||
}
|
||||
|
||||
currVersion, err := semver.NewVersion(version)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msgf("updater: Failed to parse current version")
|
||||
continue
|
||||
}
|
||||
|
||||
if !versionConstraint.Check(currVersion) {
|
||||
continue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
filteredAnnouncements = append(filteredAnnouncements, announcement)
|
||||
}
|
||||
|
||||
u.announcements = filteredAnnouncements
|
||||
|
||||
return u.announcements
|
||||
}
|
||||
|
||||
func (u *Updater) FetchAnnouncements() []Announcement {
|
||||
var announcements []Announcement
|
||||
|
||||
response, err := http.Get(constants.AnnouncementURL)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msgf("updater: Failed to get announcements")
|
||||
return announcements
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msgf("updater: Failed to read announcements")
|
||||
return announcements
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &announcements)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msgf("updater: Failed to unmarshal announcements")
|
||||
return announcements
|
||||
}
|
||||
|
||||
// Filter out announcements
|
||||
var filteredAnnouncements []Announcement
|
||||
for _, announcement := range announcements {
|
||||
if announcement.Conditions == nil {
|
||||
filteredAnnouncements = append(filteredAnnouncements, announcement)
|
||||
continue
|
||||
}
|
||||
|
||||
conditions := announcement.Conditions
|
||||
|
||||
if len(conditions.OS) > 0 && !slices.Contains(conditions.OS, runtime.GOOS) {
|
||||
continue
|
||||
}
|
||||
|
||||
filteredAnnouncements = append(filteredAnnouncements, announcement)
|
||||
}
|
||||
|
||||
u.announcements = announcements
|
||||
|
||||
if u.wsEventManager.IsPresent() {
|
||||
// Tell the client to send a request to fetch the latest announcements
|
||||
u.wsEventManager.MustGet().SendEvent(events.CheckForAnnouncements, nil)
|
||||
}
|
||||
|
||||
return announcements
|
||||
}
|
||||
207
seanime-2.9.10/internal/updater/check.go
Normal file
207
seanime-2.9.10/internal/updater/check.go
Normal file
@@ -0,0 +1,207 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
// We fetch the latest release from the website first, if it fails we fallback to GitHub API
|
||||
// This allows updates even if Seanime is removed from GitHub
|
||||
var (
|
||||
websiteUrl = "https://seanime.app/api/release"
|
||||
fallbackGithubUrl = "https://api.github.com/repos/5rahim/seanime/releases/latest"
|
||||
)
|
||||
|
||||
type (
|
||||
GitHubResponse struct {
|
||||
Url string `json:"url"`
|
||||
AssetsUrl string `json:"assets_url"`
|
||||
UploadUrl string `json:"upload_url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
TagName string `json:"tag_name"`
|
||||
TargetCommitish string `json:"target_commitish"`
|
||||
Name string `json:"name"`
|
||||
Draft bool `json:"draft"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Assets []struct {
|
||||
Url string `json:"url"`
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
ContentType string `json:"content_type"`
|
||||
State string `json:"state"`
|
||||
Size int64 `json:"size"`
|
||||
DownloadCount int64 `json:"download_count"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
TarballURL string `json:"tarball_url"`
|
||||
ZipballURL string `json:"zipball_url"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
DocsResponse struct {
|
||||
Release Release `json:"release"`
|
||||
}
|
||||
|
||||
Release struct {
|
||||
Url string `json:"url"`
|
||||
HtmlUrl string `json:"html_url"`
|
||||
NodeId string `json:"node_id"`
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Released bool `json:"released"`
|
||||
Version string `json:"version"`
|
||||
Assets []ReleaseAsset `json:"assets"`
|
||||
}
|
||||
ReleaseAsset struct {
|
||||
Url string `json:"url"`
|
||||
Id int64 `json:"id"`
|
||||
NodeId string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
ContentType string `json:"content_type"`
|
||||
Uploaded bool `json:"uploaded"`
|
||||
Size int64 `json:"size"`
|
||||
BrowserDownloadUrl string `json:"browser_download_url"`
|
||||
}
|
||||
)
|
||||
|
||||
func (u *Updater) GetReleaseName(version string) string {
|
||||
|
||||
arch := runtime.GOARCH
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
arch = "x86_64"
|
||||
case "arm64":
|
||||
arch = "arm64"
|
||||
case "386":
|
||||
return "i386"
|
||||
}
|
||||
oos := runtime.GOOS
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
oos = "Linux"
|
||||
case "windows":
|
||||
oos = "Windows"
|
||||
case "darwin":
|
||||
oos = "MacOS"
|
||||
}
|
||||
|
||||
ext := "tar.gz"
|
||||
if oos == "Windows" {
|
||||
ext = "zip"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("seanime-%s_%s_%s.%s", version, oos, arch, ext)
|
||||
}
|
||||
|
||||
func (u *Updater) fetchLatestRelease() (*Release, error) {
|
||||
var release *Release
|
||||
docsRelease, err := u.fetchLatestReleaseFromDocs()
|
||||
if err != nil {
|
||||
ghRelease, err := u.fetchLatestReleaseFromGitHub()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
release = ghRelease
|
||||
} else {
|
||||
release = docsRelease
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func (u *Updater) fetchLatestReleaseFromGitHub() (*Release, error) {
|
||||
|
||||
response, err := u.client.Get(fallbackGithubUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
byteArr, readErr := io.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("error reading response: %w\n", readErr)
|
||||
}
|
||||
|
||||
var res GitHubResponse
|
||||
err = json.Unmarshal(byteArr, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
release := &Release{
|
||||
Url: res.Url,
|
||||
HtmlUrl: res.HtmlUrl,
|
||||
NodeId: res.NodeID,
|
||||
TagName: res.TagName,
|
||||
Name: res.Name,
|
||||
Body: res.Body,
|
||||
PublishedAt: res.PublishedAt,
|
||||
Released: !res.Prerelease && !res.Draft,
|
||||
Version: strings.TrimPrefix(res.TagName, "v"),
|
||||
Assets: make([]ReleaseAsset, len(res.Assets)),
|
||||
}
|
||||
|
||||
for i, asset := range res.Assets {
|
||||
release.Assets[i] = ReleaseAsset{
|
||||
Url: asset.Url,
|
||||
Id: asset.ID,
|
||||
NodeId: asset.NodeID,
|
||||
Name: asset.Name,
|
||||
ContentType: asset.ContentType,
|
||||
Uploaded: asset.State == "uploaded",
|
||||
Size: asset.Size,
|
||||
BrowserDownloadUrl: asset.BrowserDownloadURL,
|
||||
}
|
||||
}
|
||||
|
||||
return release, nil
|
||||
}
|
||||
|
||||
func (u *Updater) fetchLatestReleaseFromDocs() (*Release, error) {
|
||||
|
||||
response, err := u.client.Get(websiteUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
statusCode := response.StatusCode
|
||||
|
||||
if statusCode == 429 {
|
||||
return nil, errors.New("rate limited, try again later")
|
||||
}
|
||||
|
||||
if !((statusCode >= 200) && (statusCode <= 299)) {
|
||||
return nil, fmt.Errorf("http error code: %d\n", statusCode)
|
||||
}
|
||||
|
||||
byteArr, readErr := io.ReadAll(response.Body)
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("error reading response: %w", readErr)
|
||||
}
|
||||
|
||||
var res DocsResponse
|
||||
err = json.Unmarshal(byteArr, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.Release.Version = strings.TrimPrefix(res.Release.TagName, "v")
|
||||
|
||||
return &res.Release, nil
|
||||
}
|
||||
138
seanime-2.9.10/internal/updater/check_test.go
Normal file
138
seanime-2.9.10/internal/updater/check_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"seanime/internal/constants"
|
||||
"seanime/internal/events"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdater_getReleaseName(t *testing.T) {
|
||||
|
||||
updater := Updater{}
|
||||
|
||||
t.Log(updater.GetReleaseName(constants.Version))
|
||||
}
|
||||
|
||||
func TestUpdater_FetchLatestRelease(t *testing.T) {
|
||||
|
||||
fallbackGithubUrl = "https://seanimedud.app/api/releases" // simulate dead endpoint
|
||||
//githubUrl = "https://api.github.com/repos/zbonfo/seanime-desktop/releases/latest"
|
||||
|
||||
updater := New(constants.Version, util.NewLogger(), events.NewMockWSEventManager(util.NewLogger()))
|
||||
release, err := updater.fetchLatestRelease()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if assert.NotNil(t, release) {
|
||||
spew.Dump(release)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdater_FetchLatestReleaseFromDocs(t *testing.T) {
|
||||
|
||||
updater := New(constants.Version, util.NewLogger(), events.NewMockWSEventManager(util.NewLogger()))
|
||||
release, err := updater.fetchLatestReleaseFromDocs()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if assert.NotNil(t, release) {
|
||||
spew.Dump(release)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdater_FetchLatestReleaseFromGitHub(t *testing.T) {
|
||||
|
||||
updater := New(constants.Version, util.NewLogger(), events.NewMockWSEventManager(util.NewLogger()))
|
||||
release, err := updater.fetchLatestReleaseFromGitHub()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if assert.NotNil(t, release) {
|
||||
spew.Dump(release)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdater_CompareVersion(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
currVersion string
|
||||
latestVersion string
|
||||
shouldUpdate bool
|
||||
}{
|
||||
{
|
||||
currVersion: "0.2.2",
|
||||
latestVersion: "0.2.2",
|
||||
shouldUpdate: false,
|
||||
},
|
||||
{
|
||||
currVersion: "2.2.0-prerelease",
|
||||
latestVersion: "2.2.0",
|
||||
shouldUpdate: true,
|
||||
},
|
||||
{
|
||||
currVersion: "2.2.0",
|
||||
latestVersion: "2.2.0-prerelease",
|
||||
shouldUpdate: false,
|
||||
},
|
||||
{
|
||||
currVersion: "0.2.2",
|
||||
latestVersion: "0.2.3",
|
||||
shouldUpdate: true,
|
||||
},
|
||||
{
|
||||
currVersion: "0.2.2",
|
||||
latestVersion: "0.3.0",
|
||||
shouldUpdate: true,
|
||||
},
|
||||
{
|
||||
currVersion: "0.2.2",
|
||||
latestVersion: "1.0.0",
|
||||
shouldUpdate: true,
|
||||
},
|
||||
{
|
||||
currVersion: "0.2.2",
|
||||
latestVersion: "0.2.1",
|
||||
shouldUpdate: false,
|
||||
},
|
||||
{
|
||||
currVersion: "1.0.0",
|
||||
latestVersion: "0.2.1",
|
||||
shouldUpdate: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.latestVersion, func(t *testing.T) {
|
||||
updateType, shouldUpdate := util.CompareVersion(tt.currVersion, tt.latestVersion)
|
||||
assert.Equal(t, tt.shouldUpdate, shouldUpdate)
|
||||
t.Log(tt.latestVersion, updateType)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestUpdater(t *testing.T) {
|
||||
|
||||
u := New(constants.Version, util.NewLogger(), events.NewMockWSEventManager(util.NewLogger()))
|
||||
|
||||
rl, err := u.GetLatestRelease()
|
||||
require.NoError(t, err)
|
||||
|
||||
rl.TagName = "v2.2.1"
|
||||
newV := strings.TrimPrefix(rl.TagName, "v")
|
||||
updateTypeI, shouldUpdate := util.CompareVersion(u.CurrentVersion, newV)
|
||||
isOlder := util.VersionIsOlderThan(u.CurrentVersion, newV)
|
||||
|
||||
util.Spew(isOlder)
|
||||
util.Spew(shouldUpdate)
|
||||
util.Spew(updateTypeI)
|
||||
}
|
||||
300
seanime-2.9.10/internal/updater/download.go
Normal file
300
seanime-2.9.10/internal/updater/download.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"seanime/internal/util"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrExtractionFailed = errors.New("could not extract assets")
|
||||
)
|
||||
|
||||
// DownloadLatestRelease will download the latest release assets and extract them
|
||||
// If the decompression fails, the returned string will be the directory to the compressed file
|
||||
// If the decompression is successful, the returned string will be the directory to the extracted files
|
||||
func (u *Updater) DownloadLatestRelease(assetUrl, dest string) (string, error) {
|
||||
if u.LatestRelease == nil {
|
||||
return "", errors.New("no new release found")
|
||||
}
|
||||
|
||||
u.logger.Debug().Str("asset_url", assetUrl).Str("dest", dest).Msg("updater: Downloading latest release")
|
||||
|
||||
fpath, err := u.downloadAsset(assetUrl, dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dest = filepath.Dir(fpath)
|
||||
|
||||
u.logger.Info().Str("dest", dest).Msg("updater: Downloaded release assets")
|
||||
|
||||
fp, err := u.decompressAsset(fpath, "")
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msg("updater: Failed to decompress release assets")
|
||||
return fp, ErrExtractionFailed
|
||||
}
|
||||
dest = fp
|
||||
|
||||
u.logger.Info().Str("dest", dest).Msg("updater: Successfully decompressed downloaded release assets")
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
func (u *Updater) DownloadLatestReleaseN(assetUrl, dest, folderName string) (string, error) {
|
||||
if u.LatestRelease == nil {
|
||||
return "", errors.New("no new release found")
|
||||
}
|
||||
|
||||
u.logger.Debug().Str("asset_url", assetUrl).Str("dest", dest).Msg("updater: Downloading latest release")
|
||||
|
||||
fpath, err := u.downloadAsset(assetUrl, dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
dest = filepath.Dir(fpath)
|
||||
|
||||
u.logger.Info().Str("dest", dest).Msg("updater: Downloaded release assets")
|
||||
|
||||
fp, err := u.decompressAsset(fpath, folderName)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msg("updater: Failed to decompress release assets")
|
||||
return fp, err
|
||||
}
|
||||
dest = fp
|
||||
|
||||
u.logger.Info().Str("dest", dest).Msg("updater: Successfully decompressed downloaded release assets")
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
func (u *Updater) decompressZip(archivePath string, folderName string) (dest string, err error) {
|
||||
topFolderName := "seanime-" + u.LatestRelease.Version
|
||||
if folderName != "" {
|
||||
topFolderName = folderName
|
||||
}
|
||||
// "/seanime-repo/seanime-v1.0.0.zip" -> "/seanime-repo/seanime-1.0.0/"
|
||||
dest = filepath.Join(filepath.Dir(archivePath), topFolderName) // "/seanime-repo/seanime-v1.0.0"
|
||||
|
||||
// Check if the destination folder already exists
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
return dest, errors.New("destination folder already exists")
|
||||
}
|
||||
|
||||
// Create the destination folder
|
||||
err = os.MkdirAll(dest, os.ModePerm)
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
|
||||
r, err := zip.OpenReader(archivePath)
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
u.logger.Debug().Msg("updater: Decompressing release assets (zip)")
|
||||
|
||||
for _, f := range r.File {
|
||||
fpath := filepath.Join(dest, f.Name)
|
||||
if f.FileInfo().IsDir() {
|
||||
os.MkdirAll(fpath, os.ModePerm)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return dest, err
|
||||
}
|
||||
|
||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
outFile.Close()
|
||||
return dest, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(outFile, rc)
|
||||
outFile.Close()
|
||||
rc.Close()
|
||||
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
}
|
||||
|
||||
r.Close()
|
||||
|
||||
err = os.Remove(archivePath)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msg("updater: Failed to remove compressed file")
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
u.logger.Debug().Msg("updater: Decompressed release assets (zip)")
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
func (u *Updater) decompressTarGz(archivePath string, folderName string) (dest string, err error) {
|
||||
topFolderName := "seanime-" + u.LatestRelease.Version
|
||||
if folderName != "" {
|
||||
topFolderName = folderName
|
||||
}
|
||||
dest = filepath.Join(filepath.Dir(archivePath), topFolderName)
|
||||
|
||||
if _, err := os.Stat(dest); err == nil {
|
||||
return dest, errors.New("destination folder already exists")
|
||||
}
|
||||
|
||||
err = os.MkdirAll(dest, os.ModePerm)
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
|
||||
file, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(file)
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
tr := tar.NewReader(gzr)
|
||||
|
||||
u.logger.Debug().Msg("updater: Decompressing release assets (gzip)")
|
||||
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
|
||||
fpath := filepath.Join(dest, header.Name)
|
||||
if header.Typeflag == tar.TypeDir {
|
||||
if err := os.MkdirAll(fpath, os.ModePerm); err != nil {
|
||||
return dest, err
|
||||
}
|
||||
} else {
|
||||
if err := os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
|
||||
return dest, err
|
||||
}
|
||||
|
||||
outFile, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(outFile, tr); err != nil {
|
||||
outFile.Close()
|
||||
return dest, err
|
||||
}
|
||||
outFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
gzr.Close()
|
||||
file.Close()
|
||||
|
||||
err = os.Remove(archivePath)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msg("updater: Failed to remove compressed file")
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
u.logger.Debug().Msg("updater: Decompressed release assets (gzip)")
|
||||
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// decompressAsset will uncompress the release assets and delete the compressed folder
|
||||
// - "/seanime-repo/seanime-v1.0.0.zip" -> "/seanime-repo/seanime-1.0.0/"
|
||||
func (u *Updater) decompressAsset(archivePath string, folderName string) (dest string, err error) {
|
||||
|
||||
defer util.HandlePanicInModuleWithError("updater/download/decompressAsset", &err)
|
||||
|
||||
u.logger.Debug().Str("archive_path", archivePath).Msg("updater: Decompressing release assets")
|
||||
|
||||
ext := filepath.Ext(archivePath)
|
||||
if ext == ".zip" {
|
||||
return u.decompressZip(archivePath, folderName)
|
||||
} else if ext == ".gz" {
|
||||
return u.decompressTarGz(archivePath, folderName)
|
||||
}
|
||||
|
||||
u.logger.Error().Msg("updater: Failed to decompress release assets, unsupported archive format")
|
||||
|
||||
return "", fmt.Errorf("unsupported archive format: %s", ext)
|
||||
|
||||
}
|
||||
|
||||
// downloadAsset will download the release assets to a folder
|
||||
// - "seanime-v1.zip" -> "/seanime-repo/seanime-v1.zip"
|
||||
func (u *Updater) downloadAsset(assetUrl, dest string) (fp string, err error) {
|
||||
|
||||
defer util.HandlePanicInModuleWithError("updater/download/downloadAsset", &err)
|
||||
|
||||
u.logger.Debug().Msg("updater: Downloading assets")
|
||||
|
||||
fp = u.getFilePath(assetUrl, dest)
|
||||
|
||||
// Get the data
|
||||
resp, err := http.Get(assetUrl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check if the request was successful (status code 200)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("failed to download file, %s", resp.Status)
|
||||
}
|
||||
|
||||
// Create the destination folder if it doesn't exist
|
||||
err = os.MkdirAll(dest, os.ModePerm)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msg("updater: Failed to download assets")
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create the file
|
||||
out, err := os.Create(fp)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msg("updater: Failed to download assets")
|
||||
return "", err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Write the body to file
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
u.logger.Error().Err(err).Msg("updater: Failed to download assets")
|
||||
return "", err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (u *Updater) getFilePath(url, dest string) string {
|
||||
// Get the file name from the URL
|
||||
fileName := filepath.Base(url)
|
||||
return filepath.Join(dest, fileName)
|
||||
}
|
||||
90
seanime-2.9.10/internal/updater/download_test.go
Normal file
90
seanime-2.9.10/internal/updater/download_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"github.com/samber/lo"
|
||||
"os"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUpdater_DownloadLatestRelease(t *testing.T) {
|
||||
|
||||
updater := New("0.2.0", util.NewLogger(), nil)
|
||||
|
||||
//tempDir := "E:\\SEANIME-REPO-TEST"
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// Get the latest release
|
||||
release, err := updater.GetLatestRelease()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Find the asset (zip file)
|
||||
asset, ok := lo.Find(release.Assets, func(asset ReleaseAsset) bool {
|
||||
return strings.HasSuffix(asset.BrowserDownloadUrl, "Windows_x86_64.zip")
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("could not find release asset")
|
||||
}
|
||||
|
||||
// Download the asset
|
||||
folderPath, err := updater.DownloadLatestRelease(asset.BrowserDownloadUrl, tempDir)
|
||||
if err != nil {
|
||||
t.Log("Downloaded to:", folderPath)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("Downloaded to:", folderPath)
|
||||
|
||||
// Check if the folder is not empty
|
||||
entries, err := os.ReadDir(folderPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
t.Fatal("folder is empty")
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
t.Log(entry.Name())
|
||||
}
|
||||
|
||||
// Delete the folder
|
||||
if err := os.RemoveAll(folderPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Find the asset (.tar.gz file)
|
||||
asset2, ok := lo.Find(release.Assets, func(asset ReleaseAsset) bool {
|
||||
return strings.HasSuffix(asset.BrowserDownloadUrl, "MacOS_arm64.tar.gz")
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("could not find release asset")
|
||||
}
|
||||
|
||||
// Download the asset
|
||||
folderPath2, err := updater.DownloadLatestRelease(asset2.BrowserDownloadUrl, tempDir)
|
||||
if err != nil {
|
||||
t.Log("Downloaded to:", folderPath2)
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Log("Downloaded to:", folderPath2)
|
||||
|
||||
// Check if the folder is not empty
|
||||
entries2, err := os.ReadDir(folderPath2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(entries2) == 0 {
|
||||
t.Fatal("folder is empty")
|
||||
}
|
||||
|
||||
for _, entry := range entries2 {
|
||||
t.Log(entry.Name())
|
||||
}
|
||||
}
|
||||
356
seanime-2.9.10/internal/updater/selfupdate.go
Normal file
356
seanime-2.9.10/internal/updater/selfupdate.go
Normal file
@@ -0,0 +1,356 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"seanime/internal/constants"
|
||||
"seanime/internal/util"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/lo"
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
const (
|
||||
tempReleaseDir = "seanime_new_release"
|
||||
backupDirName = "backup_restore_if_failed"
|
||||
)
|
||||
|
||||
type (
|
||||
SelfUpdater struct {
|
||||
logger *zerolog.Logger
|
||||
breakLoopCh chan struct{}
|
||||
originalExePath mo.Option[string]
|
||||
updater *Updater
|
||||
fallbackDest string
|
||||
|
||||
tmpExecutableName string
|
||||
}
|
||||
)
|
||||
|
||||
func NewSelfUpdater() *SelfUpdater {
|
||||
logger := util.NewLogger()
|
||||
ret := &SelfUpdater{
|
||||
logger: logger,
|
||||
breakLoopCh: make(chan struct{}),
|
||||
originalExePath: mo.None[string](),
|
||||
updater: New(constants.Version, logger, nil),
|
||||
}
|
||||
|
||||
ret.tmpExecutableName = "seanime.exe.old"
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
ret.tmpExecutableName = "seanime.exe.old"
|
||||
default:
|
||||
ret.tmpExecutableName = "seanime.old"
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Delete all files with the .old extension
|
||||
exePath := getExePath()
|
||||
entries, err := os.ReadDir(filepath.Dir(exePath))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if strings.HasSuffix(entry.Name(), ".old") {
|
||||
_ = os.RemoveAll(filepath.Join(filepath.Dir(exePath), entry.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Started returns a channel that will be closed when the app loop should be broken
|
||||
func (su *SelfUpdater) Started() <-chan struct{} {
|
||||
return su.breakLoopCh
|
||||
}
|
||||
|
||||
func (su *SelfUpdater) StartSelfUpdate(fallbackDestination string) {
|
||||
su.fallbackDest = fallbackDestination
|
||||
close(su.breakLoopCh)
|
||||
}
|
||||
|
||||
// recover will just print a message and attempt to download the latest release
|
||||
func (su *SelfUpdater) recover(assetUrl string) {
|
||||
|
||||
if su.originalExePath.IsAbsent() {
|
||||
return
|
||||
}
|
||||
|
||||
if su.fallbackDest != "" {
|
||||
su.logger.Info().Str("dest", su.fallbackDest).Msg("selfupdate: Attempting to download the latest release")
|
||||
_, _ = su.updater.DownloadLatestRelease(assetUrl, su.fallbackDest)
|
||||
}
|
||||
|
||||
su.logger.Error().Msg("selfupdate: Failed to install update. Update downloaded to 'seanime_new_release'")
|
||||
}
|
||||
|
||||
func getExePath() string {
|
||||
exe, err := os.Executable() // /path/to/seanime.exe
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
exePath, err := filepath.EvalSymlinks(exe) // /path/to/seanime.exe
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return exePath
|
||||
}
|
||||
|
||||
func (su *SelfUpdater) Run() error {
|
||||
|
||||
exePath := getExePath()
|
||||
|
||||
su.originalExePath = mo.Some(exePath)
|
||||
|
||||
exeDir := filepath.Dir(exePath) // /path/to
|
||||
|
||||
var files []string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
files = []string{
|
||||
"seanime.exe",
|
||||
"LICENSE",
|
||||
}
|
||||
default:
|
||||
files = []string{
|
||||
"seanime",
|
||||
"LICENSE",
|
||||
}
|
||||
}
|
||||
|
||||
// Get the new assets
|
||||
su.logger.Info().Msg("selfupdate: Fetching latest release info")
|
||||
|
||||
// Get the latest release
|
||||
release, err := su.updater.GetLatestRelease()
|
||||
if err != nil {
|
||||
su.logger.Error().Err(err).Msg("selfupdate: Failed to get latest release")
|
||||
return err
|
||||
}
|
||||
|
||||
// Find the asset
|
||||
assetName := su.updater.GetReleaseName(release.Version)
|
||||
asset, ok := lo.Find(release.Assets, func(asset ReleaseAsset) bool {
|
||||
return asset.Name == assetName
|
||||
})
|
||||
if !ok {
|
||||
su.logger.Error().Msg("selfupdate: Asset not found")
|
||||
return err
|
||||
}
|
||||
|
||||
su.logger.Info().Msg("selfupdate: Downloading latest release")
|
||||
|
||||
// Download the asset to exeDir/seanime_tmp
|
||||
newReleaseDir, err := su.updater.DownloadLatestReleaseN(asset.BrowserDownloadUrl, exeDir, tempReleaseDir)
|
||||
if err != nil {
|
||||
su.logger.Error().Err(err).Msg("selfupdate: Failed to download latest release")
|
||||
return err
|
||||
}
|
||||
|
||||
// DEVNOTE: Past this point, the application will be broken
|
||||
// Use "recover" to attempt to recover the application
|
||||
|
||||
su.logger.Info().Msg("selfupdate: Creating backup")
|
||||
|
||||
// Delete the backup directory if it exists
|
||||
_ = os.RemoveAll(filepath.Join(exeDir, backupDirName))
|
||||
// Create the backup directory
|
||||
backupDir := filepath.Join(exeDir, backupDirName)
|
||||
_ = os.MkdirAll(backupDir, 0755)
|
||||
|
||||
// Backup the current assets
|
||||
// Copy the files to the backup directory
|
||||
// seanime.exe + /backup_restore_if_failed/seanime.exe
|
||||
// LICENSE + /backup_restore_if_failed/LICENSE
|
||||
for _, file := range files {
|
||||
// We don't check for errors here because we don't want to stop the update process if LICENSE is not found for example
|
||||
_ = copyFile(filepath.Join(exeDir, file), filepath.Join(backupDir, file))
|
||||
}
|
||||
|
||||
su.logger.Info().Msg("selfupdate: Renaming assets")
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
renamingFailed := false
|
||||
failedEntryNames := make([]string, 0)
|
||||
|
||||
// Rename the current assets
|
||||
// seanime.exe -> seanime.exe.old
|
||||
// LICENSE -> LICENSE.old
|
||||
for _, file := range files {
|
||||
err = os.Rename(filepath.Join(exeDir, file), filepath.Join(exeDir, file+".old"))
|
||||
// We care about the error ONLY if the file is the executable
|
||||
if err != nil && (file == "seanime" || file == "seanime.exe") {
|
||||
renamingFailed = true
|
||||
failedEntryNames = append(failedEntryNames, file)
|
||||
//su.recover()
|
||||
su.logger.Error().Err(err).Msg("selfupdate: Failed to rename entry")
|
||||
//return err
|
||||
}
|
||||
}
|
||||
|
||||
if renamingFailed {
|
||||
fmt.Println("---------------------------------")
|
||||
fmt.Println("A second attempt will be made in 30 seconds")
|
||||
fmt.Println("---------------------------------")
|
||||
time.Sleep(30 * time.Second)
|
||||
// Here `failedEntryNames` should only contain NECESSARY files that failed to rename
|
||||
for _, entry := range failedEntryNames {
|
||||
err = os.Rename(filepath.Join(exeDir, entry), filepath.Join(exeDir, entry+".old"))
|
||||
if err != nil {
|
||||
su.logger.Error().Err(err).Msg("selfupdate: Failed to rename entry")
|
||||
su.recover(asset.BrowserDownloadUrl)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now all the files have been renamed, we can move the new release to the exeDir
|
||||
|
||||
su.logger.Info().Msg("selfupdate: Moving assets")
|
||||
|
||||
// Move the new release elements to the exeDir
|
||||
err = moveContents(newReleaseDir, exeDir)
|
||||
if err != nil {
|
||||
su.recover(asset.BrowserDownloadUrl)
|
||||
su.logger.Error().Err(err).Msg("selfupdate: Failed to move assets")
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.Chmod(su.originalExePath.MustGet(), 0755)
|
||||
|
||||
// Delete the new release directory
|
||||
_ = os.RemoveAll(newReleaseDir)
|
||||
|
||||
// Start the new executable
|
||||
su.logger.Info().Msg("selfupdate: Starting new executable")
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
err = openWindows(su.originalExePath.MustGet())
|
||||
case "darwin":
|
||||
err = openMacOS(su.originalExePath.MustGet())
|
||||
case "linux":
|
||||
err = openLinux(su.originalExePath.MustGet())
|
||||
default:
|
||||
su.logger.Fatal().Msg("selfupdate: Unsupported platform")
|
||||
}
|
||||
|
||||
// Remove .old files (will fail on Windows for executable)
|
||||
// Remove seanime.exe.old and LICENSE.old
|
||||
for _, file := range files {
|
||||
_ = os.RemoveAll(filepath.Join(exeDir, file+".old"))
|
||||
}
|
||||
|
||||
// Remove the backup directory
|
||||
_ = os.RemoveAll(backupDir)
|
||||
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func openWindows(path string) error {
|
||||
cmd := util.NewCmd("cmd", "/c", "start", "cmd", "/k", path)
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func openMacOS(path string) error {
|
||||
script := fmt.Sprintf(`
|
||||
tell application "Terminal"
|
||||
do script "%s"
|
||||
activate
|
||||
end tell`, path)
|
||||
cmd := exec.Command("osascript", "-e", script)
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func openLinux(path string) error {
|
||||
// Filter out the -update flag or we end up in an infinite update loop
|
||||
filteredArgs := slices.DeleteFunc(os.Args, func(s string) bool { return s == "-update" })
|
||||
|
||||
// Replace the current process with the updated executable
|
||||
return syscall.Exec(path, filteredArgs, os.Environ())
|
||||
}
|
||||
|
||||
// moveContents moves contents of newReleaseDir to exeDir without deleting existing files
|
||||
func moveContents(newReleaseDir, exeDir string) error {
|
||||
// Ensure exeDir exists
|
||||
if err := os.MkdirAll(exeDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy contents of newReleaseDir to exeDir
|
||||
return copyDir(newReleaseDir, exeDir)
|
||||
}
|
||||
|
||||
// copyFile copies a single file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
// Create destination directory if it does not exist
|
||||
if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
destinationFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destinationFile.Close()
|
||||
|
||||
_, err = io.Copy(destinationFile, sourceFile)
|
||||
return err
|
||||
}
|
||||
|
||||
// copyDir recursively copies a directory tree, attempting to preserve permissions.
|
||||
func copyDir(src string, dst string) error {
|
||||
src = filepath.Clean(src)
|
||||
dst = filepath.Clean(dst)
|
||||
|
||||
info, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dst, info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
srcPath := filepath.Join(src, entry.Name())
|
||||
dstPath := filepath.Join(dst, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
if err := copyDir(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := copyFile(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
127
seanime-2.9.10/internal/updater/updater.go
Normal file
127
seanime-2.9.10/internal/updater/updater.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"seanime/internal/events"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
const (
|
||||
PatchRelease = "patch"
|
||||
MinorRelease = "minor"
|
||||
MajorRelease = "major"
|
||||
)
|
||||
|
||||
type (
|
||||
Updater struct {
|
||||
CurrentVersion string
|
||||
hasCheckedForUpdate bool
|
||||
LatestRelease *Release
|
||||
checkForUpdate bool
|
||||
logger *zerolog.Logger
|
||||
client *http.Client
|
||||
wsEventManager mo.Option[events.WSEventManagerInterface]
|
||||
announcements []Announcement
|
||||
}
|
||||
|
||||
Update struct {
|
||||
Release *Release `json:"release,omitempty"`
|
||||
CurrentVersion string `json:"current_version,omitempty"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
)
|
||||
|
||||
func New(currVersion string, logger *zerolog.Logger, wsEventManager events.WSEventManagerInterface) *Updater {
|
||||
ret := &Updater{
|
||||
CurrentVersion: currVersion,
|
||||
hasCheckedForUpdate: false,
|
||||
checkForUpdate: true,
|
||||
logger: logger,
|
||||
client: &http.Client{
|
||||
Timeout: time.Second * 10,
|
||||
},
|
||||
wsEventManager: mo.None[events.WSEventManagerInterface](),
|
||||
}
|
||||
|
||||
if wsEventManager != nil {
|
||||
ret.wsEventManager = mo.Some[events.WSEventManagerInterface](wsEventManager)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (u *Updater) GetLatestUpdate() (*Update, error) {
|
||||
if !u.checkForUpdate {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rl, err := u.GetLatestRelease()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rl == nil || rl.TagName == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if !rl.Released {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
newV := strings.TrimPrefix(rl.TagName, "v")
|
||||
updateTypeI, shouldUpdate := util.CompareVersion(u.CurrentVersion, newV)
|
||||
if !shouldUpdate {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
updateType := ""
|
||||
if updateTypeI == -1 {
|
||||
updateType = MinorRelease
|
||||
} else if updateTypeI == -2 {
|
||||
updateType = PatchRelease
|
||||
} else if updateTypeI == -3 {
|
||||
updateType = MajorRelease
|
||||
}
|
||||
|
||||
return &Update{
|
||||
Release: rl,
|
||||
CurrentVersion: u.CurrentVersion,
|
||||
Type: updateType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *Updater) ShouldRefetchReleases() {
|
||||
u.hasCheckedForUpdate = false
|
||||
|
||||
if u.wsEventManager.IsPresent() {
|
||||
// Tell the client to send a request to fetch the latest release
|
||||
u.wsEventManager.MustGet().SendEvent(events.CheckForUpdates, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Updater) SetEnabled(checkForUpdate bool) {
|
||||
u.checkForUpdate = checkForUpdate
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// GetLatestRelease returns the latest release from the GitHub repository.
|
||||
func (u *Updater) GetLatestRelease() (*Release, error) {
|
||||
if u.hasCheckedForUpdate {
|
||||
return u.LatestRelease, nil
|
||||
}
|
||||
|
||||
release, err := u.fetchLatestRelease()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.hasCheckedForUpdate = true
|
||||
u.LatestRelease = release
|
||||
return release, nil
|
||||
}
|
||||
22
seanime-2.9.10/internal/updater/updater_test.go
Normal file
22
seanime-2.9.10/internal/updater/updater_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdater_GetLatestUpdate(t *testing.T) {
|
||||
|
||||
fallbackGithubUrl = "https://seanime.app/api/releases" // simulate dead endpoint
|
||||
|
||||
u := New("2.0.2", util.NewLogger(), nil)
|
||||
|
||||
update, err := u.GetLatestUpdate()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, update)
|
||||
|
||||
util.Spew(update)
|
||||
}
|
||||
Reference in New Issue
Block a user