feat: add support for ftp
This commit is contained in:
parent
de24e8dcf6
commit
ef7f8cc8e8
11 changed files with 522 additions and 68 deletions
195
cli/disk/ftp.go
Normal file
195
cli/disk/ftp.go
Normal 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
69
cli/disk/local.go
Normal 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
47
cli/disk/main.go
Normal 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
60
cli/disk/sftp.go
Normal 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")
|
||||
}
|
|
@ -3,12 +3,14 @@ package cli
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
|
||||
"github.com/satisfactorymodding/ficsit-cli/utils"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -32,8 +34,9 @@ type Installations struct {
|
|||
}
|
||||
|
||||
type Installation struct {
|
||||
Path string `json:"path"`
|
||||
Profile string `json:"profile"`
|
||||
Path string `json:"path"`
|
||||
Profile string `json:"profile"`
|
||||
DiskInstance disk.Disk `json:"-"`
|
||||
}
|
||||
|
||||
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) {
|
||||
absolutePath, err := filepath.Abs(installPath)
|
||||
|
||||
parsed, err := url.Parse(installPath)
|
||||
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{
|
||||
|
@ -118,24 +129,19 @@ func (i *Installations) AddInstallation(ctx *GlobalContext, installPath string,
|
|||
Profile: profile,
|
||||
}
|
||||
|
||||
// ftp://one:1234@localhost:21/
|
||||
|
||||
log.Info().Msg("installPath: " + installPath)
|
||||
log.Info().Msg("absolutePath: " + absolutePath)
|
||||
|
||||
if err := installation.Validate(ctx); err != nil {
|
||||
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
|
||||
for _, install := range i.Installations {
|
||||
stat, err := os.Stat(install.Path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
found = os.SameFile(newStat, stat)
|
||||
if found {
|
||||
if filepath.Clean(installation.Path) == filepath.Clean(install.Path) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -190,29 +196,34 @@ func (i *Installation) Validate(ctx *GlobalContext) error {
|
|||
return errors.New("profile not found")
|
||||
}
|
||||
|
||||
d, err := i.GetDisk()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
foundExecutable := false
|
||||
|
||||
_, err := os.Stat(filepath.Join(i.Path, "FactoryGame.exe"))
|
||||
err = d.Exists(filepath.Join(i.BasePath(), "FactoryGame.exe"))
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
if !d.IsNotExist(err) {
|
||||
return errors.Wrap(err, "failed reading FactoryGame.exe")
|
||||
}
|
||||
} else {
|
||||
foundExecutable = true
|
||||
}
|
||||
|
||||
_, err = os.Stat(filepath.Join(i.Path, "FactoryServer.sh"))
|
||||
err = d.Exists(filepath.Join(i.BasePath(), "FactoryServer.sh"))
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
if !d.IsNotExist(err) {
|
||||
return errors.Wrap(err, "failed reading FactoryServer.sh")
|
||||
}
|
||||
} else {
|
||||
foundExecutable = true
|
||||
}
|
||||
|
||||
_, err = os.Stat(filepath.Join(i.Path, "FactoryServer.exe"))
|
||||
err = d.Exists(filepath.Join(i.BasePath(), "FactoryServer.exe"))
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
if !d.IsNotExist(err) {
|
||||
return errors.Wrap(err, "failed reading FactoryServer.exe")
|
||||
}
|
||||
} else {
|
||||
|
@ -220,7 +231,7 @@ func (i *Installation) Validate(ctx *GlobalContext) error {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -244,7 +255,7 @@ func (i *Installation) LockFilePath(ctx *GlobalContext) (string, error) {
|
|||
lockFileName = lockFileCleaner.ReplaceAllLiteralString(lockFileName, "-")
|
||||
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) {
|
||||
|
@ -253,10 +264,15 @@ func (i *Installation) LockFile(ctx *GlobalContext) (*LockFile, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var lockFile *LockFile
|
||||
lockFileJSON, err := os.ReadFile(lockfilePath)
|
||||
d, err := i.GetDisk()
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
|
@ -280,7 +296,12 @@ func (i *Installation) WriteLockFile(ctx *GlobalContext, lockfile LockFile) erro
|
|||
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")
|
||||
}
|
||||
|
||||
|
@ -317,12 +338,17 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
|
|||
return errors.Wrap(err, "could not resolve mods")
|
||||
}
|
||||
|
||||
modsDirectory := filepath.Join(i.Path, "FactoryGame", "Mods")
|
||||
if err := os.MkdirAll(modsDirectory, 0777); err != nil {
|
||||
d, err := i.GetDisk()
|
||||
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")
|
||||
}
|
||||
|
||||
dir, err := os.ReadDir(modsDirectory)
|
||||
dir, err := d.ReadDir(modsDirectory)
|
||||
if err != nil {
|
||||
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 _, ok := lockfile[entry.Name()]; !ok {
|
||||
modDir := filepath.Join(modsDirectory, entry.Name())
|
||||
_, err := os.Stat(filepath.Join(modDir, ".smm"))
|
||||
err := d.Exists(filepath.Join(modDir, ".smm"))
|
||||
if err == nil {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
@ -396,10 +422,7 @@ func (i *Installation) Install(ctx *GlobalContext, updates chan InstallUpdate) e
|
|||
downloading = true
|
||||
|
||||
if genericUpdates != nil {
|
||||
select {
|
||||
case genericUpdates <- utils.GenericUpdate{ModReference: &modReference}:
|
||||
default:
|
||||
}
|
||||
genericUpdates <- utils.GenericUpdate{ModReference: &modReference}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -466,10 +489,15 @@ func (i *Installation) GetGameVersion(ctx *GlobalContext) (int, error) {
|
|||
return 0, err
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(i.Path, platform.VersionPath)
|
||||
file, err := os.ReadFile(fullPath)
|
||||
d, err := i.GetDisk()
|
||||
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, "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")
|
||||
}
|
||||
|
||||
d, err := i.GetDisk()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, platform := range platforms {
|
||||
fullPath := filepath.Join(i.Path, platform.VersionPath)
|
||||
_, err := os.Stat(fullPath)
|
||||
fullPath := filepath.Join(i.BasePath(), platform.VersionPath)
|
||||
err := d.Exists(fullPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if d.IsNotExist(err) {
|
||||
continue
|
||||
} else {
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -11,11 +11,12 @@ import (
|
|||
"github.com/pterm/pterm"
|
||||
"github.com/rs/zerolog"
|
||||
"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/mod"
|
||||
"github.com/satisfactorymodding/ficsit-cli/cmd/profile"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var RootCmd = &cobra.Command{
|
||||
|
|
5
go.mod
5
go.mod
|
@ -12,6 +12,7 @@ require (
|
|||
github.com/charmbracelet/bubbletea v0.21.0
|
||||
github.com/charmbracelet/glamour 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/pkg/errors v0.9.1
|
||||
github.com/pterm/pterm v0.12.41
|
||||
|
@ -40,6 +41,8 @@ require (
|
|||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/gookit/color v1.5.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/inconshreveable/mousetrap v1.0.0 // 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
|
||||
gopkg.in/ini.v1 v1.66.4 // 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
12
go.sum
|
@ -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/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/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.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
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.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
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.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.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
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/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs=
|
||||
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.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 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
|
@ -37,6 +37,8 @@ type apply struct {
|
|||
status update
|
||||
updateChannel chan update
|
||||
errorChannel chan error
|
||||
cancelChannel chan bool
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
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)
|
||||
errorChannel := make(chan error)
|
||||
cancelChannel := make(chan bool, 1)
|
||||
|
||||
model := &apply{
|
||||
root: root,
|
||||
|
@ -67,6 +70,8 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
|
|||
},
|
||||
updateChannel: updateChannel,
|
||||
errorChannel: errorChannel,
|
||||
cancelChannel: cancelChannel,
|
||||
cancelled: false,
|
||||
}
|
||||
|
||||
go func() {
|
||||
|
@ -119,6 +124,17 @@ func NewApply(root components.RootModel, parent tea.Model) tea.Model {
|
|||
result.installTotal = 100
|
||||
result.completed = append(result.completed, installation.Path)
|
||||
updateChannel <- *result
|
||||
|
||||
stop := false
|
||||
select {
|
||||
case <-cancelChannel:
|
||||
stop = true
|
||||
default:
|
||||
}
|
||||
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
result.done = true
|
||||
|
@ -140,7 +156,8 @@ func (m apply) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case KeyControlC:
|
||||
return m, tea.Quit
|
||||
case KeyEscape:
|
||||
// TODO Cancel
|
||||
m.cancelled = true
|
||||
m.cancelChannel <- true
|
||||
return m, nil
|
||||
case KeyEnter:
|
||||
if m.status.done {
|
||||
|
@ -195,7 +212,11 @@ func (m apply) View() string {
|
|||
}
|
||||
|
||||
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...))
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/muesli/reflow/truncate"
|
||||
"github.com/sahilm/fuzzy"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/tea/components"
|
||||
"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.Width = root.Size().Width
|
||||
|
||||
// TODO SSH/FTP/SFTP support
|
||||
|
||||
return model
|
||||
}
|
||||
|
||||
|
|
28
utils/io.go
28
utils/io.go
|
@ -12,6 +12,8 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/satisfactorymodding/ficsit-cli/cli/disk"
|
||||
)
|
||||
|
||||
type Progresser struct {
|
||||
|
@ -141,11 +143,11 @@ func SHA256Data(f io.Reader) (string, error) {
|
|||
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")
|
||||
hashBytes, err := os.ReadFile(hashFile)
|
||||
hashBytes, err := d.Read(hashFile)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
if !d.IsNotExist(err) {
|
||||
return errors.Wrap(err, "failed to read .smm mod hash file")
|
||||
}
|
||||
} 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 !os.IsExist(err) {
|
||||
if err := d.MkDir(location); err != nil {
|
||||
if !d.IsExist(err) {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -177,11 +179,11 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
|
|||
if !file.FileInfo().IsDir() {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := writeZipFile(outFileLocation, file); err != nil {
|
||||
if err := writeZipFile(outFileLocation, file, d); err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -208,8 +210,8 @@ func ExtractMod(f io.ReaderAt, size int64, location string, hash string, updates
|
|||
return nil
|
||||
}
|
||||
|
||||
func writeZipFile(outFileLocation string, file *zip.File) error {
|
||||
outFile, err := os.OpenFile(outFileLocation, os.O_CREATE|os.O_RDWR, 0644)
|
||||
func writeZipFile(outFileLocation string, file *zip.File, d disk.Disk) error {
|
||||
outFile, err := d.Open(outFileLocation, os.O_CREATE|os.O_RDWR)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to write to file: "+outFileLocation)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue