From ef7f8cc8e840de688e527ecada361d7bae371ae9 Mon Sep 17 00:00:00 2001 From: Vilsol Date: Thu, 23 Jun 2022 01:24:35 +0300 Subject: [PATCH] feat: add support for ftp --- cli/disk/ftp.go | 195 +++++++++++++++++++++++++++++++++ cli/disk/local.go | 69 ++++++++++++ cli/disk/main.go | 47 ++++++++ cli/disk/sftp.go | 60 ++++++++++ cli/installations.go | 141 ++++++++++++++++-------- cmd/root.go | 5 +- go.mod | 5 +- go.sum | 12 +- tea/scenes/apply.go | 25 ++++- tea/scenes/new_installation.go | 3 +- utils/io.go | 28 ++--- 11 files changed, 522 insertions(+), 68 deletions(-) create mode 100644 cli/disk/ftp.go create mode 100644 cli/disk/local.go create mode 100644 cli/disk/main.go create mode 100644 cli/disk/sftp.go diff --git a/cli/disk/ftp.go b/cli/disk/ftp.go new file mode 100644 index 0000000..32f42dc --- /dev/null +++ b/cli/disk/ftp.go @@ -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 +} diff --git a/cli/disk/local.go b/cli/disk/local.go new file mode 100644 index 0000000..af827e0 --- /dev/null +++ b/cli/disk/local.go @@ -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 +} diff --git a/cli/disk/main.go b/cli/disk/main.go new file mode 100644 index 0000000..42b2ec1 --- /dev/null +++ b/cli/disk/main.go @@ -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) +} diff --git a/cli/disk/sftp.go b/cli/disk/sftp.go new file mode 100644 index 0000000..d9dae67 --- /dev/null +++ b/cli/disk/sftp.go @@ -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") +} diff --git a/cli/installations.go b/cli/installations.go index 43f54ae..25c80d4 100644 --- a/cli/installations.go +++ b/cli/installations.go @@ -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 +} diff --git a/cmd/root.go b/cmd/root.go index ddbd247..4699dda 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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{ diff --git a/go.mod b/go.mod index 51997e8..b7ab2d0 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index c816bba..da07459 100644 --- a/go.sum +++ b/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= diff --git a/tea/scenes/apply.go b/tea/scenes/apply.go index dc6f8d1..fc5e987 100644 --- a/tea/scenes/apply.go +++ b/tea/scenes/apply.go @@ -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...)) diff --git a/tea/scenes/new_installation.go b/tea/scenes/new_installation.go index b8fb5a4..12d4d79 100644 --- a/tea/scenes/new_installation.go +++ b/tea/scenes/new_installation.go @@ -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 } diff --git a/utils/io.go b/utils/io.go index 6272416..d1cb30c 100644 --- a/utils/io.go +++ b/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) }