feat: add support for ftp

This commit is contained in:
Vilsol 2022-06-23 01:24:35 +03:00
parent de24e8dcf6
commit ef7f8cc8e8
11 changed files with 522 additions and 68 deletions

195
cli/disk/ftp.go Normal file
View file

@ -0,0 +1,195 @@
package disk
import (
"bytes"
"io"
"net/url"
"strings"
"sync"
"time"
"github.com/jlaffaye/ftp"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
var _ Disk = (*ftpDisk)(nil)
type ftpDisk struct {
path string
client *ftp.ServerConn
stepLock sync.Mutex
}
type ftpEntry struct {
*ftp.Entry
}
func (f ftpEntry) IsDir() bool {
return f.Entry.Type == ftp.EntryTypeFolder
}
func (f ftpEntry) Name() string {
return f.Entry.Name
}
func newFTP(path string) (Disk, error) {
u, err := url.Parse(path)
if err != nil {
return nil, errors.Wrap(err, "failed to parse ftp url")
}
c, err := ftp.Dial(u.Host, ftp.DialWithTimeout(time.Second*5))
if err != nil {
return nil, errors.Wrap(err, "failed to dial host "+u.Host)
}
password, _ := u.User.Password()
if err := c.Login(u.User.Username(), password); err != nil {
return nil, errors.Wrap(err, "failed to login")
}
log.Debug().Msg("logged into ftp")
return &ftpDisk{
path: u.Path,
client: c,
}, nil
}
func (l *ftpDisk) Exists(path string) error {
l.stepLock.Lock()
defer l.stepLock.Unlock()
log.Debug().Str("path", path).Str("schema", "ftp").Msg("checking if file exists")
_, err := l.client.FileSize(path)
return errors.Wrap(err, "failed to check if file exists")
}
func (l *ftpDisk) Read(path string) ([]byte, error) {
l.stepLock.Lock()
defer l.stepLock.Unlock()
log.Debug().Str("path", path).Str("schema", "ftp").Msg("reading file")
f, err := l.client.Retr(path)
if err != nil {
return nil, errors.Wrap(err, "failed to retrieve path")
}
defer f.Close()
data, err := io.ReadAll(f)
return data, errors.Wrap(err, "failed to read file")
}
func (l *ftpDisk) Write(path string, data []byte) error {
l.stepLock.Lock()
defer l.stepLock.Unlock()
log.Debug().Str("path", path).Str("schema", "ftp").Msg("writing to file")
return errors.Wrap(l.client.Stor(path, bytes.NewReader(data)), "failed to write file")
}
func (l *ftpDisk) Remove(path string) error {
l.stepLock.Lock()
defer l.stepLock.Unlock()
log.Debug().Str("path", path).Str("schema", "ftp").Msg("deleting path")
return errors.Wrap(l.client.Delete(path), "failed to delete path")
}
func (l *ftpDisk) MkDir(path string) error {
l.stepLock.Lock()
defer l.stepLock.Unlock()
log.Debug().Str("schema", "ftp").Msg("going to root directory")
err := l.client.ChangeDir("/")
if err != nil {
return errors.Wrap(err, "failed to change directory")
}
split := strings.Split(path[1:], "/")
for _, s := range split {
dir, err := l.ReadDirLock("", false)
if err != nil {
return err
}
foundDir := false
for _, entry := range dir {
if entry.IsDir() && entry.Name() == s {
foundDir = true
break
}
}
if !foundDir {
log.Debug().Str("dir", s).Str("schema", "ftp").Msg("making directory")
if err := l.client.MakeDir(s); err != nil {
return errors.Wrap(err, "failed to make directory")
}
}
log.Debug().Str("dir", s).Str("schema", "ftp").Msg("entering directory")
if err := l.client.ChangeDir(s); err != nil {
return errors.Wrap(err, "failed to enter directory")
}
}
return nil
}
func (l *ftpDisk) ReadDir(path string) ([]Entry, error) {
return l.ReadDirLock(path, true)
}
func (l *ftpDisk) ReadDirLock(path string, lock bool) ([]Entry, error) {
if lock {
l.stepLock.Lock()
defer l.stepLock.Unlock()
}
log.Debug().Str("path", path).Str("schema", "ftp").Msg("reading directory")
dir, err := l.client.List(path)
if err != nil {
return nil, errors.Wrap(err, "failed to list files in directory")
}
entries := make([]Entry, len(dir))
for i, entry := range dir {
entries[i] = ftpEntry{
Entry: entry,
}
}
return entries, nil
}
func (l *ftpDisk) IsNotExist(err error) bool {
return strings.Contains(err.Error(), "Could not get file") || strings.Contains(err.Error(), "Failed to open file")
}
func (l *ftpDisk) IsExist(err error) bool {
return strings.Contains(err.Error(), "Create directory operation failed")
}
func (l *ftpDisk) Open(path string, flag int) (io.WriteCloser, error) {
reader, writer := io.Pipe()
log.Debug().Str("path", path).Str("schema", "ftp").Msg("opening for writing")
go func() {
l.stepLock.Lock()
defer l.stepLock.Unlock()
err := l.client.Stor(path, reader)
if err != nil {
log.Err(err).Msg("failed to store file")
}
log.Debug().Str("path", path).Str("schema", "ftp").Msg("write success")
}()
return writer, nil
}

69
cli/disk/local.go Normal file
View file

@ -0,0 +1,69 @@
package disk
import (
"io"
"os"
)
var _ Disk = (*localDisk)(nil)
type localDisk struct {
path string
}
type localEntry struct {
os.DirEntry
}
func newLocal(path string) (Disk, error) {
return localDisk{path: path}, nil
}
func (l localDisk) Exists(path string) error {
_, err := os.Stat(path)
return err //nolint
}
func (l localDisk) Read(path string) ([]byte, error) {
return os.ReadFile(path) //nolint
}
func (l localDisk) Write(path string, data []byte) error {
return os.WriteFile(path, data, 0777) //nolint
}
func (l localDisk) Remove(path string) error {
return os.RemoveAll(path) //nolint
}
func (l localDisk) MkDir(path string) error {
return os.MkdirAll(path, 0777) //nolint
}
func (l localDisk) ReadDir(path string) ([]Entry, error) {
dir, err := os.ReadDir(path)
if err != nil {
return nil, err //nolint
}
entries := make([]Entry, len(dir))
for i, entry := range dir {
entries[i] = localEntry{
DirEntry: entry,
}
}
return entries, nil
}
func (l localDisk) IsNotExist(err error) bool {
return os.IsNotExist(err)
}
func (l localDisk) IsExist(err error) bool {
return os.IsExist(err)
}
func (l localDisk) Open(path string, flag int) (io.WriteCloser, error) {
return os.OpenFile(path, flag, 0777) //nolint
}

47
cli/disk/main.go Normal file
View file

@ -0,0 +1,47 @@
package disk
import (
"io"
"net/url"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
)
type Disk interface {
Exists(path string) error
Read(path string) ([]byte, error)
Write(path string, data []byte) error
Remove(path string) error
MkDir(path string) error
ReadDir(path string) ([]Entry, error)
IsNotExist(err error) bool
IsExist(err error) bool
Open(path string, flag int) (io.WriteCloser, error)
}
type Entry interface {
IsDir() bool
Name() string
}
func FromPath(path string) (Disk, error) {
parsed, err := url.Parse(path)
if err != nil {
return nil, errors.Wrap(err, "failed to parse path")
}
log.Info().Msg(path)
log.Info().Msg(parsed.Scheme)
switch parsed.Scheme {
case "ftp":
log.Info().Str("path", path).Msg("connecting to ftp")
return newFTP(path)
case "sftp":
log.Info().Str("path", path).Msg("connecting to sftp")
return newSFTP(path)
}
log.Info().Str("path", path).Msg("using local disk")
return newLocal(path)
}

60
cli/disk/sftp.go Normal file
View file

@ -0,0 +1,60 @@
package disk
import (
"io"
)
var _ Disk = (*sftpDisk)(nil)
type sftpDisk struct {
path string
}
func newSFTP(path string) (Disk, error) {
return sftpDisk{path: path}, nil
}
func (l sftpDisk) Exists(path string) error {
//TODO implement me
panic("implement me")
}
func (l sftpDisk) Read(path string) ([]byte, error) {
//TODO implement me
panic("implement me")
}
func (l sftpDisk) Write(path string, data []byte) error {
//TODO implement me
panic("implement me")
}
func (l sftpDisk) Remove(path string) error {
//TODO implement me
panic("implement me")
}
func (l sftpDisk) MkDir(path string) error {
//TODO implement me
panic("implement me")
}
func (l sftpDisk) ReadDir(path string) ([]Entry, error) {
//TODO implement me
panic("implement me")
}
func (l sftpDisk) IsNotExist(err error) bool {
//TODO implement me
panic("implement me")
}
func (l sftpDisk) IsExist(err error) bool {
//TODO implement me
panic("implement me")
}
func (l sftpDisk) Open(path string, flag int) (io.WriteCloser, error) {
//TODO implement me
panic("implement me")
}

View file

@ -3,12 +3,14 @@ package cli
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
"github.com/satisfactorymodding/ficsit-cli/utils" "github.com/satisfactorymodding/ficsit-cli/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -32,8 +34,9 @@ type Installations struct {
} }
type Installation struct { type Installation struct {
Path string `json:"path"` Path string `json:"path"`
Profile string `json:"profile"` Profile string `json:"profile"`
DiskInstance disk.Disk `json:"-"`
} }
func InitInstallations() (*Installations, error) { func InitInstallations() (*Installations, error) {
@ -107,10 +110,18 @@ func (i *Installations) Save() error {
} }
func (i *Installations) AddInstallation(ctx *GlobalContext, installPath string, profile string) (*Installation, error) { func (i *Installations) AddInstallation(ctx *GlobalContext, installPath string, profile string) (*Installation, error) {
absolutePath, err := filepath.Abs(installPath) parsed, err := url.Parse(installPath)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "could not resolve absolute path of: "+installPath) return nil, errors.Wrap(err, "failed to parse path")
}
var absolutePath = installPath
if parsed.Scheme == "" {
absolutePath, err = filepath.Abs(installPath)
if err != nil {
return nil, errors.Wrap(err, "could not resolve absolute path of: "+installPath)
}
} }
installation := &Installation{ installation := &Installation{
@ -118,24 +129,19 @@ func (i *Installations) AddInstallation(ctx *GlobalContext, installPath string,
Profile: profile, Profile: profile,
} }
// ftp://one:1234@localhost:21/
log.Info().Msg("installPath: " + installPath)
log.Info().Msg("absolutePath: " + absolutePath)
if err := installation.Validate(ctx); err != nil { if err := installation.Validate(ctx); err != nil {
return nil, errors.Wrap(err, "failed to validate installation") return nil, errors.Wrap(err, "failed to validate installation")
} }
newStat, err := os.Stat(installation.Path)
if err != nil {
return nil, errors.Wrap(err, "failed to stat installation directory")
}
found := false found := false
for _, install := range i.Installations { for _, install := range i.Installations {
stat, err := os.Stat(install.Path) if filepath.Clean(installation.Path) == filepath.Clean(install.Path) {
if err != nil { found = true
continue
}
found = os.SameFile(newStat, stat)
if found {
break break
} }
} }
@ -190,29 +196,34 @@ func (i *Installation) Validate(ctx *GlobalContext) error {
return errors.New("profile not found") return errors.New("profile not found")
} }
d, err := i.GetDisk()
if err != nil {
return err
}
foundExecutable := false foundExecutable := false
_, err := os.Stat(filepath.Join(i.Path, "FactoryGame.exe")) err = d.Exists(filepath.Join(i.BasePath(), "FactoryGame.exe"))
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !d.IsNotExist(err) {
return errors.Wrap(err, "failed reading FactoryGame.exe") return errors.Wrap(err, "failed reading FactoryGame.exe")
} }
} else { } else {
foundExecutable = true foundExecutable = true
} }
_, err = os.Stat(filepath.Join(i.Path, "FactoryServer.sh")) err = d.Exists(filepath.Join(i.BasePath(), "FactoryServer.sh"))
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !d.IsNotExist(err) {
return errors.Wrap(err, "failed reading FactoryServer.sh") return errors.Wrap(err, "failed reading FactoryServer.sh")
} }
} else { } else {
foundExecutable = true foundExecutable = true
} }
_, err = os.Stat(filepath.Join(i.Path, "FactoryServer.exe")) err = d.Exists(filepath.Join(i.BasePath(), "FactoryServer.exe"))
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !d.IsNotExist(err) {
return errors.Wrap(err, "failed reading FactoryServer.exe") return errors.Wrap(err, "failed reading FactoryServer.exe")
} }
} else { } else {
@ -220,7 +231,7 @@ func (i *Installation) Validate(ctx *GlobalContext) error {
} }
if !foundExecutable { if !foundExecutable {
return errors.New("did not find game executable in " + i.Path) return errors.New("did not find game executable in " + i.BasePath())
} }
return nil return nil
@ -244,7 +255,7 @@ func (i *Installation) LockFilePath(ctx *GlobalContext) (string, error) {
lockFileName = lockFileCleaner.ReplaceAllLiteralString(lockFileName, "-") lockFileName = lockFileCleaner.ReplaceAllLiteralString(lockFileName, "-")
lockFileName = strings.ToLower(lockFileName) + "-lock.json" lockFileName = strings.ToLower(lockFileName) + "-lock.json"
return filepath.Join(i.Path, platform.LockfilePath, lockFileName), nil return filepath.Join(i.BasePath(), platform.LockfilePath, lockFileName), nil
} }
func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) { func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
@ -253,10 +264,15 @@ func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
return nil, err return nil, err
} }
var lockFile *LockFile d, err := i.GetDisk()
lockFileJSON, err := os.ReadFile(lockfilePath)
if err != nil { if err != nil {
if !os.IsNotExist(err) { return nil, err
}
var lockFile *LockFile
lockFileJSON, err := d.Read(lockfilePath)
if err != nil {
if !d.IsNotExist(err) {
return nil, errors.Wrap(err, "failed reading lockfile") return nil, errors.Wrap(err, "failed reading lockfile")
} }
} else { } else {
@ -280,7 +296,12 @@ func (i *Installation) WriteLockFile(ctx *GlobalContext, lockfile LockFile) erro
return errors.Wrap(err, "failed to serialize lockfile json") return errors.Wrap(err, "failed to serialize lockfile json")
} }
if err := os.WriteFile(lockfilePath, marshaledLockfile, 0777); err != nil { d, err := i.GetDisk()
if err != nil {
return err
}
if err := d.Write(lockfilePath, marshaledLockfile); err != nil {
return errors.Wrap(err, "failed writing lockfile") return errors.Wrap(err, "failed writing lockfile")
} }
@ -317,12 +338,17 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
return errors.Wrap(err, "could not resolve mods") return errors.Wrap(err, "could not resolve mods")
} }
modsDirectory := filepath.Join(i.Path, "FactoryGame", "Mods") d, err := i.GetDisk()
if err := os.MkdirAll(modsDirectory, 0777); err != nil { if err != nil {
return err
}
modsDirectory := filepath.Join(i.BasePath(), "FactoryGame", "Mods")
if err := d.MkDir(modsDirectory); err != nil {
return errors.Wrap(err, "failed creating Mods directory") return errors.Wrap(err, "failed creating Mods directory")
} }
dir, err := os.ReadDir(modsDirectory) dir, err := d.ReadDir(modsDirectory)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to read mods directory") return errors.Wrap(err, "failed to read mods directory")
} }
@ -331,10 +357,10 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
if entry.IsDir() { if entry.IsDir() {
if _, ok := lockfile[entry.Name()]; !ok { if _, ok := lockfile[entry.Name()]; !ok {
modDir := filepath.Join(modsDirectory, entry.Name()) modDir := filepath.Join(modsDirectory, entry.Name())
_, err := os.Stat(filepath.Join(modDir, ".smm")) err := d.Exists(filepath.Join(modDir, ".smm"))
if err == nil { if err == nil {
log.Info().Str("mod_reference", entry.Name()).Msg("deleting mod") log.Info().Str("mod_reference", entry.Name()).Msg("deleting mod")
if err := os.RemoveAll(modDir); err != nil { if err := d.Remove(modDir); err != nil {
return errors.Wrap(err, "failed to delete mod directory") return errors.Wrap(err, "failed to delete mod directory")
} }
} }
@ -396,10 +422,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
downloading = true downloading = true
if genericUpdates != nil { if genericUpdates != nil {
select { genericUpdates <- utils.GenericUpdate{ModReference: &modReference}
case genericUpdates <- utils.GenericUpdate{ModReference: &modReference}:
default:
}
} }
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")
@ -411,7 +434,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
downloading = false downloading = false
log.Info().Str("mod_reference", modReference).Str("version", version.Version).Str("link", version.Link).Msg("extracting mod") 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); err != nil { if err := utils.ExtractMod(reader, size, filepath.Join(modsDirectory, modReference), version.Hash, genericUpdates, d); err != nil {
return errors.Wrap(err, "could not extract "+modReference) return errors.Wrap(err, "could not extract "+modReference)
} }
} }
@ -466,10 +489,15 @@ func (i *Installation) GetGameVersion(ctx *GlobalContext) (int, error) {
return 0, err return 0, err
} }
fullPath := filepath.Join(i.Path, platform.VersionPath) d, err := i.GetDisk()
file, err := os.ReadFile(fullPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { return 0, err
}
fullPath := filepath.Join(i.BasePath(), platform.VersionPath)
file, err := d.Read(fullPath)
if err != nil {
if d.IsNotExist(err) {
return 0, errors.Wrap(err, "could not find game version file") return 0, errors.Wrap(err, "could not find game version file")
} }
return 0, errors.Wrap(err, "failed reading version file") return 0, errors.Wrap(err, "failed reading version file")
@ -488,11 +516,16 @@ func (i *Installation) GetPlatform(ctx *GlobalContext) (*Platform, error) {
return nil, errors.Wrap(err, "failed to validate installation") return nil, errors.Wrap(err, "failed to validate installation")
} }
d, err := i.GetDisk()
if err != nil {
return nil, err
}
for _, platform := range platforms { for _, platform := range platforms {
fullPath := filepath.Join(i.Path, platform.VersionPath) fullPath := filepath.Join(i.BasePath(), platform.VersionPath)
_, err := os.Stat(fullPath) err := d.Exists(fullPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if d.IsNotExist(err) {
continue continue
} else { } else {
return nil, errors.Wrap(err, "failed detecting version file") return nil, errors.Wrap(err, "failed detecting version file")
@ -503,3 +536,21 @@ func (i *Installation) GetPlatform(ctx *GlobalContext) (*Platform, error) {
return nil, errors.New("no platform detected") return nil, errors.New("no platform detected")
} }
func (i *Installation) GetDisk() (disk.Disk, error) {
if i.DiskInstance != nil {
return i.DiskInstance, nil
}
var err error
i.DiskInstance, err = disk.FromPath(i.Path)
return i.DiskInstance, err
}
func (i *Installation) BasePath() string {
parsed, err := url.Parse(i.Path)
if err != nil {
return i.Path
}
return parsed.Path
}

View file

@ -11,11 +11,12 @@ import (
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cmd/installation" "github.com/satisfactorymodding/ficsit-cli/cmd/installation"
"github.com/satisfactorymodding/ficsit-cli/cmd/mod" "github.com/satisfactorymodding/ficsit-cli/cmd/mod"
"github.com/satisfactorymodding/ficsit-cli/cmd/profile" "github.com/satisfactorymodding/ficsit-cli/cmd/profile"
"github.com/spf13/cobra"
"github.com/spf13/viper"
) )
var RootCmd = &cobra.Command{ var RootCmd = &cobra.Command{

5
go.mod
View file

@ -12,6 +12,7 @@ require (
github.com/charmbracelet/bubbletea v0.21.0 github.com/charmbracelet/bubbletea v0.21.0
github.com/charmbracelet/glamour v0.5.0 github.com/charmbracelet/glamour v0.5.0
github.com/charmbracelet/lipgloss v0.5.0 github.com/charmbracelet/lipgloss v0.5.0
github.com/jlaffaye/ftp v0.0.0-20220612151834-60a941566ce4
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.41 github.com/pterm/pterm v0.12.41
@ -40,6 +41,8 @@ require (
github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/gookit/color v1.5.0 // indirect github.com/gookit/color v1.5.0 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect
@ -76,5 +79,5 @@ require (
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

12
go.sum
View file

@ -189,6 +189,10 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.6.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -197,6 +201,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jlaffaye/ftp v0.0.0-20220612151834-60a941566ce4 h1:gO/ufFrST8nt0py0FvlYHsSW81RCYeqflr8czF+UBys=
github.com/jlaffaye/ftp v0.0.0-20220612151834-60a941566ce4/go.mod h1:YFstjM4Y5zZdsON18Az8MNRgObXGJgor/UBMEQtZJes=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@ -314,8 +320,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI=
github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
@ -668,8 +674,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -37,6 +37,8 @@ type apply struct {
status update status update
updateChannel chan update updateChannel chan update
errorChannel chan error errorChannel chan error
cancelChannel chan bool
cancelled bool
} }
func NewApply(root components.RootModel, parent tea.Model) tea.Model { func NewApply(root components.RootModel, parent tea.Model) tea.Model {
@ -45,6 +47,7 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
updateChannel := make(chan update) updateChannel := make(chan update)
errorChannel := make(chan error) errorChannel := make(chan error)
cancelChannel := make(chan bool, 1)
model := &apply{ model := &apply{
root: root, root: root,
@ -67,6 +70,8 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
}, },
updateChannel: updateChannel, updateChannel: updateChannel,
errorChannel: errorChannel, errorChannel: errorChannel,
cancelChannel: cancelChannel,
cancelled: false,
} }
go func() { go func() {
@ -119,6 +124,17 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
result.installTotal = 100 result.installTotal = 100
result.completed = append(result.completed, installation.Path) result.completed = append(result.completed, installation.Path)
updateChannel <- *result updateChannel <- *result
stop := false
select {
case <-cancelChannel:
stop = true
default:
}
if stop {
break
}
} }
result.done = true result.done = true
@ -140,7 +156,8 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case KeyControlC: case KeyControlC:
return m, tea.Quit return m, tea.Quit
case KeyEscape: case KeyEscape:
// TODO Cancel m.cancelled = true
m.cancelChannel <- true
return m, nil return m, nil
case KeyEnter: case KeyEnter:
if m.status.done { if m.status.done {
@ -195,7 +212,11 @@ func (m apply) View() string {
} }
if m.status.done { if m.status.done {
strs = append(strs, utils.LabelStyle.Copy().Padding(0).Margin(1).Render("Done! Press Enter to return")) if m.cancelled {
strs = append(strs, utils.LabelStyle.Copy().Foreground(lipgloss.Color("196")).Padding(0).Margin(1).Render("Cancelled! Press Enter to return"))
} else {
strs = append(strs, utils.LabelStyle.Copy().Padding(0).Margin(1).Render("Done! Press Enter to return"))
}
} }
result := lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinVertical(lipgloss.Left, strs...)) result := lipgloss.NewStyle().MarginLeft(1).Render(lipgloss.JoinVertical(lipgloss.Left, strs...))

View file

@ -16,6 +16,7 @@ import (
"github.com/charmbracelet/lipgloss" "github.com/charmbracelet/lipgloss"
"github.com/muesli/reflow/truncate" "github.com/muesli/reflow/truncate"
"github.com/sahilm/fuzzy" "github.com/sahilm/fuzzy"
"github.com/satisfactorymodding/ficsit-cli/tea/components" "github.com/satisfactorymodding/ficsit-cli/tea/components"
"github.com/satisfactorymodding/ficsit-cli/tea/utils" "github.com/satisfactorymodding/ficsit-cli/tea/utils"
) )
@ -57,8 +58,6 @@ func NewNewInstallation(root components.RootModel, parent tea.Model) tea.Model {
model.input.Focus() model.input.Focus()
model.input.Width = root.Size().Width model.input.Width = root.Size().Width
// TODO SSH/FTP/SFTP support
return model return model
} }

View file

@ -12,6 +12,8 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
) )
type Progresser struct { type Progresser struct {
@ -141,11 +143,11 @@ 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) error { func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates chan GenericUpdate, d disk.Disk) error {
hashFile := filepath.Join(location, ".smm") hashFile := filepath.Join(location, ".smm")
hashBytes, err := os.ReadFile(hashFile) hashBytes, err := d.Read(hashFile)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !d.IsNotExist(err) {
return errors.Wrap(err, "failed to read .smm mod hash file") return errors.Wrap(err, "failed to read .smm mod hash file")
} }
} else { } else {
@ -154,16 +156,16 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
} }
} }
if err := os.MkdirAll(location, 0777); err != nil { if err := d.MkDir(location); err != nil {
if !os.IsExist(err) { if !d.IsExist(err) {
return errors.Wrap(err, "failed to create mod directory: "+location) return errors.Wrap(err, "failed to create mod directory: "+location)
} }
} else {
if err := os.RemoveAll(location); err != nil { if err := d.Remove(location); err != nil {
return errors.Wrap(err, "failed to remove directory: "+location) return errors.Wrap(err, "failed to remove directory: "+location)
} }
if err := os.MkdirAll(location, 0777); err != nil { if err := d.MkDir(location); err != nil {
return errors.Wrap(err, "failed to create mod directory: "+location) return errors.Wrap(err, "failed to create mod directory: "+location)
} }
} }
@ -177,11 +179,11 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
if !file.FileInfo().IsDir() { if !file.FileInfo().IsDir() {
outFileLocation := filepath.Join(location, file.Name) outFileLocation := filepath.Join(location, file.Name)
if err := os.MkdirAll(filepath.Dir(outFileLocation), 0777); err != nil { if err := d.MkDir(filepath.Dir(outFileLocation)); err != nil {
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); err != nil { if err := writeZipFile(outFileLocation, file, d); err != nil {
return err return err
} }
} }
@ -194,7 +196,7 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
} }
} }
if err := os.WriteFile(hashFile, []byte(hash), 0777); err != nil { if err := d.Write(hashFile, []byte(hash)); err != nil {
return errors.Wrap(err, "failed to write .smm mod hash file") return errors.Wrap(err, "failed to write .smm mod hash file")
} }
@ -208,8 +210,8 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
return nil return nil
} }
func writeZipFile(outFileLocation string, file *zip.File) error { func writeZipFile(outFileLocation string, file *zip.File, d disk.Disk) error {
outFile, err := os.OpenFile(outFileLocation, os.O_CREATE|os.O_RDWR, 0644) 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)
} }