mirror of
https://github.com/minio/mc.git
synced 2025-11-09 02:22:18 +03:00
Large MC Update (new encryption flags, functional test suite, removal of session code, minor cleanup, vuln. updates ) (#4882)
This commit is contained in:
3
.github/workflows/go.yml
vendored
3
.github/workflows/go.yml
vendored
@@ -49,6 +49,9 @@ jobs:
|
||||
ENABLE_HTTPS: 1
|
||||
MINIO_CI_CD: 1
|
||||
SERVER_ENDPOINT: localhost:9000
|
||||
MC_TEST_ENABLE_HTTPS: true
|
||||
MC_TEST_SKIP_INSECURE: true
|
||||
MC_TEST_SKIP_BUILD: true
|
||||
run: |
|
||||
wget https://dl.min.io/server/minio/release/linux-amd64/minio && chmod +x minio
|
||||
mkdir -p ~/.minio/certs/ && cp testdata/localhost.crt ~/.minio/certs/public.crt && cp testdata/localhost.key ~/.minio/certs/private.key
|
||||
|
||||
4
Makefile
4
Makefile
@@ -43,7 +43,7 @@ test: verifiers build
|
||||
@echo "Running unit tests"
|
||||
@GO111MODULE=on CGO_ENABLED=0 go test -tags kqueue ./... 1>/dev/null
|
||||
@echo "Running functional tests"
|
||||
@(env bash $(PWD)/functional-tests.sh)
|
||||
@GO111MODULE=on MC_TEST_RUN_FULL_SUITE=true go test -race -v --timeout 20m ./... -run Test_FullSuite
|
||||
|
||||
test-race: verifiers build
|
||||
@echo "Running unit tests under -race"
|
||||
@@ -54,7 +54,7 @@ verify:
|
||||
@echo "Verifying build with race"
|
||||
@GO111MODULE=on CGO_ENABLED=1 go build -race -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/mc 1>/dev/null
|
||||
@echo "Running functional tests"
|
||||
@(env bash $(PWD)/functional-tests.sh)
|
||||
@GO111MODULE=on MC_TEST_RUN_FULL_SUITE=true go test -race -v --timeout 20m ./... -run Test_FullSuite
|
||||
|
||||
# Builds mc locally.
|
||||
build: checks
|
||||
|
||||
84
README.md
84
README.md
@@ -1,41 +1,53 @@
|
||||
# MinIO Client Quickstart Guide
|
||||
[](https://slack.min.io) [](https://goreportcard.com/report/minio/mc) [](https://hub.docker.com/r/minio/mc/) [](https://github.com/minio/mc/blob/master/LICENSE)
|
||||
|
||||
# Documentation
|
||||
- [MC documentation](https://min.io/docs/minio/linux/reference/minio-mc.html)
|
||||
|
||||
MinIO Client (mc) provides a modern alternative to UNIX commands like ls, cat, cp, mirror, diff, find etc. It supports filesystems and Amazon S3 compatible cloud storage service (AWS Signature v2 and v4).
|
||||
|
||||
```
|
||||
alias set, remove and list aliases in configuration file
|
||||
ls list buckets and objects
|
||||
mb make a bucket
|
||||
rb remove a bucket
|
||||
cp copy objects
|
||||
mirror synchronize object(s) to a remote site
|
||||
cat display object contents
|
||||
head display first 'n' lines of an object
|
||||
pipe stream STDIN to an object
|
||||
share generate URL for temporary access to an object
|
||||
find search for objects
|
||||
sql run sql queries on objects
|
||||
stat show object metadata
|
||||
mv move objects
|
||||
tree list buckets and objects in a tree format
|
||||
du summarize disk usage recursively
|
||||
retention set retention for object(s)
|
||||
legalhold set legal hold for object(s)
|
||||
diff list differences in object name, size, and date between two buckets
|
||||
rm remove objects
|
||||
encrypt manage bucket encryption config
|
||||
event manage object notifications
|
||||
watch listen for object notification events
|
||||
undo undo PUT/DELETE operations
|
||||
anonymous manage anonymous access to buckets and objects
|
||||
tag manage tags for bucket(s) and object(s)
|
||||
ilm manage bucket lifecycle
|
||||
version manage bucket versioning
|
||||
replicate configure server side bucket replication
|
||||
admin manage MinIO servers
|
||||
update update mc to latest release
|
||||
ping perform liveness check
|
||||
alias manage server credentials in configuration file
|
||||
admin manage MinIO servers
|
||||
anonymous manage anonymous access to buckets and objects
|
||||
batch manage batch jobs
|
||||
cp copy objects
|
||||
cat display object contents
|
||||
diff list differences in object name, size, and date between two buckets
|
||||
du summarize disk usage recursively
|
||||
encrypt manage bucket encryption config
|
||||
event manage object notifications
|
||||
find search for objects
|
||||
get get s3 object to local
|
||||
head display first 'n' lines of an object
|
||||
ilm manage bucket lifecycle
|
||||
idp manage MinIO IDentity Provider server configuration
|
||||
license license related commands
|
||||
legalhold manage legal hold for object(s)
|
||||
ls list buckets and objects
|
||||
mb make a bucket
|
||||
mv move objects
|
||||
mirror synchronize object(s) to a remote site
|
||||
od measure single stream upload and download
|
||||
ping perform liveness check
|
||||
pipe stream STDIN to an object
|
||||
put upload an object to a bucket
|
||||
quota manage bucket quota
|
||||
rm remove object(s)
|
||||
retention set retention for object(s)
|
||||
rb remove a bucket
|
||||
replicate configure server side bucket replication
|
||||
ready checks if the cluster is ready or not
|
||||
sql run sql queries on objects
|
||||
stat show object metadata
|
||||
support support related commands
|
||||
share generate URL for temporary access to an object
|
||||
tree list buckets and objects in a tree format
|
||||
tag manage tags for bucket and object(s)
|
||||
undo undo PUT/DELETE operations
|
||||
update update mc to latest release
|
||||
version manage bucket versioning
|
||||
watch listen for object notification events
|
||||
```
|
||||
|
||||
## Docker Container
|
||||
@@ -176,9 +188,6 @@ Get your AccessKeyID and SecretAccessKey by following [Google Credentials Guide]
|
||||
mc alias set gcs https://storage.googleapis.com BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
|
||||
```
|
||||
|
||||
### Example - IBM Cloud Object Storage
|
||||
See [the complete guide](docs/minio-client-complete-guide.md) for IBM instructions.
|
||||
|
||||
## Test Your Setup
|
||||
`mc` is pre-configured with https://play.min.io, aliased as "play". It is a hosted MinIO server for testing and development purpose. To test Amazon S3, simply replace "play" with "s3" or the alias you used at the time of setup.
|
||||
|
||||
@@ -236,11 +245,6 @@ admin config diff find ls mirror policy session sql
|
||||
cat cp event head mb pipe rm share stat version
|
||||
```
|
||||
|
||||
## Explore Further
|
||||
- [MinIO Client Complete Guide](https://min.io/docs/minio/linux/reference/minio-mc.html?ref=gh)
|
||||
- [MinIO Quickstart Guide](https://min.io/docs/minio/linux/index.html#quickstart-for-linux?ref=gh)
|
||||
- [The MinIO documentation website](https://min.io/docs/minio/linux/index.html?ref=gh)
|
||||
|
||||
## Contribute to MinIO Project
|
||||
Please follow MinIO [Contributor's Guide](https://github.com/minio/mc/blob/master/CONTRIBUTING.md)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
|
||||
var adminKMSCreateKeyCmd = cli.Command{
|
||||
Name: "create",
|
||||
Usage: "creates a new master key at the KMS",
|
||||
Usage: "creates a new master KMS key",
|
||||
Action: mainAdminKMSCreateKey,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
|
||||
@@ -64,7 +64,7 @@ var catCmd = cli.Command{
|
||||
Action: mainCat,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(catFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(catFlags, encCFlag), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -74,8 +74,6 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
|
||||
EXAMPLES:
|
||||
1. Stream an object from Amazon S3 cloud storage to mplayer standard input.
|
||||
@@ -88,11 +86,11 @@ EXAMPLES:
|
||||
{{.Prompt}} {{.HelpName}} part.* > complete.img
|
||||
|
||||
4. Save an encrypted object from Amazon S3 cloud storage to a local file.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key 's3/mysql-backups=32byteslongsecretkeymustbegiven1' s3/mysql-backups/backups-201810.gz > /mnt/data/recent.gz
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "play/my-bucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/mysql-backups/backups-201810.gz > /mnt/data/recent.gz
|
||||
|
||||
5. Display the content of encrypted object. In case the encryption key contains non-printable character like tab, pass the
|
||||
base64 encoded string as key.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "play/my-bucket/=MzJieXRlc2xvbmdzZWNyZXRrZQltdXN0YmVnaXZlbjE=" play/my-bucket/my-object
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "play/my-bucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" play/my-bucket/my-object
|
||||
|
||||
6. Display the content of an object 10 days earlier
|
||||
{{.Prompt}} {{.HelpName}} --rewind 10d play/my-bucket/my-object
|
||||
@@ -323,15 +321,12 @@ func mainCat(cliCtx *cli.Context) error {
|
||||
ctx, cancelCat := context.WithCancel(globalContext)
|
||||
defer cancelCat()
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// check 'cat' cli arguments.
|
||||
o := parseCatSyntax(cliCtx)
|
||||
|
||||
// Set command flags from context.
|
||||
|
||||
// handle std input data.
|
||||
if o.stdinMode {
|
||||
fatalIf(catOut(os.Stdin, -1).Trace(), "Unable to read from standard input.")
|
||||
|
||||
@@ -68,8 +68,7 @@ func IsDeleteEvent(event notify.Event) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// getXAttr fetches the extended attribute for a particular key on
|
||||
// file
|
||||
// getXAttr fetches the extended attribute for a particular key on file
|
||||
func getXAttr(path, key string) (string, error) {
|
||||
data, e := xattr.Get(path, key)
|
||||
if e != nil {
|
||||
@@ -81,8 +80,7 @@ func getXAttr(path, key string) (string, error) {
|
||||
return hex.EncodeToString(data), nil
|
||||
}
|
||||
|
||||
// getAllXattrs returns the extended attributes for a file if supported
|
||||
// by the OS
|
||||
// getAllXattrs returns the extended attributes for a file if supported by the OS
|
||||
func getAllXattrs(path string) (map[string]string, error) {
|
||||
xMetadata := make(map[string]string)
|
||||
list, e := xattr.List(path)
|
||||
|
||||
@@ -92,6 +92,8 @@ const (
|
||||
AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date"
|
||||
// AmzObjectLockLegalHold sets object lock legal hold
|
||||
AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold"
|
||||
amzObjectSSEKMSKeyID = "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id"
|
||||
amzObjectSSE = "X-Amz-Server-Side-Encryption"
|
||||
)
|
||||
|
||||
type dialContext func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
@@ -19,7 +19,6 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -33,73 +32,12 @@ import (
|
||||
"golang.org/x/net/http/httpguts"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||
"github.com/minio/pkg/v2/env"
|
||||
)
|
||||
|
||||
// decode if the key is encoded key and returns the key
|
||||
func getDecodedKey(sseKeys string) (key string, err *probe.Error) {
|
||||
keyString := ""
|
||||
for i, sse := range strings.Split(sseKeys, ",") {
|
||||
if i > 0 {
|
||||
keyString = keyString + ","
|
||||
}
|
||||
sseString, err := parseKey(sse)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
keyString = keyString + sseString
|
||||
}
|
||||
return keyString, nil
|
||||
}
|
||||
|
||||
// Validate the key
|
||||
func parseKey(sseKeys string) (sse string, err *probe.Error) {
|
||||
encryptString := strings.SplitN(sseKeys, "=", 2)
|
||||
if len(encryptString) < 2 {
|
||||
return "", probe.NewError(errors.New("SSE-C prefix should be of the form prefix1=key1,... "))
|
||||
}
|
||||
|
||||
secretValue := encryptString[1]
|
||||
if len(secretValue) == 32 {
|
||||
return sseKeys, nil
|
||||
}
|
||||
decodedString, e := base64.StdEncoding.DecodeString(secretValue)
|
||||
if e != nil || len(decodedString) != 32 {
|
||||
return "", probe.NewError(errors.New("Encryption key should be 32 bytes plain text key or 44 bytes base64 encoded key"))
|
||||
}
|
||||
return encryptString[0] + "=" + string(decodedString), nil
|
||||
}
|
||||
|
||||
// parse and return encryption key pairs per alias.
|
||||
func getEncKeys(ctx *cli.Context) (map[string][]prefixSSEPair, *probe.Error) {
|
||||
sseServer := ctx.String("encrypt")
|
||||
var sseKeys string
|
||||
if keyPrefix := ctx.String("encrypt-key"); keyPrefix != "" {
|
||||
if sseServer != "" && strings.Contains(keyPrefix, sseServer) {
|
||||
return nil, errConflictSSE(sseServer, keyPrefix).Trace(ctx.Args()...)
|
||||
}
|
||||
sseKeys = keyPrefix
|
||||
}
|
||||
var err *probe.Error
|
||||
if sseKeys != "" {
|
||||
sseKeys, err = getDecodedKey(sseKeys)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sseKeys)
|
||||
}
|
||||
}
|
||||
|
||||
encKeyDB, err := parseAndValidateEncryptionKeys(sseKeys, sseServer)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sseKeys)
|
||||
}
|
||||
|
||||
return encKeyDB, nil
|
||||
}
|
||||
|
||||
// Check if the passed URL represents a folder. It may or may not exist yet.
|
||||
// If it exists, we can easily check if it is a folder, if it doesn't exist,
|
||||
// we can guess if the url is a folder from how it looks.
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDecodedKey(t *testing.T) {
|
||||
getDecodeCases := []struct {
|
||||
input string
|
||||
output string
|
||||
err error
|
||||
status bool
|
||||
}{
|
||||
// success scenario the key contains non printable (tab) character as key
|
||||
{"s3/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=", "s3/documents/=32byteslongsecreabcdefg givenn21", nil, true},
|
||||
// success scenario the key contains non printable (tab character) as key
|
||||
{"s3/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=,play/documents/=MzJieXRlc2xvbmdzZWNyZXRrZQltdXN0YmVnaXZlbjE=", "s3/documents/=32byteslongsecreabcdefg givenn21,play/documents/=32byteslongsecretke mustbegiven1", nil, true},
|
||||
// success scenario using a normal string
|
||||
{"s3/documents/=32byteslongsecretkeymustbegiven1", "s3/documents/=32byteslongsecretkeymustbegiven1", nil, true},
|
||||
// success scenario using a normal string
|
||||
{"s3/documents/=32byteslongsecretkeymustbegiven1,myminio/documents/=32byteslongsecretkeymustbegiven2", "s3/documents/=32byteslongsecretkeymustbegiven1,myminio/documents/=32byteslongsecretkeymustbegiven2", nil, true},
|
||||
// success scenario using a mix of normal string and encoded string
|
||||
{"s3/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=,play/documents/=32byteslongsecretkeymustbegiven1", "s3/documents/=32byteslongsecreabcdefg givenn21,play/documents/=32byteslongsecretkeymustbegiven1", nil, true},
|
||||
// success scenario using a mix of normal string and encoded string
|
||||
{"play/documents/=32byteslongsecretkeymustbegiven1,s3/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=", "play/documents/=32byteslongsecretkeymustbegiven1,s3/documents/=32byteslongsecreabcdefg givenn21", nil, true},
|
||||
// decoded key less than 32 char and conatin non printable (tab) character
|
||||
{"s3/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE", "", errors.New("Encryption key should be 32 bytes plain text key or 44 bytes base64 encoded key"), false},
|
||||
// normal key less than 32 character
|
||||
{"s3/documents/=32byteslongsecretkeymustbegiven", "", errors.New("Encryption key should be 32 bytes plain text key or 44 bytes base64 encoded key"), false},
|
||||
}
|
||||
|
||||
for idx, testCase := range getDecodeCases {
|
||||
decodedString, errDecode := getDecodedKey(testCase.input)
|
||||
if testCase.status == true {
|
||||
if errDecode != nil {
|
||||
t.Fatalf("Test %d: generated error not matching, expected = `%s`, found = `%s`", idx+1, testCase.err, errDecode)
|
||||
}
|
||||
if !reflect.DeepEqual(decodedString, testCase.output) {
|
||||
t.Fatalf("Test %d: generated key not matching, expected = `%s`, found = `%s`", idx+1, testCase.input, decodedString)
|
||||
}
|
||||
}
|
||||
|
||||
if testCase.status == false {
|
||||
if !reflect.DeepEqual(decodedString, testCase.output) {
|
||||
t.Fatalf("Test %d: generated Map not matching, expected = `%s`, found = `%s`", idx+1, testCase.input, errDecode)
|
||||
}
|
||||
if errDecode.Cause.Error() != testCase.err.Error() {
|
||||
t.Fatalf("Test %d: generated error not matching, expected = `%s`, found = `%s`", idx+1, testCase.err, errDecode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
280
cmd/cp-main.go
280
cmd/cp-main.go
@@ -18,18 +18,14 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/fatih/color"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/minio/cli"
|
||||
json "github.com/minio/colorjson"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
@@ -67,10 +63,6 @@ var (
|
||||
Name: "attr",
|
||||
Usage: "add custom metadata for the object",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "continue, c",
|
||||
Usage: "create or resume copy session",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "preserve, a",
|
||||
Usage: "preserve filesystem attributes (mode, ownership, timestamps)",
|
||||
@@ -122,7 +114,7 @@ var cpCmd = cli.Command{
|
||||
Action: mainCopy,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(cpFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(cpFlags, encFlags...), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -132,9 +124,10 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT: list of comma delimited prefixes
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key).
|
||||
MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key).
|
||||
|
||||
EXAMPLES:
|
||||
01. Copy a list of objects from local file system to Amazon S3 cloud storage.
|
||||
@@ -161,12 +154,11 @@ EXAMPLES:
|
||||
08. Copy a local folder with space separated characters to Amazon S3 cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --recursive 'workdir/documents/May 2014/' s3/miniocloud
|
||||
|
||||
09. Copy a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --recursive --encrypt-key "s3/documents/=32byteslongsecretkeymustbegiven1,myminio/documents/=32byteslongsecretkeymustbegiven2" s3/documents/ myminio/documents/
|
||||
09. Copy a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage using s3 encryption.
|
||||
{{.Prompt}} {{.HelpName}} --recursive --enc-s3 "s3/documents/=my-aws-key" --enc-s3 "myminio/documents/=my-minio-key" s3/documents/ myminio/documents/
|
||||
|
||||
10. Copy a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage. In case the encryption key contains non-printable character like tab, pass the
|
||||
base64 encoded string as key.
|
||||
{{.Prompt}} {{.HelpName}} --recursive --encrypt-key "s3/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=,myminio/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=" s3/documents/ myminio/documents/
|
||||
10. Copy a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --recursive --enc-c "s3/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" --enc-c "myminio/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5BBB" s3/documents/ myminio/documents/
|
||||
|
||||
11. Copy a list of objects from local file system to MinIO cloud storage with specified metadata, separated by ";"
|
||||
{{.Prompt}} {{.HelpName}} --attr "key1=value1;key2=value2" Music/*.mp4 play/mybucket/
|
||||
@@ -177,25 +169,22 @@ EXAMPLES:
|
||||
13. Copy a text file to an object storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object.
|
||||
{{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY myobject.txt play/mybucket
|
||||
|
||||
14. Copy a text file to an object storage and create or resume copy session.
|
||||
{{.Prompt}} {{.HelpName}} --recursive --continue dir/ play/mybucket
|
||||
|
||||
15. Copy a text file to an object storage and preserve the file system attribute as metadata.
|
||||
14. Copy a text file to an object storage and preserve the file system attribute as metadata.
|
||||
{{.Prompt}} {{.HelpName}} -a myobject.txt play/mybucket
|
||||
|
||||
16. Copy a text file to an object storage with object lock mode set to 'GOVERNANCE' with retention duration 1 day.
|
||||
15. Copy a text file to an object storage with object lock mode set to 'GOVERNANCE' with retention duration 1 day.
|
||||
{{.Prompt}} {{.HelpName}} --retention-mode governance --retention-duration 1d locked.txt play/locked-bucket/
|
||||
|
||||
17. Copy a text file to an object storage with legal-hold enabled.
|
||||
16. Copy a text file to an object storage with legal-hold enabled.
|
||||
{{.Prompt}} {{.HelpName}} --legal-hold on locked.txt play/locked-bucket/
|
||||
|
||||
18. Copy a text file to an object storage and disable multipart upload feature.
|
||||
17. Copy a text file to an object storage and disable multipart upload feature.
|
||||
{{.Prompt}} {{.HelpName}} --disable-multipart myobject.txt play/mybucket
|
||||
|
||||
19. Roll back 10 days in the past to copy the content of 'mybucket'
|
||||
18. Roll back 10 days in the past to copy the content of 'mybucket'
|
||||
{{.Prompt}} {{.HelpName}} --rewind 10d -r play/mybucket/ /tmp/dest/
|
||||
|
||||
20. Set tags to the uploaded objects
|
||||
19. Set tags to the uploaded objects
|
||||
{{.Prompt}} {{.HelpName}} -r --tags "category=prod&type=backup" ./data/ play/another-bucket/
|
||||
|
||||
`,
|
||||
@@ -269,7 +258,7 @@ func doCopy(ctx context.Context, copyOpts doCopyOpts) URLs {
|
||||
urls := uploadSourceToTargetURL(ctx, uploadSourceToTargetURLOpts{
|
||||
urls: copyOpts.cpURLs,
|
||||
progress: copyOpts.pg,
|
||||
encKeyDB: copyOpts.encKeyDB,
|
||||
encKeyDB: copyOpts.encryptionKeys,
|
||||
preserve: copyOpts.preserve,
|
||||
isZip: copyOpts.isZip,
|
||||
multipartSize: copyOpts.multipartSize,
|
||||
@@ -292,90 +281,6 @@ func doCopyFake(cpURLs URLs, pg Progress) URLs {
|
||||
return cpURLs
|
||||
}
|
||||
|
||||
// doPrepareCopyURLs scans the source URL and prepares a list of objects for copying.
|
||||
func doPrepareCopyURLs(ctx context.Context, session *sessionV8, cancelCopy context.CancelFunc) (totalBytes, totalObjects int64, errSeen bool) {
|
||||
// Separate source and target. 'cp' can take only one target,
|
||||
// but any number of sources.
|
||||
sourceURLs := session.Header.CommandArgs[:len(session.Header.CommandArgs)-1]
|
||||
targetURL := session.Header.CommandArgs[len(session.Header.CommandArgs)-1] // Last one is target
|
||||
|
||||
// Access recursive flag inside the session header.
|
||||
isRecursive := session.Header.CommandBoolFlags["recursive"]
|
||||
rewind := session.Header.CommandStringFlags["rewind"]
|
||||
versionID := session.Header.CommandStringFlags["version-id"]
|
||||
olderThan := session.Header.CommandStringFlags["older-than"]
|
||||
newerThan := session.Header.CommandStringFlags["newer-than"]
|
||||
encryptKeys := session.Header.CommandStringFlags["encrypt-key"]
|
||||
encrypt := session.Header.CommandStringFlags["encrypt"]
|
||||
encKeyDB, err := parseAndValidateEncryptionKeys(encryptKeys, encrypt)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// Create a session data file to store the processed URLs.
|
||||
dataFP := session.NewDataWriter()
|
||||
|
||||
var scanBar scanBarFunc
|
||||
if !globalQuiet && !globalJSON { // set up progress bar
|
||||
scanBar = scanBarFactory()
|
||||
}
|
||||
|
||||
opts := prepareCopyURLsOpts{
|
||||
sourceURLs: sourceURLs,
|
||||
targetURL: targetURL,
|
||||
isRecursive: isRecursive,
|
||||
encKeyDB: encKeyDB,
|
||||
olderThan: olderThan,
|
||||
newerThan: newerThan,
|
||||
timeRef: parseRewindFlag(rewind),
|
||||
versionID: versionID,
|
||||
}
|
||||
|
||||
URLsCh := prepareCopyURLs(ctx, opts)
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case cpURLs, ok := <-URLsCh:
|
||||
if !ok { // Done with URL preparation
|
||||
done = true
|
||||
break
|
||||
}
|
||||
|
||||
if cpURLs.Error != nil {
|
||||
printCopyURLsError(&cpURLs)
|
||||
errSeen = true
|
||||
break
|
||||
}
|
||||
|
||||
jsoniter := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
jsonData, e := jsoniter.Marshal(cpURLs)
|
||||
if e != nil {
|
||||
session.Delete()
|
||||
fatalIf(probe.NewError(e), "Unable to prepare URL for copying. Error in JSON marshaling.")
|
||||
}
|
||||
dataFP.Write(jsonData)
|
||||
dataFP.Write([]byte{'\n'})
|
||||
if !globalQuiet && !globalJSON {
|
||||
scanBar(cpURLs.SourceContent.URL.String())
|
||||
}
|
||||
|
||||
totalBytes += cpURLs.SourceContent.Size
|
||||
totalObjects++
|
||||
case <-globalContext.Done():
|
||||
cancelCopy()
|
||||
// Print in new line and adjust to top so that we don't print over the ongoing scan bar
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
session.Delete() // If we are interrupted during the URL scanning, we drop the session.
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
session.Header.TotalBytes = totalBytes
|
||||
session.Header.TotalObjects = totalObjects
|
||||
session.Save()
|
||||
return
|
||||
}
|
||||
|
||||
func printCopyURLsError(cpURLs *URLs) {
|
||||
// Print in new line and adjust to top so that we
|
||||
// don't print over the ongoing scan bar
|
||||
@@ -393,7 +298,7 @@ func printCopyURLsError(cpURLs *URLs) {
|
||||
}
|
||||
}
|
||||
|
||||
func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.Context, session *sessionV8, encKeyDB map[string][]prefixSSEPair, isMvCmd bool) error {
|
||||
func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.Context, encryptionKeys map[string][]prefixSSEPair, isMvCmd bool) error {
|
||||
var isCopied func(string) bool
|
||||
var totalObjects, totalBytes int64
|
||||
|
||||
@@ -416,41 +321,6 @@ func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.
|
||||
// Check if the target path has object locking enabled
|
||||
withLock, _ := isBucketLockEnabled(ctx, targetURL)
|
||||
|
||||
if session != nil {
|
||||
// isCopied returns true if an object has been already copied
|
||||
// or not. This is useful when we resume from a session.
|
||||
isCopied = isLastFactory(session.Header.LastCopied)
|
||||
|
||||
if !session.HasData() {
|
||||
totalBytes, totalObjects, errSeen = doPrepareCopyURLs(ctx, session, cancelCopy)
|
||||
} else {
|
||||
totalBytes, totalObjects = session.Header.TotalBytes, session.Header.TotalObjects
|
||||
}
|
||||
|
||||
pg.SetTotal(totalBytes)
|
||||
|
||||
go func() {
|
||||
jsoniter := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
// Prepare URL scanner from session data file.
|
||||
urlScanner := bufio.NewScanner(session.NewDataReader())
|
||||
for {
|
||||
if !urlScanner.Scan() || urlScanner.Err() != nil {
|
||||
close(cpURLsCh)
|
||||
break
|
||||
}
|
||||
|
||||
var cpURLs URLs
|
||||
if e := jsoniter.Unmarshal([]byte(urlScanner.Text()), &cpURLs); e != nil {
|
||||
errorIf(probe.NewError(e), "Unable to unmarshal %s", urlScanner.Text())
|
||||
continue
|
||||
}
|
||||
|
||||
cpURLsCh <- cpURLs
|
||||
}
|
||||
}()
|
||||
|
||||
} else {
|
||||
// Access recursive flag inside the session header.
|
||||
isRecursive := cli.Bool("recursive")
|
||||
olderThan := cli.String("older-than")
|
||||
newerThan := cli.String("newer-than")
|
||||
@@ -463,7 +333,7 @@ func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.
|
||||
sourceURLs: sourceURLs,
|
||||
targetURL: targetURL,
|
||||
isRecursive: isRecursive,
|
||||
encKeyDB: encKeyDB,
|
||||
encKeyDB: encryptionKeys,
|
||||
olderThan: olderThan,
|
||||
newerThan: newerThan,
|
||||
timeRef: parseRewindFlag(rewind),
|
||||
@@ -485,11 +355,9 @@ func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.
|
||||
}
|
||||
close(cpURLsCh)
|
||||
}()
|
||||
}
|
||||
|
||||
quitCh := make(chan struct{})
|
||||
statusCh := make(chan URLs)
|
||||
|
||||
parallel := newParallelManager(statusCh)
|
||||
|
||||
go func() {
|
||||
@@ -498,7 +366,6 @@ func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.
|
||||
close(statusCh)
|
||||
}
|
||||
|
||||
startContinue := true
|
||||
for {
|
||||
select {
|
||||
case <-quitCh:
|
||||
@@ -562,19 +429,11 @@ func doCopySession(ctx context.Context, cancelCopy context.CancelFunc, cli *cli.
|
||||
}, 0)
|
||||
} else {
|
||||
// Print the copy resume summary once in start
|
||||
if startContinue && cli.Bool("continue") {
|
||||
if pb, ok := pg.(*progressBar); ok {
|
||||
startSize := humanize.IBytes(uint64(pb.Start().Get()))
|
||||
totalSize := humanize.IBytes(uint64(pb.Total))
|
||||
console.Println("Resuming copy from ", startSize, " / ", totalSize)
|
||||
}
|
||||
startContinue = false
|
||||
}
|
||||
parallel.queueTask(func() URLs {
|
||||
return doCopy(ctx, doCopyOpts{
|
||||
cpURLs: cpURLs,
|
||||
pg: pg,
|
||||
encKeyDB: encKeyDB,
|
||||
encryptionKeys: encryptionKeys,
|
||||
isMvCmd: isMvCmd,
|
||||
preserve: preserve,
|
||||
isZip: isZip,
|
||||
@@ -598,9 +457,6 @@ loop:
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
if session != nil {
|
||||
session.CloseAndDie()
|
||||
}
|
||||
break loop
|
||||
case cpURLs, ok := <-statusCh:
|
||||
// Status channel is closed, we should return.
|
||||
@@ -608,10 +464,6 @@ loop:
|
||||
break loop
|
||||
}
|
||||
if cpURLs.Error == nil {
|
||||
if session != nil {
|
||||
session.Header.LastCopied = cpURLs.SourceContent.URL.String()
|
||||
session.Save()
|
||||
}
|
||||
cpAllFilesErr = false
|
||||
} else {
|
||||
|
||||
@@ -643,12 +495,6 @@ loop:
|
||||
}
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
// For critical errors we should exit. Session
|
||||
// can be resumed after the user figures out
|
||||
// the problem.
|
||||
session.copyCloseAndDie(session.Header.CommandBoolFlags["session"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -688,97 +534,25 @@ func mainCopy(cliCtx *cli.Context) error {
|
||||
ctx, cancelCopy := context.WithCancel(globalContext)
|
||||
defer cancelCopy()
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// Parse metadata.
|
||||
userMetaMap := make(map[string]string)
|
||||
if cliCtx.String("attr") != "" {
|
||||
userMetaMap, err = getMetaDataEntry(cliCtx.String("attr"))
|
||||
fatalIf(err, "Unable to parse attribute %v", cliCtx.String("attr"))
|
||||
}
|
||||
|
||||
// check 'copy' cli arguments.
|
||||
checkCopySyntax(cliCtx)
|
||||
// Additional command specific theme customization.
|
||||
console.SetColor("Copy", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
recursive := cliCtx.Bool("recursive")
|
||||
rewind := cliCtx.String("rewind")
|
||||
versionID := cliCtx.String("version-id")
|
||||
olderThan := cliCtx.String("older-than")
|
||||
newerThan := cliCtx.String("newer-than")
|
||||
storageClass := cliCtx.String("storage-class")
|
||||
retentionMode := cliCtx.String(rmFlag)
|
||||
retentionDuration := cliCtx.String(rdFlag)
|
||||
legalHold := strings.ToUpper(cliCtx.String(lhFlag))
|
||||
tags := cliCtx.String("tags")
|
||||
sseKeys := os.Getenv("MC_ENCRYPT_KEY")
|
||||
if key := cliCtx.String("encrypt-key"); key != "" {
|
||||
sseKeys = key
|
||||
var err *probe.Error
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encryptionKeyMap, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
if err != nil {
|
||||
err.Trace(cliCtx.Args()...)
|
||||
}
|
||||
fatalIf(err, "SSE Error")
|
||||
|
||||
if sseKeys != "" {
|
||||
sseKeys, err = getDecodedKey(sseKeys)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
}
|
||||
sse := cliCtx.String("encrypt")
|
||||
|
||||
var session *sessionV8
|
||||
|
||||
if cliCtx.Bool("continue") {
|
||||
sessionID := getHash("cp", os.Args[1:])
|
||||
if isSessionExists(sessionID) {
|
||||
session, err = loadSessionV8(sessionID)
|
||||
fatalIf(err.Trace(sessionID), "Unable to load session.")
|
||||
} else {
|
||||
session = newSessionV8(sessionID)
|
||||
session.Header.CommandType = "cp"
|
||||
session.Header.CommandBoolFlags["recursive"] = recursive
|
||||
session.Header.CommandStringFlags["rewind"] = rewind
|
||||
session.Header.CommandStringFlags["version-id"] = versionID
|
||||
session.Header.CommandStringFlags["older-than"] = olderThan
|
||||
session.Header.CommandStringFlags["newer-than"] = newerThan
|
||||
session.Header.CommandStringFlags["storage-class"] = storageClass
|
||||
session.Header.CommandStringFlags["tags"] = tags
|
||||
session.Header.CommandStringFlags[rmFlag] = retentionMode
|
||||
session.Header.CommandStringFlags[rdFlag] = retentionDuration
|
||||
session.Header.CommandStringFlags[lhFlag] = legalHold
|
||||
session.Header.CommandStringFlags["encrypt-key"] = sseKeys
|
||||
session.Header.CommandStringFlags["encrypt"] = sse
|
||||
session.Header.CommandBoolFlags["session"] = cliCtx.Bool("continue")
|
||||
|
||||
if cliCtx.Bool("preserve") {
|
||||
session.Header.CommandBoolFlags["preserve"] = cliCtx.Bool("preserve")
|
||||
}
|
||||
session.Header.UserMetaData = userMetaMap
|
||||
session.Header.CommandBoolFlags["md5"] = cliCtx.Bool("md5")
|
||||
session.Header.CommandBoolFlags["disable-multipart"] = cliCtx.Bool("disable-multipart")
|
||||
|
||||
var e error
|
||||
if session.Header.RootPath, e = os.Getwd(); e != nil {
|
||||
session.Delete()
|
||||
fatalIf(probe.NewError(e), "Unable to get current working folder.")
|
||||
}
|
||||
|
||||
// extract URLs.
|
||||
session.Header.CommandArgs = cliCtx.Args()
|
||||
}
|
||||
}
|
||||
|
||||
e := doCopySession(ctx, cancelCopy, cliCtx, session, encKeyDB, false)
|
||||
if session != nil {
|
||||
session.Delete()
|
||||
}
|
||||
|
||||
return e
|
||||
return doCopySession(ctx, cancelCopy, cliCtx, encryptionKeyMap, false)
|
||||
}
|
||||
|
||||
type doCopyOpts struct {
|
||||
cpURLs URLs
|
||||
pg ProgressReader
|
||||
encKeyDB map[string][]prefixSSEPair
|
||||
encryptionKeys map[string][]prefixSSEPair
|
||||
isMvCmd, preserve, isZip bool
|
||||
updateProgressTotal bool
|
||||
multipartSize string
|
||||
|
||||
@@ -202,7 +202,7 @@ func mainDiff(cliCtx *cli.Context) error {
|
||||
defer cancelDiff()
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// check 'diff' cli arguments.
|
||||
|
||||
@@ -62,7 +62,7 @@ var duCmd = cli.Command{
|
||||
Action: mainDu,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(duFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(duFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -72,8 +72,6 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
|
||||
EXAMPLES:
|
||||
1. Summarize disk usage of 'jazz-songs' bucket recursively.
|
||||
@@ -121,7 +119,7 @@ func (r duMessage) JSON() string {
|
||||
return string(msgBytes)
|
||||
}
|
||||
|
||||
func du(ctx context.Context, urlStr string, timeRef time.Time, withVersions bool, depth int, encKeyDB map[string][]prefixSSEPair) (sz, objs int64, err error) {
|
||||
func du(ctx context.Context, urlStr string, timeRef time.Time, withVersions bool, depth int) (sz, objs int64, err error) {
|
||||
targetAlias, targetURL, _ := mustExpandAlias(urlStr)
|
||||
|
||||
if !strings.HasSuffix(targetURL, "/") {
|
||||
@@ -176,7 +174,7 @@ func du(ctx context.Context, urlStr string, timeRef time.Time, withVersions bool
|
||||
if targetAlias != "" {
|
||||
subDirAlias = targetAlias + "/" + content.URL.Path
|
||||
}
|
||||
used, n, err := du(ctx, subDirAlias, timeRef, withVersions, depth, encKeyDB)
|
||||
used, n, err := du(ctx, subDirAlias, timeRef, withVersions, depth)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -223,10 +221,6 @@ func mainDu(cliCtx *cli.Context) error {
|
||||
ctx, cancelRm := context.WithCancel(globalContext)
|
||||
defer cancelRm()
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// du specific flags.
|
||||
depth := cliCtx.Int("depth")
|
||||
if depth == 0 {
|
||||
@@ -250,7 +244,7 @@ func mainDu(cliCtx *cli.Context) error {
|
||||
fatalIf(errInvalidArgument().Trace(urlStr), fmt.Sprintf("Source `%s` is not a folder. Only folders are supported by 'du' command.", urlStr))
|
||||
}
|
||||
|
||||
if _, _, err := du(ctx, urlStr, timeRef, withVersions, depth, encKeyDB); duErr == nil {
|
||||
if _, _, err := du(ctx, urlStr, timeRef, withVersions, depth); duErr == nil {
|
||||
duErr = err
|
||||
}
|
||||
}
|
||||
|
||||
252
cmd/encryption-methods.go
Normal file
252
cmd/encryption-methods.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||
//
|
||||
// # This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||
)
|
||||
|
||||
type sseKeyType int
|
||||
|
||||
const (
|
||||
sseNone sseKeyType = iota
|
||||
sseC
|
||||
sseKMS
|
||||
sseS3
|
||||
)
|
||||
|
||||
// struct representing object prefix and sse keys association.
|
||||
type prefixSSEPair struct {
|
||||
Prefix string
|
||||
SSE encrypt.ServerSide
|
||||
}
|
||||
|
||||
// byPrefixLength implements sort.Interface.
|
||||
type byPrefixLength []prefixSSEPair
|
||||
|
||||
func (p byPrefixLength) Len() int { return len(p) }
|
||||
func (p byPrefixLength) Less(i, j int) bool {
|
||||
if len(p[i].Prefix) != len(p[j].Prefix) {
|
||||
return len(p[i].Prefix) > len(p[j].Prefix)
|
||||
}
|
||||
return p[i].Prefix < p[j].Prefix
|
||||
}
|
||||
|
||||
func (p byPrefixLength) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// get SSE Key if object prefix matches with given resource.
|
||||
func getSSE(resource string, encKeys []prefixSSEPair) encrypt.ServerSide {
|
||||
for _, k := range encKeys {
|
||||
if strings.HasPrefix(resource, k.Prefix) {
|
||||
return k.SSE
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAndCreateEncryptionKeys(ctx *cli.Context) (encMap map[string][]prefixSSEPair, err *probe.Error) {
|
||||
encMap = make(map[string][]prefixSSEPair, 0)
|
||||
|
||||
for _, v := range ctx.StringSlice("enc-kms") {
|
||||
prefixPair, alias, err := validateAndParseKey(ctx, v, sseKMS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encMap[alias] = append(encMap[alias], *prefixPair)
|
||||
}
|
||||
|
||||
for _, v := range ctx.StringSlice("enc-s3") {
|
||||
prefixPair, alias, err := validateAndParseKey(ctx, v, sseS3)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encMap[alias] = append(encMap[alias], *prefixPair)
|
||||
}
|
||||
|
||||
for _, v := range ctx.StringSlice("enc-c") {
|
||||
prefixPair, alias, err := validateAndParseKey(ctx, v, sseC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encMap[alias] = append(encMap[alias], *prefixPair)
|
||||
}
|
||||
|
||||
for i := range encMap {
|
||||
err = validateOverLappingSSEKeys(encMap[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
for alias, ps := range encMap {
|
||||
if hostCfg := mustGetHostConfig(alias); hostCfg == nil {
|
||||
for _, p := range ps {
|
||||
return nil, errSSEInvalidAlias(p.Prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, encKeys := range encMap {
|
||||
sort.Sort(byPrefixLength(encKeys))
|
||||
}
|
||||
|
||||
return encMap, nil
|
||||
}
|
||||
|
||||
func validateAndParseKey(ctx *cli.Context, key string, keyType sseKeyType) (SSEPair *prefixSSEPair, alias string, perr *probe.Error) {
|
||||
matchedCount := 0
|
||||
alias, prefix, encKey, keyErr := parseSSEKey(key, keyType)
|
||||
if keyErr != nil {
|
||||
return nil, "", keyErr
|
||||
}
|
||||
if alias == "" {
|
||||
return nil, "", errSSEInvalidAlias(prefix).Trace(key)
|
||||
}
|
||||
|
||||
if (keyType == sseKMS || keyType == sseC) && encKey == "" {
|
||||
return nil, "", errSSEClientKeyFormat("SSE-C/KMS key should be of the form alias/prefix=key,... ").Trace(key)
|
||||
}
|
||||
|
||||
for _, arg := range ctx.Args() {
|
||||
if strings.HasPrefix(arg, alias+"/"+prefix) {
|
||||
matchedCount++
|
||||
}
|
||||
}
|
||||
|
||||
if matchedCount == 0 {
|
||||
return nil, "", errSSEPrefixMatch()
|
||||
}
|
||||
|
||||
ssePairPrefix := alias + "/" + prefix
|
||||
var sse encrypt.ServerSide
|
||||
var err error
|
||||
|
||||
switch keyType {
|
||||
case sseC:
|
||||
sse, err = encrypt.NewSSEC([]byte(encKey))
|
||||
case sseKMS:
|
||||
sse, err = encrypt.NewSSEKMS(encKey, nil)
|
||||
case sseS3:
|
||||
sse = encrypt.NewSSE()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, "", probe.NewError(err).Trace(key)
|
||||
}
|
||||
|
||||
return &prefixSSEPair{
|
||||
Prefix: ssePairPrefix,
|
||||
SSE: sse,
|
||||
}, alias, nil
|
||||
}
|
||||
|
||||
func validateOverLappingSSEKeys(keyMap []prefixSSEPair) (err *probe.Error) {
|
||||
for i := 0; i < len(keyMap); i++ {
|
||||
for j := i + 1; j < len(keyMap); j++ {
|
||||
if strings.HasPrefix(keyMap[i].Prefix, keyMap[j].Prefix) ||
|
||||
strings.HasPrefix(keyMap[j].Prefix, keyMap[i].Prefix) {
|
||||
return errSSEOverlappingAlias(keyMap[i].Prefix, keyMap[j].Prefix)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func splitKey(sseKey string) (alias, prefix string) {
|
||||
x := strings.SplitN(sseKey, "/", 2)
|
||||
switch len(x) {
|
||||
case 2:
|
||||
return x[0], x[1]
|
||||
case 1:
|
||||
return x[0], ""
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func parseSSEKey(sseKey string, keyType sseKeyType) (
|
||||
alias string,
|
||||
prefix string,
|
||||
key string,
|
||||
err *probe.Error,
|
||||
) {
|
||||
if keyType == sseS3 {
|
||||
alias, prefix = splitKey(sseKey)
|
||||
return
|
||||
}
|
||||
|
||||
var path string
|
||||
alias, path = splitKey(sseKey)
|
||||
splitPath := strings.Split(path, "=")
|
||||
if len(splitPath) == 0 {
|
||||
err = errSSEKeyMissing().Trace(sseKey)
|
||||
return
|
||||
}
|
||||
|
||||
aliasPlusPrefix := strings.Join(splitPath[:len(splitPath)-1], "=")
|
||||
prefix = strings.Replace(aliasPlusPrefix, alias+"/", "", 1)
|
||||
key = splitPath[len(splitPath)-1]
|
||||
|
||||
if keyType == sseC {
|
||||
keyB, de := base64.RawStdEncoding.DecodeString(key)
|
||||
if de != nil {
|
||||
err = errSSEClientKeyFormat("One of the inserted keys was " + strconv.Itoa(len(key)) + " bytes and did not have valid base64 raw encoding.").Trace(sseKey)
|
||||
return
|
||||
}
|
||||
key = string(keyB)
|
||||
if len(key) != 32 {
|
||||
err = errSSEClientKeyFormat("The plain text key was " + strconv.Itoa(len(key)) + " bytes but should be 32 bytes long").Trace(sseKey)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if keyType == sseKMS {
|
||||
if !validKMSKeyName(key) {
|
||||
err = errSSEKMSKeyFormat("One of the inserted keys was " + strconv.Itoa(len(key)) + " bytes and did not have a valid KMS key name.").Trace(sseKey)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func validKMSKeyName(s string) bool {
|
||||
if s == "" || s == "_" {
|
||||
return false
|
||||
}
|
||||
|
||||
n := len(s) - 1
|
||||
for i, r := range s {
|
||||
switch {
|
||||
case r >= '0' && r <= '9':
|
||||
case r >= 'A' && r <= 'Z':
|
||||
case r >= 'a' && r <= 'z':
|
||||
case r == '-' && i > 0 && i < n:
|
||||
case r == '_':
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
134
cmd/encryption-methods_test.go
Normal file
134
cmd/encryption-methods_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||
//
|
||||
// # This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEncryptionKeys(t *testing.T) {
|
||||
baseAlias := "mintest"
|
||||
basePrefix := "two/layer/prefix"
|
||||
baseObject := "object_name"
|
||||
sseKey := "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA"
|
||||
sseKeyPlain := "01234567890123456789012345678900"
|
||||
|
||||
// INVALID KEYS
|
||||
sseKeyInvalidShort := "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2"
|
||||
sseKeyInvalidSymbols := "MDEyMzQ1Njc4O____jM0N!!!ODkwMTIzNDU2Nzg5MDA"
|
||||
sseKeyInvalidSpaces := "MDE yMzQ1Njc4OTAxM jM0NTY3ODkwMTIzNDU2Nzg5MDA"
|
||||
sseKeyInvalidPrefixSpace := " MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA"
|
||||
sseKeyInvalidOneShort := "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MD"
|
||||
|
||||
testCases := []struct {
|
||||
encryptionKey string
|
||||
keyPlain string
|
||||
alias string
|
||||
prefix string
|
||||
object string
|
||||
sseType sseKeyType
|
||||
success bool
|
||||
}{
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKey),
|
||||
keyPlain: sseKeyPlain,
|
||||
alias: baseAlias,
|
||||
prefix: basePrefix,
|
||||
object: baseObject,
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s=/%s=/%s==%s", baseAlias, basePrefix, baseObject, sseKey),
|
||||
keyPlain: sseKeyPlain,
|
||||
alias: baseAlias + "=",
|
||||
prefix: basePrefix + "=",
|
||||
object: baseObject + "=",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s//%s//%s/=%s", baseAlias, basePrefix, baseObject, sseKey),
|
||||
keyPlain: sseKeyPlain,
|
||||
alias: baseAlias + "/",
|
||||
prefix: basePrefix + "/",
|
||||
object: baseObject + "/",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s==%s", baseAlias, basePrefix, baseObject, sseKey),
|
||||
keyPlain: sseKeyPlain,
|
||||
alias: baseAlias,
|
||||
prefix: basePrefix,
|
||||
object: baseObject + "=",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s!@_==_$^&*=%s", baseAlias, basePrefix, baseObject, sseKey),
|
||||
keyPlain: sseKeyPlain,
|
||||
alias: baseAlias,
|
||||
prefix: basePrefix,
|
||||
object: baseObject + "!@_==_$^&*",
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s=%sXXXXX", baseAlias, basePrefix, baseObject, sseKey),
|
||||
success: false,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyInvalidShort),
|
||||
success: false,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyInvalidSymbols),
|
||||
success: false,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyInvalidSpaces),
|
||||
success: false,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s=%s", baseAlias, basePrefix, baseObject, sseKeyInvalidPrefixSpace),
|
||||
success: false,
|
||||
},
|
||||
{
|
||||
encryptionKey: fmt.Sprintf("%s/%s/%s==%s", baseAlias, basePrefix, baseObject, sseKeyInvalidOneShort),
|
||||
success: false,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
alias, prefix, key, err := parseSSEKey(tc.encryptionKey, sseC)
|
||||
if tc.success {
|
||||
if err != nil {
|
||||
t.Fatalf("Test %d: Expected success, got %s", i+1, err)
|
||||
}
|
||||
if fmt.Sprintf("%s/%s", alias, prefix) != fmt.Sprintf("%s/%s/%s", tc.alias, tc.prefix, tc.object) {
|
||||
t.Fatalf("Test %d: alias and prefix parsing was invalid, expected %s/%s/%s, got %s/%s", i, tc.alias, tc.prefix, tc.object, alias, prefix)
|
||||
}
|
||||
if key != tc.keyPlain {
|
||||
t.Fatalf("Test %d: sse key parsing is invalid, expected %s, got %s", i, tc.keyPlain, key)
|
||||
}
|
||||
}
|
||||
|
||||
if !tc.success {
|
||||
if err == nil {
|
||||
t.Fatalf("Test %d: Expected error, got success", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
cmd/error.go
17
cmd/error.go
@@ -178,3 +178,20 @@ func deprecatedError(newCommandName string) {
|
||||
err := probe.NewError(fmt.Errorf("Please use '%s' instead", newCommandName))
|
||||
fatal(err, "Deprecated command")
|
||||
}
|
||||
|
||||
// deprecatedError function for deprecated flags
|
||||
func deprecatedFlagError(oldFlag, newFlag string) {
|
||||
err := probe.NewError(fmt.Errorf("'%s' has been deprecated, please use %s instead", oldFlag, newFlag))
|
||||
fatal(err, "a deprecated Flag")
|
||||
}
|
||||
|
||||
func deprecatedFlagsWarning(cliCtx *cli.Context) {
|
||||
for _, v := range cliCtx.Args() {
|
||||
switch v {
|
||||
case "--encrypt", "-encrypt":
|
||||
deprecatedFlagError("--encrypt", "--enc-s3 or --enc-kms")
|
||||
case "--encrypt-key", "-encrypt-key":
|
||||
deprecatedFlagError("--encrypt-key", "--enc-c")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ func mainFind(cliCtx *cli.Context) error {
|
||||
console.SetColor("FindExecErr", color.New(color.FgRed, color.Italic, color.Bold))
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
checkFindSyntax(ctx, cliCtx, encKeyDB)
|
||||
|
||||
34
cmd/flags.go
34
cmd/flags.go
@@ -88,16 +88,26 @@ var globalFlags = []cli.Flag{
|
||||
},
|
||||
}
|
||||
|
||||
// Flags common across all I/O commands such as cp, mirror, stat, pipe etc.
|
||||
var ioFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "encrypt-key",
|
||||
Usage: "encrypt/decrypt objects (using server-side encryption with customer provided keys)",
|
||||
EnvVar: envPrefix + "ENCRYPT_KEY",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "encrypt",
|
||||
Usage: "encrypt/decrypt objects (using server-side encryption with server managed keys)",
|
||||
EnvVar: envPrefix + "ENCRYPT",
|
||||
},
|
||||
// bundled encryption flags
|
||||
var encFlags = []cli.Flag{
|
||||
encCFlag,
|
||||
encKSMFlag,
|
||||
encS3Flag,
|
||||
}
|
||||
|
||||
var encCFlag = cli.StringSliceFlag{
|
||||
Name: "enc-c",
|
||||
Usage: "encrypt/decrypt objects using client provided keys. (multiple keys can be provided) Format: Raw base64 encoding.",
|
||||
}
|
||||
|
||||
var encKSMFlag = cli.StringSliceFlag{
|
||||
Name: "enc-kms",
|
||||
Usage: "encrypt/decrypt objects using specific server-side encryption keys. (multiple keys can be provided)",
|
||||
EnvVar: envPrefix + "ENC_KMS",
|
||||
}
|
||||
|
||||
var encS3Flag = cli.StringSliceFlag{
|
||||
Name: "enc-s3",
|
||||
Usage: "encrypt/decrypt objects using server-side default keys and configurations. (multiple keys can be provided).",
|
||||
EnvVar: envPrefix + "ENC_S3",
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ var getCmd = cli.Command{
|
||||
Action: mainGet,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(ioFlags, globalFlags...), getFlags...),
|
||||
Flags: append(append(globalFlags, encCFlag), getFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -47,29 +47,32 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT: list of comma delimited prefixes
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
|
||||
EXAMPLES:
|
||||
1. Get an object from S3 storage to local file system
|
||||
{{.Prompt}} {{.HelpName}} ALIAS/BUCKET/object path-to/object
|
||||
1. Get an object from MinIO storage to local file system
|
||||
{{.Prompt}} {{.HelpName}} play/mybucket/object path-to/object
|
||||
|
||||
2. Get an object from MinIO storage using encryption
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "play/mybucket/object=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" play/mybucket/object path-to/object
|
||||
`,
|
||||
}
|
||||
|
||||
// mainGet is the entry point for get command.
|
||||
func mainGet(cliCtx *cli.Context) (e error) {
|
||||
ctx, cancelGet := context.WithCancel(globalContext)
|
||||
defer cancelGet()
|
||||
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
args := cliCtx.Args()
|
||||
if len(args) != 2 {
|
||||
showCommandHelpAndExit(cliCtx, 1) // last argument is exit code.
|
||||
}
|
||||
|
||||
ctx, cancelGet := context.WithCancel(globalContext)
|
||||
defer cancelGet()
|
||||
|
||||
encryptionKeys, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
if err != nil {
|
||||
err.Trace(cliCtx.Args()...)
|
||||
}
|
||||
fatalIf(err, "unable to parse encryption keys")
|
||||
|
||||
// get source and target
|
||||
sourceURLs := args[:len(args)-1]
|
||||
targetURL := args[len(args)-1]
|
||||
@@ -89,7 +92,7 @@ func mainGet(cliCtx *cli.Context) (e error) {
|
||||
opts := prepareCopyURLsOpts{
|
||||
sourceURLs: sourceURLs,
|
||||
targetURL: targetURL,
|
||||
encKeyDB: encKeyDB,
|
||||
encKeyDB: encryptionKeys,
|
||||
ignoreBucketExistsCheck: true,
|
||||
}
|
||||
|
||||
@@ -121,7 +124,7 @@ func mainGet(cliCtx *cli.Context) (e error) {
|
||||
urls := doCopy(ctx, doCopyOpts{
|
||||
cpURLs: getURLs,
|
||||
pg: pg,
|
||||
encKeyDB: encKeyDB,
|
||||
encryptionKeys: encryptionKeys,
|
||||
updateProgressTotal: true,
|
||||
})
|
||||
if urls.Error != nil {
|
||||
|
||||
@@ -59,7 +59,7 @@ var headCmd = cli.Command{
|
||||
Action: mainHead,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(headFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(headFlags, encCFlag), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -69,8 +69,6 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
|
||||
NOTE:
|
||||
'{{.HelpName}}' automatically decompresses 'gzip', 'bzip2' compressed objects.
|
||||
@@ -80,11 +78,10 @@ EXAMPLES:
|
||||
{{.Prompt}} {{.HelpName}} -n 1 s3/csv-data/population.csv.gz
|
||||
|
||||
2. Display only first line from server encrypted object on Amazon S3.
|
||||
{{.Prompt}} {{.HelpName}} -n 1 --encrypt-key 's3/csv-data=32byteslongsecretkeymustbegiven1' s3/csv-data/population.csv
|
||||
{{.Prompt}} {{.HelpName}} -n 1 --enc-c 's3/csv-data=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA' s3/csv-data/population.csv
|
||||
|
||||
3. Display only first line from server encrypted object on Amazon S3. In case the encryption key contains non-printable character like tab, pass the
|
||||
base64 encoded string as key.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "s3/json-data=MzJieXRlc2xvbmdzZWNyZXRrZQltdXN0YmVnaXZlbjE=" s3/json-data/population.json
|
||||
3. Display only first line from server encrypted object on Amazon S3.
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "s3/json-data=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/json-data/population.json
|
||||
|
||||
4. Display the first lines of a specific object version.
|
||||
{{.Prompt}} {{.HelpName}} --version-id "3ddac055-89a7-40fa-8cd3-530a5581b6b8" s3/json-data/population.json
|
||||
@@ -188,7 +185,7 @@ func parseHeadSyntax(ctx *cli.Context) (args []string, versionID string, timeRef
|
||||
// mainHead is the main entry point for head command.
|
||||
func mainHead(ctx *cli.Context) error {
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(ctx)
|
||||
encryptionKeys, err := validateAndCreateEncryptionKeys(ctx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
args, versionID, timeRef := parseHeadSyntax(ctx)
|
||||
@@ -203,7 +200,15 @@ func mainHead(ctx *cli.Context) error {
|
||||
|
||||
// Convert arguments to URLs: expand alias, fix format.
|
||||
for _, url := range ctx.Args() {
|
||||
fatalIf(headURL(url, versionID, timeRef, encKeyDB, ctx.Int64("lines"), ctx.Bool("zip")).Trace(url), "Unable to read from `"+url+"`.")
|
||||
err = headURL(
|
||||
url,
|
||||
versionID,
|
||||
timeRef,
|
||||
encryptionKeys,
|
||||
ctx.Int64("lines"),
|
||||
ctx.Bool("zip"),
|
||||
)
|
||||
fatalIf(err.Trace(url), "Unable to read from `"+url+"`.")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -57,7 +57,7 @@ var ilmRestoreCmd = cli.Command{
|
||||
Action: mainILMRestore,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(ilmRestoreFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(ilmRestoreFlags, encCFlag), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -86,7 +86,7 @@ EXAMPLES:
|
||||
{{.Prompt}} {{.HelpName}} --recursive --versions myminio/mybucket/dir/
|
||||
|
||||
5. Restore an SSE-C encrypted object.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "myminio/mybucket/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=" myminio/mybucket/myobject.txt
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "myminio/mybucket/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" myminio/mybucket/myobject.txt
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ func mainILMRestore(cliCtx *cli.Context) (cErr error) {
|
||||
includeVersions := cliCtx.Bool("versions")
|
||||
days := cliCtx.Int("days")
|
||||
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
targetAlias, targetURL, _ := mustExpandAlias(aliasedURL)
|
||||
|
||||
10
cmd/main.go
10
cmd/main.go
@@ -256,9 +256,6 @@ func migrate() {
|
||||
// Migrate config files if any.
|
||||
migrateConfig()
|
||||
|
||||
// Migrate session files if any.
|
||||
migrateSession()
|
||||
|
||||
// Migrate shared urls if any.
|
||||
migrateShare()
|
||||
}
|
||||
@@ -275,11 +272,6 @@ func initMC() {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if mc session directory exists.
|
||||
if !isSessionDirExists() {
|
||||
fatalIf(createSessionDir().Trace(), "Unable to create session config directory.")
|
||||
}
|
||||
|
||||
// Check if mc share directory exists.
|
||||
if !isShareDirExists() {
|
||||
initShareConfig()
|
||||
@@ -360,6 +352,8 @@ func installAutoCompletion() {
|
||||
}
|
||||
|
||||
func registerBefore(ctx *cli.Context) error {
|
||||
deprecatedFlagsWarning(ctx)
|
||||
|
||||
if ctx.IsSet("config-dir") {
|
||||
// Set the config directory.
|
||||
setMcConfigDir(ctx.String("config-dir"))
|
||||
|
||||
@@ -151,7 +151,7 @@ var mirrorCmd = cli.Command{
|
||||
Action: mainMirror,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(mirrorFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(mirrorFlags, encFlags...), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -161,9 +161,10 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT: list of comma delimited prefixes
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key).
|
||||
MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key).
|
||||
|
||||
EXAMPLES:
|
||||
01. Mirror a bucket recursively from MinIO cloud storage to a bucket on Amazon S3 cloud storage.
|
||||
@@ -206,20 +207,16 @@ EXAMPLES:
|
||||
12. Mirror objects older than 30 days from Amazon S3 bucket test to a local folder.
|
||||
{{.Prompt}} {{.HelpName}} --older-than 30d s3/test ~/test
|
||||
|
||||
13. Mirror server encrypted objects from MinIO cloud storage to a bucket on Amazon S3 cloud storage
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "minio/photos=32byteslongsecretkeymustbegiven1,s3/archive=32byteslongsecretkeymustbegiven2" minio/photos/ s3/archive/
|
||||
13. Mirror server encrypted objects from Amazon S3 cloud storage to a bucket on Amazon S3 cloud storage
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "minio/archive=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" --enc-c "s3/archive=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5BBB" s3/archive/ minio/archive/
|
||||
|
||||
14. Mirror server encrypted objects from MinIO cloud storage to a bucket on Amazon S3 cloud storage. In case the encryption key contains
|
||||
non-printable character like tab, pass the base64 encoded string as key.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "s3/photos/=32byteslongsecretkeymustbegiven1,play/archive/=MzJieXRlc2xvbmdzZWNyZXRrZQltdXN0YmVnaXZlbjE=" s3/photos/ play/archive/
|
||||
|
||||
15. Update 'Cache-Control' header on all existing objects recursively.
|
||||
14. Update 'Cache-Control' header on all existing objects recursively.
|
||||
{{.Prompt}} {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000" myminio/video-files myminio/video-files
|
||||
|
||||
16. Mirror a local folder recursively to Amazon S3 cloud storage and preserve all local file attributes.
|
||||
15. Mirror a local folder recursively to Amazon S3 cloud storage and preserve all local file attributes.
|
||||
{{.Prompt}} {{.HelpName}} -a backup/ s3/archive
|
||||
|
||||
17. Cross mirror between sites in a active-active deployment.
|
||||
16. Cross mirror between sites in a active-active deployment.
|
||||
Site-A: {{.Prompt}} {{.HelpName}} --active-active siteA siteB
|
||||
Site-B: {{.Prompt}} {{.HelpName}} --active-active siteB siteA
|
||||
`,
|
||||
@@ -1103,8 +1100,7 @@ func mainMirror(cliCtx *cli.Context) error {
|
||||
ctx, cancelMirror := context.WithCancel(globalContext)
|
||||
defer cancelMirror()
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// check 'mirror' cli arguments.
|
||||
|
||||
108
cmd/mv-main.go
108
cmd/mv-main.go
@@ -20,7 +20,6 @@ package cmd
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/fatih/color"
|
||||
@@ -52,10 +51,6 @@ var (
|
||||
Name: "attr",
|
||||
Usage: "add custom metadata for the object",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "continue, c",
|
||||
Usage: "create or resume move session",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "preserve, a",
|
||||
Usage: "preserve filesystem attributes (mode, ownership, timestamps)",
|
||||
@@ -74,7 +69,7 @@ var mvCmd = cli.Command{
|
||||
Action: mainMove,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(mvFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(mvFlags, encFlags...), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -84,9 +79,10 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT: list of comma delimited prefixes
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key).
|
||||
MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key).
|
||||
|
||||
EXAMPLES:
|
||||
01. Move a list of objects from local file system to Amazon S3 cloud storage.
|
||||
@@ -113,30 +109,26 @@ EXAMPLES:
|
||||
08. Move a local folder with space separated characters to Amazon S3 cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --recursive 'workdir/documents/May 2014/' s3/miniocloud
|
||||
|
||||
09. Move a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --recursive --encrypt-key "s3/documents/=32byteslongsecretkeymustbegiven1,myminio/documents/=32byteslongsecretkeymustbegiven2" s3/documents/ myminio/documents/
|
||||
|
||||
10. Move a folder with encrypted objects recursively from Amazon S3 to MinIO cloud storage. In case the encryption key contains non-printable character like tab, pass the
|
||||
base64 encoded string as key.
|
||||
{{.Prompt}} {{.HelpName}} --recursive --encrypt-key "s3/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=,myminio/documents/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=" s3/documents/ myminio/documents/
|
||||
|
||||
11. Move a list of objects from local file system to MinIO cloud storage with specified metadata, separated by ";"
|
||||
09. Move a list of objects from local file system to MinIO cloud storage with specified metadata, separated by ";"
|
||||
{{.Prompt}} {{.HelpName}} --attr "key1=value1;key2=value2" Music/*.mp4 play/mybucket/
|
||||
|
||||
12. Move a folder recursively from MinIO cloud storage to Amazon S3 cloud storage with Cache-Control and custom metadata, separated by ";".
|
||||
10. Move a folder recursively from MinIO cloud storage to Amazon S3 cloud storage with Cache-Control and custom metadata, separated by ";".
|
||||
{{.Prompt}} {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000;key1=value1;key2=value2" --recursive play/mybucket/myfolder/ s3/mybucket/
|
||||
|
||||
13. Move a text file to an object storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object.
|
||||
11. Move a text file to an object storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object.
|
||||
{{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY myobject.txt play/mybucket
|
||||
|
||||
14. Move a text file to an object storage and create or resume copy session.
|
||||
{{.Prompt}} {{.HelpName}} --recursive --continue dir/ play/mybucket
|
||||
|
||||
15. Move a text file to an object storage and preserve the file system attribute as metadata.
|
||||
12. Move a text file to an object storage and preserve the file system attribute as metadata.
|
||||
{{.Prompt}} {{.HelpName}} -a myobject.txt play/mybucket
|
||||
|
||||
16. Move a text file to an object storage and disable multipart upload feature.
|
||||
13. Move a text file to an object storage and disable multipart upload feature.
|
||||
{{.Prompt}} {{.HelpName}} --disable-multipart myobject.txt play/mybucket
|
||||
|
||||
14. Move a folder using client provided encryption keys from Amazon S3 to MinIO cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --r --enc-c "s3/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MBB" --enc-c "myminio/documents/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/documents/ myminio/documents/
|
||||
|
||||
15. Move a folder using specific server managed encryption keys from Amazon S3 to MinIO cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --r --enc-s3 "s3/documents/=my-s3-key" --enc-s3 "myminio/documents/=my-minio-key" s3/documents/ myminio/documents/
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -211,19 +203,8 @@ func mainMove(cliCtx *cli.Context) error {
|
||||
ctx, cancelMove := context.WithCancel(globalContext)
|
||||
defer cancelMove()
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// Parse metadata.
|
||||
userMetaMap := make(map[string]string)
|
||||
if cliCtx.String("attr") != "" {
|
||||
userMetaMap, err = getMetaDataEntry(cliCtx.String("attr"))
|
||||
fatalIf(err, "Unable to parse attribute %v", cliCtx.String("attr"))
|
||||
}
|
||||
|
||||
// check 'copy' cli arguments.
|
||||
checkCopySyntax(cliCtx)
|
||||
console.SetColor("Copy", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
if cliCtx.NArg() == 2 {
|
||||
args := cliCtx.Args()
|
||||
@@ -234,63 +215,12 @@ func mainMove(cliCtx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("Copy", color.New(color.FgGreen, color.Bold))
|
||||
var err *probe.Error
|
||||
|
||||
recursive := cliCtx.Bool("recursive")
|
||||
olderThan := cliCtx.String("older-than")
|
||||
newerThan := cliCtx.String("newer-than")
|
||||
storageClass := cliCtx.String("storage-class")
|
||||
sseKeys := os.Getenv("MC_ENCRYPT_KEY")
|
||||
if key := cliCtx.String("encrypt-key"); key != "" {
|
||||
sseKeys = key
|
||||
}
|
||||
|
||||
if sseKeys != "" {
|
||||
sseKeys, err = getDecodedKey(sseKeys)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
}
|
||||
sse := cliCtx.String("encrypt")
|
||||
|
||||
var session *sessionV8
|
||||
|
||||
if cliCtx.Bool("continue") {
|
||||
sessionID := getHash("mv", cliCtx.Args())
|
||||
if isSessionExists(sessionID) {
|
||||
session, err = loadSessionV8(sessionID)
|
||||
fatalIf(err.Trace(sessionID), "Unable to load session.")
|
||||
} else {
|
||||
session = newSessionV8(sessionID)
|
||||
session.Header.CommandType = "mv"
|
||||
session.Header.CommandBoolFlags["recursive"] = recursive
|
||||
session.Header.CommandStringFlags["older-than"] = olderThan
|
||||
session.Header.CommandStringFlags["newer-than"] = newerThan
|
||||
session.Header.CommandStringFlags["storage-class"] = storageClass
|
||||
session.Header.CommandStringFlags["encrypt-key"] = sseKeys
|
||||
session.Header.CommandStringFlags["encrypt"] = sse
|
||||
session.Header.CommandBoolFlags["session"] = cliCtx.Bool("continue")
|
||||
|
||||
if cliCtx.Bool("preserve") {
|
||||
session.Header.CommandBoolFlags["preserve"] = cliCtx.Bool("preserve")
|
||||
}
|
||||
session.Header.UserMetaData = userMetaMap
|
||||
session.Header.CommandBoolFlags["disable-multipart"] = cliCtx.Bool("disable-multipart")
|
||||
|
||||
var e error
|
||||
if session.Header.RootPath, e = os.Getwd(); e != nil {
|
||||
session.Delete()
|
||||
fatalIf(probe.NewError(e), "Unable to get current working folder.")
|
||||
}
|
||||
|
||||
// extract URLs.
|
||||
session.Header.CommandArgs = cliCtx.Args()
|
||||
}
|
||||
}
|
||||
|
||||
e := doCopySession(ctx, cancelMove, cliCtx, session, encKeyDB, true)
|
||||
if session != nil {
|
||||
session.Delete()
|
||||
}
|
||||
e := doCopySession(ctx, cancelMove, cliCtx, encKeyDB, true)
|
||||
|
||||
console.Colorize("Copy", "Waiting for move operations to complete")
|
||||
rmManager.close()
|
||||
|
||||
@@ -71,7 +71,7 @@ var pipeCmd = cli.Command{
|
||||
Action: mainPipe,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(pipeFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(pipeFlags, encFlags...), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -81,9 +81,10 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT: list of comma delimited prefix values
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key).
|
||||
MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key).
|
||||
|
||||
EXAMPLES:
|
||||
1. Write contents of stdin to a file on local filesystem.
|
||||
@@ -95,16 +96,19 @@ EXAMPLES:
|
||||
3. Copy an ISO image to an object on Amazon S3 cloud storage.
|
||||
{{.Prompt}} cat debian-8.2.iso | {{.HelpName}} s3/opensource-isos/gnuos.iso
|
||||
|
||||
4. Stream MySQL database dump to Amazon S3 directly.
|
||||
4. Copy an ISO image to an object on minio storage using KMS encryption.
|
||||
{{.Prompt}} cat debian-8.2.iso | {{.HelpName}} --enc-kms="minio/opensource-isos=my-key-name" minio/opensource-isos/gnuos.iso
|
||||
|
||||
5. Stream MySQL database dump to Amazon S3 directly.
|
||||
{{.Prompt}} mysqldump -u root -p ******* accountsdb | {{.HelpName}} s3/sql-backups/backups/accountsdb-oct-9-2015.sql
|
||||
|
||||
5. Write contents of stdin to an object on Amazon S3 cloud storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object.
|
||||
6. Write contents of stdin to an object on Amazon S3 cloud storage and assign REDUCED_REDUNDANCY storage-class to the uploaded object.
|
||||
{{.Prompt}} {{.HelpName}} --storage-class REDUCED_REDUNDANCY s3/personalbuck/meeting-notes.txt
|
||||
|
||||
6. Copy to MinIO cloud storage with specified metadata, separated by ";"
|
||||
7. Copy to MinIO cloud storage with specified metadata, separated by ";"
|
||||
{{.Prompt}} cat music.mp3 | {{.HelpName}} --attr "Cache-Control=max-age=90000,min-fresh=9000;Artist=Unknown" play/mybucket/music.mp3
|
||||
|
||||
7. Set tags to the uploaded objects
|
||||
8. Set tags to the uploaded objects
|
||||
{{.Prompt}} tar cvf - . | {{.HelpName}} --tags "category=prod&type=backup" play/mybucket/backup.tar
|
||||
`,
|
||||
}
|
||||
@@ -182,12 +186,9 @@ func checkPipeSyntax(ctx *cli.Context) {
|
||||
func mainPipe(ctx *cli.Context) error {
|
||||
// validate pipe input arguments.
|
||||
checkPipeSyntax(ctx)
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(ctx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// validate pipe input arguments.
|
||||
checkPipeSyntax(ctx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(ctx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// globalQuiet is true for no window size to get. We just need --quiet here.
|
||||
quiet := ctx.IsSet("quiet")
|
||||
|
||||
@@ -51,7 +51,7 @@ var putCmd = cli.Command{
|
||||
Action: mainPut,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(ioFlags, globalFlags...), putFlags...),
|
||||
Flags: append(append(encFlags, globalFlags...), putFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -61,17 +61,26 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT: list of comma delimited prefixes
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
MC_ENC_KMS: KMS encryption key in the form of (alias/prefix=key).
|
||||
MC_ENC_S3: S3 encryption key in the form of (alias/prefix=key).
|
||||
|
||||
EXAMPLES:
|
||||
1. Put an object from local file system to S3 storage
|
||||
{{.Prompt}} {{.HelpName}} path-to/object ALIAS/BUCKET
|
||||
{{.Prompt}} {{.HelpName}} path-to/object play/mybucket
|
||||
|
||||
2. Put an object from local file system to S3 bucket with name
|
||||
{{.Prompt}} {{.HelpName}} path-to/object ALIAS/BUCKET/OBJECT-NAME
|
||||
{{.Prompt}} {{.HelpName}} path-to/object play/mybucket/object
|
||||
|
||||
3. Put an object from local file system to S3 bucket under a prefix
|
||||
{{.Prompt}} {{.HelpName}} path-to/object ALIAS/BUCKET/PREFIX/
|
||||
{{.Prompt}} {{.HelpName}} path-to/object play/mybucket/object-prefix/
|
||||
|
||||
4. Put an object to MinIO storage using sse-c encryption
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "play/mybucket/object=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" path-to/object play/mybucket/object
|
||||
|
||||
5. Put an object to MinIO storage using sse-kms encryption
|
||||
{{.Prompt}} {{.HelpName}} --enc-kms path-to/object play/mybucket/object
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -99,8 +108,12 @@ func mainPut(cliCtx *cli.Context) (e error) {
|
||||
fatalIf(errInvalidArgument().Trace(strconv.Itoa(threads)), "Invalid number of threads")
|
||||
}
|
||||
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
// Parse encryption keys per command.
|
||||
encryptionKeys, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
if err != nil {
|
||||
err.Trace(cliCtx.Args()...)
|
||||
}
|
||||
fatalIf(err, "SSE Error")
|
||||
|
||||
if len(args) < 2 {
|
||||
fatalIf(errInvalidArgument().Trace(args...), "Invalid number of arguments.")
|
||||
@@ -125,7 +138,7 @@ func mainPut(cliCtx *cli.Context) (e error) {
|
||||
opts := prepareCopyURLsOpts{
|
||||
sourceURLs: sourceURLs,
|
||||
targetURL: targetURL,
|
||||
encKeyDB: encKeyDB,
|
||||
encKeyDB: encryptionKeys,
|
||||
ignoreBucketExistsCheck: true,
|
||||
}
|
||||
|
||||
@@ -159,7 +172,7 @@ func mainPut(cliCtx *cli.Context) (e error) {
|
||||
urls := doCopy(ctx, doCopyOpts{
|
||||
cpURLs: putURLs,
|
||||
pg: pg,
|
||||
encKeyDB: encKeyDB,
|
||||
encryptionKeys: encryptionKeys,
|
||||
multipartSize: size,
|
||||
multipartThreads: strconv.Itoa(threads),
|
||||
})
|
||||
|
||||
@@ -111,7 +111,7 @@ var rmCmd = cli.Command{
|
||||
Action: mainRm,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(rmFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(rmFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -121,8 +121,6 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
|
||||
EXAMPLES:
|
||||
01. Remove a file.
|
||||
@@ -152,16 +150,13 @@ EXAMPLES:
|
||||
09. Drop all incomplete uploads on the bucket 'jazz-songs'.
|
||||
{{.Prompt}} {{.HelpName}} --incomplete --recursive --force s3/jazz-songs/
|
||||
|
||||
10. Remove an encrypted object from Amazon S3 cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "s3/sql-backups/=32byteslongsecretkeymustbegiven1" s3/sql-backups/1999/old-backup.tgz
|
||||
|
||||
11. Bypass object retention in governance mode and delete the object.
|
||||
10. Bypass object retention in governance mode and delete the object.
|
||||
{{.Prompt}} {{.HelpName}} --bypass s3/pop-songs/
|
||||
|
||||
12. Remove a particular version ID.
|
||||
11. Remove a particular version ID.
|
||||
{{.Prompt}} {{.HelpName}} s3/docs/money.xls --version-id "f20f3792-4bd4-4288-8d3c-b9d05b3b62f6"
|
||||
|
||||
13. Remove all object versions older than one year.
|
||||
12. Remove all object versions older than one year.
|
||||
{{.Prompt}} {{.HelpName}} s3/docs/ --recursive --versions --rewind 365d
|
||||
|
||||
14. Perform a fake removal of object(s) versions that are non-current and older than 10 days. If top-level version is a delete
|
||||
@@ -211,7 +206,7 @@ func (r rmMessage) JSON() string {
|
||||
}
|
||||
|
||||
// Validate command line arguments.
|
||||
func checkRmSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string][]prefixSSEPair) {
|
||||
func checkRmSyntax(ctx context.Context, cliCtx *cli.Context) {
|
||||
// Set command flags from context.
|
||||
isForce := cliCtx.Bool("force")
|
||||
isRecursive := cliCtx.Bool("recursive")
|
||||
@@ -255,7 +250,7 @@ func checkRmSyntax(ctx context.Context, cliCtx *cli.Context, encKeyDB map[string
|
||||
// Note: UNC path using / works properly in go 1.9.2 even though it breaks the UNC specification.
|
||||
url = filepath.ToSlash(filepath.Clean(url))
|
||||
// namespace removal applies only for non FS. So filter out if passed url represents a directory
|
||||
dir, _ := isAliasURLDir(ctx, url, encKeyDB, time.Time{}, false)
|
||||
dir, _ := isAliasURLDir(ctx, url, nil, time.Time{}, false)
|
||||
if dir {
|
||||
_, path := url2Alias(url)
|
||||
isNamespaceRemoval = (path == "")
|
||||
@@ -312,7 +307,6 @@ func removeSingle(url, versionID string, opts removeOpts) error {
|
||||
urlStr: url,
|
||||
versionID: versionID,
|
||||
fileAttr: false,
|
||||
encKeyDB: opts.encKeyDB,
|
||||
timeRef: time.Time{},
|
||||
isZip: false,
|
||||
ignoreBucketExistsCheck: false,
|
||||
@@ -407,7 +401,6 @@ type removeOpts struct {
|
||||
isForceDel bool
|
||||
olderThan string
|
||||
newerThan string
|
||||
encKeyDB map[string][]prefixSSEPair
|
||||
}
|
||||
|
||||
func printDryRunMsg(targetAlias string, content *ClientContent, printModTime bool) {
|
||||
@@ -707,14 +700,8 @@ func mainRm(cliCtx *cli.Context) error {
|
||||
ctx, cancelRm := context.WithCancel(globalContext)
|
||||
defer cancelRm()
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
checkRmSyntax(ctx, cliCtx)
|
||||
|
||||
// check 'rm' cli arguments.
|
||||
checkRmSyntax(ctx, cliCtx, encKeyDB)
|
||||
|
||||
// rm specific flags.
|
||||
isIncomplete := cliCtx.Bool("incomplete")
|
||||
isRecursive := cliCtx.Bool("recursive")
|
||||
isFake := cliCtx.Bool("dry-run") || cliCtx.Bool("fake")
|
||||
@@ -752,7 +739,6 @@ func mainRm(cliCtx *cli.Context) error {
|
||||
isBypass: isBypass,
|
||||
olderThan: olderThan,
|
||||
newerThan: newerThan,
|
||||
encKeyDB: encKeyDB,
|
||||
})
|
||||
} else {
|
||||
e = removeSingle(url, versionID, removeOpts{
|
||||
@@ -763,7 +749,6 @@ func mainRm(cliCtx *cli.Context) error {
|
||||
isBypass: isBypass,
|
||||
olderThan: olderThan,
|
||||
newerThan: newerThan,
|
||||
encKeyDB: encKeyDB,
|
||||
})
|
||||
}
|
||||
if rerr == nil {
|
||||
@@ -790,7 +775,6 @@ func mainRm(cliCtx *cli.Context) error {
|
||||
isBypass: isBypass,
|
||||
olderThan: olderThan,
|
||||
newerThan: newerThan,
|
||||
encKeyDB: encKeyDB,
|
||||
})
|
||||
} else {
|
||||
e = removeSingle(url, versionID, removeOpts{
|
||||
@@ -801,7 +785,6 @@ func mainRm(cliCtx *cli.Context) error {
|
||||
isBypass: isBypass,
|
||||
olderThan: olderThan,
|
||||
newerThan: newerThan,
|
||||
encKeyDB: encKeyDB,
|
||||
})
|
||||
}
|
||||
if rerr == nil {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/pkg/v2/console"
|
||||
)
|
||||
|
||||
// fixateScanBar truncates or stretches text to fit within the terminal size.
|
||||
func fixateScanBar(text string, width int) string {
|
||||
if len([]rune(text)) > width {
|
||||
// Trim text to fit within the screen
|
||||
trimSize := len([]rune(text)) - width + 3 //"..."
|
||||
if trimSize < len([]rune(text)) {
|
||||
text = "..." + text[trimSize:]
|
||||
}
|
||||
} else {
|
||||
text += strings.Repeat(" ", width-len([]rune(text)))
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// Progress bar function report objects being scaned.
|
||||
type scanBarFunc func(string)
|
||||
|
||||
// scanBarFactory returns a progress bar function to report URL scanning.
|
||||
func scanBarFactory() scanBarFunc {
|
||||
fileCount := 0
|
||||
|
||||
// Cursor animate channel.
|
||||
cursorCh := cursorAnimate()
|
||||
return func(source string) {
|
||||
scanPrefix := fmt.Sprintf("[%s] %s ", humanize.Comma(int64(fileCount)), <-cursorCh)
|
||||
source = fixateScanBar(source, globalTermWidth-len([]rune(scanPrefix)))
|
||||
barText := scanPrefix + source
|
||||
console.PrintC("\r" + barText + "\r")
|
||||
fileCount++
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/minio/pkg/v2/console"
|
||||
"github.com/minio/pkg/v2/quick"
|
||||
)
|
||||
|
||||
// Migrates session header version '7' to '8'. The only
|
||||
// change was the adding of insecure global flag
|
||||
func migrateSessionV7ToV8() {
|
||||
for _, sid := range getSessionIDs() {
|
||||
sV7, err := loadSessionV7(sid)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err.ToGoError()) {
|
||||
continue
|
||||
}
|
||||
fatalIf(err.Trace(sid), "Unable to load version `7`. Migration failed please report this issue at https://github.com/minio/mc/issues.")
|
||||
}
|
||||
|
||||
// Close underlying session data file.
|
||||
sV7.DataFP.Close()
|
||||
|
||||
sessionVersion, e := strconv.Atoi(sV7.Header.Version)
|
||||
fatalIf(probe.NewError(e), "Unable to load version `7`. Migration failed please report this issue at https://github.com/minio/mc/issues.")
|
||||
if sessionVersion > 7 { // It is new format.
|
||||
continue
|
||||
}
|
||||
|
||||
sessionFile, err := getSessionFile(sid)
|
||||
fatalIf(err.Trace(sid), "Unable to get session file.")
|
||||
|
||||
// Initialize v7 header and migrate to new config.
|
||||
sV8Header := &sessionV8Header{}
|
||||
sV8Header.Version = globalSessionConfigVersion
|
||||
sV8Header.When = sV7.Header.When
|
||||
sV8Header.RootPath = sV7.Header.RootPath
|
||||
sV8Header.GlobalBoolFlags = sV7.Header.GlobalBoolFlags
|
||||
sV8Header.GlobalIntFlags = sV7.Header.GlobalIntFlags
|
||||
sV8Header.GlobalStringFlags = sV7.Header.GlobalStringFlags
|
||||
sV8Header.CommandType = sV7.Header.CommandType
|
||||
sV8Header.CommandArgs = sV7.Header.CommandArgs
|
||||
sV8Header.CommandBoolFlags = sV7.Header.CommandBoolFlags
|
||||
sV8Header.CommandIntFlags = sV7.Header.CommandIntFlags
|
||||
sV8Header.CommandStringFlags = sV7.Header.CommandStringFlags
|
||||
sV8Header.LastCopied = sV7.Header.LastCopied
|
||||
sV8Header.LastRemoved = sV7.Header.LastRemoved
|
||||
sV8Header.TotalBytes = sV7.Header.TotalBytes
|
||||
sV8Header.TotalObjects = int64(sV7.Header.TotalObjects)
|
||||
|
||||
// Add insecure flag to the new V8 header
|
||||
sV8Header.GlobalBoolFlags["insecure"] = false
|
||||
|
||||
qs, e := quick.NewConfig(sV8Header, nil)
|
||||
fatalIf(probe.NewError(e).Trace(sid), "Unable to initialize quick config for session '8' header.")
|
||||
|
||||
e = qs.Save(sessionFile)
|
||||
fatalIf(probe.NewError(e).Trace(sid, sessionFile), "Unable to migrate session from '7' to '8'.")
|
||||
|
||||
console.Println("Successfully migrated `" + sessionFile + "` from version `" + sV7.Header.Version + "` to " + "`" + sV8Header.Version + "`.")
|
||||
}
|
||||
}
|
||||
|
||||
// Migrates session header version '6' to '7'. Only change is
|
||||
// LastRemoved field which was added in version '7'.
|
||||
func migrateSessionV6ToV7() {
|
||||
for _, sid := range getSessionIDs() {
|
||||
sV6Header, err := loadSessionV6Header(sid)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err.ToGoError()) {
|
||||
continue
|
||||
}
|
||||
fatalIf(err.Trace(sid), "Unable to load version `6`. Migration failed please report this issue at https://github.com/minio/mc/issues.")
|
||||
}
|
||||
|
||||
sessionVersion, e := strconv.Atoi(sV6Header.Version)
|
||||
fatalIf(probe.NewError(e), "Unable to load version `6`. Migration failed please report this issue at https://github.com/minio/mc/issues.")
|
||||
if sessionVersion > 6 { // It is new format.
|
||||
continue
|
||||
}
|
||||
|
||||
sessionFile, err := getSessionFile(sid)
|
||||
fatalIf(err.Trace(sid), "Unable to get session file.")
|
||||
|
||||
// Initialize v7 header and migrate to new config.
|
||||
sV7Header := &sessionV7Header{}
|
||||
sV7Header.Version = "7"
|
||||
sV7Header.When = sV6Header.When
|
||||
sV7Header.RootPath = sV6Header.RootPath
|
||||
sV7Header.GlobalBoolFlags = sV6Header.GlobalBoolFlags
|
||||
sV7Header.GlobalIntFlags = sV6Header.GlobalIntFlags
|
||||
sV7Header.GlobalStringFlags = sV6Header.GlobalStringFlags
|
||||
sV7Header.CommandType = sV6Header.CommandType
|
||||
sV7Header.CommandArgs = sV6Header.CommandArgs
|
||||
sV7Header.CommandBoolFlags = sV6Header.CommandBoolFlags
|
||||
sV7Header.CommandIntFlags = sV6Header.CommandIntFlags
|
||||
sV7Header.CommandStringFlags = sV6Header.CommandStringFlags
|
||||
sV7Header.LastCopied = sV6Header.LastCopied
|
||||
sV7Header.LastRemoved = ""
|
||||
sV7Header.TotalBytes = sV6Header.TotalBytes
|
||||
sV7Header.TotalObjects = sV6Header.TotalObjects
|
||||
|
||||
qs, e := quick.NewConfig(sV7Header, nil)
|
||||
fatalIf(probe.NewError(e).Trace(sid), "Unable to initialize quick config for session '7' header.")
|
||||
|
||||
e = qs.Save(sessionFile)
|
||||
fatalIf(probe.NewError(e).Trace(sid, sessionFile), "Unable to migrate session from '6' to '7'.")
|
||||
|
||||
console.Println("Successfully migrated `" + sessionFile + "` from version `" + sV6Header.Version + "` to " + "`" + sV7Header.Version + "`.")
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate session version '5' to version '6', all older sessions are
|
||||
// in-fact removed and not migrated. All session files from '6' and
|
||||
// above should be migrated - See: migrateSessionV6ToV7().
|
||||
func migrateSessionV5ToV6() {
|
||||
for _, sid := range getSessionIDs() {
|
||||
sV6Header, err := loadSessionV6Header(sid)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err.ToGoError()) {
|
||||
continue
|
||||
}
|
||||
fatalIf(err.Trace(sid), "Unable to load version `6`. Migration failed please report this issue at https://github.com/minio/mc/issues.")
|
||||
}
|
||||
|
||||
sessionVersion, e := strconv.Atoi(sV6Header.Version)
|
||||
fatalIf(probe.NewError(e), "Unable to load version `6`. Migration failed please report this issue at https://github.com/minio/mc/issues.")
|
||||
if sessionVersion > 5 { // It is new format.
|
||||
continue
|
||||
}
|
||||
|
||||
/*** Remove all session files older than v6 ***/
|
||||
|
||||
sessionFile, err := getSessionFile(sid)
|
||||
fatalIf(err.Trace(sid), "Unable to get session file.")
|
||||
|
||||
sessionDataFile, err := getSessionDataFile(sid)
|
||||
fatalIf(err.Trace(sid), "Unable to get session data file.")
|
||||
|
||||
console.Println("Removing unsupported session file `" + sessionFile + "` version `" + sV6Header.Version + "`.")
|
||||
if e := os.Remove(sessionFile); e != nil {
|
||||
fatalIf(probe.NewError(e), "Unable to remove version `"+sV6Header.Version+"` session file `"+sessionFile+"`.")
|
||||
}
|
||||
if e := os.Remove(sessionDataFile); e != nil {
|
||||
fatalIf(probe.NewError(e), "Unable to remove version `"+sV6Header.Version+"` session data file `"+sessionDataFile+"`.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/minio/pkg/v2/quick"
|
||||
)
|
||||
|
||||
// ///////////////// Session V6 ///////////////////
|
||||
// sessionV6Header for resumable sessions.
|
||||
type sessionV6Header struct {
|
||||
Version string `json:"version"`
|
||||
When time.Time `json:"time"`
|
||||
RootPath string `json:"workingFolder"`
|
||||
GlobalBoolFlags map[string]bool `json:"globalBoolFlags"`
|
||||
GlobalIntFlags map[string]int `json:"globalIntFlags"`
|
||||
GlobalStringFlags map[string]string `json:"globalStringFlags"`
|
||||
CommandType string `json:"commandType"`
|
||||
CommandArgs []string `json:"cmdArgs"`
|
||||
CommandBoolFlags map[string]bool `json:"cmdBoolFlags"`
|
||||
CommandIntFlags map[string]int `json:"cmdIntFlags"`
|
||||
CommandStringFlags map[string]string `json:"cmdStringFlags"`
|
||||
LastCopied string `json:"lastCopied"`
|
||||
TotalBytes int64 `json:"totalBytes"`
|
||||
TotalObjects int `json:"totalObjects"`
|
||||
}
|
||||
|
||||
func loadSessionV6Header(sid string) (*sessionV6Header, *probe.Error) {
|
||||
if !isSessionDirExists() {
|
||||
return nil, errInvalidArgument().Trace()
|
||||
}
|
||||
|
||||
sessionFile, err := getSessionFile(sid)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sid)
|
||||
}
|
||||
|
||||
if _, e := os.Stat(sessionFile); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
|
||||
sV6Header := &sessionV6Header{}
|
||||
sV6Header.Version = "6"
|
||||
qs, e := quick.NewConfig(sV6Header, nil)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e).Trace(sid, sV6Header.Version)
|
||||
}
|
||||
e = qs.Load(sessionFile)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e).Trace(sid, sV6Header.Version)
|
||||
}
|
||||
|
||||
sV6Header = qs.Data().(*sessionV6Header)
|
||||
return sV6Header, nil
|
||||
}
|
||||
|
||||
/////////////////// Session V7 ///////////////////
|
||||
// RESERVED FOR FUTURE
|
||||
|
||||
// sessionV7Header for resumable sessions.
|
||||
type sessionV7Header struct {
|
||||
Version string `json:"version"`
|
||||
When time.Time `json:"time"`
|
||||
RootPath string `json:"workingFolder"`
|
||||
GlobalBoolFlags map[string]bool `json:"globalBoolFlags"`
|
||||
GlobalIntFlags map[string]int `json:"globalIntFlags"`
|
||||
GlobalStringFlags map[string]string `json:"globalStringFlags"`
|
||||
CommandType string `json:"commandType"`
|
||||
CommandArgs []string `json:"cmdArgs"`
|
||||
CommandBoolFlags map[string]bool `json:"cmdBoolFlags"`
|
||||
CommandIntFlags map[string]int `json:"cmdIntFlags"`
|
||||
CommandStringFlags map[string]string `json:"cmdStringFlags"`
|
||||
LastCopied string `json:"lastCopied"`
|
||||
LastRemoved string `json:"lastRemoved"`
|
||||
TotalBytes int64 `json:"totalBytes"`
|
||||
TotalObjects int `json:"totalObjects"`
|
||||
}
|
||||
|
||||
// sessionV7 resumable session container.
|
||||
type sessionV7 struct {
|
||||
Header *sessionV7Header
|
||||
SessionID string
|
||||
mutex *sync.Mutex
|
||||
DataFP *sessionDataFP
|
||||
}
|
||||
|
||||
// loadSessionV7 - reads session file if exists and re-initiates internal variables
|
||||
func loadSessionV7(sid string) (*sessionV7, *probe.Error) {
|
||||
if !isSessionDirExists() {
|
||||
return nil, errInvalidArgument().Trace()
|
||||
}
|
||||
sessionFile, err := getSessionFile(sid)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sid)
|
||||
}
|
||||
|
||||
if _, e := os.Stat(sessionFile); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
|
||||
s := &sessionV7{}
|
||||
s.Header = &sessionV7Header{}
|
||||
s.SessionID = sid
|
||||
s.Header.Version = "7"
|
||||
qs, e := quick.NewConfig(s.Header, nil)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e).Trace(sid, s.Header.Version)
|
||||
}
|
||||
e = qs.Load(sessionFile)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e).Trace(sid, s.Header.Version)
|
||||
}
|
||||
|
||||
s.mutex = new(sync.Mutex)
|
||||
s.Header = qs.Data().(*sessionV7Header)
|
||||
|
||||
sessionDataFile, err := getSessionDataFile(s.SessionID)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sid, s.Header.Version)
|
||||
}
|
||||
|
||||
dataFile, e := os.Open(sessionDataFile)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
s.DataFP = &sessionDataFP{false, dataFile}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -1,400 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package cmd - session V8 - Version 8 stores session header and session data in
|
||||
// two separate files. Session data contains fully prepared URL list.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
json "github.com/minio/colorjson"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
"github.com/minio/pkg/v2/console"
|
||||
"github.com/minio/pkg/v2/quick"
|
||||
)
|
||||
|
||||
// sessionV8Header for resumable sessions.
|
||||
type sessionV8Header struct {
|
||||
Version string `json:"version"`
|
||||
When time.Time `json:"time"`
|
||||
RootPath string `json:"workingFolder"`
|
||||
GlobalBoolFlags map[string]bool `json:"globalBoolFlags"`
|
||||
GlobalIntFlags map[string]int `json:"globalIntFlags"`
|
||||
GlobalStringFlags map[string]string `json:"globalStringFlags"`
|
||||
CommandType string `json:"commandType"`
|
||||
CommandArgs []string `json:"cmdArgs"`
|
||||
CommandBoolFlags map[string]bool `json:"cmdBoolFlags"`
|
||||
CommandIntFlags map[string]int `json:"cmdIntFlags"`
|
||||
CommandStringFlags map[string]string `json:"cmdStringFlags"`
|
||||
LastCopied string `json:"lastCopied"`
|
||||
LastRemoved string `json:"lastRemoved"`
|
||||
TotalBytes int64 `json:"totalBytes"`
|
||||
TotalObjects int64 `json:"totalObjects"`
|
||||
UserMetaData map[string]string `json:"metaData"`
|
||||
}
|
||||
|
||||
// sessionMessage container for session messages
|
||||
type sessionMessage struct {
|
||||
Status string `json:"status"`
|
||||
SessionID string `json:"sessionId"`
|
||||
Time time.Time `json:"time"`
|
||||
CommandType string `json:"commandType"`
|
||||
CommandArgs []string `json:"commandArgs"`
|
||||
}
|
||||
|
||||
// sessionV8 resumable session container.
|
||||
type sessionV8 struct {
|
||||
Header *sessionV8Header
|
||||
SessionID string
|
||||
mutex *sync.Mutex
|
||||
DataFP *sessionDataFP
|
||||
}
|
||||
|
||||
// sessionDataFP data file pointer.
|
||||
type sessionDataFP struct {
|
||||
dirty bool
|
||||
*os.File
|
||||
}
|
||||
|
||||
func (file *sessionDataFP) Write(p []byte) (int, error) {
|
||||
file.dirty = true
|
||||
return file.File.Write(p)
|
||||
}
|
||||
|
||||
// String colorized session message.
|
||||
func (s sessionV8) String() string {
|
||||
message := console.Colorize("SessionID", fmt.Sprintf("%s -> ", s.SessionID))
|
||||
message = message + console.Colorize("SessionTime", fmt.Sprintf("[%s]", s.Header.When.Local().Format(printDate)))
|
||||
message = message + console.Colorize("Command", fmt.Sprintf(" %s %s", s.Header.CommandType, strings.Join(s.Header.CommandArgs, " ")))
|
||||
return message
|
||||
}
|
||||
|
||||
// JSON jsonified session message.
|
||||
func (s sessionV8) JSON() string {
|
||||
sessionMsg := sessionMessage{
|
||||
SessionID: s.SessionID,
|
||||
Time: s.Header.When.Local(),
|
||||
CommandType: s.Header.CommandType,
|
||||
CommandArgs: s.Header.CommandArgs,
|
||||
}
|
||||
sessionMsg.Status = "success"
|
||||
sessionBytes, e := json.MarshalIndent(sessionMsg, "", " ")
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(sessionBytes)
|
||||
}
|
||||
|
||||
// loadSessionV8 - reads session file if exists and re-initiates internal variables
|
||||
func loadSessionV8(sid string) (*sessionV8, *probe.Error) {
|
||||
if !isSessionDirExists() {
|
||||
return nil, errInvalidArgument().Trace()
|
||||
}
|
||||
sessionFile, err := getSessionFile(sid)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sid)
|
||||
}
|
||||
|
||||
if _, e := os.Stat(sessionFile); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
|
||||
// Initialize new session.
|
||||
s := &sessionV8{
|
||||
Header: &sessionV8Header{
|
||||
Version: globalSessionConfigVersion,
|
||||
},
|
||||
SessionID: sid,
|
||||
}
|
||||
|
||||
// Initialize session config loader.
|
||||
qs, e := quick.NewConfig(s.Header, nil)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e).Trace(sid, s.Header.Version)
|
||||
}
|
||||
|
||||
if e = qs.Load(sessionFile); e != nil {
|
||||
return nil, probe.NewError(e).Trace(sid, s.Header.Version)
|
||||
}
|
||||
|
||||
// Validate if the version matches with expected current version.
|
||||
sV8Header := qs.Data().(*sessionV8Header)
|
||||
if sV8Header.Version != globalSessionConfigVersion {
|
||||
msg := fmt.Sprintf("Session header version %s does not match mc session version %s.\n",
|
||||
sV8Header.Version, globalSessionConfigVersion)
|
||||
return nil, probe.NewError(errors.New(msg)).Trace(sid, sV8Header.Version)
|
||||
}
|
||||
|
||||
s.mutex = new(sync.Mutex)
|
||||
s.Header = sV8Header
|
||||
|
||||
sessionDataFile, err := getSessionDataFile(s.SessionID)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sid, s.Header.Version)
|
||||
}
|
||||
|
||||
dataFile, e := os.Open(sessionDataFile)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
s.DataFP = &sessionDataFP{false, dataFile}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// newSessionV8 provides a new session.
|
||||
func newSessionV8(sessionID string) *sessionV8 {
|
||||
s := &sessionV8{}
|
||||
s.Header = &sessionV8Header{}
|
||||
s.Header.Version = globalSessionConfigVersion
|
||||
// map of command and files copied.
|
||||
s.Header.GlobalBoolFlags = make(map[string]bool)
|
||||
s.Header.GlobalIntFlags = make(map[string]int)
|
||||
s.Header.GlobalStringFlags = make(map[string]string)
|
||||
s.Header.CommandArgs = nil
|
||||
s.Header.CommandBoolFlags = make(map[string]bool)
|
||||
s.Header.CommandIntFlags = make(map[string]int)
|
||||
s.Header.CommandStringFlags = make(map[string]string)
|
||||
s.Header.UserMetaData = make(map[string]string)
|
||||
s.Header.When = UTCNow()
|
||||
s.mutex = new(sync.Mutex)
|
||||
s.SessionID = sessionID
|
||||
|
||||
sessionDataFile, err := getSessionDataFile(s.SessionID)
|
||||
fatalIf(err.Trace(s.SessionID), "Unable to create session data file \""+sessionDataFile+"\".")
|
||||
|
||||
dataFile, e := os.Create(sessionDataFile)
|
||||
fatalIf(probe.NewError(e), "Unable to create session data file \""+sessionDataFile+"\".")
|
||||
|
||||
s.DataFP = &sessionDataFP{false, dataFile}
|
||||
|
||||
// Capture state of global flags.
|
||||
s.setGlobals()
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// HasData provides true if this is a session resume, false otherwise.
|
||||
func (s sessionV8) HasData() bool {
|
||||
return s.Header.LastCopied != "" || s.Header.LastRemoved != ""
|
||||
}
|
||||
|
||||
// NewDataReader provides reader interface to session data file.
|
||||
func (s *sessionV8) NewDataReader() io.Reader {
|
||||
// DataFP is always intitialized, either via new or load functions.
|
||||
s.DataFP.Seek(0, io.SeekStart)
|
||||
return io.Reader(s.DataFP)
|
||||
}
|
||||
|
||||
// NewDataReader provides writer interface to session data file.
|
||||
func (s *sessionV8) NewDataWriter() io.Writer {
|
||||
// DataFP is always intitialized, either via new or load functions.
|
||||
s.DataFP.Seek(0, io.SeekStart)
|
||||
// when moving to file position 0 we want to truncate the file as well,
|
||||
// otherwise we'll partly overwrite existing data
|
||||
s.DataFP.Truncate(0)
|
||||
return io.Writer(s.DataFP)
|
||||
}
|
||||
|
||||
// Save this session.
|
||||
func (s *sessionV8) Save() *probe.Error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if s.DataFP.dirty {
|
||||
if err := s.DataFP.Sync(); err != nil {
|
||||
return probe.NewError(err)
|
||||
}
|
||||
s.DataFP.dirty = false
|
||||
}
|
||||
|
||||
qs, e := quick.NewConfig(s.Header, nil)
|
||||
if e != nil {
|
||||
return probe.NewError(e).Trace(s.SessionID)
|
||||
}
|
||||
|
||||
sessionFile, err := getSessionFile(s.SessionID)
|
||||
if err != nil {
|
||||
return err.Trace(s.SessionID)
|
||||
}
|
||||
e = qs.Save(sessionFile)
|
||||
if e != nil {
|
||||
return probe.NewError(e).Trace(sessionFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// setGlobals captures the state of global variables into session header.
|
||||
// Used by newSession.
|
||||
func (s *sessionV8) setGlobals() {
|
||||
s.Header.GlobalBoolFlags["quiet"] = globalQuiet
|
||||
s.Header.GlobalBoolFlags["debug"] = globalDebug
|
||||
s.Header.GlobalBoolFlags["json"] = globalJSON
|
||||
s.Header.GlobalBoolFlags["noColor"] = globalNoColor
|
||||
s.Header.GlobalBoolFlags["insecure"] = globalInsecure
|
||||
}
|
||||
|
||||
// IsModified - returns if in memory session header has changed from
|
||||
// its on disk value.
|
||||
func (s *sessionV8) isModified(sessionFile string) (bool, *probe.Error) {
|
||||
qs, e := quick.NewConfig(s.Header, nil)
|
||||
if e != nil {
|
||||
return false, probe.NewError(e).Trace(s.SessionID)
|
||||
}
|
||||
|
||||
currentHeader := &sessionV8Header{}
|
||||
currentQS, e := quick.LoadConfig(sessionFile, nil, currentHeader)
|
||||
if e != nil {
|
||||
// If session does not exist for the first, return modified to
|
||||
// be true.
|
||||
if os.IsNotExist(e) {
|
||||
return true, nil
|
||||
}
|
||||
// For all other errors return.
|
||||
return false, probe.NewError(e).Trace(s.SessionID)
|
||||
}
|
||||
|
||||
changedFields, e := qs.DeepDiff(currentQS)
|
||||
if e != nil {
|
||||
return false, probe.NewError(e).Trace(s.SessionID)
|
||||
}
|
||||
|
||||
// Returns true if there are changed entries.
|
||||
return len(changedFields) > 0, nil
|
||||
}
|
||||
|
||||
// save - wrapper for quick.Save and saves only if sessionHeader is
|
||||
// modified.
|
||||
func (s *sessionV8) save() *probe.Error {
|
||||
sessionFile, err := getSessionFile(s.SessionID)
|
||||
if err != nil {
|
||||
return err.Trace(s.SessionID)
|
||||
}
|
||||
|
||||
// Verify if sessionFile is modified.
|
||||
modified, err := s.isModified(sessionFile)
|
||||
if err != nil {
|
||||
return err.Trace(s.SessionID)
|
||||
}
|
||||
// Header is modified, we save it.
|
||||
if modified {
|
||||
qs, e := quick.NewConfig(s.Header, nil)
|
||||
if e != nil {
|
||||
return probe.NewError(e).Trace(s.SessionID)
|
||||
}
|
||||
// Save an return.
|
||||
e = qs.Save(sessionFile)
|
||||
if e != nil {
|
||||
return probe.NewError(e).Trace(sessionFile)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close ends this session and removes all associated session files.
|
||||
func (s *sessionV8) Close() *probe.Error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if err := s.DataFP.Close(); err != nil {
|
||||
return probe.NewError(err)
|
||||
}
|
||||
|
||||
// Attempt to save the header if modified.
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// Delete removes all the session files.
|
||||
func (s *sessionV8) Delete() *probe.Error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
if s.DataFP != nil {
|
||||
name := s.DataFP.Name()
|
||||
// close file pro-actively before deleting
|
||||
// ignore any error, it could be possibly that
|
||||
// the file is closed already
|
||||
s.DataFP.Close()
|
||||
|
||||
// Remove the data file.
|
||||
if e := os.Remove(name); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch the session file.
|
||||
sessionFile, err := getSessionFile(s.SessionID)
|
||||
if err != nil {
|
||||
return err.Trace(s.SessionID)
|
||||
}
|
||||
|
||||
// Remove session file
|
||||
if e := os.Remove(sessionFile); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
|
||||
// Remove session backup file if any, ignore any error.
|
||||
os.Remove(sessionFile + ".old")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close a session and exit.
|
||||
func (s sessionV8) CloseAndDie() {
|
||||
s.Close()
|
||||
console.Fatalln("Session safely terminated. Run the same command to resume copy again.")
|
||||
}
|
||||
|
||||
func (s sessionV8) copyCloseAndDie(sessionFlag bool) {
|
||||
if sessionFlag {
|
||||
s.Close()
|
||||
console.Fatalln("Command terminated safely. Run this command to resume copy again.")
|
||||
} else {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.DataFP.Close() // ignore error.
|
||||
}
|
||||
}
|
||||
|
||||
// Create a factory function to simplify checking if
|
||||
// object was last operated on.
|
||||
func isLastFactory(lastURL string) func(string) bool {
|
||||
last := true // closure
|
||||
return func(sourceURL string) bool {
|
||||
if sourceURL == "" {
|
||||
fatalIf(errInvalidArgument().Trace(), "Empty source argument passed.")
|
||||
}
|
||||
if lastURL == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if last {
|
||||
if lastURL == sourceURL {
|
||||
last = false // from next call onwards we say false.
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
134
cmd/session.go
134
cmd/session.go
@@ -1,134 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
)
|
||||
|
||||
// migrateSession migrates all previous migration to latest.
|
||||
func migrateSession() {
|
||||
// We no longer support sessions older than v5. They will be removed.
|
||||
migrateSessionV5ToV6()
|
||||
|
||||
// Migrate V6 to V7.
|
||||
migrateSessionV6ToV7()
|
||||
|
||||
// Migrate V7 to V8
|
||||
migrateSessionV7ToV8()
|
||||
}
|
||||
|
||||
// createSessionDir - create session directory.
|
||||
func createSessionDir() *probe.Error {
|
||||
sessionDir, err := getSessionDir()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
|
||||
if e := os.MkdirAll(sessionDir, 0o700); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSessionDir - get session directory.
|
||||
func getSessionDir() (string, *probe.Error) {
|
||||
configDir, err := getMcConfigDir()
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
}
|
||||
|
||||
sessionDir := filepath.Join(configDir, globalSessionDir)
|
||||
return sessionDir, nil
|
||||
}
|
||||
|
||||
// isSessionDirExists - verify if session directory exists.
|
||||
func isSessionDirExists() bool {
|
||||
sessionDir, err := getSessionDir()
|
||||
fatalIf(err.Trace(), "Unable to determine session folder.")
|
||||
|
||||
if _, e := os.Stat(sessionDir); e != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getSessionFile - get current session file.
|
||||
func getSessionFile(sid string) (string, *probe.Error) {
|
||||
sessionDir, err := getSessionDir()
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
}
|
||||
|
||||
sessionFile := filepath.Join(sessionDir, sid+".json")
|
||||
return sessionFile, nil
|
||||
}
|
||||
|
||||
// isSessionExists verifies if given session exists.
|
||||
func isSessionExists(sid string) bool {
|
||||
sessionFile, err := getSessionFile(sid)
|
||||
fatalIf(err.Trace(sid), "Unable to determine session filename for `"+sid+"`.")
|
||||
|
||||
if _, e := os.Stat(sessionFile); e != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true // Session exists.
|
||||
}
|
||||
|
||||
// getSessionDataFile - get session data file for a given session.
|
||||
func getSessionDataFile(sid string) (string, *probe.Error) {
|
||||
sessionDir, err := getSessionDir()
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
}
|
||||
|
||||
sessionDataFile := filepath.Join(sessionDir, sid+".data")
|
||||
return sessionDataFile, nil
|
||||
}
|
||||
|
||||
// getSessionIDs - get all active sessions.
|
||||
func getSessionIDs() (sids []string) {
|
||||
sessionDir, err := getSessionDir()
|
||||
fatalIf(err.Trace(), "Unable to access session folder.")
|
||||
|
||||
sessionList, e := filepath.Glob(sessionDir + "/*.json")
|
||||
fatalIf(probe.NewError(e), "Unable to access session folder `"+sessionDir+"`.")
|
||||
|
||||
for _, path := range sessionList {
|
||||
sids = append(sids, strings.TrimSuffix(filepath.Base(path), ".json"))
|
||||
}
|
||||
return sids
|
||||
}
|
||||
|
||||
func getHash(prefix string, args []string) string {
|
||||
hasher := sha256.New()
|
||||
for _, arg := range args {
|
||||
if _, err := hasher.Write([]byte(arg)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
return prefix + "-" + hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
checkv1 "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
// newRandomID generates a random id of regular lower case and uppercase english characters.
|
||||
func newRandomID(n int) string {
|
||||
sid := make([]rune, n)
|
||||
for i := range sid {
|
||||
sid[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(sid)
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestValidSessionID(c *checkv1.C) {
|
||||
validSid := regexp.MustCompile("^[a-zA-Z]+$")
|
||||
sid := newRandomID(8)
|
||||
c.Assert(len(sid), checkv1.Equals, 8)
|
||||
c.Assert(validSid.MatchString(sid), checkv1.Equals, true)
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestSession(c *checkv1.C) {
|
||||
err := createSessionDir()
|
||||
c.Assert(err, checkv1.IsNil)
|
||||
c.Assert(isSessionDirExists(), checkv1.Equals, true)
|
||||
|
||||
session := newSessionV8(getHash("cp", []string{"mybucket", "myminio/mybucket"}))
|
||||
c.Assert(session.Header.CommandArgs, checkv1.IsNil)
|
||||
c.Assert(len(session.SessionID) >= 8, checkv1.Equals, true)
|
||||
_, e := os.Stat(session.DataFP.Name())
|
||||
c.Assert(e, checkv1.IsNil)
|
||||
|
||||
err = session.Close()
|
||||
c.Assert(err, checkv1.IsNil)
|
||||
c.Assert(isSessionExists(session.SessionID), checkv1.Equals, true)
|
||||
|
||||
savedSession, err := loadSessionV8(session.SessionID)
|
||||
c.Assert(err, checkv1.IsNil)
|
||||
c.Assert(session.SessionID, checkv1.Equals, savedSession.SessionID)
|
||||
|
||||
err = savedSession.Close()
|
||||
c.Assert(err, checkv1.IsNil)
|
||||
|
||||
err = savedSession.Delete()
|
||||
c.Assert(err, checkv1.IsNil)
|
||||
c.Assert(isSessionExists(session.SessionID), checkv1.Equals, false)
|
||||
_, e = os.Stat(session.DataFP.Name())
|
||||
c.Assert(e, checkv1.NotNil)
|
||||
}
|
||||
@@ -187,7 +187,7 @@ func doShareDownloadURL(ctx context.Context, targetURL, versionID string, isRecu
|
||||
// Make new entries to shareDB.
|
||||
contentType := "" // Not useful for download shares.
|
||||
shareDB.Set(objectURL, shareURL, expiry, contentType)
|
||||
printMsg(shareMesssage{
|
||||
printMsg(shareMessage{
|
||||
ObjectURL: objectURL,
|
||||
ShareURL: shareURL,
|
||||
TimeLeft: expiry,
|
||||
@@ -205,7 +205,7 @@ func mainShareDownload(cliCtx *cli.Context) error {
|
||||
defer cancelShareDownload()
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// check input arguments.
|
||||
|
||||
@@ -92,7 +92,7 @@ func doShareList(cmd string) *probe.Error {
|
||||
|
||||
// Print previously shared entries.
|
||||
for shareURL, share := range shareDB.Shares {
|
||||
printMsg(shareMesssage{
|
||||
printMsg(shareMessage{
|
||||
ObjectURL: share.URL,
|
||||
ShareURL: shareURL,
|
||||
TimeLeft: share.Expiry - time.Since(share.Date),
|
||||
|
||||
@@ -171,7 +171,7 @@ func doShareUploadURL(ctx context.Context, objectURL string, isRecursive bool, e
|
||||
return err.Trace(objectURL)
|
||||
}
|
||||
|
||||
printMsg(shareMesssage{
|
||||
printMsg(shareMessage{
|
||||
ObjectURL: objectURL,
|
||||
ShareURL: curlCmd,
|
||||
TimeLeft: expiry,
|
||||
|
||||
@@ -51,7 +51,7 @@ var (
|
||||
)
|
||||
|
||||
// Structured share command message.
|
||||
type shareMesssage struct {
|
||||
type shareMessage struct {
|
||||
Status string `json:"status"`
|
||||
ObjectURL string `json:"url"`
|
||||
ShareURL string `json:"share"`
|
||||
@@ -60,7 +60,7 @@ type shareMesssage struct {
|
||||
}
|
||||
|
||||
// String - Themefied string message for console printing.
|
||||
func (s shareMesssage) String() string {
|
||||
func (s shareMessage) String() string {
|
||||
msg := console.Colorize("URL", fmt.Sprintf("URL: %s\n", s.ObjectURL))
|
||||
msg += console.Colorize("Expire", fmt.Sprintf("Expire: %s\n", timeDurationToHumanizedDuration(s.TimeLeft)))
|
||||
if s.ContentType != "" {
|
||||
@@ -78,7 +78,7 @@ func (s shareMesssage) String() string {
|
||||
}
|
||||
|
||||
// JSON - JSONified message for scripting.
|
||||
func (s shareMesssage) JSON() string {
|
||||
func (s shareMessage) JSON() string {
|
||||
s.Status = "success"
|
||||
shareMessageBytes, e := json.MarshalIndent(s, "", " ")
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
@@ -80,7 +80,7 @@ var sqlCmd = cli.Command{
|
||||
Action: mainSQL,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(sqlFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(sqlFlags, encCFlag), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -90,8 +90,6 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
|
||||
SERIALIZATION OPTIONS:
|
||||
For query serialization options, refer to https://min.io/docs/minio/linux/reference/minio-mc/mc-sql.html#command-mc.sql
|
||||
@@ -104,7 +102,7 @@ EXAMPLES:
|
||||
{{.Prompt}} {{.HelpName}} --query "select count(s.power) from S3Object s" myminio/iot-devices/power-ratio.csv
|
||||
|
||||
3. Run a query on an encrypted object with customer provided keys.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "myminio/iot-devices=32byteslongsecretkeymustbegiven1" \
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "myminio/iot-devices=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" \
|
||||
--query "select count(s.power) from S3Object s" myminio/iot-devices/power-ratio-encrypted.csv
|
||||
|
||||
4. Run a query on an object on MinIO in gzip format using ; as field delimiter,
|
||||
@@ -445,7 +443,7 @@ func mainSQL(cliCtx *cli.Context) error {
|
||||
query string
|
||||
)
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// validate sql input arguments.
|
||||
|
||||
@@ -56,7 +56,7 @@ var statCmd = cli.Command{
|
||||
Action: mainStat,
|
||||
OnUsageError: onUsageError,
|
||||
Before: setGlobalsFromContext,
|
||||
Flags: append(append(statFlags, ioFlags...), globalFlags...),
|
||||
Flags: append(append(statFlags, encCFlag), globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.HelpName}} - {{.Usage}}
|
||||
|
||||
@@ -66,8 +66,6 @@ USAGE:
|
||||
FLAGS:
|
||||
{{range .VisibleFlags}}{{.}}
|
||||
{{end}}
|
||||
ENVIRONMENT VARIABLES:
|
||||
MC_ENCRYPT_KEY: list of comma delimited prefix=secret values
|
||||
|
||||
EXAMPLES:
|
||||
1. Stat all contents of mybucket on Amazon S3 cloud storage.
|
||||
@@ -79,17 +77,14 @@ EXAMPLES:
|
||||
3. Stat files recursively on a local filesystem on Microsoft Windows.
|
||||
{{.Prompt}} {{.HelpName}} --recursive C:\Users\mydocuments\
|
||||
|
||||
4. Stat encrypted files on Amazon S3 cloud storage.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "s3/personal-docs/=32byteslongsecretkeymustbegiven1" s3/personal-docs/2018-account_report.docx
|
||||
|
||||
5. Stat encrypted files on Amazon S3 cloud storage. In case the encryption key contains non-printable character like tab, pass the
|
||||
4. Stat encrypted files on Amazon S3 cloud storage. In case the encryption key contains non-printable character like tab, pass the
|
||||
base64 encoded string as key.
|
||||
{{.Prompt}} {{.HelpName}} --encrypt-key "s3/personal-document/=MzJieXRlc2xvbmdzZWNyZWFiY2RlZmcJZ2l2ZW5uMjE=" s3/personal-document/2019-account_report.docx
|
||||
{{.Prompt}} {{.HelpName}} --enc-c "s3/personal-document/=MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDA" s3/personal-document/2019-account_report.docx
|
||||
|
||||
6. Stat a specific object version.
|
||||
5. Stat a specific object version.
|
||||
{{.Prompt}} {{.HelpName}} --version-id "CL3sWgdSN2pNntSf6UnZAuh2kcu8E8si" s3/personal-docs/2018-account_report.docx
|
||||
|
||||
7. Stat all objects versions recursively created before 1st January 2020.
|
||||
6. Stat all objects versions recursively created before 1st January 2020.
|
||||
{{.Prompt}} {{.HelpName}} --versions --rewind 2020.01.01T00:00 s3/personal-docs/
|
||||
`,
|
||||
}
|
||||
@@ -154,7 +149,7 @@ func mainStat(cliCtx *cli.Context) error {
|
||||
console.SetColor("Count", color.New(color.FgGreen))
|
||||
|
||||
// Parse encryption keys per command.
|
||||
encKeyDB, err := getEncKeys(cliCtx)
|
||||
encKeyDB, err := validateAndCreateEncryptionKeys(cliCtx)
|
||||
fatalIf(err, "Unable to parse encryption keys.")
|
||||
|
||||
// check 'stat' cli arguments.
|
||||
|
||||
2887
cmd/suite_test.go
Normal file
2887
cmd/suite_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -148,9 +148,46 @@ var errSourceIsDir = func(URL string) *probe.Error {
|
||||
return probe.NewError(sourceIsDirErr(errors.New(msg))).Untrace()
|
||||
}
|
||||
|
||||
type conflictSSEErr error
|
||||
type sseInvalidAliasErr error
|
||||
|
||||
var errConflictSSE = func(sseServer, sseKeys string) *probe.Error {
|
||||
err := fmt.Errorf("SSE alias '%s' overlaps with SSE-C aliases '%s'", sseServer, sseKeys)
|
||||
return probe.NewError(conflictSSEErr(err)).Untrace()
|
||||
var errSSEInvalidAlias = func(prefix string) *probe.Error {
|
||||
msg := "SSE prefix " + prefix + " has an invalid alias."
|
||||
return probe.NewError(sseInvalidAliasErr(errors.New(msg))).Untrace()
|
||||
}
|
||||
|
||||
type sseOverlappingAliasErr error
|
||||
|
||||
var errSSEOverlappingAlias = func(prefix, overlappingPrefix string) *probe.Error {
|
||||
msg := "SSE prefix " + prefix + " overlaps with " + overlappingPrefix
|
||||
return probe.NewError(sseOverlappingAliasErr(errors.New(msg))).Untrace()
|
||||
}
|
||||
|
||||
type ssePrefixMatchErr error
|
||||
|
||||
var errSSEPrefixMatch = func() *probe.Error {
|
||||
msg := "SSE prefixes do not match any object paths."
|
||||
return probe.NewError(ssePrefixMatchErr(errors.New(msg))).Untrace()
|
||||
}
|
||||
|
||||
type sseKeyMissingError error
|
||||
|
||||
var errSSEKeyMissing = func() *probe.Error {
|
||||
m := "SSE key is missing"
|
||||
return probe.NewError(sseKeyMissingError(errors.New(m))).Untrace()
|
||||
}
|
||||
|
||||
type sseKMSKeyFormatErr error
|
||||
|
||||
var errSSEKMSKeyFormat = func(msg string) *probe.Error {
|
||||
m := "SSE key format error. "
|
||||
m += msg
|
||||
return probe.NewError(sseKMSKeyFormatErr(errors.New(m))).Untrace()
|
||||
}
|
||||
|
||||
type sseClientKeyFormatErr error
|
||||
|
||||
var errSSEClientKeyFormat = func(msg string) *probe.Error {
|
||||
m := "Encryption key should be 44 bytes raw base64 encoded key."
|
||||
m += msg
|
||||
return probe.NewError(sseClientKeyFormatErr(errors.New(m))).Untrace()
|
||||
}
|
||||
|
||||
104
cmd/utils.go
104
cmd/utils.go
@@ -29,7 +29,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -37,7 +36,6 @@ import (
|
||||
"github.com/mattn/go-ieproxy"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||
|
||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/minio/mc/pkg/probe"
|
||||
@@ -213,108 +211,6 @@ func getLookupType(l string) minio.BucketLookupType {
|
||||
return minio.BucketLookupAuto
|
||||
}
|
||||
|
||||
// struct representing object prefix and sse keys association.
|
||||
type prefixSSEPair struct {
|
||||
Prefix string
|
||||
SSE encrypt.ServerSide
|
||||
}
|
||||
|
||||
// parse and validate encryption keys entered on command line
|
||||
func parseAndValidateEncryptionKeys(sseKeys, sse string) (encMap map[string][]prefixSSEPair, err *probe.Error) {
|
||||
encMap, err = parseEncryptionKeys(sseKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sse != "" {
|
||||
for _, prefix := range strings.Split(sse, ",") {
|
||||
alias, _ := url2Alias(prefix)
|
||||
encMap[alias] = append(encMap[alias], prefixSSEPair{
|
||||
Prefix: prefix,
|
||||
SSE: encrypt.NewSSE(),
|
||||
})
|
||||
}
|
||||
}
|
||||
for alias, ps := range encMap {
|
||||
if hostCfg := mustGetHostConfig(alias); hostCfg == nil {
|
||||
for _, p := range ps {
|
||||
return nil, probe.NewError(errors.New("SSE prefix " + p.Prefix + " has invalid alias"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return encMap, nil
|
||||
}
|
||||
|
||||
// parse list of comma separated alias/prefix=sse key values entered on command line and
|
||||
// construct a map of alias to prefix and sse pairs.
|
||||
func parseEncryptionKeys(sseKeys string) (encMap map[string][]prefixSSEPair, err *probe.Error) {
|
||||
encMap = make(map[string][]prefixSSEPair)
|
||||
if sseKeys == "" {
|
||||
return
|
||||
}
|
||||
prefix := ""
|
||||
index := 0 // start index of prefix
|
||||
vs := 0 // start index of sse-c key
|
||||
sseKeyLen := 32
|
||||
delim := 1
|
||||
k := len(sseKeys)
|
||||
for index < k {
|
||||
i := strings.Index(sseKeys[index:], "=")
|
||||
if i == -1 {
|
||||
return nil, probe.NewError(errors.New("SSE-C prefix should be of the form prefix1=key1,... "))
|
||||
}
|
||||
prefix = sseKeys[index : index+i]
|
||||
alias, _ := url2Alias(prefix)
|
||||
vs = i + 1 + index
|
||||
if vs+32 > k {
|
||||
return nil, probe.NewError(errors.New("SSE-C key should be 32 bytes long"))
|
||||
}
|
||||
if (vs+sseKeyLen < k) && sseKeys[vs+sseKeyLen] != ',' {
|
||||
return nil, probe.NewError(errors.New("SSE-C prefix=secret should be delimited by , and secret should be 32 bytes long"))
|
||||
}
|
||||
sseKey := sseKeys[vs : vs+sseKeyLen]
|
||||
if _, ok := encMap[alias]; !ok {
|
||||
encMap[alias] = make([]prefixSSEPair, 0)
|
||||
}
|
||||
sse, e := encrypt.NewSSEC([]byte(sseKey))
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
encMap[alias] = append(encMap[alias], prefixSSEPair{
|
||||
Prefix: prefix,
|
||||
SSE: sse,
|
||||
})
|
||||
// advance index sseKeyLen + delim bytes for the next key start
|
||||
index = vs + sseKeyLen + delim
|
||||
}
|
||||
|
||||
// Sort encryption keys in descending order of prefix length
|
||||
for _, encKeys := range encMap {
|
||||
sort.Sort(byPrefixLength(encKeys))
|
||||
}
|
||||
|
||||
// Success.
|
||||
return encMap, nil
|
||||
}
|
||||
|
||||
// byPrefixLength implements sort.Interface.
|
||||
type byPrefixLength []prefixSSEPair
|
||||
|
||||
func (p byPrefixLength) Len() int { return len(p) }
|
||||
func (p byPrefixLength) Less(i, j int) bool {
|
||||
return len(p[i].Prefix) > len(p[j].Prefix)
|
||||
}
|
||||
func (p byPrefixLength) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
// get SSE Key if object prefix matches with given resource.
|
||||
func getSSE(resource string, encKeys []prefixSSEPair) encrypt.ServerSide {
|
||||
for _, k := range encKeys {
|
||||
if strings.HasPrefix(resource, k.Prefix) {
|
||||
return k.SSE
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return true if target url is a part of a source url such as:
|
||||
// alias/bucket/ and alias/bucket/dir/, however
|
||||
func isURLContains(srcURL, tgtURL, sep string) bool {
|
||||
|
||||
@@ -20,87 +20,8 @@ package cmd
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/minio-go/v7/pkg/encrypt"
|
||||
)
|
||||
|
||||
func TestParseEncryptionKeys(t *testing.T) {
|
||||
sseKey1, err := encrypt.NewSSEC([]byte("32byteslongsecretkeymustbegiven2"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sseKey2, err := encrypt.NewSSEC([]byte("32byteslongsecretkeymustbegiven1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sseSpaceKey1, err := encrypt.NewSSEC([]byte("32byteslongsecret mustbegiven1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sseCommaKey1, err := encrypt.NewSSEC([]byte("32byteslongsecretkey,ustbegiven1"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
testCases := []struct {
|
||||
encryptionKey string
|
||||
expectedEncMap map[string][]prefixSSEPair
|
||||
success bool
|
||||
}{
|
||||
{
|
||||
encryptionKey: "myminio1/test2=32byteslongsecretkeymustbegiven2",
|
||||
expectedEncMap: map[string][]prefixSSEPair{"myminio1": {{
|
||||
Prefix: "myminio1/test2",
|
||||
SSE: sseKey1,
|
||||
}}},
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
encryptionKey: "myminio1/test2=32byteslongsecretkeymustbegiven",
|
||||
expectedEncMap: nil,
|
||||
success: false,
|
||||
},
|
||||
{
|
||||
encryptionKey: "myminio1/test2=32byteslongsecretkey,ustbegiven1",
|
||||
expectedEncMap: map[string][]prefixSSEPair{"myminio1": {{
|
||||
Prefix: "myminio1/test2",
|
||||
SSE: sseCommaKey1,
|
||||
}}},
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
encryptionKey: "myminio1/test2=32byteslongsecret mustbegiven1",
|
||||
expectedEncMap: map[string][]prefixSSEPair{"myminio1": {{
|
||||
Prefix: "myminio1/test2",
|
||||
SSE: sseSpaceKey1,
|
||||
}}},
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
encryptionKey: "myminio1/test2=32byteslongsecretkeymustbegiven2,myminio1/test1/a=32byteslongsecretkeymustbegiven1",
|
||||
expectedEncMap: map[string][]prefixSSEPair{"myminio1": {{
|
||||
Prefix: "myminio1/test1/a",
|
||||
SSE: sseKey2,
|
||||
}, {
|
||||
Prefix: "myminio1/test2",
|
||||
SSE: sseKey1,
|
||||
}}},
|
||||
success: true,
|
||||
},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
encMap, err := parseEncryptionKeys(testCase.encryptionKey)
|
||||
if err != nil && testCase.success {
|
||||
t.Fatalf("Test %d: Expected success, got %s", i+1, err)
|
||||
}
|
||||
if err == nil && !testCase.success {
|
||||
t.Fatalf("Test %d: Expected error, got success", i+1)
|
||||
}
|
||||
if testCase.success && !reflect.DeepEqual(encMap, testCase.expectedEncMap) {
|
||||
t.Errorf("Test %d: Expected %s, got %s", i+1, testCase.expectedEncMap, encMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAttribute(t *testing.T) {
|
||||
metaDataCases := []struct {
|
||||
input string
|
||||
|
||||
395
docs/LICENSE
395
docs/LICENSE
@@ -1,395 +0,0 @@
|
||||
Attribution 4.0 International
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Corporation ("Creative Commons") is not a law firm and
|
||||
does not provide legal services or legal advice. Distribution of
|
||||
Creative Commons public licenses does not create a lawyer-client or
|
||||
other relationship. Creative Commons makes its licenses and related
|
||||
information available on an "as-is" basis. Creative Commons gives no
|
||||
warranties regarding its licenses, any material licensed under their
|
||||
terms and conditions, or any related information. Creative Commons
|
||||
disclaims all liability for damages resulting from their use to the
|
||||
fullest extent possible.
|
||||
|
||||
Using Creative Commons Public Licenses
|
||||
|
||||
Creative Commons public licenses provide a standard set of terms and
|
||||
conditions that creators and other rights holders may use to share
|
||||
original works of authorship and other material subject to copyright
|
||||
and certain other rights specified in the public license below. The
|
||||
following considerations are for informational purposes only, are not
|
||||
exhaustive, and do not form part of our licenses.
|
||||
|
||||
Considerations for licensors: Our public licenses are
|
||||
intended for use by those authorized to give the public
|
||||
permission to use material in ways otherwise restricted by
|
||||
copyright and certain other rights. Our licenses are
|
||||
irrevocable. Licensors should read and understand the terms
|
||||
and conditions of the license they choose before applying it.
|
||||
Licensors should also secure all rights necessary before
|
||||
applying our licenses so that the public can reuse the
|
||||
material as expected. Licensors should clearly mark any
|
||||
material not subject to the license. This includes other CC-
|
||||
licensed material, or material used under an exception or
|
||||
limitation to copyright. More considerations for licensors:
|
||||
wiki.creativecommons.org/Considerations_for_licensors
|
||||
|
||||
Considerations for the public: By using one of our public
|
||||
licenses, a licensor grants the public permission to use the
|
||||
licensed material under specified terms and conditions. If
|
||||
the licensor's permission is not necessary for any reason--for
|
||||
example, because of any applicable exception or limitation to
|
||||
copyright--then that use is not regulated by the license. Our
|
||||
licenses grant only permissions under copyright and certain
|
||||
other rights that a licensor has authority to grant. Use of
|
||||
the licensed material may still be restricted for other
|
||||
reasons, including because others have copyright or other
|
||||
rights in the material. A licensor may make special requests,
|
||||
such as asking that all changes be marked or described.
|
||||
Although not required by our licenses, you are encouraged to
|
||||
respect those requests where reasonable. More_considerations
|
||||
for the public:
|
||||
wiki.creativecommons.org/Considerations_for_licensees
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons Attribution 4.0 International Public License
|
||||
|
||||
By exercising the Licensed Rights (defined below), You accept and agree
|
||||
to be bound by the terms and conditions of this Creative Commons
|
||||
Attribution 4.0 International Public License ("Public License"). To the
|
||||
extent this Public License may be interpreted as a contract, You are
|
||||
granted the Licensed Rights in consideration of Your acceptance of
|
||||
these terms and conditions, and the Licensor grants You such rights in
|
||||
consideration of benefits the Licensor receives from making the
|
||||
Licensed Material available under these terms and conditions.
|
||||
|
||||
|
||||
Section 1 -- Definitions.
|
||||
|
||||
a. Adapted Material means material subject to Copyright and Similar
|
||||
Rights that is derived from or based upon the Licensed Material
|
||||
and in which the Licensed Material is translated, altered,
|
||||
arranged, transformed, or otherwise modified in a manner requiring
|
||||
permission under the Copyright and Similar Rights held by the
|
||||
Licensor. For purposes of this Public License, where the Licensed
|
||||
Material is a musical work, performance, or sound recording,
|
||||
Adapted Material is always produced where the Licensed Material is
|
||||
synched in timed relation with a moving image.
|
||||
|
||||
b. Adapter's License means the license You apply to Your Copyright
|
||||
and Similar Rights in Your contributions to Adapted Material in
|
||||
accordance with the terms and conditions of this Public License.
|
||||
|
||||
c. Copyright and Similar Rights means copyright and/or similar rights
|
||||
closely related to copyright including, without limitation,
|
||||
performance, broadcast, sound recording, and Sui Generis Database
|
||||
Rights, without regard to how the rights are labeled or
|
||||
categorized. For purposes of this Public License, the rights
|
||||
specified in Section 2(b)(1)-(2) are not Copyright and Similar
|
||||
Rights.
|
||||
|
||||
d. Effective Technological Measures means those measures that, in the
|
||||
absence of proper authority, may not be circumvented under laws
|
||||
fulfilling obligations under Article 11 of the WIPO Copyright
|
||||
Treaty adopted on December 20, 1996, and/or similar international
|
||||
agreements.
|
||||
|
||||
e. Exceptions and Limitations means fair use, fair dealing, and/or
|
||||
any other exception or limitation to Copyright and Similar Rights
|
||||
that applies to Your use of the Licensed Material.
|
||||
|
||||
f. Licensed Material means the artistic or literary work, database,
|
||||
or other material to which the Licensor applied this Public
|
||||
License.
|
||||
|
||||
g. Licensed Rights means the rights granted to You subject to the
|
||||
terms and conditions of this Public License, which are limited to
|
||||
all Copyright and Similar Rights that apply to Your use of the
|
||||
Licensed Material and that the Licensor has authority to license.
|
||||
|
||||
h. Licensor means the individual(s) or entity(ies) granting rights
|
||||
under this Public License.
|
||||
|
||||
i. Share means to provide material to the public by any means or
|
||||
process that requires permission under the Licensed Rights, such
|
||||
as reproduction, public display, public performance, distribution,
|
||||
dissemination, communication, or importation, and to make material
|
||||
available to the public including in ways that members of the
|
||||
public may access the material from a place and at a time
|
||||
individually chosen by them.
|
||||
|
||||
j. Sui Generis Database Rights means rights other than copyright
|
||||
resulting from Directive 96/9/EC of the European Parliament and of
|
||||
the Council of 11 March 1996 on the legal protection of databases,
|
||||
as amended and/or succeeded, as well as other essentially
|
||||
equivalent rights anywhere in the world.
|
||||
|
||||
k. You means the individual or entity exercising the Licensed Rights
|
||||
under this Public License. Your has a corresponding meaning.
|
||||
|
||||
|
||||
Section 2 -- Scope.
|
||||
|
||||
a. License grant.
|
||||
|
||||
1. Subject to the terms and conditions of this Public License,
|
||||
the Licensor hereby grants You a worldwide, royalty-free,
|
||||
non-sublicensable, non-exclusive, irrevocable license to
|
||||
exercise the Licensed Rights in the Licensed Material to:
|
||||
|
||||
a. reproduce and Share the Licensed Material, in whole or
|
||||
in part; and
|
||||
|
||||
b. produce, reproduce, and Share Adapted Material.
|
||||
|
||||
2. Exceptions and Limitations. For the avoidance of doubt, where
|
||||
Exceptions and Limitations apply to Your use, this Public
|
||||
License does not apply, and You do not need to comply with
|
||||
its terms and conditions.
|
||||
|
||||
3. Term. The term of this Public License is specified in Section
|
||||
6(a).
|
||||
|
||||
4. Media and formats; technical modifications allowed. The
|
||||
Licensor authorizes You to exercise the Licensed Rights in
|
||||
all media and formats whether now known or hereafter created,
|
||||
and to make technical modifications necessary to do so. The
|
||||
Licensor waives and/or agrees not to assert any right or
|
||||
authority to forbid You from making technical modifications
|
||||
necessary to exercise the Licensed Rights, including
|
||||
technical modifications necessary to circumvent Effective
|
||||
Technological Measures. For purposes of this Public License,
|
||||
simply making modifications authorized by this Section 2(a)
|
||||
(4) never produces Adapted Material.
|
||||
|
||||
5. Downstream recipients.
|
||||
|
||||
a. Offer from the Licensor -- Licensed Material. Every
|
||||
recipient of the Licensed Material automatically
|
||||
receives an offer from the Licensor to exercise the
|
||||
Licensed Rights under the terms and conditions of this
|
||||
Public License.
|
||||
|
||||
b. No downstream restrictions. You may not offer or impose
|
||||
any additional or different terms or conditions on, or
|
||||
apply any Effective Technological Measures to, the
|
||||
Licensed Material if doing so restricts exercise of the
|
||||
Licensed Rights by any recipient of the Licensed
|
||||
Material.
|
||||
|
||||
6. No endorsement. Nothing in this Public License constitutes or
|
||||
may be construed as permission to assert or imply that You
|
||||
are, or that Your use of the Licensed Material is, connected
|
||||
with, or sponsored, endorsed, or granted official status by,
|
||||
the Licensor or others designated to receive attribution as
|
||||
provided in Section 3(a)(1)(A)(i).
|
||||
|
||||
b. Other rights.
|
||||
|
||||
1. Moral rights, such as the right of integrity, are not
|
||||
licensed under this Public License, nor are publicity,
|
||||
privacy, and/or other similar personality rights; however, to
|
||||
the extent possible, the Licensor waives and/or agrees not to
|
||||
assert any such rights held by the Licensor to the limited
|
||||
extent necessary to allow You to exercise the Licensed
|
||||
Rights, but not otherwise.
|
||||
|
||||
2. Patent and trademark rights are not licensed under this
|
||||
Public License.
|
||||
|
||||
3. To the extent possible, the Licensor waives any right to
|
||||
collect royalties from You for the exercise of the Licensed
|
||||
Rights, whether directly or through a collecting society
|
||||
under any voluntary or waivable statutory or compulsory
|
||||
licensing scheme. In all other cases the Licensor expressly
|
||||
reserves any right to collect such royalties.
|
||||
|
||||
|
||||
Section 3 -- License Conditions.
|
||||
|
||||
Your exercise of the Licensed Rights is expressly made subject to the
|
||||
following conditions.
|
||||
|
||||
a. Attribution.
|
||||
|
||||
1. If You Share the Licensed Material (including in modified
|
||||
form), You must:
|
||||
|
||||
a. retain the following if it is supplied by the Licensor
|
||||
with the Licensed Material:
|
||||
|
||||
i. identification of the creator(s) of the Licensed
|
||||
Material and any others designated to receive
|
||||
attribution, in any reasonable manner requested by
|
||||
the Licensor (including by pseudonym if
|
||||
designated);
|
||||
|
||||
ii. a copyright notice;
|
||||
|
||||
iii. a notice that refers to this Public License;
|
||||
|
||||
iv. a notice that refers to the disclaimer of
|
||||
warranties;
|
||||
|
||||
v. a URI or hyperlink to the Licensed Material to the
|
||||
extent reasonably practicable;
|
||||
|
||||
b. indicate if You modified the Licensed Material and
|
||||
retain an indication of any previous modifications; and
|
||||
|
||||
c. indicate the Licensed Material is licensed under this
|
||||
Public License, and include the text of, or the URI or
|
||||
hyperlink to, this Public License.
|
||||
|
||||
2. You may satisfy the conditions in Section 3(a)(1) in any
|
||||
reasonable manner based on the medium, means, and context in
|
||||
which You Share the Licensed Material. For example, it may be
|
||||
reasonable to satisfy the conditions by providing a URI or
|
||||
hyperlink to a resource that includes the required
|
||||
information.
|
||||
|
||||
3. If requested by the Licensor, You must remove any of the
|
||||
information required by Section 3(a)(1)(A) to the extent
|
||||
reasonably practicable.
|
||||
|
||||
4. If You Share Adapted Material You produce, the Adapter's
|
||||
License You apply must not prevent recipients of the Adapted
|
||||
Material from complying with this Public License.
|
||||
|
||||
|
||||
Section 4 -- Sui Generis Database Rights.
|
||||
|
||||
Where the Licensed Rights include Sui Generis Database Rights that
|
||||
apply to Your use of the Licensed Material:
|
||||
|
||||
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
|
||||
to extract, reuse, reproduce, and Share all or a substantial
|
||||
portion of the contents of the database;
|
||||
|
||||
b. if You include all or a substantial portion of the database
|
||||
contents in a database in which You have Sui Generis Database
|
||||
Rights, then the database in which You have Sui Generis Database
|
||||
Rights (but not its individual contents) is Adapted Material; and
|
||||
|
||||
c. You must comply with the conditions in Section 3(a) if You Share
|
||||
all or a substantial portion of the contents of the database.
|
||||
|
||||
For the avoidance of doubt, this Section 4 supplements and does not
|
||||
replace Your obligations under this Public License where the Licensed
|
||||
Rights include other Copyright and Similar Rights.
|
||||
|
||||
|
||||
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
|
||||
|
||||
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
|
||||
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
|
||||
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
|
||||
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
|
||||
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
|
||||
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
|
||||
ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU.
|
||||
|
||||
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
|
||||
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
|
||||
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
|
||||
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
|
||||
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
|
||||
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
|
||||
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
|
||||
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR
|
||||
IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
c. The disclaimer of warranties and limitation of liability provided
|
||||
above shall be interpreted in a manner that, to the extent
|
||||
possible, most closely approximates an absolute disclaimer and
|
||||
waiver of all liability.
|
||||
|
||||
|
||||
Section 6 -- Term and Termination.
|
||||
|
||||
a. This Public License applies for the term of the Copyright and
|
||||
Similar Rights licensed here. However, if You fail to comply with
|
||||
this Public License, then Your rights under this Public License
|
||||
terminate automatically.
|
||||
|
||||
b. Where Your right to use the Licensed Material has terminated under
|
||||
Section 6(a), it reinstates:
|
||||
|
||||
1. automatically as of the date the violation is cured, provided
|
||||
it is cured within 30 days of Your discovery of the
|
||||
violation; or
|
||||
|
||||
2. upon express reinstatement by the Licensor.
|
||||
|
||||
For the avoidance of doubt, this Section 6(b) does not affect any
|
||||
right the Licensor may have to seek remedies for Your violations
|
||||
of this Public License.
|
||||
|
||||
c. For the avoidance of doubt, the Licensor may also offer the
|
||||
Licensed Material under separate terms or conditions or stop
|
||||
distributing the Licensed Material at any time; however, doing so
|
||||
will not terminate this Public License.
|
||||
|
||||
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
||||
License.
|
||||
|
||||
|
||||
Section 7 -- Other Terms and Conditions.
|
||||
|
||||
a. The Licensor shall not be bound by any additional or different
|
||||
terms or conditions communicated by You unless expressly agreed.
|
||||
|
||||
b. Any arrangements, understandings, or agreements regarding the
|
||||
Licensed Material not stated herein are separate from and
|
||||
independent of the terms and conditions of this Public License.
|
||||
|
||||
|
||||
Section 8 -- Interpretation.
|
||||
|
||||
a. For the avoidance of doubt, this Public License does not, and
|
||||
shall not be interpreted to, reduce, limit, restrict, or impose
|
||||
conditions on any use of the Licensed Material that could lawfully
|
||||
be made without permission under this Public License.
|
||||
|
||||
b. To the extent possible, if any provision of this Public License is
|
||||
deemed unenforceable, it shall be automatically reformed to the
|
||||
minimum extent necessary to make it enforceable. If the provision
|
||||
cannot be reformed, it shall be severed from this Public License
|
||||
without affecting the enforceability of the remaining terms and
|
||||
conditions.
|
||||
|
||||
c. No term or condition of this Public License will be waived and no
|
||||
failure to comply consented to unless expressly agreed to by the
|
||||
Licensor.
|
||||
|
||||
d. Nothing in this Public License constitutes or may be interpreted
|
||||
as a limitation upon, or waiver of, any privileges and immunities
|
||||
that apply to the Licensor or You, including from the legal
|
||||
processes of any jurisdiction or authority.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
Creative Commons is not a party to its public
|
||||
licenses. Notwithstanding, Creative Commons may elect to apply one of
|
||||
its public licenses to material it publishes and in those instances
|
||||
will be considered the “Licensor.” The text of the Creative Commons
|
||||
public licenses is dedicated to the public domain under the CC0 Public
|
||||
Domain Dedication. Except for the limited purpose of indicating that
|
||||
material is shared under a Creative Commons public license or as
|
||||
otherwise permitted by the Creative Commons policies published at
|
||||
creativecommons.org/policies, Creative Commons does not authorize the
|
||||
use of the trademark "Creative Commons" or any other trademark or logo
|
||||
of Creative Commons without its prior written consent including,
|
||||
without limitation, in connection with any unauthorized modifications
|
||||
to any of its public licenses or any other arrangements,
|
||||
understandings, or agreements concerning use of licensed material. For
|
||||
the avoidance of doubt, this paragraph does not form part of the
|
||||
public licenses.
|
||||
|
||||
Creative Commons may be contacted at creativecommons.org.
|
||||
@@ -1,44 +0,0 @@
|
||||
# For maintainers only
|
||||
|
||||
## Responsibilities
|
||||
|
||||
Please go through this link [Maintainer Responsibility](https://gist.github.com/abperiasamy/f4d9b31d3186bbd26522)
|
||||
|
||||
### Setup your mc Github Repository
|
||||
|
||||
Fork [mc upstream](https://github.com/minio/mc/fork) source repository to your own personal repository.
|
||||
|
||||
```
|
||||
|
||||
$ mkdir -p $GOPATH/src/github.com/minio
|
||||
$ cd $GOPATH/src/github.com/minio
|
||||
$ git clone https://github.com/$USER_ID/mc
|
||||
$
|
||||
|
||||
```
|
||||
|
||||
``mc`` uses [govendor](https://github.com/kardianos/govendor) for its dependency management.
|
||||
|
||||
### To manage dependencies
|
||||
|
||||
#### Add new dependencies
|
||||
|
||||
- Run `go get foo/bar`
|
||||
- Edit your code to import foo/bar
|
||||
- Run `govendor add foo/bar` from top-level folder
|
||||
|
||||
#### Remove dependencies
|
||||
|
||||
- Run `govendor remove foo/bar`
|
||||
|
||||
#### Update dependencies
|
||||
|
||||
- Run `govendor remove +vendor`
|
||||
- Run to update the dependent package `go get -u foo/bar`
|
||||
- Run `govendor add +external`
|
||||
|
||||
### Making new releases
|
||||
|
||||
`mc` doesn't follow semantic versioning style, `mc` instead uses the release date and time as the release versions.
|
||||
|
||||
`make release` will generate new binary into `release` directory.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
||||
# MinIO Client Configuration Files Guide [](https://gitter.im/minio/minio?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
In this document we will walk you through the configuration files of MinIO Client.
|
||||
|
||||
## MinIO Client configuration directory
|
||||
MinIO Client configurations are stored in file name ``.mc``. It is a hidden file which resides on user's home directory.
|
||||
|
||||
**This how the structure of the directory looks like:**
|
||||
|
||||
```
|
||||
tree ~/.mc
|
||||
/home/supernova/.mc
|
||||
├── config.json
|
||||
├── session
|
||||
└── share
|
||||
2 directories, 5 files
|
||||
```
|
||||
### Files and directories
|
||||
|
||||
#### ``session`` directory
|
||||
``session`` directory keeps metadata information of all incomplete upload or mirror. You can run ``mc session list`` to list the same.
|
||||
|
||||
#### ``config.json``
|
||||
config.json is the configuration file for MinIO Client, it gets generated after you install and start MinIO. All the credentials, endpoint information we add via ``mc alias`` are stored/modified here.
|
||||
|
||||
```
|
||||
cat config.json
|
||||
{
|
||||
"version": "10",
|
||||
"aliases": {
|
||||
"XL": {
|
||||
"url": "http://127.0.0.1:9000",
|
||||
"accessKey": "YI7S1CKXB76RGOGT6R8W",
|
||||
"secretKey": "FJ9PWUVNXGPfiI72WMRFepN3LsFgW3MjsxSALroV",
|
||||
"api": "S3v4",
|
||||
"path": "auto"
|
||||
},
|
||||
"fs": {
|
||||
"url": "http://127.0.0.1:9000",
|
||||
"accessKey": "YI7S1CKXB76RGOGT6R8W",
|
||||
"secretKey": "FJ9PWUVNXGPfiI72WMRFepN3LsFgW3MjsxSALroV",
|
||||
"api": "S3v4",
|
||||
"path": "auto"
|
||||
},
|
||||
"gcs": {
|
||||
"url": "https://storage.googleapis.com",
|
||||
"accessKey": "YOUR-ACCESS-KEY-HERE",
|
||||
"secretKey": "YOUR-SECRET-KEY-HERE",
|
||||
"api": "S3v2",
|
||||
"path": "auto"
|
||||
},
|
||||
"play": {
|
||||
"url": "https://play.min.io",
|
||||
"accessKey": "Q3AM3UQ867SPQQA43P2F",
|
||||
"secretKey": "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
|
||||
"api": "S3v4",
|
||||
"path": "auto"
|
||||
},
|
||||
"s3": {
|
||||
"url": "https://s3.amazonaws.com",
|
||||
"accessKey": "YOUR-ACCESS-KEY-HERE",
|
||||
"secretKey": "YOUR-SECRET-KEY-HERE",
|
||||
"api": "S3v4",
|
||||
"path": "auto"
|
||||
},
|
||||
"ibm": {
|
||||
"url": "https://s3.YOUR-REGION.cloud-object-storage.appdomain.cloud",
|
||||
"accessKey": "YOUR-HMAC-ACCESS-KEY-ID",
|
||||
"secretKey": "YOUR-HMAC-SECRET-ACCESS-KEY",
|
||||
"api": "S3v4",
|
||||
"path": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
``version`` tells the version of the file.
|
||||
|
||||
``aliases`` stores authentication credentials which will be used by MinIO Client.
|
||||
|
||||
#### ``config.json.old``
|
||||
This file keeps previous config file version details.
|
||||
|
||||
#### ``share`` directory
|
||||
``share`` directory keeps metadata information of all upload and download URL for objects which is used by MinIO client ``mc share`` command.
|
||||
|
||||
## Explore Further
|
||||
* [MinIO Client Complete Guide](https://min.io/docs/minio/linux/reference/minio-mc.html?ref=gh)
|
||||
|
||||
|
||||
|
||||
|
||||
53
go.mod
53
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/minio/mc
|
||||
|
||||
go 1.19
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/bubbletea v0.25.0
|
||||
@@ -10,23 +10,23 @@ require (
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/inconshreveable/mousetrap v1.1.0
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/klauspost/compress v1.17.4
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.6
|
||||
github.com/mattn/go-ieproxy v0.0.11
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/minio/cli v1.24.2
|
||||
github.com/minio/colorjson v1.0.6
|
||||
github.com/minio/filepath v1.0.0
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/minio-go/v7 v7.0.67
|
||||
github.com/minio/minio-go/v7 v7.0.69
|
||||
github.com/minio/selfupdate v0.6.0
|
||||
github.com/minio/sha256-simd v1.0.1
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/pkg/xattr v0.4.9
|
||||
github.com/posener/complete v1.2.3
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/prometheus/prom2json v1.3.3 // indirect
|
||||
github.com/rjeczalik/notify v0.9.3
|
||||
github.com/rs/xid v1.5.0
|
||||
@@ -45,7 +45,7 @@ require (
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/juju/ratelimit v1.0.2
|
||||
github.com/minio/madmin-go/v3 v3.0.50
|
||||
github.com/minio/pkg/v2 v2.0.7
|
||||
github.com/minio/pkg/v2 v2.0.16
|
||||
github.com/muesli/reflow v0.3.0
|
||||
github.com/olekukonko/tablewriter v0.0.5
|
||||
github.com/vbauerster/mpb/v8 v8.7.1
|
||||
@@ -58,14 +58,14 @@ require (
|
||||
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.29 // indirect
|
||||
github.com/minio/mux v1.9.0 // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/safchain/ethtool v0.3.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -74,23 +74,22 @@ require (
|
||||
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/jedib0t/go-pretty/v6 v6.4.9
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/iter v1.0.2 // indirect
|
||||
github.com/lestrrat-go/jwx v1.2.29 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
@@ -102,27 +101,25 @@ require (
|
||||
github.com/muesli/termenv v0.15.2
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/prometheus/common v0.45.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0
|
||||
github.com/rivo/uniseg v0.4.4 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/prometheus/common v0.52.2 // indirect
|
||||
github.com/prometheus/procfs v0.13.0
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/secure-io/sio-go v0.3.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tinylib/msgp v1.1.9 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.11 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.11 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.11 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.13 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.13 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.19.0
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect
|
||||
google.golang.org/grpc v1.60.1 // indirect
|
||||
google.golang.org/grpc v1.63.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
103
go.sum
103
go.sum
@@ -31,8 +31,9 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
@@ -51,10 +52,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
@@ -62,8 +61,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -80,11 +79,11 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
|
||||
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -108,8 +107,8 @@ github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmt
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIgJQzlui7nK1Ncm0tp2ktmPj8xO4N/0=
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI=
|
||||
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
@@ -130,8 +129,6 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
|
||||
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
|
||||
github.com/minio/cli v1.24.2 h1:J+fCUh9mhPLjN3Lj/YhklXvxj8mnyE/D6FpFduXJ2jg=
|
||||
github.com/minio/cli v1.24.2/go.mod h1:bYxnK0uS629N3Bq+AOZZ+6lwF77Sodk4+UL9vNuXhOY=
|
||||
github.com/minio/colorjson v1.0.6 h1:m7TUvpvt0u7FBmVIEQNIa0T4NBQlxrcMBp4wJKsg2Ik=
|
||||
@@ -142,12 +139,12 @@ github.com/minio/madmin-go/v3 v3.0.50 h1:+RQMetVFvPQmAOEDN/xmLhwk9+xOzu3rqwnlZEs
|
||||
github.com/minio/madmin-go/v3 v3.0.50/go.mod h1:ZDF7kf5fhmxLhbGTqyq5efs4ao0v4eWf7nOuef/ljJs=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.67 h1:BeBvZWAS+kRJm1vGTMJYVjKUNoo0FoEt/wUWdUtfmh8=
|
||||
github.com/minio/minio-go/v7 v7.0.67/go.mod h1:+UXocnUeZ3wHvVh5s95gcrA4YjMIbccT6ubB+1m054A=
|
||||
github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
|
||||
github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
|
||||
github.com/minio/mux v1.9.0 h1:dWafQFyEfGhJvK6AwLOt83bIG5bxKxKJnKMCi0XAaoA=
|
||||
github.com/minio/mux v1.9.0/go.mod h1:1pAare17ZRL5GpmNL+9YmqHoWnLmMZF9C/ioUCfy0BQ=
|
||||
github.com/minio/pkg/v2 v2.0.7 h1:vJZ+XUTDeUe/cHpPZSyG/+54252dg6RQKU5K1jXfy/A=
|
||||
github.com/minio/pkg/v2 v2.0.7/go.mod h1:yayUTo82b0RK+e97hGb1naC787mOtUEyDs3SIcwSyHI=
|
||||
github.com/minio/pkg/v2 v2.0.16 h1:qBw2D08JE7fu4UORIxx0O4L09NM0wtMrw9sJRU5R1u0=
|
||||
github.com/minio/pkg/v2 v2.0.16/go.mod h1:V+OP/fKRD/qhJMQpdXXrCXcLYjGMpHKEE26zslthm5k=
|
||||
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
|
||||
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
|
||||
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||
@@ -179,25 +176,26 @@ github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE=
|
||||
github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
|
||||
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.52.2 h1:LW8Vk7BccEdONfrJBDffQGRtpSzi5CQaRZGtboOO2ck=
|
||||
github.com/prometheus/common v0.52.2/go.mod h1:lrWtQx+iDfn2mbH5GUzlH9TSHyfZpHkSiG1W7y3sF2Q=
|
||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||
github.com/prometheus/prom2json v1.3.3 h1:IYfSMiZ7sSOfliBoo89PcufjWO4eAR0gznGcETyaUgo=
|
||||
github.com/prometheus/prom2json v1.3.3/go.mod h1:Pv4yIPktEkK7btWsrUTWDDDrnpUrAELaOCj+oFwlgmc=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rjeczalik/notify v0.9.3 h1:6rJAzHTGKXGj76sbRgDiDcYj/HniypXmSJo1SWakZeY=
|
||||
github.com/rjeczalik/notify v0.9.3/go.mod h1:gF3zSOrafR9DQEWSE8TjfI9NkooDxbyT4UgRGKZA0lc=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
@@ -215,8 +213,6 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -224,7 +220,6 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
@@ -253,17 +248,18 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.etcd.io/etcd/api/v3 v3.5.11 h1:B54KwXbWDHyD3XYAwprxNzTe7vlhR69LuBgZnMVvS7E=
|
||||
go.etcd.io/etcd/api/v3 v3.5.11/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.11 h1:bT2xVspdiCj2910T0V+/KHcVKjkUrCZVtk8J2JF2z1A=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.11/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4=
|
||||
go.etcd.io/etcd/client/v3 v3.5.11 h1:ajWtgoNSZJ1gmS8k+icvPtqsqEav+iUorF7b0qozgUU=
|
||||
go.etcd.io/etcd/client/v3 v3.5.11/go.mod h1:a6xQUEqFJ8vztO1agJh/KQKOMfFI8og52ZconzcDJwE=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.etcd.io/etcd/api/v3 v3.5.13 h1:8WXU2/NBge6AUF1K1gOexB6e07NgsN1hXK0rSTtgSp4=
|
||||
go.etcd.io/etcd/api/v3 v3.5.13/go.mod h1:gBqlqkcMMZMVTMm4NDZloEVJzxQOQIls8splbqBDa0c=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8=
|
||||
go.etcd.io/etcd/client/v3 v3.5.13 h1:o0fHTNJLeO0MyVbc7I3fsCf6nrOqn5d+diSarKnB2js=
|
||||
go.etcd.io/etcd/client/v3 v3.5.13/go.mod h1:cqiAeY8b5DEEcpxvgWKsbLIWNM/8Wy2xJSDMtioMcoI=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
@@ -297,8 +293,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -314,7 +310,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -353,16 +348,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
|
||||
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA=
|
||||
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
|
||||
google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
Reference in New Issue
Block a user