Resolution, downloads, lockfiles
This commit is contained in:
parent
cc663b678a
commit
0647db1620
20 changed files with 565 additions and 81 deletions
8
.github/workflows/push.yaml
vendored
8
.github/workflows/push.yaml
vendored
|
@ -62,6 +62,12 @@ jobs:
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup steamcmd
|
||||||
|
uses: CyberAndrii/setup-steamcmd@v1
|
||||||
|
|
||||||
|
- name: Install Satisfactory Dedicated Server
|
||||||
|
run: steamcmd +login anonymous +force_install_dir $GITHUB_WORKSPACE/SatisfactoryDedicatedServer +app_update 1690800 validate +quit && ls -lR
|
||||||
|
|
||||||
- name: Download GQL schema
|
- name: Download GQL schema
|
||||||
run: "npx graphqurl https://api.ficsit.app/v2/query --introspect -H 'content-type: application/json' > schema.graphql"
|
run: "npx graphqurl https://api.ficsit.app/v2/query --introspect -H 'content-type: application/json' > schema.graphql"
|
||||||
|
|
||||||
|
@ -70,4 +76,6 @@ jobs:
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./...
|
run: go test ./...
|
||||||
|
env:
|
||||||
|
SF_DEDICATED_SERVER: $GITHUB_WORKSPACE/SatisfactoryDedicatedServer
|
||||||
|
|
||||||
|
|
|
@ -15,5 +15,6 @@ func SetDefaults() {
|
||||||
viper.SetDefault("profiles-file", "profiles.json")
|
viper.SetDefault("profiles-file", "profiles.json")
|
||||||
viper.SetDefault("installations-file", "installations.json")
|
viper.SetDefault("installations-file", "installations.json")
|
||||||
viper.SetDefault("dry-run", false)
|
viper.SetDefault("dry-run", false)
|
||||||
viper.SetDefault("api", "https://api.ficsit.app/v2/query")
|
viper.SetDefault("api-base", "https://api.ficsit.app")
|
||||||
|
viper.SetDefault("graphql-api", "/v2/query")
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,19 @@ package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/Khan/genqlient/graphql"
|
"github.com/Khan/genqlient/graphql"
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
||||||
|
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||||
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const smlDownloadTemplate = `https://github.com/satisfactorymodding/SatisfactoryModLoader/releases/download/%s/SML.zip`
|
||||||
|
|
||||||
type DependencyResolver struct {
|
type DependencyResolver struct {
|
||||||
apiClient graphql.Client
|
apiClient graphql.Client
|
||||||
}
|
}
|
||||||
|
@ -18,25 +23,94 @@ func NewDependencyResolver(apiClient graphql.Client) DependencyResolver {
|
||||||
return DependencyResolver{apiClient: apiClient}
|
return DependencyResolver{apiClient: apiClient}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d DependencyResolver) ResolveModDependencies(constraints map[string]string) (map[string]ModVersion, error) {
|
func (d DependencyResolver) ResolveModDependencies(constraints map[string]string, lockFile *LockFile, gameVersion int) (LockFile, error) {
|
||||||
results := make(map[string]ModVersion)
|
smlVersionsDB, err := ficsit.SMLVersions(context.TODO(), d.apiClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed fetching SMl versions")
|
||||||
|
}
|
||||||
|
|
||||||
toResolve := constraints
|
instance := &resolvingInstance{
|
||||||
|
Resolver: d,
|
||||||
|
InputLock: lockFile,
|
||||||
|
ToResolve: utils.CopyMap(constraints),
|
||||||
|
OutputLock: make(LockFile),
|
||||||
|
SMLVersions: smlVersionsDB,
|
||||||
|
GameVersion: gameVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := instance.Step(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance.OutputLock, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type resolvingInstance struct {
|
||||||
|
Resolver DependencyResolver
|
||||||
|
|
||||||
|
InputLock *LockFile
|
||||||
|
|
||||||
|
ToResolve map[string]string
|
||||||
|
|
||||||
|
OutputLock LockFile
|
||||||
|
|
||||||
|
SMLVersions *ficsit.SMLVersionsResponse
|
||||||
|
GameVersion int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resolvingInstance) Step() error {
|
||||||
|
if len(r.ToResolve) > 0 {
|
||||||
|
if err := r.LockStep(make(map[string]bool)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for len(toResolve) > 0 {
|
|
||||||
converted := make([]ficsit.ModVersionConstraint, 0)
|
converted := make([]ficsit.ModVersionConstraint, 0)
|
||||||
for id, constraint := range toResolve {
|
for id, constraint := range r.ToResolve {
|
||||||
|
if id != "SML" {
|
||||||
converted = append(converted, ficsit.ModVersionConstraint{
|
converted = append(converted, ficsit.ModVersionConstraint{
|
||||||
ModIdOrReference: id,
|
ModIdOrReference: id,
|
||||||
Version: constraint,
|
Version: constraint,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
smlVersionConstraint, _ := semver.NewConstraint(constraint)
|
||||||
|
if existingSML, ok := r.OutputLock[id]; ok {
|
||||||
|
if !smlVersionConstraint.Check(semver.MustParse(existingSML.Version)) {
|
||||||
|
return errors.New("failed resolving dependencies. requires different versions of " + id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toResolve = make(map[string]string)
|
var chosenSMLVersion *semver.Version
|
||||||
|
for _, version := range r.SMLVersions.SmlVersions.Sml_versions {
|
||||||
|
if version.Satisfactory_version > r.GameVersion {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
dependencies, err := ficsit.ResolveModDependencies(context.TODO(), d.apiClient, converted)
|
currentVersion := semver.MustParse(version.Version)
|
||||||
|
if smlVersionConstraint.Check(currentVersion) {
|
||||||
|
if chosenSMLVersion == nil || currentVersion.GreaterThan(chosenSMLVersion) {
|
||||||
|
chosenSMLVersion = currentVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if chosenSMLVersion == nil {
|
||||||
|
return fmt.Errorf("could not find an SML version that matches constraint %s and game version %d", constraint, r.GameVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.OutputLock[id] = LockedMod{
|
||||||
|
Version: chosenSMLVersion.String(),
|
||||||
|
Link: fmt.Sprintf(smlDownloadTemplate, chosenSMLVersion.String()),
|
||||||
|
Dependencies: map[string]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ToResolve = make(map[string]string)
|
||||||
|
|
||||||
|
// TODO Cache
|
||||||
|
dependencies, err := ficsit.ResolveModDependencies(context.TODO(), r.Resolver.apiClient, converted)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed resolving mod dependencies")
|
return errors.Wrap(err, "failed resolving mod dependencies")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mod := range dependencies.Mods {
|
for _, mod := range dependencies.Mods {
|
||||||
|
@ -55,6 +129,8 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
|
||||||
modVersions[i] = ModVersion{
|
modVersions[i] = ModVersion{
|
||||||
ID: version.Id,
|
ID: version.Id,
|
||||||
Version: version.Version,
|
Version: version.Version,
|
||||||
|
Link: viper.GetString("api-base") + version.Link,
|
||||||
|
Hash: version.Hash,
|
||||||
Dependencies: versionDependencies,
|
Dependencies: versionDependencies,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,21 +142,34 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
|
||||||
})
|
})
|
||||||
|
|
||||||
// Pick latest version
|
// Pick latest version
|
||||||
|
// TODO Clone and branch
|
||||||
selectedVersion := modVersions[0]
|
selectedVersion := modVersions[0]
|
||||||
|
|
||||||
if _, ok := results[mod.Mod_reference]; ok {
|
if _, ok := r.OutputLock[mod.Mod_reference]; ok {
|
||||||
if results[mod.Mod_reference].Version != selectedVersion.Version {
|
if r.OutputLock[mod.Mod_reference].Version != selectedVersion.Version {
|
||||||
return nil, errors.New("failed resolving dependencies. requires different versions of " + mod.Mod_reference)
|
return errors.New("failed resolving dependencies. requires different versions of " + mod.Mod_reference)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
results[mod.Mod_reference] = selectedVersion
|
modDependencies := make(map[string]string)
|
||||||
|
for _, dependency := range selectedVersion.Dependencies {
|
||||||
|
if !dependency.Optional {
|
||||||
|
modDependencies[dependency.ModReference] = dependency.Constraint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.OutputLock[mod.Mod_reference] = LockedMod{
|
||||||
|
Version: selectedVersion.Version,
|
||||||
|
Hash: selectedVersion.Hash,
|
||||||
|
Link: selectedVersion.Link,
|
||||||
|
Dependencies: modDependencies,
|
||||||
|
}
|
||||||
|
|
||||||
for _, dependency := range selectedVersion.Dependencies {
|
for _, dependency := range selectedVersion.Dependencies {
|
||||||
if previousSelectedVersion, ok := results[dependency.ModReference]; ok {
|
if previousSelectedVersion, ok := r.OutputLock[dependency.ModReference]; ok {
|
||||||
constraint, _ := semver.NewConstraint(dependency.Constraint)
|
constraint, _ := semver.NewConstraint(dependency.Constraint)
|
||||||
if !constraint.Check(semver.MustParse(previousSelectedVersion.Version)) {
|
if !constraint.Check(semver.MustParse(previousSelectedVersion.Version)) {
|
||||||
return nil, errors.Errorf("mod %s version %s does not match constraint %s",
|
return errors.Errorf("mod %s version %s does not match constraint %s",
|
||||||
dependency.ModReference,
|
dependency.ModReference,
|
||||||
previousSelectedVersion.Version,
|
previousSelectedVersion.Version,
|
||||||
dependency.Constraint,
|
dependency.Constraint,
|
||||||
|
@ -88,22 +177,96 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO If already exists, verify which constraint is newer and use that
|
if resolving, ok := r.ToResolve[dependency.ModReference]; ok {
|
||||||
toResolve[dependency.ModReference] = dependency.Constraint
|
constraint, _ := semver.NewConstraint(dependency.Constraint)
|
||||||
|
resolvingConstraint, _ := semver.NewConstraint(resolving)
|
||||||
|
intersects, _ := constraint.Intersects(resolvingConstraint)
|
||||||
|
if !intersects {
|
||||||
|
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
||||||
|
dependency.ModReference,
|
||||||
|
resolving,
|
||||||
|
dependency.Constraint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dependency.Optional {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ToResolve[dependency.ModReference] = dependency.Constraint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, constraint := range converted {
|
for _, constraint := range converted {
|
||||||
// Ignore SML
|
if _, ok := r.OutputLock[constraint.ModIdOrReference]; !ok {
|
||||||
if constraint.ModIdOrReference == "SML" {
|
return errors.New("failed resolving dependency: " + constraint.ModIdOrReference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(r.ToResolve) > 0 {
|
||||||
|
if err := r.Step(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *resolvingInstance) LockStep(viewed map[string]bool) error {
|
||||||
|
added := false
|
||||||
|
if r.InputLock != nil {
|
||||||
|
for modReference, version := range r.ToResolve {
|
||||||
|
if _, ok := viewed[modReference]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := results[constraint.ModIdOrReference]; !ok {
|
viewed[modReference] = true
|
||||||
return nil, errors.New("failed resolving dependency: " + constraint.ModIdOrReference)
|
|
||||||
}
|
if locked, ok := (*r.InputLock)[modReference]; ok {
|
||||||
|
constraint, _ := semver.NewConstraint(version)
|
||||||
|
if constraint.Check(semver.MustParse(locked.Version)) {
|
||||||
|
delete(r.ToResolve, modReference)
|
||||||
|
r.OutputLock[modReference] = locked
|
||||||
|
for k, v := range locked.Dependencies {
|
||||||
|
if alreadyResolving, ok := r.ToResolve[k]; ok {
|
||||||
|
cs1, _ := semver.NewConstraint(v)
|
||||||
|
cs2, _ := semver.NewConstraint(alreadyResolving)
|
||||||
|
intersects, _ := cs1.Intersects(cs2)
|
||||||
|
if !intersects {
|
||||||
|
return errors.Errorf("mod %s constraint %s does not intersect with %s",
|
||||||
|
k,
|
||||||
|
v,
|
||||||
|
alreadyResolving,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return results, nil
|
if outVersion, ok := r.OutputLock[k]; ok {
|
||||||
|
constraint, _ := semver.NewConstraint(v)
|
||||||
|
if !constraint.Check(semver.MustParse(outVersion.Version)) {
|
||||||
|
return errors.Errorf("mod %s version %s does not match constraint %s",
|
||||||
|
k,
|
||||||
|
outVersion.Version,
|
||||||
|
v,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.ToResolve[k] = v
|
||||||
|
added = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if added {
|
||||||
|
if err := r.LockStep(viewed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
@ -102,8 +105,14 @@ func (i *Installations) Save() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Installations) AddInstallation(ctx *GlobalContext, installPath string, profile string) (*Installation, error) {
|
func (i *Installations) AddInstallation(ctx *GlobalContext, installPath string, profile string) (*Installation, error) {
|
||||||
|
absolutePath, err := filepath.Abs(installPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "could not resolve absolute path of: "+installPath)
|
||||||
|
}
|
||||||
|
|
||||||
installation := &Installation{
|
installation := &Installation{
|
||||||
Path: installPath,
|
Path: absolutePath,
|
||||||
Profile: profile,
|
Profile: profile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,7 +229,66 @@ func (i *Installation) Install(ctx *GlobalContext) error {
|
||||||
return errors.Wrap(err, "failed to validate installation")
|
return errors.Wrap(err, "failed to validate installation")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Install from lockfile
|
platform, err := i.GetPlatform(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lockfilePath := path.Join(i.Path, platform.LockfilePath)
|
||||||
|
|
||||||
|
var lockFile *LockFile
|
||||||
|
lockFileJSON, err := os.ReadFile(lockfilePath)
|
||||||
|
if err != nil {
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return errors.Wrap(err, "failed reading lockfile")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := json.Unmarshal(lockFileJSON, &lockFile); err != nil {
|
||||||
|
return errors.Wrap(err, "failed parsing lockfile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver := NewDependencyResolver(ctx.APIClient)
|
||||||
|
|
||||||
|
gameVersion, err := i.GetGameVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to detect game version")
|
||||||
|
}
|
||||||
|
|
||||||
|
lockfile, err := ctx.Profiles.Profiles[i.Profile].Resolve(resolver, lockFile, gameVersion)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "could not resolve mods")
|
||||||
|
}
|
||||||
|
|
||||||
|
modsDirectory := path.Join(i.Path, "FactoryGame", "Mods")
|
||||||
|
if err := os.MkdirAll(modsDirectory, 0777); err != nil {
|
||||||
|
return errors.Wrap(err, "failed creating Mods directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
for modReference, version := range lockfile {
|
||||||
|
// Only install if a link is provided, otherwise assume mod is already installed
|
||||||
|
if version.Link != "" {
|
||||||
|
reader, size, err := utils.DownloadOrCache(modReference+"_"+version.Version+".zip", version.Hash, version.Link)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to download "+modReference+" from: "+version.Link)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.ExtractMod(reader, size, path.Join(modsDirectory, modReference)); err != nil {
|
||||||
|
return errors.Wrap(err, "could not extract "+modReference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
marshaledLockfile, err := json.MarshalIndent(lockfile, "", " ")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to serialize lockfile json")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(lockfilePath, marshaledLockfile, 0777); err != nil {
|
||||||
|
return errors.Wrap(err, "failed writing lockfile")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -242,3 +310,63 @@ func (i *Installation) SetProfile(ctx *GlobalContext, profile string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gameVersionFile struct {
|
||||||
|
MajorVersion int `json:"MajorVersion"`
|
||||||
|
MinorVersion int `json:"MinorVersion"`
|
||||||
|
PatchVersion int `json:"PatchVersion"`
|
||||||
|
Changelist int `json:"Changelist"`
|
||||||
|
CompatibleChangelist int `json:"CompatibleChangelist"`
|
||||||
|
IsLicenseeVersion int `json:"IsLicenseeVersion"`
|
||||||
|
IsPromotedBuild int `json:"IsPromotedBuild"`
|
||||||
|
BranchName string `json:"BranchName"`
|
||||||
|
BuildID string `json:"BuildId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installation) GetGameVersion(ctx *GlobalContext) (int, error) {
|
||||||
|
if err := i.Validate(ctx); err != nil {
|
||||||
|
return 0, errors.Wrap(err, "failed to validate installation")
|
||||||
|
}
|
||||||
|
|
||||||
|
platform, err := i.GetPlatform(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := path.Join(i.Path, platform.VersionPath)
|
||||||
|
file, err := os.ReadFile(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return 0, errors.Wrap(err, "could not find game version file")
|
||||||
|
}
|
||||||
|
return 0, errors.Wrap(err, "failed reading version file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionData gameVersionFile
|
||||||
|
if err := json.Unmarshal(file, &versionData); err != nil {
|
||||||
|
return 0, errors.Wrap(err, "failed to parse version file json")
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionData.Changelist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installation) GetPlatform(ctx *GlobalContext) (*Platform, error) {
|
||||||
|
if err := i.Validate(ctx); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to validate installation")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, platform := range platforms {
|
||||||
|
fullPath := path.Join(i.Path, platform.VersionPath)
|
||||||
|
_, err := os.Stat(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
return nil, errors.Wrap(err, "failed detecting version file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &platform, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("no platform detected")
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/MarvinJWendt/testza"
|
"github.com/MarvinJWendt/testza"
|
||||||
|
@ -27,11 +28,13 @@ func TestAddInstallation(t *testing.T) {
|
||||||
testza.AssertNoError(t, profile.AddMod("AreaActions", ">=1.6.5"))
|
testza.AssertNoError(t, profile.AddMod("AreaActions", ">=1.6.5"))
|
||||||
testza.AssertNoError(t, profile.AddMod("ArmorModules__Modpack_All", ">=1.4.1"))
|
testza.AssertNoError(t, profile.AddMod("ArmorModules__Modpack_All", ">=1.4.1"))
|
||||||
|
|
||||||
// TODO Re-enable conditionally
|
serverLocation := os.Getenv("SF_DEDICATED_SERVER")
|
||||||
//installation, err := ctx.Installations.AddInstallation(ctx, "../testdata/server", profileName)
|
if serverLocation != "" {
|
||||||
//testza.AssertNoError(t, err)
|
installation, err := ctx.Installations.AddInstallation(ctx, serverLocation, profileName)
|
||||||
//testza.AssertNotNil(t, installation)
|
testza.AssertNoError(t, err)
|
||||||
//
|
testza.AssertNotNil(t, installation)
|
||||||
//err = installation.Install(ctx)
|
|
||||||
//testza.AssertNoError(t, err)
|
err = installation.Install(ctx)
|
||||||
|
testza.AssertNoError(t, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,5 +4,15 @@ type LockFile map[string]LockedMod
|
||||||
|
|
||||||
type LockedMod struct {
|
type LockedMod struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Link string `json:"link"`
|
||||||
Dependencies map[string]string `json:"dependencies"`
|
Dependencies map[string]string `json:"dependencies"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l LockFile) Clone() LockFile {
|
||||||
|
lockFile := make(LockFile)
|
||||||
|
for k, v := range l {
|
||||||
|
lockFile[k] = v
|
||||||
|
}
|
||||||
|
return lockFile
|
||||||
|
}
|
||||||
|
|
23
cli/platforms.go
Normal file
23
cli/platforms.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import "path"
|
||||||
|
|
||||||
|
type Platform struct {
|
||||||
|
VersionPath string
|
||||||
|
LockfilePath string
|
||||||
|
}
|
||||||
|
|
||||||
|
var platforms = []Platform{
|
||||||
|
{
|
||||||
|
VersionPath: path.Join("Engine", "Binaries", "Linux", "UE4Server-Linux-Shipping.version"),
|
||||||
|
LockfilePath: path.Join("FactoryGame", "Mods", "mods-lock.json"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VersionPath: path.Join("Engine", "Binaries", "Win64", "UE4Server-Win64-Shipping.version"),
|
||||||
|
LockfilePath: path.Join("FactoryGame", "Mods", "mods-lock.json"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
VersionPath: path.Join("Engine", "Binaries", "Win64", "FactoryGame-Win64-Shipping.version"),
|
||||||
|
LockfilePath: path.Join("FactoryGame", "Mods", "mods-lock.json"),
|
||||||
|
},
|
||||||
|
}
|
|
@ -47,7 +47,7 @@ type Profile struct {
|
||||||
|
|
||||||
type ProfileMod struct {
|
type ProfileMod struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Enabled bool `json:"enabled"` // TODO Implement
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitProfiles() (*Profiles, error) {
|
func InitProfiles() (*Profiles, error) {
|
||||||
|
@ -244,6 +244,7 @@ func (p *Profile) AddMod(reference string, version string) error {
|
||||||
|
|
||||||
p.Mods[reference] = ProfileMod{
|
p.Mods[reference] = ProfileMod{
|
||||||
Version: version,
|
Version: version,
|
||||||
|
Enabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -274,31 +275,18 @@ func (p *Profile) HasMod(reference string) bool {
|
||||||
// An optional lockfile can be passed if one exists.
|
// An optional lockfile can be passed if one exists.
|
||||||
//
|
//
|
||||||
// Returns an error if resolution is impossible.
|
// Returns an error if resolution is impossible.
|
||||||
func (p *Profile) Resolve(resolver DependencyResolver, lockFile *LockFile) (*LockFile, error) {
|
func (p *Profile) Resolve(resolver DependencyResolver, lockFile *LockFile, gameVersion int) (LockFile, error) {
|
||||||
toResolve := make(map[string]string)
|
toResolve := make(map[string]string)
|
||||||
for modReference, mod := range p.Mods {
|
for modReference, mod := range p.Mods {
|
||||||
|
if mod.Enabled {
|
||||||
toResolve[modReference] = mod.Version
|
toResolve[modReference] = mod.Version
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dependencies, err := resolver.ResolveModDependencies(toResolve)
|
resultLockfile, err := resolver.ResolveModDependencies(toResolve, lockFile, gameVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed resolving profile dependencies")
|
return nil, errors.Wrap(err, "failed resolving profile dependencies")
|
||||||
}
|
}
|
||||||
|
|
||||||
resultLockFile := LockFile(make(map[string]LockedMod))
|
return resultLockfile, nil
|
||||||
|
|
||||||
for modReference, version := range dependencies {
|
|
||||||
modDependencies := make(map[string]string)
|
|
||||||
|
|
||||||
for _, dependency := range version.Dependencies {
|
|
||||||
modDependencies[dependency.ModReference] = dependency.Constraint
|
|
||||||
}
|
|
||||||
|
|
||||||
resultLockFile[modReference] = LockedMod{
|
|
||||||
Version: version.Version,
|
|
||||||
Dependencies: modDependencies,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &resultLockFile, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,45 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/MarvinJWendt/testza"
|
"github.com/MarvinJWendt/testza"
|
||||||
"github.com/satisfactorymodding/ficsit-cli/ficsit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestProfileResolution(t *testing.T) {
|
func TestProfileResolution(t *testing.T) {
|
||||||
api := ficsit.InitAPI()
|
ctx, err := InitCLI()
|
||||||
resolver := NewDependencyResolver(api)
|
testza.AssertNoError(t, err)
|
||||||
|
|
||||||
|
resolver := NewDependencyResolver(ctx.APIClient)
|
||||||
|
|
||||||
resolved, err := (&Profile{
|
resolved, err := (&Profile{
|
||||||
Name: DefaultProfileName,
|
Name: DefaultProfileName,
|
||||||
Mods: map[string]ProfileMod{
|
Mods: map[string]ProfileMod{
|
||||||
"RefinedPower": {
|
"RefinedPower": {
|
||||||
Version: "3.0.9",
|
Version: "3.0.9",
|
||||||
|
Enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).Resolve(resolver, nil)
|
}).Resolve(resolver, nil, math.MaxInt)
|
||||||
|
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
testza.AssertNotNil(t, resolved)
|
testza.AssertNotNil(t, resolved)
|
||||||
testza.AssertLen(t, *resolved, 3)
|
testza.AssertLen(t, resolved, 4)
|
||||||
|
|
||||||
_, err = (&Profile{
|
_, err = (&Profile{
|
||||||
Name: DefaultProfileName,
|
Name: DefaultProfileName,
|
||||||
Mods: map[string]ProfileMod{
|
Mods: map[string]ProfileMod{
|
||||||
"RefinedPower": {
|
"RefinedPower": {
|
||||||
Version: "3.0.9",
|
Version: "3.0.9",
|
||||||
|
Enabled: true,
|
||||||
},
|
},
|
||||||
"RefinedRDLib": {
|
"RefinedRDLib": {
|
||||||
Version: "1.0.6",
|
Version: "1.0.6",
|
||||||
|
Enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).Resolve(resolver, nil)
|
}).Resolve(resolver, nil, math.MaxInt)
|
||||||
|
|
||||||
testza.AssertEqual(t, "failed resolving profile dependencies: mod RefinedRDLib version 1.0.6 does not match constraint ^1.0.7", err.Error())
|
testza.AssertEqual(t, "failed resolving profile dependencies: mod RefinedRDLib version 1.0.6 does not match constraint ^1.0.7", err.Error())
|
||||||
|
|
||||||
|
@ -43,9 +48,10 @@ func TestProfileResolution(t *testing.T) {
|
||||||
Mods: map[string]ProfileMod{
|
Mods: map[string]ProfileMod{
|
||||||
"ThisModDoesNotExist$$$": {
|
"ThisModDoesNotExist$$$": {
|
||||||
Version: ">0.0.0",
|
Version: ">0.0.0",
|
||||||
|
Enabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}).Resolve(resolver, nil)
|
}).Resolve(resolver, nil, math.MaxInt)
|
||||||
|
|
||||||
testza.AssertEqual(t, "failed resolving profile dependencies: failed resolving dependency: ThisModDoesNotExist$$$", err.Error())
|
testza.AssertEqual(t, "failed resolving profile dependencies: failed resolving dependency: ThisModDoesNotExist$$$", err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ package cli
|
||||||
type ModVersion struct {
|
type ModVersion struct {
|
||||||
ID string
|
ID string
|
||||||
Version string
|
Version string
|
||||||
|
Link string
|
||||||
|
Hash string
|
||||||
Dependencies []VersionDependency
|
Dependencies []VersionDependency
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ var rootCmd = &cobra.Command{
|
||||||
}
|
}
|
||||||
|
|
||||||
if viper.GetString("log-file") != "" {
|
if viper.GetString("log-file") != "" {
|
||||||
logFile, err := os.OpenFile(viper.GetString("log-file"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
logFile, err := os.OpenFile(viper.GetString("log-file"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0777)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to open log file")
|
return errors.Wrap(err, "failed to open log file")
|
||||||
|
@ -122,7 +122,8 @@ func init() {
|
||||||
rootCmd.PersistentFlags().String("profiles-file", "profiles.json", "The profiles file")
|
rootCmd.PersistentFlags().String("profiles-file", "profiles.json", "The profiles file")
|
||||||
rootCmd.PersistentFlags().String("installations-file", "installations.json", "The installations file")
|
rootCmd.PersistentFlags().String("installations-file", "installations.json", "The installations file")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().String("api", "https://api.ficsit.app/v2/query", "URL for API")
|
rootCmd.PersistentFlags().String("api-base", "https://api.ficsit.app", "URL for API")
|
||||||
|
rootCmd.PersistentFlags().String("graphql-api", "/v2/query", "Path for GraphQL API")
|
||||||
|
|
||||||
_ = viper.BindPFlag("log", rootCmd.PersistentFlags().Lookup("log"))
|
_ = viper.BindPFlag("log", rootCmd.PersistentFlags().Lookup("log"))
|
||||||
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
|
_ = viper.BindPFlag("log-file", rootCmd.PersistentFlags().Lookup("log-file"))
|
||||||
|
@ -136,5 +137,6 @@ func init() {
|
||||||
_ = viper.BindPFlag("profiles-file", rootCmd.PersistentFlags().Lookup("profiles-file"))
|
_ = viper.BindPFlag("profiles-file", rootCmd.PersistentFlags().Lookup("profiles-file"))
|
||||||
_ = viper.BindPFlag("installations-file", rootCmd.PersistentFlags().Lookup("installations-file"))
|
_ = viper.BindPFlag("installations-file", rootCmd.PersistentFlags().Lookup("installations-file"))
|
||||||
|
|
||||||
_ = viper.BindPFlag("api", rootCmd.PersistentFlags().Lookup("api"))
|
_ = viper.BindPFlag("api-base", rootCmd.PersistentFlags().Lookup("api-base"))
|
||||||
|
_ = viper.BindPFlag("graphql-api", rootCmd.PersistentFlags().Lookup("graphql-api"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ query ResolveModDependencies ($filter: [ModVersionConstraint!]!) {
|
||||||
versions {
|
versions {
|
||||||
id
|
id
|
||||||
version
|
version
|
||||||
|
link
|
||||||
|
hash
|
||||||
dependencies {
|
dependencies {
|
||||||
condition
|
condition
|
||||||
mod_id
|
mod_id
|
||||||
|
|
|
@ -5,10 +5,7 @@ query SMLVersions {
|
||||||
sml_versions {
|
sml_versions {
|
||||||
id
|
id
|
||||||
version
|
version
|
||||||
link
|
|
||||||
satisfactory_version
|
satisfactory_version
|
||||||
stability
|
|
||||||
updated_at
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -8,5 +8,5 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitAPI() graphql.Client {
|
func InitAPI() graphql.Client {
|
||||||
return graphql.NewClient(viper.GetString("api"), http.DefaultClient)
|
return graphql.NewClient(viper.GetString("api-base")+viper.GetString("graphql-api"), http.DefaultClient)
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -19,6 +19,8 @@ require (
|
||||||
github.com/spf13/viper v1.11.0
|
github.com/spf13/viper v1.11.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/Masterminds/semver/v3 v3.1.1 => github.com/Vilsol/semver/v3 v3.1.2-0.20220414201711-64ef71d40f9a
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/agnivade/levenshtein v1.1.0 // indirect
|
github.com/agnivade/levenshtein v1.1.0 // indirect
|
||||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -52,11 +52,11 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/
|
||||||
github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY=
|
github.com/MarvinJWendt/testza v0.3.5/go.mod h1:ExbTpWmA1z2E9HSskvrNcwApoX4F9bID692s10nuHRY=
|
||||||
github.com/MarvinJWendt/testza v0.4.1 h1:bqidLqFVtySvyq7D+xIfFKefl+AfJtDpivXC9fx3hm4=
|
github.com/MarvinJWendt/testza v0.4.1 h1:bqidLqFVtySvyq7D+xIfFKefl+AfJtDpivXC9fx3hm4=
|
||||||
github.com/MarvinJWendt/testza v0.4.1/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
github.com/MarvinJWendt/testza v0.4.1/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE=
|
||||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
|
||||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
|
||||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
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/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||||
|
github.com/Vilsol/semver/v3 v3.1.2-0.20220414201711-64ef71d40f9a h1:Z443bc6RS9J5qRi7KGqWpStbNYxhDWtSqK/mPQNsIO4=
|
||||||
|
github.com/Vilsol/semver/v3 v3.1.2-0.20220414201711-64ef71d40f9a/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||||
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM=
|
||||||
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
|
github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM=
|
||||||
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
|
||||||
|
|
|
@ -80,10 +80,6 @@ func (m *rootModel) GetGlobal() *cli.GlobalContext {
|
||||||
return m.global
|
return m.global
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *rootModel) ResolveModDependencies(constraints map[string]string) (map[string]cli.ModVersion, error) {
|
|
||||||
return m.dependencyResolver.ResolveModDependencies(constraints)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunTea(global *cli.GlobalContext) error {
|
func RunTea(global *cli.GlobalContext) error {
|
||||||
if err := tea.NewProgram(scenes.NewMainMenu(newModel(global)), tea.WithAltScreen(), tea.WithMouseCellMotion()).Start(); err != nil {
|
if err := tea.NewProgram(scenes.NewMainMenu(newModel(global)), tea.WithAltScreen(), tea.WithMouseCellMotion()).Start(); err != nil {
|
||||||
return errors.Wrap(err, "internal tea error")
|
return errors.Wrap(err, "internal tea error")
|
||||||
|
|
|
@ -53,7 +53,7 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // TODO
|
panic(err) // TODO Handle Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(versions.Mod.Versions) == 0 {
|
if len(versions.Mod.Versions) == 0 {
|
||||||
|
@ -71,7 +71,7 @@ func NewModVersionList(root components.RootModel, parent tea.Model, mod utils.Mo
|
||||||
version := allVersions[currentOffset+currentI]
|
version := allVersions[currentOffset+currentI]
|
||||||
err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version)
|
err := root.GetCurrentProfile().AddMod(mod.Reference, version.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err) // TODO
|
panic(err) // TODO Handle Error
|
||||||
}
|
}
|
||||||
return currentModel.parent, nil
|
return currentModel.parent, nil
|
||||||
},
|
},
|
||||||
|
|
144
utils/io.go
Normal file
144
utils/io.go
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DownloadOrCache(cacheKey string, hash string, url string) (r io.ReaderAt, size int64, err error) {
|
||||||
|
downloadCache := path.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||||
|
if err := os.MkdirAll(downloadCache, 0777); err != nil {
|
||||||
|
if !os.IsExist(err) {
|
||||||
|
return nil, 0, errors.Wrap(err, "failed creating download cache")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
location := path.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
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 f, resp.ContentLength, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SHA256Data(f io.Reader) (string, error) {
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return "", errors.Wrap(err, "failed to compute hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractMod(f io.ReaderAt, size int64, location string) error {
|
||||||
|
if err := os.MkdirAll(location, 0777); err != nil {
|
||||||
|
if !os.IsExist(err) {
|
||||||
|
return errors.Wrap(err, "failed to create mod directory: "+location)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := os.RemoveAll(location); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to remove directory: "+location)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(location, 0777); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create mod directory: "+location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := zip.NewReader(f, size)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to read file as zip")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range reader.File {
|
||||||
|
if !file.FileInfo().IsDir() {
|
||||||
|
outFileLocation := path.Join(location, file.Name)
|
||||||
|
|
||||||
|
if err := os.MkdirAll(path.Dir(outFileLocation), 0777); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to create mod directory: "+location)
|
||||||
|
}
|
||||||
|
|
||||||
|
outFile, err := os.OpenFile(outFileLocation, os.O_CREATE|os.O_RDWR, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to write to file: "+location)
|
||||||
|
}
|
||||||
|
|
||||||
|
inFile, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to process mod zip")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(outFile, inFile); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to write to file: "+location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
9
utils/structures.go
Normal file
9
utils/structures.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
func CopyMap[T comparable, M any](m map[T]M) map[T]M {
|
||||||
|
m2 := make(map[T]M, len(m))
|
||||||
|
for k, v := range m {
|
||||||
|
m2[k] = v
|
||||||
|
}
|
||||||
|
return m2
|
||||||
|
}
|
Loading…
Reference in a new issue