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