Metadata Access from Uplink CLI (#3310)
This commit is contained in:
parent
fd9f860fd6
commit
3b78addb2d
@ -5,6 +5,7 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -24,6 +25,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
progress *bool
|
progress *bool
|
||||||
expires *string
|
expires *string
|
||||||
|
metadata *string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -32,8 +34,10 @@ func init() {
|
|||||||
Short: "Copies a local file or Storj object to another location locally or in Storj",
|
Short: "Copies a local file or Storj object to another location locally or in Storj",
|
||||||
RunE: copyMain,
|
RunE: copyMain,
|
||||||
}, RootCmd)
|
}, RootCmd)
|
||||||
|
|
||||||
progress = cpCmd.Flags().Bool("progress", true, "if true, show progress")
|
progress = cpCmd.Flags().Bool("progress", true, "if true, show progress")
|
||||||
expires = cpCmd.Flags().String("expires", "", "optional expiration date of an object. Please use format (yyyy-mm-ddThh:mm:ssZhh:mm)")
|
expires = cpCmd.Flags().String("expires", "", "optional expiration date of an object. Please use format (yyyy-mm-ddThh:mm:ssZhh:mm)")
|
||||||
|
metadata = cpCmd.Flags().String("metadata", "", "optional metadata for the object. Please use a single level JSON object of string to string only")
|
||||||
}
|
}
|
||||||
|
|
||||||
// upload transfers src from local machine to s3 compatible object dst
|
// upload transfers src from local machine to s3 compatible object dst
|
||||||
@ -102,6 +106,17 @@ func upload(ctx context.Context, src fpath.FPath, dst fpath.FPath, showProgress
|
|||||||
opts.Expires = expiration.UTC()
|
opts.Expires = expiration.UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *metadata != "" {
|
||||||
|
var md map[string]string
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(*metadata), &md)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Metadata = md
|
||||||
|
}
|
||||||
|
|
||||||
opts.Volatile.RedundancyScheme = cfg.GetRedundancyScheme()
|
opts.Volatile.RedundancyScheme = cfg.GetRedundancyScheme()
|
||||||
opts.Volatile.EncryptionParameters = cfg.GetEncryptionParameters()
|
opts.Volatile.EncryptionParameters = cfg.GetEncryptionParameters()
|
||||||
|
|
||||||
@ -252,7 +267,7 @@ func copyObject(ctx context.Context, src fpath.FPath, dst fpath.FPath) (err erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyMain is the function executed when cpCmd is called
|
// copyMain is the function executed when cpCmd is called.
|
||||||
func copyMain(cmd *cobra.Command, args []string) (err error) {
|
func copyMain(cmd *cobra.Command, args []string) (err error) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
return fmt.Errorf("No object specified for copy")
|
return fmt.Errorf("No object specified for copy")
|
||||||
|
17
cmd/uplink/cmd/meta.go
Normal file
17
cmd/uplink/cmd/meta.go
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var metaCmd *cobra.Command
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
metaCmd = addCmd(&cobra.Command{
|
||||||
|
Use: "meta",
|
||||||
|
Short: "Metadata related commands",
|
||||||
|
}, RootCmd)
|
||||||
|
}
|
100
cmd/uplink/cmd/meta_get.go
Normal file
100
cmd/uplink/cmd/meta_get.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/zeebo/errs"
|
||||||
|
|
||||||
|
"storj.io/storj/internal/fpath"
|
||||||
|
"storj.io/storj/pkg/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
addCmd(&cobra.Command{
|
||||||
|
Use: "get [KEY] PATH",
|
||||||
|
Short: "Get a Storj object's metadata",
|
||||||
|
RunE: metaGetMain,
|
||||||
|
}, metaCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// metaGetMain is the function executed when metaGetCmd is called.
|
||||||
|
func metaGetMain(cmd *cobra.Command, args []string) (err error) {
|
||||||
|
var key *string
|
||||||
|
var path string
|
||||||
|
|
||||||
|
switch len(args) {
|
||||||
|
case 0:
|
||||||
|
return fmt.Errorf("No object specified")
|
||||||
|
case 1:
|
||||||
|
path = args[0]
|
||||||
|
case 2:
|
||||||
|
key = &args[0]
|
||||||
|
path = args[1]
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Too many arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, _ := process.Ctx(cmd)
|
||||||
|
|
||||||
|
src, err := fpath.New(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if src.IsLocal() {
|
||||||
|
return fmt.Errorf("The source destination must be a Storj URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
project, bucket, err := cfg.GetProjectAndBucket(ctx, src.Bucket())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer closeProjectAndBucket(project, bucket)
|
||||||
|
|
||||||
|
object, err := bucket.OpenObject(ctx, src.Path())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { err = errs.Combine(err, object.Close()) }()
|
||||||
|
|
||||||
|
if key != nil {
|
||||||
|
var keyNorm string
|
||||||
|
err := json.Unmarshal([]byte("\""+*key+"\""), &keyNorm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := object.Meta.Metadata[keyNorm]
|
||||||
|
if ok != true {
|
||||||
|
return fmt.Errorf("Key does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
str, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s\n", str[1:len(str)-1])
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if object.Meta.Metadata != nil {
|
||||||
|
str, err := json.MarshalIndent(object.Meta.Metadata, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s\n", string(str))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("{}\n")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
156
cmd/uplink/cmd/meta_test.go
Normal file
156
cmd/uplink/cmd/meta_test.go
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
// Copyright (C) 2019 Storj Labs, Inc.
|
||||||
|
// See LICENSE for copying information.
|
||||||
|
|
||||||
|
package cmd_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"storj.io/storj/internal/testcontext"
|
||||||
|
"storj.io/storj/internal/testplanet"
|
||||||
|
"storj.io/storj/internal/testrand"
|
||||||
|
)
|
||||||
|
|
||||||
|
func min(x, y int) int {
|
||||||
|
if x < y {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
return y
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetGetMeta(t *testing.T) {
|
||||||
|
testplanet.Run(t, testplanet.Config{
|
||||||
|
SatelliteCount: 1,
|
||||||
|
StorageNodeCount: 4,
|
||||||
|
UplinkCount: 1,
|
||||||
|
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||||
|
uplinkExe := ctx.Compile("storj.io/storj/cmd/uplink")
|
||||||
|
|
||||||
|
// Configure uplink.
|
||||||
|
{
|
||||||
|
output, err := exec.Command(uplinkExe,
|
||||||
|
"--config-dir", ctx.Dir("uplink"),
|
||||||
|
"setup",
|
||||||
|
"--non-interactive",
|
||||||
|
"--scope", planet.Uplinks[0].GetConfig(planet.Satellites[0]).Scope,
|
||||||
|
).CombinedOutput()
|
||||||
|
t.Log(string(output))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create bucket.
|
||||||
|
bucketName := testrand.BucketName()
|
||||||
|
|
||||||
|
{
|
||||||
|
output, err := exec.Command(uplinkExe,
|
||||||
|
"--config-dir", ctx.Dir("uplink"),
|
||||||
|
"mb",
|
||||||
|
"sj://"+bucketName,
|
||||||
|
).CombinedOutput()
|
||||||
|
t.Log(string(output))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload file with metadata.
|
||||||
|
metadata := testrand.Metadata()
|
||||||
|
|
||||||
|
metadataBs, err := json.Marshal(metadata)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
metadataStr := string(metadataBs)
|
||||||
|
|
||||||
|
var metadataNorm map[string]string
|
||||||
|
err = json.Unmarshal(metadataBs, &metadataNorm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
path := testrand.Path()
|
||||||
|
uri := "sj://" + bucketName + "/" + path
|
||||||
|
|
||||||
|
{
|
||||||
|
output, err := exec.Command(uplinkExe,
|
||||||
|
"--config-dir", ctx.Dir("uplink"),
|
||||||
|
"cp",
|
||||||
|
"--metadata", metadataStr,
|
||||||
|
"-", uri,
|
||||||
|
).CombinedOutput()
|
||||||
|
t.Log(string(output))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all metadata.
|
||||||
|
{
|
||||||
|
cmd := exec.Command(uplinkExe,
|
||||||
|
"--config-dir", ctx.Dir("uplink"),
|
||||||
|
"meta", "get", uri,
|
||||||
|
)
|
||||||
|
t.Log(cmd)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
t.Log(string(output))
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
if ee, ok := err.(*exec.ExitError); ok {
|
||||||
|
t.Log(ee)
|
||||||
|
t.Log(string(ee.Stderr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var md map[string]string
|
||||||
|
err = json.Unmarshal(output, &md)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, metadataNorm, md)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get specific metadata.
|
||||||
|
//
|
||||||
|
// NOTE: The CLI expects JSON encoded strings for input and
|
||||||
|
// output. The key and value returned from the CLI have to be
|
||||||
|
// converted from the JSON encoded string into the Go native
|
||||||
|
// string for comparison.
|
||||||
|
for key, value := range metadataNorm {
|
||||||
|
key, value := key, value
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("Fetching key %q", key[:min(len(key), 8)]), func(t *testing.T) {
|
||||||
|
keyNorm, err := json.Marshal(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cmd := exec.Command(uplinkExe,
|
||||||
|
"--config-dir", ctx.Dir("uplink"),
|
||||||
|
"meta", "get", string(keyNorm[1:len(keyNorm)-1]), uri,
|
||||||
|
)
|
||||||
|
t.Log(cmd)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if err != nil {
|
||||||
|
if ee, ok := err.(*exec.ExitError); ok {
|
||||||
|
t.Log(ee)
|
||||||
|
t.Log(string(ee.Stderr))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing newline.
|
||||||
|
if len(output) > 0 && string(output[len(output)-1]) == "\n" {
|
||||||
|
output = output[:len(output)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputNorm string
|
||||||
|
err = json.Unmarshal([]byte("\""+string(output)+"\""), &outputNorm)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, value, outputNorm)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -115,3 +115,92 @@ func UUID() uuid.UUID {
|
|||||||
Read(uuid[:])
|
Read(uuid[:])
|
||||||
return uuid
|
return uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BucketName creates a random bucket name mostly confirming to the
|
||||||
|
// restrictions of S3:
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html
|
||||||
|
//
|
||||||
|
// NOTE: This may not generate values that cover all valid values (for Storj or
|
||||||
|
// S3). This is a best effort to cover most cases we believe our design
|
||||||
|
// requires and will need to be revisited when a more explicit design spec is
|
||||||
|
// created.
|
||||||
|
func BucketName() string {
|
||||||
|
const (
|
||||||
|
edges = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
body = "abcdefghijklmnopqrstuvwxyz0123456789-"
|
||||||
|
min = 3
|
||||||
|
max = 63
|
||||||
|
)
|
||||||
|
|
||||||
|
size := rand.Intn(max-min) + min
|
||||||
|
|
||||||
|
b := make([]byte, size)
|
||||||
|
for i := range b {
|
||||||
|
switch i {
|
||||||
|
case 0:
|
||||||
|
fallthrough
|
||||||
|
case size - 1:
|
||||||
|
b[i] = edges[rand.Intn(len(edges))]
|
||||||
|
default:
|
||||||
|
b[i] = body[rand.Intn(len(body))]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata creates random metadata mostly conforming to the restrictions of S3:
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#object-metadata
|
||||||
|
//
|
||||||
|
// NOTE: This may not generate values that cover all valid values (for Storj or
|
||||||
|
// S3). This is a best effort to cover most cases we believe our design
|
||||||
|
// requires and will need to be revisited when a more explicit design spec is
|
||||||
|
// created.
|
||||||
|
func Metadata() map[string]string {
|
||||||
|
const (
|
||||||
|
max = 2 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
total := rand.Intn(max)
|
||||||
|
metadata := make(map[string]string)
|
||||||
|
|
||||||
|
for used := 0; total-used > 1; {
|
||||||
|
keySize := rand.Intn(total-(used+1)) + 1
|
||||||
|
key := BytesInt(keySize)
|
||||||
|
used += len(key)
|
||||||
|
|
||||||
|
valueSize := rand.Intn(total - used)
|
||||||
|
value := BytesInt(valueSize)
|
||||||
|
used += len(value)
|
||||||
|
|
||||||
|
metadata[string(key)] = string(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path creates a random path mostly conforming to the retrictions of S3:
|
||||||
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingMetadata.html#object-keys
|
||||||
|
//
|
||||||
|
// NOTE: This may not generate values that cover all valid values (for Storj or
|
||||||
|
// S3). This is a best effort to cover most cases we believe our design
|
||||||
|
// requires and will need to be revisited when a more explicit design spec is
|
||||||
|
// created.
|
||||||
|
func Path() string {
|
||||||
|
const (
|
||||||
|
max = 1 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
total := rand.Intn(max)
|
||||||
|
path := ""
|
||||||
|
|
||||||
|
for used := 0; len(path) < total; {
|
||||||
|
if used != 0 {
|
||||||
|
path += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
path += SegmentID(rand.Intn(total - used)).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return path[:total]
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user