2021-04-06 20:19:11 +01:00
|
|
|
// Copyright (C) 2021 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
2021-05-06 17:56:57 +01:00
|
|
|
package ulloc
|
2021-04-06 20:19:11 +01:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/zeebo/errs"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Location represets a local path, a remote object, or stdin/stdout.
|
|
|
|
type Location struct {
|
2021-06-25 02:55:13 +01:00
|
|
|
bucket string // if nonempty, is remote
|
|
|
|
loc string // key or path
|
|
|
|
std bool // if refers to stdin/stdout
|
|
|
|
}
|
|
|
|
|
|
|
|
// cleanPath is used to normalize all the filepath separators, remove
|
|
|
|
// any .. or . components, and keep the trailing slash if necessary.
|
|
|
|
func cleanPath(path string) string {
|
|
|
|
// convert path to only filepath.Separator
|
|
|
|
if filepath.Separator != '/' {
|
|
|
|
path = strings.ReplaceAll(path, `/`, `\`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// now we can use the filepath.Clean routine
|
|
|
|
cleaned := filepath.Clean(path)
|
|
|
|
if cleaned == "." {
|
|
|
|
cleaned = ""
|
|
|
|
}
|
|
|
|
|
|
|
|
// convert all slashes to forward slashes from now on
|
|
|
|
if filepath.Separator != '/' {
|
|
|
|
cleaned = strings.ReplaceAll(cleaned, `\`, `/`)
|
|
|
|
}
|
|
|
|
|
|
|
|
// if cleaned at this point is either the current working
|
|
|
|
// directory (meaning the empty string) or the root directory
|
|
|
|
// meaning "/", then we don't need to add a slash, so return now.
|
|
|
|
if cleaned == "" || cleaned == "/" {
|
|
|
|
return cleaned
|
|
|
|
}
|
|
|
|
|
|
|
|
// if the passed in path ended with a slash, clean should, too.
|
|
|
|
if strings.HasSuffix(path, string(filepath.Separator)) {
|
|
|
|
cleaned += "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
return cleaned
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
|
|
|
|
2021-05-06 17:56:57 +01:00
|
|
|
// NewLocal returns a new Location that refers to a local path.
|
|
|
|
func NewLocal(path string) Location {
|
2021-06-25 02:55:13 +01:00
|
|
|
return Location{loc: cleanPath(path)}
|
2021-05-06 17:56:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewRemote returns a new location that refers to a remote path.
|
|
|
|
func NewRemote(bucket, key string) Location {
|
2021-06-25 02:55:13 +01:00
|
|
|
return Location{bucket: bucket, loc: key}
|
2021-05-06 17:56:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewStd returns a new location that refers to stdin or stdout.
|
|
|
|
func NewStd() Location {
|
2021-06-25 02:55:13 +01:00
|
|
|
return Location{loc: "-", std: true}
|
2021-05-06 17:56:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Parse turns the string form of the location into the structured Location
|
|
|
|
// value and an error if it is unable to or the location is invalid.
|
|
|
|
func Parse(location string) (p Location, err error) {
|
2021-04-06 20:19:11 +01:00
|
|
|
if location == "-" {
|
2021-05-06 17:56:57 +01:00
|
|
|
return NewStd(), nil
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Locations, Chapter 2, Verses 9 to 21.
|
|
|
|
//
|
|
|
|
// And the Devs spake, saying,
|
|
|
|
// First shalt thou find the Special Prefix "sj:".
|
|
|
|
// Then, shalt thou count two slashes, no more, no less.
|
|
|
|
// Two shall be the number thou shalt count,
|
|
|
|
// and the number of the counting shall be two.
|
|
|
|
// Three shalt thou not count, nor either count thou one,
|
|
|
|
// excepting that thou then proceed to two.
|
|
|
|
// Four is right out!
|
|
|
|
// Once the number two, being the second number, be reached,
|
|
|
|
// then interpret thou thy location as a remote location,
|
|
|
|
// which being made of a bucket and key, shall split it.
|
|
|
|
|
|
|
|
if strings.HasPrefix(location, "sj://") || strings.HasPrefix(location, "s3://") {
|
|
|
|
trimmed := location[5:] // remove the scheme
|
|
|
|
idx := strings.IndexByte(trimmed, '/') // find the bucket index
|
|
|
|
|
|
|
|
// handles sj:// or sj:///foo
|
|
|
|
if len(trimmed) == 0 || idx == 0 {
|
|
|
|
return Location{}, errs.New("invalid path: empty bucket in path: %q", location)
|
|
|
|
}
|
|
|
|
|
|
|
|
var bucket, key string
|
|
|
|
if idx == -1 { // handles sj://foo
|
|
|
|
bucket, key = trimmed, ""
|
|
|
|
} else { // handles sj://foo/bar
|
|
|
|
bucket, key = trimmed[:idx], trimmed[idx+1:]
|
|
|
|
}
|
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
return Location{bucket: bucket, loc: key}, nil
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
return NewLocal(location), nil
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
// Loc returns either the key or path associated with the location.
|
|
|
|
func (p Location) Loc() string { return p.loc }
|
|
|
|
|
2021-04-06 20:19:11 +01:00
|
|
|
// Std returns true if the location refers to stdin/stdout.
|
2021-06-25 02:55:13 +01:00
|
|
|
func (p Location) Std() bool { return p.std }
|
2021-04-06 20:19:11 +01:00
|
|
|
|
|
|
|
// Remote returns true if the location is remote.
|
2021-06-25 02:55:13 +01:00
|
|
|
func (p Location) Remote() bool { return !p.Std() && p.bucket != "" }
|
2021-04-06 20:19:11 +01:00
|
|
|
|
|
|
|
// Local returns true if the location is local.
|
2021-06-25 02:55:13 +01:00
|
|
|
func (p Location) Local() bool { return !p.Std() && p.bucket == "" }
|
2021-04-06 20:19:11 +01:00
|
|
|
|
|
|
|
// String returns the string form of the location.
|
|
|
|
func (p Location) String() string {
|
|
|
|
if p.Std() {
|
|
|
|
return "-"
|
2021-06-25 02:55:13 +01:00
|
|
|
} else if p.Remote() {
|
|
|
|
return fmt.Sprintf("sj://%s/%s", p.bucket, p.loc)
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
2021-06-25 02:55:13 +01:00
|
|
|
return p.loc
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
// Parent returns the section of the key or path up to and including the final slash.
|
2021-05-05 22:53:08 +01:00
|
|
|
func (p Location) Parent() string {
|
|
|
|
if p.Std() {
|
|
|
|
return ""
|
2021-06-25 02:55:13 +01:00
|
|
|
} else if idx := strings.LastIndexByte(p.loc, '/'); idx >= 0 {
|
|
|
|
return p.loc[:idx+1]
|
2021-05-05 22:53:08 +01:00
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
// Base returns the last base component of the key or path not including the last slash.
|
2021-04-06 20:19:11 +01:00
|
|
|
func (p Location) Base() (string, bool) {
|
|
|
|
if p.Std() {
|
|
|
|
return "", false
|
2021-06-25 02:55:13 +01:00
|
|
|
} else if idx := strings.LastIndexByte(p.loc, '/'); idx >= 0 {
|
|
|
|
p.loc = p.loc[idx+1:]
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
2021-06-25 02:55:13 +01:00
|
|
|
return p.loc, len(p.loc) > 0
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// RelativeTo returns the string that when appended to the location string
|
|
|
|
// will return a string equivalent to the passed in target location.
|
|
|
|
func (p Location) RelativeTo(target Location) (string, error) {
|
|
|
|
if p.Std() || target.Std() {
|
|
|
|
return "", errs.New("cannot create relative location for stdin/stdout")
|
2021-06-25 02:55:13 +01:00
|
|
|
} else if target.Remote() != p.Remote() {
|
2021-04-06 20:19:11 +01:00
|
|
|
return "", errs.New("cannot create remote and local relative location")
|
|
|
|
} else if target.bucket != p.bucket {
|
|
|
|
return "", errs.New("cannot change buckets in relative remote location")
|
2021-06-25 02:55:13 +01:00
|
|
|
} else if !strings.HasPrefix(target.loc, p.loc) {
|
2021-04-06 20:19:11 +01:00
|
|
|
return "", errs.New("cannot make relative location because keys are not prefixes")
|
|
|
|
}
|
2021-06-25 02:55:13 +01:00
|
|
|
idx := strings.LastIndexByte(p.loc, '/') + 1
|
|
|
|
return target.loc[idx:], nil
|
2021-04-06 20:19:11 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// AppendKey adds the key to the end of the existing key, separating with the
|
|
|
|
// appropriate slash if necessary.
|
|
|
|
func (p Location) AppendKey(key string) Location {
|
2021-06-25 02:55:13 +01:00
|
|
|
if p.Remote() {
|
|
|
|
p.loc += key
|
2021-04-06 20:19:11 +01:00
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
// clean up the key so that it can't create a location beneath p.loc
|
|
|
|
key = cleanPath("/" + key)[1:]
|
|
|
|
p.loc = cleanPath(p.loc + key)
|
2021-04-06 20:19:11 +01:00
|
|
|
|
|
|
|
return p
|
|
|
|
}
|
2021-05-05 22:53:08 +01:00
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
// HasPrefix returns true if the passed in Location is a prefix.
|
|
|
|
func (p Location) HasPrefix(pre Location) bool {
|
2021-05-05 22:53:08 +01:00
|
|
|
if p.Std() {
|
2021-06-25 02:55:13 +01:00
|
|
|
return pre.Std()
|
|
|
|
} else if p.Remote() != pre.Remote() {
|
2021-05-05 22:53:08 +01:00
|
|
|
return false
|
2021-06-25 02:55:13 +01:00
|
|
|
} else if p.bucket != pre.bucket {
|
2021-05-05 22:53:08 +01:00
|
|
|
return false
|
|
|
|
}
|
2021-06-25 02:55:13 +01:00
|
|
|
return strings.HasPrefix(p.loc, pre.loc)
|
2021-05-05 22:53:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// ListKeyName returns the full first component of the key after the provided
|
|
|
|
// prefix and a boolean indicating if the component is itself a prefix.
|
|
|
|
func (p Location) ListKeyName(prefix Location) (string, bool) {
|
2021-06-25 02:55:13 +01:00
|
|
|
rem := p.loc[len(prefix.Parent()):]
|
2021-05-05 22:53:08 +01:00
|
|
|
if idx := strings.IndexByte(rem, '/'); idx >= 0 {
|
|
|
|
return rem[:idx+1], true
|
|
|
|
}
|
|
|
|
return rem, false
|
|
|
|
}
|
2021-05-06 17:56:57 +01:00
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
// RemovePrefix removes the prefix from the key or path in the location if they
|
2021-05-06 17:56:57 +01:00
|
|
|
// begin with it.
|
2021-06-25 02:55:13 +01:00
|
|
|
func (p Location) RemovePrefix(prefix Location) Location {
|
|
|
|
if !p.HasPrefix(prefix) {
|
|
|
|
return p
|
2021-05-06 17:56:57 +01:00
|
|
|
}
|
2021-06-25 02:55:13 +01:00
|
|
|
p.loc = strings.TrimPrefix(p.loc, prefix.loc)
|
2021-05-06 17:56:57 +01:00
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
|
|
|
// RemoteParts returns the bucket and key for the location and a bool indicating
|
|
|
|
// if those values are valid because the location is remote.
|
|
|
|
func (p Location) RemoteParts() (bucket, key string, ok bool) {
|
2021-06-25 02:55:13 +01:00
|
|
|
return p.bucket, p.loc, p.Remote()
|
2021-05-06 17:56:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// LocalParts returns the path for the location and a bool indicating if that
|
|
|
|
// value is valid because the location is local.
|
|
|
|
func (p Location) LocalParts() (path string, ok bool) {
|
2021-06-25 02:55:13 +01:00
|
|
|
return p.loc, p.Local()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Directoryish returns if the location is syntatically directoryish, meaning
|
|
|
|
// that the location component is either empty or ends with a slash.
|
|
|
|
func (p Location) Directoryish() bool {
|
|
|
|
return !p.Std() && (p.loc == "" || p.loc[len(p.loc)-1] == '/')
|
|
|
|
}
|
|
|
|
|
|
|
|
// AsDirectoryish appends a trailing slash to the location if it is not
|
|
|
|
// already directoryish.
|
|
|
|
func (p Location) AsDirectoryish() Location {
|
|
|
|
if p.Directoryish() || p.Std() {
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
p.loc += "/"
|
|
|
|
return p
|
|
|
|
}
|
|
|
|
|
|
|
|
// Undirectoryish removes any trailing slashes from the location.
|
|
|
|
func (p Location) Undirectoryish() Location {
|
|
|
|
p.loc = strings.TrimRight(p.loc, "/")
|
|
|
|
return p
|
2021-05-06 17:56:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Less returns true if the location is less than the passed in location.
|
|
|
|
func (p Location) Less(q Location) bool {
|
2021-06-25 02:55:13 +01:00
|
|
|
if !p.Remote() && q.Remote() {
|
2021-05-06 17:56:57 +01:00
|
|
|
return true
|
2021-06-25 02:55:13 +01:00
|
|
|
} else if !q.Remote() && p.Remote() {
|
2021-05-06 17:56:57 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
if p.bucket < q.bucket {
|
|
|
|
return true
|
|
|
|
} else if q.bucket < p.bucket {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2021-06-25 02:55:13 +01:00
|
|
|
if p.loc < q.loc {
|
2021-05-06 17:56:57 +01:00
|
|
|
return true
|
2021-06-25 02:55:13 +01:00
|
|
|
} else if q.loc < p.loc {
|
2021-05-06 17:56:57 +01:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|