Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI: Refactoring with prompts and auto-completion #1072

Merged
merged 9 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/cli-prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Refactor Reva CLI with prompts

The current CLI is a bit cumbersome to use with users having to type commands
containing all the associated config with no prompts or auto-completes. This PR
refactors the CLI with these functionalities.

https://github.com/cs3org/reva/pull/1072
235 changes: 235 additions & 0 deletions cmd/reva/arguments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Copyright 2018-2020 CERN
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// In applying this license, CERN does not waive the privileges and immunities
// granted to it by virtue of its status as an Intergovernmental Organization
// or submit itself to any jurisdiction.

package main

import (
"bytes"
"encoding/gob"
"errors"
"sync"
"time"

"github.com/c-bata/go-prompt"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)

type argumentCompleter struct {
suggestions []prompt.Suggest
expiration time.Time
sync.RWMutex
}

func (c *Completer) loginArgumentCompleter() []prompt.Suggest {
if s, ok := checkCache(c.loginArguments); ok {
return s
}

types := []string{}
suggests := []prompt.Suggest{}

b, err := executeCommand(loginCommand(), "-list")
if err == nil {
dec := gob.NewDecoder(&b)
if err := dec.Decode(&types); err != nil {
return []prompt.Suggest{}
}

for _, t := range types {
suggests = append(suggests, prompt.Suggest{Text: t})
}
}

cacheSuggestions(c.loginArguments, suggests)
return suggests
}

func (c *Completer) lsArgumentCompleter(onlyDirs bool) []prompt.Suggest {
if onlyDirs {
if s, ok := checkCache(c.lsDirArguments); ok {
return s
}
} else {
if s, ok := checkCache(c.lsArguments); ok {
return s
}
}

info := []*provider.ResourceInfo{}
suggests := []prompt.Suggest{}

b, err := executeCommand(lsCommand(), "/home")
if err == nil {
dec := gob.NewDecoder(&b)
if err := dec.Decode(&info); err != nil {
return []prompt.Suggest{}
}

suggests = append(suggests, prompt.Suggest{Text: "/home"})
for _, r := range info {
if !onlyDirs || r.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
suggests = append(suggests, prompt.Suggest{Text: r.Path})
}
}
}

if onlyDirs {
cacheSuggestions(c.lsDirArguments, suggests)
} else {
cacheSuggestions(c.lsArguments, suggests)
}
return suggests
}

func (c *Completer) ocmShareArgumentCompleter() []prompt.Suggest {
if s, ok := checkCache(c.ocmShareArguments); ok {
return s
}

info := []*ocm.Share{}
suggests := []prompt.Suggest{}

b, err := executeCommand(ocmShareListCommand())
if err == nil {
dec := gob.NewDecoder(&b)
if err := dec.Decode(&info); err != nil {
return []prompt.Suggest{}
}

for _, r := range info {
suggests = append(suggests, prompt.Suggest{Text: r.Id.OpaqueId})
}
}

cacheSuggestions(c.ocmShareArguments, suggests)
return suggests
}

func (c *Completer) ocmShareReceivedArgumentCompleter() []prompt.Suggest {
if s, ok := checkCache(c.ocmShareReceivedArguments); ok {
return s
}

info := []*ocm.ReceivedShare{}
suggests := []prompt.Suggest{}

b, err := executeCommand(ocmShareListReceivedCommand())
if err == nil {
dec := gob.NewDecoder(&b)
if err := dec.Decode(&info); err != nil {
return []prompt.Suggest{}
}

for _, r := range info {
suggests = append(suggests, prompt.Suggest{Text: r.Share.Id.OpaqueId})
}
}

cacheSuggestions(c.ocmShareReceivedArguments, suggests)
return suggests
}

func (c *Completer) shareArgumentCompleter() []prompt.Suggest {
if s, ok := checkCache(c.shareArguments); ok {
return s
}

info := []*collaboration.Share{}
suggests := []prompt.Suggest{}

b, err := executeCommand(shareListCommand())
if err == nil {
dec := gob.NewDecoder(&b)
if err := dec.Decode(&info); err != nil {
return []prompt.Suggest{}
}

for _, r := range info {
suggests = append(suggests, prompt.Suggest{Text: r.Id.OpaqueId})
}
}

cacheSuggestions(c.shareArguments, suggests)
return suggests
}

func (c *Completer) shareReceivedArgumentCompleter() []prompt.Suggest {
if s, ok := checkCache(c.shareReceivedArguments); ok {
return s
}

info := []*collaboration.ReceivedShare{}
suggests := []prompt.Suggest{}

b, err := executeCommand(shareListReceivedCommand())
if err == nil {
dec := gob.NewDecoder(&b)
if err := dec.Decode(&info); err != nil {
return []prompt.Suggest{}
}

for _, r := range info {
suggests = append(suggests, prompt.Suggest{Text: r.Share.Id.OpaqueId})
}
}

cacheSuggestions(c.shareReceivedArguments, suggests)
return suggests
}

func executeCommand(cmd *command, args ...string) (bytes.Buffer, error) {
var b bytes.Buffer
var err error
if err = cmd.Parse(args); err != nil {
return b, err
}
defer cmd.ResetFlags()

c := make(chan error, 1)
go func() {
c <- cmd.Action(&b)
}()

select {
case err = <-c:
if err != nil {
return b, err
}
case <-time.After(500 * time.Millisecond):
return b, errors.New("command timed out")
}
return b, nil
}

func checkCache(a *argumentCompleter) ([]prompt.Suggest, bool) {
a.RLock()
defer a.RUnlock()
if time.Now().Before(a.expiration) {
return a.suggestions, true
}
return nil, false
}

func cacheSuggestions(a *argumentCompleter, suggests []prompt.Suggest) {
a.Lock()
a.suggestions = suggests
a.expiration = time.Now().Add(time.Second * 10)
a.Unlock()
}
11 changes: 7 additions & 4 deletions cmd/reva/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,17 @@ package main
import (
"flag"
"fmt"
"io"
)

// command is the representation to create commands
// Command is the representation to create commands
type command struct {
*flag.FlagSet
Name string
Action func() error
Action func(w ...io.Writer) error
Usage func() string
Description func() string
ResetFlags func()
}

// newCommand creates a new command
Expand All @@ -40,14 +42,15 @@ func newCommand(name string) *command {
Usage: func() string {
return fmt.Sprintf("Usage: %s", name)
},
Action: func() error {
Action: func(w ...io.Writer) error {
fmt.Println("Hello REVA")
return nil
},
Description: func() string {
return "TODO description"
},
FlagSet: fs,
FlagSet: fs,
ResetFlags: func() {},
}
return cmd
}
52 changes: 26 additions & 26 deletions cmd/reva/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,37 +34,17 @@ const (
editorPermission string = "editor"
)

func getConfigFile() string {
user, err := gouser.Current()
if err != nil {
panic(err)
}

return path.Join(user.HomeDir, ".reva.config")
type config struct {
Host string `json:"host"`
}

func getTokenFile() string {
func getConfigFile() string {
user, err := gouser.Current()
if err != nil {
panic(err)
}

return path.Join(user.HomeDir, ".reva-token")
}

func writeToken(token string) {
err := ioutil.WriteFile(getTokenFile(), []byte(token), 0600)
if err != nil {
panic(err)
}
}

func readToken() (string, error) {
data, err := ioutil.ReadFile(getTokenFile())
if err != nil {
return "", err
}
return string(data), nil
return path.Join(user.HomeDir, ".reva.config")
}

func readConfig() (*config, error) {
Expand All @@ -89,8 +69,28 @@ func writeConfig(c *config) error {
return ioutil.WriteFile(getConfigFile(), data, 0600)
}

type config struct {
Host string `json:"host"`
func getTokenFile() string {
user, err := gouser.Current()
if err != nil {
panic(err)
}

return path.Join(user.HomeDir, ".reva-token")
}

func readToken() (string, error) {
data, err := ioutil.ReadFile(getTokenFile())
if err != nil {
return "", err
}
return string(data), nil
}

func writeToken(token string) {
err := ioutil.WriteFile(getTokenFile(), []byte(token), 0600)
if err != nil {
panic(err)
}
}

func read(r *bufio.Reader) (string, error) {
Expand Down
Loading