feat: threaded download pooling (#48)

* feat: threaded download pooling
refactor: splice out resolver

* chore: remove debug
This commit is contained in:
Vilsol 2023-12-16 03:59:58 -08:00 committed by GitHub
parent b6592fe185
commit 4195463c60
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 409 additions and 1056 deletions

View file

@ -58,5 +58,4 @@ linters:
- wrapcheck
- gci
- gocritic
- gofumpt
- nonamedreturns

125
cli/cache/download.go vendored
View file

@ -6,14 +6,39 @@ import (
"net/http"
"os"
"path/filepath"
"sync"
"github.com/pkg/errors"
"github.com/puzpuzpuz/xsync/v3"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/utils"
)
type downloadGroup struct {
err error
wait chan bool
hash string
updates []chan<- utils.GenericProgress
size int64
}
var downloadSync = *xsync.NewMapOf[string, *downloadGroup]()
func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- utils.GenericProgress, downloadSemaphore chan int) (*os.File, int64, error) {
group, loaded := downloadSync.LoadOrCompute(cacheKey, func() *downloadGroup {
return &downloadGroup{
hash: hash,
updates: make([]chan<- utils.GenericProgress, 0),
wait: make(chan bool),
}
})
_, _ = downloadSync.Compute(cacheKey, func(oldValue *downloadGroup, loaded bool) (*downloadGroup, bool) {
oldValue.updates = append(oldValue.updates, updates)
return oldValue, false
})
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
if err := os.MkdirAll(downloadCache, 0o777); err != nil {
if !os.IsExist(err) {
@ -23,6 +48,72 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
location := filepath.Join(downloadCache, cacheKey)
if loaded {
if group.hash != hash {
return nil, 0, errors.New("hash mismatch in download group")
}
<-group.wait
if group.err != nil {
return nil, 0, group.err
}
f, err := os.Open(location)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
}
return f, group.size, nil
}
defer downloadSync.Delete(cacheKey)
upstreamUpdates := make(chan utils.GenericProgress)
defer close(upstreamUpdates)
upstreamWaiter := make(chan bool)
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
outer:
for {
select {
case update := <-upstreamUpdates:
for _, u := range group.updates {
u <- update
}
case <-upstreamWaiter:
break outer
}
}
}()
size, err := downloadInternal(cacheKey, location, hash, url, upstreamUpdates, downloadSemaphore)
if err != nil {
group.err = err
close(group.wait)
return nil, 0, err
}
close(upstreamWaiter)
wg.Wait()
group.size = size
close(group.wait)
f, err := os.Open(location)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
}
return f, size, nil
}
func downloadInternal(cacheKey string, location string, hash string, url string, updates chan<- utils.GenericProgress, downloadSemaphore chan int) (int64, error) {
stat, err := os.Stat(location)
if err == nil {
existingHash := ""
@ -30,36 +121,31 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
if hash != "" {
f, err := os.Open(location)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to open file: "+location)
return 0, errors.Wrap(err, "failed to open file: "+location)
}
defer f.Close()
existingHash, err = utils.SHA256Data(f)
if err != nil {
return nil, 0, errors.Wrap(err, "could not compute hash for file: "+location)
return 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
return stat.Size(), nil
}
if err := os.Remove(location); err != nil {
return nil, 0, errors.Wrap(err, "failed to delete file: "+location)
return 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)
return 0, errors.Wrap(err, "failed to stat file: "+location)
}
if updates != nil {
headResp, err := http.Head(url)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to head: "+url)
return 0, errors.Wrap(err, "failed to head: "+url)
}
defer headResp.Body.Close()
updates <- utils.GenericProgress{Total: headResp.ContentLength}
@ -72,18 +158,18 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
out, err := os.Create(location)
if err != nil {
return nil, 0, errors.Wrap(err, "failed creating file at: "+location)
return 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)
return 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)
return 0, fmt.Errorf("bad status: %s on url: %s", resp.Status, url)
}
progresser := &utils.Progresser{
@ -94,12 +180,7 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
_, 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)
return 0, errors.Wrap(err, "failed writing file to disk")
}
if updates != nil {
@ -108,8 +189,8 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- ut
_, err = addFileToCache(cacheKey)
if err != nil {
return nil, 0, errors.Wrap(err, "failed to add file to cache")
return 0, errors.Wrap(err, "failed to add file to cache")
}
return f, resp.ContentLength, nil
return resp.ContentLength, nil
}

View file

@ -26,7 +26,7 @@ func InitCLI(apiOnly bool) (*GlobalContext, error) {
apiClient := ficsit.InitAPI()
mixedProvider := provider.InitMixedProvider(apiClient)
mixedProvider := provider.InitMixedProvider(provider.NewFicsitProvider(apiClient), provider.NewLocalProvider())
if viper.GetBool("offline") {
mixedProvider.Offline = true

View file

@ -1,209 +0,0 @@
package cli
import (
"context"
"fmt"
"slices"
"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/puzpuzpuz/xsync/v3"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
const (
rootPkg = "$$root$$"
smlPkg = "SML"
factoryGamePkg = "FactoryGame"
)
type DependencyResolver struct {
provider provider.Provider
}
func NewDependencyResolver(provider provider.Provider) DependencyResolver {
return DependencyResolver{provider}
}
type ficsitAPISource struct {
provider provider.Provider
lockfile *LockFile
toInstall map[string]semver.Constraint
modVersionInfo *xsync.MapOf[string, ficsit.AllVersionsResponse]
gameVersion semver.Version
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
}
func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersion, error) {
if pkg == rootPkg {
return []pubgrub.PackageVersion{{Version: semver.Version{}, Dependencies: f.toInstall}}, nil
}
if pkg == factoryGamePkg {
return []pubgrub.PackageVersion{{Version: f.gameVersion}}, nil
}
if pkg == smlPkg {
versions := make([]pubgrub.PackageVersion, len(f.smlVersions))
for i, smlVersion := range f.smlVersions {
v, err := semver.NewVersion(smlVersion.Version)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse version %s", smlVersion.Version)
}
gameConstraint, err := semver.NewConstraint(fmt.Sprintf(">=%d", smlVersion.Satisfactory_version))
if err != nil {
return nil, errors.Wrapf(err, "failed to parse constraint %s", fmt.Sprintf(">=%d", smlVersion.Satisfactory_version))
}
versions[i] = pubgrub.PackageVersion{
Version: v,
Dependencies: map[string]semver.Constraint{
factoryGamePkg: gameConstraint,
},
}
}
return versions, nil
}
response, err := f.provider.ModVersionsWithDependencies(context.TODO(), pkg)
if err != nil {
return nil, errors.Wrapf(err, "failed to fetch mod %s", pkg)
}
if !response.Success {
if response.Error != nil {
return nil, errors.Errorf("mod %s not found: %s", pkg, response.Error.Message)
}
return nil, errors.Errorf("mod %s not found", pkg)
}
f.modVersionInfo.Store(pkg, *response)
versions := make([]pubgrub.PackageVersion, len(response.Data))
for i, modVersion := range response.Data {
v, err := semver.NewVersion(modVersion.Version)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse version %s", modVersion.Version)
}
dependencies := make(map[string]semver.Constraint)
optionalDependencies := make(map[string]semver.Constraint)
for _, dependency := range modVersion.Dependencies {
c, err := semver.NewConstraint(dependency.Condition)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse constraint %s", dependency.Condition)
}
if dependency.Optional {
optionalDependencies[dependency.ModID] = c
} else {
dependencies[dependency.ModID] = c
}
}
versions[i] = pubgrub.PackageVersion{
Version: v,
Dependencies: dependencies,
OptionalDependencies: optionalDependencies,
}
}
return versions, nil
}
func (f *ficsitAPISource) PickVersion(pkg string, versions []semver.Version) semver.Version {
if f.lockfile != nil {
if existing, ok := f.lockfile.Mods[pkg]; ok {
v, err := semver.NewVersion(existing.Version)
if err == nil {
if slices.ContainsFunc(versions, func(version semver.Version) bool {
return v.Compare(version) == 0
}) {
return v
}
}
}
}
return helpers.StandardVersionPriority(versions)
}
func (d DependencyResolver) ResolveModDependencies(constraints map[string]string, lockFile *LockFile, gameVersion int) (*LockFile, error) {
smlVersionsDB, err := d.provider.SMLVersions(context.TODO())
if err != nil {
return nil, errors.Wrap(err, "failed fetching SML versions")
}
gameVersionSemver, err := semver.NewVersion(fmt.Sprintf("%d", gameVersion))
if err != nil {
return nil, errors.Wrap(err, "failed parsing game version")
}
toInstall := make(map[string]semver.Constraint, len(constraints))
for k, v := range constraints {
c, err := semver.NewConstraint(v)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse constraint %s", v)
}
toInstall[k] = c
}
ficsitSource := &ficsitAPISource{
provider: d.provider,
smlVersions: smlVersionsDB.SmlVersions.Sml_versions,
gameVersion: gameVersionSemver,
lockfile: lockFile,
toInstall: toInstall,
modVersionInfo: xsync.NewMapOf[string, ficsit.AllVersionsResponse](),
}
result, err := pubgrub.Solve(helpers.NewCachingSource(ficsitSource), rootPkg)
if err != nil {
finalError := err
var solverErr pubgrub.SolvingError
if errors.As(err, &solverErr) {
finalError = DependencyResolverError{SolvingError: solverErr, provider: d.provider, smlVersions: smlVersionsDB.SmlVersions.Sml_versions, gameVersion: gameVersion}
}
return nil, errors.Wrap(finalError, "failed to solve dependencies")
}
delete(result, rootPkg)
delete(result, factoryGamePkg)
outputLock := MakeLockfile()
for k, v := range result {
if k == smlPkg {
for _, version := range ficsitSource.smlVersions {
if version.Version == v.String() {
targets := make(map[string]LockedModTarget)
for _, target := range version.Targets {
targets[string(target.TargetName)] = LockedModTarget{
Link: target.Link,
}
}
outputLock.Mods[k] = LockedMod{
Version: v.String(),
Targets: targets,
}
break
}
}
continue
}
value, _ := ficsitSource.modVersionInfo.Load(k)
versions := value.Data
for _, ver := range versions {
if ver.Version == v.RawString() {
targets := make(map[string]LockedModTarget)
for _, target := range ver.Targets {
targets[target.TargetName] = LockedModTarget{
Link: viper.GetString("api-base") + "/v1/version/" + ver.ID + "/" + target.TargetName + "/download",
Hash: target.Hash,
}
}
outputLock.Mods[k] = LockedMod{
Version: v.String(),
Targets: targets,
}
break
}
}
}
return outputLock, nil
}

View file

@ -1,134 +0,0 @@
package cli
import (
"context"
"fmt"
"strings"
"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
provider provider.Provider
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
gameVersion int
}
func (e DependencyResolverError) Error() string {
rootPkg := e.Cause().Terms()[0].Dependency()
writer := pubgrub.NewStandardErrorWriter(rootPkg).
WithIncompatibilityStringer(
MakeDependencyResolverErrorStringer(e.provider, e.smlVersions, e.gameVersion),
)
e.WriteTo(writer)
return writer.String()
}
type DependencyResolverErrorStringer struct {
pubgrub.StandardIncompatibilityStringer
provider provider.Provider
packageNames map[string]string
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
gameVersion int
}
func MakeDependencyResolverErrorStringer(provider provider.Provider, smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion, gameVersion int) *DependencyResolverErrorStringer {
s := &DependencyResolverErrorStringer{
provider: provider,
smlVersions: smlVersions,
gameVersion: gameVersion,
packageNames: map[string]string{},
}
s.StandardIncompatibilityStringer = pubgrub.NewStandardIncompatibilityStringer().WithTermStringer(s)
return s
}
func (w *DependencyResolverErrorStringer) getPackageName(pkg string) string {
if pkg == smlPkg {
return "SML"
}
if pkg == factoryGamePkg {
return "Satisfactory"
}
if name, ok := w.packageNames[pkg]; ok {
return name
}
result, err := w.provider.GetModName(context.TODO(), pkg)
if err != nil {
return pkg
}
w.packageNames[pkg] = result.Mod.Name
return result.Mod.Name
}
func (w *DependencyResolverErrorStringer) Term(t pubgrub.Term, includeVersion bool) string {
name := w.getPackageName(t.Dependency())
fullName := fmt.Sprintf("%s (%s)", name, t.Dependency())
if name == t.Dependency() {
fullName = t.Dependency()
}
if includeVersion {
if t.Constraint().IsAny() {
return fmt.Sprintf("every version of %s", fullName)
}
switch t.Dependency() {
case factoryGamePkg:
// Remove ".0.0" from the versions mentioned, since only the major is ever used
return fmt.Sprintf("%s \"%s\"", fullName, strings.ReplaceAll(t.Constraint().String(), ".0.0", ""))
case smlPkg:
var matched []semver.Version
for _, v := range w.smlVersions {
ver, err := semver.NewVersion(v.Version)
if err != nil {
// Assume it is contained in the constraint
matched = append(matched, semver.Version{})
continue
}
if t.Constraint().Contains(ver) {
matched = append(matched, ver)
}
}
if len(matched) == 1 {
return fmt.Sprintf("%s \"%s\"", fullName, matched[0])
}
return fmt.Sprintf("%s \"%s\"", fullName, t.Constraint())
default:
res, err := w.provider.ModVersions(context.TODO(), t.Dependency(), ficsit.VersionFilter{
Limit: 100,
})
if err != nil {
return fmt.Sprintf("%s \"%s\"", fullName, t.Constraint())
}
var matched []semver.Version
for _, v := range res.Mod.Versions {
ver, err := semver.NewVersion(v.Version)
if err != nil {
// Assume it is contained in the constraint
matched = append(matched, semver.Version{})
continue
}
if t.Constraint().Contains(ver) {
matched = append(matched, ver)
}
}
if len(matched) == 1 {
return fmt.Sprintf("%s \"%s\"", fullName, matched[0])
}
return fmt.Sprintf("%s \"%s\"", fullName, t.Constraint())
}
}
return fullName
}
func (w *DependencyResolverErrorStringer) IncompatibilityString(incompatibility *pubgrub.Incompatibility, rootPkg string) string {
terms := incompatibility.Terms()
if len(terms) == 1 && terms[0].Dependency() == factoryGamePkg {
return fmt.Sprintf("Satisfactory CL%d is installed", w.gameVersion)
}
return w.StandardIncompatibilityStringer.IncompatibilityString(incompatibility, rootPkg)
}

View file

@ -12,6 +12,7 @@ import (
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"golang.org/x/sync/errgroup"
@ -257,7 +258,7 @@ func (i *Installation) LockFilePath(ctx *GlobalContext) (string, error) {
return filepath.Join(i.BasePath(), platform.LockfilePath, lockFileName), nil
}
func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
func (i *Installation) LockFile(ctx *GlobalContext) (*resolver.LockFile, error) {
lockfilePath, err := i.LockFilePath(ctx)
if err != nil {
return nil, err
@ -268,7 +269,7 @@ func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
return nil, err
}
var lockFile *LockFile
var lockFile *resolver.LockFile
lockFileJSON, err := d.Read(lockfilePath)
if err != nil {
if !d.IsNotExist(err) {
@ -283,7 +284,7 @@ func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
return lockFile, nil
}
func (i *Installation) WriteLockFile(ctx *GlobalContext, lockfile *LockFile) error {
func (i *Installation) WriteLockFile(ctx *GlobalContext, lockfile *resolver.LockFile) error {
lockfilePath, err := i.LockFilePath(ctx)
if err != nil {
return err
@ -327,20 +328,20 @@ func (i *Installation) Wipe() error {
return nil
}
func (i *Installation) ResolveProfile(ctx *GlobalContext) (*LockFile, error) {
func (i *Installation) ResolveProfile(ctx *GlobalContext) (*resolver.LockFile, error) {
lockFile, err := i.LockFile(ctx)
if err != nil {
return nil, err
}
resolver := NewDependencyResolver(ctx.Provider)
depResolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base"))
gameVersion, err := i.GetGameVersion(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to detect game version")
}
lockfile, err := ctx.Profiles.Profiles[i.Profile].Resolve(resolver, lockFile, gameVersion)
lockfile, err := ctx.Profiles.Profiles[i.Profile].Resolve(depResolver, lockFile, gameVersion)
if err != nil {
return nil, errors.Wrap(err, "could not resolve mods")
}
@ -382,7 +383,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate)
return errors.Wrap(err, "failed to detect platform")
}
lockfile := MakeLockfile()
lockfile := resolver.NewLockfile()
if !i.Vanilla {
var err error
@ -502,7 +503,7 @@ func (i *Installation) UpdateMods(ctx *GlobalContext, mods []string) error {
return errors.Wrap(err, "failed to read lock file")
}
resolver := NewDependencyResolver(ctx.Provider)
resolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base"))
gameVersion, err := i.GetGameVersion(ctx)
if err != nil {

View file

@ -34,8 +34,8 @@ func TestAddInstallation(t *testing.T) {
profileName := "InstallationTest"
profile, err := ctx.Profiles.AddProfile(profileName)
testza.AssertNoError(t, err)
testza.AssertNoError(t, profile.AddMod("AreaActions", ">=1.6.5"))
testza.AssertNoError(t, profile.AddMod("RefinedPower", ">=3.2.10"))
testza.AssertNoError(t, profile.AddMod("AreaActions", "1.6.5"))
testza.AssertNoError(t, profile.AddMod("RefinedPower", "3.2.10"))
serverLocation := os.Getenv("SF_DEDICATED_SERVER")
if serverLocation != "" {

View file

@ -1,54 +0,0 @@
package cli
type LockfileVersion int
const (
InitialLockfileVersion = LockfileVersion(iota)
ModTargetsLockfileVersion
// Always last
nextLockfileVersion
CurrentLockfileVersion = nextLockfileVersion - 1
)
type LockFile struct {
Mods map[string]LockedMod `json:"mods"`
Version LockfileVersion `json:"version"`
}
type LockedMod struct {
Dependencies map[string]string `json:"dependencies"`
Targets map[string]LockedModTarget `json:"targets"`
Version string `json:"version"`
}
type LockedModTarget struct {
Hash string `json:"hash"`
Link string `json:"link"`
}
func MakeLockfile() *LockFile {
return &LockFile{
Mods: make(map[string]LockedMod),
Version: CurrentLockfileVersion,
}
}
func (l *LockFile) Clone() *LockFile {
lockFile := &LockFile{
Mods: make(map[string]LockedMod),
Version: l.Version,
}
for k, v := range l.Mods {
lockFile.Mods[k] = v
}
return lockFile
}
func (l *LockFile) Remove(modID ...string) *LockFile {
for _, s := range modID {
delete(l.Mods, s)
}
return l
}

View file

@ -1,6 +1,7 @@
package cli
import (
"context"
"encoding/json"
"fmt"
"os"
@ -9,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/utils"
@ -43,8 +45,9 @@ type Profiles struct {
}
type Profile struct {
Mods map[string]ProfileMod `json:"mods"`
Name string `json:"name"`
Mods map[string]ProfileMod `json:"mods"`
Name string `json:"name"`
RequiredTargets []resolver.TargetName `json:"required_targets"`
}
type ProfileMod struct {
@ -288,7 +291,7 @@ func (p *Profile) HasMod(reference string) bool {
// An optional lockfile can be passed if one exists.
//
// Returns an error if resolution is impossible.
func (p *Profile) Resolve(resolver DependencyResolver, lockFile *LockFile, gameVersion int) (*LockFile, error) {
func (p *Profile) Resolve(resolver resolver.DependencyResolver, lockFile *resolver.LockFile, gameVersion int) (*resolver.LockFile, error) {
toResolve := make(map[string]string)
for modReference, mod := range p.Mods {
if mod.Enabled {
@ -296,7 +299,7 @@ func (p *Profile) Resolve(resolver DependencyResolver, lockFile *LockFile, gameV
}
}
resultLockfile, err := resolver.ResolveModDependencies(toResolve, lockFile, gameVersion)
resultLockfile, err := resolver.ResolveModDependencies(context.TODO(), toResolve, lockFile, gameVersion, p.RequiredTargets)
if err != nil {
return nil, errors.Wrap(err, "failed resolving profile dependencies")
}

View file

@ -4,40 +4,117 @@ import (
"context"
"github.com/Khan/genqlient/graphql"
"github.com/pkg/errors"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
type ficsitProvider struct {
type FicsitProvider struct {
client graphql.Client
}
func initFicsitProvider(client graphql.Client) ficsitProvider {
return ficsitProvider{
func NewFicsitProvider(client graphql.Client) FicsitProvider {
return FicsitProvider{
client,
}
}
func (p ficsitProvider) Mods(context context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) {
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) {
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) {
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) SMLVersions(context context.Context) ([]resolver.SMLVersion, error) {
response, err := ficsit.SMLVersions(context, p.client)
if err != nil {
return nil, err
}
smlVersions := make([]resolver.SMLVersion, len(response.SmlVersions.Sml_versions))
for i, version := range response.GetSmlVersions().Sml_versions {
targets := make([]resolver.SMLVersionTarget, len(version.Targets))
for j, target := range version.Targets {
targets[j] = resolver.SMLVersionTarget{
TargetName: resolver.TargetName(target.TargetName),
Link: target.Link,
}
}
smlVersions[i] = resolver.SMLVersion{
ID: version.Id,
Version: version.Version,
SatisfactoryVersion: version.Satisfactory_version,
Targets: targets,
}
}
return smlVersions, nil
}
func (p ficsitProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
return ficsit.GetAllModVersions(modID)
func (p FicsitProvider) ModVersionsWithDependencies(_ context.Context, modID string) ([]resolver.ModVersion, error) {
response, err := ficsit.GetAllModVersions(modID)
if err != nil {
return nil, err
}
if response.Error != nil {
return nil, errors.New(response.Error.Message)
}
modVersions := make([]resolver.ModVersion, len(response.Data))
for i, modVersion := range response.Data {
dependencies := make([]resolver.Dependency, len(modVersion.Dependencies))
for j, dependency := range modVersion.Dependencies {
dependencies[j] = resolver.Dependency{
ModID: dependency.ModID,
Condition: dependency.Condition,
Optional: dependency.Optional,
}
}
targets := make([]resolver.Target, len(modVersion.Targets))
for j, target := range modVersion.Targets {
targets[j] = resolver.Target{
VersionID: target.VersionID,
TargetName: resolver.TargetName(target.TargetName),
Hash: target.Hash,
Size: target.Size,
}
}
modVersions[i] = resolver.ModVersion{
ID: modVersion.ID,
Version: modVersion.Version,
Dependencies: dependencies,
Targets: targets,
}
}
return modVersions, err
}
func (p ficsitProvider) GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error) {
return ficsit.GetModName(context, p.client, modReference)
func (p FicsitProvider) GetModName(context context.Context, modReference string) (*resolver.ModName, error) {
response, err := ficsit.GetModName(context, p.client, modReference)
if err != nil {
return nil, err
}
return &resolver.ModName{
ID: response.Mod.Id,
ModReference: response.Mod.Mod_reference,
Name: response.Mod.Name,
}, nil
}
func (p FicsitProvider) IsOffline() bool {
return false
}

View file

@ -6,18 +6,19 @@ import (
"time"
"github.com/pkg/errors"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/cli/cache"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
type localProvider struct{}
type LocalProvider struct{}
func initLocalProvider() localProvider {
return localProvider{}
func NewLocalProvider() LocalProvider {
return LocalProvider{}
}
func (p localProvider) Mods(_ context.Context, filter ficsit.ModFilter) (*ficsit.ModsResponse, error) {
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")
@ -89,7 +90,7 @@ func (p localProvider) Mods(_ context.Context, filter ficsit.ModFilter) (*ficsit
}, nil
}
func (p localProvider) GetMod(_ context.Context, modReference string) (*ficsit.GetModResponse, error) {
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")
@ -125,79 +126,44 @@ func (p localProvider) GetMod(_ context.Context, modReference string) (*ficsit.G
}, 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) {
func (p LocalProvider) SMLVersions(_ context.Context) ([]resolver.SMLVersion, error) {
cachedSMLFiles, err := cache.GetCacheMod("SML")
if err != nil {
return nil, errors.Wrap(err, "failed to get cache")
}
smlVersions := make([]ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion, 0)
smlVersions := make([]resolver.SMLVersion, 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?
smlVersions = append(smlVersions, resolver.SMLVersion{
ID: "SML:" + smlFile.Plugin.SemVersion,
Version: smlFile.Plugin.SemVersion,
SatisfactoryVersion: 0, // TODO: where can this be obtained from?
})
}
return &ficsit.SMLVersionsResponse{
SmlVersions: ficsit.SMLVersionsSmlVersionsGetSMLVersions{
Count: len(smlVersions),
Sml_versions: smlVersions,
},
}, nil
return smlVersions, nil
}
func (p localProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
func (p LocalProvider) ModVersionsWithDependencies(_ context.Context, modID string) ([]resolver.ModVersion, error) {
cachedModFiles, err := cache.GetCacheMod(modID)
if err != nil {
return nil, errors.Wrap(err, "failed to get cache")
}
versions := make([]ficsit.ModVersion, 0)
versions := make([]resolver.ModVersion, 0)
for _, modFile := range cachedModFiles {
versions = append(versions, ficsit.ModVersion{
versions = append(versions, resolver.ModVersion{
ID: modID + ":" + modFile.Plugin.SemVersion,
Version: modFile.Plugin.SemVersion,
})
}
return &ficsit.AllVersionsResponse{
Success: true,
Data: versions,
}, nil
return versions, nil
}
func (p localProvider) GetModName(_ context.Context, modReference string) (*ficsit.GetModNameResponse, error) {
func (p LocalProvider) GetModName(_ context.Context, modReference string) (*resolver.ModName, error) {
cachedModFiles, err := cache.GetCacheMod(modReference)
if err != nil {
return nil, errors.Wrap(err, "failed to get cache")
@ -207,11 +173,13 @@ func (p localProvider) GetModName(_ context.Context, modReference string) (*fics
return nil, errors.New("mod not found")
}
return &ficsit.GetModNameResponse{
Mod: ficsit.GetModNameMod{
Id: modReference,
Name: cachedModFiles[0].Plugin.FriendlyName,
Mod_reference: modReference,
},
return &resolver.ModName{
ID: modReference,
Name: cachedModFiles[0].Plugin.FriendlyName,
ModReference: modReference,
}, nil
}
func (p LocalProvider) IsOffline() bool {
return true
}

View file

@ -3,65 +3,58 @@ package provider
import (
"context"
"github.com/Khan/genqlient/graphql"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
type MixedProvider struct {
ficsitProvider ficsitProvider
localProvider localProvider
Offline bool
onlineProvider Provider
offlineProvider Provider
Offline bool
}
func InitMixedProvider(client graphql.Client) *MixedProvider {
func InitMixedProvider(onlineProvider Provider, offlineProvider Provider) *MixedProvider {
return &MixedProvider{
ficsitProvider: initFicsitProvider(client),
localProvider: initLocalProvider(),
Offline: false,
onlineProvider: onlineProvider,
offlineProvider: offlineProvider,
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.offlineProvider.Mods(context, filter)
}
return p.ficsitProvider.Mods(context, filter)
return p.onlineProvider.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.offlineProvider.GetMod(context, modReference)
}
return p.ficsitProvider.GetMod(context, modReference)
return p.onlineProvider.GetMod(context, modReference)
}
func (p MixedProvider) ModVersions(context context.Context, modReference string, filter ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) {
func (p MixedProvider) SMLVersions(context context.Context) ([]resolver.SMLVersion, error) {
if p.Offline {
return p.localProvider.ModVersions(context, modReference, filter)
return p.offlineProvider.SMLVersions(context) // nolint
}
return p.ficsitProvider.ModVersions(context, modReference, filter)
return p.onlineProvider.SMLVersions(context) // nolint
}
func (p MixedProvider) SMLVersions(context context.Context) (*ficsit.SMLVersionsResponse, error) {
func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) ([]resolver.ModVersion, error) {
if p.Offline {
return p.localProvider.SMLVersions(context)
return p.offlineProvider.ModVersionsWithDependencies(context, modID) // nolint
}
return p.ficsitProvider.SMLVersions(context)
return p.onlineProvider.ModVersionsWithDependencies(context, modID) // nolint
}
func (p MixedProvider) ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
func (p MixedProvider) GetModName(context context.Context, modReference string) (*resolver.ModName, error) {
if p.Offline {
return p.localProvider.ModVersionsWithDependencies(context, modID)
return p.offlineProvider.GetModName(context, modReference) // nolint
}
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)
return p.onlineProvider.GetModName(context, modReference) // nolint
}
func (p MixedProvider) IsOffline() bool {

View file

@ -3,15 +3,14 @@ package provider
import (
"context"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
type Provider interface {
resolver.Provider
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)
ModVersionsWithDependencies(context context.Context, modID string) (*ficsit.AllVersionsResponse, error)
GetModName(context context.Context, modReference string) (*ficsit.GetModNameResponse, error)
IsOffline() bool
}

View file

@ -1,12 +1,15 @@
package cli
import (
"context"
"math"
"os"
"testing"
"github.com/MarvinJWendt/testza"
"github.com/rs/zerolog/log"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cfg"
)
@ -15,10 +18,6 @@ func init() {
cfg.SetDefaults()
}
func profilesGetResolver() DependencyResolver {
return NewDependencyResolver(MockProvider{})
}
func installWatcher() chan<- InstallUpdate {
c := make(chan InstallUpdate)
go func() {
@ -35,60 +34,6 @@ func installWatcher() chan<- InstallUpdate {
return c
}
func TestProfileResolution(t *testing.T) {
resolver := profilesGetResolver()
resolved, err := (&Profile{
Name: DefaultProfileName,
Mods: map[string]ProfileMod{
"RefinedPower": {
Version: "3.2.10",
Enabled: true,
},
},
}).Resolve(resolver, nil, math.MaxInt)
testza.AssertNoError(t, err)
testza.AssertNotNil(t, resolved)
testza.AssertLen(t, resolved.Mods, 4)
}
func TestProfileRequiredOlderVersion(t *testing.T) {
resolver := profilesGetResolver()
_, err := (&Profile{
Name: DefaultProfileName,
Mods: map[string]ProfileMod{
"RefinedPower": {
Version: "3.2.11",
Enabled: true,
},
"RefinedRDLib": {
Version: "1.1.5",
Enabled: true,
},
},
}).Resolve(resolver, nil, math.MaxInt)
testza.AssertEqual(t, "failed resolving profile dependencies: failed to solve dependencies: Because installing Refined Power (RefinedPower) \"3.2.11\" and Refined Power (RefinedPower) \"3.2.11\" depends on RefinedRDLib \"^1.1.6\", installing RefinedRDLib \"^1.1.6\".\nSo, because installing RefinedRDLib \"1.1.5\", version solving failed.", err.Error())
}
func TestResolutionNonExistentMod(t *testing.T) {
resolver := profilesGetResolver()
_, err := (&Profile{
Name: DefaultProfileName,
Mods: map[string]ProfileMod{
"ThisModDoesNotExist$$$": {
Version: ">0.0.0",
Enabled: true,
},
},
}).Resolve(resolver, nil, math.MaxInt)
testza.AssertEqual(t, "failed resolving profile dependencies: failed to solve dependencies: failed to make decision: failed to get package versions: mod ThisModDoesNotExist$$$ not found: mod not found", err.Error())
}
func TestUpdateMods(t *testing.T) {
ctx, err := InitCLI(false)
testza.AssertNoError(t, err)
@ -98,17 +43,11 @@ func TestUpdateMods(t *testing.T) {
ctx.Provider = MockProvider{}
resolver := NewDependencyResolver(ctx.Provider)
depResolver := resolver.NewDependencyResolver(ctx.Provider, viper.GetString("api-base"))
oldLockfile, err := (&Profile{
Name: DefaultProfileName,
Mods: map[string]ProfileMod{
"FicsitRemoteMonitoring": {
Version: "0.9.8",
Enabled: true,
},
},
}).Resolve(resolver, nil, math.MaxInt)
oldLockfile, err := depResolver.ResolveModDependencies(context.Background(), map[string]string{
"FicsitRemoteMonitoring": "0.9.8",
}, nil, math.MaxInt, nil)
testza.AssertNoError(t, err)
testza.AssertNotNil(t, oldLockfile)

View file

@ -4,13 +4,17 @@ import (
"context"
"time"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
)
var _ provider.Provider = (*MockProvider)(nil)
type MockProvider struct{}
type MockProvider struct {
resolver.MockProvider
}
func (m MockProvider) Mods(_ context.Context, f ficsit.ModFilter) (*ficsit.ModsResponse, error) {
if f.Offset > 0 {
@ -66,392 +70,111 @@ func (m MockProvider) Mods(_ context.Context, f ficsit.ModFilter) (*ficsit.ModsR
}, nil
}
var commonTargets = []resolver.Target{
{
TargetName: "Windows",
Hash: "698df20278b3de3ec30405569a22050c6721cc682389312258c14948bd8f38ae",
},
{
TargetName: "WindowsServer",
Hash: "7be01ed372e0cf3287a04f5cb32bb9dcf6f6e7a5b7603b7e43669ec4c6c1457f",
},
{
TargetName: "LinuxServer",
Hash: "bdbd4cb1b472a5316621939ae2fe270fd0e3c0f0a75666a9cbe74ff1313c3663",
},
}
func (m MockProvider) ModVersionsWithDependencies(ctx context.Context, modID string) ([]resolver.ModVersion, error) {
switch modID {
case "AreaActions":
return []resolver.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "1.6.7",
Dependencies: []resolver.Dependency{
{
ModID: "SML",
Condition: "^3.4.1",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "1.6.6",
Dependencies: []resolver.Dependency{
{
ModID: "SML",
Condition: "^3.2.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "1.6.5",
Dependencies: []resolver.Dependency{
{
ModID: "SML",
Condition: "^3.0.0",
Optional: false,
},
},
Targets: commonTargets,
},
}, nil
case "FicsitRemoteMonitoring":
return []resolver.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "0.10.1",
Dependencies: []resolver.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "0.10.0",
Dependencies: []resolver.Dependency{
{
ModID: "SML",
Condition: "^3.5.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "0.9.8",
Dependencies: []resolver.Dependency{
{
ModID: "SML",
Condition: "^3.4.1",
Optional: false,
},
},
Targets: commonTargets,
},
}, nil
}
return m.MockProvider.ModVersionsWithDependencies(ctx, modID) // nolint
}
func (m MockProvider) GetMod(_ context.Context, _ string) (*ficsit.GetModResponse, error) {
// Currently used only by TUI
return nil, nil
}
func (m MockProvider) ModVersions(_ context.Context, modReference string, _ ficsit.VersionFilter) (*ficsit.ModVersionsResponse, error) {
switch modReference {
//nolint
case "RefinedPower":
return &ficsit.ModVersionsResponse{Mod: ficsit.ModVersionsMod{
Id: "DGiLzB3ZErWu2V",
Versions: []ficsit.ModVersionsModVersionsVersion{
{Id: "Eqgr4VcB8y1z9a", Version: "3.2.13"},
{Id: "BwVKMJNP8doDLg", Version: "3.2.11"},
{Id: "4XTjMpqFngbu9r", Version: "3.2.10"},
},
}}, nil
//nolint
case "RefinedRDLib":
return &ficsit.ModVersionsResponse{Mod: ficsit.ModVersionsMod{
Id: "B24emzbs6xVZQr",
Versions: []ficsit.ModVersionsModVersionsVersion{
{Id: "2XcE6RUzGhZW7p", Version: "1.1.7"},
{Id: "52RMLEigqT5Ksn", Version: "1.1.6"},
{Id: "F4HY9eP4D5XjWQ", Version: "1.1.5"},
},
}}, nil
}
panic("ModVersions: " + modReference)
}
func (m MockProvider) SMLVersions(_ context.Context) (*ficsit.SMLVersionsResponse, error) {
return &ficsit.SMLVersionsResponse{
SmlVersions: ficsit.SMLVersionsSmlVersionsGetSMLVersions{
Count: 4,
Sml_versions: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion{
{
Id: "v2.2.1",
Version: "2.2.1",
Satisfactory_version: 125236,
Targets: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget{},
},
{
Id: "v3.3.2",
Version: "3.3.2",
Satisfactory_version: 194714,
Targets: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget{
{
TargetName: ficsit.TargetNameWindows,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.3.2/SML.zip",
},
},
},
{
Id: "v3.6.0",
Version: "3.6.0",
Satisfactory_version: 264901,
Targets: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget{
{
TargetName: ficsit.TargetNameWindows,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.0/SML.zip",
},
{
TargetName: ficsit.TargetNameWindowsserver,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.0/SML.zip",
},
{
TargetName: ficsit.TargetNameLinuxserver,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.0/SML.zip",
},
},
},
{
Id: "v3.6.1",
Version: "3.6.1",
Satisfactory_version: 264901,
Targets: []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersionTargetsSMLVersionTarget{
{
TargetName: ficsit.TargetNameWindows,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.1/SML.zip",
},
{
TargetName: ficsit.TargetNameWindowsserver,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.1/SML.zip",
},
{
TargetName: ficsit.TargetNameLinuxserver,
Link: "https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/v3.6.1/SML.zip",
},
},
},
},
},
}, nil
}
var commonTargets = []ficsit.Target{
{
TargetName: "Windows",
Hash: "62f5c84eca8480b3ffe7d6c90f759e3b463f482530e27d854fd48624fdd3acc9",
},
{
TargetName: "WindowsServer",
Hash: "8a83fcd4abece4192038769cc672fff6764d72c32fb6c7a8c58d66156bb07917",
},
{
TargetName: "LinuxServer",
Hash: "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85",
},
}
func (m MockProvider) ModVersionsWithDependencies(_ context.Context, modID string) (*ficsit.AllVersionsResponse, error) {
switch modID {
case "RefinedPower":
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "3.2.13",
Dependencies: []ficsit.Dependency{
{
ModID: "ModularUI",
Condition: "^2.1.11",
Optional: false,
},
{
ModID: "RefinedRDLib",
Condition: "^1.1.7",
Optional: false,
},
{
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "3.2.11",
Dependencies: []ficsit.Dependency{
{
ModID: "ModularUI",
Condition: "^2.1.10",
Optional: false,
},
{
ModID: "RefinedRDLib",
Condition: "^1.1.6",
Optional: false,
},
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "3.2.10",
Dependencies: []ficsit.Dependency{
{
ModID: "ModularUI",
Condition: "^2.1.9",
Optional: false,
},
{
ModID: "RefinedRDLib",
Condition: "^1.1.5",
Optional: false,
},
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
},
}, nil
case "AreaActions":
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "1.6.7",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.4.1",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "1.6.6",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.2.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "1.6.5",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.0.0",
Optional: false,
},
},
Targets: commonTargets,
},
},
}, nil
case "RefinedRDLib":
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "1.1.7",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "1.1.6",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "1.1.5",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
},
}, nil
case "ModularUI":
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "2.1.12",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.1",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "2.1.11",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "2.1.10",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
},
}, nil
case "ThisModDoesNotExist$$$":
return &ficsit.AllVersionsResponse{
Success: false,
Error: &ficsit.Error{
Message: "mod not found",
Code: 200,
},
}, nil
case "FicsitRemoteMonitoring":
return &ficsit.AllVersionsResponse{
Success: true,
Data: []ficsit.ModVersion{
{
ID: "7QcfNdo5QAAyoC",
Version: "0.10.1",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.6.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "0.10.0",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.5.0",
Optional: false,
},
},
Targets: commonTargets,
},
{
ID: "7QcfNdo5QAAyoC",
Version: "0.9.8",
Dependencies: []ficsit.Dependency{
{
ModID: "SML",
Condition: "^3.4.1",
Optional: false,
},
},
Targets: commonTargets,
},
},
}, nil
}
panic("ModVersionsWithDependencies: " + modID)
}
func (m MockProvider) GetModName(_ context.Context, modReference string) (*ficsit.GetModNameResponse, error) {
switch modReference {
case "RefinedPower":
return &ficsit.GetModNameResponse{Mod: ficsit.GetModNameMod{
Id: "DGiLzB3ZErWu2V",
Mod_reference: "RefinedPower",
Name: "Refined Power",
}}, nil
case "RefinedRDLib":
return &ficsit.GetModNameResponse{Mod: ficsit.GetModNameMod{
Id: "B24emzbs6xVZQr",
Mod_reference: "RefinedRDLib",
Name: "RefinedRDLib",
}}, nil
}
panic("GetModName: " + modReference)
}
func (m MockProvider) IsOffline() bool {
return false
}

View file

@ -1,19 +0,0 @@
package cli
type ModVersion struct {
ID string
Version string
Targets map[string]VersionTarget
Dependencies []VersionDependency
}
type VersionTarget struct {
Link string
Hash string
}
type VersionDependency struct {
ModReference string
Constraint string
Optional bool
}

3
go.mod
View file

@ -15,13 +15,13 @@ require (
github.com/charmbracelet/lipgloss v0.9.1
github.com/charmbracelet/x/exp/teatest v0.0.0-20231206171822-6e7b9b308fe7
github.com/jlaffaye/ftp v0.2.0
github.com/mircearoata/pubgrub-go v0.3.3
github.com/muesli/reflow v0.3.0
github.com/pkg/errors v0.9.1
github.com/pterm/pterm v0.12.71
github.com/puzpuzpuz/xsync/v3 v3.0.2
github.com/rs/zerolog v1.31.0
github.com/sahilm/fuzzy v0.1.0
github.com/satisfactorymodding/ficsit-resolver v0.0.2
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.0
golang.org/x/sync v0.5.0
@ -62,6 +62,7 @@ require (
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/microcosm-cc/bluemonday v1.0.26 // indirect
github.com/mircearoata/pubgrub-go v0.3.3 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect

2
go.sum
View file

@ -189,6 +189,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/satisfactorymodding/ficsit-resolver v0.0.2 h1:dj/OsDLpaMUqCHpfBVHvDMUv2nf5gT4HS2ydBMkmtcQ=
github.com/satisfactorymodding/ficsit-resolver v0.0.2/go.mod h1:ckKMmMvDoYbbkEbWXEsMes608uvv6EKphXPhHX8LKSc=
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=

View file

@ -5,6 +5,8 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/pkg/errors"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
@ -14,8 +16,8 @@ import (
type rootModel struct {
headerComponent tea.Model
dependencyResolver cli.DependencyResolver
global *cli.GlobalContext
dependencyResolver resolver.DependencyResolver
currentSize tea.WindowSizeMsg
}
@ -26,7 +28,7 @@ func newModel(global *cli.GlobalContext) *rootModel {
Width: 20,
Height: 14,
},
dependencyResolver: cli.NewDependencyResolver(global.Provider),
dependencyResolver: resolver.NewDependencyResolver(global.Provider, viper.GetString("api-base")),
}
m.headerComponent = components.NewHeaderComponent(m)

View file

@ -248,7 +248,7 @@ func (m apply) View() string {
for _, installPath := range installationList {
s := m.status[installPath]
strs = append(strs, lipgloss.NewStyle().Margin(topMargins, 0, bottomMargins, 1).Render(lipgloss.JoinHorizontal(
strs = append(strs, lipgloss.NewStyle().Margin(topMargins, 0, bottomMargins).Render(lipgloss.JoinHorizontal(
lipgloss.Left,
m.overall.ViewAs(s.overallProgress.Percentage()),
" - ",
@ -265,22 +265,22 @@ func (m apply) View() string {
for _, modReference := range modReferences {
p := s.modProgresses[modReference]
if p.complete || s.done {
strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+modReference)
strs = append(strs, lipgloss.NewStyle().MarginLeft(2).Foreground(lipgloss.Color("22")).Render("✓ ")+modReference)
} else {
if p.downloading {
strs = append(strs, lipgloss.JoinHorizontal(
strs = append(strs, lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinHorizontal(
lipgloss.Left,
m.sub.ViewAs(p.downloadProgress.Percentage()),
" - ",
lipgloss.NewStyle().Render(modReference+" (Downloading)"),
))
)))
} else {
strs = append(strs, lipgloss.JoinHorizontal(
strs = append(strs, lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinHorizontal(
lipgloss.Left,
m.sub.ViewAs(p.extractProgress.Percentage()),
" - ",
lipgloss.NewStyle().Render(modReference+" (Extracting)"),
))
)))
}
}
}

View file

@ -11,7 +11,6 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys"
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
@ -53,45 +52,26 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
go func() {
items := make([]list.Item, 0)
allVersions := make([]ficsit.ModVersionsModVersionsVersion, 0)
offset := 0
for {
versions, err := root.GetProvider().ModVersions(context.TODO(), mod.Reference, ficsit.VersionFilter{
Limit: 100,
Offset: offset,
Order: ficsit.OrderDesc,
Order_by: ficsit.VersionFieldsCreatedAt,
versions, err := root.GetProvider().ModVersionsWithDependencies(context.TODO(), mod.Reference)
if err != nil {
m.err <- err.Error()
return
}
for _, version := range versions {
tempVersion := version
items = append(items, utils.SimpleItem[selectModVersionList]{
ItemTitle: tempVersion.Version,
Activate: func(msg tea.Msg, currentModel selectModVersionList) (tea.Model, tea.Cmd) {
err := root.GetCurrentProfile().AddMod(mod.Reference, tempVersion.Version)
if err != nil {
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
currentModel.error = errorComponent
return currentModel, cmd
}
return currentModel.parent, nil
},
})
if err != nil {
m.err <- err.Error()
return
}
if len(versions.Mod.Versions) == 0 {
break
}
allVersions = append(allVersions, versions.Mod.Versions...)
for i := 0; i < len(versions.Mod.Versions); i++ {
currentOffset := offset
currentI := i
items = append(items, utils.SimpleItem[selectModVersionList]{
ItemTitle: versions.Mod.Versions[i].Version,
Activate: func(msg tea.Msg, currentModel selectModVersionList) (tea.Model, tea.Cmd) {
version := allVersions[currentOffset+currentI]
err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version)
if err != nil {
errorComponent, cmd := components.NewErrorComponent(err.Error(), time.Second*5)
currentModel.error = errorComponent
return currentModel, cmd
}
return currentModel.parent, nil
},
})
}
offset += len(versions.Mod.Versions)
}
m.items <- items

View file

@ -13,8 +13,9 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/truncate"
resolver "github.com/satisfactorymodding/ficsit-resolver"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cli"
"github.com/satisfactorymodding/ficsit-cli/ficsit"
"github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys"
@ -111,7 +112,7 @@ func (m updateModsList) LoadModData() {
return
}
resolver := cli.NewDependencyResolver(m.root.GetProvider())
resolver := resolver.NewDependencyResolver(m.root.GetProvider(), viper.GetString("api-base"))
updatedLockfile, err := currentProfile.Resolve(resolver, nil, gameVersion)
if err != nil {