cmd/uplinkng: refactor into some focused packages

the directory was starting to get pretty large and
it was making it hard to pick concise names for
types and variables. this moves the location
stuff into a cmd/uplinkng/ulloc package, the
filesystem stuff into a cmd/uplinkng/ulfs package,
and the testing stuff into a cmd/uplinkng/ultest
package.

this should make the remaining stuff in cmd/uplinkng
only the business logic of how to implement the
commands, rather than also including a bunch of
helper utilities and scaffolding.

Change-Id: Id0901625ebfff9b1cf2dae52366aceb3b6c8f5b6
This commit is contained in:
Jeff Wendling 2021-05-06 12:56:57 -04:00
parent b24ea2ead5
commit 98be54b9a3
16 changed files with 842 additions and 701 deletions

View File

@ -11,6 +11,9 @@ import (
progressbar "github.com/cheggaaa/pb/v3"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdCp struct {
@ -19,8 +22,8 @@ type cmdCp struct {
recursive bool
dryrun bool
source Location
dest Location
source ulloc.Location
dest ulloc.Location
}
func (c *cmdCp) Setup(a clingy.Arguments, f clingy.Flags) {
@ -34,8 +37,8 @@ func (c *cmdCp) Setup(a clingy.Arguments, f clingy.Flags) {
clingy.Transform(strconv.ParseBool),
).(bool)
c.source = a.New("source", "Source to copy", clingy.Transform(parseLocation)).(Location)
c.dest = a.New("dest", "Desination to copy", clingy.Transform(parseLocation)).(Location)
c.source = a.New("source", "Source to copy", clingy.Transform(ulloc.Parse)).(ulloc.Location)
c.dest = a.New("dest", "Desination to copy", clingy.Transform(ulloc.Parse)).(ulloc.Location)
}
func (c *cmdCp) Execute(ctx clingy.Context) error {
@ -51,7 +54,7 @@ func (c *cmdCp) Execute(ctx clingy.Context) error {
return c.copyFile(ctx, fs, c.source, c.dest, true)
}
func (c *cmdCp) copyRecursive(ctx clingy.Context, fs filesystem) error {
func (c *cmdCp) copyRecursive(ctx clingy.Context, fs ulfs.Filesystem) error {
if c.source.Std() || c.dest.Std() {
return errs.New("cannot recursively copy to stdin/stdout")
}
@ -86,7 +89,7 @@ func (c *cmdCp) copyRecursive(ctx clingy.Context, fs filesystem) error {
return nil
}
func (c *cmdCp) copyFile(ctx clingy.Context, fs filesystem, source, dest Location, progress bool) error {
func (c *cmdCp) copyFile(ctx clingy.Context, fs ulfs.Filesystem, source, dest ulloc.Location, progress bool) error {
if isDir := fs.IsLocalDir(ctx, dest); isDir {
base, ok := source.Base()
if !ok {
@ -131,11 +134,11 @@ func (c *cmdCp) copyFile(ctx clingy.Context, fs filesystem, source, dest Locatio
return errs.Wrap(wh.Commit())
}
func copyVerb(source, dest Location) string {
func copyVerb(source, dest ulloc.Location) string {
switch {
case dest.remote:
case dest.Remote():
return "upload"
case source.remote:
case source.Remote():
return "download"
default:
return "copy"

View File

@ -8,6 +8,9 @@ import (
"time"
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdLs struct {
@ -18,7 +21,7 @@ type cmdLs struct {
pending bool
utc bool
prefix *Location
prefix *ulloc.Location
}
func (c *cmdLs) Setup(a clingy.Arguments, f clingy.Flags) {
@ -39,8 +42,8 @@ func (c *cmdLs) Setup(a clingy.Arguments, f clingy.Flags) {
).(bool)
c.prefix = a.New("prefix", "Prefix to list (sj://BUCKET[/KEY])", clingy.Optional,
clingy.Transform(parseLocation),
).(*Location)
clingy.Transform(ulloc.Parse),
).(*ulloc.Location)
}
func (c *cmdLs) Execute(ctx clingy.Context) error {
@ -68,7 +71,7 @@ func (c *cmdLs) listBuckets(ctx clingy.Context) error {
return iter.Err()
}
func (c *cmdLs) listLocation(ctx clingy.Context, prefix Location) error {
func (c *cmdLs) listLocation(ctx clingy.Context, prefix ulloc.Location) error {
fs, err := c.OpenFilesystem(ctx, bypassEncryption(c.encrypted))
if err != nil {
return err
@ -79,7 +82,7 @@ func (c *cmdLs) listLocation(ctx clingy.Context, prefix Location) error {
defer tw.Done()
// create the object iterator of either existing objects or pending multipart uploads
var iter objectIterator
var iter ulfs.ObjectIterator
if c.pending {
iter, err = fs.ListUploads(ctx, prefix, c.recursive)
} else {

View File

@ -5,32 +5,34 @@ package main
import (
"testing"
"storj.io/storj/cmd/uplinkng/ultest"
)
func TestLsErrors(t *testing.T) {
state := Setup(t)
state := ultest.Setup(commands)
// empty bucket name is a parse error
state.Fail(t, "ls", "sj:///jeff")
state.Fail(t, "ls", "sj:///user")
}
func TestLsRemote(t *testing.T) {
state := Setup(t,
WithFile("sj://jeff/deep/aaa/bbb/1"),
WithFile("sj://jeff/deep/aaa/bbb/2"),
WithFile("sj://jeff/deep/aaa/bbb/3"),
WithFile("sj://jeff/foobar"),
WithFile("sj://jeff/foobar/"),
WithFile("sj://jeff/foobar/1"),
WithFile("sj://jeff/foobar/2"),
WithFile("sj://jeff/foobar/3"),
WithFile("sj://jeff/foobaz/1"),
state := ultest.Setup(commands,
ultest.WithFile("sj://user/deep/aaa/bbb/1"),
ultest.WithFile("sj://user/deep/aaa/bbb/2"),
ultest.WithFile("sj://user/deep/aaa/bbb/3"),
ultest.WithFile("sj://user/foobar"),
ultest.WithFile("sj://user/foobar/"),
ultest.WithFile("sj://user/foobar/1"),
ultest.WithFile("sj://user/foobar/2"),
ultest.WithFile("sj://user/foobar/3"),
ultest.WithFile("sj://user/foobaz/1"),
WithPendingFile("sj://jeff/invisible"),
ultest.WithPendingFile("sj://user/invisible"),
)
t.Run("Recursive", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff", "--recursive", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user", "--recursive", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:01 0 deep/aaa/bbb/1
OBJ 1970-01-01 00:00:02 0 deep/aaa/bbb/2
@ -45,7 +47,7 @@ func TestLsRemote(t *testing.T) {
})
t.Run("Basic", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/fo", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/fo", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:04 0 foobar
PRE foobar/
@ -54,7 +56,7 @@ func TestLsRemote(t *testing.T) {
})
t.Run("ExactPrefix", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/foobar", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/foobar", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:04 0 foobar
PRE foobar/
@ -62,7 +64,7 @@ func TestLsRemote(t *testing.T) {
})
t.Run("ExactPrefixWithSlash", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/foobar/", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/foobar/", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:05 0
OBJ 1970-01-01 00:00:06 0 1
@ -72,17 +74,17 @@ func TestLsRemote(t *testing.T) {
})
t.Run("MultipleLayers", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/deep/").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/deep/").RequireStdout(t, `
KIND CREATED SIZE KEY
PRE aaa/
`)
state.Succeed(t, "ls", "sj://jeff/deep/aaa/").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/deep/aaa/").RequireStdout(t, `
KIND CREATED SIZE KEY
PRE bbb/
`)
state.Succeed(t, "ls", "sj://jeff/deep/aaa/bbb/", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/deep/aaa/bbb/", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:01 0 1
OBJ 1970-01-01 00:00:02 0 2
@ -92,22 +94,22 @@ func TestLsRemote(t *testing.T) {
}
func TestLsPending(t *testing.T) {
state := Setup(t,
WithPendingFile("sj://jeff/deep/aaa/bbb/1"),
WithPendingFile("sj://jeff/deep/aaa/bbb/2"),
WithPendingFile("sj://jeff/deep/aaa/bbb/3"),
WithPendingFile("sj://jeff/foobar"),
WithPendingFile("sj://jeff/foobar/"),
WithPendingFile("sj://jeff/foobar/1"),
WithPendingFile("sj://jeff/foobar/2"),
WithPendingFile("sj://jeff/foobar/3"),
WithPendingFile("sj://jeff/foobaz/1"),
state := ultest.Setup(commands,
ultest.WithPendingFile("sj://user/deep/aaa/bbb/1"),
ultest.WithPendingFile("sj://user/deep/aaa/bbb/2"),
ultest.WithPendingFile("sj://user/deep/aaa/bbb/3"),
ultest.WithPendingFile("sj://user/foobar"),
ultest.WithPendingFile("sj://user/foobar/"),
ultest.WithPendingFile("sj://user/foobar/1"),
ultest.WithPendingFile("sj://user/foobar/2"),
ultest.WithPendingFile("sj://user/foobar/3"),
ultest.WithPendingFile("sj://user/foobaz/1"),
WithFile("sj://jeff/invisible"),
ultest.WithFile("sj://user/invisible"),
)
t.Run("Recursive", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff", "--recursive", "--pending", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user", "--recursive", "--pending", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:01 0 deep/aaa/bbb/1
OBJ 1970-01-01 00:00:02 0 deep/aaa/bbb/2
@ -122,7 +124,7 @@ func TestLsPending(t *testing.T) {
})
t.Run("Basic", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/fo", "--pending", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/fo", "--pending", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:04 0 foobar
PRE foobar/
@ -131,7 +133,7 @@ func TestLsPending(t *testing.T) {
})
t.Run("ExactPrefix", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/foobar", "--pending", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/foobar", "--pending", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:04 0 foobar
PRE foobar/
@ -139,7 +141,7 @@ func TestLsPending(t *testing.T) {
})
t.Run("ExactPrefixWithSlash", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/foobar/", "--pending", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/foobar/", "--pending", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:05 0
OBJ 1970-01-01 00:00:06 0 1
@ -149,17 +151,17 @@ func TestLsPending(t *testing.T) {
})
t.Run("MultipleLayers", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/deep/", "--pending").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/deep/", "--pending").RequireStdout(t, `
KIND CREATED SIZE KEY
PRE aaa/
`)
state.Succeed(t, "ls", "sj://jeff/deep/aaa/", "--pending").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/deep/aaa/", "--pending").RequireStdout(t, `
KIND CREATED SIZE KEY
PRE bbb/
`)
state.Succeed(t, "ls", "sj://jeff/deep/aaa/bbb/", "--pending", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/deep/aaa/bbb/", "--pending", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:01 0 1
OBJ 1970-01-01 00:00:02 0 2
@ -169,24 +171,24 @@ func TestLsPending(t *testing.T) {
}
func TestLsDifficult(t *testing.T) {
state := Setup(t,
WithFile("sj://jeff//"),
WithFile("sj://jeff///"),
WithFile("sj://jeff////"),
state := ultest.Setup(commands,
ultest.WithFile("sj://user//"),
ultest.WithFile("sj://user///"),
ultest.WithFile("sj://user////"),
WithFile("sj://jeff//starts-slash"),
ultest.WithFile("sj://user//starts-slash"),
WithFile("sj://jeff/ends-slash"),
WithFile("sj://jeff/ends-slash/"),
WithFile("sj://jeff/ends-slash//"),
ultest.WithFile("sj://user/ends-slash"),
ultest.WithFile("sj://user/ends-slash/"),
ultest.WithFile("sj://user/ends-slash//"),
WithFile("sj://jeff/mid-slash"),
WithFile("sj://jeff/mid-slash//2"),
WithFile("sj://jeff/mid-slash/1"),
ultest.WithFile("sj://user/mid-slash"),
ultest.WithFile("sj://user/mid-slash//2"),
ultest.WithFile("sj://user/mid-slash/1"),
)
t.Run("Recursive", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff", "--recursive", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user", "--recursive", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:01 0 /
OBJ 1970-01-01 00:00:02 0 //
@ -202,7 +204,7 @@ func TestLsDifficult(t *testing.T) {
})
t.Run("Basic", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
PRE /
OBJ 1970-01-01 00:00:05 0 ends-slash
@ -211,7 +213,7 @@ func TestLsDifficult(t *testing.T) {
PRE mid-slash/
`)
state.Succeed(t, "ls", "sj://jeff/", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
PRE /
OBJ 1970-01-01 00:00:05 0 ends-slash
@ -222,58 +224,58 @@ func TestLsDifficult(t *testing.T) {
})
t.Run("OnlySlash", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff//", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user//", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:01 0
PRE /
OBJ 1970-01-01 00:00:04 0 starts-slash
`)
state.Succeed(t, "ls", "sj://jeff///", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user///", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:02 0
PRE /
`)
state.Succeed(t, "ls", "sj://jeff////", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user////", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:03 0
`)
})
t.Run("EndsSlash", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/ends-slash", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/ends-slash", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:05 0 ends-slash
PRE ends-slash/
`)
state.Succeed(t, "ls", "sj://jeff/ends-slash/", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/ends-slash/", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:06 0
PRE /
`)
state.Succeed(t, "ls", "sj://jeff/ends-slash//", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/ends-slash//", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:07 0
`)
})
t.Run("MidSlash", func(t *testing.T) {
state.Succeed(t, "ls", "sj://jeff/mid-slash", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/mid-slash", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:08 0 mid-slash
PRE mid-slash/
`)
state.Succeed(t, "ls", "sj://jeff/mid-slash/", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/mid-slash/", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
PRE /
OBJ 1970-01-01 00:00:10 0 1
`)
state.Succeed(t, "ls", "sj://jeff/mid-slash//", "--utc").RequireStdout(t, `
state.Succeed(t, "ls", "sj://user/mid-slash//", "--utc").RequireStdout(t, `
KIND CREATED SIZE KEY
OBJ 1970-01-01 00:00:09 0 2
`)

View File

@ -5,12 +5,14 @@ package main
import (
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdMetaGet struct {
projectProvider
location Location
location ulloc.Location
entry *string
}
@ -18,8 +20,8 @@ func (c *cmdMetaGet) Setup(a clingy.Arguments, f clingy.Flags) {
c.projectProvider.Setup(a, f)
c.location = a.New("location", "Location of object (sj://BUCKET/KEY)",
clingy.Transform(parseLocation),
).(Location)
clingy.Transform(ulloc.Parse),
).(ulloc.Location)
c.entry = a.New("entry", "Metadata entry to get", clingy.Optional).(*string)
}

View File

@ -7,6 +7,8 @@ import (
"strconv"
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulloc"
)
type cmdRm struct {
@ -15,7 +17,7 @@ type cmdRm struct {
recursive bool
encrypted bool
location Location
location ulloc.Location
}
func (c *cmdRm) Setup(a clingy.Arguments, f clingy.Flags) {
@ -30,8 +32,8 @@ func (c *cmdRm) Setup(a clingy.Arguments, f clingy.Flags) {
).(bool)
c.location = a.New("location", "Location to remove (sj://BUCKET[/KEY])",
clingy.Transform(parseLocation),
).(Location)
clingy.Transform(ulloc.Parse),
).(ulloc.Location)
}
func (c *cmdRm) Execute(ctx clingy.Context) error {

View File

@ -1,62 +0,0 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"context"
"github.com/zeebo/clingy"
)
//
// filesystemMixed dispatches to either the local or remote filesystem depending on the path
//
type filesystemMixed struct {
local *filesystemLocal
remote *filesystemRemote
}
func (m *filesystemMixed) Close() error {
return m.remote.Close()
}
func (m *filesystemMixed) Open(ctx clingy.Context, loc Location) (readHandle, error) {
if loc.Remote() {
return m.remote.Open(ctx, loc.bucket, loc.key)
} else if loc.Std() {
return newGenericReadHandle(ctx.Stdin()), nil
}
return m.local.Open(ctx, loc.path)
}
func (m *filesystemMixed) Create(ctx clingy.Context, loc Location) (writeHandle, error) {
if loc.Remote() {
return m.remote.Create(ctx, loc.bucket, loc.key)
} else if loc.Std() {
return newGenericWriteHandle(ctx.Stdout()), nil
}
return m.local.Create(ctx, loc.path)
}
func (m *filesystemMixed) ListObjects(ctx context.Context, prefix Location, recursive bool) (objectIterator, error) {
if prefix.Remote() {
return m.remote.ListObjects(ctx, prefix.bucket, prefix.key, recursive), nil
}
return m.local.ListObjects(ctx, prefix.path, recursive)
}
func (m *filesystemMixed) ListUploads(ctx context.Context, prefix Location, recursive bool) (objectIterator, error) {
if prefix.Remote() {
return m.remote.ListPendingMultiparts(ctx, prefix.bucket, prefix.key, recursive), nil
}
return emptyObjectIterator{}, nil
}
func (m *filesystemMixed) IsLocalDir(ctx context.Context, loc Location) bool {
if !loc.Local() {
return false
}
return m.local.IsLocalDir(ctx, loc.path)
}

View File

@ -1,476 +0,0 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"bytes"
"context"
"sort"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
)
//
// helpers to execute commands for tests
//
func Setup(t *testing.T, opts ...ExecuteOption) State {
return State{
opts: opts,
}
}
type State struct {
opts []ExecuteOption
}
// Succeed is the same as Run followed by result.RequireSuccess.
func (st State) Succeed(t *testing.T, args ...string) Result {
result := st.Run(t, args...)
result.RequireSuccess(t)
return result
}
// Fail is the same as Run followed by result.RequireFailure.
func (st State) Fail(t *testing.T, args ...string) Result {
result := st.Run(t, args...)
result.RequireFailure(t)
return result
}
func (st State) Run(t *testing.T, args ...string) Result {
var stdout bytes.Buffer
var stderr bytes.Buffer
var stdin bytes.Buffer
var ops []Operation
var ran bool
ok, err := clingy.Environment{
Name: "uplink-test",
Args: args,
Stdin: &stdin,
Stdout: &stdout,
Stderr: &stderr,
Wrap: func(ctx clingy.Context, cmd clingy.Cmd) error {
tfs := newTestFilesystem()
for _, opt := range st.opts {
if err := opt(ctx, tfs); err != nil {
return errs.Wrap(err)
}
}
tfs.ops = nil
if len(tfs.stdin) > 0 {
_, _ = stdin.WriteString(tfs.stdin)
}
if setter, ok := cmd.(interface {
setTestFilesystem(filesystem)
}); ok {
setter.setTestFilesystem(tfs)
}
ran = true
err := cmd.Execute(ctx)
ops = tfs.ops
return err
},
}.Run(context.Background(), commands)
if ok && err == nil {
require.True(t, ran, "no command was executed: %q", args)
}
return Result{
Stdout: stdout.String(),
Stderr: stderr.String(),
Ok: ok,
Err: err,
Operations: ops,
}
}
type ExecuteOption func(ctx clingy.Context, tfs *testFilesystem) error
func WithFile(location string) ExecuteOption {
return func(ctx clingy.Context, tfs *testFilesystem) error {
loc, err := parseLocation(location)
if err != nil {
return err
}
if loc.Remote() {
tfs.ensureBucket(loc.bucket)
}
wh, err := tfs.Create(ctx, loc)
if err != nil {
return err
}
return wh.Commit()
}
}
func WithPendingFile(location string) ExecuteOption {
return func(ctx clingy.Context, tfs *testFilesystem) error {
loc, err := parseLocation(location)
if err != nil {
return err
}
if loc.Remote() {
tfs.ensureBucket(loc.bucket)
}
_, err = tfs.Create(ctx, loc)
return err
}
}
//
// execution results
//
type Result struct {
Stdout string
Stderr string
Ok bool
Err error
Operations []Operation
}
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.NoError(t, r.Err)
}
func (r Result) RequireFailure(t *testing.T) {
require.False(t, r.Ok && r.Err == nil, "command ran with no error")
}
func (r Result) RequireStdout(t *testing.T, stdout string) {
require.Equal(t, trimNewlineSpaces(stdout), trimNewlineSpaces(r.Stdout))
}
func (r Result) RequireStderr(t *testing.T, stderr string) {
require.Equal(t, trimNewlineSpaces(stderr), trimNewlineSpaces(r.Stderr))
}
func parseErrors(s string) []string {
lines := strings.Split(s, "\n")
start := 0
for i, line := range lines {
if line == "Errors:" {
start = i + 1
} else if len(line) > 0 && line[0] != ' ' {
return lines[start:i]
}
}
return nil
}
func trimNewlineSpaces(s string) string {
lines := strings.Split(s, "\n")
j := 0
for _, line := range lines {
if trimmed := strings.TrimSpace(line); len(trimmed) > 0 {
lines[j] = trimmed
j++
}
}
return strings.Join(lines[:j], "\n")
}
type Operation struct {
Kind string
Loc string
Error bool
}
func newOp(kind string, loc Location, err error) Operation {
return Operation{
Kind: kind,
Loc: loc.String(),
Error: err != nil,
}
}
//
// filesystem
//
type testFilesystem struct {
stdin string
ops []Operation
created int64
files map[Location]byteFileData
pending map[Location][]*byteWriteHandle
buckets map[string]struct{}
}
func newTestFilesystem() *testFilesystem {
return &testFilesystem{
files: make(map[Location]byteFileData),
pending: make(map[Location][]*byteWriteHandle),
buckets: make(map[string]struct{}),
}
}
type byteFileData struct {
data []byte
created int64
}
func (tfs *testFilesystem) ensureBucket(name string) {
tfs.buckets[name] = struct{}{}
}
func (tfs *testFilesystem) Close() error {
return nil
}
func (tfs *testFilesystem) Open(ctx clingy.Context, loc Location) (_ readHandle, err error) {
defer func() { tfs.ops = append(tfs.ops, newOp("open", loc, err)) }()
bf, ok := tfs.files[loc]
if !ok {
return nil, errs.New("file does not exist")
}
return &byteReadHandle{Buffer: bytes.NewBuffer(bf.data)}, nil
}
func (tfs *testFilesystem) Create(ctx clingy.Context, loc Location) (_ writeHandle, err error) {
defer func() { tfs.ops = append(tfs.ops, newOp("create", loc, err)) }()
if loc.Remote() {
if _, ok := tfs.buckets[loc.bucket]; !ok {
return nil, errs.New("bucket %q does not exist", loc.bucket)
}
}
tfs.created++
wh := &byteWriteHandle{
buf: bytes.NewBuffer(nil),
loc: loc,
tfs: tfs,
cre: tfs.created,
}
tfs.pending[loc] = append(tfs.pending[loc], wh)
return wh, nil
}
func (tfs *testFilesystem) ListObjects(ctx context.Context, prefix Location, recursive bool) (objectIterator, error) {
var infos []objectInfo
for loc, bf := range tfs.files {
if loc.HasPrefix(prefix) {
infos = append(infos, objectInfo{
Loc: loc,
Created: time.Unix(bf.created, 0),
})
}
}
sort.Sort(objectInfos(infos))
if !recursive {
infos = collapseObjectInfos(prefix, infos)
}
return &objectInfoIterator{infos: infos}, nil
}
func (tfs *testFilesystem) ListUploads(ctx context.Context, prefix Location, recursive bool) (objectIterator, error) {
var infos []objectInfo
for loc, whs := range tfs.pending {
if loc.HasPrefix(prefix) {
for _, wh := range whs {
infos = append(infos, objectInfo{
Loc: loc,
Created: time.Unix(wh.cre, 0),
})
}
}
}
sort.Sort(objectInfos(infos))
if !recursive {
infos = collapseObjectInfos(prefix, infos)
}
return &objectInfoIterator{infos: infos}, nil
}
func (tfs *testFilesystem) IsLocalDir(ctx context.Context, loc Location) bool {
// TODO: implement this
return false
}
//
// readHandle
//
type byteReadHandle struct {
*bytes.Buffer
}
func (b *byteReadHandle) Close() error { return nil }
func (b *byteReadHandle) Info() objectInfo { return objectInfo{} }
//
// writeHandle
//
type byteWriteHandle struct {
buf *bytes.Buffer
loc Location
tfs *testFilesystem
cre int64
done bool
}
func (b *byteWriteHandle) Write(p []byte) (int, error) {
return b.buf.Write(p)
}
func (b *byteWriteHandle) Commit() error {
if err := b.close(); err != nil {
return err
}
b.tfs.ops = append(b.tfs.ops, newOp("commit", b.loc, nil))
b.tfs.files[b.loc] = byteFileData{
data: b.buf.Bytes(),
created: b.cre,
}
return nil
}
func (b *byteWriteHandle) Abort() error {
if err := b.close(); err != nil {
return err
}
b.tfs.ops = append(b.tfs.ops, newOp("append", b.loc, nil))
return nil
}
func (b *byteWriteHandle) close() error {
if b.done {
return errs.New("already done")
}
b.done = true
handles := b.tfs.pending[b.loc]
for i, v := range handles {
if v == b {
handles = append(handles[:i], handles[i+1:]...)
break
}
}
if len(handles) > 0 {
b.tfs.pending[b.loc] = handles
} else {
delete(b.tfs.pending, b.loc)
}
return nil
}
//
// objectIterator
//
type objectInfoIterator struct {
infos []objectInfo
current objectInfo
}
func (li *objectInfoIterator) Next() bool {
if len(li.infos) == 0 {
return false
}
li.current, li.infos = li.infos[0], li.infos[1:]
return true
}
func (li *objectInfoIterator) Err() error {
return nil
}
func (li *objectInfoIterator) Item() objectInfo {
return li.current
}
type objectInfos []objectInfo
func (ois objectInfos) Len() int { return len(ois) }
func (ois objectInfos) Swap(i int, j int) { ois[i], ois[j] = ois[j], ois[i] }
func (ois objectInfos) Less(i int, j int) bool {
li, lj := ois[i].Loc, ois[j].Loc
if !li.remote && lj.remote {
return true
} else if !lj.remote && li.remote {
return false
}
if li.bucket < lj.bucket {
return true
} else if lj.bucket < li.bucket {
return false
}
if li.key < lj.key {
return true
} else if lj.key < li.key {
return false
}
if li.path < lj.path {
return true
} else if lj.path < li.path {
return false
}
return false
}
func collapseObjectInfos(prefix Location, infos []objectInfo) []objectInfo {
collapsing := false
current := ""
j := 0
for _, oi := range infos {
first, ok := oi.Loc.ListKeyName(prefix)
if ok {
if collapsing && first == current {
continue
}
collapsing = true
current = first
oi.IsPrefix = true
}
oi.Loc = oi.Loc.SetKey(first)
infos[j] = oi
j++
}
return infos[:j]
}

View File

@ -8,6 +8,7 @@ import (
"github.com/zeebo/clingy"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/uplink"
privateAccess "storj.io/uplink/private/access"
)
@ -16,16 +17,16 @@ type projectProvider struct {
access string
testProject *uplink.Project
testFilesystem filesystem
testFilesystem ulfs.Filesystem
}
func (pp *projectProvider) Setup(a clingy.Arguments, f clingy.Flags) {
pp.access = f.New("access", "Which access to use", "").(string)
}
func (pp *projectProvider) setTestFilesystem(fs filesystem) { pp.testFilesystem = fs }
func (pp *projectProvider) SetTestFilesystem(fs ulfs.Filesystem) { pp.testFilesystem = fs }
func (pp *projectProvider) OpenFilesystem(ctx context.Context, options ...projectOption) (filesystem, error) {
func (pp *projectProvider) OpenFilesystem(ctx context.Context, options ...projectOption) (ulfs.Filesystem, error) {
if pp.testFilesystem != nil {
return pp.testFilesystem, nil
}
@ -34,12 +35,7 @@ func (pp *projectProvider) OpenFilesystem(ctx context.Context, options ...projec
if err != nil {
return nil, err
}
return &filesystemMixed{
local: &filesystemLocal{},
remote: &filesystemRemote{
project: project,
},
}, nil
return ulfs.NewMixed(ulfs.NewLocal(), ulfs.NewRemote(project)), nil
}
func (pp *projectProvider) OpenProject(ctx context.Context, options ...projectOption) (*uplink.Project, error) {

View File

@ -1,7 +1,7 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
package ulfs
import (
"context"
@ -13,40 +13,37 @@ import (
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulloc"
"storj.io/uplink"
)
// filesystem represents either the local filesystem or the data backed by a project.
type filesystem interface {
// Filesystem represents either the local Filesystem or the data backed by a project.
type Filesystem interface {
Close() error
Open(ctx clingy.Context, loc Location) (readHandle, error)
Create(ctx clingy.Context, loc Location) (writeHandle, error)
ListObjects(ctx context.Context, prefix Location, recursive bool) (objectIterator, error)
ListUploads(ctx context.Context, prefix Location, recursive bool) (objectIterator, error)
IsLocalDir(ctx context.Context, loc Location) bool
Open(ctx clingy.Context, loc ulloc.Location) (ReadHandle, error)
Create(ctx clingy.Context, loc ulloc.Location) (WriteHandle, error)
ListObjects(ctx context.Context, prefix ulloc.Location, recursive bool) (ObjectIterator, error)
ListUploads(ctx context.Context, prefix ulloc.Location, recursive bool) (ObjectIterator, error)
IsLocalDir(ctx context.Context, loc ulloc.Location) bool
}
//
// object info
//
// objectInfo is a simpler *uplink.Object that contains the minimal information the
// ObjectInfo is a simpler *uplink.Object that contains the minimal information the
// uplink command needs that multiple types can be converted to.
type objectInfo struct {
Loc Location
type ObjectInfo struct {
Loc ulloc.Location
IsPrefix bool
Created time.Time
ContentLength int64
}
// uplinkObjectToObjectInfo returns an objectInfo converted from an *uplink.Object.
func uplinkObjectToObjectInfo(bucket string, obj *uplink.Object) objectInfo {
return objectInfo{
Loc: Location{
bucket: bucket,
key: obj.Key,
remote: true,
},
func uplinkObjectToObjectInfo(bucket string, obj *uplink.Object) ObjectInfo {
return ObjectInfo{
Loc: ulloc.NewRemote(bucket, obj.Key),
IsPrefix: obj.IsPrefix,
Created: obj.System.Created,
ContentLength: obj.System.ContentLength,
@ -54,13 +51,9 @@ func uplinkObjectToObjectInfo(bucket string, obj *uplink.Object) objectInfo {
}
// uplinkUploadInfoToObjectInfo returns an objectInfo converted from an *uplink.Object.
func uplinkUploadInfoToObjectInfo(bucket string, upl *uplink.UploadInfo) objectInfo {
return objectInfo{
Loc: Location{
bucket: bucket,
key: upl.Key,
remote: true,
},
func uplinkUploadInfoToObjectInfo(bucket string, upl *uplink.UploadInfo) ObjectInfo {
return ObjectInfo{
Loc: ulloc.NewRemote(bucket, upl.Key),
IsPrefix: upl.IsPrefix,
Created: upl.System.Created,
ContentLength: upl.System.ContentLength,
@ -71,11 +64,11 @@ func uplinkUploadInfoToObjectInfo(bucket string, upl *uplink.UploadInfo) objectI
// read handles
//
// readHandle is something that can be read from.
type readHandle interface {
// ReadHandle is something that can be read from.
type ReadHandle interface {
io.Reader
io.Closer
Info() objectInfo
Info() ObjectInfo
}
// uplinkReadHandle implements readHandle for *uplink.Downloads.
@ -94,12 +87,12 @@ func newUplinkReadHandle(bucket string, dl *uplink.Download) *uplinkReadHandle {
func (u *uplinkReadHandle) Read(p []byte) (int, error) { return u.dl.Read(p) }
func (u *uplinkReadHandle) Close() error { return u.dl.Close() }
func (u *uplinkReadHandle) Info() objectInfo { return uplinkObjectToObjectInfo(u.bucket, u.dl.Info()) }
func (u *uplinkReadHandle) Info() ObjectInfo { return uplinkObjectToObjectInfo(u.bucket, u.dl.Info()) }
// osReadHandle implements readHandle for *os.Files.
type osReadHandle struct {
raw *os.File
info objectInfo
info ObjectInfo
}
// newOsReadHandle constructs an *osReadHandle from an *os.File.
@ -110,8 +103,8 @@ func newOSReadHandle(fh *os.File) (*osReadHandle, error) {
}
return &osReadHandle{
raw: fh,
info: objectInfo{
Loc: Location{path: fh.Name()},
info: ObjectInfo{
Loc: ulloc.NewLocal(fh.Name()),
IsPrefix: false,
Created: fi.ModTime(), // TODO: os specific crtime
ContentLength: fi.Size(),
@ -121,7 +114,7 @@ func newOSReadHandle(fh *os.File) (*osReadHandle, error) {
func (o *osReadHandle) Read(p []byte) (int, error) { return o.raw.Read(p) }
func (o *osReadHandle) Close() error { return o.raw.Close() }
func (o *osReadHandle) Info() objectInfo { return o.info }
func (o *osReadHandle) Info() ObjectInfo { return o.info }
// genericReadHandle implements readHandle for an io.Reader.
type genericReadHandle struct{ r io.Reader }
@ -133,14 +126,14 @@ func newGenericReadHandle(r io.Reader) *genericReadHandle {
func (g *genericReadHandle) Read(p []byte) (int, error) { return g.r.Read(p) }
func (g *genericReadHandle) Close() error { return nil }
func (g *genericReadHandle) Info() objectInfo { return objectInfo{ContentLength: -1} }
func (g *genericReadHandle) Info() ObjectInfo { return ObjectInfo{ContentLength: -1} }
//
// write handles
//
// writeHandle is anything that can be written to with commit/abort semantics.
type writeHandle interface {
// WriteHandle is anything that can be written to with commit/abort semantics.
type WriteHandle interface {
io.Writer
Commit() error
Abort() error
@ -212,11 +205,11 @@ func (g *genericWriteHandle) Abort() error { return nil }
// object iteration
//
// objectIterator is an interface type for iterating over objectInfo values.
type objectIterator interface {
// ObjectIterator is an interface type for iterating over objectInfo values.
type ObjectIterator interface {
Next() bool
Err() error
Item() objectInfo
Item() ObjectInfo
}
// filteredObjectIterator removes any iteration entries that do not begin with the filter.
@ -225,7 +218,7 @@ type objectIterator interface {
type filteredObjectIterator struct {
trim string
filter string
iter objectIterator
iter ObjectIterator
}
func (f *filteredObjectIterator) Next() bool {
@ -245,15 +238,9 @@ func (f *filteredObjectIterator) Next() bool {
func (f *filteredObjectIterator) Err() error { return f.iter.Err() }
func (f *filteredObjectIterator) Item() objectInfo {
func (f *filteredObjectIterator) Item() ObjectInfo {
item := f.iter.Item()
path := item.Loc
if path.remote {
path.key = path.key[len(f.trim):]
} else {
path.path = path.path[len(f.trim):]
}
item.Loc = path
item.Loc = item.Loc.RemoveKeyPrefix(f.trim)
return item
}
@ -262,4 +249,4 @@ type emptyObjectIterator struct{}
func (emptyObjectIterator) Next() bool { return false }
func (emptyObjectIterator) Err() error { return nil }
func (emptyObjectIterator) Item() objectInfo { return objectInfo{} }
func (emptyObjectIterator) Item() ObjectInfo { return ObjectInfo{} }

View File

@ -1,7 +1,7 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
package ulfs
import (
"context"
@ -11,12 +11,19 @@ import (
"strings"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
// filesystemLocal implements something close to a filesystem but backed by the local disk.
type filesystemLocal struct{}
// Local implements something close to a filesystem but backed by the local disk.
type Local struct{}
func (l *filesystemLocal) abs(path string) (string, error) {
// NewLocal constructs a Local filesystem.
func NewLocal() *Local {
return &Local{}
}
func (l *Local) abs(path string) (string, error) {
abs, err := filepath.Abs(path)
if err != nil {
return "", errs.Wrap(err)
@ -28,7 +35,8 @@ func (l *filesystemLocal) abs(path string) (string, error) {
return abs, nil
}
func (l *filesystemLocal) Open(ctx context.Context, path string) (readHandle, error) {
// Open returns a read ReadHandle for the given local path.
func (l *Local) Open(ctx context.Context, path string) (ReadHandle, error) {
path, err := l.abs(path)
if err != nil {
return nil, err
@ -41,7 +49,8 @@ func (l *filesystemLocal) Open(ctx context.Context, path string) (readHandle, er
return newOSReadHandle(fh)
}
func (l *filesystemLocal) Create(ctx context.Context, path string) (writeHandle, error) {
// Create makes any directories necessary to create a file at path and returns a WriteHandle.
func (l *Local) Create(ctx context.Context, path string) (WriteHandle, error) {
path, err := l.abs(path)
if err != nil {
return nil, err
@ -66,7 +75,9 @@ func (l *filesystemLocal) Create(ctx context.Context, path string) (writeHandle,
return newOSWriteHandle(fh), nil
}
func (l *filesystemLocal) ListObjects(ctx context.Context, path string, recursive bool) (objectIterator, error) {
// ListObjects returns an ObjectIterator listing files and directories that have string prefix
// with the provided path.
func (l *Local) ListObjects(ctx context.Context, path string, recursive bool) (ObjectIterator, error) {
path, err := l.abs(path)
if err != nil {
return nil, err
@ -110,7 +121,8 @@ func (l *filesystemLocal) ListObjects(ctx context.Context, path string, recursiv
}, nil
}
func (l *filesystemLocal) IsLocalDir(ctx context.Context, path string) bool {
// IsLocalDir returns true if the path is a directory.
func (l *Local) IsLocalDir(ctx context.Context, path string) bool {
fi, err := os.Stat(path)
if err != nil {
return false
@ -141,7 +153,7 @@ func (fi *fileinfoObjectIterator) Next() bool {
func (fi *fileinfoObjectIterator) Err() error { return nil }
func (fi *fileinfoObjectIterator) Item() objectInfo {
func (fi *fileinfoObjectIterator) Item() ObjectInfo {
name := filepath.Join(fi.base, fi.current.Name())
isDir := fi.current.IsDir()
if isDir {
@ -154,8 +166,8 @@ func (fi *fileinfoObjectIterator) Item() objectInfo {
name = strings.ReplaceAll(name, string(filepath.Separator), "/")
}
return objectInfo{
Loc: Location{path: name},
return ObjectInfo{
Loc: ulloc.NewLocal(name),
IsPrefix: isDir,
Created: fi.current.ModTime(), // TODO: use real crtime
ContentLength: fi.current.Size(),

View File

@ -0,0 +1,81 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ulfs
import (
"context"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
// Mixed dispatches to either the local or remote filesystem depending on the location.
type Mixed struct {
local *Local
remote *Remote
}
// NewMixed returns a Mixed backed by the provided local and remote filesystems.
func NewMixed(local *Local, remote *Remote) *Mixed {
return &Mixed{
local: local,
remote: remote,
}
}
// Close releases any resources that the Mixed contails.
func (m *Mixed) Close() error {
return m.remote.Close()
}
// Open returns a ReadHandle to either a local file, remote object, or stdin.
func (m *Mixed) Open(ctx clingy.Context, loc ulloc.Location) (ReadHandle, error) {
if bucket, key, ok := loc.RemoteParts(); ok {
return m.remote.Open(ctx, bucket, key)
} else if path, ok := loc.LocalParts(); ok {
return m.local.Open(ctx, path)
}
return newGenericReadHandle(ctx.Stdin()), nil
}
// Create returns a WriteHandle to either a local file, remote object, or stdout.
func (m *Mixed) Create(ctx clingy.Context, loc ulloc.Location) (WriteHandle, error) {
if bucket, key, ok := loc.RemoteParts(); ok {
return m.remote.Create(ctx, bucket, key)
} else if path, ok := loc.LocalParts(); ok {
return m.local.Create(ctx, path)
}
return newGenericWriteHandle(ctx.Stdout()), nil
}
// ListObjects lists either files and directories with some local path prefix or remote objects
// with a given bucket and key.
func (m *Mixed) ListObjects(ctx context.Context, prefix ulloc.Location, recursive bool) (ObjectIterator, error) {
if bucket, key, ok := prefix.RemoteParts(); ok {
return m.remote.ListObjects(ctx, bucket, key, recursive), nil
} else if path, ok := prefix.LocalParts(); ok {
return m.local.ListObjects(ctx, path, recursive)
}
return nil, errs.New("unable to list objects for prefix %q", prefix)
}
// ListUploads lists all of the pending uploads for remote objects with some given bucket and key.
func (m *Mixed) ListUploads(ctx context.Context, prefix ulloc.Location, recursive bool) (ObjectIterator, error) {
if bucket, key, ok := prefix.RemoteParts(); ok {
return m.remote.ListUploads(ctx, bucket, key, recursive), nil
} else if prefix.Local() {
return emptyObjectIterator{}, nil
}
return nil, errs.New("unable to list uploads for prefix %q", prefix)
}
// IsLocalDir returns true if the location is a directory that is local.
func (m *Mixed) IsLocalDir(ctx context.Context, loc ulloc.Location) bool {
if path, ok := loc.LocalParts(); ok {
return m.local.IsLocalDir(ctx, path)
}
return false
}

View File

@ -1,7 +1,7 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
package ulfs
import (
"context"
@ -12,16 +12,25 @@ import (
"storj.io/uplink"
)
// filesystemRemote implements something close to a filesystem but backed by an uplink project.
type filesystemRemote struct {
// Remote implements something close to a filesystem but backed by an uplink project.
type Remote struct {
project *uplink.Project
}
func (r *filesystemRemote) Close() error {
// NewRemote returns something close to a filesystem and returns objects using the project.
func NewRemote(project *uplink.Project) *Remote {
return &Remote{
project: project,
}
}
// Close releases any resources that the Remote contains.
func (r *Remote) Close() error {
return r.project.Close()
}
func (r *filesystemRemote) Open(ctx context.Context, bucket, key string) (readHandle, error) {
// Open returns a ReadHandle for the object identified by a given bucket and key.
func (r *Remote) Open(ctx context.Context, bucket, key string) (ReadHandle, error) {
fh, err := r.project.DownloadObject(ctx, bucket, key, nil)
if err != nil {
return nil, errs.Wrap(err)
@ -29,7 +38,8 @@ func (r *filesystemRemote) Open(ctx context.Context, bucket, key string) (readHa
return newUplinkReadHandle(bucket, fh), nil
}
func (r *filesystemRemote) Create(ctx context.Context, bucket, key string) (writeHandle, error) {
// Create returns a WriteHandle for the object identified by a given bucket and key.
func (r *Remote) Create(ctx context.Context, bucket, key string) (WriteHandle, error) {
fh, err := r.project.UploadObject(ctx, bucket, key, nil)
if err != nil {
return nil, err
@ -37,7 +47,8 @@ func (r *filesystemRemote) Create(ctx context.Context, bucket, key string) (writ
return newUplinkWriteHandle(fh), nil
}
func (r *filesystemRemote) ListObjects(ctx context.Context, bucket, prefix string, recursive bool) objectIterator {
// ListObjects lists all of the objects in some bucket that begin with the given prefix.
func (r *Remote) ListObjects(ctx context.Context, bucket, prefix string, recursive bool) ObjectIterator {
parentPrefix := ""
if idx := strings.LastIndexByte(prefix, '/'); idx >= 0 {
parentPrefix = prefix[:idx+1]
@ -60,7 +71,8 @@ func (r *filesystemRemote) ListObjects(ctx context.Context, bucket, prefix strin
}
}
func (r *filesystemRemote) ListPendingMultiparts(ctx context.Context, bucket, prefix string, recursive bool) objectIterator {
// ListUploads lists all of the pending uploads in some bucket that begin with the given prefix.
func (r *Remote) ListUploads(ctx context.Context, bucket, prefix string, recursive bool) ObjectIterator {
parentPrefix := ""
if idx := strings.LastIndexByte(prefix, '/'); idx >= 0 {
parentPrefix = prefix[:idx+1]
@ -99,7 +111,7 @@ func newUplinkObjectIterator(bucket string, iter *uplink.ObjectIterator) *uplink
func (u *uplinkObjectIterator) Next() bool { return u.iter.Next() }
func (u *uplinkObjectIterator) Err() error { return u.iter.Err() }
func (u *uplinkObjectIterator) Item() objectInfo {
func (u *uplinkObjectIterator) Item() ObjectInfo {
return uplinkObjectToObjectInfo(u.bucket, u.iter.Item())
}
@ -119,6 +131,6 @@ func newUplinkUploadIterator(bucket string, iter *uplink.UploadIterator) *uplink
func (u *uplinkUploadIterator) Next() bool { return u.iter.Next() }
func (u *uplinkUploadIterator) Err() error { return u.iter.Err() }
func (u *uplinkUploadIterator) Item() objectInfo {
func (u *uplinkUploadIterator) Item() ObjectInfo {
return uplinkUploadInfoToObjectInfo(u.bucket, u.iter.Item())
}

View File

@ -1,7 +1,7 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package main
package ulloc
import (
"fmt"
@ -19,9 +19,30 @@ type Location struct {
remote bool
}
func parseLocation(location string) (p Location, err error) {
// NewLocal returns a new Location that refers to a local path.
func NewLocal(path string) Location {
return Location{path: path}
}
// NewRemote returns a new location that refers to a remote path.
func NewRemote(bucket, key string) Location {
return Location{
bucket: bucket,
key: key,
remote: true,
}
}
// NewStd returns a new location that refers to stdin or stdout.
func NewStd() Location {
return Location{path: "-", key: "-"}
}
// 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) {
if location == "-" {
return Location{path: "-", key: "-"}, nil
return NewStd(), nil
}
// Locations, Chapter 2, Verses 9 to 21.
@ -199,3 +220,55 @@ func (p Location) ListKeyName(prefix Location) (string, bool) {
}
return rem, false
}
// RemoveKeyPrefix removes the prefix from the key or path in the location if they
// begin with it.
func (p Location) RemoveKeyPrefix(prefix string) Location {
if p.remote {
p.key = strings.TrimPrefix(p.key, prefix)
} else {
p.path = strings.TrimPrefix(p.path, prefix)
}
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) {
return p.bucket, p.key, p.Remote()
}
// 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) {
return p.path, p.Local()
}
// Less returns true if the location is less than the passed in location.
func (p Location) Less(q Location) bool {
if !p.remote && q.remote {
return true
} else if !q.remote && p.remote {
return false
}
if p.bucket < q.bucket {
return true
} else if q.bucket < p.bucket {
return false
}
if p.key < q.key {
return true
} else if q.key < p.key {
return false
}
if p.path < q.path {
return true
} else if q.path < p.path {
return false
}
return false
}

View File

@ -0,0 +1,261 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ultest
import (
"bytes"
"context"
"sort"
"time"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
//
// ulfs.Filesystem
//
type testFilesystem struct {
stdin string
ops []Operation
created int64
files map[ulloc.Location]byteFileData
pending map[ulloc.Location][]*byteWriteHandle
buckets map[string]struct{}
}
func newTestFilesystem() *testFilesystem {
return &testFilesystem{
files: make(map[ulloc.Location]byteFileData),
pending: make(map[ulloc.Location][]*byteWriteHandle),
buckets: make(map[string]struct{}),
}
}
type byteFileData struct {
data []byte
created int64
}
func (tfs *testFilesystem) ensureBucket(name string) {
tfs.buckets[name] = struct{}{}
}
func (tfs *testFilesystem) Close() error {
return nil
}
func (tfs *testFilesystem) Open(ctx clingy.Context, loc ulloc.Location) (_ ulfs.ReadHandle, err error) {
defer func() { tfs.ops = append(tfs.ops, newOp("open", loc, err)) }()
bf, ok := tfs.files[loc]
if !ok {
return nil, errs.New("file does not exist")
}
return &byteReadHandle{Buffer: bytes.NewBuffer(bf.data)}, nil
}
func (tfs *testFilesystem) Create(ctx clingy.Context, loc ulloc.Location) (_ ulfs.WriteHandle, err error) {
defer func() { tfs.ops = append(tfs.ops, newOp("create", loc, err)) }()
if bucket, _, ok := loc.RemoteParts(); ok {
if _, ok := tfs.buckets[bucket]; !ok {
return nil, errs.New("bucket %q does not exist", bucket)
}
}
tfs.created++
wh := &byteWriteHandle{
buf: bytes.NewBuffer(nil),
loc: loc,
tfs: tfs,
cre: tfs.created,
}
tfs.pending[loc] = append(tfs.pending[loc], wh)
return wh, nil
}
func (tfs *testFilesystem) ListObjects(ctx context.Context, prefix ulloc.Location, recursive bool) (ulfs.ObjectIterator, error) {
var infos []ulfs.ObjectInfo
for loc, bf := range tfs.files {
if loc.HasPrefix(prefix) {
infos = append(infos, ulfs.ObjectInfo{
Loc: loc,
Created: time.Unix(bf.created, 0),
})
}
}
sort.Sort(objectInfos(infos))
if !recursive {
infos = collapseObjectInfos(prefix, infos)
}
return &objectInfoIterator{infos: infos}, nil
}
func (tfs *testFilesystem) ListUploads(ctx context.Context, prefix ulloc.Location, recursive bool) (ulfs.ObjectIterator, error) {
var infos []ulfs.ObjectInfo
for loc, whs := range tfs.pending {
if loc.HasPrefix(prefix) {
for _, wh := range whs {
infos = append(infos, ulfs.ObjectInfo{
Loc: loc,
Created: time.Unix(wh.cre, 0),
})
}
}
}
sort.Sort(objectInfos(infos))
if !recursive {
infos = collapseObjectInfos(prefix, infos)
}
return &objectInfoIterator{infos: infos}, nil
}
func (tfs *testFilesystem) IsLocalDir(ctx context.Context, loc ulloc.Location) bool {
// TODO: implement this
return false
}
//
// ulfs.ReadHandle
//
type byteReadHandle struct {
*bytes.Buffer
}
func (b *byteReadHandle) Close() error { return nil }
func (b *byteReadHandle) Info() ulfs.ObjectInfo { return ulfs.ObjectInfo{} }
//
// ulfs.WriteHandle
//
type byteWriteHandle struct {
buf *bytes.Buffer
loc ulloc.Location
tfs *testFilesystem
cre int64
done bool
}
func (b *byteWriteHandle) Write(p []byte) (int, error) {
return b.buf.Write(p)
}
func (b *byteWriteHandle) Commit() error {
if err := b.close(); err != nil {
return err
}
b.tfs.ops = append(b.tfs.ops, newOp("commit", b.loc, nil))
b.tfs.files[b.loc] = byteFileData{
data: b.buf.Bytes(),
created: b.cre,
}
return nil
}
func (b *byteWriteHandle) Abort() error {
if err := b.close(); err != nil {
return err
}
b.tfs.ops = append(b.tfs.ops, newOp("append", b.loc, nil))
return nil
}
func (b *byteWriteHandle) close() error {
if b.done {
return errs.New("already done")
}
b.done = true
handles := b.tfs.pending[b.loc]
for i, v := range handles {
if v == b {
handles = append(handles[:i], handles[i+1:]...)
break
}
}
if len(handles) > 0 {
b.tfs.pending[b.loc] = handles
} else {
delete(b.tfs.pending, b.loc)
}
return nil
}
//
// ulfs.ObjectIterator
//
type objectInfoIterator struct {
infos []ulfs.ObjectInfo
current ulfs.ObjectInfo
}
func (li *objectInfoIterator) Next() bool {
if len(li.infos) == 0 {
return false
}
li.current, li.infos = li.infos[0], li.infos[1:]
return true
}
func (li *objectInfoIterator) Err() error {
return nil
}
func (li *objectInfoIterator) Item() ulfs.ObjectInfo {
return li.current
}
type objectInfos []ulfs.ObjectInfo
func (ois objectInfos) Len() int { return len(ois) }
func (ois objectInfos) Swap(i int, j int) { ois[i], ois[j] = ois[j], ois[i] }
func (ois objectInfos) Less(i int, j int) bool { return ois[i].Loc.Less(ois[j].Loc) }
func collapseObjectInfos(prefix ulloc.Location, infos []ulfs.ObjectInfo) []ulfs.ObjectInfo {
collapsing := false
current := ""
j := 0
for _, oi := range infos {
first, ok := oi.Loc.ListKeyName(prefix)
if ok {
if collapsing && first == current {
continue
}
collapsing = true
current = first
oi.IsPrefix = true
}
oi.Loc = oi.Loc.SetKey(first)
infos[j] = oi
j++
}
return infos[:j]
}

View File

@ -0,0 +1,91 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ultest
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
"storj.io/storj/cmd/uplinkng/ulloc"
)
// Result captures all the output of running a command for inspection.
type Result struct {
Stdout string
Stderr string
Ok bool
Err error
Operations []Operation
}
// RequireSuccess fails if the Result did not observe a successful execution.
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.NoError(t, r.Err)
}
// RequireFailure fails if the Result did not observe a failed execution.
func (r Result) RequireFailure(t *testing.T) {
require.False(t, r.Ok && r.Err == nil, "command ran with no error")
}
// RequireStdout requires that the execution wrote to stdout the provided string.
// Blank lines are ignored and all lines are space trimmed for the comparison.
func (r Result) RequireStdout(t *testing.T, stdout string) {
require.Equal(t, trimNewlineSpaces(stdout), trimNewlineSpaces(r.Stdout))
}
// RequireStderr requires that the execution wrote to stderr the provided string.
// Blank lines are ignored and all lines are space trimmed for the comparison.
func (r Result) RequireStderr(t *testing.T, stderr string) {
require.Equal(t, trimNewlineSpaces(stderr), trimNewlineSpaces(r.Stderr))
}
func parseErrors(s string) []string {
lines := strings.Split(s, "\n")
start := 0
for i, line := range lines {
if line == "Errors:" {
start = i + 1
} else if len(line) > 0 && line[0] != ' ' {
return lines[start:i]
}
}
return nil
}
func trimNewlineSpaces(s string) string {
lines := strings.Split(s, "\n")
j := 0
for _, line := range lines {
if trimmed := strings.TrimSpace(line); len(trimmed) > 0 {
lines[j] = trimmed
j++
}
}
return strings.Join(lines[:j], "\n")
}
// Operation represents some kind of filesystem operation that happened
// on some location, and if the operation failed.
type Operation struct {
Kind string
Loc string
Error bool
}
func newOp(kind string, loc ulloc.Location, err error) Operation {
return Operation{
Kind: kind,
Loc: loc.String(),
Error: err != nil,
}
}

View File

@ -0,0 +1,154 @@
// Copyright (C) 2021 Storj Labs, Inc.
// See LICENSE for copying information.
package ultest
import (
"bytes"
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/zeebo/clingy"
"github.com/zeebo/errs"
"storj.io/storj/cmd/uplinkng/ulfs"
"storj.io/storj/cmd/uplinkng/ulloc"
)
// Setup returns some State that can be run multiple times with different command
// line arguments.
func Setup(cmds func(clingy.Commands, clingy.Flags), opts ...ExecuteOption) State {
return State{
cmds: cmds,
opts: opts,
}
}
// State represents some state and environment for a command to execute in.
type State struct {
cmds func(clingy.Commands, clingy.Flags)
opts []ExecuteOption
}
// With appends the provided options and returns a new State.
func (st State) With(opts ...ExecuteOption) State {
st.opts = append([]ExecuteOption(nil), st.opts...)
st.opts = append(st.opts, opts...)
return st
}
// Succeed is the same as Run followed by result.RequireSuccess.
func (st State) Succeed(t *testing.T, args ...string) Result {
result := st.Run(t, args...)
result.RequireSuccess(t)
return result
}
// Fail is the same as Run followed by result.RequireFailure.
func (st State) Fail(t *testing.T, args ...string) Result {
result := st.Run(t, args...)
result.RequireFailure(t)
return result
}
// Run executes the command specified by the args and returns a Result.
func (st State) Run(t *testing.T, args ...string) Result {
var stdout bytes.Buffer
var stderr bytes.Buffer
var stdin bytes.Buffer
var ops []Operation
var ran bool
ok, err := clingy.Environment{
Name: "uplink-test",
Args: args,
Stdin: &stdin,
Stdout: &stdout,
Stderr: &stderr,
Wrap: func(ctx clingy.Context, cmd clingy.Cmd) error {
tfs := newTestFilesystem()
for _, opt := range st.opts {
if err := opt.fn(ctx, tfs); err != nil {
return errs.Wrap(err)
}
}
tfs.ops = nil
if len(tfs.stdin) > 0 {
_, _ = stdin.WriteString(tfs.stdin)
}
if setter, ok := cmd.(interface {
SetTestFilesystem(ulfs.Filesystem)
}); ok {
setter.SetTestFilesystem(tfs)
}
ran = true
err := cmd.Execute(ctx)
ops = tfs.ops
return err
},
}.Run(context.Background(), st.cmds)
if ok && err == nil {
require.True(t, ran, "no command was executed: %q", args)
}
return Result{
Stdout: stdout.String(),
Stderr: stderr.String(),
Ok: ok,
Err: err,
Operations: ops,
}
}
// ExecuteOption allows one to control the environment that a command executes in.
type ExecuteOption struct {
fn func(ctx clingy.Context, tfs *testFilesystem) error
}
// WithStdin sets the command to execute with the provided string as standard input.
func WithStdin(stdin string) ExecuteOption {
return ExecuteOption{func(_ clingy.Context, tfs *testFilesystem) error {
tfs.stdin = stdin
return nil
}}
}
// WithFile sets the command to execute with a file created at the given location.
func WithFile(location string) ExecuteOption {
return ExecuteOption{func(ctx clingy.Context, tfs *testFilesystem) error {
loc, err := ulloc.Parse(location)
if err != nil {
return err
}
if bucket, _, ok := loc.RemoteParts(); ok {
tfs.ensureBucket(bucket)
}
wh, err := tfs.Create(ctx, loc)
if err != nil {
return err
}
return wh.Commit()
}}
}
// WithPendingFile sets the command to execute with a pending upload happening to
// the provided location.
func WithPendingFile(location string) ExecuteOption {
return ExecuteOption{func(ctx clingy.Context, tfs *testFilesystem) error {
loc, err := ulloc.Parse(location)
if err != nil {
return err
}
if bucket, _, ok := loc.RemoteParts(); ok {
tfs.ensureBucket(bucket)
}
_, err = tfs.Create(ctx, loc)
return err
}}
}