feat: parallel downloads (#43)
* feat: parallel downloads * feat: mod extract progress using file size * feat: pass mod version in install progress updates * fix: only close update channels after finished sending * chore: verbose ci tests * fix: store mod in cache chore: add progress logging to tests * chore: lint * test: add concurrent download limit * fix: prevent concurrent map access * chore: bump pubgrub fix: fix race conditions --------- Co-authored-by: Vilsol <me@vil.so>
This commit is contained in:
parent
a192a63c82
commit
6088d1e8eb
12 changed files with 401 additions and 222 deletions
2
.github/workflows/push.yaml
vendored
2
.github/workflows/push.yaml
vendored
|
@ -88,6 +88,6 @@ jobs:
|
||||||
run: go generate -tags tools -x ./...
|
run: go generate -tags tools -x ./...
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test ./...
|
run: go test -v ./...
|
||||||
env:
|
env:
|
||||||
SF_DEDICATED_SERVER: ${{ github.workspace }}/SatisfactoryDedicatedServer
|
SF_DEDICATED_SERVER: ${{ github.workspace }}/SatisfactoryDedicatedServer
|
||||||
|
|
|
@ -17,4 +17,5 @@ func SetDefaults() {
|
||||||
viper.SetDefault("dry-run", false)
|
viper.SetDefault("dry-run", false)
|
||||||
viper.SetDefault("api-base", "https://api.ficsit.app")
|
viper.SetDefault("api-base", "https://api.ficsit.app")
|
||||||
viper.SetDefault("graphql-api", "/v2/query")
|
viper.SetDefault("graphql-api", "/v2/query")
|
||||||
|
viper.SetDefault("concurrent-downloads", 5)
|
||||||
}
|
}
|
||||||
|
|
51
cli/cache/download.go
vendored
51
cli/cache/download.go
vendored
|
@ -13,34 +13,11 @@ import (
|
||||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Progresser struct {
|
func DownloadOrCache(cacheKey string, hash string, url string, updates chan<- utils.GenericProgress, downloadSemaphore chan int) (io.ReaderAt, int64, error) {
|
||||||
io.Reader
|
if updates != nil {
|
||||||
updates chan utils.GenericUpdate
|
defer close(updates)
|
||||||
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")
|
downloadCache := filepath.Join(viper.GetString("cache-dir"), "downloadCache")
|
||||||
if err := os.MkdirAll(downloadCache, 0o777); err != nil {
|
if err := os.MkdirAll(downloadCache, 0o777); err != nil {
|
||||||
if !os.IsExist(err) {
|
if !os.IsExist(err) {
|
||||||
|
@ -82,6 +59,20 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan util
|
||||||
return nil, 0, errors.Wrap(err, "failed to stat file: "+location)
|
return nil, 0, errors.Wrap(err, "failed to stat file: "+location)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if updates != nil {
|
||||||
|
headResp, err := http.Head(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "failed to head: "+url)
|
||||||
|
}
|
||||||
|
defer headResp.Body.Close()
|
||||||
|
updates <- utils.GenericProgress{Total: headResp.ContentLength}
|
||||||
|
}
|
||||||
|
|
||||||
|
if downloadSemaphore != nil {
|
||||||
|
downloadSemaphore <- 1
|
||||||
|
defer func() { <-downloadSemaphore }()
|
||||||
|
}
|
||||||
|
|
||||||
out, err := os.Create(location)
|
out, err := os.Create(location)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, errors.Wrap(err, "failed creating file at: "+location)
|
return nil, 0, errors.Wrap(err, "failed creating file at: "+location)
|
||||||
|
@ -98,10 +89,10 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan util
|
||||||
return nil, 0, fmt.Errorf("bad status: %s on url: %s", resp.Status, url)
|
return nil, 0, fmt.Errorf("bad status: %s on url: %s", resp.Status, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
progresser := &Progresser{
|
progresser := &utils.Progresser{
|
||||||
Reader: resp.Body,
|
Reader: resp.Body,
|
||||||
total: resp.ContentLength,
|
Total: resp.ContentLength,
|
||||||
updates: updates,
|
Updates: updates,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = io.Copy(out, progresser)
|
_, err = io.Copy(out, progresser)
|
||||||
|
@ -116,7 +107,7 @@ func DownloadOrCache(cacheKey string, hash string, url string, updates chan util
|
||||||
|
|
||||||
if updates != nil {
|
if updates != nil {
|
||||||
select {
|
select {
|
||||||
case updates <- utils.GenericUpdate{Progress: 1}:
|
case updates <- utils.GenericProgress{Completed: resp.ContentLength, Total: resp.ContentLength}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"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/puzpuzpuz/xsync/v3"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
|
"github.com/satisfactorymodding/ficsit-cli/cli/provider"
|
||||||
|
@ -35,7 +36,7 @@ type ficsitAPISource struct {
|
||||||
provider provider.Provider
|
provider provider.Provider
|
||||||
lockfile *LockFile
|
lockfile *LockFile
|
||||||
toInstall map[string]semver.Constraint
|
toInstall map[string]semver.Constraint
|
||||||
modVersionInfo map[string]ficsit.ModVersionsWithDependenciesResponse
|
modVersionInfo *xsync.MapOf[string, ficsit.ModVersionsWithDependenciesResponse]
|
||||||
gameVersion semver.Version
|
gameVersion semver.Version
|
||||||
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
|
smlVersions []ficsit.SMLVersionsSmlVersionsGetSMLVersionsSml_versionsSMLVersion
|
||||||
}
|
}
|
||||||
|
@ -74,7 +75,7 @@ func (f *ficsitAPISource) GetPackageVersions(pkg string) ([]pubgrub.PackageVersi
|
||||||
if response.Mod.Id == "" {
|
if response.Mod.Id == "" {
|
||||||
return nil, errors.Errorf("mod %s not found", pkg)
|
return nil, errors.Errorf("mod %s not found", pkg)
|
||||||
}
|
}
|
||||||
f.modVersionInfo[pkg] = *response
|
f.modVersionInfo.Store(pkg, *response)
|
||||||
versions := make([]pubgrub.PackageVersion, len(response.Mod.Versions))
|
versions := make([]pubgrub.PackageVersion, len(response.Mod.Versions))
|
||||||
for i, modVersion := range response.Mod.Versions {
|
for i, modVersion := range response.Mod.Versions {
|
||||||
v, err := semver.NewVersion(modVersion.Version)
|
v, err := semver.NewVersion(modVersion.Version)
|
||||||
|
@ -145,7 +146,7 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
|
||||||
gameVersion: gameVersionSemver,
|
gameVersion: gameVersionSemver,
|
||||||
lockfile: lockFile,
|
lockfile: lockFile,
|
||||||
toInstall: toInstall,
|
toInstall: toInstall,
|
||||||
modVersionInfo: make(map[string]ficsit.ModVersionsWithDependenciesResponse),
|
modVersionInfo: xsync.NewMapOf[string, ficsit.ModVersionsWithDependenciesResponse](),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := pubgrub.Solve(helpers.NewCachingSource(ficsitSource), rootPkg)
|
result, err := pubgrub.Solve(helpers.NewCachingSource(ficsitSource), rootPkg)
|
||||||
|
@ -170,7 +171,8 @@ func (d DependencyResolver) ResolveModDependencies(constraints map[string]string
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
versions := ficsitSource.modVersionInfo[k].Mod.Versions
|
value, _ := ficsitSource.modVersionInfo.Load(k)
|
||||||
|
versions := value.Mod.Versions
|
||||||
for _, ver := range versions {
|
for _, ver := range versions {
|
||||||
if ver.Version == v.RawString() {
|
if ver.Version == v.RawString() {
|
||||||
outputLock[k] = LockedMod{
|
outputLock[k] = LockedMod{
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
|
||||||
"github.com/satisfactorymodding/ficsit-cli/cli/cache"
|
"github.com/satisfactorymodding/ficsit-cli/cli/cache"
|
||||||
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
|
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
|
||||||
|
@ -351,14 +352,27 @@ func (i *Installation) ResolveProfile(ctx *GlobalContext) (LockFile, error) {
|
||||||
return lockfile, nil
|
return lockfile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstallUpdateType string
|
||||||
|
|
||||||
|
var (
|
||||||
|
InstallUpdateTypeOverall InstallUpdateType = "overall"
|
||||||
|
InstallUpdateTypeModDownload InstallUpdateType = "download"
|
||||||
|
InstallUpdateTypeModExtract InstallUpdateType = "extract"
|
||||||
|
InstallUpdateTypeModComplete InstallUpdateType = "complete"
|
||||||
|
)
|
||||||
|
|
||||||
type InstallUpdate struct {
|
type InstallUpdate struct {
|
||||||
ModName string
|
Type InstallUpdateType
|
||||||
OverallProgress float64
|
Item InstallUpdateItem
|
||||||
DownloadProgress float64
|
Progress utils.GenericProgress
|
||||||
ExtractProgress float64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) error {
|
type InstallUpdateItem struct {
|
||||||
|
Mod string
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Installation) Install(ctx *GlobalContext, updates chan<- InstallUpdate) error {
|
||||||
if err := i.Validate(ctx); err != nil {
|
if err := i.Validate(ctx); err != nil {
|
||||||
return errors.Wrap(err, "failed to validate installation")
|
return errors.Wrap(err, "failed to validate installation")
|
||||||
}
|
}
|
||||||
|
@ -403,78 +417,65 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloading := true
|
log.Info().Int("concurrency", viper.GetInt("concurrent-downloads")).Str("path", i.Path).Msg("starting installation")
|
||||||
completed := 0
|
|
||||||
|
|
||||||
var genericUpdates chan utils.GenericUpdate
|
errg := errgroup.Group{}
|
||||||
|
channelUsers := sync.WaitGroup{}
|
||||||
|
downloadSemaphore := make(chan int, viper.GetInt("concurrent-downloads"))
|
||||||
|
defer close(downloadSemaphore)
|
||||||
|
|
||||||
|
var modComplete chan int
|
||||||
if updates != nil {
|
if updates != nil {
|
||||||
var wg sync.WaitGroup
|
channelUsers.Add(1)
|
||||||
wg.Add(1)
|
modComplete = make(chan int)
|
||||||
defer wg.Wait()
|
defer close(modComplete)
|
||||||
|
|
||||||
genericUpdates = make(chan utils.GenericUpdate)
|
|
||||||
defer close(genericUpdates)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer channelUsers.Done()
|
||||||
|
completed := 0
|
||||||
update := InstallUpdate{
|
for range modComplete {
|
||||||
OverallProgress: float64(completed) / float64(len(lockfile)),
|
completed++
|
||||||
DownloadProgress: 0,
|
overallUpdate := InstallUpdate{
|
||||||
ExtractProgress: 0,
|
Type: InstallUpdateTypeOverall,
|
||||||
}
|
Progress: utils.GenericProgress{
|
||||||
|
Completed: int64(completed),
|
||||||
select {
|
Total: int64(len(lockfile)),
|
||||||
case updates <- update:
|
},
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
for up := range genericUpdates {
|
|
||||||
if downloading {
|
|
||||||
update.DownloadProgress = up.Progress
|
|
||||||
} else {
|
|
||||||
update.DownloadProgress = 1
|
|
||||||
update.ExtractProgress = up.Progress
|
|
||||||
}
|
|
||||||
|
|
||||||
if up.ModReference != nil {
|
|
||||||
update.ModName = *up.ModReference
|
|
||||||
}
|
|
||||||
|
|
||||||
update.OverallProgress = float64(completed) / float64(len(lockfile))
|
|
||||||
|
|
||||||
select {
|
|
||||||
case updates <- update:
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
updates <- overallUpdate
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
for modReference, version := range lockfile {
|
for modReference, version := range lockfile {
|
||||||
|
channelUsers.Add(1)
|
||||||
|
modReference := modReference
|
||||||
|
version := version
|
||||||
|
errg.Go(func() error {
|
||||||
|
defer channelUsers.Done()
|
||||||
// Only install if a link is provided, otherwise assume mod is already installed
|
// Only install if a link is provided, otherwise assume mod is already installed
|
||||||
if version.Link != "" {
|
if version.Link != "" {
|
||||||
downloading = true
|
err := downloadAndExtractMod(modReference, version.Version, version.Link, version.Hash, modsDirectory, updates, downloadSemaphore, d)
|
||||||
|
|
||||||
if genericUpdates != nil {
|
|
||||||
genericUpdates <- utils.GenericUpdate{ModReference: &modReference}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Str("mod_reference", modReference).Str("version", version.Version).Str("link", version.Link).Msg("downloading mod")
|
|
||||||
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.Wrapf(err, "failed to install %s@%s", modReference, version.Version)
|
||||||
}
|
|
||||||
|
|
||||||
downloading = false
|
|
||||||
|
|
||||||
log.Info().Str("mod_reference", modReference).Str("version", version.Version).Str("link", version.Link).Msg("extracting mod")
|
|
||||||
if err := utils.ExtractMod(reader, size, filepath.Join(modsDirectory, modReference), version.Hash, genericUpdates, d); err != nil {
|
|
||||||
return errors.Wrap(err, "could not extract "+modReference)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
completed++
|
if modComplete != nil {
|
||||||
|
modComplete <- 1
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if updates != nil {
|
||||||
|
go func() {
|
||||||
|
channelUsers.Wait()
|
||||||
|
close(updates)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := errg.Wait(); err != nil {
|
||||||
|
return errors.Wrap(err, "failed to install mods")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -518,6 +519,84 @@ func (i *Installation) UpdateMods(ctx *GlobalContext, mods []string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadAndExtractMod(modReference string, version string, link string, hash string, modsDirectory string, updates chan<- InstallUpdate, downloadSemaphore chan int, d disk.Disk) error {
|
||||||
|
var downloadUpdates chan utils.GenericProgress
|
||||||
|
|
||||||
|
if updates != nil {
|
||||||
|
// Forward the inner updates as InstallUpdates
|
||||||
|
downloadUpdates = make(chan utils.GenericProgress)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for up := range downloadUpdates {
|
||||||
|
updates <- InstallUpdate{
|
||||||
|
Item: InstallUpdateItem{
|
||||||
|
Mod: modReference,
|
||||||
|
Version: version,
|
||||||
|
},
|
||||||
|
Type: InstallUpdateTypeModDownload,
|
||||||
|
Progress: up,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("mod_reference", modReference).Str("version", version).Str("link", link).Msg("downloading mod")
|
||||||
|
reader, size, err := cache.DownloadOrCache(modReference+"_"+version+".zip", hash, link, downloadUpdates, downloadSemaphore)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to download "+modReference+" from: "+link)
|
||||||
|
}
|
||||||
|
|
||||||
|
var extractUpdates chan utils.GenericProgress
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
if updates != nil {
|
||||||
|
// Forward the inner updates as InstallUpdates
|
||||||
|
extractUpdates = make(chan utils.GenericProgress)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for up := range extractUpdates {
|
||||||
|
select {
|
||||||
|
case updates <- InstallUpdate{
|
||||||
|
Item: InstallUpdateItem{
|
||||||
|
Mod: modReference,
|
||||||
|
Version: version,
|
||||||
|
},
|
||||||
|
Type: InstallUpdateTypeModExtract,
|
||||||
|
Progress: up,
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Str("mod_reference", modReference).Str("version", version).Str("link", link).Msg("extracting mod")
|
||||||
|
if err := utils.ExtractMod(reader, size, filepath.Join(modsDirectory, modReference), hash, extractUpdates, d); err != nil {
|
||||||
|
return errors.Wrap(err, "could not extract "+modReference)
|
||||||
|
}
|
||||||
|
|
||||||
|
if updates != nil {
|
||||||
|
select {
|
||||||
|
case updates <- InstallUpdate{
|
||||||
|
Type: InstallUpdateTypeModComplete,
|
||||||
|
Item: InstallUpdateItem{
|
||||||
|
Mod: modReference,
|
||||||
|
Version: version,
|
||||||
|
},
|
||||||
|
}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
close(extractUpdates)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Installation) SetProfile(ctx *GlobalContext, profile string) error {
|
func (i *Installation) SetProfile(ctx *GlobalContext, profile string) error {
|
||||||
found := false
|
found := false
|
||||||
for _, p := range ctx.Profiles.Profiles {
|
for _, p := range ctx.Profiles.Profiles {
|
||||||
|
|
|
@ -35,11 +35,11 @@ func TestAddInstallation(t *testing.T) {
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
testza.AssertNotNil(t, installation)
|
testza.AssertNotNil(t, installation)
|
||||||
|
|
||||||
err = installation.Install(ctx, nil)
|
err = installation.Install(ctx, installWatcher())
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
|
|
||||||
installation.Vanilla = true
|
installation.Vanilla = true
|
||||||
err = installation.Install(ctx, nil)
|
err = installation.Install(ctx, installWatcher())
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,15 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/MarvinJWendt/testza"
|
"github.com/MarvinJWendt/testza"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/satisfactorymodding/ficsit-cli/cfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cfg.SetDefaults()
|
||||||
|
}
|
||||||
|
|
||||||
func profilesGetResolver() DependencyResolver {
|
func profilesGetResolver() DependencyResolver {
|
||||||
ctx, err := InitCLI(false)
|
ctx, err := InitCLI(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -17,6 +24,22 @@ func profilesGetResolver() DependencyResolver {
|
||||||
return NewDependencyResolver(ctx.Provider)
|
return NewDependencyResolver(ctx.Provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func installWatcher() chan<- InstallUpdate {
|
||||||
|
c := make(chan InstallUpdate)
|
||||||
|
go func() {
|
||||||
|
for i := range c {
|
||||||
|
if i.Progress.Total == i.Progress.Completed {
|
||||||
|
if i.Type != InstallUpdateTypeOverall {
|
||||||
|
log.Info().Str("mod_reference", i.Item.Mod).Str("version", i.Item.Version).Str("type", string(i.Type)).Msg("progress completed")
|
||||||
|
} else {
|
||||||
|
log.Info().Msg("overall completed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
func TestProfileResolution(t *testing.T) {
|
func TestProfileResolution(t *testing.T) {
|
||||||
resolver := profilesGetResolver()
|
resolver := profilesGetResolver()
|
||||||
|
|
||||||
|
@ -108,7 +131,7 @@ func TestUpdateMods(t *testing.T) {
|
||||||
err = installation.WriteLockFile(ctx, oldLockfile)
|
err = installation.WriteLockFile(ctx, oldLockfile)
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
|
|
||||||
err = installation.Install(ctx, nil)
|
err = installation.Install(ctx, installWatcher())
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
|
|
||||||
lockFile, err := installation.LockFile(ctx)
|
lockFile, err := installation.LockFile(ctx)
|
||||||
|
@ -126,7 +149,7 @@ func TestUpdateMods(t *testing.T) {
|
||||||
testza.AssertEqual(t, 2, len(*lockFile))
|
testza.AssertEqual(t, 2, len(*lockFile))
|
||||||
testza.AssertEqual(t, "1.6.6", (*lockFile)["AreaActions"].Version)
|
testza.AssertEqual(t, "1.6.6", (*lockFile)["AreaActions"].Version)
|
||||||
|
|
||||||
err = installation.Install(ctx, nil)
|
err = installation.Install(ctx, installWatcher())
|
||||||
testza.AssertNoError(t, err)
|
testza.AssertNoError(t, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,7 @@ func init() {
|
||||||
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")
|
RootCmd.PersistentFlags().Bool("offline", false, "Whether to only use local data")
|
||||||
|
RootCmd.PersistentFlags().Int("concurrent-downloads", 5, "Maximum number of concurrent downloads")
|
||||||
|
|
||||||
_ = 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"))
|
||||||
|
@ -157,4 +158,5 @@ func init() {
|
||||||
_ = 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"))
|
_ = viper.BindPFlag("offline", RootCmd.PersistentFlags().Lookup("offline"))
|
||||||
|
_ = viper.BindPFlag("concurrent-downloads", RootCmd.PersistentFlags().Lookup("concurrent-downloads"))
|
||||||
}
|
}
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -13,14 +13,16 @@ require (
|
||||||
github.com/charmbracelet/glamour v0.5.0
|
github.com/charmbracelet/glamour v0.5.0
|
||||||
github.com/charmbracelet/lipgloss v0.6.0
|
github.com/charmbracelet/lipgloss v0.6.0
|
||||||
github.com/jlaffaye/ftp v0.1.0
|
github.com/jlaffaye/ftp v0.1.0
|
||||||
github.com/mircearoata/pubgrub-go v0.3.2
|
github.com/mircearoata/pubgrub-go v0.3.3
|
||||||
github.com/muesli/reflow v0.3.0
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pterm/pterm v0.12.67
|
github.com/pterm/pterm v0.12.67
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.0.2
|
||||||
github.com/rs/zerolog v1.28.0
|
github.com/rs/zerolog v1.28.0
|
||||||
github.com/sahilm/fuzzy v0.1.0
|
github.com/sahilm/fuzzy v0.1.0
|
||||||
github.com/spf13/cobra v1.6.0
|
github.com/spf13/cobra v1.6.0
|
||||||
github.com/spf13/viper v1.13.0
|
github.com/spf13/viper v1.13.0
|
||||||
|
golang.org/x/sync v0.1.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
7
go.sum
7
go.sum
|
@ -269,8 +269,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
|
||||||
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
|
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM=
|
||||||
github.com/mircearoata/pubgrub-go v0.3.2 h1:AeC2bvHebii6YrfyN+AST06WiQUENKQkREA7Xoi4/HY=
|
github.com/mircearoata/pubgrub-go v0.3.3 h1:XGwL8Xh5GX+mbnvWItbM/lVJxAq3NZtfUtbJ/hUf2ig=
|
||||||
github.com/mircearoata/pubgrub-go v0.3.2/go.mod h1:9oWL9ZXdjFYvnGl95qiM1dTciFNx1MN8fUnG3SUwDi8=
|
github.com/mircearoata/pubgrub-go v0.3.3/go.mod h1:9oWL9ZXdjFYvnGl95qiM1dTciFNx1MN8fUnG3SUwDi8=
|
||||||
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
|
@ -310,6 +310,8 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b
|
||||||
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s=
|
||||||
github.com/pterm/pterm v0.12.67 h1:5iB7ajIQROYfxYD7+sFJ4+KJhFJ+xn7QOVBm4s6RUF0=
|
github.com/pterm/pterm v0.12.67 h1:5iB7ajIQROYfxYD7+sFJ4+KJhFJ+xn7QOVBm4s6RUF0=
|
||||||
github.com/pterm/pterm v0.12.67/go.mod h1:nFuT9ZVkkCi8o4L1dtWuYPwDQxggLh4C263qG5nTLpQ=
|
github.com/pterm/pterm v0.12.67/go.mod h1:nFuT9ZVkkCi8o4L1dtWuYPwDQxggLh4C263qG5nTLpQ=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.0.2 h1:3yESHrRFYr6xzkz61LLkvNiPFXxJEAABanTQpKbAaew=
|
||||||
|
github.com/puzpuzpuz/xsync/v3 v3.0.2/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
@ -493,6 +495,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package scenes
|
package scenes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/charmbracelet/bubbles/progress"
|
"github.com/charmbracelet/bubbles/progress"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
@ -9,19 +11,23 @@ import (
|
||||||
"github.com/satisfactorymodding/ficsit-cli/cli"
|
"github.com/satisfactorymodding/ficsit-cli/cli"
|
||||||
"github.com/satisfactorymodding/ficsit-cli/tea/components"
|
"github.com/satisfactorymodding/ficsit-cli/tea/components"
|
||||||
"github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys"
|
"github.com/satisfactorymodding/ficsit-cli/tea/scenes/keys"
|
||||||
"github.com/satisfactorymodding/ficsit-cli/tea/utils"
|
teaUtils "github.com/satisfactorymodding/ficsit-cli/tea/utils"
|
||||||
|
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ tea.Model = (*apply)(nil)
|
var _ tea.Model = (*apply)(nil)
|
||||||
|
|
||||||
type update struct {
|
type modProgress struct {
|
||||||
|
downloadProgress utils.GenericProgress
|
||||||
|
extractProgress utils.GenericProgress
|
||||||
|
downloading bool
|
||||||
|
complete bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type status struct {
|
||||||
|
modProgresses map[string]modProgress
|
||||||
installName string
|
installName string
|
||||||
modName string
|
overallProgress utils.GenericProgress
|
||||||
completed []string
|
|
||||||
installTotal int
|
|
||||||
installCurrent int
|
|
||||||
modTotal int
|
|
||||||
modCurrent int
|
|
||||||
done bool
|
done bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,11 +35,13 @@ type apply struct {
|
||||||
root components.RootModel
|
root components.RootModel
|
||||||
parent tea.Model
|
parent tea.Model
|
||||||
error *components.ErrorComponent
|
error *components.ErrorComponent
|
||||||
updateChannel chan update
|
installChannel chan string
|
||||||
|
updateChannel chan cli.InstallUpdate
|
||||||
|
doneChannel chan bool
|
||||||
errorChannel chan error
|
errorChannel chan error
|
||||||
cancelChannel chan bool
|
cancelChannel chan bool
|
||||||
title string
|
title string
|
||||||
status update
|
status status
|
||||||
overall progress.Model
|
overall progress.Model
|
||||||
sub progress.Model
|
sub progress.Model
|
||||||
cancelled bool
|
cancelled bool
|
||||||
|
@ -43,86 +51,46 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
|
||||||
overall := progress.New(progress.WithSolidFill("118"))
|
overall := progress.New(progress.WithSolidFill("118"))
|
||||||
sub := progress.New(progress.WithSolidFill("202"))
|
sub := progress.New(progress.WithSolidFill("202"))
|
||||||
|
|
||||||
updateChannel := make(chan update)
|
installChannel := make(chan string)
|
||||||
|
updateChannel := make(chan cli.InstallUpdate)
|
||||||
|
doneChannel := make(chan bool, 1)
|
||||||
errorChannel := make(chan error)
|
errorChannel := make(chan error)
|
||||||
cancelChannel := make(chan bool, 1)
|
cancelChannel := make(chan bool, 1)
|
||||||
|
|
||||||
model := &apply{
|
model := &apply{
|
||||||
root: root,
|
root: root,
|
||||||
parent: parent,
|
parent: parent,
|
||||||
title: utils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"),
|
title: teaUtils.NonListTitleStyle.MarginTop(1).MarginBottom(1).Render("Applying Changes"),
|
||||||
overall: overall,
|
overall: overall,
|
||||||
sub: sub,
|
sub: sub,
|
||||||
status: update{
|
status: status{
|
||||||
completed: []string{},
|
|
||||||
|
|
||||||
installName: "",
|
installName: "",
|
||||||
installTotal: 100,
|
|
||||||
installCurrent: 0,
|
|
||||||
|
|
||||||
modName: "",
|
|
||||||
modTotal: 100,
|
|
||||||
modCurrent: 0,
|
|
||||||
|
|
||||||
done: false,
|
done: false,
|
||||||
},
|
},
|
||||||
|
installChannel: installChannel,
|
||||||
updateChannel: updateChannel,
|
updateChannel: updateChannel,
|
||||||
|
doneChannel: doneChannel,
|
||||||
errorChannel: errorChannel,
|
errorChannel: errorChannel,
|
||||||
cancelChannel: cancelChannel,
|
cancelChannel: cancelChannel,
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
result := &update{
|
|
||||||
completed: make([]string, 0),
|
|
||||||
|
|
||||||
installName: "",
|
|
||||||
installTotal: 100,
|
|
||||||
installCurrent: 0,
|
|
||||||
|
|
||||||
modName: "",
|
|
||||||
modTotal: 100,
|
|
||||||
modCurrent: 0,
|
|
||||||
|
|
||||||
done: false,
|
|
||||||
}
|
|
||||||
updateChannel <- *result
|
|
||||||
|
|
||||||
for _, installation := range root.GetGlobal().Installations.Installations {
|
for _, installation := range root.GetGlobal().Installations.Installations {
|
||||||
result.installName = installation.Path
|
installChannel <- installation.Path
|
||||||
updateChannel <- *result
|
|
||||||
|
|
||||||
installChannel := make(chan cli.InstallUpdate)
|
|
||||||
|
|
||||||
|
installUpdateChannel := make(chan cli.InstallUpdate)
|
||||||
go func() {
|
go func() {
|
||||||
for data := range installChannel {
|
for update := range installUpdateChannel {
|
||||||
result.installName = installation.Path
|
updateChannel <- update
|
||||||
result.installCurrent = int(data.OverallProgress * 100)
|
|
||||||
|
|
||||||
if data.DownloadProgress < 1 {
|
|
||||||
result.modName = "Downloading: " + data.ModName
|
|
||||||
result.modCurrent = int(data.DownloadProgress * 100)
|
|
||||||
} else {
|
|
||||||
result.modName = "Extracting: " + data.ModName
|
|
||||||
result.modCurrent = int(data.ExtractProgress * 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateChannel <- *result
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := installation.Install(root.GetGlobal(), installChannel); err != nil {
|
if err := installation.Install(root.GetGlobal(), installUpdateChannel); err != nil {
|
||||||
errorChannel <- err
|
errorChannel <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
close(installChannel)
|
|
||||||
|
|
||||||
result.modName = ""
|
|
||||||
result.installTotal = 100
|
|
||||||
result.completed = append(result.completed, installation.Path)
|
|
||||||
updateChannel <- *result
|
|
||||||
|
|
||||||
stop := false
|
stop := false
|
||||||
select {
|
select {
|
||||||
case <-cancelChannel:
|
case <-cancelChannel:
|
||||||
|
@ -135,16 +103,14 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result.done = true
|
doneChannel <- true
|
||||||
result.installName = ""
|
|
||||||
updateChannel <- *result
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m apply) Init() tea.Cmd {
|
func (m apply) Init() tea.Cmd {
|
||||||
return utils.Ticker()
|
return teaUtils.Ticker()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
@ -169,10 +135,38 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
m.root.SetSize(msg)
|
m.root.SetSize(msg)
|
||||||
case components.ErrorComponentTimeoutMsg:
|
case components.ErrorComponentTimeoutMsg:
|
||||||
m.error = nil
|
m.error = nil
|
||||||
case utils.TickMsg:
|
case teaUtils.TickMsg:
|
||||||
select {
|
select {
|
||||||
case newStatus := <-m.updateChannel:
|
case <-m.doneChannel:
|
||||||
m.status = newStatus
|
m.status.done = true
|
||||||
|
m.status.installName = ""
|
||||||
|
break
|
||||||
|
case installName := <-m.installChannel:
|
||||||
|
m.status.installName = installName
|
||||||
|
m.status.modProgresses = make(map[string]modProgress)
|
||||||
|
m.status.overallProgress = utils.GenericProgress{}
|
||||||
|
break
|
||||||
|
case update := <-m.updateChannel:
|
||||||
|
switch update.Type {
|
||||||
|
case cli.InstallUpdateTypeOverall:
|
||||||
|
m.status.overallProgress = update.Progress
|
||||||
|
case cli.InstallUpdateTypeModDownload:
|
||||||
|
m.status.modProgresses[update.Item.Mod] = modProgress{
|
||||||
|
downloadProgress: update.Progress,
|
||||||
|
downloading: true,
|
||||||
|
complete: false,
|
||||||
|
}
|
||||||
|
case cli.InstallUpdateTypeModExtract:
|
||||||
|
m.status.modProgresses[update.Item.Mod] = modProgress{
|
||||||
|
extractProgress: update.Progress,
|
||||||
|
downloading: false,
|
||||||
|
complete: false,
|
||||||
|
}
|
||||||
|
case cli.InstallUpdateTypeModComplete:
|
||||||
|
m.status.modProgresses[update.Item.Mod] = modProgress{
|
||||||
|
complete: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case err := <-m.errorChannel:
|
case err := <-m.errorChannel:
|
||||||
wrappedErrMessage := wrap.String(err.Error(), int(float64(m.root.Size().Width)*0.8))
|
wrappedErrMessage := wrap.String(err.Error(), int(float64(m.root.Size().Width)*0.8))
|
||||||
|
@ -183,7 +177,7 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
// Skip if nothing there
|
// Skip if nothing there
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return m, utils.Ticker()
|
return m, teaUtils.Ticker()
|
||||||
}
|
}
|
||||||
|
|
||||||
return m, nil
|
return m, nil
|
||||||
|
@ -191,30 +185,38 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
|
||||||
func (m apply) View() string {
|
func (m apply) View() string {
|
||||||
strs := make([]string, 0)
|
strs := make([]string, 0)
|
||||||
for _, s := range m.status.completed {
|
|
||||||
strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+s)
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.status.installName != "" {
|
if m.status.installName != "" {
|
||||||
marginTop := 0
|
strs = append(strs, lipgloss.NewStyle().Render(m.status.installName))
|
||||||
if len(m.status.completed) > 0 {
|
strs = append(strs, lipgloss.NewStyle().MarginBottom(1).Render(m.overall.ViewAs(m.status.overallProgress.Percentage())))
|
||||||
marginTop = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
strs = append(strs, lipgloss.NewStyle().MarginTop(marginTop).Render(m.status.installName))
|
keys := make([]string, 0)
|
||||||
strs = append(strs, m.overall.ViewAs(float64(m.status.installCurrent)/float64(m.status.installTotal)))
|
for k := range m.status.modProgresses {
|
||||||
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
if m.status.modName != "" {
|
for _, modReference := range keys {
|
||||||
strs = append(strs, lipgloss.NewStyle().MarginTop(1).Render(m.status.modName))
|
p := m.status.modProgresses[modReference]
|
||||||
strs = append(strs, m.sub.ViewAs(float64(m.status.modCurrent)/float64(m.status.modTotal)))
|
if p.complete {
|
||||||
|
strs = append(strs, lipgloss.NewStyle().Foreground(lipgloss.Color("22")).Render("✓ ")+modReference)
|
||||||
|
} else {
|
||||||
|
if p.downloading {
|
||||||
|
strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Downloading)"))
|
||||||
|
strs = append(strs, m.sub.ViewAs(p.downloadProgress.Percentage()))
|
||||||
|
} else {
|
||||||
|
strs = append(strs, lipgloss.NewStyle().Render(modReference+" (Extracting)"))
|
||||||
|
strs = append(strs, m.sub.ViewAs(p.extractProgress.Percentage()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.status.done {
|
if m.status.done {
|
||||||
if m.cancelled {
|
if m.cancelled {
|
||||||
strs = append(strs, utils.LabelStyle.Copy().Foreground(lipgloss.Color("196")).Padding(0).Margin(1).Render("Cancelled! Press Enter to return"))
|
strs = append(strs, teaUtils.LabelStyle.Copy().Foreground(lipgloss.Color("196")).Padding(0).Margin(1).Render("Cancelled! Press Enter to return"))
|
||||||
} else {
|
} else {
|
||||||
strs = append(strs, utils.LabelStyle.Copy().Padding(0).Margin(1).Render("Done! Press Enter to return"))
|
strs = append(strs, teaUtils.LabelStyle.Copy().Padding(0).Margin(1).Render("Done! Press Enter to return"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
102
utils/io.go
102
utils/io.go
|
@ -7,15 +7,51 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
|
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GenericUpdate struct {
|
type GenericProgress struct {
|
||||||
ModReference *string
|
Completed int64
|
||||||
Progress float64
|
Total int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gp GenericProgress) Percentage() float64 {
|
||||||
|
if gp.Total == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(gp.Completed) / float64(gp.Total)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Progresser struct {
|
||||||
|
io.Reader
|
||||||
|
Updates chan<- GenericProgress
|
||||||
|
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 <- GenericProgress{Completed: pt.Running, Total: pt.Total}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
return n, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, errors.Wrap(err, "failed to read")
|
||||||
}
|
}
|
||||||
|
|
||||||
func SHA256Data(f io.Reader) (string, error) {
|
func SHA256Data(f io.Reader) (string, error) {
|
||||||
|
@ -27,7 +63,7 @@ func SHA256Data(f io.Reader) (string, error) {
|
||||||
return hex.EncodeToString(h.Sum(nil)), nil
|
return hex.EncodeToString(h.Sum(nil)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates chan GenericUpdate, d disk.Disk) error {
|
func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates chan<- GenericProgress, d disk.Disk) error {
|
||||||
hashFile := filepath.Join(location, ".smm")
|
hashFile := filepath.Join(location, ".smm")
|
||||||
hashBytes, err := d.Read(hashFile)
|
hashBytes, err := d.Read(hashFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -59,7 +95,24 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
|
||||||
return errors.Wrap(err, "failed to read file as zip")
|
return errors.Wrap(err, "failed to read file as zip")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, file := range reader.File {
|
totalSize := int64(0)
|
||||||
|
|
||||||
|
for _, file := range reader.File {
|
||||||
|
totalSize += int64(file.UncompressedSize64)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalExtracted := int64(0)
|
||||||
|
totalExtractedPtr := &totalExtracted
|
||||||
|
|
||||||
|
channelUsers := sync.WaitGroup{}
|
||||||
|
|
||||||
|
if updates != nil {
|
||||||
|
defer func() {
|
||||||
|
channelUsers.Wait()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range reader.File {
|
||||||
if !file.FileInfo().IsDir() {
|
if !file.FileInfo().IsDir() {
|
||||||
outFileLocation := filepath.Join(location, file.Name)
|
outFileLocation := filepath.Join(location, file.Name)
|
||||||
|
|
||||||
|
@ -67,16 +120,26 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
|
||||||
return errors.Wrap(err, "failed to create mod directory: "+location)
|
return errors.Wrap(err, "failed to create mod directory: "+location)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writeZipFile(outFileLocation, file, d); err != nil {
|
var fileUpdates chan GenericProgress
|
||||||
return err
|
if updates != nil {
|
||||||
|
fileUpdates = make(chan GenericProgress)
|
||||||
|
channelUsers.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer channelUsers.Done()
|
||||||
|
for fileUpdate := range fileUpdates {
|
||||||
|
updates <- GenericProgress{
|
||||||
|
Completed: atomic.LoadInt64(totalExtractedPtr) + fileUpdate.Completed,
|
||||||
|
Total: totalSize,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
if updates != nil {
|
if err := writeZipFile(outFileLocation, file, d, fileUpdates); err != nil {
|
||||||
select {
|
return err
|
||||||
case updates <- GenericUpdate{Progress: float64(i) / float64(len(reader.File)-1)}:
|
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
atomic.AddInt64(totalExtractedPtr, int64(file.UncompressedSize64))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +149,7 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
|
||||||
|
|
||||||
if updates != nil {
|
if updates != nil {
|
||||||
select {
|
select {
|
||||||
case updates <- GenericUpdate{Progress: 1}:
|
case updates <- GenericProgress{Completed: totalSize, Total: totalSize}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +157,11 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeZipFile(outFileLocation string, file *zip.File, d disk.Disk) error {
|
func writeZipFile(outFileLocation string, file *zip.File, d disk.Disk, updates chan<- GenericProgress) error {
|
||||||
|
if updates != nil {
|
||||||
|
defer close(updates)
|
||||||
|
}
|
||||||
|
|
||||||
outFile, err := d.Open(outFileLocation, os.O_CREATE|os.O_RDWR)
|
outFile, err := d.Open(outFileLocation, os.O_CREATE|os.O_RDWR)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to write to file: "+outFileLocation)
|
return errors.Wrap(err, "failed to write to file: "+outFileLocation)
|
||||||
|
@ -106,8 +173,15 @@ func writeZipFile(outFileLocation string, file *zip.File, d disk.Disk) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to process mod zip")
|
return errors.Wrap(err, "failed to process mod zip")
|
||||||
}
|
}
|
||||||
|
defer inFile.Close()
|
||||||
|
|
||||||
if _, err := io.Copy(outFile, inFile); err != nil {
|
progressInReader := &Progresser{
|
||||||
|
Reader: inFile,
|
||||||
|
Total: int64(file.UncompressedSize64),
|
||||||
|
Updates: updates,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(outFile, progressInReader); err != nil {
|
||||||
return errors.Wrap(err, "failed to write to file: "+outFileLocation)
|
return errors.Wrap(err, "failed to write to file: "+outFileLocation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue