2023-12-06 04:47:41 +00:00
|
|
|
package cache
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/json"
|
2023-12-16 14:19:53 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2023-12-06 04:47:41 +00:00
|
|
|
"io"
|
2023-12-16 14:19:53 +00:00
|
|
|
"log/slog"
|
2023-12-06 04:47:41 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
"github.com/mircearoata/pubgrub-go/pubgrub/semver"
|
2023-12-07 16:57:31 +00:00
|
|
|
"github.com/puzpuzpuz/xsync/v3"
|
2023-12-06 04:47:41 +00:00
|
|
|
"github.com/spf13/viper"
|
|
|
|
)
|
|
|
|
|
|
|
|
const IconFilename = "Resources/Icon128.png" // This is the path UE expects for the icon
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
type Mod struct {
|
|
|
|
ModReference string
|
|
|
|
Name string
|
|
|
|
Author string
|
|
|
|
Icon *string
|
|
|
|
LatestVersion string
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
var loadedMods *xsync.MapOf[string, Mod]
|
2023-12-06 04:47:41 +00:00
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
func GetCacheMods() (*xsync.MapOf[string, Mod], error) {
|
|
|
|
if loadedMods != nil {
|
|
|
|
return loadedMods, nil
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
2024-10-02 08:59:56 +00:00
|
|
|
return LoadCacheMods()
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
func GetCacheMod(mod string) (Mod, error) {
|
|
|
|
cache, err := GetCacheMods()
|
2023-12-06 04:47:41 +00:00
|
|
|
if err != nil {
|
2024-10-02 08:59:56 +00:00
|
|
|
return Mod{}, err
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
2023-12-07 16:57:31 +00:00
|
|
|
value, _ := cache.Load(mod)
|
|
|
|
return value, nil
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
func LoadCacheMods() (*xsync.MapOf[string, Mod], error) {
|
|
|
|
loadedMods = xsync.NewMapOf[string, Mod]()
|
2023-12-06 04:47:41 +00:00
|
|
|
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
|
|
|
if _, err := os.Stat(downloadCache); os.IsNotExist(err) {
|
2024-10-02 08:59:56 +00:00
|
|
|
return loadedMods, nil
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
items, err := os.ReadDir(downloadCache)
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed reading download cache: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, item := range items {
|
|
|
|
if item.IsDir() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = addFileToCache(item.Name())
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
slog.Error("failed to add file to cache", slog.String("file", item.Name()), slog.Any("err", err))
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
}
|
2024-10-02 08:59:56 +00:00
|
|
|
return loadedMods, nil
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
func addFileToCache(filename string) (*Mod, error) {
|
2023-12-06 04:47:41 +00:00
|
|
|
cacheFile, err := readCacheFile(filename)
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to read cache file: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
loadedMods.Compute(cacheFile.ModReference, func(oldValue Mod, loaded bool) (Mod, bool) {
|
|
|
|
if !loaded {
|
|
|
|
return *cacheFile, false
|
|
|
|
}
|
|
|
|
oldVersion, err := semver.NewVersion(oldValue.LatestVersion)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("failed to parse version", slog.String("version", oldValue.LatestVersion), slog.Any("err", err))
|
|
|
|
return *cacheFile, false
|
|
|
|
}
|
|
|
|
newVersion, err := semver.NewVersion(cacheFile.LatestVersion)
|
|
|
|
if err != nil {
|
|
|
|
slog.Error("failed to parse version", slog.String("version", cacheFile.LatestVersion), slog.Any("err", err))
|
|
|
|
return oldValue, false
|
|
|
|
}
|
|
|
|
if newVersion.Compare(oldVersion) > 0 {
|
|
|
|
return *cacheFile, false
|
|
|
|
}
|
|
|
|
return oldValue, false
|
2023-12-07 16:57:31 +00:00
|
|
|
})
|
|
|
|
|
2023-12-06 04:47:41 +00:00
|
|
|
return cacheFile, nil
|
|
|
|
}
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
func readCacheFile(filename string) (*Mod, error) {
|
2023-12-06 04:47:41 +00:00
|
|
|
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
|
|
|
path := filepath.Join(downloadCache, filename)
|
|
|
|
stat, err := os.Stat(path)
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to stat file: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
zipFile, err := os.Open(path)
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to open file: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
defer zipFile.Close()
|
|
|
|
|
|
|
|
size := stat.Size()
|
|
|
|
reader, err := zip.NewReader(zipFile, size)
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to read zip: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var upluginFile *zip.File
|
|
|
|
for _, file := range reader.File {
|
|
|
|
if strings.HasSuffix(file.Name, ".uplugin") {
|
|
|
|
upluginFile = file
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if upluginFile == nil {
|
|
|
|
return nil, errors.New("no uplugin file found in zip")
|
|
|
|
}
|
|
|
|
|
|
|
|
upluginReader, err := upluginFile.Open()
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to open uplugin file: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var uplugin UPlugin
|
|
|
|
data, err := io.ReadAll(upluginReader)
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to read uplugin file: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &uplugin); err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to unmarshal uplugin file: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
modReference := strings.TrimSuffix(upluginFile.Name, ".uplugin")
|
|
|
|
|
|
|
|
var iconFile *zip.File
|
|
|
|
for _, file := range reader.File {
|
|
|
|
if file.Name == IconFilename {
|
|
|
|
iconFile = file
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var icon *string
|
|
|
|
if iconFile != nil {
|
|
|
|
iconReader, err := iconFile.Open()
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to open icon file: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
defer iconReader.Close()
|
|
|
|
|
|
|
|
data, err := io.ReadAll(iconReader)
|
|
|
|
if err != nil {
|
2023-12-16 14:19:53 +00:00
|
|
|
return nil, fmt.Errorf("failed to read icon file: %w", err)
|
2023-12-06 04:47:41 +00:00
|
|
|
}
|
|
|
|
iconData := base64.StdEncoding.EncodeToString(data)
|
|
|
|
icon = &iconData
|
|
|
|
}
|
|
|
|
|
2024-10-02 08:59:56 +00:00
|
|
|
return &Mod{
|
|
|
|
ModReference: modReference,
|
|
|
|
Name: uplugin.FriendlyName,
|
|
|
|
Author: uplugin.CreatedBy,
|
|
|
|
Icon: icon,
|
|
|
|
LatestVersion: uplugin.SemVersion,
|
2023-12-06 04:47:41 +00:00
|
|
|
}, nil
|
|
|
|
}
|