cmd/uplinkng: access creation/restriction and review fixes
Change-Id: I649ae3615363685c28c39d1efb6a65fcad507f46
This commit is contained in:
parent
dc69e1b16e
commit
e33f8d7170
110
cmd/uplinkng/access_maker.go
Normal file
110
cmd/uplinkng/access_maker.go
Normal file
@ -0,0 +1,110 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/zeebo/clingy"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/storj/cmd/uplinkng/ulext"
|
||||
"storj.io/uplink"
|
||||
)
|
||||
|
||||
type accessMaker struct {
|
||||
ex ulext.External
|
||||
|
||||
print bool
|
||||
save bool
|
||||
name string
|
||||
force bool
|
||||
use bool
|
||||
|
||||
perms accessPermissions
|
||||
}
|
||||
|
||||
func (am *accessMaker) Setup(params clingy.Parameters, ex ulext.External, forceSave bool) {
|
||||
am.ex = ex
|
||||
am.save = forceSave
|
||||
am.print = !forceSave
|
||||
|
||||
if !forceSave {
|
||||
am.save = params.Flag("save", "Save the access", true,
|
||||
clingy.Transform(strconv.ParseBool),
|
||||
).(bool)
|
||||
}
|
||||
|
||||
am.name = params.Flag("name", "Name to save newly created access, if --save is true", "").(string)
|
||||
|
||||
am.force = params.Flag("force", "Force overwrite an existing saved access grant", false,
|
||||
clingy.Short('f'),
|
||||
clingy.Transform(strconv.ParseBool),
|
||||
).(bool)
|
||||
|
||||
am.use = params.Flag("use", "Set the saved access to be the one used by default", false,
|
||||
clingy.Transform(strconv.ParseBool),
|
||||
).(bool)
|
||||
|
||||
if !forceSave {
|
||||
params.Break()
|
||||
am.perms.Setup(params)
|
||||
}
|
||||
}
|
||||
|
||||
func (am *accessMaker) Execute(ctx clingy.Context, access *uplink.Access) (err error) {
|
||||
defaultName, accesses, err := am.ex.GetAccessInfo(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if am.save {
|
||||
// pick a default name for the access if we're saving and there are
|
||||
// no saved accesses. otherwise, prompt.
|
||||
if am.name == "" && len(accesses) == 0 {
|
||||
am.name = "default"
|
||||
}
|
||||
|
||||
if am.name == "" {
|
||||
am.name, err = am.ex.PromptInput(ctx, "Name:")
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := accesses[am.name]; ok && !am.force {
|
||||
return errs.New("Access %q already exists. Overwrite by specifying --force or choose a new name with --name", am.name)
|
||||
}
|
||||
}
|
||||
|
||||
access, err = am.perms.Apply(access)
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
accessValue, err := access.Serialize()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
if am.print {
|
||||
fmt.Fprintln(ctx, accessValue)
|
||||
}
|
||||
|
||||
if am.save {
|
||||
accesses[am.name] = accessValue
|
||||
if am.use || defaultName == "" {
|
||||
defaultName = am.name
|
||||
}
|
||||
|
||||
if err := am.ex.SaveAccessInfo(defaultName, accesses); err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(ctx, "Access %q saved to %q\n", am.name, am.ex.AccessInfoFile())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -8,12 +8,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/zeebo/clingy"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/storj/cmd/uplinkng/ulloc"
|
||||
"storj.io/uplink"
|
||||
)
|
||||
|
||||
// accessPermissions holds flags and provides a Setup method for commands that
|
||||
// have to modify permissions on access grants.
|
||||
type accessPermissions struct {
|
||||
prefixes []string // prefixes is the set of path prefixes that the grant will be limited to
|
||||
prefixes []uplink.SharePrefix // prefixes 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
|
||||
@ -28,10 +32,24 @@ type accessPermissions struct {
|
||||
}
|
||||
|
||||
func (ap *accessPermissions) Setup(params clingy.Parameters) {
|
||||
ap.prefixes = params.Flag("prefix", "Key prefix access will be restricted to", []string{},
|
||||
clingy.Repeated).([]string)
|
||||
transformSharePrefix := func(loc ulloc.Location) (uplink.SharePrefix, error) {
|
||||
bucket, key, ok := loc.RemoteParts()
|
||||
if !ok {
|
||||
return uplink.SharePrefix{}, errs.New("invalid prefix: must be remote: %q", loc)
|
||||
}
|
||||
return uplink.SharePrefix{
|
||||
Bucket: bucket,
|
||||
Prefix: key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
ap.readonly = params.Flag("readonly", "Implies --disallow-writes and --disallow-deletes", true,
|
||||
ap.prefixes = params.Flag("prefix", "Key prefix access will be restricted to", []ulloc.Location{},
|
||||
clingy.Transform(ulloc.Parse),
|
||||
clingy.Transform(transformSharePrefix),
|
||||
clingy.Repeated,
|
||||
).([]uplink.SharePrefix)
|
||||
|
||||
ap.readonly = params.Flag("readonly", "Implies --disallow-writes and --disallow-deletes", false,
|
||||
clingy.Transform(strconv.ParseBool)).(bool)
|
||||
ap.writeonly = params.Flag("writeonly", "Implies --disallow-reads and --disallow-lists", false,
|
||||
clingy.Transform(strconv.ParseBool)).(bool)
|
||||
@ -45,24 +63,44 @@ func (ap *accessPermissions) Setup(params clingy.Parameters) {
|
||||
ap.disallowWrites = params.Flag("disallow-writes", "Disallow writes with the access", false,
|
||||
clingy.Transform(strconv.ParseBool)).(bool)
|
||||
|
||||
now := time.Now()
|
||||
transformHumanDate := clingy.Transform(func(date string) (time.Time, error) {
|
||||
switch {
|
||||
case date == "":
|
||||
return time.Time{}, nil
|
||||
case date == "now":
|
||||
return now, nil
|
||||
case date[0] == '+' || date[0] == '-':
|
||||
d, err := time.ParseDuration(date)
|
||||
return now.Add(d), errs.Wrap(err)
|
||||
default:
|
||||
t, err := time.Parse(time.RFC3339, date)
|
||||
return t, errs.Wrap(err)
|
||||
}
|
||||
})
|
||||
|
||||
ap.notBefore = params.Flag("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)
|
||||
"Disallow access before this time (e.g. '+2h', 'now', '2020-01-02T15:04:05Z0700')",
|
||||
time.Time{}, transformHumanDate, clingy.Type("relative_date")).(time.Time)
|
||||
ap.notAfter = params.Flag("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)
|
||||
"Disallow access after this time (e.g. '+2h', 'now', '2020-01-02T15:04:05Z0700')",
|
||||
time.Time{}, transformHumanDate, clingy.Type("relative_date")).(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)
|
||||
func (ap *accessPermissions) Apply(access *uplink.Access) (*uplink.Access, error) {
|
||||
permission := uplink.Permission{
|
||||
AllowDelete: !ap.disallowDeletes && !ap.readonly,
|
||||
AllowList: !ap.disallowLists && !ap.writeonly,
|
||||
AllowDownload: !ap.disallowReads && !ap.writeonly,
|
||||
AllowUpload: !ap.disallowWrites && !ap.readonly,
|
||||
NotBefore: ap.notBefore,
|
||||
NotAfter: ap.notAfter,
|
||||
}
|
||||
|
||||
access, err := access.Share(permission, ap.prefixes...)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
|
||||
return access, nil
|
||||
}
|
||||
|
@ -4,22 +4,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/zeebo/clingy"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/storj/cmd/uplinkng/ulext"
|
||||
)
|
||||
|
||||
type cmdAccessCreate struct {
|
||||
ex ulext.External
|
||||
|
||||
accessPermissions
|
||||
am accessMaker
|
||||
|
||||
token string
|
||||
passphrase string
|
||||
name string
|
||||
save bool
|
||||
}
|
||||
|
||||
func newCmdAccessCreate(ex ulext.External) *cmdAccessCreate {
|
||||
@ -29,12 +25,31 @@ func newCmdAccessCreate(ex ulext.External) *cmdAccessCreate {
|
||||
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)
|
||||
c.name = params.Flag("name", "Name to save newly created access, if --save is true", "default").(string)
|
||||
c.save = params.Flag("save", "Save the access", true, clingy.Transform(strconv.ParseBool)).(bool)
|
||||
|
||||
c.accessPermissions.Setup(params)
|
||||
params.Break()
|
||||
c.am.Setup(params, c.ex, false)
|
||||
}
|
||||
|
||||
func (c *cmdAccessCreate) Execute(ctx clingy.Context) error {
|
||||
return nil
|
||||
func (c *cmdAccessCreate) Execute(ctx clingy.Context) (err error) {
|
||||
if c.token == "" {
|
||||
c.token, err = c.ex.PromptInput(ctx, "Setup token:")
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.passphrase == "" {
|
||||
// TODO: secret prompt
|
||||
c.passphrase, err = c.ex.PromptInput(ctx, "Passphrase:")
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
}
|
||||
|
||||
access, err := c.ex.RequestAccess(ctx, c.token, c.passphrase)
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
return c.am.Execute(ctx, access)
|
||||
}
|
||||
|
37
cmd/uplinkng/cmd_access_restrict.go
Normal file
37
cmd/uplinkng/cmd_access_restrict.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/zeebo/clingy"
|
||||
|
||||
"storj.io/storj/cmd/uplinkng/ulext"
|
||||
)
|
||||
|
||||
type cmdAccessRestrict struct {
|
||||
ex ulext.External
|
||||
am accessMaker
|
||||
|
||||
access string
|
||||
}
|
||||
|
||||
func newCmdAccessRestrict(ex ulext.External) *cmdAccessRestrict {
|
||||
return &cmdAccessRestrict{ex: ex}
|
||||
}
|
||||
|
||||
func (c *cmdAccessRestrict) Setup(params clingy.Parameters) {
|
||||
c.access = params.Flag("access", "Which access to restrict", "").(string)
|
||||
|
||||
params.Break()
|
||||
c.am.Setup(params, c.ex, false)
|
||||
}
|
||||
|
||||
func (c *cmdAccessRestrict) Execute(ctx clingy.Context) error {
|
||||
access, err := c.ex.OpenAccess(c.access)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.am.Execute(ctx, access)
|
||||
}
|
@ -4,8 +4,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/zeebo/clingy"
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
@ -15,11 +13,9 @@ import (
|
||||
|
||||
type cmdAccessSave struct {
|
||||
ex ulext.External
|
||||
am accessMaker
|
||||
|
||||
access string
|
||||
name string
|
||||
force bool
|
||||
use bool
|
||||
}
|
||||
|
||||
func newCmdAccessSave(ex ulext.External) *cmdAccessSave {
|
||||
@ -27,24 +23,13 @@ func newCmdAccessSave(ex ulext.External) *cmdAccessSave {
|
||||
}
|
||||
|
||||
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)
|
||||
c.access = params.Flag("access", "Access value to save (prompted if unspecified)", "").(string)
|
||||
|
||||
c.force = params.Flag("force", "Force overwrite an existing saved access grant", false,
|
||||
clingy.Short('f'),
|
||||
clingy.Transform(strconv.ParseBool),
|
||||
).(bool)
|
||||
c.use = params.Flag("use", "Set the saved access to be the one used by default", false,
|
||||
clingy.Transform(strconv.ParseBool),
|
||||
).(bool)
|
||||
params.Break()
|
||||
c.am.Setup(params, c.ex, true)
|
||||
}
|
||||
|
||||
func (c *cmdAccessSave) Execute(ctx clingy.Context) error {
|
||||
defaultName, accesses, err := c.ex.GetAccessInfo(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *cmdAccessSave) Execute(ctx clingy.Context) (err error) {
|
||||
if c.access == "" {
|
||||
c.access, err = c.ex.PromptInput(ctx, "Access:")
|
||||
if err != nil {
|
||||
@ -52,17 +37,10 @@ func (c *cmdAccessSave) Execute(ctx clingy.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := uplink.ParseAccess(c.access); err != nil {
|
||||
access, err := uplink.ParseAccess(c.access)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := accesses[c.name]; ok && !c.force {
|
||||
return errs.New("Access %q already exists. Overwrite by specifying --force or choose a new name with --name", c.name)
|
||||
}
|
||||
|
||||
accesses[c.name] = c.access
|
||||
if c.use || defaultName == "" {
|
||||
defaultName = c.name
|
||||
}
|
||||
|
||||
return c.ex.SaveAccessInfo(defaultName, accesses)
|
||||
return c.am.Execute(ctx, access)
|
||||
}
|
||||
|
@ -66,8 +66,8 @@ func (ex *external) Setup(f clingy.Flags) {
|
||||
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) AccessInfoFile() 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
|
||||
|
@ -4,10 +4,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/uplink"
|
||||
)
|
||||
|
||||
func (ex *external) loadAccesses() error {
|
||||
@ -15,7 +19,7 @@ func (ex *external) loadAccesses() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
fh, err := os.Open(ex.accessFile())
|
||||
fh, err := os.Open(ex.AccessInfoFile())
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@ -39,6 +43,29 @@ func (ex *external) loadAccesses() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ex *external) OpenAccess(accessName string) (access *uplink.Access, err error) {
|
||||
accessDefault, accesses, err := ex.GetAccessInfo(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if accessName != "" {
|
||||
accessDefault = accessName
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
return access, nil
|
||||
}
|
||||
|
||||
func (ex *external) GetAccessInfo(required bool) (string, map[string]string, error) {
|
||||
if !ex.access.loaded {
|
||||
if err := ex.loadAccesses(); err != nil {
|
||||
@ -62,7 +89,7 @@ func (ex *external) GetAccessInfo(required bool) (string, map[string]string, err
|
||||
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)
|
||||
accessFh, err := os.OpenFile(ex.AccessInfoFile(), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
@ -95,3 +122,17 @@ func (ex *external) SaveAccessInfo(defaultName string, accesses map[string]strin
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ex *external) RequestAccess(ctx context.Context, token, passphrase string) (*uplink.Access, error) {
|
||||
idx := strings.IndexByte(token, '/')
|
||||
if idx == -1 {
|
||||
return nil, errs.New("invalid setup token. should be 'satelliteAddress/apiKey'")
|
||||
}
|
||||
satelliteAddr, apiKey := token[:idx], token[idx+1:]
|
||||
|
||||
access, err := uplink.RequestAccessWithPassphrase(ctx, satelliteAddr, apiKey, passphrase)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
return access, nil
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ func (ex *external) loadConfig() error {
|
||||
}
|
||||
ex.config.values = make(map[string][]string)
|
||||
|
||||
fh, err := os.Open(ex.configFile())
|
||||
fh, err := os.Open(ex.ConfigFile())
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
@ -46,7 +46,7 @@ func (ex *external) loadConfig() error {
|
||||
func (ex *external) saveConfig(entries []ini.Entry) error {
|
||||
// TODO(jeff): write it atomically
|
||||
|
||||
newFh, err := os.Create(ex.configFile())
|
||||
newFh, err := os.Create(ex.ConfigFile())
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ func (ex *external) migrate() (err error) {
|
||||
defer func() { ex.migration.err = err }()
|
||||
|
||||
// if the config file exists, there is no need to migrate
|
||||
if _, err := os.Stat(ex.configFile()); err == nil {
|
||||
if _, err := os.Stat(ex.ConfigFile()); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -23,22 +23,7 @@ func (ex *external) OpenFilesystem(ctx context.Context, accessName string, optio
|
||||
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"
|
||||
}
|
||||
access, err := ex.OpenAccess(accessName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ func commands(cmds clingy.Commands, ex ulext.External) {
|
||||
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("restrict", "Restrict an access", newCmdAccessRestrict(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))
|
||||
|
@ -19,8 +19,11 @@ 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)
|
||||
|
||||
AccessInfoFile() string
|
||||
OpenAccess(accessName string) (access *uplink.Access, err error)
|
||||
GetAccessInfo(required bool) (string, map[string]string, error)
|
||||
SaveAccessInfo(defaultName string, accesses map[string]string) error
|
||||
RequestAccess(ctx context.Context, token, passphrase string) (*uplink.Access, error)
|
||||
|
||||
PromptInput(ctx clingy.Context, prompt string) (input string, err error)
|
||||
}
|
||||
|
@ -32,6 +32,9 @@ func (l *Local) abs(path string) (string, error) {
|
||||
!strings.HasSuffix(abs, string(filepath.Separator)) {
|
||||
abs += string(filepath.Separator)
|
||||
}
|
||||
if filepath.Separator != '/' {
|
||||
abs = strings.ReplaceAll(abs, string(filepath.Separator), "/")
|
||||
}
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
@ -108,9 +111,13 @@ func (l *Local) ListObjects(ctx context.Context, path string, recursive bool) (O
|
||||
if recursive {
|
||||
err = filepath.Walk(prefix, func(path string, info os.FileInfo, err error) error {
|
||||
if err == nil && !info.IsDir() {
|
||||
rel, err := filepath.Rel(prefix, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, &namedFileInfo{
|
||||
FileInfo: info,
|
||||
name: path[len(prefix):],
|
||||
name: rel,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
@ -175,13 +182,6 @@ func (fi *fileinfoObjectIterator) Item() ObjectInfo {
|
||||
if isDir {
|
||||
name += string(filepath.Separator)
|
||||
}
|
||||
|
||||
// TODO(jeff): is this the right thing to do on windows? is there more to do?
|
||||
// convert the paths to be forward slash based because keys are supposed to always be remote
|
||||
if filepath.Separator != '/' {
|
||||
name = strings.ReplaceAll(name, string(filepath.Separator), "/")
|
||||
}
|
||||
|
||||
return ObjectInfo{
|
||||
Loc: ulloc.NewLocal(name),
|
||||
IsPrefix: isDir,
|
||||
|
@ -15,6 +15,8 @@ import (
|
||||
)
|
||||
|
||||
type external struct {
|
||||
ulext.External
|
||||
|
||||
fs ulfs.Filesystem
|
||||
project *uplink.Project
|
||||
}
|
||||
@ -34,6 +36,10 @@ func (ex *external) OpenProject(ctx context.Context, access string, options ...u
|
||||
return ex.project, nil
|
||||
}
|
||||
|
||||
func (ex *external) OpenAccess(accessName string) (access *uplink.Access, err error) {
|
||||
return nil, errs.New("not implemented")
|
||||
}
|
||||
|
||||
func (ex *external) GetAccessInfo(required bool) (string, map[string]string, error) {
|
||||
return "", nil, errs.New("not implemented")
|
||||
}
|
||||
|
@ -26,8 +26,8 @@ type Result struct {
|
||||
func (r Result) RequireSuccess(t *testing.T) {
|
||||
if !r.Ok {
|
||||
errs := parseErrors(r.Stdout)
|
||||
require.True(t, r.Ok, "test did not run successfully. errors:\n%s",
|
||||
strings.Join(errs, "\n"))
|
||||
require.FailNow(t, "test did not run successfully",
|
||||
"%s", strings.Join(errs, "\n"))
|
||||
}
|
||||
require.NoError(t, r.Err)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user