cmd/uplinkng: initial commit with skeleton

Change-Id: I764618cc60c46882955e9b08b72b3c162aa4929f
This commit is contained in:
Jeff Wendling 2021-03-31 11:56:34 -04:00
parent 10372afbe4
commit a1bf9ab6de
23 changed files with 1176 additions and 8 deletions

View File

@ -0,0 +1,68 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"strconv"
"time"
"github.com/zeebo/clingy"
)
// accessPermissions holds flags and provides a Setup method for commands that
// have to modify permissions on access grants.
type accessPermissions struct {
paths []string // paths is the set of path prefixes that the grant will be limited to
readonly bool // implies disallowWrites and disallowDeletes
writeonly bool // implies disallowReads and disallowLists
disallowDeletes bool
disallowLists bool
disallowReads bool
disallowWrites bool
notBefore time.Time
notAfter time.Time
}
func (ap *accessPermissions) Setup(a clingy.Arguments, f clingy.Flags) {
ap.paths = f.New("path", "Path prefix access will be restricted to", []string{},
clingy.Repeated).([]string)
ap.readonly = f.New("readonly", "Implies --disallow-writes and --disallow-deletes", true,
clingy.Transform(strconv.ParseBool)).(bool)
ap.writeonly = f.New("writeonly", "Implies --disallow-reads and --disallow-lists", false,
clingy.Transform(strconv.ParseBool)).(bool)
ap.disallowDeletes = f.New("disallow-deletes", "Disallow deletes with the access", false,
clingy.Transform(strconv.ParseBool)).(bool)
ap.disallowLists = f.New("disallow-lists", "Disallow lists with the access", false,
clingy.Transform(strconv.ParseBool)).(bool)
ap.disallowReads = f.New("disallow-reads", "Disallow reasd with the access", false,
clingy.Transform(strconv.ParseBool)).(bool)
ap.disallowWrites = f.New("disallow-writes", "Disallow writes with the access", false,
clingy.Transform(strconv.ParseBool)).(bool)
ap.notBefore = f.New("not-before",
"Disallow access before this time (e.g. '+2h', '2020-01-02T15:04:05Z0700')",
time.Time{}, clingy.Transform(parseRelativeTime), clingy.Type("relative_time")).(time.Time)
ap.notAfter = f.New("not-after",
"Disallow access after this time (e.g. '+2h', '2020-01-02T15:04:05Z0700')",
time.Time{}, clingy.Transform(parseRelativeTime), clingy.Type("relative_time")).(time.Time)
}
func parseRelativeTime(v string) (time.Time, error) {
if len(v) == 0 {
return time.Time{}, nil
} else if v[0] == '+' || v[0] == '-' {
d, err := time.ParseDuration(v)
if err != nil {
return time.Time{}, err
}
return time.Now().Add(d), nil
} else {
return time.Parse(time.RFC3339, v)
}
}

View File

@ -0,0 +1,32 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"strconv"
"github.com/zeebo/clingy"
)
type cmdAccessCreate struct {
accessPermissions
token string
passphrase string
name string
save bool
}
func (c *cmdAccessCreate) Setup(a clingy.Arguments, f clingy.Flags) {
c.token = f.New("token", "Setup token from satellite UI (prompted if unspecified)", "").(string)
c.passphrase = f.New("passphrase", "Passphrase used for encryption (prompted if unspecified)", "").(string)
c.name = f.New("name", "Name to save newly created access, if --save is true", "default").(string)
c.save = f.New("save", "Save the access", true, clingy.Transform(strconv.ParseBool)).(bool)
c.accessPermissions.Setup(a, f)
}
func (c *cmdAccessCreate) Execute(ctx clingy.Context) error {
return nil
}

View File

@ -0,0 +1,32 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
)
type cmdAccessDelete struct {
name string
}
func (c *cmdAccessDelete) Setup(a clingy.Arguments, f clingy.Flags) {
c.name = a.New("name", "Access to delete").(string)
}
func (c *cmdAccessDelete) Execute(ctx clingy.Context) error {
accessDefault, accesses, err := gf.GetAccessInfo()
if err != nil {
return err
}
if c.name == accessDefault {
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)
}

View File

@ -0,0 +1,73 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"fmt"
"sort"
"strconv"
"strings"
"text/tabwriter"
"github.com/zeebo/clingy"
"storj.io/uplink"
)
type cmdAccessList struct {
verbose bool
}
func (c *cmdAccessList) Setup(a clingy.Arguments, f clingy.Flags) {
c.verbose = f.New("verbose", "Verbose output of accesses", false,
clingy.Short('v'),
clingy.Transform(strconv.ParseBool),
).(bool)
}
func (c *cmdAccessList) Execute(ctx clingy.Context) error {
accessDefault, accesses, err := gf.GetAccessInfo()
if err != nil {
return err
}
tw := tabwriter.NewWriter(ctx.Stdout(), 4, 4, 4, ' ', 0)
defer func() { _ = tw.Flush() }()
if c.verbose {
fmt.Fprintln(tw, "CURRENT\tNAME\tSATELLITE\tVALUE")
} else {
fmt.Fprintln(tw, "CURRENT\tNAME\tSATELLITE")
}
var names []string
for name := range accesses {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
access, err := uplink.ParseAccess(accesses[name])
if err != nil {
return err
}
address := access.SatelliteAddress()
if idx := strings.IndexByte(address, '@'); !c.verbose && idx >= 0 {
address = address[idx+1:]
}
inUse := ' '
if name == accessDefault {
inUse = '*'
}
if c.verbose {
fmt.Fprintf(tw, "%c\t%s\t%s\t%s\n", inUse, name, address, accesses[name])
} else {
fmt.Fprintf(tw, "%c\t%s\t%s\n", inUse, name, address)
}
}
return nil
}

View File

@ -0,0 +1,16 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import "github.com/zeebo/clingy"
type cmdAccessRevoke struct {
}
func (c *cmdAccessRevoke) Setup(a clingy.Arguments, f clingy.Flags) {
}
func (c *cmdAccessRevoke) Execute(ctx clingy.Context) error {
return nil
}

View File

@ -0,0 +1,61 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"strconv"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/uplink"
)
type cmdAccessSave struct {
access string
name string
force bool
use bool
}
func (c *cmdAccessSave) Setup(a clingy.Arguments, f clingy.Flags) {
c.access = f.New("access", "Access to save (prompted if unspecified)", "").(string)
c.name = f.New("name", "Name to save the access grant under", "default").(string)
c.force = f.New("force", "Force overwrite an existing saved access grant", false,
clingy.Short('f'),
clingy.Transform(strconv.ParseBool),
).(bool)
c.use = f.New("use", "Set the saved access to be the one used by default", false,
clingy.Transform(strconv.ParseBool),
).(bool)
}
func (c *cmdAccessSave) Execute(ctx clingy.Context) error {
// TODO(jeff): need to distinguish errors from reading the file from
// errors due to the file not existing. otherwise, there's no way
// to save the first access.
accessDefault, accesses, err := gf.GetAccessInfo()
if err != nil {
return err
}
if c.access == "" {
return errs.New("TODO: implement prompting for the access")
}
if _, err := uplink.ParseAccess(c.access); err != nil {
return err
}
if _, ok := accesses[c.name]; ok && !c.force {
return errs.New("Access %q already exists. Overwrite by specifying --force", c.name)
}
accesses[c.name] = c.access
if c.use {
accessDefault = c.name
}
return gf.SaveAccessInfo(accessDefault, accesses)
}

View File

@ -0,0 +1,28 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
)
type cmdAccessUse struct {
name string
}
func (c *cmdAccessUse) Setup(a clingy.Arguments, f clingy.Flags) {
c.name = a.New("name", "Access to use").(string)
}
func (c *cmdAccessUse) Execute(ctx clingy.Context) error {
_, accesses, err := gf.GetAccessInfo()
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)
}

34
cmd/uplinkng/cmd_cp.go Normal file
View File

@ -0,0 +1,34 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"strconv"
"github.com/zeebo/clingy"
)
type cmdCp struct {
projectProvider
recursive bool
source string
dest string
}
func (c *cmdCp) Setup(a clingy.Arguments, f clingy.Flags) {
c.projectProvider.Setup(a, f)
c.recursive = f.New("recursive", "Peform a recursive copy", false,
clingy.Short('r'),
clingy.Transform(strconv.ParseBool),
).(bool)
c.source = a.New("source", "Source to copy").(string)
c.dest = a.New("dest", "Desination to copy").(string)
}
func (c *cmdCp) Execute(ctx clingy.Context) error {
return nil
}

61
cmd/uplinkng/cmd_ls.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"fmt"
"strconv"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
)
type cmdLs struct {
projectProvider
recursive bool
encrypted bool
path *string
}
func (c *cmdLs) Setup(a clingy.Arguments, f clingy.Flags) {
c.projectProvider.Setup(a, f)
c.recursive = f.New("recursive", "List recursively", false,
clingy.Short('r'),
clingy.Transform(strconv.ParseBool),
).(bool)
c.encrypted = f.New("encrypted", "Shows paths as base64-encoded encrypted paths", false,
clingy.Transform(strconv.ParseBool),
).(bool)
c.path = a.New("path", "Path to list (sj://BUCKET[/KEY])", clingy.Optional).(*string)
}
func (c *cmdLs) Execute(ctx clingy.Context) error {
if c.path == nil {
return c.listBuckets(ctx)
}
return c.listPath(ctx, *c.path)
}
func (c *cmdLs) listBuckets(ctx clingy.Context) error {
project, err := c.OpenProject(ctx)
if err != nil {
return err
}
defer func() { _ = project.Close() }()
iter := project.ListBuckets(ctx, nil)
for iter.Next() {
item := iter.Item()
fmt.Fprintln(ctx, "BKT", item.Created.Local().Format("2006-01-02 15:04:05"), item.Name)
}
return iter.Err()
}
func (c *cmdLs) listPath(ctx clingy.Context, path string) error {
return errs.New("TODO")
}

24
cmd/uplinkng/cmd_mb.go Normal file
View File

@ -0,0 +1,24 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"github.com/zeebo/clingy"
)
type cmdMb struct {
projectProvider
name string
}
func (c *cmdMb) Setup(a clingy.Arguments, f clingy.Flags) {
c.projectProvider.Setup(a, f)
c.name = a.New("name", "Bucket name (sj://BUCKET)").(string)
}
func (c *cmdMb) Execute(ctx clingy.Context) error {
return nil
}

View File

@ -0,0 +1,26 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"github.com/zeebo/clingy"
)
type cmdMetaGet struct {
projectProvider
path string
entry *string
}
func (c *cmdMetaGet) Setup(a clingy.Arguments, f clingy.Flags) {
c.projectProvider.Setup(a, f)
c.path = a.New("path", "Path to object (sj://BUCKET/KEY)").(string)
c.entry = a.New("entry", "Metadata entry to get", clingy.Optional).(*string)
}
func (c *cmdMetaGet) Execute(ctx clingy.Context) error {
return nil
}

32
cmd/uplinkng/cmd_rb.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"strconv"
"github.com/zeebo/clingy"
)
type cmdRb struct {
projectProvider
force bool
name string
}
func (c *cmdRb) Setup(a clingy.Arguments, f clingy.Flags) {
c.projectProvider.Setup(a, f)
c.force = f.New("force", "Deletes any objects in bucket first", false,
clingy.Transform(strconv.ParseBool),
).(bool)
c.name = a.New("name", "Bucket name (sj://BUCKET)").(string)
}
func (c *cmdRb) Execute(ctx clingy.Context) error {
return nil
}

37
cmd/uplinkng/cmd_rm.go Normal file
View File

@ -0,0 +1,37 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"strconv"
"github.com/zeebo/clingy"
)
type cmdRm struct {
projectProvider
recursive bool
encrypted bool
path string
}
func (c *cmdRm) Setup(a clingy.Arguments, f clingy.Flags) {
c.projectProvider.Setup(a, f)
c.recursive = f.New("recursive", "List recursively", false,
clingy.Short('r'),
clingy.Transform(strconv.ParseBool),
).(bool)
c.encrypted = f.New("encrypted", "Shows paths as base64-encoded encrypted paths", false,
clingy.Transform(strconv.ParseBool),
).(bool)
c.path = a.New("path", "Path to remove (sj://BUCKET[/KEY])").(string)
}
func (c *cmdRm) Execute(ctx clingy.Context) error {
return nil
}

18
cmd/uplinkng/cmd_share.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import "github.com/zeebo/clingy"
type cmdShare struct {
projectProvider
}
func (c *cmdShare) Setup(a clingy.Arguments, f clingy.Flags) {
c.projectProvider.Setup(a, f)
}
func (c *cmdShare) Execute(ctx clingy.Context) error {
return nil
}

View File

@ -0,0 +1,45 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"fmt"
"runtime/debug"
"strconv"
"strings"
"text/tabwriter"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
)
type cmdVersion struct {
verbose bool
}
func (c *cmdVersion) Setup(a clingy.Arguments, f clingy.Flags) {
c.verbose = f.New(
"verbose", "prints all dependency versions", false,
clingy.Short('v'),
clingy.Transform(strconv.ParseBool)).(bool)
}
func (c *cmdVersion) Execute(ctx clingy.Context) error {
bi, ok := debug.ReadBuildInfo()
if !ok {
return errs.New("unable to read build info")
}
tw := tabwriter.NewWriter(ctx.Stdout(), 4, 4, 4, ' ', 0)
defer func() { _ = tw.Flush() }()
fmt.Fprintf(tw, "%s\t%s\n", bi.Main.Path, bi.Main.Version)
for _, mod := range bi.Deps {
if c.verbose || strings.HasPrefix(mod.Path, "storj.io/") {
fmt.Fprintf(tw, " %s\t%s\n", mod.Path, mod.Version)
}
}
return nil
}

View File

@ -0,0 +1,203 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"encoding/json"
"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.New(
"interactive", "Controls if interactive input is allowed", true,
clingy.Transform(strconv.ParseBool),
clingy.Advanced,
).(bool)
g.configDir = f.New(
"config-dir", "Directory that stores the configuration", appDir(false, "storj", "uplink"),
).(string)
g.oldConfigDir = f.New(
"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() (string, map[string]string, error) {
if !g.accessesLoaded {
if err := g.loadAccesses(); err != nil {
return "", nil, err
}
if !g.accessesLoaded {
return "", nil, errs.New("must configure accesses")
}
}
// 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.Cmd) error {
if err := g.migrate(); err != nil {
return err
}
if !g.configLoaded {
// TODO(jeff): prompt for initial config setup
_ = false
}
return cmd.Execute(ctx)
}

View File

@ -0,0 +1,170 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"io"
"os"
"strings"
"github.com/zeebo/errs"
"github.com/zeebo/ini"
"gopkg.in/yaml.v3"
)
// 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
}
g.migrated = true
// if the config file exists, there is no need to migrate
if _, err := os.Stat(g.configFile()); err == nil {
return nil
}
// if the old config file does not exist, we cannot migrate
oldFh, err := os.Open(g.oldConfigFile())
if err != nil {
return nil
}
defer func() { _ = oldFh.Close() }()
// load the information necessary to write the new config from
// the old file.
access, accesses, entries, err := g.loadOldConfig(oldFh)
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 {
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 {
return errs.Wrap(err)
}
// now, write out the config file from the stored entries.
if err := g.saveConfig(entries); err != nil {
return errs.Wrap(err)
}
// migration complete!
return nil
}
// loadOldConfig 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) {
access := ""
accesses := make(map[string]string)
entries := make([]ini.Entry, 0)
// load the old config if possible and write out a new config
var node yaml.Node
if err := yaml.NewDecoder(r).Decode(&node); err != nil {
return "", nil, nil, errs.Wrap(err)
}
// walking a yaml node is unfortunately recursive, so we have to do this
// predeclaration trick to do a recursive inline function.
var walk func(*yaml.Node, []string) error
walk = func(node *yaml.Node, stack []string) error {
if node.Kind != yaml.MappingNode {
return errs.New("unexpected non-map node in yaml document")
} else if len(node.Content)%2 != 0 {
return errs.New("map has odd number of content entries in yaml document")
}
section := strings.Join(stack, ".")
// walk the map entries in pairs. the first entry is the key, and the second is
// the value.
for i := 0; i < len(node.Content); i += 2 {
keyn, valuen := node.Content[i], node.Content[i+1]
key, value := keyn.Value, valuen.Value
// we don't support key kinds other than scalar. yaml may not either. shrug.
if keyn.Kind != yaml.ScalarNode {
return errs.New("map has non-scalar key type")
}
switch valuen.Kind {
case yaml.ScalarNode:
// we want to intercept the access and accesses values from the config
// because they go into a separate file now. check for keys that match
// one of those and stuff them away outside of entries.
if key == "access" {
access = key
} else if strings.HasPrefix(key, "accesses.") {
accesses[key[len("accesses."):]] = value
} else if section == "accesses" {
accesses[key] = value
} else {
entries = append(entries, ini.Entry{
Key: key,
Value: value,
Section: section,
})
}
case yaml.MappingNode:
if err := walk(valuen, append(stack, key)); err != nil {
return err
}
default:
return errs.New("yaml map contains non-scalar or map content entry")
}
}
return nil
}
if node.Kind != yaml.DocumentNode {
return "", nil, nil, errs.New("yaml root node is not document")
}
if len(node.Content) != 1 || node.Content[0].Kind != yaml.MappingNode {
return "", nil, nil, errs.New("yaml root node does not contain a single map")
}
if err := walk(node.Content[0], nil); err != nil {
return "", nil, nil, err
}
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.
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
}

57
cmd/uplinkng/main.go Normal file
View File

@ -0,0 +1,57 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"context"
"flag"
"fmt"
"os"
"github.com/zeebo/clingy"
)
var gf = newGlobalFlags()
func main() {
env := clingy.Environment{
Name: "uplink",
Args: os.Args[1:],
Dynamic: gf.Dynamic,
Wrap: gf.Wrap,
}
ok, err := env.Run(context.Background(), func(c clingy.Commands, f clingy.Flags) {
// setup the dynamic global flags first so that they may be consulted
// by the stdlib flags during their definition.
gf.Setup(f)
newStdlibFlags(flag.CommandLine).Setup(f)
c.Group("access", "Access related commands", func() {
c.New("save", "Save an existing access", new(cmdAccessSave))
c.New("create", "Create an access from a setup token", new(cmdAccessCreate))
c.New("delete", "Delete an access from local store", new(cmdAccessDelete))
c.New("list", "List saved accesses", new(cmdAccessList))
c.New("use", "Set default access to use", new(cmdAccessUse))
c.New("revoke", "Revoke an access", new(cmdAccessRevoke))
})
c.New("share", "Shares restricted accesses to objects", new(cmdShare))
c.New("mb", "Create a new bucket", new(cmdMb))
c.New("rb", "Remove a bucket bucket", new(cmdRb))
c.New("cp", "Copies files or objects into or out of tardigrade", new(cmdCp))
c.New("ls", "Lists buckets, prefixes, or objects", new(cmdLs))
c.New("rm", "Remove an object", new(cmdRm))
c.Group("meta", "Object metadata related commands", func() {
c.New("get", "Get an object's metadata", new(cmdMetaGet))
})
c.New("version", "Prints version information", new(cmdVersion))
})
if err != nil {
fmt.Fprintf(os.Stderr, "%+v\n", err)
}
if !ok || err != nil {
os.Exit(1)
}
}

View File

@ -0,0 +1,46 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"context"
"github.com/zeebo/clingy"
"storj.io/uplink"
)
type projectProvider struct {
access string
openProject func(ctx context.Context) (*uplink.Project, error)
}
func (pp *projectProvider) Setup(a clingy.Arguments, f clingy.Flags) {
pp.access = f.New("access", "Which access to use", "").(string)
}
func (pp *projectProvider) OpenProject(ctx context.Context) (*uplink.Project, error) {
if pp.openProject != nil {
return pp.openProject(ctx)
}
accessDefault, accesses, err := gf.GetAccessInfo()
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)
}
if err != nil {
return nil, err
}
return uplink.OpenProject(ctx, access)
}

View File

@ -0,0 +1,36 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"flag"
"github.com/zeebo/clingy"
)
type stdlibFlags struct {
fs *flag.FlagSet
}
func newStdlibFlags(fs *flag.FlagSet) *stdlibFlags {
return &stdlibFlags{
fs: fs,
}
}
func (s *stdlibFlags) Setup(f clingy.Flags) {
// we use the Transform function to store the value as a side
// effect so that we can return an error if one occurs through
// the expected clingy pipeline.
s.fs.VisitAll(func(fl *flag.Flag) {
name, _ := flag.UnquoteUsage(fl)
f.New(fl.Name, fl.Usage, fl.DefValue,
clingy.Advanced,
clingy.Type(name),
clingy.Transform(func(val string) (string, error) {
return "", fl.Value.Set(val)
}),
)
})
}

57
cmd/uplinkng/utils.go Normal file
View File

@ -0,0 +1,57 @@
// 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...)...)
}

7
go.mod
View File

@ -27,6 +27,7 @@ require (
github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1
github.com/pkg/errors v0.9.1 // indirect
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect
github.com/shopspring/decimal v1.2.0
github.com/spacemonkeygo/monkit/v3 v3.0.10
@ -38,8 +39,11 @@ require (
github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
github.com/zeebo/assert v1.3.0
github.com/zeebo/clingy v0.0.0-20210406153335-0504f579bda1
github.com/zeebo/errs v1.2.2
github.com/zeebo/ini v0.0.0-20210331155437-86af75b4f524
go.etcd.io/bbolt v1.3.5
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0
golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
@ -48,9 +52,10 @@ require (
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
google.golang.org/api v0.20.0 // indirect
gopkg.in/segmentio/analytics-go.v3 v3.1.0
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
storj.io/common v0.0.0-20210504141454-bcb03a80052f
storj.io/drpc v0.0.20
storj.io/monkit-jaeger v0.0.0-20210426161729-debb1cbcbbd7
storj.io/private v0.0.0-20210511083637-239fca6e9894
storj.io/uplink v1.5.0-rc.1.0.20210506124440-cfeb286eeeb9
storj.io/uplink v1.5.0-rc.1.0.20210512164354-e2e5889614a9
)

21
go.sum
View File

@ -386,8 +386,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9
github.com/pelletier/go-toml v1.9.0 h1:NOd0BRdOKpPf0SxkL3HxSQOG7rNh+4kl6PHcBPFs7Q0=
github.com/pelletier/go-toml v1.9.0/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -518,13 +519,19 @@ github.com/zeebo/assert v0.0.0-20181109011804-10f827ce2ed6/go.mod h1:yssERNPivll
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/clingy v0.0.0-20210406153335-0504f579bda1 h1:fxciFdO44MX0vCX58l+BQUi3jXw5CJu4uytNwvmS9r4=
github.com/zeebo/clingy v0.0.0-20210406153335-0504f579bda1/go.mod h1:8+xc/32PGdlAA4nzjz2Bgb3Rf/LAN8/KGrXxIfnlPmw=
github.com/zeebo/errs v1.1.1/go.mod h1:Yj8dHrUQwls1bF3dr/vcSIu+qf4mI7idnTcHfoACc6I=
github.com/zeebo/errs v1.2.2 h1:5NFypMTuSdoySVTqlNs1dEoU21QVamMQJxW/Fii5O7g=
github.com/zeebo/errs v1.2.2/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/errs/v2 v2.0.3 h1:WwqAmopgot4ZC+CgIveP+H91Nf78NDEGWjtAXen45Hw=
github.com/zeebo/errs/v2 v2.0.3/go.mod h1:OKmvVZt4UqpyJrYFykDKm168ZquJ55pbbIVUICNmLN0=
github.com/zeebo/float16 v0.1.0 h1:kRqxv5og6z1emEyz5FpW0/BVHe5VfxEAw6b1ljCZlUc=
github.com/zeebo/float16 v0.1.0/go.mod h1:fssGvvXu+XS8MH57cKmyrLB/cqioYeYX/2mXCN3a5wo=
github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54 h1:+cwNE5KJ3pika4HuzmDHkDlK5myo0G9Sv+eO7WWxnUQ=
github.com/zeebo/incenc v0.0.0-20180505221441-0d92902eec54/go.mod h1:EI8LcOBDlSL3POyqwC1eJhOYlMBMidES+613EtmmT5w=
github.com/zeebo/ini v0.0.0-20210331155437-86af75b4f524 h1:B+9mpufIVeXdXTCnW11CwWb/dYBJ53N4ew829SODXF0=
github.com/zeebo/ini v0.0.0-20210331155437-86af75b4f524/go.mod h1:oiTrvEJ3c6v+Kpfz1tun0BO+EuR3eKdH4tF+WvEbjw8=
github.com/zeebo/structs v1.0.2 h1:kvcd7s2LqXuO9cdV5LqrGHCOAfCBXaZpKCA3jD9SJIc=
github.com/zeebo/structs v1.0.2/go.mod h1:LphfpprlqJQcbCq+eA3iIK/NsejMwk9mlfH/tM1XuKQ=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
@ -546,12 +553,13 @@ go.opentelemetry.io/otel/trace v0.18.0 h1:ilCfc/fptVKaDMK1vWk0elxpolurJbEgey9J6g
go.opentelemetry.io/otel/trace v0.18.0/go.mod h1:FzdUu3BPwZSZebfQ1vl5/tAa8LyMLXSJN57AXIt/iDk=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
@ -841,7 +849,6 @@ sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2
sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0=
storj.io/common v0.0.0-20200424175742-65ac59022f4f/go.mod h1:pZyXiIE7bGETIRXtfs0nICqMwp7PM8HqnDuyUeldNA0=
storj.io/common v0.0.0-20201026135900-1aaeec90670b/go.mod h1:GqdmNf3fLm2UZX/7Zr0BLFCJ4gFjgm6eHrk/fnmr5jQ=
storj.io/common v0.0.0-20210429174118-60091ebbbdaf/go.mod h1:PdP3eTld9RqSV3E4K44JSlw7Z/zNsymj9rnKuHFKhJE=
storj.io/common v0.0.0-20210504141454-bcb03a80052f h1:TwWEzxjvhnkCKUGds4HQKtAgFBjzf/C0hcA1luiNuKI=
storj.io/common v0.0.0-20210504141454-bcb03a80052f/go.mod h1:PdP3eTld9RqSV3E4K44JSlw7Z/zNsymj9rnKuHFKhJE=
storj.io/drpc v0.0.11/go.mod h1:TiFc2obNjL9/3isMW1Rpxjy8V9uE0B2HMeMFGiiI7Iw=
@ -853,5 +860,5 @@ storj.io/monkit-jaeger v0.0.0-20210426161729-debb1cbcbbd7 h1:zi0w9zoBfvuqysSAqxJ
storj.io/monkit-jaeger v0.0.0-20210426161729-debb1cbcbbd7/go.mod h1:gj4vuCeyCRjRmH8LIrgoyU9Dc9uR6H+/GcDUXmTbf80=
storj.io/private v0.0.0-20210511083637-239fca6e9894 h1:ANILx94AKXmvXAf+hs0HMb85Qi2Y4k7RjEb9S7OhK+M=
storj.io/private v0.0.0-20210511083637-239fca6e9894/go.mod h1:iAc+LGwXYCe+YRRTlkfkg95ZBEL8pWHLVZ508/KQjOs=
storj.io/uplink v1.5.0-rc.1.0.20210506124440-cfeb286eeeb9 h1:rSP8cSfLqkYtRLUlGkwj1CeafNf7YPRgTstgwHu0tC8=
storj.io/uplink v1.5.0-rc.1.0.20210506124440-cfeb286eeeb9/go.mod h1:VzJd+P1sfcVnGCxm0mPPhOBkbov0gLZ+/QXeKkkZ1tI=
storj.io/uplink v1.5.0-rc.1.0.20210512164354-e2e5889614a9 h1:F+A+Ki4eo3uzYXxesihRBq7PYBhU8MgfZeebd4O8hio=
storj.io/uplink v1.5.0-rc.1.0.20210512164354-e2e5889614a9/go.mod h1:geRW2dh4rvPhgruFZbN71LSYkMmCJLpwg0y8K/uLr3Y=