storj/cmd/uplinkng/location.go
Jeff Wendling b24ea2ead5 cmd/uplinkng: test framework and ls tests
this adds a test framework with fake implementations of a
filesystem so that unit tests can be written asserting
the output of different command invocations as well as
the effects they have on a hypothetical filesystem and
storj network.

it also implements the mb command lol

Change-Id: I134c7ea6bf34f46192956c274a96cb5df7632ac0
2021-06-14 15:23:41 -04:00

202 lines
5.4 KiB
Go

// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"fmt"
"path/filepath"
"strings"
"github.com/zeebo/errs"
)
// Location represets a local path, a remote object, or stdin/stdout.
type Location struct {
path string
bucket string
key string
remote bool
}
func parseLocation(location string) (p Location, err error) {
if location == "-" {
return Location{path: "-", key: "-"}, nil
}
// 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:]
}
return Location{bucket: bucket, key: key, remote: true}, nil
}
return Location{path: location, remote: false}, nil
}
// Std returns true if the location refers to stdin/stdout.
func (p Location) Std() bool { return p.path == "-" && p.key == "-" }
// Remote returns true if the location is remote.
func (p Location) Remote() bool { return !p.Std() && p.remote }
// Local returns true if the location is local.
func (p Location) Local() bool { return !p.Std() && !p.remote }
// String returns the string form of the location.
func (p Location) String() string {
if p.Std() {
return "-"
} else if p.remote {
return fmt.Sprintf("sj://%s/%s", p.bucket, p.key)
}
return p.path
}
// Key returns either the path or the object key.
func (p Location) Key() string {
if p.remote {
return p.key
}
return p.path
}
// SetKey sets the key portion of the location.
func (p Location) SetKey(s string) Location {
if p.remote {
p.key = s
} else {
p.path = s
}
return p
}
// Parent returns the section of the key up to and including the final slash.
func (p Location) Parent() string {
if p.Std() {
return ""
} else if p.remote {
if idx := strings.LastIndexByte(p.key, '/'); idx >= 0 {
return p.key[:idx+1]
}
return ""
}
if idx := strings.LastIndexByte(p.path, filepath.Separator); idx >= 0 {
return p.path[:idx+1]
}
return ""
}
// Base returns the last base component of the key.
func (p Location) Base() (string, bool) {
if p.Std() {
return "", false
} else if p.remote {
key := p.key
if idx := strings.LastIndexByte(key, '/'); idx >= 0 {
key = key[idx:]
}
return key, len(key) > 0
}
base := filepath.Base(p.path)
if base == "." || base == string(filepath.Separator) || base == "" {
return "", false
}
return base, true
}
// 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")
} else if target.remote != p.remote {
return "", errs.New("cannot create remote and local relative location")
} else if !target.remote {
abs, err := filepath.Abs(p.path)
if err != nil {
return "", errs.Wrap(err)
}
rel, err := filepath.Rel(abs, target.path)
if err != nil {
return "", errs.Wrap(err)
}
return rel, nil
} else if target.bucket != p.bucket {
return "", errs.New("cannot change buckets in relative remote location")
} else if !strings.HasPrefix(target.key, p.key) {
return "", errs.New("cannot make relative location because keys are not prefixes")
}
return target.key[len(p.key):], nil
}
// 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 {
if p.remote {
p.key += key
return p
}
// convert any / to the local filesystem slash if necessary
if filepath.Separator != '/' {
key = strings.ReplaceAll(key, "/", string(filepath.Separator))
}
// clean up issues with // or /../ or /./ etc.
key = filepath.Clean(string(filepath.Separator) + key)[1:]
p.path = filepath.Join(p.path, key)
return p
}
// HasPrefix returns true if the passed in loc is a prefix.
func (p Location) HasPrefix(loc Location) bool {
if p.Std() {
return loc.Std()
} else if p.remote != loc.remote {
return false
} else if !p.remote {
return strings.HasPrefix(p.path, loc.path)
} else if p.bucket != loc.bucket {
return false
}
return strings.HasPrefix(p.key, loc.key)
}
// 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) {
rem := p.Key()[len(prefix.Parent()):]
if idx := strings.IndexByte(rem, '/'); idx >= 0 {
return rem[:idx+1], true
}
return rem, false
}