diff --git a/ais/backend/aws.go b/ais/backend/aws.go index f332dacd2a..537aca63ca 100644 --- a/ais/backend/aws.go +++ b/ais/backend/aws.go @@ -893,6 +893,14 @@ func awsErrorToAISError(awsError error, bck *cmn.Bck, objName string) (int, erro code = reqErr.ErrorCode() ) if errors.As(awsError, &rspErr) { + // [NOTE] when bucket does not exist, or is not accessible AWS may + // return http status 301 ("MovedPermanently") and, + // to further confusion, supplies it with ErrorCode() == "PermanentRedirect", + // which is supposed to be 308 + if rspErr.HTTPStatusCode() == http.StatusMovedPermanently { + return http.StatusNotFound, cmn.NewErrRemoteBckNotFound(bck) + } + return rspErr.HTTPStatusCode(), _awsErr(awsError, code) } diff --git a/cmd/cli/cli/bucket_hdlr.go b/cmd/cli/cli/bucket_hdlr.go index 63a5cc2052..5a33fad12d 100644 --- a/cmd/cli/cli/bucket_hdlr.go +++ b/cmd/cli/cli/bucket_hdlr.go @@ -617,9 +617,22 @@ func listAnyHandler(c *cli.Context) error { if errV := errBucketNameInvalid(c, uri, err); errV != nil { return errV } + // (e.g. 'ais ls object ais://blah ...') + if cmn.IsErrEmptyProvider(err) { + uri = c.Args().Get(1) + var ( + err2 error + warn = fmt.Sprintf("word %q is misplaced, see 'ais ls --help' for details", c.Args().Get(0)) + ) + actionWarn(c, warn) + bck, objName, err2 = cmn.ParseBckObjectURI(uri, opts) + if err2 == nil { + goto proceed + } + } return err } - +proceed: switch { case objName != "": // (1) list archive, or diff --git a/cmd/cli/cli/object.go b/cmd/cli/cli/object.go index 0b4ff67659..4eed751909 100644 --- a/cmd/cli/cli/object.go +++ b/cmd/cli/cli/object.go @@ -258,7 +258,7 @@ func showObjProps(c *cli.Context, bck cmn.Bck, objName string, silent bool) (not selectedProps []string hargs = api.HeadArgs{ FltPresence: apc.FltPresentCluster, - Silent: flagIsSet(c, silentFlag), + Silent: flagIsSet(c, silentFlag) || silent, } isList = actionIsHandler(c.Command.Action, listAnyHandler) isRemote = bck.IsRemote() @@ -269,26 +269,29 @@ func showObjProps(c *cli.Context, bck cmn.Bck, objName string, silent bool) (not } if flagIsSet(c, objNotCachedPropsFlag) || flagIsSet(c, allObjsOrBcksFlag) { hargs.FltPresence = apc.FltExists - } else if silent && !hargs.Silent { - hargs.Silent = isRemote // silence 404 when called via 'ais ls ', `ais prefetch`, etc. } + + // do objProps, err := api.HeadObject(apiBP, bck, objName, hargs) if err != nil { notfound = cmn.IsStatusNotFound(err) if !notfound { return notfound, err } - var hint string + var hint, tag string + if !isList { + tag = "object " + } if apc.IsFltPresent(hargs.FltPresence) && isRemote { if isList { if flagIsSet(c, listObjCachedFlag) { hint = fmt.Sprintf(" (tip: try 'ais ls' without %s option)", qflprn(listObjCachedFlag)) } } else { - hint = fmt.Sprintf(" (tip: try %s option)", qflprn(objNotCachedPropsFlag)) + hint = fmt.Sprintf(" (tip: try %s option or use 'ais ls' to lookup by prefix)", qflprn(objNotCachedPropsFlag)) } } - return notfound, fmt.Errorf("%q not found in %s%s", objName, bck.Cname(""), hint) + return notfound, fmt.Errorf("%s%q not found in %s%s", tag, objName, bck.Cname(""), hint) } if flagIsSet(c, allPropsFlag) { diff --git a/cmd/cli/go.mod b/cmd/cli/go.mod index 5392879c31..d2b45108a9 100644 --- a/cmd/cli/go.mod +++ b/cmd/cli/go.mod @@ -3,7 +3,7 @@ module github.com/NVIDIA/aistore/cmd/cli go 1.23.2 require ( - github.com/NVIDIA/aistore v1.3.26-0.20241022221110-ec9d71b82df4 + github.com/NVIDIA/aistore v1.3.26-0.20241030143942-035070df9f54 github.com/fatih/color v1.17.0 github.com/json-iterator/go v1.1.12 github.com/onsi/ginkgo/v2 v2.20.2 diff --git a/cmd/cli/go.sum b/cmd/cli/go.sum index 3dda6e0587..5b52b4d62e 100644 --- a/cmd/cli/go.sum +++ b/cmd/cli/go.sum @@ -1,7 +1,7 @@ code.cloudfoundry.org/bytefmt v0.0.0-20190710193110-1eb035ffe2b6/go.mod h1:wN/zk7mhREp/oviagqUXY3EwuHhWyOvAdsn5Y4CzOrc= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/NVIDIA/aistore v1.3.26-0.20241022221110-ec9d71b82df4 h1:bb2bM8SR+E28B+asH0kbWzfp/b+Yd/96NK/BhimfPL0= -github.com/NVIDIA/aistore v1.3.26-0.20241022221110-ec9d71b82df4/go.mod h1:Q6J3YIeiL4A6oWga3qCJ8+XI1CUvdde7Gua/HfueGlQ= +github.com/NVIDIA/aistore v1.3.26-0.20241030143942-035070df9f54 h1:j/phqhzqYkIPwydLekPQkW2jlIGmooNiVx3IUivz1M4= +github.com/NVIDIA/aistore v1.3.26-0.20241030143942-035070df9f54/go.mod h1:Q6J3YIeiL4A6oWga3qCJ8+XI1CUvdde7Gua/HfueGlQ= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= diff --git a/cmn/api.go b/cmn/api.go index fe71f4c7ed..9b8d3a66b1 100644 --- a/cmn/api.go +++ b/cmn/api.go @@ -203,14 +203,15 @@ func (bp *Bprops) Validate(targetCnt int) error { debug.Assert(apc.IsProvider(bp.Provider)) if !bp.BackendBck.IsEmpty() { if bp.Provider != apc.AIS { - return fmt.Errorf("wrong bucket provider %q: only AIS buckets can have remote backend (%q)", + return fmt.Errorf("invalid provider %q: only ais:// buckets can have remote backend (%q)", bp.Provider, bp.BackendBck) } if bp.BackendBck.Provider == "" { + // (compare with `ErrEmptyProvider`) return fmt.Errorf("backend bucket %q: provider is empty", bp.BackendBck) } if bp.BackendBck.Name == "" { - return fmt.Errorf("backend bucket %q name is empty", bp.BackendBck) + return fmt.Errorf("backend bucket %q: name is empty", bp.BackendBck) } if !bp.BackendBck.IsRemote() { return fmt.Errorf("backend bucket %q must be remote", bp.BackendBck) diff --git a/cmn/uri.go b/cmn/uri.go index 46d24a37c4..83a7ac3e4a 100644 --- a/cmn/uri.go +++ b/cmn/uri.go @@ -16,9 +16,24 @@ import ( "github.com/OneOfOne/xxhash" ) -type ParseURIOpts struct { - DefaultProvider string // If set the provider will be used as provider. - IsQuery bool // Determines if the URI should be parsed as query. +type ( + ParseURIOpts struct { + DefaultProvider string // If set the provider will be used as provider. + IsQuery bool // Determines if the URI should be parsed as query. + } + ErrEmptyProvider struct { + name string + detail string + } +) + +func (e *ErrEmptyProvider) Error() string { + return fmt.Sprintf("backend provider cannot be empty%s (did you mean \"ais://%s\"?)", e.detail, e.name) +} + +func IsErrEmptyProvider(err error) bool { + _, ok := err.(*ErrEmptyProvider) + return ok } // @@ -45,7 +60,6 @@ func OrigURLBck2Name(origURLBck string) (bckName string) { } func ParseBckObjectURI(uri string, opts ParseURIOpts) (bck Bck, objName string, err error) { - const fmtErrEmpty = "backend provider cannot be empty%s (did you mean \"ais://%s\"?)" parts := strings.SplitN(uri, apc.BckProviderSeparator, 2) if len(parts) > 1 && parts[0] != "" { if bck.Provider, err = NormalizeProvider(parts[0]); err != nil { @@ -63,7 +77,7 @@ func ParseBckObjectURI(uri string, opts ParseURIOpts) (bck Bck, objName string, return bck, "", err } if !opts.IsQuery && bck.Provider == "" { - return bck, "", fmt.Errorf(fmtErrEmpty, " when namespace is not", bck) + return bck, "", &ErrEmptyProvider{uri, " when namespace is not"} } if len(parts) == 1 { if parts[0] == string(apc.NsUUIDPrefix) && opts.IsQuery { @@ -87,7 +101,7 @@ func ParseBckObjectURI(uri string, opts ParseURIOpts) (bck Bck, objName string, return bck, "", err } if bck.Provider == "" { - return bck, "", fmt.Errorf(fmtErrEmpty, "", bck) + return bck, "", &ErrEmptyProvider{uri, ""} } } if len(parts) > 1 {