feat: offline mode (#14)
* chore: move mod downloading to cli/cache * feat: data providers, ficsit and local * feat: keep cache in memory, load on init * feat: log invalid cache files instead of returning error * chore: make linter happy * feat: fill cached mod Authors field from CreatedBy * chore: make linter happy again * feat: add icon and size to cached mods * feat: cache the cached file hashes * fix: change to new provider access style --------- Co-authored-by: Vilsol <me@vil.so>
This commit is contained in:
parent
ea983cf851
commit
e4b02a792d
24 changed files with 909 additions and 141 deletions
164
cli/cache/cache.go
vendored
Normal file
164
cli/cache/cache.go
vendored
Normal file
|
@ -0,0 +1,164 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const IconFilename = "Resources/Icon128.png" // This is the path UE expects for the icon
|
||||
|
||||
type File struct {
|
||||
Icon *string
|
||||
ModReference string
|
||||
Hash string
|
||||
Plugin UPlugin
|
||||
Size int64
|
||||
}
|
||||
|
||||
var loadedCache map[string][]File
|
||||
|
||||
func GetCache() (map[string][]File, error) {
|
||||
if loadedCache != nil {
|
||||
return loadedCache, nil
|
||||
}
|
||||
return LoadCache()
|
||||
}
|
||||
|
||||
func GetCacheMod(mod string) ([]File, error) {
|
||||
cache, err := GetCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cache[mod], nil
|
||||
}
|
||||
|
||||
func LoadCache() (map[string][]File, error) {
|
||||
loadedCache = map[string][]File{}
|
||||
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||
if _, err := os.Stat(downloadCache); os.IsNotExist(err) {
|
||||
return map[string][]File{}, nil
|
||||
}
|
||||
|
||||
items, err := os.ReadDir(downloadCache)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed reading download cache")
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
if item.IsDir() {
|
||||
continue
|
||||
}
|
||||
if item.Name() == integrityFilename {
|
||||
continue
|
||||
}
|
||||
|
||||
_, err = addFileToCache(item.Name())
|
||||
if err != nil {
|
||||
log.Err(err).Str("file", item.Name()).Msg("failed to add file to cache")
|
||||
}
|
||||
}
|
||||
return loadedCache, nil
|
||||
}
|
||||
|
||||
func addFileToCache(filename string) (*File, error) {
|
||||
cacheFile, err := readCacheFile(filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read cache file")
|
||||
}
|
||||
|
||||
loadedCache[cacheFile.ModReference] = append(loadedCache[cacheFile.ModReference], *cacheFile)
|
||||
return cacheFile, nil
|
||||
}
|
||||
|
||||
func readCacheFile(filename string) (*File, error) {
|
||||
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||
path := filepath.Join(downloadCache, filename)
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to stat file")
|
||||
}
|
||||
|
||||
zipFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to open file")
|
||||
}
|
||||
defer zipFile.Close()
|
||||
|
||||
size := stat.Size()
|
||||
reader, err := zip.NewReader(zipFile, size)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read zip")
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, errors.Wrap(err, "failed to open uplugin file")
|
||||
}
|
||||
|
||||
var uplugin UPlugin
|
||||
data, err := io.ReadAll(upluginReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read uplugin file")
|
||||
}
|
||||
if err := json.Unmarshal(data, &uplugin); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal uplugin file")
|
||||
}
|
||||
|
||||
modReference := strings.TrimSuffix(upluginFile.Name, ".uplugin")
|
||||
|
||||
hash, err := getFileHash(filename)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get file hash")
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, errors.Wrap(err, "failed to open icon file")
|
||||
}
|
||||
defer iconReader.Close()
|
||||
|
||||
data, err := io.ReadAll(iconReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read icon file")
|
||||
}
|
||||
iconData := base64.StdEncoding.EncodeToString(data)
|
||||
icon = &iconData
|
||||
}
|
||||
|
||||
return &File{
|
||||
ModReference: modReference,
|
||||
Hash: hash,
|
||||
Size: size,
|
||||
Icon: icon,
|
||||
Plugin: uplugin,
|
||||
}, nil
|
||||
}
|
130
cli/cache/download.go
vendored
Normal file
130
cli/cache/download.go
vendored
Normal file
|
@ -0,0 +1,130 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||
)
|
||||
|
||||
type Progresser struct {
|
||||
io.Reader
|
||||
updates chan utils.GenericUpdate
|
||||
total int64
|
||||
running int64
|
||||
}
|
||||
|
||||
func (pt *Progresser) Read(p []byte) (int, error) {
|
||||
n, err := pt.Reader.Read(p)
|
||||
pt.running += int64(n)
|
||||
|
||||
if err == nil {
|
||||
if pt.updates != nil {
|
||||
select {
|
||||
case pt.updates <- utils.GenericUpdate{Progress: float64(pt.running) / float64(pt.total)}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
return n, errors.Wrap(err, "failed to read")
|
||||
}
|
||||
|
||||
func DownloadOrCache(cacheKey string, hash string, url string, updates chan utils.GenericUpdate) (io.ReaderAt, int64, error) {
|
||||
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||
if err := os.MkdirAll(downloadCache, 0o777); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return nil, 0, errors.Wrap(err, "failed creating download cache")
|
||||
}
|
||||
}
|
||||
|
||||
location := filepath.Join(downloadCache, cacheKey)
|
||||
|
||||
stat, err := os.Stat(location)
|
||||
if err == nil {
|
||||
existingHash := ""
|
||||
|
||||
if hash != "" {
|
||||
f, err := os.Open(location)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
|
||||
}
|
||||
|
||||
existingHash, err = utils.SHA256Data(f)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "could not compute hash for file: "+location)
|
||||
}
|
||||
}
|
||||
|
||||
if hash == existingHash {
|
||||
f, err := os.Open(location)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
|
||||
}
|
||||
|
||||
return f, stat.Size(), nil
|
||||
}
|
||||
|
||||
if err := os.Remove(location); err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to delete file: "+location)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, 0, errors.Wrap(err, "failed to stat file: "+location)
|
||||
}
|
||||
|
||||
out, err := os.Create(location)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed creating file at: "+location)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to fetch: "+url)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, 0, fmt.Errorf("bad status: %s on url: %s", resp.Status, url)
|
||||
}
|
||||
|
||||
progresser := &Progresser{
|
||||
Reader: resp.Body,
|
||||
total: resp.ContentLength,
|
||||
updates: updates,
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, progresser)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed writing file to disk")
|
||||
}
|
||||
|
||||
f, err := os.Open(location)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
|
||||
}
|
||||
|
||||
if updates != nil {
|
||||
select {
|
||||
case updates <- utils.GenericUpdate{Progress: 1}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
_, err = addFileToCache(cacheKey)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to add file to cache")
|
||||
}
|
||||
|
||||
return f, resp.ContentLength, nil
|
||||
}
|
107
cli/cache/integrity.go
vendored
Normal file
107
cli/cache/integrity.go
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||
)
|
||||
|
||||
type hashInfo struct {
|
||||
Modified time.Time
|
||||
Hash string
|
||||
Size int64
|
||||
}
|
||||
|
||||
var hashCache map[string]hashInfo
|
||||
|
||||
var integrityFilename = ".integrity"
|
||||
|
||||
func getFileHash(file string) (string, error) {
|
||||
if hashCache == nil {
|
||||
loadHashCache()
|
||||
}
|
||||
cachedHash, ok := hashCache[file]
|
||||
if !ok {
|
||||
return cacheFileHash(file)
|
||||
}
|
||||
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||
stat, err := os.Stat(filepath.Join(downloadCache, file))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to stat file")
|
||||
}
|
||||
if stat.Size() != cachedHash.Size || stat.ModTime() != cachedHash.Modified {
|
||||
return cacheFileHash(file)
|
||||
}
|
||||
return cachedHash.Hash, nil
|
||||
}
|
||||
|
||||
func cacheFileHash(file string) (string, error) {
|
||||
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||
stat, err := os.Stat(filepath.Join(downloadCache, file))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to stat file")
|
||||
}
|
||||
f, err := os.Open(filepath.Join(downloadCache, file))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to open file")
|
||||
}
|
||||
defer f.Close()
|
||||
hash, err := utils.SHA256Data(f)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to hash file")
|
||||
}
|
||||
hashCache[file] = hashInfo{
|
||||
Hash: hash,
|
||||
Size: stat.Size(),
|
||||
Modified: stat.ModTime(),
|
||||
}
|
||||
saveHashCache()
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func loadHashCache() {
|
||||
hashCache = map[string]hashInfo{}
|
||||
cacheFile := filepath.Join(viper.GetString("cache-dir"), "downloadCache", integrityFilename)
|
||||
if _, err := os.Stat(cacheFile); os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
f, err := os.Open(cacheFile)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to open hash cache, recreating")
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
hashCacheJSON, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to read hash cache, recreating")
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(hashCacheJSON, &hashCache); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to unmarshal hash cache, recreating")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func saveHashCache() {
|
||||
cacheFile := filepath.Join(viper.GetString("cache-dir"), "downloadCache", integrityFilename)
|
||||
hashCacheJSON, err := json.Marshal(hashCache)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to marshal hash cache")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.WriteFile(cacheFile, hashCacheJSON, 0o755); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to write hash cache")
|
||||
return
|
||||
}
|
||||
}
|
16
cli/cache/uplugin.go
vendored
Normal file
16
cli/cache/uplugin.go
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
package cache
|
||||
|
||||
type UPlugin struct {
|
||||
SemVersion string `json:"SemVersion"`
|
||||
FriendlyName string `json:"FriendlyName"`
|
||||
Description string `json:"Description"`
|
||||
CreatedBy string `json:"CreatedBy"`
|
||||
Plugins []Plugins `json:"Plugins"`
|
||||
}
|
||||
type Plugins struct {
|
||||
Name string `json:"Name"`
|
||||
SemVersion string `json:"SemVersion"`
|
||||
Enabled bool `json:"Enabled"`
|
||||
BasePlugin bool `json:"BasePlugin"`
|
||||
Optional bool `json:"Optional"`
|
||||
}
|
|
@ -3,7 +3,10 @@ package cli
|
|||
import (
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/cache"
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
|
||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||
)
|
||||
|
||||
|
@ -11,6 +14,7 @@ type GlobalContext struct {
|
|||
Installations *Installations
|
||||
Profiles *Profiles
|
||||
APIClient graphql.Client
|
||||
Provider *provider.MixedProvider
|
||||
}
|
||||
|
||||
var globalContext *GlobalContext
|
||||
|
@ -20,6 +24,14 @@ func InitCLI(apiOnly bool) (*GlobalContext, error) {
|
|||
return globalContext, nil
|
||||
}
|
||||
|
||||
apiClient := ficsit.InitAPI()
|
||||
|
||||
mixedProvider := provider.InitMixedProvider(apiClient)
|
||||
|
||||
if viper.GetBool("offline") {
|
||||
mixedProvider.Offline = true
|
||||
}
|
||||
|
||||
if !apiOnly {
|
||||
profiles, err := InitProfiles()
|
||||
if err != nil {
|
||||
|
@ -31,14 +43,21 @@ func InitCLI(apiOnly bool) (*GlobalContext, error) {
|
|||
return nil, errors.Wrap(err, "failed to initialize installations")
|
||||
}
|
||||
|
||||
_, err = cache.LoadCache()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load cache")
|
||||
}
|
||||
|
||||
globalContext = &GlobalContext{
|
||||
Installations: installations,
|
||||
Profiles: profiles,
|
||||
APIClient: ficsit.InitAPI(),
|
||||
APIClient: apiClient,
|
||||
Provider: mixedProvider,
|
||||
}
|
||||
} else {
|
||||
globalContext = &GlobalContext{
|
||||
APIClient: ficsit.InitAPI(),
|
||||
APIClient: apiClient,
|
||||
Provider: mixedProvider,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,24 +5,24 @@ import (
|
|||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
"github.com/mircearoata/pubgrub-go/pubgrub"
|
||||
"github.com/mircearoata/pubgrub-go/pubgrub/helpers"
|
||||
"github.com/mircearoata/pubgrub-go/pubgrub/semver"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
|
||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||
)
|
||||
|
||||
const smlDownloadTemplate = `https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v%s/SML.zip`
|
||||
|
||||
type DependencyResolver struct {
|
||||
apiClient graphql.Client
|
||||
provider provider.Provider
|
||||
}
|
||||
|
||||
func NewDependencyResolver(apiClient graphql.Client) DependencyResolver {
|
||||
return DependencyResolver{apiClient: apiClient}
|
||||
func NewDependencyResolver(provider provider.Provider) DependencyResolver {
|
||||
return DependencyResolver{provider}
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -32,7 +32,7 @@ var (
|
|||
)
|
||||
|
||||
type ficsitAPISource struct {
|
||||
apiClient graphql.Client
|
||||
provider provider.Provider
|
||||
lockfile *LockFile
|
||||
toInstall map[string]semver.Constraint
|
||||
modVersionInfo map[string]ficsit.ModVersionsWithDependenciesResponse
|
||||
|
@ -67,7 +67,7 @@ func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersi
|
|||
}
|
||||
return versions, nil
|
||||
}
|
||||
response, err := ficsit.ModVersionsWithDependencies(context.TODO(), f.apiClient, pkg)
|
||||
response, err := f.provider.ModVersionsWithDependencies(context.TODO(), pkg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to fetch mod %s", pkg)
|
||||
}
|
||||
|
@ -120,7 +120,7 @@ func (f *ficsitAPISource) PickVersion(pkg string, versions []semver.Version) sem
|
|||
}
|
||||
|
||||
func (d DependencyResolver) ResolveModDependencies(constraints map[string]string, lockFile *LockFile, gameVersion int) (LockFile, error) {
|
||||
smlVersionsDB, err := ficsit.SMLVersions(context.TODO(), d.apiClient)
|
||||
smlVersionsDB, err := d.provider.SMLVersions(context.TODO())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed fetching SML versions")
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
|
|||
}
|
||||
|
||||
ficsitSource := &ficsitAPISource{
|
||||
apiClient: d.apiClient,
|
||||
provider: d.provider,
|
||||
smlVersions: smlVersionsDB.SmlVersions.Sml_versions,
|
||||
gameVersion: gameVersionSemver,
|
||||
lockfile: lockFile,
|
||||
|
@ -153,7 +153,7 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
|
|||
finalError := err
|
||||
var solverErr pubgrub.SolvingError
|
||||
if errors.As(err, &solverErr) {
|
||||
finalError = DependencyResolverError{SolvingError: solverErr, apiClient: d.apiClient, smlVersions: smlVersionsDB.SmlVersions.Sml_versions, gameVersion: gameVersion}
|
||||
finalError = DependencyResolverError{SolvingError: solverErr, provider: d.provider, smlVersions: smlVersionsDB.SmlVersions.Sml_versions, gameVersion: gameVersion}
|
||||
}
|
||||
return nil, errors.Wrap(finalError, "failed to solve dependencies")
|
||||
}
|
||||
|
|
|
@ -5,16 +5,16 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
"github.com/mircearoata/pubgrub-go/pubgrub"
|
||||
"github.com/mircearoata/pubgrub-go/pubgrub/semver"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
|
||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||
)
|
||||
|
||||
type DependencyResolverError struct {
|
||||
pubgrub.SolvingError
|
||||
apiClient graphql.Client
|
||||
provider provider.Provider
|
||||
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
|
||||
gameVersion int
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ func (e DependencyResolverError) Error() string {
|
|||
rootPkg := e.Cause().Terms()[0].Dependency()
|
||||
writer := pubgrub.NewStandardErrorWriter(rootPkg).
|
||||
WithIncompatibilityStringer(
|
||||
MakeDependencyResolverErrorStringer(e.apiClient, e.smlVersions, e.gameVersion),
|
||||
MakeDependencyResolverErrorStringer(e.provider, e.smlVersions, e.gameVersion),
|
||||
)
|
||||
e.WriteTo(writer)
|
||||
return writer.String()
|
||||
|
@ -31,15 +31,15 @@ func (e DependencyResolverError) Error() string {
|
|||
|
||||
type DependencyResolverErrorStringer struct {
|
||||
pubgrub.StandardIncompatibilityStringer
|
||||
apiClient graphql.Client
|
||||
provider provider.Provider
|
||||
packageNames map[string]string
|
||||
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
|
||||
gameVersion int
|
||||
}
|
||||
|
||||
func MakeDependencyResolverErrorStringer(apiClient graphql.Client, smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion, gameVersion int) *DependencyResolverErrorStringer {
|
||||
func MakeDependencyResolverErrorStringer(provider provider.Provider, smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion, gameVersion int) *DependencyResolverErrorStringer {
|
||||
s := &DependencyResolverErrorStringer{
|
||||
apiClient: apiClient,
|
||||
provider: provider,
|
||||
smlVersions: smlVersions,
|
||||
gameVersion: gameVersion,
|
||||
packageNames: map[string]string{},
|
||||
|
@ -58,7 +58,7 @@ func (w *DependencyResolverErrorStringer) getPackageName(pkg string) string {
|
|||
if name, ok := w.packageNames[pkg]; ok {
|
||||
return name
|
||||
}
|
||||
result, err := ficsit.GetModName(context.Background(), w.apiClient, pkg)
|
||||
result, err := w.provider.GetModName(context.TODO(), pkg)
|
||||
if err != nil {
|
||||
return pkg
|
||||
}
|
||||
|
@ -98,7 +98,7 @@ func (w *DependencyResolverErrorStringer) Term(t pubgrub.Term, includeVersion bo
|
|||
}
|
||||
return fmt.Sprintf("%s \"%s\"", fullName, t.Constraint())
|
||||
default:
|
||||
res, err := ficsit.ModVersions(context.Background(), w.apiClient, t.Dependency(), ficsit.VersionFilter{
|
||||
res, err := w.provider.ModVersions(context.TODO(), t.Dependency(), ficsit.VersionFilter{
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/cache"
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
|
||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||
)
|
||||
|
@ -317,7 +318,7 @@ func (i *Installation) ResolveProfile(ctx *GlobalContext) (LockFile, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
resolver := NewDependencyResolver(ctx.APIClient)
|
||||
resolver := NewDependencyResolver(ctx.Provider)
|
||||
|
||||
gameVersion, err := i.GetGameVersion(ctx)
|
||||
if err != nil {
|
||||
|
@ -446,7 +447,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
|
|||
}
|
||||
|
||||
log.Info().Str("mod_reference", modReference).Str("version", version.Version).Str("link", version.Link).Msg("downloading mod")
|
||||
reader, size, err := utils.DownloadOrCache(modReference+"_"+version.Version+".zip", version.Hash, version.Link, genericUpdates)
|
||||
reader, size, err := cache.DownloadOrCache(modReference+"_"+version.Version+".zip", version.Hash, version.Link, genericUpdates)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to download "+modReference+" from: "+version.Link)
|
||||
}
|
||||
|
|
47
cli/provider/ficsit.go
Normal file
47
cli/provider/ficsit.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||
)
|
||||
|
||||
type ficsitProvider struct {
|
||||
client graphql.Client
|
||||
}
|
||||
|
||||
func initFicsitProvider(client graphql.Client) ficsitProvider {
|
||||
return ficsitProvider{
|
||||
client,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ficsitProvider) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) {
|
||||
return ficsit.Mods(context, p.client, filter)
|
||||
}
|
||||
|
||||
func (p ficsitProvider) GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) {
|
||||
return ficsit.GetMod(context, p.client, modReference)
|
||||
}
|
||||
|
||||
func (p ficsitProvider) ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) {
|
||||
return ficsit.ModVersions(context, p.client, modReference, filter)
|
||||
}
|
||||
|
||||
func (p ficsitProvider) SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error) {
|
||||
return ficsit.SMLVersions(context, p.client)
|
||||
}
|
||||
|
||||
func (p ficsitProvider) ResolveModDependencies(context context.Context, filter []ficsit.ModVersionConstraint) (*ficsit.ResolveModDependenciesResponse, error) {
|
||||
return ficsit.ResolveModDependencies(context, p.client, filter)
|
||||
}
|
||||
|
||||
func (p ficsitProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) {
|
||||
return ficsit.ModVersionsWithDependencies(context, p.client, modID)
|
||||
}
|
||||
|
||||
func (p ficsitProvider) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) {
|
||||
return ficsit.GetModName(context, p.client, modReference)
|
||||
}
|
289
cli/provider/local.go
Normal file
289
cli/provider/local.go
Normal file
|
@ -0,0 +1,289 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/cache"
|
||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||
)
|
||||
|
||||
type localProvider struct{}
|
||||
|
||||
func initLocalProvider() localProvider {
|
||||
return localProvider{}
|
||||
}
|
||||
|
||||
func (p localProvider) Mods(_ context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) {
|
||||
cachedMods, err := cache.GetCache()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get cache")
|
||||
}
|
||||
|
||||
mods := make([]ficsit.ModsModsGetModsModsMod, 0)
|
||||
|
||||
for modReference, files := range cachedMods {
|
||||
if modReference == "SML" {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(filter.References) > 0 {
|
||||
skip := true
|
||||
|
||||
for _, a := range filter.References {
|
||||
if a == modReference {
|
||||
skip = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if skip {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
mods = append(mods, ficsit.ModsModsGetModsModsMod{
|
||||
Id: modReference,
|
||||
Name: files[0].Plugin.FriendlyName,
|
||||
Mod_reference: modReference,
|
||||
Last_version_date: time.Now(),
|
||||
Created_at: time.Now(),
|
||||
Downloads: 0,
|
||||
Popularity: 0,
|
||||
Hotness: 0,
|
||||
})
|
||||
}
|
||||
|
||||
if filter.Limit == 0 {
|
||||
filter.Limit = 25
|
||||
}
|
||||
|
||||
low := filter.Offset
|
||||
high := filter.Offset + filter.Limit
|
||||
|
||||
if low > len(mods) {
|
||||
return &ficsit.ModsResponse{
|
||||
Mods: ficsit.ModsModsGetMods{
|
||||
Count: 0,
|
||||
Mods: []ficsit.ModsModsGetModsModsMod{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if high > len(mods) {
|
||||
high = len(mods)
|
||||
}
|
||||
|
||||
mods = mods[low:high]
|
||||
|
||||
return &ficsit.ModsResponse{
|
||||
Mods: ficsit.ModsModsGetMods{
|
||||
Count: len(mods),
|
||||
Mods: mods,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p localProvider) GetMod(_ context.Context, modReference string) (*ficsit.GetModResponse, error) {
|
||||
cachedModFiles, err := cache.GetCacheMod(modReference)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get cache")
|
||||
}
|
||||
|
||||
if len(cachedModFiles) == 0 {
|
||||
return nil, errors.New("mod not found")
|
||||
}
|
||||
|
||||
authors := make([]ficsit.GetModModAuthorsUserMod, 0)
|
||||
|
||||
for _, author := range strings.Split(cachedModFiles[0].Plugin.CreatedBy, ",") {
|
||||
authors = append(authors, ficsit.GetModModAuthorsUserMod{
|
||||
Role: "Unknown",
|
||||
User: ficsit.GetModModAuthorsUserModUser{
|
||||
Username: author,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return &ficsit.GetModResponse{
|
||||
Mod: ficsit.GetModMod{
|
||||
Id: modReference,
|
||||
Name: cachedModFiles[0].Plugin.FriendlyName,
|
||||
Mod_reference: modReference,
|
||||
Created_at: time.Now(),
|
||||
Views: 0,
|
||||
Downloads: 0,
|
||||
Authors: authors,
|
||||
Full_description: "",
|
||||
Source_url: "",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p localProvider) ModVersions(_ context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) {
|
||||
cachedModFiles, err := cache.GetCacheMod(modReference)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get cache")
|
||||
}
|
||||
|
||||
if filter.Limit == 0 {
|
||||
filter.Limit = 25
|
||||
}
|
||||
|
||||
versions := make([]ficsit.ModVersionsModVersionsVersion, 0)
|
||||
|
||||
for _, modFile := range cachedModFiles[filter.Offset : filter.Offset+filter.Limit] {
|
||||
versions = append(versions, ficsit.ModVersionsModVersionsVersion{
|
||||
Id: modReference + ":" + modFile.Plugin.SemVersion,
|
||||
Version: modFile.Plugin.SemVersion,
|
||||
})
|
||||
}
|
||||
|
||||
return &ficsit.ModVersionsResponse{
|
||||
Mod: ficsit.ModVersionsMod{
|
||||
Id: modReference,
|
||||
Versions: versions,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p localProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsResponse, error) {
|
||||
cachedSMLFiles, err := cache.GetCacheMod("SML")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get cache")
|
||||
}
|
||||
|
||||
smlVersions := make([]ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion, 0)
|
||||
|
||||
for _, smlFile := range cachedSMLFiles {
|
||||
smlVersions = append(smlVersions, ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion{
|
||||
Id: "SML:" + smlFile.Plugin.SemVersion,
|
||||
Version: smlFile.Plugin.SemVersion,
|
||||
Satisfactory_version: 0, // TODO: where can this be obtained from?
|
||||
})
|
||||
}
|
||||
|
||||
return &ficsit.SMLVersionsResponse{
|
||||
SmlVersions: ficsit.SMLVersionsSmlVersionsGetSMLVersions{
|
||||
Count: len(smlVersions),
|
||||
Sml_versions: smlVersions,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p localProvider) ResolveModDependencies(_ context.Context, filter []ficsit.ModVersionConstraint) (*ficsit.ResolveModDependenciesResponse, error) {
|
||||
cachedMods, err := cache.GetCache()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get cache")
|
||||
}
|
||||
|
||||
mods := make([]ficsit.ResolveModDependenciesModsModVersion, 0)
|
||||
|
||||
constraintMap := make(map[string]string)
|
||||
|
||||
for _, constraint := range filter {
|
||||
constraintMap[constraint.ModIdOrReference] = constraint.Version
|
||||
}
|
||||
|
||||
for modReference, modFiles := range cachedMods {
|
||||
constraint, ok := constraintMap[modReference]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
semverConstraint, err := semver.NewConstraint(constraint)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse constraint for %s", modReference)
|
||||
}
|
||||
|
||||
versions := make([]ficsit.ResolveModDependenciesModsModVersionVersionsVersion, 0)
|
||||
|
||||
for _, modFile := range modFiles {
|
||||
semverVersion, err := semver.NewVersion(modFile.Plugin.SemVersion)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse version for %s", modReference)
|
||||
}
|
||||
|
||||
if !semverConstraint.Check(semverVersion) {
|
||||
continue
|
||||
}
|
||||
|
||||
dependencies := make([]ficsit.ResolveModDependenciesModsModVersionVersionsVersionDependenciesVersionDependency, 0)
|
||||
|
||||
for _, dependency := range modFile.Plugin.Plugins {
|
||||
if dependency.BasePlugin {
|
||||
continue
|
||||
}
|
||||
dependencies = append(dependencies, ficsit.ResolveModDependenciesModsModVersionVersionsVersionDependenciesVersionDependency{
|
||||
Mod_id: dependency.Name,
|
||||
Condition: dependency.SemVersion,
|
||||
Optional: dependency.Optional,
|
||||
})
|
||||
}
|
||||
|
||||
versions = append(versions, ficsit.ResolveModDependenciesModsModVersionVersionsVersion{
|
||||
Id: modReference + ":" + modFile.Plugin.SemVersion,
|
||||
Version: modFile.Plugin.SemVersion,
|
||||
Link: "",
|
||||
Hash: modFile.Hash,
|
||||
Dependencies: dependencies,
|
||||
})
|
||||
}
|
||||
|
||||
mods = append(mods, ficsit.ResolveModDependenciesModsModVersion{
|
||||
Id: modReference,
|
||||
Mod_reference: modReference,
|
||||
Versions: versions,
|
||||
})
|
||||
}
|
||||
|
||||
return &ficsit.ResolveModDependenciesResponse{
|
||||
Mods: mods,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p localProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) {
|
||||
cachedModFiles, err := cache.GetCacheMod(modID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get cache")
|
||||
}
|
||||
|
||||
versions := make([]ficsit.ModVersionsWithDependenciesModVersionsVersion, 0)
|
||||
|
||||
for _, modFile := range cachedModFiles {
|
||||
versions = append(versions, ficsit.ModVersionsWithDependenciesModVersionsVersion{
|
||||
Id: modID + ":" + modFile.Plugin.SemVersion,
|
||||
Version: modFile.Plugin.SemVersion,
|
||||
})
|
||||
}
|
||||
|
||||
return &ficsit.ModVersionsWithDependenciesResponse{
|
||||
Mod: ficsit.ModVersionsWithDependenciesMod{
|
||||
Id: modID,
|
||||
Versions: versions,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p localProvider) GetModName(_ context.Context, modReference string) (*ficsit.GetModNameResponse, error) {
|
||||
cachedModFiles, err := cache.GetCacheMod(modReference)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get cache")
|
||||
}
|
||||
|
||||
if len(cachedModFiles) == 0 {
|
||||
return nil, errors.New("mod not found")
|
||||
}
|
||||
|
||||
return &ficsit.GetModNameResponse{
|
||||
Mod: ficsit.GetModNameMod{
|
||||
Id: modReference,
|
||||
Name: cachedModFiles[0].Plugin.FriendlyName,
|
||||
Mod_reference: modReference,
|
||||
},
|
||||
}, nil
|
||||
}
|
72
cli/provider/mixed.go
Normal file
72
cli/provider/mixed.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Khan/genqlient/graphql"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||
)
|
||||
|
||||
type MixedProvider struct {
|
||||
ficsitProvider ficsitProvider
|
||||
localProvider localProvider
|
||||
Offline bool
|
||||
}
|
||||
|
||||
func InitMixedProvider(client graphql.Client) *MixedProvider {
|
||||
return &MixedProvider{
|
||||
ficsitProvider: initFicsitProvider(client),
|
||||
localProvider: initLocalProvider(),
|
||||
Offline: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (p MixedProvider) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) {
|
||||
if p.Offline {
|
||||
return p.localProvider.Mods(context, filter)
|
||||
}
|
||||
return p.ficsitProvider.Mods(context, filter)
|
||||
}
|
||||
|
||||
func (p MixedProvider) GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error) {
|
||||
if p.Offline {
|
||||
return p.localProvider.GetMod(context, modReference)
|
||||
}
|
||||
return p.ficsitProvider.GetMod(context, modReference)
|
||||
}
|
||||
|
||||
func (p MixedProvider) ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) {
|
||||
if p.Offline {
|
||||
return p.localProvider.ModVersions(context, modReference, filter)
|
||||
}
|
||||
return p.ficsitProvider.ModVersions(context, modReference, filter)
|
||||
}
|
||||
|
||||
func (p MixedProvider) SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error) {
|
||||
if p.Offline {
|
||||
return p.localProvider.SMLVersions(context)
|
||||
}
|
||||
return p.ficsitProvider.SMLVersions(context)
|
||||
}
|
||||
|
||||
func (p MixedProvider) ResolveModDependencies(context context.Context, filter []ficsit.ModVersionConstraint) (*ficsit.ResolveModDependenciesResponse, error) {
|
||||
if p.Offline {
|
||||
return p.localProvider.ResolveModDependencies(context, filter)
|
||||
}
|
||||
return p.ficsitProvider.ResolveModDependencies(context, filter)
|
||||
}
|
||||
|
||||
func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error) {
|
||||
if p.Offline {
|
||||
return p.localProvider.ModVersionsWithDependencies(context, modID)
|
||||
}
|
||||
return p.ficsitProvider.ModVersionsWithDependencies(context, modID)
|
||||
}
|
||||
|
||||
func (p MixedProvider) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) {
|
||||
if p.Offline {
|
||||
return p.localProvider.GetModName(context, modReference)
|
||||
}
|
||||
return p.ficsitProvider.GetModName(context, modReference)
|
||||
}
|
18
cli/provider/provider.go
Normal file
18
cli/provider/provider.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||
)
|
||||
|
||||
type Provider interface {
|
||||
Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error)
|
||||
GetMod(context context.Context, modReference string) (*ficsit.GetModResponse, error)
|
||||
ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error)
|
||||
SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error)
|
||||
ResolveModDependencies(context context.Context, filter []ficsit.ModVersionConstraint) (*ficsit.ResolveModDependenciesResponse, error)
|
||||
|
||||
ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.ModVersionsWithDependenciesResponse, error)
|
||||
GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error)
|
||||
}
|
|
@ -13,7 +13,7 @@ func profilesGetResolver() DependencyResolver {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
return NewDependencyResolver(ctx.APIClient)
|
||||
return NewDependencyResolver(ctx.Provider)
|
||||
}
|
||||
|
||||
func TestProfileResolution(t *testing.T) {
|
||||
|
|
|
@ -138,6 +138,8 @@ func init() {
|
|||
RootCmd.PersistentFlags().String("graphql-api", "/v2/query", "Path for GraphQL API")
|
||||
RootCmd.PersistentFlags().String("api-key", "", "API key to use when sending requests")
|
||||
|
||||
RootCmd.PersistentFlags().Bool("offline", false, "Whether to only use local data")
|
||||
|
||||
_ = viper.BindPFlag("log", RootCmd.PersistentFlags().Lookup("log"))
|
||||
_ = viper.BindPFlag("log-file", RootCmd.PersistentFlags().Lookup("log-file"))
|
||||
_ = viper.BindPFlag("quiet", RootCmd.PersistentFlags().Lookup("quiet"))
|
||||
|
@ -153,4 +155,6 @@ func init() {
|
|||
_ = viper.BindPFlag("api-base", RootCmd.PersistentFlags().Lookup("api-base"))
|
||||
_ = viper.BindPFlag("graphql-api", RootCmd.PersistentFlags().Lookup("graphql-api"))
|
||||
_ = viper.BindPFlag("api-key", RootCmd.PersistentFlags().Lookup("api-key"))
|
||||
|
||||
_ = viper.BindPFlag("offline", RootCmd.PersistentFlags().Lookup("offline"))
|
||||
}
|
||||
|
|
1
go.mod
1
go.mod
|
@ -6,6 +6,7 @@ require (
|
|||
github.com/JohannesKaufmann/html-to-markdown v1.3.6
|
||||
github.com/Khan/genqlient v0.5.0
|
||||
github.com/MarvinJWendt/testza v0.5.2
|
||||
github.com/Masterminds/semver/v3 v3.2.1
|
||||
github.com/PuerkitoBio/goquery v1.8.0
|
||||
github.com/charmbracelet/bubbles v0.14.0
|
||||
github.com/charmbracelet/bubbletea v0.22.1
|
||||
|
|
2
go.sum
2
go.sum
|
@ -60,6 +60,8 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/
|
|||
github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||
github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4=
|
||||
github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||
|
|
|
@ -57,5 +57,10 @@ func (h headerComponent) View() string {
|
|||
out += "N/A"
|
||||
}
|
||||
|
||||
if h.root.GetProvider().Offline {
|
||||
out += "\n"
|
||||
out += h.labelStyle.Render("Offline")
|
||||
}
|
||||
|
||||
return lipgloss.NewStyle().Margin(1, 0).Render(out)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
tea "github.com/charmbracelet/bubbletea"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli"
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
|
||||
)
|
||||
|
||||
type RootModel interface {
|
||||
|
@ -17,6 +18,7 @@ type RootModel interface {
|
|||
SetCurrentInstallation(installation *cli.Installation) error
|
||||
|
||||
GetAPIClient() graphql.Client
|
||||
GetProvider() *provider.MixedProvider
|
||||
|
||||
Size() tea.WindowSizeMsg
|
||||
SetSize(size tea.WindowSizeMsg)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli"
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
|
||||
"github.com/satisfactorymodding/ficsit-cli/tea/components"
|
||||
"github.com/satisfactorymodding/ficsit-cli/tea/scenes"
|
||||
)
|
||||
|
@ -25,7 +26,7 @@ func newModel(global *cli.GlobalContext) *rootModel {
|
|||
Width: 20,
|
||||
Height: 14,
|
||||
},
|
||||
dependencyResolver: cli.NewDependencyResolver(global.APIClient),
|
||||
dependencyResolver: cli.NewDependencyResolver(global.Provider),
|
||||
}
|
||||
|
||||
m.headerComponent = components.NewHeaderComponent(m)
|
||||
|
@ -63,6 +64,10 @@ func (m *rootModel) GetAPIClient() graphql.Client {
|
|||
return m.global.APIClient
|
||||
}
|
||||
|
||||
func (m *rootModel) GetProvider() *provider.MixedProvider {
|
||||
return m.global.Provider
|
||||
}
|
||||
|
||||
func (m *rootModel) Size() tea.WindowSizeMsg {
|
||||
return m.currentSize
|
||||
}
|
||||
|
|
|
@ -111,7 +111,7 @@ func (m installedModsList) LoadModData() {
|
|||
i++
|
||||
}
|
||||
|
||||
mods, err := ficsit.Mods(context.TODO(), m.root.GetAPIClient(), ficsit.ModFilter{
|
||||
mods, err := m.root.GetProvider().Mods(context.TODO(), ficsit.ModFilter{
|
||||
References: references,
|
||||
})
|
||||
if err != nil {
|
||||
|
|
|
@ -87,7 +87,7 @@ func NewModInfo(root components.RootModel, parent tea.Model, mod utils.Mod) tea.
|
|||
model.help.Width = root.Size().Width
|
||||
|
||||
go func() {
|
||||
fullMod, err := ficsit.GetMod(context.TODO(), root.GetAPIClient(), mod.Reference)
|
||||
fullMod, err := root.GetProvider().GetMod(context.TODO(), mod.Reference)
|
||||
if err != nil {
|
||||
model.modError <- err.Error()
|
||||
return
|
||||
|
|
|
@ -221,7 +221,7 @@ func NewMods(root components.RootModel, parent tea.Model) tea.Model {
|
|||
allMods := make([]ficsit.ModsModsGetModsModsMod, 0)
|
||||
offset := 0
|
||||
for {
|
||||
mods, err := ficsit.Mods(context.TODO(), root.GetAPIClient(), ficsit.ModFilter{
|
||||
mods, err := root.GetProvider().Mods(context.TODO(), ficsit.ModFilter{
|
||||
Limit: 100,
|
||||
Offset: offset,
|
||||
Order_by: ficsit.ModFieldsLastVersionDate,
|
||||
|
|
|
@ -56,7 +56,7 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
|
|||
allVersions := make([]ficsit.ModVersionsModVersionsVersion, 0)
|
||||
offset := 0
|
||||
for {
|
||||
versions, err := ficsit.ModVersions(context.TODO(), root.GetAPIClient(), mod.Reference, ficsit.VersionFilter{
|
||||
versions, err := root.GetProvider().ModVersions(context.TODO(), mod.Reference, ficsit.VersionFilter{
|
||||
Limit: 100,
|
||||
Offset: offset,
|
||||
Order: ficsit.OrderDesc,
|
||||
|
|
114
utils/io.go
114
utils/io.go
|
@ -4,134 +4,20 @@ import (
|
|||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
|
||||
)
|
||||
|
||||
type Progresser struct {
|
||||
io.Reader
|
||||
updates chan GenericUpdate
|
||||
total int64
|
||||
running int64
|
||||
}
|
||||
|
||||
func (pt *Progresser) Read(p []byte) (int, error) {
|
||||
n, err := pt.Reader.Read(p)
|
||||
pt.running += int64(n)
|
||||
|
||||
if err == nil {
|
||||
if pt.updates != nil {
|
||||
select {
|
||||
case pt.updates <- GenericUpdate{Progress: float64(pt.running) / float64(pt.total)}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
return n, errors.Wrap(err, "failed to read")
|
||||
}
|
||||
|
||||
type GenericUpdate struct {
|
||||
ModReference *string
|
||||
Progress float64
|
||||
}
|
||||
|
||||
func DownloadOrCache(cacheKey string, hash string, url string, updates chan GenericUpdate) (io.ReaderAt, int64, error) {
|
||||
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||
if err := os.MkdirAll(downloadCache, 0o777); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
return nil, 0, errors.Wrap(err, "failed creating download cache")
|
||||
}
|
||||
}
|
||||
|
||||
location := filepath.Join(downloadCache, cacheKey)
|
||||
|
||||
stat, err := os.Stat(location)
|
||||
if err == nil {
|
||||
existingHash := ""
|
||||
|
||||
if hash != "" {
|
||||
f, err := os.Open(location)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
|
||||
}
|
||||
|
||||
existingHash, err = SHA256Data(f)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "could not compute hash for file: "+location)
|
||||
}
|
||||
}
|
||||
|
||||
if hash == existingHash {
|
||||
f, err := os.Open(location)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
|
||||
}
|
||||
|
||||
return f, stat.Size(), nil
|
||||
}
|
||||
|
||||
if err := os.Remove(location); err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to delete file: "+location)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
return nil, 0, errors.Wrap(err, "failed to stat file: "+location)
|
||||
}
|
||||
|
||||
out, err := os.Create(location)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed creating file at: "+location)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to fetch: "+url)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, 0, fmt.Errorf("bad status: %s on url: %s", resp.Status, url)
|
||||
}
|
||||
|
||||
progresser := &Progresser{
|
||||
Reader: resp.Body,
|
||||
total: resp.ContentLength,
|
||||
updates: updates,
|
||||
}
|
||||
|
||||
_, err = io.Copy(out, progresser)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed writing file to disk")
|
||||
}
|
||||
|
||||
f, err := os.Open(location)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
|
||||
}
|
||||
|
||||
if updates != nil {
|
||||
select {
|
||||
case updates <- GenericUpdate{Progress: 1}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return f, resp.ContentLength, nil
|
||||
}
|
||||
|
||||
func SHA256Data(f io.Reader) (string, error) {
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
|
|
Loading…
Reference in a new issue