cmd/uplinkng: remove global flags

this changes globalFlags to be a ulext.External
interface value that is passed to each command.

rather than have the ulext.External have a Setup
call in the way that the projectProvider used to
we make all of the state arguments to the functions
and have the commands call setup themselves.

the reason it is in its own package is so that
cmd/uplinkng can import cmd/uplinkng/ultest
but cmd/uplinkng/ultest needs to refer to whatever
the interface type is to call the function that
creates the commands.

there's also quite a bit of shuffling around of
code and names. sorry if that makes it tricky
to review. there should be no logic changes, though.

a side benefit is there's no longer a need to do
a type assertion in ultest to make it set the
fake filesystem to use. that can be passed in
directly now. additionally, this makes the
access commands much easier to test.

Change-Id: I29cf6a2144248a58b7a605a7ae0a5ada5cfd57b6
This commit is contained in:
Jeff Wendling 2021-05-26 16:19:29 -04:00
parent f474bb6179
commit ef7b89cc03
26 changed files with 673 additions and 480 deletions

View File

@ -7,9 +7,13 @@ import (
"strconv"
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulext"
)
type cmdAccessCreate struct {
ex ulext.External
accessPermissions
token string
@ -18,6 +22,10 @@ type cmdAccessCreate struct {
save bool
}
func newCmdAccessCreate(ex ulext.External) *cmdAccessCreate {
return &cmdAccessCreate{ex: ex}
}
func (c *cmdAccessCreate) Setup(params clingy.Parameters) {
c.token = params.Flag("token", "Setup token from satellite UI (prompted if unspecified)", "").(string)
c.passphrase = params.Flag("passphrase", "Passphrase used for encryption (prompted if unspecified)", "").(string)

View File

@ -6,27 +6,35 @@ package main
import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
)
type cmdAccessDelete struct {
ex ulext.External
name string
}
func newCmdAccessDelete(ex ulext.External) *cmdAccessDelete {
return &cmdAccessDelete{ex: ex}
}
func (c *cmdAccessDelete) Setup(params clingy.Parameters) {
c.name = params.Arg("name", "Access to delete").(string)
}
func (c *cmdAccessDelete) Execute(ctx clingy.Context) error {
accessDefault, accesses, err := gf.GetAccessInfo(true)
defaultName, accesses, err := c.ex.GetAccessInfo(true)
if err != nil {
return err
}
if c.name == accessDefault {
if c.name == defaultName {
return errs.New("cannot delete current access")
}
if _, ok := accesses[c.name]; !ok {
return errs.New("unknown access: %q", c.name)
}
delete(accesses, c.name)
return gf.SaveAccessInfo(accessDefault, accesses)
return c.ex.SaveAccessInfo(defaultName, accesses)
}

View File

@ -10,13 +10,20 @@ import (
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/uplink"
)
type cmdAccessList struct {
ex ulext.External
verbose bool
}
func newCmdAccessList(ex ulext.External) *cmdAccessList {
return &cmdAccessList{ex: ex}
}
func (c *cmdAccessList) Setup(params clingy.Parameters) {
c.verbose = params.Flag("verbose", "Verbose output of accesses", false,
clingy.Short('v'),
@ -25,7 +32,7 @@ func (c *cmdAccessList) Setup(params clingy.Parameters) {
}
func (c *cmdAccessList) Execute(ctx clingy.Context) error {
accessDefault, accesses, err := gf.GetAccessInfo(true)
defaultName, accesses, err := c.ex.GetAccessInfo(true)
if err != nil {
return err
}
@ -55,7 +62,7 @@ func (c *cmdAccessList) Execute(ctx clingy.Context) error {
}
inUse := ' '
if name == accessDefault {
if name == defaultName {
inUse = '*'
}

View File

@ -3,9 +3,18 @@
package main
import "github.com/zeebo/clingy"
import (
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulext"
)
type cmdAccessRevoke struct {
ex ulext.External
}
func newCmdAccessRevoke(ex ulext.External) *cmdAccessRevoke {
return &cmdAccessRevoke{ex: ex}
}
func (c *cmdAccessRevoke) Setup(params clingy.Parameters) {

View File

@ -9,16 +9,23 @@ import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/uplink"
)
type cmdAccessSave struct {
ex ulext.External
access string
name string
force bool
use bool
}
func newCmdAccessSave(ex ulext.External) *cmdAccessSave {
return &cmdAccessSave{ex: ex}
}
func (c *cmdAccessSave) Setup(params clingy.Parameters) {
c.access = params.Flag("access", "Access to save (prompted if unspecified)", "").(string)
c.name = params.Flag("name", "Name to save the access grant under", "default").(string)
@ -33,17 +40,18 @@ func (c *cmdAccessSave) Setup(params clingy.Parameters) {
}
func (c *cmdAccessSave) Execute(ctx clingy.Context) error {
accessDefault, accesses, err := gf.GetAccessInfo(false)
defaultName, accesses, err := c.ex.GetAccessInfo(false)
if err != nil {
return err
}
if c.access == "" {
c.access, err = gf.PromptInput(ctx, "Access:")
c.access, err = c.ex.PromptInput(ctx, "Access:")
if err != nil {
return err
return errs.Wrap(err)
}
}
if _, err := uplink.ParseAccess(c.access); err != nil {
return err
}
@ -52,9 +60,9 @@ func (c *cmdAccessSave) Execute(ctx clingy.Context) error {
}
accesses[c.name] = c.access
if c.use || accessDefault == "" {
accessDefault = c.name
if c.use || defaultName == "" {
defaultName = c.name
}
return gf.SaveAccessInfo(accessDefault, accesses)
return c.ex.SaveAccessInfo(defaultName, accesses)
}

View File

@ -6,23 +6,31 @@ package main
import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
)
type cmdAccessUse struct {
ex ulext.External
name string
}
func newCmdAccessUse(ex ulext.External) *cmdAccessUse {
return &cmdAccessUse{ex: ex}
}
func (c *cmdAccessUse) Setup(params clingy.Parameters) {
c.name = params.Arg("name", "Access to use").(string)
}
func (c *cmdAccessUse) Execute(ctx clingy.Context) error {
_, accesses, err := gf.GetAccessInfo(true)
_, accesses, err := c.ex.GetAccessInfo(true)
if err != nil {
return err
}
if _, ok := accesses[c.name]; !ok {
return errs.New("unknown access: %q", c.name)
}
return gf.SaveAccessInfo(c.name, accesses)
return c.ex.SaveAccessInfo(c.name, accesses)
}

View File

@ -12,13 +12,15 @@ import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdCp struct {
projectProvider
ex ulext.External
access string
recursive bool
dryrun bool
progress bool
@ -27,9 +29,12 @@ type cmdCp struct {
dest ulloc.Location
}
func (c *cmdCp) Setup(params clingy.Parameters) {
c.projectProvider.Setup(params)
func newCmdCp(ex ulext.External) *cmdCp {
return &cmdCp{ex: ex}
}
func (c *cmdCp) Setup(params clingy.Parameters) {
c.access = params.Flag("access", "Which access to use", "").(string)
c.recursive = params.Flag("recursive", "Peform a recursive copy", false,
clingy.Short('r'),
clingy.Transform(strconv.ParseBool),
@ -46,7 +51,7 @@ func (c *cmdCp) Setup(params clingy.Parameters) {
}
func (c *cmdCp) Execute(ctx clingy.Context) error {
fs, err := c.OpenFilesystem(ctx)
fs, err := c.ex.OpenFilesystem(ctx, c.access)
if err != nil {
return err
}

View File

@ -9,13 +9,15 @@ import (
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdLs struct {
projectProvider
ex ulext.External
access string
recursive bool
encrypted bool
pending bool
@ -24,9 +26,12 @@ type cmdLs struct {
prefix *ulloc.Location
}
func (c *cmdLs) Setup(params clingy.Parameters) {
c.projectProvider.Setup(params)
func newCmdLs(ex ulext.External) *cmdLs {
return &cmdLs{ex: ex}
}
func (c *cmdLs) Setup(params clingy.Parameters) {
c.access = params.Flag("access", "Which access to use", "").(string)
c.recursive = params.Flag("recursive", "List recursively", false,
clingy.Short('r'),
clingy.Transform(strconv.ParseBool),
@ -54,7 +59,7 @@ func (c *cmdLs) Execute(ctx clingy.Context) error {
}
func (c *cmdLs) listBuckets(ctx clingy.Context) error {
project, err := c.OpenProject(ctx)
project, err := c.ex.OpenProject(ctx, c.access)
if err != nil {
return err
}
@ -72,7 +77,7 @@ func (c *cmdLs) listBuckets(ctx clingy.Context) error {
}
func (c *cmdLs) listLocation(ctx clingy.Context, prefix ulloc.Location) error {
fs, err := c.OpenFilesystem(ctx, bypassEncryption(c.encrypted))
fs, err := c.ex.OpenFilesystem(ctx, c.access, ulext.BypassEncryption(c.encrypted))
if err != nil {
return err
}

View File

@ -6,22 +6,30 @@ package main
import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
)
type cmdMb struct {
projectProvider
ex ulext.External
access string
name string
}
func newCmdMb(ex ulext.External) *cmdMb {
return &cmdMb{ex: ex}
}
func (c *cmdMb) Setup(params clingy.Parameters) {
c.projectProvider.Setup(params)
c.access = params.Flag("access", "Which access to use", "").(string)
c.name = params.Arg("name", "Bucket name (sj://BUCKET)").(string)
}
func (c *cmdMb) Execute(ctx clingy.Context) error {
project, err := c.OpenProject(ctx)
project, err := c.ex.OpenProject(ctx, c.access)
if err != nil {
return errs.Wrap(err)
}

View File

@ -11,21 +11,26 @@ import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdMetaGet struct {
projectProvider
ex ulext.External
access string
encrypted bool
location ulloc.Location
entry *string
}
func (c *cmdMetaGet) Setup(params clingy.Parameters) {
c.projectProvider.Setup(params)
func newCmdMetaGet(ex ulext.External) *cmdMetaGet {
return &cmdMetaGet{ex: ex}
}
func (c *cmdMetaGet) Setup(params clingy.Parameters) {
c.access = params.Flag("access", "Which access to use", "").(string)
c.encrypted = params.Flag("encrypted", "Shows keys base64 encoded without decrypting", false,
clingy.Transform(strconv.ParseBool),
).(bool)
@ -37,7 +42,7 @@ func (c *cmdMetaGet) Setup(params clingy.Parameters) {
}
func (c *cmdMetaGet) Execute(ctx clingy.Context) error {
project, err := c.OpenProject(ctx, bypassEncryption(c.encrypted))
project, err := c.ex.OpenProject(ctx, c.access, ulext.BypassEncryption(c.encrypted))
if err != nil {
return err
}

View File

@ -10,20 +10,25 @@ import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdRb struct {
projectProvider
ex ulext.External
force bool
access string
force bool
loc ulloc.Location
}
func (c *cmdRb) Setup(params clingy.Parameters) {
c.projectProvider.Setup(params)
func newCmdRb(ex ulext.External) *cmdRb {
return &cmdRb{ex: ex}
}
func (c *cmdRb) Setup(params clingy.Parameters) {
c.access = params.Flag("access", "Which access to use", "").(string)
c.force = params.Flag("force", "Deletes any objects in bucket first", false,
clingy.Transform(strconv.ParseBool),
).(bool)
@ -34,7 +39,7 @@ func (c *cmdRb) Setup(params clingy.Parameters) {
}
func (c *cmdRb) Execute(ctx clingy.Context) error {
project, err := c.OpenProject(ctx)
project, err := c.ex.OpenProject(ctx, c.access)
if err != nil {
return err
}

View File

@ -10,21 +10,26 @@ import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdRm struct {
projectProvider
ex ulext.External
access string
recursive bool
encrypted bool
location ulloc.Location
}
func (c *cmdRm) Setup(params clingy.Parameters) {
c.projectProvider.Setup(params)
func newCmdRm(ex ulext.External) *cmdRm {
return &cmdRm{ex: ex}
}
func (c *cmdRm) Setup(params clingy.Parameters) {
c.access = params.Flag("access", "Which access to use", "").(string)
c.recursive = params.Flag("recursive", "Remove recursively", false,
clingy.Short('r'),
clingy.Transform(strconv.ParseBool),
@ -39,7 +44,7 @@ func (c *cmdRm) Setup(params clingy.Parameters) {
}
func (c *cmdRm) Execute(ctx clingy.Context) error {
fs, err := c.OpenFilesystem(ctx, bypassEncryption(c.encrypted))
fs, err := c.ex.OpenFilesystem(ctx, c.access, ulext.BypassEncryption(c.encrypted))
if err != nil {
return err
}

View File

@ -3,14 +3,24 @@
package main
import "github.com/zeebo/clingy"
import (
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulext"
)
type cmdShare struct {
projectProvider
ex ulext.External
access string
}
func newCmdShare(ex ulext.External) *cmdShare {
return &cmdShare{ex: ex}
}
func (c *cmdShare) Setup(params clingy.Parameters) {
c.projectProvider.Setup(params)
c.access = params.Flag("access", "Which access to use", "").(string)
}
func (c *cmdShare) Execute(ctx clingy.Context) error {

View File

@ -16,6 +16,10 @@ type cmdVersion struct {
verbose bool
}
func newCmdVersion() *cmdVersion {
return &cmdVersion{}
}
func (c *cmdVersion) Setup(params clingy.Parameters) {
c.verbose = params.Flag(
"verbose", "prints all dependency versions", false,

167
cmd/uplinkng/external.go Normal file
View File

@ -0,0 +1,167 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
)
type external struct {
interactive bool // controls if interactive input is allowed
dirs struct {
loaded bool // true if Setup has been called
current string // current config directory
legacy string // old config directory
}
migration struct {
migrated bool // true if a migration has been attempted
err error // any error from the migration attempt
}
config struct {
loaded bool // true if the existing config file is successfully loaded
values map[string][]string // the existing configuration
}
access struct {
loaded bool // true if we've successfully loaded access.json
defaultName string // default access name to use from accesses
accesses map[string]string // map of all of the stored accesses
}
}
func newExternal() *external {
return &external{}
}
func (ex *external) Setup(f clingy.Flags) {
ex.interactive = f.Flag(
"interactive", "Controls if interactive input is allowed", true,
clingy.Transform(strconv.ParseBool),
clingy.Advanced,
).(bool)
ex.dirs.current = f.Flag(
"config-dir", "Directory that stores the configuration",
appDir(false, "storj", "uplink"),
).(string)
ex.dirs.legacy = f.Flag(
"legacy-config-dir", "Directory that stores legacy configuration. Only used during migration",
appDir(true, "storj", "uplink"),
clingy.Advanced,
).(string)
ex.dirs.loaded = true
}
func (ex *external) accessFile() string { return filepath.Join(ex.dirs.current, "access.json") }
func (ex *external) configFile() string { return filepath.Join(ex.dirs.current, "config.ini") }
func (ex *external) legacyConfigFile() string { return filepath.Join(ex.dirs.legacy, "config.yaml") }
// Dynamic is called by clingy to look up values for global flags not specified on the command
// line. This call lets us fill in values from config files or environment variables.
func (ex *external) Dynamic(name string) (vals []string, err error) {
key := "UPLINK_" + strings.ToUpper(strings.ReplaceAll(name, "-", "_"))
if val, ok := os.LookupEnv(key); ok {
return []string{val}, nil
}
// if we have not yet loaded the directories, we should not try to migrate
// and load the current config.
if !ex.dirs.loaded {
return nil, nil
}
// allow errors from migration and configuration loading so that calls to
// `uplink setup` can happen and write out a new configuration.
if err := ex.migrate(); err != nil {
return nil, nil //nolint
}
if err := ex.loadConfig(); err != nil {
return nil, nil //nolint
}
return ex.config.values[name], nil
}
// Wrap is called by clingy with the command to be executed.
func (ex *external) Wrap(ctx clingy.Context, cmd clingy.Command) error {
if err := ex.migrate(); err != nil {
// TODO(jeff): prompt for initial setup?
return err
}
if !ex.config.loaded {
// TODO(jeff): prompt for initial config setup
_ = false
}
return cmd.Execute(ctx)
}
// PromptInput gets a line of input text from the user and returns an error if
// interactive mode is disabled.
func (ex *external) PromptInput(ctx clingy.Context, prompt string) (input string, err error) {
if !ex.interactive {
return "", errs.New("required user input in non-interactive setting")
}
fmt.Fprint(ctx.Stdout(), prompt, " ")
_, err = fmt.Fscanln(ctx.Stdin(), &input)
return input, err
}
// appDir returns best base directory for the currently running operating system. It
// has a legacy bool to have it return the same values that storj.io/common/fpath.ApplicationDir
// would have returned.
func appDir(legacy bool, subdir ...string) string {
for i := range subdir {
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
subdir[i] = strings.Title(subdir[i])
} else {
subdir[i] = strings.ToLower(subdir[i])
}
}
var appdir string
home := os.Getenv("HOME")
switch runtime.GOOS {
case "windows":
// Windows standards: https://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx?f=255&MSPPError=-2147217396
for _, env := range []string{"AppData", "AppDataLocal", "UserProfile", "Home"} {
val := os.Getenv(env)
if val != "" {
appdir = val
break
}
}
case "darwin":
// Mac standards: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html
appdir = filepath.Join(home, "Library", "Application Support")
case "linux":
fallthrough
default:
// Linux standards: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
if legacy {
appdir = os.Getenv("XDG_DATA_HOME")
if appdir == "" && home != "" {
appdir = filepath.Join(home, ".local", "share")
}
} else {
appdir = os.Getenv("XDG_CONFIG_HOME")
if appdir == "" && home != "" {
appdir = filepath.Join(home, ".config")
}
}
}
return filepath.Join(append([]string{appdir}, subdir...)...)
}

View File

@ -0,0 +1,97 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"encoding/json"
"os"
"github.com/zeebo/errs"
)
func (ex *external) loadAccesses() error {
if ex.access.accesses != nil {
return nil
}
fh, err := os.Open(ex.accessFile())
if os.IsNotExist(err) {
return nil
} else if err != nil {
return errs.Wrap(err)
}
defer func() { _ = fh.Close() }()
var jsonInput struct {
Default string
Accesses map[string]string
}
if err := json.NewDecoder(fh).Decode(&jsonInput); err != nil {
return errs.Wrap(err)
}
ex.access.defaultName = jsonInput.Default
ex.access.accesses = jsonInput.Accesses
ex.access.loaded = true
return nil
}
func (ex *external) GetAccessInfo(required bool) (string, map[string]string, error) {
if !ex.access.loaded {
if err := ex.loadAccesses(); err != nil {
return "", nil, err
}
if required && !ex.access.loaded {
return "", nil, errs.New("No accesses configured. Use 'access save' to create one")
}
}
// return a copy to avoid mutations messing things up
accesses := make(map[string]string)
for name, accessData := range ex.access.accesses {
accesses[name] = accessData
}
return ex.access.defaultName, accesses, nil
}
// SaveAccessInfo writes out the access file using the provided values.
func (ex *external) SaveAccessInfo(defaultName string, accesses map[string]string) error {
// TODO(jeff): write it atomically
accessFh, err := os.OpenFile(ex.accessFile(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return errs.Wrap(err)
}
defer func() { _ = accessFh.Close() }()
var jsonOutput = struct {
Default string
Accesses map[string]string
}{
Default: defaultName,
Accesses: accesses,
}
data, err := json.MarshalIndent(jsonOutput, "", "\t")
if err != nil {
return errs.Wrap(err)
}
if _, err := accessFh.Write(data); err != nil {
return errs.Wrap(err)
}
if err := accessFh.Sync(); err != nil {
return errs.Wrap(err)
}
if err := accessFh.Close(); err != nil {
return errs.Wrap(err)
}
return nil
}

View File

@ -0,0 +1,73 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"os"
"github.com/zeebo/errs"
"github.com/zeebo/ini"
)
// loadConfig loads the configuration file from disk if it is not already loaded.
// This makes calls to loadConfig idempotent.
func (ex *external) loadConfig() error {
if ex.config.values != nil {
return nil
}
ex.config.values = make(map[string][]string)
fh, err := os.Open(ex.configFile())
if os.IsNotExist(err) {
return nil
} else if err != nil {
return errs.Wrap(err)
}
defer func() { _ = fh.Close() }()
err = ini.Read(fh, func(ent ini.Entry) error {
if ent.Section != "" {
ent.Key = ent.Section + "." + ent.Key
}
ex.config.values[ent.Key] = append(ex.config.values[ent.Key], ent.Value)
return nil
})
if err != nil {
return err
}
ex.config.loaded = true
return nil
}
// saveConfig writes out the config file using the provided values.
// It is only intended to be used during initial migration and setup.
func (ex *external) saveConfig(entries []ini.Entry) error {
// TODO(jeff): write it atomically
newFh, err := os.Create(ex.configFile())
if err != nil {
return errs.Wrap(err)
}
defer func() { _ = newFh.Close() }()
err = ini.Write(newFh, func(emit func(ini.Entry)) {
for _, ent := range entries {
emit(ent)
}
})
if err != nil {
return errs.Wrap(err)
}
if err := newFh.Sync(); err != nil {
return errs.Wrap(err)
}
if err := newFh.Close(); err != nil {
return errs.Wrap(err)
}
return nil
}

View File

@ -16,44 +16,47 @@ import (
// migrate attempts to create the config file from the old config file if the
// config file does not exist. It will only attempt to do so at most once
// and so calls to migrate are idempotent.
func (g *globalFlags) migrate() (err error) {
if g.migrated {
return nil
func (ex *external) migrate() (err error) {
if ex.migration.migrated {
return ex.migration.err
}
g.migrated = true
ex.migration.migrated = true
// save any migration error that may have happened
defer func() { ex.migration.err = err }()
// if the config file exists, there is no need to migrate
if _, err := os.Stat(g.configFile()); err == nil {
if _, err := os.Stat(ex.configFile()); err == nil {
return nil
}
// if the old config file does not exist, we cannot migrate
oldFh, err := os.Open(g.oldConfigFile())
legacyFh, err := os.Open(ex.legacyConfigFile())
if err != nil {
return nil
}
defer func() { _ = oldFh.Close() }()
defer func() { _ = legacyFh.Close() }()
// load the information necessary to write the new config from
// the old file.
access, accesses, entries, err := g.loadOldConfig(oldFh)
access, accesses, entries, err := ex.parseLegacyConfig(legacyFh)
if err != nil {
return errs.Wrap(err)
}
// ensure the directory that will hold the config files exists.
if err := os.MkdirAll(g.configDir, 0755); err != nil {
if err := os.MkdirAll(ex.dirs.current, 0755); err != nil {
return errs.Wrap(err)
}
// first, create and write the access file. that way, if there's an error
// creating the config file, we will recreate this file.
if err := g.SaveAccessInfo(access, accesses); err != nil {
if err := ex.SaveAccessInfo(access, accesses); err != nil {
return errs.Wrap(err)
}
// now, write out the config file from the stored entries.
if err := g.saveConfig(entries); err != nil {
if err := ex.saveConfig(entries); err != nil {
return errs.Wrap(err)
}
@ -61,9 +64,9 @@ func (g *globalFlags) migrate() (err error) {
return nil
}
// loadOldConfig loads the default access name, the map of available accesses, and
// parseLegacyConfig loads the default access name, the map of available accesses, and
// a list of config entries from the yaml file in the reader.
func (g *globalFlags) loadOldConfig(r io.Reader) (string, map[string]string, []ini.Entry, error) {
func (ex *external) parseLegacyConfig(r io.Reader) (string, map[string]string, []ini.Entry, error) {
access := ""
accesses := make(map[string]string)
entries := make([]ini.Entry, 0)
@ -141,30 +144,3 @@ func (g *globalFlags) loadOldConfig(r io.Reader) (string, map[string]string, []i
return access, accesses, entries, nil
}
// saveConfig writes out the config file using the provided values.
// It is only intended to be used during initial migration and setup.
func (g *globalFlags) saveConfig(entries []ini.Entry) error {
// TODO(jeff): write it atomically
newFh, err := os.Create(g.configFile())
if err != nil {
return errs.Wrap(err)
}
defer func() { _ = newFh.Close() }()
err = ini.Write(newFh, func(emit func(ini.Entry)) {
for _, ent := range entries {
emit(ent)
}
})
if err != nil {
return errs.Wrap(err)
}
if err := newFh.Close(); err != nil {
return errs.Wrap(err)
}
return nil
}

View File

@ -0,0 +1,53 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"context"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/uplink"
privateAccess "storj.io/uplink/private/access"
)
func (ex *external) OpenFilesystem(ctx context.Context, accessName string, options ...ulext.Option) (ulfs.Filesystem, error) {
project, err := ex.OpenProject(ctx, accessName, options...)
if err != nil {
return nil, err
}
return ulfs.NewMixed(ulfs.NewLocal(), ulfs.NewRemote(project)), nil
}
func (ex *external) OpenProject(ctx context.Context, accessName string, options ...ulext.Option) (*uplink.Project, error) {
opts := ulext.LoadOptions(options...)
accessDefault, accesses, err := ex.GetAccessInfo(true)
if err != nil {
return nil, err
}
if accessName != "" {
accessDefault = accessName
}
var access *uplink.Access
if data, ok := accesses[accessDefault]; ok {
access, err = uplink.ParseAccess(data)
} else {
access, err = uplink.ParseAccess(accessDefault)
// TODO: if this errors then it's probably a name so don't report an error
// that says "it failed to parse"
}
if err != nil {
return nil, err
}
if opts.EncryptionBypass {
if err := privateAccess.EnablePathEncryptionBypass(access); err != nil {
return nil, err
}
}
return uplink.OpenProject(ctx, access)
}

View File

@ -1,215 +0,0 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"github.com/zeebo/ini"
)
type globalFlags struct {
interactive bool
configDir string
oldConfigDir string
setup bool
migrated bool
configLoaded bool
config map[string][]string
accessesLoaded bool
accessDefault string
accesses map[string]string
}
func newGlobalFlags() *globalFlags {
return &globalFlags{}
}
func (g *globalFlags) Setup(f clingy.Flags) {
g.interactive = f.Flag(
"interactive", "Controls if interactive input is allowed", true,
clingy.Transform(strconv.ParseBool),
clingy.Advanced,
).(bool)
g.configDir = f.Flag(
"config-dir", "Directory that stores the configuration",
appDir(false, "storj", "uplink"),
).(string)
g.oldConfigDir = f.Flag(
"old-config-dir", "Directory that stores legacy configuration. Only used during migration",
appDir(true, "storj", "uplink"),
clingy.Advanced,
).(string)
g.setup = true
}
func (g *globalFlags) accessFile() string { return filepath.Join(g.configDir, "access.json") }
func (g *globalFlags) configFile() string { return filepath.Join(g.configDir, "config.ini") }
func (g *globalFlags) oldConfigFile() string { return filepath.Join(g.oldConfigDir, "config.yaml") }
func (g *globalFlags) Dynamic(name string) (vals []string, err error) {
if !g.setup {
return nil, nil
}
if err := g.migrate(); err != nil {
return nil, err
}
if err := g.loadConfig(); err != nil {
return nil, err
}
key := "UPLINK_" + strings.ToUpper(strings.ReplaceAll(name, "-", "_"))
if val, ok := os.LookupEnv(key); ok {
return []string{val}, nil
}
return g.config[name], nil
}
// loadConfig loads the configuration file from disk if it is not already loaded.
// This makes calls to loadConfig idempotent.
func (g *globalFlags) loadConfig() error {
if g.config != nil {
return nil
}
g.config = make(map[string][]string)
fh, err := os.Open(g.configFile())
if os.IsNotExist(err) {
return nil
} else if err != nil {
return errs.Wrap(err)
}
defer func() { _ = fh.Close() }()
err = ini.Read(fh, func(ent ini.Entry) error {
if ent.Section != "" {
ent.Key = ent.Section + "." + ent.Key
}
g.config[ent.Key] = append(g.config[ent.Key], ent.Value)
return nil
})
if err != nil {
return err
}
g.configLoaded = true
return nil
}
func (g *globalFlags) loadAccesses() error {
if g.accesses != nil {
return nil
}
fh, err := os.Open(g.accessFile())
if os.IsNotExist(err) {
return nil
} else if err != nil {
return errs.Wrap(err)
}
defer func() { _ = fh.Close() }()
var jsonInput struct {
Default string
Accesses map[string]string
}
if err := json.NewDecoder(fh).Decode(&jsonInput); err != nil {
return errs.Wrap(err)
}
g.accessDefault = jsonInput.Default
g.accesses = jsonInput.Accesses
g.accessesLoaded = true
return nil
}
func (g *globalFlags) GetAccessInfo(required bool) (string, map[string]string, error) {
if !g.accessesLoaded {
if err := g.loadAccesses(); err != nil {
return "", nil, err
}
if required && !g.accessesLoaded {
return "", nil, errs.New("No accesses configured. Use 'access save' to create one")
}
}
// return a copy to avoid mutations messing things up
accesses := make(map[string]string)
for name, accessData := range g.accesses {
accesses[name] = accessData
}
return g.accessDefault, accesses, nil
}
// SaveAccessInfo writes out the access file using the provided values.
func (g *globalFlags) SaveAccessInfo(accessDefault string, accesses map[string]string) error {
// TODO(jeff): write it atomically
accessFh, err := os.OpenFile(g.accessFile(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return errs.Wrap(err)
}
defer func() { _ = accessFh.Close() }()
var jsonOutput = struct {
Default string
Accesses map[string]string
}{
Default: accessDefault,
Accesses: accesses,
}
data, err := json.MarshalIndent(jsonOutput, "", "\t")
if err != nil {
return errs.Wrap(err)
}
if _, err := accessFh.Write(data); err != nil {
return errs.Wrap(err)
}
if err := accessFh.Sync(); err != nil {
return errs.Wrap(err)
}
if err := accessFh.Close(); err != nil {
return errs.Wrap(err)
}
return nil
}
func (g *globalFlags) Wrap(ctx clingy.Context, cmd clingy.Command) error {
if err := g.migrate(); err != nil {
return err
}
if !g.configLoaded {
// TODO(jeff): prompt for initial config setup
_ = false
}
return cmd.Execute(ctx)
}
func (g *globalFlags) PromptInput(ctx clingy.Context, prompt string) (input string, err error) {
if !g.interactive {
return "", errs.New("required user input in non-interactive setting")
}
fmt.Fprint(ctx.Stdout(), prompt, " ")
_, err = fmt.Fscanln(ctx.Stdin(), &input)
return input, err
}

View File

@ -10,24 +10,22 @@ import (
"os"
"github.com/zeebo/clingy"
_ "storj.io/private/process"
"storj.io/storj/cmd/uplinkng/ulext"
)
var gf = newGlobalFlags()
func main() {
ex := newExternal()
ok, err := clingy.Environment{
Name: "uplink",
Args: os.Args[1:],
Dynamic: gf.Dynamic,
Wrap: gf.Wrap,
Name: "uplink",
Args: os.Args[1:],
Dynamic: ex.Dynamic,
Wrap: ex.Wrap,
}.Run(context.Background(), func(cmds clingy.Commands) {
// setup the dynamic global flags first so that they may be consulted
// by the stdlib flags during their definition.
gf.Setup(cmds)
ex.Setup(cmds) // setup ex first so that stdlib flags can consult config
newStdlibFlags(flag.CommandLine).Setup(cmds)
commands(cmds)
commands(cmds, ex)
})
if err != nil {
fmt.Fprintf(os.Stderr, "%+v\n", err)
@ -37,23 +35,23 @@ func main() {
}
}
func commands(cmds clingy.Commands) {
func commands(cmds clingy.Commands, ex ulext.External) {
cmds.Group("access", "Access related commands", func() {
cmds.New("save", "Save an existing access", new(cmdAccessSave))
cmds.New("create", "Create an access from a setup token", new(cmdAccessCreate))
cmds.New("delete", "Delete an access from local store", new(cmdAccessDelete))
cmds.New("list", "List saved accesses", new(cmdAccessList))
cmds.New("use", "Set default access to use", new(cmdAccessUse))
cmds.New("revoke", "Revoke an access", new(cmdAccessRevoke))
cmds.New("save", "Save an existing access", newCmdAccessSave(ex))
cmds.New("create", "Create an access from a setup token", newCmdAccessCreate(ex))
cmds.New("delete", "Delete an access from local store", newCmdAccessDelete(ex))
cmds.New("list", "List saved accesses", newCmdAccessList(ex))
cmds.New("use", "Set default access to use", newCmdAccessUse(ex))
cmds.New("revoke", "Revoke an access", newCmdAccessRevoke(ex))
})
cmds.New("share", "Shares restricted accesses to objects", new(cmdShare))
cmds.New("mb", "Create a new bucket", new(cmdMb))
cmds.New("rb", "Remove a bucket bucket", new(cmdRb))
cmds.New("cp", "Copies files or objects into or out of tardigrade", new(cmdCp))
cmds.New("ls", "Lists buckets, prefixes, or objects", new(cmdLs))
cmds.New("rm", "Remove an object", new(cmdRm))
cmds.New("share", "Shares restricted accesses to objects", newCmdShare(ex))
cmds.New("mb", "Create a new bucket", newCmdMb(ex))
cmds.New("rb", "Remove a bucket bucket", newCmdRb(ex))
cmds.New("cp", "Copies files or objects into or out of tardigrade", newCmdCp(ex))
cmds.New("ls", "Lists buckets, prefixes, or objects", newCmdLs(ex))
cmds.New("rm", "Remove an object", newCmdRm(ex))
cmds.Group("meta", "Object metadata related commands", func() {
cmds.New("get", "Get an object's metadata", new(cmdMetaGet))
cmds.New("get", "Get an object's metadata", newCmdMetaGet(ex))
})
cmds.New("version", "Prints version information", new(cmdVersion))
cmds.New("version", "Prints version information", newCmdVersion())
}

View File

@ -1,90 +0,0 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"context"
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/uplink"
privateAccess "storj.io/uplink/private/access"
)
type projectProvider struct {
access string
testProject *uplink.Project
testFilesystem ulfs.Filesystem
}
func (pp *projectProvider) Setup(params clingy.Parameters) {
pp.access = params.Flag("access", "Which access to use", "").(string)
}
func (pp *projectProvider) SetTestFilesystem(fs ulfs.Filesystem) { pp.testFilesystem = fs }
func (pp *projectProvider) OpenFilesystem(ctx context.Context, options ...projectOption) (ulfs.Filesystem, error) {
if pp.testFilesystem != nil {
return pp.testFilesystem, nil
}
project, err := pp.OpenProject(ctx, options...)
if err != nil {
return nil, err
}
return ulfs.NewMixed(ulfs.NewLocal(), ulfs.NewRemote(project)), nil
}
func (pp *projectProvider) OpenProject(ctx context.Context, options ...projectOption) (*uplink.Project, error) {
if pp.testProject != nil {
return pp.testProject, nil
}
var opts projectOptions
for _, opt := range options {
opt.apply(&opts)
}
accessDefault, accesses, err := gf.GetAccessInfo(true)
if err != nil {
return nil, err
}
if pp.access != "" {
accessDefault = pp.access
}
var access *uplink.Access
if data, ok := accesses[accessDefault]; ok {
access, err = uplink.ParseAccess(data)
} else {
access, err = uplink.ParseAccess(accessDefault)
// TODO: if this errors then it's probably a name so don't report an error
// that says "it failed to parse"
}
if err != nil {
return nil, err
}
if opts.encryptionBypass {
if err := privateAccess.EnablePathEncryptionBypass(access); err != nil {
return nil, err
}
}
return uplink.OpenProject(ctx, access)
}
type projectOptions struct {
encryptionBypass bool
}
type projectOption struct {
apply func(*projectOptions)
}
func bypassEncryption(bypass bool) projectOption {
return projectOption{apply: func(opt *projectOptions) { opt.encryptionBypass = bypass }}
}

View File

@ -0,0 +1,49 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
// Package ulext provides an interface for the CLI to interface with the external world.
package ulext
import (
"context"
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/uplink"
)
// External is the interface for all of the ways that the uplink command may interact with
// any external state.
type External interface {
OpenFilesystem(ctx context.Context, accessName string, options ...Option) (ulfs.Filesystem, error)
OpenProject(ctx context.Context, accessName string, options ...Option) (*uplink.Project, error)
GetAccessInfo(required bool) (string, map[string]string, error)
SaveAccessInfo(defaultName string, accesses map[string]string) error
PromptInput(ctx clingy.Context, prompt string) (input string, err error)
}
// Options contains all of the possible options for opening a filesystem or project.
type Options struct {
EncryptionBypass bool
}
// LoadOptions takes a slice of Option values and returns a filled out Options struct.
func LoadOptions(options ...Option) (opts Options) {
for _, opt := range options {
opt.apply(&opts)
}
return opts
}
// Option is a single option that controls the Options struct.
type Option struct {
apply func(*Options)
}
// BypassEncryption will disable decrypting of path names if bypass is true.
func BypassEncryption(bypass bool) Option {
return Option{apply: func(opt *Options) { opt.EncryptionBypass = bypass }}
}

View File

@ -0,0 +1,47 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ultest
import (
"context"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/uplink"
)
type external struct {
fs ulfs.Filesystem
project *uplink.Project
}
func newExternal(fs ulfs.Filesystem, project *uplink.Project) *external {
return &external{
fs: fs,
project: project,
}
}
func (ex *external) OpenFilesystem(ctx context.Context, access string, options ...ulext.Option) (ulfs.Filesystem, error) {
return ex.fs, nil
}
func (ex *external) OpenProject(ctx context.Context, access string, options ...ulext.Option) (*uplink.Project, error) {
return ex.project, nil
}
func (ex *external) GetAccessInfo(required bool) (string, map[string]string, error) {
return "", nil, errs.New("not implemented")
}
func (ex *external) SaveAccessInfo(accessDefault string, accesses map[string]string) error {
return errs.New("not implemented")
}
func (ex *external) PromptInput(ctx clingy.Context, prompt string) (input string, err error) {
return "", errs.New("not implemented")
}

View File

@ -11,13 +11,17 @@ import (
"github.com/stretchr/testify/require"
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulext"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
// Commands is an alias to refer to a function that builds clingy commands.
type Commands = func(clingy.Commands, ulext.External)
// Setup returns some State that can be run multiple times with different command
// line arguments.
func Setup(cmds func(clingy.Commands), opts ...ExecuteOption) State {
func Setup(cmds Commands, opts ...ExecuteOption) State {
return State{
cmds: cmds,
opts: opts,
@ -26,7 +30,7 @@ func Setup(cmds func(clingy.Commands), opts ...ExecuteOption) State {
// State represents some state and environment for a command to execute in.
type State struct {
cmds func(clingy.Commands)
cmds Commands
opts []ExecuteOption
}
@ -77,16 +81,12 @@ func (st State) Run(t *testing.T, args ...string) Result {
_, _ = stdin.WriteString(tfs.stdin)
}
if setter, ok := cmd.(interface {
SetTestFilesystem(ulfs.Filesystem)
}); ok {
setter.SetTestFilesystem(tfs)
}
ran = true
return cmd.Execute(ctx)
},
}.Run(context.Background(), st.cmds)
}.Run(context.Background(), func(cmds clingy.Commands) {
st.cmds(cmds, newExternal(tfs, nil))
})
if ok && err == nil {
require.True(t, ran, "no command was executed: %q", args)

View File

@ -1,57 +0,0 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"os"
"path/filepath"
"runtime"
"strings"
)
// appDir returns best base directory for the currently running operating system. It
// has a legacy bool to have it return the same values that storj.io/common/fpath.ApplicationDir
// would have returned.
func appDir(legacy bool, subdir ...string) string {
for i := range subdir {
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
subdir[i] = strings.Title(subdir[i])
} else {
subdir[i] = strings.ToLower(subdir[i])
}
}
var appdir string
home := os.Getenv("HOME")
switch runtime.GOOS {
case "windows":
// Windows standards: https://msdn.microsoft.com/en-us/library/windows/apps/hh465094.aspx?f=255&MSPPError=-2147217396
for _, env := range []string{"AppData", "AppDataLocal", "UserProfile", "Home"} {
val := os.Getenv(env)
if val != "" {
appdir = val
break
}
}
case "darwin":
// Mac standards: https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/MacOSXDirectories/MacOSXDirectories.html
appdir = filepath.Join(home, "Library", "Application Support")
case "linux":
fallthrough
default:
// Linux standards: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
if legacy {
appdir = os.Getenv("XDG_DATA_HOME")
if appdir == "" && home != "" {
appdir = filepath.Join(home, ".local", "share")
}
} else {
appdir = os.Getenv("XDG_CONFIG_HOME")
if appdir == "" && home != "" {
appdir = filepath.Join(home, ".config")
}
}
}
return filepath.Join(append([]string{appdir}, subdir...)...)
}