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:
parent
f474bb6179
commit
ef7b89cc03
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 = '*'
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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
167
cmd/uplinkng/external.go
Normal 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...)...)
|
||||
}
|
97
cmd/uplinkng/external_access.go
Normal file
97
cmd/uplinkng/external_access.go
Normal 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
|
||||
}
|
73
cmd/uplinkng/external_config.go
Normal file
73
cmd/uplinkng/external_config.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
53
cmd/uplinkng/external_project.go
Normal file
53
cmd/uplinkng/external_project.go
Normal 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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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())
|
||||
}
|
||||
|
@ -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 }}
|
||||
}
|
49
cmd/uplinkng/ulext/external.go
Normal file
49
cmd/uplinkng/ulext/external.go
Normal 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 }}
|
||||
}
|
47
cmd/uplinkng/ultest/external.go
Normal file
47
cmd/uplinkng/ultest/external.go
Normal 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")
|
||||
}
|
@ -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)
|
||||
|
@ -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...)...)
|
||||
}
|
Loading…
Reference in New Issue
Block a user