mirror of
https://github.com/minio/mc.git
synced 2025-11-12 01:02:26 +03:00
Refacture structure proposal. (#1793)
This commit is contained in:
committed by
Harshavardhana
parent
c9ecd023fa
commit
b51fb32054
37
command/access-perms.go
Normal file
37
command/access-perms.go
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
// isValidAccessPERM - is provided access perm string supported.
|
||||
func (b accessPerms) isValidAccessPERM() bool {
|
||||
switch b {
|
||||
case accessNone, accessDownload, accessUpload, accessBoth:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// accessPerms - access level.
|
||||
type accessPerms string
|
||||
|
||||
// different types of Access perm's currently supported by policy command.
|
||||
const (
|
||||
accessNone = accessPerms("none")
|
||||
accessDownload = accessPerms("download")
|
||||
accessUpload = accessPerms("upload")
|
||||
accessBoth = accessPerms("both")
|
||||
)
|
||||
120
command/accounting-reader.go
Normal file
120
command/accounting-reader.go
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// accounter keeps tabs of ongoing data transfer information.
|
||||
type accounter struct {
|
||||
current int64
|
||||
|
||||
Total int64
|
||||
startTime time.Time
|
||||
startValue int64
|
||||
refreshRate time.Duration
|
||||
currentValue int64
|
||||
finishOnce sync.Once
|
||||
isFinished chan struct{}
|
||||
}
|
||||
|
||||
// Instantiate a new accounter.
|
||||
func newAccounter(total int64) *accounter {
|
||||
acct := &accounter{
|
||||
Total: total,
|
||||
startTime: time.Now(),
|
||||
startValue: 0,
|
||||
refreshRate: time.Millisecond * 200,
|
||||
isFinished: make(chan struct{}),
|
||||
currentValue: -1,
|
||||
}
|
||||
go acct.writer()
|
||||
return acct
|
||||
}
|
||||
|
||||
// write calculate the final speed.
|
||||
func (a *accounter) write(current int64) float64 {
|
||||
fromStart := time.Now().Sub(a.startTime)
|
||||
currentFromStart := current - a.startValue
|
||||
if currentFromStart > 0 {
|
||||
speed := float64(currentFromStart) / (float64(fromStart) / float64(time.Second))
|
||||
return speed
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// writer update new accounting data for a specified refreshRate.
|
||||
func (a *accounter) writer() {
|
||||
a.Update()
|
||||
for {
|
||||
select {
|
||||
case <-a.isFinished:
|
||||
return
|
||||
case <-time.After(a.refreshRate):
|
||||
a.Update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// accountStat cantainer for current stats captured.
|
||||
type accountStat struct {
|
||||
Total int64
|
||||
Transferred int64
|
||||
Speed float64
|
||||
}
|
||||
|
||||
// Stat provides current stats captured.
|
||||
func (a *accounter) Stat() accountStat {
|
||||
var acntStat accountStat
|
||||
a.finishOnce.Do(func() {
|
||||
close(a.isFinished)
|
||||
acntStat.Total = a.Total
|
||||
acntStat.Transferred = a.current
|
||||
acntStat.Speed = a.write(atomic.LoadInt64(&a.current))
|
||||
})
|
||||
return acntStat
|
||||
}
|
||||
|
||||
// Update update with new values loaded atomically.
|
||||
func (a *accounter) Update() {
|
||||
c := atomic.LoadInt64(&a.current)
|
||||
if c != a.currentValue {
|
||||
a.write(c)
|
||||
a.currentValue = c
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the current value atomically.
|
||||
func (a *accounter) Set(n int64) *accounter {
|
||||
atomic.StoreInt64(&a.current, n)
|
||||
return a
|
||||
}
|
||||
|
||||
// Add add to current value atomically.
|
||||
func (a *accounter) Add(n int64) int64 {
|
||||
return atomic.AddInt64(&a.current, n)
|
||||
}
|
||||
|
||||
// Read implements Reader which internally updates current value.
|
||||
func (a *accounter) Read(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
a.Add(int64(n))
|
||||
return
|
||||
}
|
||||
167
command/api_handlers_test.go
Normal file
167
command/api_handlers_test.go
Normal file
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type objectAPIHandler struct {
|
||||
lock *sync.Mutex
|
||||
bucket string
|
||||
object map[string][]byte
|
||||
}
|
||||
|
||||
func (h objectAPIHandler) getHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
switch {
|
||||
case r.URL.Path == "/":
|
||||
response := []byte("<ListAllMyBucketsResult xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"><Buckets><Bucket><Name>bucket</Name><CreationDate>2015-05-20T23:05:09.230Z</CreationDate></Bucket></Buckets><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner></ListAllMyBucketsResult>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
return
|
||||
case r.URL.Path == "/bucket":
|
||||
_, ok := r.URL.Query()["acl"]
|
||||
if ok {
|
||||
response := []byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?><AccessControlPolicy><Owner><ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID><DisplayName>CustomersName@amazon.com</DisplayName></Owner><AccessControlList><Grant><Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\"><ID>75aa57f09aa0c8caeab4f8c24e99d10f8e7faeebf76c078efc7c6caea54ba06a</ID><DisplayName>CustomersName@amazon.com</DisplayName></Grantee><Permission>FULL_CONTROL</Permission></Grant></AccessControlList></AccessControlPolicy>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
return
|
||||
}
|
||||
fallthrough
|
||||
case r.URL.Path == "/bucket":
|
||||
response := []byte("<ListBucketResult xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"><Contents><ETag>b1946ac92492d2347c6235b4d2611184</ETag><Key>object0</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><ETag>b1946ac92492d2347c6235b4d2611184</ETag><Key>object1</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><ETag>b1946ac92492d2347c6235b4d2611184</ETag><Key>object2</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><ETag>b1946ac92492d2347c6235b4d2611184</ETag><Key>object3</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><ETag>b1946ac92492d2347c6235b4d2611184</ETag><Key>object4</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><ETag>b1946ac92492d2347c6235b4d2611184</ETag><Key>object5</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><ETag>b1946ac92492d2347c6235b4d2611184</ETag><Key>object6</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Contents><ETag>b1946ac92492d2347c6235b4d2611184</ETag><Key>object7</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Delimiter></Delimiter><EncodingType></EncodingType><IsTruncated>false</IsTruncated><Marker></Marker><MaxKeys>1000</MaxKeys><Name>testbucket</Name><NextMarker></NextMarker><Prefix></Prefix></ListBucketResult>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
return
|
||||
case r.URL.Path != "":
|
||||
if _, ok := h.object[filepath.Base(r.URL.Path)]; !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(h.object[filepath.Base(r.URL.Path)])))
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
w.Header().Set("ETag", "b1946ac92492d2347c6235b4d2611184")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.Copy(w, bytes.NewReader(h.object[filepath.Base(r.URL.Path)]))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h objectAPIHandler) headHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
switch {
|
||||
case r.URL.Path == "/":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
case r.URL.Path == "/bucket":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
case r.URL.Path != "":
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(h.object[filepath.Base(r.URL.Path)])))
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
w.Header().Set("ETag", "b1946ac92492d2347c6235b4d2611184")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func isValidBucket(bucket string) bool {
|
||||
if len(bucket) < 3 || len(bucket) > 63 {
|
||||
return false
|
||||
}
|
||||
if bucket[0] == '.' || bucket[len(bucket)-1] == '.' {
|
||||
return false
|
||||
}
|
||||
if match, _ := regexp.MatchString("\\.\\.", bucket); match {
|
||||
return false
|
||||
}
|
||||
// We don't support buckets with '.' in them
|
||||
match, _ := regexp.MatchString("^[a-zA-Z][a-zA-Z0-9\\-]+[a-zA-Z0-9]$", bucket)
|
||||
return match
|
||||
}
|
||||
|
||||
func (h objectAPIHandler) putHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h.lock.Lock()
|
||||
defer h.lock.Unlock()
|
||||
switch {
|
||||
case r.URL.Path == "/":
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
case r.URL.Path == "/bucket":
|
||||
_, ok := r.URL.Query()["acl"]
|
||||
if ok {
|
||||
switch r.Header.Get("x-amz-acl") {
|
||||
case "public-read-write":
|
||||
fallthrough
|
||||
case "public-read":
|
||||
fallthrough
|
||||
case "private":
|
||||
fallthrough
|
||||
case "authenticated-read":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
case r.URL.Path != "":
|
||||
if !isValidBucket(strings.Split(r.URL.Path, "/")[1]) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
length, e := strconv.Atoi(r.Header.Get("Content-Length"))
|
||||
if e != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
if _, e = io.CopyN(&buffer, r.Body, int64(length)); e != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
h.object[filepath.Base(r.URL.Path)] = buffer.Bytes()
|
||||
w.Header().Set("ETag", "b1946ac92492d2347c6235b4d2611184")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h objectAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
h.getHandler(w, r)
|
||||
case r.Method == "HEAD":
|
||||
h.headHandler(w, r)
|
||||
case r.Method == "PUT":
|
||||
h.putHandler(w, r)
|
||||
}
|
||||
}
|
||||
28
command/build-constants.go
Normal file
28
command/build-constants.go
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
var (
|
||||
// mcVersion - version time.RFC3339.
|
||||
mcVersion = "DEVELOPMENT.GOGET"
|
||||
// mcReleaseTag - release tag in TAG.%Y-%m-%dT%H-%M-%SZ.
|
||||
mcReleaseTag = "DEVELOPMENT.GOGET"
|
||||
// mcCommitID - latest commit id.
|
||||
mcCommitID = "DEVELOPMENT.GOGET"
|
||||
// mcShortCommitID - first 12 characters from mcCommitID.
|
||||
mcShortCommitID = mcCommitID[:12]
|
||||
)
|
||||
152
command/cat-main.go
Normal file
152
command/cat-main.go
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Minio Client, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
catFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of cat",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Display contents of a file.
|
||||
var catCmd = cli.Command{
|
||||
Name: "cat",
|
||||
Usage: "Display contents of a file.",
|
||||
Action: mainCat,
|
||||
Flags: append(catFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] SOURCE [SOURCE...]
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Stream an object from Amazon S3 cloud storage to mplayer standard input.
|
||||
$ mc {{.Name}} s3/ferenginar/klingon_opera_aktuh_maylotah.ogg | mplayer -
|
||||
|
||||
2. Concantenate contents of file1.txt and stdin to standard output.
|
||||
$ mc {{.Name}} file1.txt - > file.txt
|
||||
|
||||
3. Concantenate multiple files to one.
|
||||
$ mc {{.Name}} part.* > complete.img
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
// checkCatSyntax performs command-line input validation for cat command.
|
||||
func checkCatSyntax(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if !args.Present() {
|
||||
args = []string{"-"}
|
||||
}
|
||||
for _, arg := range args {
|
||||
if strings.HasPrefix(arg, "-") && len(arg) > 1 {
|
||||
fatalIf(probe.NewError(errors.New("")), fmt.Sprintf("Unknown flag ‘%s’ passed.", arg))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// catURL displays contents of a URL to stdout.
|
||||
func catURL(sourceURL string) *probe.Error {
|
||||
var reader io.Reader
|
||||
switch sourceURL {
|
||||
case "-":
|
||||
reader = os.Stdin
|
||||
default:
|
||||
// Ignore size, since os.Stat() would not return proper size all the
|
||||
// time for local filesystem for example /proc files.
|
||||
var err *probe.Error
|
||||
if reader, err = getSourceStream(sourceURL); err != nil {
|
||||
return err.Trace(sourceURL)
|
||||
}
|
||||
}
|
||||
return catOut(reader).Trace(sourceURL)
|
||||
}
|
||||
|
||||
// catOut reads from reader stream and writes to stdout.
|
||||
func catOut(r io.Reader) *probe.Error {
|
||||
// Read till EOF.
|
||||
if _, e := io.Copy(os.Stdout, r); e != nil {
|
||||
switch e := e.(type) {
|
||||
case *os.PathError:
|
||||
if e.Err == syscall.EPIPE {
|
||||
// stdout closed by the user. Gracefully exit.
|
||||
return nil
|
||||
}
|
||||
return probe.NewError(e)
|
||||
default:
|
||||
return probe.NewError(e)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mainCat is the main entry point for cat command.
|
||||
func mainCat(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'cat' cli arguments.
|
||||
checkCatSyntax(ctx)
|
||||
|
||||
// Set command flags from context.
|
||||
stdinMode := false
|
||||
if !ctx.Args().Present() {
|
||||
stdinMode = true
|
||||
}
|
||||
|
||||
// handle std input data.
|
||||
if stdinMode {
|
||||
fatalIf(catOut(os.Stdin).Trace(), "Unable to read from standard input.")
|
||||
return
|
||||
}
|
||||
|
||||
// if Args contain ‘-’, we need to preserve its order specially.
|
||||
args := []string(ctx.Args())
|
||||
if ctx.Args().First() == "-" {
|
||||
for i, arg := range os.Args {
|
||||
if arg == "cat" {
|
||||
// Overwrite ctx.Args with os.Args.
|
||||
args = os.Args[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert arguments to URLs: expand alias, fix format.
|
||||
for _, url := range args {
|
||||
fatalIf(catURL(url).Trace(url), "Unable to read from ‘"+url+"’.")
|
||||
}
|
||||
}
|
||||
184
command/client-errors.go
Normal file
184
command/client-errors.go
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import "fmt"
|
||||
|
||||
/// Collection of standard errors
|
||||
|
||||
// APINotImplemented - api not implemented
|
||||
type APINotImplemented struct {
|
||||
API string
|
||||
APIType string
|
||||
}
|
||||
|
||||
func (e APINotImplemented) Error() string {
|
||||
return "‘" + e.API + "’ feature " + "is not implemented for ‘" + e.APIType + "’."
|
||||
}
|
||||
|
||||
// GenericBucketError - generic bucket operations error
|
||||
type GenericBucketError struct {
|
||||
Bucket string
|
||||
}
|
||||
|
||||
// BucketDoesNotExist - bucket does not exist.
|
||||
type BucketDoesNotExist GenericBucketError
|
||||
|
||||
func (e BucketDoesNotExist) Error() string {
|
||||
return "Bucket ‘" + e.Bucket + "’ does not exist."
|
||||
}
|
||||
|
||||
// BucketExists - bucket exists.
|
||||
type BucketExists GenericBucketError
|
||||
|
||||
func (e BucketExists) Error() string {
|
||||
return "Bucket ‘" + e.Bucket + "’ exists."
|
||||
}
|
||||
|
||||
// BucketNameEmpty - bucket name empty (http://goo.gl/wJlzDz)
|
||||
type BucketNameEmpty struct{}
|
||||
|
||||
func (e BucketNameEmpty) Error() string {
|
||||
return "Bucket name cannot be empty."
|
||||
}
|
||||
|
||||
// BucketInvalid - bucket name invalid.
|
||||
type BucketInvalid struct {
|
||||
Bucket string
|
||||
}
|
||||
|
||||
func (e BucketInvalid) Error() string {
|
||||
return "Bucket name " + e.Bucket + " not valid."
|
||||
}
|
||||
|
||||
// ObjectAlreadyExists - typed return for MethodNotAllowed
|
||||
type ObjectAlreadyExists struct {
|
||||
Object string
|
||||
}
|
||||
|
||||
func (e ObjectAlreadyExists) Error() string {
|
||||
return "Object ‘" + e.Object + "’ already exists."
|
||||
}
|
||||
|
||||
// ObjectAlreadyExistsAsDirectory - typed return for XMinioObjectExistsAsDirectory
|
||||
type ObjectAlreadyExistsAsDirectory struct {
|
||||
Object string
|
||||
}
|
||||
|
||||
func (e ObjectAlreadyExistsAsDirectory) Error() string {
|
||||
return "Object ‘" + e.Object + "’ already exists as directory."
|
||||
}
|
||||
|
||||
// ObjectOnGlacier - object is of storage class glacier.
|
||||
type ObjectOnGlacier struct {
|
||||
Object string
|
||||
}
|
||||
|
||||
func (e ObjectOnGlacier) Error() string {
|
||||
return "Object ‘" + e.Object + "’ is on Glacier storage."
|
||||
}
|
||||
|
||||
// BucketNameTopLevel - generic error
|
||||
type BucketNameTopLevel struct{}
|
||||
|
||||
func (e BucketNameTopLevel) Error() string {
|
||||
return "Buckets can only be created at the top level."
|
||||
}
|
||||
|
||||
// GenericFileError - generic file error.
|
||||
type GenericFileError struct {
|
||||
Path string
|
||||
}
|
||||
|
||||
// PathNotFound (ENOENT) - file not found.
|
||||
type PathNotFound GenericFileError
|
||||
|
||||
func (e PathNotFound) Error() string {
|
||||
return "Requested file ‘" + e.Path + "’ not found"
|
||||
}
|
||||
|
||||
// PathIsNotRegular (ENOTREG) - file is not a regular file.
|
||||
type PathIsNotRegular GenericFileError
|
||||
|
||||
func (e PathIsNotRegular) Error() string {
|
||||
return "Requested file ‘" + e.Path + "’ is not a regular file."
|
||||
}
|
||||
|
||||
// PathInsufficientPermission (EPERM) - permission denied.
|
||||
type PathInsufficientPermission GenericFileError
|
||||
|
||||
func (e PathInsufficientPermission) Error() string {
|
||||
return "Insufficient permissions to access this file ‘" + e.Path + "’"
|
||||
}
|
||||
|
||||
// BrokenSymlink (ENOTENT) - file has broken symlink.
|
||||
type BrokenSymlink GenericFileError
|
||||
|
||||
func (e BrokenSymlink) Error() string {
|
||||
return "Requested file ‘" + e.Path + "’ has broken symlink"
|
||||
}
|
||||
|
||||
// TooManyLevelsSymlink (ELOOP) - file has too many levels of symlinks.
|
||||
type TooManyLevelsSymlink GenericFileError
|
||||
|
||||
func (e TooManyLevelsSymlink) Error() string {
|
||||
return "Requested file ‘" + e.Path + "’ has too many levels of symlinks"
|
||||
}
|
||||
|
||||
// EmptyPath (EINVAL) - invalid argument.
|
||||
type EmptyPath struct{}
|
||||
|
||||
func (e EmptyPath) Error() string {
|
||||
return "Invalid path, path cannot be empty"
|
||||
}
|
||||
|
||||
// ObjectMissing (EINVAL) - object key missing.
|
||||
type ObjectMissing struct{}
|
||||
|
||||
func (e ObjectMissing) Error() string {
|
||||
return "Object key is missing, object key cannot be empty"
|
||||
}
|
||||
|
||||
// UnexpectedShortWrite - write wrote less bytes than expected.
|
||||
type UnexpectedShortWrite struct {
|
||||
InputSize int
|
||||
WriteSize int
|
||||
}
|
||||
|
||||
func (e UnexpectedShortWrite) Error() string {
|
||||
msg := fmt.Sprintf("Wrote less data than requested. Expected ‘%d’ bytes, but only wrote ‘%d’ bytes.", e.InputSize, e.WriteSize)
|
||||
return msg
|
||||
}
|
||||
|
||||
// UnexpectedEOF (EPIPE) - reader closed prematurely.
|
||||
type UnexpectedEOF struct {
|
||||
TotalSize int64
|
||||
TotalWritten int64
|
||||
}
|
||||
|
||||
func (e UnexpectedEOF) Error() string {
|
||||
msg := fmt.Sprintf("Input reader closed pre-maturely. Expected ‘%d’ bytes, but only received ‘%d’ bytes.", e.TotalSize, e.TotalWritten)
|
||||
return msg
|
||||
}
|
||||
|
||||
// UnexpectedExcessRead - reader wrote more data than requested.
|
||||
type UnexpectedExcessRead UnexpectedEOF
|
||||
|
||||
func (e UnexpectedExcessRead) Error() string {
|
||||
msg := fmt.Sprintf("Received excess data on input reader. Expected only ‘%d’ bytes, but received ‘%d’ bytes.", e.TotalSize, e.TotalWritten)
|
||||
return msg
|
||||
}
|
||||
1009
command/client-fs.go
Normal file
1009
command/client-fs.go
Normal file
File diff suppressed because it is too large
Load Diff
344
command/client-fs_test.go
Normal file
344
command/client-fs_test.go
Normal file
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this fs except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Test list files in a folder.
|
||||
func (s *TestSuite) TestList(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
// Create multiple files.
|
||||
objectPath := filepath.Join(root, "object1")
|
||||
fsClient, err := fsNew(objectPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
data := "hello"
|
||||
|
||||
reader := bytes.NewReader([]byte(data))
|
||||
var n int64
|
||||
n, err = fsClient.Put(reader, int64(len(data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
|
||||
objectPath = filepath.Join(root, "object2")
|
||||
fsClient, err = fsNew(objectPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
reader = bytes.NewReader([]byte(data))
|
||||
n, err = fsClient.Put(reader, int64(len(data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
|
||||
fsClient, err = fsNew(root)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// Verify previously create files and list them.
|
||||
var contents []*clientContent
|
||||
for content := range fsClient.List(false, false) {
|
||||
if content.Err != nil {
|
||||
err = content.Err
|
||||
break
|
||||
}
|
||||
contents = append(contents, content)
|
||||
}
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(contents), Equals, 1)
|
||||
c.Assert(contents[0].Type.IsDir(), Equals, true)
|
||||
|
||||
// Create another file.
|
||||
objectPath = filepath.Join(root, "test1/newObject1")
|
||||
fsClient, err = fsNew(objectPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
reader = bytes.NewReader([]byte(data))
|
||||
n, err = fsClient.Put(reader, int64(len(data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
|
||||
fsClient, err = fsNew(root)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
contents = nil
|
||||
// List non recursive to list only top level files.
|
||||
for content := range fsClient.List(false, false) {
|
||||
if content.Err != nil {
|
||||
err = content.Err
|
||||
break
|
||||
}
|
||||
contents = append(contents, content)
|
||||
}
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(contents), Equals, 1)
|
||||
c.Assert(contents[0].Type.IsDir(), Equals, true)
|
||||
|
||||
fsClient, err = fsNew(root)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
contents = nil
|
||||
// List recursively all files and verify.
|
||||
for content := range fsClient.List(true, false) {
|
||||
if content.Err != nil {
|
||||
err = content.Err
|
||||
break
|
||||
}
|
||||
contents = append(contents, content)
|
||||
}
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(len(contents), Equals, 3)
|
||||
|
||||
var regularFiles int
|
||||
var regularDirs int
|
||||
// Test number of expected files and directories.
|
||||
for _, content := range contents {
|
||||
if content.Type.IsRegular() {
|
||||
regularFiles++
|
||||
continue
|
||||
}
|
||||
if content.Type.IsDir() {
|
||||
regularDirs++
|
||||
continue
|
||||
}
|
||||
}
|
||||
c.Assert(regularDirs, Equals, 0)
|
||||
c.Assert(regularFiles, Equals, 3)
|
||||
|
||||
// Create an ignored file and list to verify if its ignored.
|
||||
objectPath = filepath.Join(root, "test1/.DS_Store")
|
||||
fsClient, err = fsNew(objectPath)
|
||||
|
||||
reader = bytes.NewReader([]byte(data))
|
||||
n, err = fsClient.Put(reader, int64(len(data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
|
||||
fsClient, err = fsNew(root)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
contents = nil
|
||||
// List recursively all files and verify.
|
||||
for content := range fsClient.List(true, false) {
|
||||
if content.Err != nil {
|
||||
err = content.Err
|
||||
break
|
||||
}
|
||||
contents = append(contents, content)
|
||||
}
|
||||
|
||||
c.Assert(err, IsNil)
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
c.Assert(len(contents), Equals, 3)
|
||||
default:
|
||||
c.Assert(len(contents), Equals, 4)
|
||||
}
|
||||
|
||||
regularFiles = 0
|
||||
// Test number of expected files.
|
||||
for _, content := range contents {
|
||||
if content.Type.IsRegular() {
|
||||
regularFiles++
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
c.Assert(regularFiles, Equals, 3)
|
||||
default:
|
||||
c.Assert(regularFiles, Equals, 4)
|
||||
}
|
||||
}
|
||||
|
||||
// Test put bucket aka 'mkdir()' operation.
|
||||
func (s *TestSuite) TestPutBucket(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
bucketPath := filepath.Join(root, "bucket")
|
||||
fsClient, err := fsNew(bucketPath)
|
||||
c.Assert(err, IsNil)
|
||||
err = fsClient.MakeBucket("us-east-1")
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
// Test stat bucket aka 'stat()' operation.
|
||||
func (s *TestSuite) TestStatBucket(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
bucketPath := filepath.Join(root, "bucket")
|
||||
|
||||
fsClient, err := fsNew(bucketPath)
|
||||
c.Assert(err, IsNil)
|
||||
err = fsClient.MakeBucket("us-east-1")
|
||||
c.Assert(err, IsNil)
|
||||
_, err = fsClient.Stat()
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
|
||||
// Test bucket acl fails for directories.
|
||||
func (s *TestSuite) TestBucketACLFails(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
bucketPath := filepath.Join(root, "bucket")
|
||||
fsClient, err := fsNew(bucketPath)
|
||||
c.Assert(err, IsNil)
|
||||
err = fsClient.MakeBucket("us-east-1")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
// On windows setting permissions is not supported.
|
||||
if runtime.GOOS != "windows" {
|
||||
err = fsClient.SetAccess("readonly")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
_, err = fsClient.GetAccess()
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
}
|
||||
|
||||
// Test creating a file.
|
||||
func (s *TestSuite) TestPut(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
objectPath := filepath.Join(root, "object")
|
||||
fsClient, err := fsNew(objectPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
data := "hello"
|
||||
reader := bytes.NewReader([]byte(data))
|
||||
var n int64
|
||||
n, err = fsClient.Put(reader, int64(len(data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
}
|
||||
|
||||
// Test read a file.
|
||||
func (s *TestSuite) TestGet(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
objectPath := filepath.Join(root, "object")
|
||||
fsClient, err := fsNew(objectPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
data := "hello"
|
||||
var reader io.Reader
|
||||
reader = bytes.NewReader([]byte(data))
|
||||
n, err := fsClient.Put(reader, int64(len(data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
|
||||
reader, err = fsClient.Get()
|
||||
c.Assert(err, IsNil)
|
||||
var results bytes.Buffer
|
||||
_, e = io.Copy(&results, reader)
|
||||
c.Assert(e, IsNil)
|
||||
c.Assert([]byte(data), DeepEquals, results.Bytes())
|
||||
|
||||
}
|
||||
|
||||
// Test get range in a file.
|
||||
func (s *TestSuite) TestGetRange(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
objectPath := filepath.Join(root, "object")
|
||||
fsClient, err := fsNew(objectPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
data := "hello world"
|
||||
var reader io.Reader
|
||||
reader = bytes.NewReader([]byte(data))
|
||||
n, err := fsClient.Put(reader, int64(len(data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
|
||||
reader, err = fsClient.Get()
|
||||
c.Assert(err, IsNil)
|
||||
var results bytes.Buffer
|
||||
buf := make([]byte, 5)
|
||||
m, e := reader.(io.ReaderAt).ReadAt(buf, 0)
|
||||
c.Assert(e, IsNil)
|
||||
c.Assert(m, Equals, 5)
|
||||
_, e = results.Write(buf)
|
||||
c.Assert(e, IsNil)
|
||||
c.Assert([]byte("hello"), DeepEquals, results.Bytes())
|
||||
}
|
||||
|
||||
// Test stat file.
|
||||
func (s *TestSuite) TestStatObject(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
|
||||
objectPath := filepath.Join(root, "object")
|
||||
fsClient, err := fsNew(objectPath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
data := "hello"
|
||||
dataLen := len(data)
|
||||
reader := bytes.NewReader([]byte(data))
|
||||
n, err := fsClient.Put(reader, int64(dataLen), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
|
||||
content, err := fsClient.Stat()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(content.Size, Equals, int64(dataLen))
|
||||
}
|
||||
|
||||
// Test copy.
|
||||
func (s *TestSuite) TestCopy(c *C) {
|
||||
root, e := ioutil.TempDir(os.TempDir(), "fs-")
|
||||
c.Assert(e, IsNil)
|
||||
defer os.RemoveAll(root)
|
||||
sourcePath := filepath.Join(root, "source")
|
||||
targetPath := filepath.Join(root, "target")
|
||||
fsClientTarget, err := fsNew(targetPath)
|
||||
fsClientSource, err := fsNew(sourcePath)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
data := "hello world"
|
||||
var reader io.Reader
|
||||
reader = bytes.NewReader([]byte(data))
|
||||
n, err := fsClientSource.Put(reader, int64(len(data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(data)))
|
||||
|
||||
err = fsClientTarget.Copy(sourcePath, int64(len(data)), nil)
|
||||
c.Assert(err, IsNil)
|
||||
}
|
||||
91
command/client-s3-trace_v2.go
Normal file
91
command/client-s3-trace_v2.go
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/mc/pkg/httptracer"
|
||||
)
|
||||
|
||||
// traceV2 - tracing structure for signature version '2'.
|
||||
type traceV2 struct{}
|
||||
|
||||
// newTraceV2 - initialize Trace structure
|
||||
func newTraceV2() httptracer.HTTPTracer {
|
||||
return traceV2{}
|
||||
}
|
||||
|
||||
// Request - Trace HTTP Request
|
||||
func (t traceV2) Request(req *http.Request) (err error) {
|
||||
origAuth := req.Header.Get("Authorization")
|
||||
|
||||
if strings.TrimSpace(origAuth) != "" {
|
||||
// Authorization (S3 v2 signature) Format:
|
||||
// Authorization: AWS AKIAJVA5BMMU2RHO6IO1:Y10YHUZ0DTUterAUI6w3XKX7Iqk=
|
||||
|
||||
// Set a temporary redacted auth
|
||||
req.Header.Set("Authorization", "AWS **REDACTED**:**REDACTED**")
|
||||
|
||||
var reqTrace []byte
|
||||
reqTrace, err = httputil.DumpRequestOut(req, false) // Only display header
|
||||
if err == nil {
|
||||
console.Debug(string(reqTrace))
|
||||
}
|
||||
|
||||
// Undo
|
||||
req.Header.Set("Authorization", origAuth)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Response - Trace HTTP Response
|
||||
func (t traceV2) Response(resp *http.Response) (err error) {
|
||||
var respTrace []byte
|
||||
// For errors we make sure to dump response body as well.
|
||||
if resp.StatusCode != http.StatusOK &&
|
||||
resp.StatusCode != http.StatusPartialContent &&
|
||||
resp.StatusCode != http.StatusNoContent {
|
||||
respTrace, err = httputil.DumpResponse(resp, true)
|
||||
} else {
|
||||
// WORKAROUND for https://github.com/golang/go/issues/13942.
|
||||
// httputil.DumpResponse does not print response headers for
|
||||
// all successful calls which have response ContentLength set
|
||||
// to zero. Keep this workaround until the above bug is fixed.
|
||||
if resp.ContentLength == 0 {
|
||||
var buffer bytes.Buffer
|
||||
if err = resp.Header.Write(&buffer); err != nil {
|
||||
return err
|
||||
}
|
||||
respTrace = buffer.Bytes()
|
||||
respTrace = append(respTrace, []byte("\r\n")...)
|
||||
} else {
|
||||
respTrace, err = httputil.DumpResponse(resp, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
console.Debug(string(respTrace))
|
||||
}
|
||||
return err
|
||||
}
|
||||
100
command/client-s3-trace_v4.go
Normal file
100
command/client-s3-trace_v4.go
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/mc/pkg/httptracer"
|
||||
)
|
||||
|
||||
// traceV4 - tracing structure for signature version '4'.
|
||||
type traceV4 struct{}
|
||||
|
||||
// newTraceV4 - initialize Trace structure
|
||||
func newTraceV4() httptracer.HTTPTracer {
|
||||
return traceV4{}
|
||||
}
|
||||
|
||||
// Request - Trace HTTP Request
|
||||
func (t traceV4) Request(req *http.Request) (err error) {
|
||||
origAuth := req.Header.Get("Authorization")
|
||||
|
||||
if strings.TrimSpace(origAuth) != "" {
|
||||
// Authorization (S3 v4 signature) Format:
|
||||
// Authorization: AWS4-HMAC-SHA256 Credential=AKIAJNACEGBGMXBHLEZA/20150524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=bbfaa693c626021bcb5f911cd898a1a30206c1fad6bad1e0eb89e282173bd24c
|
||||
|
||||
// Strip out accessKeyID from: Credential=<access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request
|
||||
regCred := regexp.MustCompile("Credential=([A-Z0-9]+)/")
|
||||
newAuth := regCred.ReplaceAllString(origAuth, "Credential=**REDACTED**/")
|
||||
|
||||
// Strip out 256-bit signature from: Signature=<256-bit signature>
|
||||
regSign := regexp.MustCompile("Signature=([[0-9a-f]+)")
|
||||
newAuth = regSign.ReplaceAllString(newAuth, "Signature=**REDACTED**")
|
||||
|
||||
// Set a temporary redacted auth
|
||||
req.Header.Set("Authorization", newAuth)
|
||||
|
||||
var reqTrace []byte
|
||||
reqTrace, err = httputil.DumpRequestOut(req, false) // Only display header
|
||||
if err == nil {
|
||||
console.Debug(string(reqTrace))
|
||||
}
|
||||
|
||||
// Undo
|
||||
req.Header.Set("Authorization", origAuth)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Response - Trace HTTP Response
|
||||
func (t traceV4) Response(resp *http.Response) (err error) {
|
||||
var respTrace []byte
|
||||
// For errors we make sure to dump response body as well.
|
||||
if resp.StatusCode != http.StatusOK &&
|
||||
resp.StatusCode != http.StatusPartialContent &&
|
||||
resp.StatusCode != http.StatusNoContent {
|
||||
respTrace, err = httputil.DumpResponse(resp, true)
|
||||
} else {
|
||||
// WORKAROUND for https://github.com/golang/go/issues/13942.
|
||||
// httputil.DumpResponse does not print response headers for
|
||||
// all successful calls which have response ContentLength set
|
||||
// to zero. Keep this workaround until the above bug is fixed.
|
||||
if resp.ContentLength == 0 {
|
||||
var buffer bytes.Buffer
|
||||
if err = resp.Header.Write(&buffer); err != nil {
|
||||
return err
|
||||
}
|
||||
respTrace = buffer.Bytes()
|
||||
respTrace = append(respTrace, []byte("\r\n")...)
|
||||
} else {
|
||||
respTrace, err = httputil.DumpResponse(resp, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
console.Debug(string(respTrace))
|
||||
}
|
||||
return err
|
||||
}
|
||||
803
command/client-s3.go
Normal file
803
command/client-s3.go
Normal file
@@ -0,0 +1,803 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/minio/mc/pkg/httptracer"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"gopkg.in/minio/minio-go.v2"
|
||||
)
|
||||
|
||||
// S3 client
|
||||
type s3Client struct {
|
||||
mutex *sync.Mutex
|
||||
targetURL *clientURL
|
||||
api *minio.Client
|
||||
virtualStyle bool
|
||||
}
|
||||
|
||||
const (
|
||||
amazonHostName = "s3.amazonaws.com"
|
||||
googleHostName = "storage.googleapis.com"
|
||||
)
|
||||
|
||||
// newFactory encloses New function with client cache.
|
||||
func newFactory() func(config *Config) (Client, *probe.Error) {
|
||||
clientCache := make(map[uint32]*minio.Client)
|
||||
mutex := &sync.Mutex{}
|
||||
|
||||
// Return New function.
|
||||
return func(config *Config) (Client, *probe.Error) {
|
||||
// Creates a parsed URL.
|
||||
targetURL := newClientURL(config.HostURL)
|
||||
// By default enable HTTPs.
|
||||
secure := true
|
||||
if targetURL.Scheme == "http" {
|
||||
secure = false
|
||||
}
|
||||
|
||||
// Instantiate s3
|
||||
s3Clnt := &s3Client{}
|
||||
// Allocate a new mutex.
|
||||
s3Clnt.mutex = new(sync.Mutex)
|
||||
// Save the target URL.
|
||||
s3Clnt.targetURL = targetURL
|
||||
|
||||
// Save if target supports virtual host style.
|
||||
hostName := targetURL.Host
|
||||
s3Clnt.virtualStyle = isVirtualHostStyle(hostName)
|
||||
|
||||
if s3Clnt.virtualStyle {
|
||||
// If Amazon URL replace it with 's3.amazonaws.com'
|
||||
if isAmazon(hostName) {
|
||||
hostName = amazonHostName
|
||||
}
|
||||
// If Google URL replace it with 'storage.googleapis.com'
|
||||
if isGoogle(hostName) {
|
||||
hostName = googleHostName
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a hash out of s3Conf.
|
||||
confHash := fnv.New32a()
|
||||
confHash.Write([]byte(hostName + config.AccessKey + config.SecretKey))
|
||||
confSum := confHash.Sum32()
|
||||
|
||||
// Lookup previous cache by hash.
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
var api *minio.Client
|
||||
found := false
|
||||
if api, found = clientCache[confSum]; !found {
|
||||
// Not found. Instantiate a new minio
|
||||
var e error
|
||||
if strings.ToUpper(config.Signature) == "S3V2" {
|
||||
// if Signature version '2' use NewV2 directly.
|
||||
api, e = minio.NewV2(hostName, config.AccessKey, config.SecretKey, secure)
|
||||
} else {
|
||||
// if Signature version '4' use NewV4 directly.
|
||||
api, e = minio.NewV4(hostName, config.AccessKey, config.SecretKey, secure)
|
||||
}
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
if config.Debug {
|
||||
transport := http.DefaultTransport
|
||||
if config.Signature == "S3v4" {
|
||||
transport = httptracer.GetNewTraceTransport(newTraceV4(), http.DefaultTransport)
|
||||
}
|
||||
if config.Signature == "S3v2" {
|
||||
transport = httptracer.GetNewTraceTransport(newTraceV2(), http.DefaultTransport)
|
||||
}
|
||||
// Set custom transport.
|
||||
api.SetCustomTransport(transport)
|
||||
}
|
||||
// Cache the new minio client with hash of config as key.
|
||||
clientCache[confSum] = api
|
||||
}
|
||||
// Set app info.
|
||||
api.SetAppInfo(config.AppName, config.AppVersion)
|
||||
|
||||
// Store the new api object.
|
||||
s3Clnt.api = api
|
||||
|
||||
return s3Clnt, nil
|
||||
}
|
||||
}
|
||||
|
||||
// s3New returns an initialized s3Client structure. If debug is enabled,
|
||||
// it also enables an internal trace transport.
|
||||
var s3New = newFactory()
|
||||
|
||||
// GetURL get url.
|
||||
func (c *s3Client) GetURL() clientURL {
|
||||
return *c.targetURL
|
||||
}
|
||||
|
||||
// Get - get object.
|
||||
func (c *s3Client) Get() (io.Reader, *probe.Error) {
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
reader, e := c.api.GetObject(bucket, object)
|
||||
if e != nil {
|
||||
errResponse := minio.ToErrorResponse(e)
|
||||
if errResponse.Code == "AccessDenied" {
|
||||
return nil, probe.NewError(PathInsufficientPermission{Path: c.targetURL.String()})
|
||||
}
|
||||
if errResponse.Code == "NoSuchBucket" {
|
||||
return nil, probe.NewError(BucketDoesNotExist{
|
||||
Bucket: bucket,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "InvalidBucketName" {
|
||||
return nil, probe.NewError(BucketInvalid{
|
||||
Bucket: bucket,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "NoSuchKey" || errResponse.Code == "InvalidArgument" {
|
||||
return nil, probe.NewError(ObjectMissing{})
|
||||
}
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// Copy - copy object
|
||||
func (c *s3Client) Copy(source string, size int64, progress io.Reader) *probe.Error {
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
if bucket == "" {
|
||||
return probe.NewError(BucketNameEmpty{})
|
||||
}
|
||||
// Empty copy conditions
|
||||
copyConds := minio.NewCopyConditions()
|
||||
e := c.api.CopyObject(bucket, object, source, copyConds)
|
||||
if e != nil {
|
||||
errResponse := minio.ToErrorResponse(e)
|
||||
if errResponse.Code == "AccessDenied" {
|
||||
return probe.NewError(PathInsufficientPermission{
|
||||
Path: c.targetURL.String(),
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "NoSuchBucket" {
|
||||
return probe.NewError(BucketDoesNotExist{
|
||||
Bucket: bucket,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "InvalidBucketName" {
|
||||
return probe.NewError(BucketInvalid{
|
||||
Bucket: bucket,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "NoSuchKey" || errResponse.Code == "InvalidArgument" {
|
||||
return probe.NewError(ObjectMissing{})
|
||||
}
|
||||
return probe.NewError(e)
|
||||
}
|
||||
// Successful copy update progress bar if there is one.
|
||||
if progress != nil {
|
||||
if _, e := io.CopyN(ioutil.Discard, progress, size); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put - put object.
|
||||
func (c *s3Client) Put(reader io.Reader, size int64, contentType string, progress io.Reader) (int64, *probe.Error) {
|
||||
// md5 is purposefully ignored since AmazonS3 does not return proper md5sum
|
||||
// for a multipart upload and there is no need to cross verify,
|
||||
// invidual parts are properly verified fully in transit and also upon completion
|
||||
// of the multipart request.
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
if bucket == "" {
|
||||
return 0, probe.NewError(BucketNameEmpty{})
|
||||
}
|
||||
n, e := c.api.PutObjectWithProgress(bucket, object, reader, contentType, progress)
|
||||
if e != nil {
|
||||
errResponse := minio.ToErrorResponse(e)
|
||||
if errResponse.Code == "UnexpectedEOF" || e == io.EOF {
|
||||
return n, probe.NewError(UnexpectedEOF{
|
||||
TotalSize: size,
|
||||
TotalWritten: n,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "AccessDenied" {
|
||||
return n, probe.NewError(PathInsufficientPermission{
|
||||
Path: c.targetURL.String(),
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "MethodNotAllowed" {
|
||||
return n, probe.NewError(ObjectAlreadyExists{
|
||||
Object: object,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "XMinioObjectExistsAsDirectory" {
|
||||
return n, probe.NewError(ObjectAlreadyExistsAsDirectory{
|
||||
Object: object,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "NoSuchBucket" {
|
||||
return n, probe.NewError(BucketDoesNotExist{
|
||||
Bucket: bucket,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "InvalidBucketName" {
|
||||
return n, probe.NewError(BucketInvalid{
|
||||
Bucket: bucket,
|
||||
})
|
||||
}
|
||||
if errResponse.Code == "NoSuchKey" || errResponse.Code == "InvalidArgument" {
|
||||
return n, probe.NewError(ObjectMissing{})
|
||||
}
|
||||
return n, probe.NewError(e)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Remove - remove object or bucket.
|
||||
func (c *s3Client) Remove(incomplete bool) *probe.Error {
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
// Remove only incomplete object.
|
||||
if incomplete && object != "" {
|
||||
e := c.api.RemoveIncompleteUpload(bucket, object)
|
||||
return probe.NewError(e)
|
||||
}
|
||||
var e error
|
||||
if object == "" {
|
||||
e = c.api.RemoveBucket(bucket)
|
||||
} else {
|
||||
e = c.api.RemoveObject(bucket, object)
|
||||
}
|
||||
return probe.NewError(e)
|
||||
}
|
||||
|
||||
// We support '.' with bucket names but we fallback to using path
|
||||
// style requests instead for such buckets
|
||||
var validBucketName = regexp.MustCompile(`^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$`)
|
||||
|
||||
// isValidBucketName - verify bucket name in accordance with
|
||||
// - http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html
|
||||
func isValidBucketName(bucketName string) *probe.Error {
|
||||
if strings.TrimSpace(bucketName) == "" {
|
||||
return probe.NewError(errors.New("Bucket name cannot be empty."))
|
||||
}
|
||||
if len(bucketName) < 3 || len(bucketName) > 63 {
|
||||
return probe.NewError(errors.New("Bucket name should be more than 3 characters and less than 64 characters"))
|
||||
}
|
||||
if !validBucketName.MatchString(bucketName) {
|
||||
return probe.NewError(errors.New("Bucket name can contain alphabet, '-' and numbers, but first character should be an alphabet or number"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MakeBucket - make a new bucket.
|
||||
func (c *s3Client) MakeBucket(region string) *probe.Error {
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
if object != "" {
|
||||
return probe.NewError(BucketNameTopLevel{})
|
||||
}
|
||||
if err := isValidBucketName(bucket); err != nil {
|
||||
return err.Trace(bucket)
|
||||
}
|
||||
e := c.api.MakeBucket(bucket, region)
|
||||
if e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAccess get access policy permissions.
|
||||
func (c *s3Client) GetAccess() (string, *probe.Error) {
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
if bucket == "" {
|
||||
return "", probe.NewError(BucketNameEmpty{})
|
||||
}
|
||||
bucketPolicy, e := c.api.GetBucketPolicy(bucket, object)
|
||||
if e != nil {
|
||||
return "", probe.NewError(e)
|
||||
}
|
||||
return string(bucketPolicy), nil
|
||||
}
|
||||
|
||||
// SetAccess set access policy permissions.
|
||||
func (c *s3Client) SetAccess(bucketPolicy string) *probe.Error {
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
if bucket == "" {
|
||||
return probe.NewError(BucketNameEmpty{})
|
||||
}
|
||||
e := c.api.SetBucketPolicy(bucket, object, minio.BucketPolicy(bucketPolicy))
|
||||
if e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// listObjectWrapper - select ObjectList version depending on the target hostname
|
||||
func (c *s3Client) listObjectWrapper(bucket, object string, isRecursive bool, doneCh chan struct{}) <-chan minio.ObjectInfo {
|
||||
if c.targetURL.Host == amazonHostName {
|
||||
return c.api.ListObjectsV2(bucket, object, isRecursive, doneCh)
|
||||
}
|
||||
return c.api.ListObjects(bucket, object, isRecursive, doneCh)
|
||||
}
|
||||
|
||||
// Stat - send a 'HEAD' on a bucket or object to fetch its metadata.
|
||||
func (c *s3Client) Stat() (*clientContent, *probe.Error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
objectMetadata := &clientContent{}
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
// Bucket name cannot be empty, stat on URL has no meaning.
|
||||
if bucket == "" {
|
||||
return nil, probe.NewError(BucketNameEmpty{})
|
||||
} else if object == "" {
|
||||
e := c.api.BucketExists(bucket)
|
||||
if e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
bucketMetadata := &clientContent{}
|
||||
bucketMetadata.URL = *c.targetURL
|
||||
bucketMetadata.Type = os.ModeDir
|
||||
return bucketMetadata, nil
|
||||
}
|
||||
isRecursive := false
|
||||
|
||||
// Remove trailing slashes needed for the following ListObjects call.
|
||||
// In addition, Stat() will be as smart as the client fs version and will
|
||||
// facilitate the work of the upper layers
|
||||
object = strings.TrimRight(object, string(c.targetURL.Separator))
|
||||
|
||||
for objectStat := range c.listObjectWrapper(bucket, object, isRecursive, nil) {
|
||||
if objectStat.Err != nil {
|
||||
return nil, probe.NewError(objectStat.Err)
|
||||
}
|
||||
if objectStat.Key == object {
|
||||
objectMetadata.URL = *c.targetURL
|
||||
objectMetadata.Time = objectStat.LastModified
|
||||
objectMetadata.Size = objectStat.Size
|
||||
objectMetadata.Type = os.FileMode(0664)
|
||||
return objectMetadata, nil
|
||||
}
|
||||
if strings.HasSuffix(objectStat.Key, string(c.targetURL.Separator)) {
|
||||
objectMetadata.URL = *c.targetURL
|
||||
objectMetadata.Type = os.ModeDir
|
||||
return objectMetadata, nil
|
||||
}
|
||||
}
|
||||
return nil, probe.NewError(ObjectMissing{})
|
||||
}
|
||||
|
||||
func isAmazon(host string) bool {
|
||||
matchAmazon, _ := filepath.Match("*.s3*.amazonaws.com", host)
|
||||
return matchAmazon
|
||||
}
|
||||
|
||||
func isGoogle(host string) bool {
|
||||
matchGoogle, _ := filepath.Match("*.storage.googleapis.com", host)
|
||||
return matchGoogle
|
||||
}
|
||||
|
||||
// Figure out if the URL is of 'virtual host' style.
|
||||
// Currently only supported hosts with virtual style are Amazon S3 and Google Cloud Storage.
|
||||
func isVirtualHostStyle(host string) bool {
|
||||
return isAmazon(host) || isGoogle(host)
|
||||
}
|
||||
|
||||
// url2BucketAndObject gives bucketName and objectName from URL path.
|
||||
func (c *s3Client) url2BucketAndObject() (bucketName, objectName string) {
|
||||
path := c.targetURL.Path
|
||||
// Convert any virtual host styled requests.
|
||||
//
|
||||
// For the time being this check is introduced for S3,
|
||||
// If you have custom virtual styled hosts please.
|
||||
// List them below.
|
||||
if c.virtualStyle {
|
||||
var bucket string
|
||||
hostIndex := strings.Index(c.targetURL.Host, "s3")
|
||||
if hostIndex == -1 {
|
||||
hostIndex = strings.Index(c.targetURL.Host, "storage.googleapis")
|
||||
}
|
||||
if hostIndex > 0 {
|
||||
bucket = c.targetURL.Host[:hostIndex-1]
|
||||
path = string(c.targetURL.Separator) + bucket + c.targetURL.Path
|
||||
}
|
||||
}
|
||||
splits := strings.SplitN(path, string(c.targetURL.Separator), 3)
|
||||
switch len(splits) {
|
||||
case 0, 1:
|
||||
bucketName = ""
|
||||
objectName = ""
|
||||
case 2:
|
||||
bucketName = splits[1]
|
||||
objectName = ""
|
||||
case 3:
|
||||
bucketName = splits[1]
|
||||
objectName = splits[2]
|
||||
}
|
||||
return bucketName, objectName
|
||||
}
|
||||
|
||||
/// Bucket API operations.
|
||||
|
||||
// List - list at delimited path, if not recursive.
|
||||
func (c *s3Client) List(recursive, incomplete bool) <-chan *clientContent {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
contentCh := make(chan *clientContent)
|
||||
if incomplete {
|
||||
if recursive {
|
||||
go c.listIncompleteRecursiveInRoutine(contentCh)
|
||||
} else {
|
||||
go c.listIncompleteInRoutine(contentCh)
|
||||
}
|
||||
} else {
|
||||
if recursive {
|
||||
go c.listRecursiveInRoutine(contentCh)
|
||||
} else {
|
||||
go c.listInRoutine(contentCh)
|
||||
}
|
||||
}
|
||||
return contentCh
|
||||
}
|
||||
|
||||
func (c *s3Client) listIncompleteInRoutine(contentCh chan *clientContent) {
|
||||
defer close(contentCh)
|
||||
// get bucket and object from URL.
|
||||
b, o := c.url2BucketAndObject()
|
||||
switch {
|
||||
case b == "" && o == "":
|
||||
buckets, err := c.api.ListBuckets()
|
||||
if err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(err),
|
||||
}
|
||||
return
|
||||
}
|
||||
isRecursive := false
|
||||
for _, bucket := range buckets {
|
||||
for object := range c.api.ListIncompleteUploads(bucket.Name, o, isRecursive, nil) {
|
||||
if object.Err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(object.Err),
|
||||
}
|
||||
return
|
||||
}
|
||||
content := &clientContent{}
|
||||
url := *c.targetURL
|
||||
// Join bucket with - incoming object key.
|
||||
url.Path = filepath.Join(string(url.Separator), bucket.Name, object.Key)
|
||||
if c.virtualStyle {
|
||||
url.Path = filepath.Join(string(url.Separator), object.Key)
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(object.Key, string(c.targetURL.Separator)):
|
||||
// We need to keep the trailing Separator, do not use filepath.Join().
|
||||
content.URL = url
|
||||
content.Time = time.Now()
|
||||
content.Type = os.ModeDir
|
||||
default:
|
||||
content.URL = url
|
||||
content.Size = object.Size
|
||||
content.Time = object.Initiated
|
||||
content.Type = os.ModeTemporary
|
||||
}
|
||||
contentCh <- content
|
||||
}
|
||||
}
|
||||
default:
|
||||
isRecursive := false
|
||||
for object := range c.api.ListIncompleteUploads(b, o, isRecursive, nil) {
|
||||
if object.Err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(object.Err),
|
||||
}
|
||||
return
|
||||
}
|
||||
content := &clientContent{}
|
||||
url := *c.targetURL
|
||||
// Join bucket with - incoming object key.
|
||||
url.Path = filepath.Join(string(url.Separator), b, object.Key)
|
||||
if c.virtualStyle {
|
||||
url.Path = filepath.Join(string(url.Separator), object.Key)
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(object.Key, string(c.targetURL.Separator)):
|
||||
// We need to keep the trailing Separator, do not use filepath.Join().
|
||||
content.URL = url
|
||||
content.Time = time.Now()
|
||||
content.Type = os.ModeDir
|
||||
default:
|
||||
content.URL = url
|
||||
content.Size = object.Size
|
||||
content.Time = object.Initiated
|
||||
content.Type = os.ModeTemporary
|
||||
}
|
||||
contentCh <- content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *s3Client) listIncompleteRecursiveInRoutine(contentCh chan *clientContent) {
|
||||
defer close(contentCh)
|
||||
// get bucket and object from URL.
|
||||
b, o := c.url2BucketAndObject()
|
||||
switch {
|
||||
case b == "" && o == "":
|
||||
buckets, err := c.api.ListBuckets()
|
||||
if err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(err),
|
||||
}
|
||||
return
|
||||
}
|
||||
isRecursive := true
|
||||
for _, bucket := range buckets {
|
||||
for object := range c.api.ListIncompleteUploads(bucket.Name, o, isRecursive, nil) {
|
||||
if object.Err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(object.Err),
|
||||
}
|
||||
return
|
||||
}
|
||||
url := *c.targetURL
|
||||
url.Path = filepath.Join(url.Path, bucket.Name, object.Key)
|
||||
content := &clientContent{}
|
||||
content.URL = url
|
||||
content.Size = object.Size
|
||||
content.Time = object.Initiated
|
||||
content.Type = os.ModeTemporary
|
||||
contentCh <- content
|
||||
}
|
||||
}
|
||||
default:
|
||||
isRecursive := true
|
||||
for object := range c.api.ListIncompleteUploads(b, o, isRecursive, nil) {
|
||||
if object.Err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(object.Err),
|
||||
}
|
||||
return
|
||||
}
|
||||
url := *c.targetURL
|
||||
// Join bucket and incoming object key.
|
||||
url.Path = filepath.Join(string(url.Separator), b, object.Key)
|
||||
if c.virtualStyle {
|
||||
url.Path = filepath.Join(string(url.Separator), object.Key)
|
||||
}
|
||||
content := &clientContent{}
|
||||
content.URL = url
|
||||
content.Size = object.Size
|
||||
content.Time = object.Initiated
|
||||
content.Type = os.ModeTemporary
|
||||
contentCh <- content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *s3Client) listInRoutine(contentCh chan *clientContent) {
|
||||
defer close(contentCh)
|
||||
// get bucket and object from URL.
|
||||
b, o := c.url2BucketAndObject()
|
||||
switch {
|
||||
case b == "" && o == "":
|
||||
buckets, e := c.api.ListBuckets()
|
||||
if e != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(e),
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, bucket := range buckets {
|
||||
url := *c.targetURL
|
||||
url.Path = filepath.Join(url.Path, bucket.Name)
|
||||
content := &clientContent{}
|
||||
content.URL = url
|
||||
content.Size = 0
|
||||
content.Time = bucket.CreationDate
|
||||
content.Type = os.ModeDir
|
||||
contentCh <- content
|
||||
}
|
||||
case b != "" && !strings.HasSuffix(c.targetURL.Path, string(c.targetURL.Separator)) && o == "":
|
||||
buckets, e := c.api.ListBuckets()
|
||||
if e != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(e),
|
||||
}
|
||||
}
|
||||
for _, bucket := range buckets {
|
||||
if bucket.Name == b {
|
||||
content := &clientContent{}
|
||||
content.URL = *c.targetURL
|
||||
content.Size = 0
|
||||
content.Time = bucket.CreationDate
|
||||
content.Type = os.ModeDir
|
||||
contentCh <- content
|
||||
break
|
||||
}
|
||||
}
|
||||
default:
|
||||
isRecursive := false
|
||||
for object := range c.listObjectWrapper(b, o, isRecursive, nil) {
|
||||
if object.Err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(object.Err),
|
||||
}
|
||||
return
|
||||
}
|
||||
content := &clientContent{}
|
||||
url := *c.targetURL
|
||||
// Join bucket and incoming object key.
|
||||
url.Path = filepath.Join(string(url.Separator), b, object.Key)
|
||||
if c.virtualStyle {
|
||||
url.Path = filepath.Join(string(url.Separator), object.Key)
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(object.Key, string(c.targetURL.Separator)):
|
||||
// We need to keep the trailing Separator, do not use filepath.Join().
|
||||
content.URL = url
|
||||
content.Time = time.Now()
|
||||
content.Type = os.ModeDir
|
||||
default:
|
||||
content.URL = url
|
||||
content.Size = object.Size
|
||||
content.Time = object.LastModified
|
||||
content.Type = os.FileMode(0664)
|
||||
}
|
||||
contentCh <- content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// S3 offers a range of storage classes designed for
|
||||
// different use cases, following list captures these.
|
||||
const (
|
||||
// General purpose.
|
||||
// s3StorageClassStandard = "STANDARD"
|
||||
// Infrequent access.
|
||||
// s3StorageClassInfrequent = "STANDARD_IA"
|
||||
// Reduced redundancy access.
|
||||
// s3StorageClassRedundancy = "REDUCED_REDUNDANCY"
|
||||
// Archive access.
|
||||
s3StorageClassGlacier = "GLACIER"
|
||||
)
|
||||
|
||||
func (c *s3Client) listRecursiveInRoutine(contentCh chan *clientContent) {
|
||||
defer close(contentCh)
|
||||
// get bucket and object from URL.
|
||||
b, o := c.url2BucketAndObject()
|
||||
switch {
|
||||
case b == "" && o == "":
|
||||
buckets, err := c.api.ListBuckets()
|
||||
if err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(err),
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, bucket := range buckets {
|
||||
bucketURL := *c.targetURL
|
||||
bucketURL.Path = filepath.Join(bucketURL.Path, bucket.Name)
|
||||
contentCh <- &clientContent{
|
||||
URL: bucketURL,
|
||||
Type: os.ModeDir,
|
||||
Time: bucket.CreationDate,
|
||||
}
|
||||
isRecursive := true
|
||||
for object := range c.listObjectWrapper(bucket.Name, o, isRecursive, nil) {
|
||||
// Return error if we encountered glacier object and continue.
|
||||
if object.StorageClass == s3StorageClassGlacier {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(ObjectOnGlacier{object.Key}),
|
||||
}
|
||||
continue
|
||||
}
|
||||
if object.Err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(object.Err),
|
||||
}
|
||||
continue
|
||||
}
|
||||
content := &clientContent{}
|
||||
objectURL := *c.targetURL
|
||||
objectURL.Path = filepath.Join(objectURL.Path, bucket.Name, object.Key)
|
||||
content.URL = objectURL
|
||||
content.Size = object.Size
|
||||
content.Time = object.LastModified
|
||||
content.Type = os.FileMode(0664)
|
||||
contentCh <- content
|
||||
}
|
||||
}
|
||||
default:
|
||||
isRecursive := true
|
||||
for object := range c.listObjectWrapper(b, o, isRecursive, nil) {
|
||||
if object.Err != nil {
|
||||
contentCh <- &clientContent{
|
||||
Err: probe.NewError(object.Err),
|
||||
}
|
||||
continue
|
||||
}
|
||||
content := &clientContent{}
|
||||
url := *c.targetURL
|
||||
// Join bucket and incoming object key.
|
||||
url.Path = filepath.Join(string(url.Separator), b, object.Key)
|
||||
// If virtualStyle replace the url.Path back.
|
||||
if c.virtualStyle {
|
||||
url.Path = filepath.Join(string(url.Separator), object.Key)
|
||||
}
|
||||
content.URL = url
|
||||
content.Size = object.Size
|
||||
content.Time = object.LastModified
|
||||
content.Type = os.FileMode(0664)
|
||||
contentCh <- content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ShareDownload - get a usable presigned object url to share.
|
||||
func (c *s3Client) ShareDownload(expires time.Duration) (string, *probe.Error) {
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
// No additional request parameters are set for the time being.
|
||||
reqParams := make(url.Values)
|
||||
presignedURL, e := c.api.PresignedGetObject(bucket, object, expires, reqParams)
|
||||
if e != nil {
|
||||
return "", probe.NewError(e)
|
||||
}
|
||||
return presignedURL.String(), nil
|
||||
}
|
||||
|
||||
// ShareUpload - get data for presigned post http form upload.
|
||||
func (c *s3Client) ShareUpload(isRecursive bool, expires time.Duration, contentType string) (map[string]string, *probe.Error) {
|
||||
bucket, object := c.url2BucketAndObject()
|
||||
p := minio.NewPostPolicy()
|
||||
if e := p.SetExpires(time.Now().UTC().Add(expires)); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
if strings.TrimSpace(contentType) != "" || contentType != "" {
|
||||
// No need to verify for error here, since we have stripped out spaces.
|
||||
p.SetContentType(contentType)
|
||||
}
|
||||
if e := p.SetBucket(bucket); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
if isRecursive {
|
||||
if e := p.SetKeyStartsWith(object); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
} else {
|
||||
if e := p.SetKey(object); e != nil {
|
||||
return nil, probe.NewError(e)
|
||||
}
|
||||
}
|
||||
_, m, e := c.api.PresignedPostPolicy(p)
|
||||
return m, probe.NewError(e)
|
||||
}
|
||||
238
command/client-s3_test.go
Normal file
238
command/client-s3_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
// bucketHandler is an http.Handler that verifies bucket responses and validates incoming requests
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
type bucketHandler struct {
|
||||
resource string
|
||||
}
|
||||
|
||||
func (h bucketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "GET":
|
||||
// Handler for incoming getBucketLocation request.
|
||||
if _, ok := r.URL.Query()["location"]; ok {
|
||||
response := []byte("<LocationConstraint xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"></LocationConstraint>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case r.URL.Path == "/":
|
||||
// Handler for incoming ListBuckets request.
|
||||
response := []byte("<ListAllMyBucketsResult xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"><Buckets><Bucket><Name>bucket</Name><CreationDate>2015-05-20T23:05:09.230Z</CreationDate></Bucket></Buckets><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner></ListAllMyBucketsResult>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
case r.URL.Path == "/bucket/":
|
||||
// Handler for incoming ListObjects request.
|
||||
response := []byte("<ListBucketResult xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"><Contents><ETag>259d04a13802ae09c7e41be50ccc6baa</ETag><Key>object</Key><LastModified>2015-05-21T18:24:21.097Z</LastModified><Size>22061</Size><Owner><ID>minio</ID><DisplayName>minio</DisplayName></Owner><StorageClass>STANDARD</StorageClass></Contents><Delimiter></Delimiter><EncodingType></EncodingType><IsTruncated>false</IsTruncated><Marker></Marker><MaxKeys>1000</MaxKeys><Name>testbucket</Name><NextMarker></NextMarker><Prefix></Prefix></ListBucketResult>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
}
|
||||
case r.Method == "PUT":
|
||||
switch {
|
||||
case r.URL.Path == h.resource:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
case r.Method == "HEAD":
|
||||
switch {
|
||||
case r.URL.Path == h.resource:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// objectHandler is an http.Handler that verifies object responses and validates incoming requests
|
||||
type objectHandler struct {
|
||||
resource string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (h objectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == "PUT":
|
||||
// Handler for PUT object request.
|
||||
length, e := strconv.Atoi(r.Header.Get("Content-Length"))
|
||||
if e != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
if _, e = io.CopyN(&buffer, r.Body, int64(length)); e != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(h.data, buffer.Bytes()) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
case r.Method == "HEAD":
|
||||
// Handler for Stat object request.
|
||||
if r.URL.Path != h.resource {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(h.data)))
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
case r.Method == "POST":
|
||||
// Handler for multipart upload request.
|
||||
if _, ok := r.URL.Query()["uploads"]; ok {
|
||||
if r.URL.Path == h.resource {
|
||||
response := []byte("<InitiateMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Bucket>bucket</Bucket><Key>object</Key><UploadId>EXAMPLEJZ6e0YupT2h66iePQCc9IEbYbDUy4RTpMeoSMLPRp8Z5o1u8feSRonpvnWsKKG35tI2LB9VDPiCgTy.Gq2VxQLYjrue4Nq.NBdqI-</UploadId></InitiateMultipartUploadResult>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
return
|
||||
}
|
||||
}
|
||||
if _, ok := r.URL.Query()["uploadId"]; ok {
|
||||
if r.URL.Path == h.resource {
|
||||
response := []byte("<CompleteMultipartUploadResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Location>http://bucket.s3.amazonaws.com/object</Location><Bucket>bucket</Bucket><Key>object</Key><ETag>\"3858f62230ac3c915f300c664312c11f-9\"</ETag></CompleteMultipartUploadResult>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.URL.Path != h.resource {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
case r.Method == "GET":
|
||||
// Handler for get bucket location request.
|
||||
if _, ok := r.URL.Query()["location"]; ok {
|
||||
response := []byte("<LocationConstraint xmlns=\"http://doc.s3.amazonaws.com/2006-03-01\"></LocationConstraint>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
return
|
||||
}
|
||||
// Handler for list multipart upload request.
|
||||
if _, ok := r.URL.Query()["uploads"]; ok {
|
||||
if r.URL.Path == "/bucket/" {
|
||||
response := []byte("<ListMultipartUploadsResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><Bucket>bucket</Bucket><KeyMarker/><UploadIdMarker/><NextKeyMarker/><NextUploadIdMarker/><EncodingType/><MaxUploads>1000</MaxUploads><IsTruncated>false</IsTruncated><Prefix/><Delimiter/></ListMultipartUploadsResult>")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(response)))
|
||||
w.Write(response)
|
||||
return
|
||||
}
|
||||
}
|
||||
if r.URL.Path != h.resource {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(h.data)))
|
||||
w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
|
||||
w.Header().Set("ETag", "9af2f8218b150c351ad802c6f3d66abe")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.Copy(w, bytes.NewReader(h.data))
|
||||
}
|
||||
}
|
||||
|
||||
// Test bucket operations.
|
||||
func (s *TestSuite) TestBucketOperations(c *C) {
|
||||
bucket := bucketHandler(bucketHandler{
|
||||
resource: "/bucket/",
|
||||
})
|
||||
server := httptest.NewServer(bucket)
|
||||
defer server.Close()
|
||||
|
||||
conf := new(Config)
|
||||
conf.HostURL = server.URL + bucket.resource
|
||||
conf.AccessKey = "WLGDGYAQYIGI833EV05A"
|
||||
conf.SecretKey = "BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF"
|
||||
conf.Signature = "S3v4"
|
||||
s3c, err := s3New(conf)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = s3c.MakeBucket("us-east-1")
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
conf.HostURL = server.URL + string(s3c.GetURL().Separator)
|
||||
s3c, err = s3New(conf)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
for content := range s3c.List(false, false) {
|
||||
c.Assert(content.Err, IsNil)
|
||||
c.Assert(content.Type.IsDir(), Equals, true)
|
||||
}
|
||||
|
||||
conf.HostURL = server.URL + "/bucket"
|
||||
s3c, err = s3New(conf)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
for content := range s3c.List(false, false) {
|
||||
c.Assert(content.Err, IsNil)
|
||||
c.Assert(content.Type.IsDir(), Equals, true)
|
||||
}
|
||||
|
||||
conf.HostURL = server.URL + "/bucket/"
|
||||
s3c, err = s3New(conf)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
for content := range s3c.List(false, false) {
|
||||
c.Assert(content.Err, IsNil)
|
||||
c.Assert(content.Type.IsRegular(), Equals, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Test all object operations.
|
||||
func (s *TestSuite) TestObjectOperations(c *C) {
|
||||
object := objectHandler(objectHandler{
|
||||
resource: "/bucket/object",
|
||||
data: []byte("Hello, World"),
|
||||
})
|
||||
server := httptest.NewServer(object)
|
||||
defer server.Close()
|
||||
|
||||
conf := new(Config)
|
||||
conf.HostURL = server.URL + object.resource
|
||||
conf.AccessKey = "WLGDGYAQYIGI833EV05A"
|
||||
conf.SecretKey = "BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF"
|
||||
conf.Signature = "S3v4"
|
||||
s3c, err := s3New(conf)
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
var reader io.Reader
|
||||
reader = bytes.NewReader(object.data)
|
||||
n, err := s3c.Put(reader, int64(len(object.data)), "application/octet-stream", nil)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(n, Equals, int64(len(object.data)))
|
||||
|
||||
reader, err = s3c.Get()
|
||||
var buffer bytes.Buffer
|
||||
{
|
||||
_, err := io.Copy(&buffer, reader)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(buffer.Bytes(), DeepEquals, object.data)
|
||||
}
|
||||
}
|
||||
239
command/client-url.go
Normal file
239
command/client-url.go
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// url client url structure
|
||||
type clientURL struct {
|
||||
Type clientURLType
|
||||
Scheme string
|
||||
Host string
|
||||
Path string
|
||||
SchemeSeparator string
|
||||
Separator rune
|
||||
}
|
||||
|
||||
// clientURLType - enum of different url types
|
||||
type clientURLType int
|
||||
|
||||
// enum types
|
||||
const (
|
||||
objectStorage = iota // Minio and S3 compatible cloud storage
|
||||
fileSystem // POSIX compatible file systems
|
||||
)
|
||||
|
||||
// Maybe rawurl is of the form scheme:path. (Scheme must be [a-zA-Z][a-zA-Z0-9+-.]*)
|
||||
// If so, return scheme, path; else return "", rawurl.
|
||||
func getScheme(rawurl string) (scheme, path string) {
|
||||
urlSplits := strings.Split(rawurl, "://")
|
||||
if len(urlSplits) == 2 {
|
||||
scheme, uri := urlSplits[0], "//"+urlSplits[1]
|
||||
// ignore numbers in scheme
|
||||
validScheme := regexp.MustCompile("^[a-zA-Z]+$")
|
||||
if uri != "" {
|
||||
if validScheme.MatchString(scheme) {
|
||||
return scheme, uri
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", rawurl
|
||||
}
|
||||
|
||||
// Assuming s is of the form [s delimiter s].
|
||||
// If so, return s, [delimiter]s or return s, s if cutdelimiter == true
|
||||
// If no delimiter found return s, "".
|
||||
func splitSpecial(s string, delimiter string, cutdelimiter bool) (string, string) {
|
||||
i := strings.Index(s, delimiter)
|
||||
if i < 0 {
|
||||
// if delimiter not found return as is.
|
||||
return s, ""
|
||||
}
|
||||
// if delimiter should be removed, remove it.
|
||||
if cutdelimiter {
|
||||
return s[0:i], s[i+len(delimiter):]
|
||||
}
|
||||
// return split strings with delimiter
|
||||
return s[0:i], s[i:]
|
||||
}
|
||||
|
||||
// getHost - extract host from authority string, we do not support ftp style username@ yet.
|
||||
func getHost(authority string) (host string) {
|
||||
i := strings.LastIndex(authority, "@")
|
||||
if i >= 0 {
|
||||
// TODO support, username@password style userinfo, useful for ftp support.
|
||||
return
|
||||
}
|
||||
return authority
|
||||
}
|
||||
|
||||
// newClientURL returns an abstracted URL for filesystems and object storage.
|
||||
func newClientURL(urlStr string) *clientURL {
|
||||
scheme, rest := getScheme(urlStr)
|
||||
if strings.HasPrefix(rest, "//") {
|
||||
// if rest has '//' prefix, skip them
|
||||
var authority string
|
||||
authority, rest = splitSpecial(rest[2:], "/", false)
|
||||
if rest == "" {
|
||||
rest = "/"
|
||||
}
|
||||
host := getHost(authority)
|
||||
if host != "" && (scheme == "http" || scheme == "https") {
|
||||
return &clientURL{
|
||||
Scheme: scheme,
|
||||
Type: objectStorage,
|
||||
Host: host,
|
||||
Path: rest,
|
||||
SchemeSeparator: "://",
|
||||
Separator: '/',
|
||||
}
|
||||
}
|
||||
}
|
||||
return &clientURL{
|
||||
Type: fileSystem,
|
||||
Path: rest,
|
||||
Separator: filepath.Separator,
|
||||
}
|
||||
}
|
||||
|
||||
// joinURLs join two input urls and returns a url
|
||||
func joinURLs(url1, url2 *clientURL) *clientURL {
|
||||
var url1Path, url2Path string
|
||||
url1Path = filepath.ToSlash(url1.Path)
|
||||
url2Path = filepath.ToSlash(url2.Path)
|
||||
if strings.HasSuffix(url1Path, "/") {
|
||||
url1.Path = url1Path + strings.TrimPrefix(url2Path, "/")
|
||||
} else {
|
||||
url1.Path = url1Path + "/" + strings.TrimPrefix(url2Path, "/")
|
||||
}
|
||||
return url1
|
||||
}
|
||||
|
||||
// String convert URL into its canonical form.
|
||||
func (u clientURL) String() string {
|
||||
var buf bytes.Buffer
|
||||
// if fileSystem no translation needed, return as is.
|
||||
if u.Type == fileSystem {
|
||||
return u.Path
|
||||
}
|
||||
// if objectStorage convert from any non standard paths to a supported URL path style.
|
||||
if u.Type == objectStorage {
|
||||
buf.WriteString(u.Scheme)
|
||||
buf.WriteByte(':')
|
||||
buf.WriteString("//")
|
||||
if h := u.Host; h != "" {
|
||||
buf.WriteString(h)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if u.Path != "" && u.Path[0] != '\\' && u.Host != "" && u.Path[0] != '/' {
|
||||
buf.WriteByte('/')
|
||||
}
|
||||
buf.WriteString(strings.Replace(u.Path, "\\", "/", -1))
|
||||
default:
|
||||
if u.Path != "" && u.Path[0] != '/' && u.Host != "" {
|
||||
buf.WriteByte('/')
|
||||
}
|
||||
buf.WriteString(u.Path)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func isURLVirtualHostStyle(hostURL string) bool {
|
||||
matchS3, _ := filepath.Match("*.s3*.amazonaws.com", hostURL)
|
||||
matchGoogle, _ := filepath.Match("*.storage.googleapis.com", hostURL)
|
||||
return matchS3 || matchGoogle
|
||||
}
|
||||
|
||||
// urlJoinPath Join a path to existing URL.
|
||||
func urlJoinPath(url1, url2 string) string {
|
||||
u1 := newClientURL(url1)
|
||||
u2 := newClientURL(url2)
|
||||
return joinURLs(u1, u2).String()
|
||||
}
|
||||
|
||||
// url2Stat returns stat info for URL.
|
||||
func url2Stat(urlStr string) (client Client, content *clientContent, err *probe.Error) {
|
||||
client, err = newClient(urlStr)
|
||||
if err != nil {
|
||||
return nil, nil, err.Trace(urlStr)
|
||||
}
|
||||
content, err = client.Stat()
|
||||
if err != nil {
|
||||
return nil, nil, err.Trace(urlStr)
|
||||
}
|
||||
return client, content, nil
|
||||
}
|
||||
|
||||
// url2Alias separates alias and path from the URL. Aliased URL is of
|
||||
// the form alias/path/to/blah.
|
||||
func url2Alias(aliasedURL string) (alias, path string) {
|
||||
// Save aliased url.
|
||||
urlStr := aliasedURL
|
||||
|
||||
// Convert '/' on windows to filepath.Separator.
|
||||
urlStr = filepath.FromSlash(urlStr)
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Remove '/' prefix before alias if any to support '\\home' alias
|
||||
// style under Windows
|
||||
urlStr = strings.TrimPrefix(urlStr, string(filepath.Separator))
|
||||
}
|
||||
|
||||
// Remove everything after alias (i.e. after '/').
|
||||
urlParts := strings.SplitN(urlStr, string(filepath.Separator), 2)
|
||||
if len(urlParts) == 2 {
|
||||
// Convert windows style path separator to Unix style.
|
||||
return urlParts[0], urlParts[1]
|
||||
}
|
||||
return urlParts[0], ""
|
||||
}
|
||||
|
||||
// isURLPrefixExists - check if object key prefix exists.
|
||||
func isURLPrefixExists(urlPrefix string, incomplete bool) bool {
|
||||
clnt, err := newClient(urlPrefix)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
isRecursive := false
|
||||
isIncomplete := incomplete
|
||||
for entry := range clnt.List(isRecursive, isIncomplete) {
|
||||
return entry.Err == nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// guessURLContentType - guess content-type of the URL.
|
||||
// on failure just return 'application/octet-stream'.
|
||||
func guessURLContentType(urlStr string) string {
|
||||
url := newClientURL(urlStr)
|
||||
contentType := mime.TypeByExtension(filepath.Ext(url.Path))
|
||||
if contentType == "" {
|
||||
contentType = "application/octet-stream"
|
||||
}
|
||||
return contentType
|
||||
}
|
||||
53
command/client-url_test.go
Normal file
53
command/client-url_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import . "gopkg.in/check.v1"
|
||||
|
||||
// TestURL - tests url parsing and fields.
|
||||
func (s *TestSuite) TestURL(c *C) {
|
||||
urlStr := "foo?.go"
|
||||
url := newClientURL(urlStr)
|
||||
c.Assert(url.Path, Equals, "foo?.go")
|
||||
|
||||
urlStr = "https://s3.amazonaws.com/mybucket/foo?.go"
|
||||
url = newClientURL(urlStr)
|
||||
c.Assert(url.Scheme, Equals, "https")
|
||||
c.Assert(url.Host, Equals, "s3.amazonaws.com")
|
||||
c.Assert(url.Path, Equals, "/mybucket/foo?.go")
|
||||
}
|
||||
|
||||
// TestURLJoinPath - tests joining two different urls.
|
||||
func (s *TestSuite) TestURLJoinPath(c *C) {
|
||||
// Join two URLs
|
||||
url1 := "http://s3.mycompany.io/dev"
|
||||
url2 := "http://s3.aws.amazon.com/mybucket/bin/zgrep"
|
||||
url := urlJoinPath(url1, url2)
|
||||
c.Assert(url, Equals, "http://s3.mycompany.io/dev/mybucket/bin/zgrep")
|
||||
|
||||
// Join URL and a path
|
||||
url1 = "http://s3.mycompany.io/dev"
|
||||
url2 = "mybucket/bin/zgrep"
|
||||
url = urlJoinPath(url1, url2)
|
||||
c.Assert(url, Equals, "http://s3.mycompany.io/dev/mybucket/bin/zgrep")
|
||||
|
||||
// Check if it strips URL2's tailing ‘/’
|
||||
url1 = "http://s3.mycompany.io/dev"
|
||||
url2 = "mybucket/bin/"
|
||||
url = urlJoinPath(url1, url2)
|
||||
c.Assert(url, Equals, "http://s3.mycompany.io/dev/mybucket/bin/")
|
||||
}
|
||||
75
command/client.go
Normal file
75
command/client.go
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// Client - client interface
|
||||
type Client interface {
|
||||
// Common operations
|
||||
Stat() (content *clientContent, err *probe.Error)
|
||||
List(recursive, incomplete bool) <-chan *clientContent
|
||||
|
||||
// Bucket operations
|
||||
MakeBucket(region string) *probe.Error
|
||||
|
||||
// Access policy operations.
|
||||
GetAccess() (access string, error *probe.Error)
|
||||
SetAccess(access string) *probe.Error
|
||||
|
||||
// I/O operations
|
||||
Get() (reader io.Reader, err *probe.Error)
|
||||
Put(reader io.Reader, size int64, contentType string, progress io.Reader) (n int64, err *probe.Error)
|
||||
Copy(source string, size int64, progress io.Reader) *probe.Error
|
||||
|
||||
// I/O operations with expiration
|
||||
ShareDownload(expires time.Duration) (string, *probe.Error)
|
||||
ShareUpload(bool, time.Duration, string) (map[string]string, *probe.Error)
|
||||
|
||||
// Delete operations
|
||||
Remove(incomplete bool) *probe.Error
|
||||
|
||||
// GetURL returns back internal url
|
||||
GetURL() clientURL
|
||||
}
|
||||
|
||||
// Content container for content metadata
|
||||
type clientContent struct {
|
||||
URL clientURL
|
||||
Time time.Time
|
||||
Size int64
|
||||
Type os.FileMode
|
||||
Err *probe.Error
|
||||
}
|
||||
|
||||
// Config - see http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?RESTAuthentication.html
|
||||
type Config struct {
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Signature string
|
||||
HostURL string
|
||||
AppName string
|
||||
AppVersion string
|
||||
AppComments []string
|
||||
Debug bool
|
||||
}
|
||||
185
command/common-methods.go
Normal file
185
command/common-methods.go
Normal file
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// Check if the target URL represents folder. It may or may not exist yet.
|
||||
func isTargetURLDir(targetURL string) bool {
|
||||
targetURLParse := newClientURL(targetURL)
|
||||
_, targetContent, err := url2Stat(targetURL)
|
||||
if err != nil {
|
||||
_, aliasedTargetURL, _ := mustExpandAlias(targetURL)
|
||||
if aliasedTargetURL == targetURL {
|
||||
return false
|
||||
}
|
||||
if targetURLParse.Path == string(targetURLParse.Separator) && targetURLParse.Scheme != "" {
|
||||
return false
|
||||
}
|
||||
if strings.HasSuffix(targetURLParse.Path, string(targetURLParse.Separator)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if !targetContent.Type.IsDir() { // Target is a dir.
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getSource gets a reader from URL.
|
||||
func getSourceStream(urlStr string) (reader io.Reader, err *probe.Error) {
|
||||
alias, urlStrFull, _, err := expandAlias(urlStr)
|
||||
if err != nil {
|
||||
return nil, err.Trace(urlStr)
|
||||
}
|
||||
return getSourceStreamFromAlias(alias, urlStrFull)
|
||||
}
|
||||
|
||||
// getSourceStreamFromAlias gets a reader from URL.
|
||||
func getSourceStreamFromAlias(alias string, urlStr string) (reader io.Reader, err *probe.Error) {
|
||||
sourceClnt, err := newClientFromAlias(alias, urlStr)
|
||||
if err != nil {
|
||||
return nil, err.Trace(alias, urlStr)
|
||||
}
|
||||
reader, err = sourceClnt.Get()
|
||||
if err != nil {
|
||||
return nil, err.Trace(alias, urlStr)
|
||||
}
|
||||
return reader, nil
|
||||
}
|
||||
|
||||
// putTargetStreamFromAlias writes to URL from Reader.
|
||||
func putTargetStreamFromAlias(alias string, urlStr string, reader io.Reader, size int64, progress io.Reader) (int64, *probe.Error) {
|
||||
targetClnt, err := newClientFromAlias(alias, urlStr)
|
||||
if err != nil {
|
||||
return 0, err.Trace(alias, urlStr)
|
||||
}
|
||||
contentType := guessURLContentType(urlStr)
|
||||
var n int64
|
||||
n, err = targetClnt.Put(reader, size, contentType, progress)
|
||||
if err != nil {
|
||||
return n, err.Trace(alias, urlStr)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// putTargetStream writes to URL from reader. If length=-1, read until EOF.
|
||||
func putTargetStream(urlStr string, reader io.Reader, size int64) (int64, *probe.Error) {
|
||||
alias, urlStrFull, _, err := expandAlias(urlStr)
|
||||
if err != nil {
|
||||
return 0, err.Trace(alias, urlStr)
|
||||
}
|
||||
return putTargetStreamFromAlias(alias, urlStrFull, reader, size, nil)
|
||||
}
|
||||
|
||||
// copyTargetStreamFromAlias copies to URL from source.
|
||||
func copySourceStreamFromAlias(alias string, urlStr string, source string, size int64, progress io.Reader) *probe.Error {
|
||||
targetClnt, err := newClientFromAlias(alias, urlStr)
|
||||
if err != nil {
|
||||
return err.Trace(alias, urlStr)
|
||||
}
|
||||
err = targetClnt.Copy(source, size, progress)
|
||||
if err != nil {
|
||||
return err.Trace(alias, urlStr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newClientFromAlias gives a new client interface for matching
|
||||
// alias entry in the mc config file. If no matching host config entry
|
||||
// is found, fs client is returned.
|
||||
func newClientFromAlias(alias string, urlStr string) (Client, *probe.Error) {
|
||||
hostCfg := mustGetHostConfig(alias)
|
||||
if hostCfg == nil {
|
||||
// No matching host config. So we treat it like a
|
||||
// filesystem.
|
||||
fsClient, err := fsNew(urlStr)
|
||||
if err != nil {
|
||||
return nil, err.Trace(alias, urlStr)
|
||||
}
|
||||
return fsClient, nil
|
||||
}
|
||||
|
||||
// We have a valid alias and hostConfig. We populate the
|
||||
// credentials from the match found in the config file.
|
||||
s3Config := new(Config)
|
||||
|
||||
// secretKey retrieved from the environement overrides the one
|
||||
// present in the config file
|
||||
keysPairEnv := os.Getenv("MC_SECRET_" + alias)
|
||||
keysPairArray := strings.Split(keysPairEnv, ":")
|
||||
var accessKeyEnv, secretKeyEnv string
|
||||
if len(keysPairArray) >= 1 {
|
||||
accessKeyEnv = keysPairArray[0]
|
||||
}
|
||||
if len(keysPairArray) >= 2 {
|
||||
secretKeyEnv = keysPairArray[1]
|
||||
}
|
||||
if len(keysPairEnv) > 0 &&
|
||||
isValidAccessKey(accessKeyEnv) && isValidSecretKey(secretKeyEnv) {
|
||||
s3Config.AccessKey = accessKeyEnv
|
||||
s3Config.SecretKey = secretKeyEnv
|
||||
} else {
|
||||
if len(keysPairEnv) > 0 {
|
||||
console.Errorln("Access/Secret keys associated to `" + alias + "' " +
|
||||
"are found in your environment but not suitable for use. " +
|
||||
"Falling back to the standard config.")
|
||||
}
|
||||
s3Config.AccessKey = hostCfg.AccessKey
|
||||
s3Config.SecretKey = hostCfg.SecretKey
|
||||
}
|
||||
|
||||
s3Config.Signature = hostCfg.API
|
||||
s3Config.AppName = "mc"
|
||||
s3Config.AppVersion = mcVersion
|
||||
s3Config.AppComments = []string{os.Args[0], runtime.GOOS, runtime.GOARCH}
|
||||
s3Config.HostURL = urlStr
|
||||
s3Config.Debug = globalDebug
|
||||
|
||||
s3Client, err := s3New(s3Config)
|
||||
if err != nil {
|
||||
return nil, err.Trace(alias, urlStr)
|
||||
}
|
||||
return s3Client, nil
|
||||
}
|
||||
|
||||
// urlRgx - verify if aliased url is real URL.
|
||||
var urlRgx = regexp.MustCompile("^https?://")
|
||||
|
||||
// newClient gives a new client interface
|
||||
func newClient(aliasedURL string) (Client, *probe.Error) {
|
||||
alias, urlStrFull, hostCfg, err := expandAlias(aliasedURL)
|
||||
if err != nil {
|
||||
return nil, err.Trace(aliasedURL)
|
||||
}
|
||||
// Verify if the aliasedURL is a real URL, fail in those cases
|
||||
// indicating the user to add alias.
|
||||
if hostCfg == nil && urlRgx.MatchString(aliasedURL) {
|
||||
return nil, errInvalidAliasedURL(aliasedURL).Trace(aliasedURL)
|
||||
}
|
||||
return newClientFromAlias(alias, urlStrFull)
|
||||
}
|
||||
241
command/config-fix.go
Normal file
241
command/config-fix.go
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
func fixConfig() {
|
||||
// Fix config V3
|
||||
fixConfigV3()
|
||||
// Fix config V6
|
||||
fixConfigV6()
|
||||
// Fix config V6 for hosts
|
||||
fixConfigV6ForHosts()
|
||||
|
||||
/* No more fixing job. Here after we bump the version for changes always.
|
||||
*/
|
||||
}
|
||||
|
||||
/////////////////// Broken Config V3 ///////////////////
|
||||
type brokenHostConfigV3 struct {
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
}
|
||||
|
||||
type brokenConfigV3 struct {
|
||||
Version string
|
||||
ACL string
|
||||
Access string
|
||||
Aliases map[string]string
|
||||
Hosts map[string]brokenHostConfigV3
|
||||
}
|
||||
|
||||
// newConfigV3 - get new config broken version 3.
|
||||
func newBrokenConfigV3() *brokenConfigV3 {
|
||||
conf := new(brokenConfigV3)
|
||||
conf.Version = "3"
|
||||
conf.Aliases = make(map[string]string)
|
||||
conf.Hosts = make(map[string]brokenHostConfigV3)
|
||||
return conf
|
||||
}
|
||||
|
||||
// Fix config version ‘3’. Some v3 config files are written without
|
||||
// proper hostConfig JSON tags. They may also contain unused ACL and
|
||||
// Access fields. Rewrite the hostConfig with proper fields using JSON
|
||||
// tags and drop the unused (ACL, Access) fields.
|
||||
func fixConfigV3() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
brokenCfgV3 := newBrokenConfigV3()
|
||||
brokenMcCfgV3, err := quick.Load(mustGetMcConfigPath(), brokenCfgV3)
|
||||
fatalIf(err.Trace(), "Unable to load config.")
|
||||
|
||||
if brokenMcCfgV3.Version() != "3" {
|
||||
return
|
||||
}
|
||||
|
||||
cfgV3 := newConfigV3()
|
||||
isMutated := false
|
||||
for k, v := range brokenMcCfgV3.Data().(*brokenConfigV3).Aliases {
|
||||
cfgV3.Aliases[k] = v
|
||||
}
|
||||
|
||||
for host, brokenHostCfgV3 := range brokenMcCfgV3.Data().(*brokenConfigV3).Hosts {
|
||||
|
||||
// If any of these fields contains any real value anytime,
|
||||
// it means we have already fixed the broken configuration.
|
||||
// We don't have to regenerate again.
|
||||
if brokenHostCfgV3.AccessKeyID != "" && brokenHostCfgV3.SecretAccessKey != "" {
|
||||
isMutated = true
|
||||
}
|
||||
|
||||
// Use the correct hostConfig with JSON tags in it.
|
||||
cfgV3.Hosts[host] = hostConfigV3{
|
||||
AccessKeyID: brokenHostCfgV3.AccessKeyID,
|
||||
SecretAccessKey: brokenHostCfgV3.SecretAccessKey,
|
||||
}
|
||||
}
|
||||
|
||||
// We blindly drop ACL and Access fields from the broken config v3.
|
||||
|
||||
if isMutated {
|
||||
mcNewConfigV3, err := quick.New(cfgV3)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘3’.")
|
||||
|
||||
err = mcNewConfigV3.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘3’.")
|
||||
|
||||
console.Infof("Successfully fixed %s broken config for version ‘3’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
}
|
||||
|
||||
// If the host key does not have http(s), fix it.
|
||||
func fixConfigV6ForHosts() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
|
||||
brokenMcCfgV6, err := quick.Load(mustGetMcConfigPath(), newConfigV6())
|
||||
fatalIf(err.Trace(), "Unable to load config.")
|
||||
|
||||
if brokenMcCfgV6.Version() != "6" {
|
||||
return
|
||||
}
|
||||
|
||||
newCfgV6 := newConfigV6()
|
||||
isMutated := false
|
||||
|
||||
// Copy aliases.
|
||||
for k, v := range brokenMcCfgV6.Data().(*configV6).Aliases {
|
||||
newCfgV6.Aliases[k] = v
|
||||
}
|
||||
|
||||
url := &clientURL{}
|
||||
// Copy hosts.
|
||||
for host, hostCfgV6 := range brokenMcCfgV6.Data().(*configV6).Hosts {
|
||||
// Already fixed - Copy and move on.
|
||||
if strings.HasPrefix(host, "https") || strings.HasPrefix(host, "http") {
|
||||
newCfgV6.Hosts[host] = hostCfgV6
|
||||
continue
|
||||
}
|
||||
|
||||
// If host entry does not contain "http(s)", introduce a new entry and delete the old one.
|
||||
if host == "s3.amazonaws.com" || host == "storage.googleapis.com" ||
|
||||
host == "localhost:9000" || host == "127.0.0.1:9000" ||
|
||||
host == "play.minio.io:9000" || host == "dl.minio.io:9000" {
|
||||
console.Infoln("Found broken host entries, replacing " + host + " with https://" + host + ".")
|
||||
url.Host = host
|
||||
url.Scheme = "https"
|
||||
url.SchemeSeparator = "://"
|
||||
newCfgV6.Hosts[url.String()] = hostCfgV6
|
||||
isMutated = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if isMutated {
|
||||
// Save the new config back to the disk.
|
||||
mcCfgV6, err := quick.New(newCfgV6)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘v6’.")
|
||||
|
||||
err = mcCfgV6.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘v6’.")
|
||||
}
|
||||
}
|
||||
|
||||
// fixConfigV6 - fix all the unnecessary glob URLs present in existing config version 6.
|
||||
func fixConfigV6() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
config, err := quick.New(newConfigV6())
|
||||
fatalIf(err.Trace(), "Unable to initialize config.")
|
||||
|
||||
err = config.Load(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(mustGetMcConfigPath()), "Unable to load config.")
|
||||
|
||||
if config.Data().(*configV6).Version != "6" {
|
||||
return
|
||||
}
|
||||
|
||||
newConfig := new(configV6)
|
||||
isMutated := false
|
||||
newConfig.Aliases = make(map[string]string)
|
||||
newConfig.Hosts = make(map[string]hostConfigV6)
|
||||
newConfig.Version = "6"
|
||||
newConfig.Aliases = config.Data().(*configV6).Aliases
|
||||
for host, hostCfg := range config.Data().(*configV6).Hosts {
|
||||
if strings.Contains(host, "*") {
|
||||
fatalIf(err.Trace(),
|
||||
fmt.Sprintf("Glob style ‘*’ pattern matching is no longer supported. Please fix ‘%s’ entry manually.", host))
|
||||
}
|
||||
if strings.Contains(host, "*s3*") || strings.Contains(host, "*.s3*") {
|
||||
console.Infoln("Found glob url, replacing " + host + " with s3.amazonaws.com")
|
||||
newConfig.Hosts["s3.amazonaws.com"] = hostCfg
|
||||
isMutated = true
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "s3*") {
|
||||
console.Infoln("Found glob url, replacing " + host + " with s3.amazonaws.com")
|
||||
newConfig.Hosts["s3.amazonaws.com"] = hostCfg
|
||||
isMutated = true
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "*amazonaws.com") || strings.Contains(host, "*.amazonaws.com") {
|
||||
console.Infoln("Found glob url, replacing " + host + " with s3.amazonaws.com")
|
||||
newConfig.Hosts["s3.amazonaws.com"] = hostCfg
|
||||
isMutated = true
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "*storage.googleapis.com") {
|
||||
console.Infoln("Found glob url, replacing " + host + " with storage.googleapis.com")
|
||||
newConfig.Hosts["storage.googleapis.com"] = hostCfg
|
||||
isMutated = true
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "localhost:*") {
|
||||
console.Infoln("Found glob url, replacing " + host + " with localhost:9000")
|
||||
newConfig.Hosts["localhost:9000"] = hostCfg
|
||||
isMutated = true
|
||||
continue
|
||||
}
|
||||
if strings.Contains(host, "127.0.0.1:*") {
|
||||
console.Infoln("Found glob url, replacing " + host + " with 127.0.0.1:9000")
|
||||
newConfig.Hosts["127.0.0.1:9000"] = hostCfg
|
||||
isMutated = true
|
||||
continue
|
||||
}
|
||||
// Other entries are hopefully OK. Copy them blindly.
|
||||
newConfig.Hosts[host] = hostCfg
|
||||
}
|
||||
|
||||
if isMutated {
|
||||
newConf, err := quick.New(newConfig)
|
||||
fatalIf(err.Trace(), "Unable to initialize newly fixed config.")
|
||||
|
||||
err = newConf.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(mustGetMcConfigPath()), "Unable to save newly fixed config path.")
|
||||
console.Infof("Successfully fixed %s broken config for version ‘6’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
}
|
||||
283
command/config-host-main.go
Normal file
283
command/config-host-main.go
Normal file
@@ -0,0 +1,283 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
configHostFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of config host",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var configHostCmd = cli.Command{
|
||||
Name: "host",
|
||||
Usage: "List, modify and remove hosts in configuration file.",
|
||||
Flags: append(configHostFlags, globalFlags...),
|
||||
Action: mainConfigHost,
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc config {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc config {{.Name}} OPERATION
|
||||
|
||||
OPERATION:
|
||||
add ALIAS URL ACCESS-KEY SECRET-KEY [API]
|
||||
remove ALIAS
|
||||
list
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Add Amazon S3 storage service under "myphotos" alias. For security reasons turn off bash history momentarily.
|
||||
$ set +o history
|
||||
$ mc config {{.Name}} add myphotos https://s3.amazonaws.com BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12
|
||||
$ set -o history
|
||||
|
||||
2. Add Google Cloud Storage service under "goodisk" alias.
|
||||
$ mc config {{.Name}} add goodisk https://storage.googleapis.com BKIKJAA5BMMU2RHO6IBB V8f1CwQqAcwo80UEIJEjc5gVQUSSx5ohQ9GSrr12 S3v2
|
||||
|
||||
3. List all hosts.
|
||||
$ mc config {{.Name}} list
|
||||
|
||||
4. Remove "goodisk" config.
|
||||
$ mc config {{.Name}} remove goodisk
|
||||
`,
|
||||
}
|
||||
|
||||
// hostMessage container for content message structure
|
||||
type hostMessage struct {
|
||||
op string
|
||||
Status string `json:"status"`
|
||||
Alias string `json:"alias"`
|
||||
URL string `json:"URL"`
|
||||
AccessKey string `json:"accessKey,omitempty"`
|
||||
SecretKey string `json:"secretKey,omitempty"`
|
||||
API string `json:"api,omitempty"`
|
||||
}
|
||||
|
||||
// String colorized host message
|
||||
func (h hostMessage) String() string {
|
||||
switch h.op {
|
||||
case "list":
|
||||
message := console.Colorize("Alias", fmt.Sprintf("%s: ", h.Alias))
|
||||
message += console.Colorize("URL", fmt.Sprintf("%s", h.URL))
|
||||
if h.AccessKey != "" || h.SecretKey != "" {
|
||||
message += " <- " + console.Colorize("AccessKey", fmt.Sprintf(" %s", h.AccessKey))
|
||||
message += " | " + console.Colorize("SecretKey", fmt.Sprintf(" %s", h.SecretKey))
|
||||
message += " | " + console.Colorize("API", fmt.Sprintf(" %s", h.API))
|
||||
}
|
||||
return message
|
||||
case "remove":
|
||||
return console.Colorize("HostMessage", "Removed ‘"+h.Alias+"’ successfully.")
|
||||
case "add":
|
||||
return console.Colorize("HostMessage", "Added ‘"+h.Alias+"’ successfully.")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// JSON jsonified host message
|
||||
func (h hostMessage) JSON() string {
|
||||
h.Status = "success"
|
||||
jsonMessageBytes, e := json.Marshal(h)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(jsonMessageBytes)
|
||||
}
|
||||
|
||||
// Validate command-line input args.
|
||||
func checkConfigHostSyntax(ctx *cli.Context) {
|
||||
// show help if nothing is set
|
||||
if !ctx.Args().Present() {
|
||||
cli.ShowCommandHelpAndExit(ctx, "host", 1) // last argument is exit code
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(ctx.Args().First()) {
|
||||
case "add":
|
||||
checkConfigHostAddSyntax(ctx)
|
||||
case "remove":
|
||||
checkConfigHostRemoveSyntax(ctx)
|
||||
case "list":
|
||||
default:
|
||||
cli.ShowCommandHelpAndExit(ctx, "host", 1) // last argument is exit code
|
||||
}
|
||||
}
|
||||
|
||||
// checkConfigHostAddSyntax - verifies input arguments to 'config host add'.
|
||||
func checkConfigHostAddSyntax(ctx *cli.Context) {
|
||||
tailArgs := ctx.Args().Tail()
|
||||
tailsArgsNr := len(tailArgs)
|
||||
if tailsArgsNr < 4 || tailsArgsNr > 5 {
|
||||
fatalIf(errInvalidArgument().Trace(ctx.Args().Tail()...),
|
||||
"Incorrect number of arguments for host add command.")
|
||||
}
|
||||
|
||||
alias := tailArgs.Get(0)
|
||||
url := tailArgs.Get(1)
|
||||
accessKey := tailArgs.Get(2)
|
||||
secretKey := tailArgs.Get(3)
|
||||
api := tailArgs.Get(4)
|
||||
|
||||
if !isValidAlias(alias) {
|
||||
fatalIf(errDummy().Trace(alias), "Invalid alias ‘"+alias+"’.")
|
||||
}
|
||||
|
||||
if !isValidHostURL(url) {
|
||||
fatalIf(errDummy().Trace(url),
|
||||
"Invalid URL ‘"+url+"’.")
|
||||
}
|
||||
|
||||
if !isValidAccessKey(accessKey) {
|
||||
fatalIf(errInvalidArgument().Trace(accessKey),
|
||||
"Invalid access key ‘"+accessKey+"’.")
|
||||
}
|
||||
|
||||
if !isValidSecretKey(secretKey) {
|
||||
fatalIf(errInvalidArgument().Trace(secretKey),
|
||||
"Invalid secret key ‘"+secretKey+"’.")
|
||||
}
|
||||
|
||||
if api != "" && !isValidAPI(api) { // Empty value set to default "S3v4".
|
||||
fatalIf(errInvalidArgument().Trace(api),
|
||||
"Unrecognized API signature. Valid options are ‘[S3v4, S3v2]’.")
|
||||
}
|
||||
}
|
||||
|
||||
// checkConfigHostRemoveSyntax - verifies input arguments to 'config host remove'.
|
||||
func checkConfigHostRemoveSyntax(ctx *cli.Context) {
|
||||
tailArgs := ctx.Args().Tail()
|
||||
|
||||
if len(ctx.Args().Tail()) != 1 {
|
||||
fatalIf(errInvalidArgument().Trace(tailArgs...),
|
||||
"Incorrect number of arguments for remove host command.")
|
||||
}
|
||||
|
||||
if !isValidAlias(tailArgs.Get(0)) {
|
||||
fatalIf(errDummy().Trace(tailArgs.Get(0)),
|
||||
"Invalid alias ‘"+tailArgs.Get(0)+"’.")
|
||||
}
|
||||
}
|
||||
|
||||
func mainConfigHost(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'config host' cli arguments.
|
||||
checkConfigHostSyntax(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("HostMessage", color.New(color.FgGreen))
|
||||
console.SetColor("Alias", color.New(color.FgCyan, color.Bold))
|
||||
console.SetColor("URL", color.New(color.FgCyan))
|
||||
console.SetColor("AccessKey", color.New(color.FgBlue))
|
||||
console.SetColor("SecretKey", color.New(color.FgBlue))
|
||||
console.SetColor("API", color.New(color.FgYellow))
|
||||
|
||||
cmd := ctx.Args().First()
|
||||
args := ctx.Args().Tail()
|
||||
|
||||
// Switch case through commands.
|
||||
switch strings.TrimSpace(cmd) {
|
||||
case "add":
|
||||
alias := args.Get(0)
|
||||
url := args.Get(1)
|
||||
accessKey := args.Get(2)
|
||||
secretKey := args.Get(3)
|
||||
api := args.Get(4)
|
||||
if api == "" {
|
||||
api = "S3v4"
|
||||
}
|
||||
hostCfg := hostConfigV8{
|
||||
URL: url,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
API: api,
|
||||
}
|
||||
addHost(alias, hostCfg) // Add a host with specified credentials.
|
||||
case "remove":
|
||||
alias := args.Get(0)
|
||||
removeHost(alias) // Remove a host.
|
||||
case "list":
|
||||
listHosts() // List all configured hosts.
|
||||
}
|
||||
}
|
||||
|
||||
// addHost - add a host config.
|
||||
func addHost(alias string, hostCfgV8 hostConfigV8) {
|
||||
mcCfgV8, err := loadMcConfig()
|
||||
fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config ‘"+mustGetMcConfigPath()+"’.")
|
||||
|
||||
// Add new host.
|
||||
mcCfgV8.Hosts[alias] = hostCfgV8
|
||||
|
||||
err = saveMcConfig(mcCfgV8)
|
||||
fatalIf(err.Trace(alias), "Unable to update hosts in config version ‘"+mustGetMcConfigPath()+"’.")
|
||||
|
||||
printMsg(hostMessage{
|
||||
op: "add",
|
||||
Alias: alias,
|
||||
URL: hostCfgV8.URL,
|
||||
AccessKey: hostCfgV8.AccessKey,
|
||||
SecretKey: hostCfgV8.SecretKey,
|
||||
API: hostCfgV8.API,
|
||||
})
|
||||
}
|
||||
|
||||
// removeHost - removes a host.
|
||||
func removeHost(alias string) {
|
||||
conf, err := loadMcConfig()
|
||||
fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config version ‘"+globalMCConfigVersion+"’.")
|
||||
|
||||
// Remove host.
|
||||
delete(conf.Hosts, alias)
|
||||
|
||||
err = saveMcConfig(conf)
|
||||
fatalIf(err.Trace(alias), "Unable to save deleted hosts in config version ‘"+globalMCConfigVersion+"’.")
|
||||
|
||||
printMsg(hostMessage{op: "remove", Alias: alias})
|
||||
}
|
||||
|
||||
// listHosts - list all host URLs.
|
||||
func listHosts() {
|
||||
conf, err := loadMcConfig()
|
||||
fatalIf(err.Trace(globalMCConfigVersion), "Unable to load config version ‘"+globalMCConfigVersion+"’.")
|
||||
|
||||
for k, v := range conf.Hosts {
|
||||
printMsg(hostMessage{
|
||||
op: "list",
|
||||
Alias: k,
|
||||
URL: v.URL,
|
||||
AccessKey: v.AccessKey,
|
||||
SecretKey: v.SecretKey,
|
||||
API: v.API,
|
||||
})
|
||||
}
|
||||
}
|
||||
77
command/config-main.go
Normal file
77
command/config-main.go
Normal file
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import "github.com/minio/cli"
|
||||
|
||||
// Configure minio client
|
||||
//
|
||||
// ----
|
||||
// NOTE: that the configure command only writes values to the config file.
|
||||
// It does not use any configuration values from the environment variables.
|
||||
//
|
||||
// One needs to edit configuration file manually, this is purposefully done
|
||||
// so to avoid taking credentials over cli arguments. It is a security precaution
|
||||
// ----
|
||||
//
|
||||
|
||||
var (
|
||||
configFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of config.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
var configCmd = cli.Command{
|
||||
Name: "config",
|
||||
Usage: "Manage configuration file.",
|
||||
Action: mainConfig,
|
||||
Flags: append(configFlags, globalFlags...),
|
||||
Subcommands: []cli.Command{
|
||||
configHostCmd,
|
||||
},
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} [FLAGS] COMMAND
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}
|
||||
`,
|
||||
}
|
||||
|
||||
// mainConfig is the handle for "mc config" command. provides sub-commands which write configuration data in json format to config file.
|
||||
func mainConfig(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
if ctx.Args().First() != "" { // command help.
|
||||
cli.ShowCommandHelp(ctx, ctx.Args().First())
|
||||
} else {
|
||||
// command with Subcommands is an App.
|
||||
cli.ShowAppHelp(ctx)
|
||||
}
|
||||
|
||||
// Sub-commands like "host" and "alias" have their own main.
|
||||
}
|
||||
437
command/config-migrate.go
Normal file
437
command/config-migrate.go
Normal file
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
// migrate config files from the any older version to the latest.
|
||||
func migrateConfig() {
|
||||
// Migrate config V1 to V101
|
||||
migrateConfigV1ToV101()
|
||||
// Migrate config V101 to V2
|
||||
migrateConfigV101ToV2()
|
||||
// Migrate config V2 to V3
|
||||
migrateConfigV2ToV3()
|
||||
// Migrate config V3 to V4
|
||||
migrateConfigV3ToV4()
|
||||
// Migrate config V4 to V5
|
||||
migrateConfigV4ToV5()
|
||||
// Migrate config V5 to V6
|
||||
migrateConfigV5ToV6()
|
||||
// Migrate config V6 to V7
|
||||
migrateConfigV6ToV7()
|
||||
// Migrate config V7 to V8
|
||||
migrateConfigV7ToV8()
|
||||
}
|
||||
|
||||
// Migrate from config version 1.0 to 1.0.1. Populate example entries and save it back.
|
||||
func migrateConfigV1ToV101() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
mcCfgV1, err := quick.Load(mustGetMcConfigPath(), newConfigV1())
|
||||
fatalIf(err.Trace(), "Unable to load config version ‘1’.")
|
||||
|
||||
// If loaded config version does not match 1.0.0, we do nothing.
|
||||
if mcCfgV1.Version() != "1.0.0" {
|
||||
return
|
||||
}
|
||||
|
||||
// 1.0.1 is compatible to 1.0.0. We are just adding new entries.
|
||||
cfgV101 := newConfigV101()
|
||||
|
||||
// Copy aliases.
|
||||
for k, v := range mcCfgV1.Data().(*configV1).Aliases {
|
||||
cfgV101.Aliases[k] = v
|
||||
}
|
||||
|
||||
// Copy hosts.
|
||||
for k, hostCfgV1 := range mcCfgV1.Data().(*configV1).Hosts {
|
||||
cfgV101.Hosts[k] = hostConfigV101{
|
||||
AccessKeyID: hostCfgV1.AccessKeyID,
|
||||
SecretAccessKey: hostCfgV1.SecretAccessKey,
|
||||
}
|
||||
}
|
||||
|
||||
// Example localhost entry.
|
||||
if _, ok := cfgV101.Hosts["localhost:*"]; !ok {
|
||||
cfgV101.Hosts["localhost:*"] = hostConfigV101{}
|
||||
}
|
||||
|
||||
// Example loopback IP entry.
|
||||
if _, ok := cfgV101.Hosts["127.0.0.1:*"]; !ok {
|
||||
cfgV101.Hosts["127.0.0.1:*"] = hostConfigV101{}
|
||||
}
|
||||
|
||||
// Example AWS entry.
|
||||
// Look for glob string (not glob match). We used to support glob based key matching earlier.
|
||||
if _, ok := cfgV101.Hosts["*.s3*.amazonaws.com"]; !ok {
|
||||
cfgV101.Hosts["*.s3*.amazonaws.com"] = hostConfigV101{
|
||||
AccessKeyID: "YOUR-ACCESS-KEY-ID-HERE",
|
||||
SecretAccessKey: "YOUR-SECRET-ACCESS-KEY-HERE",
|
||||
}
|
||||
}
|
||||
|
||||
// Save the new config back to the disk.
|
||||
mcCfgV101, err := quick.New(cfgV101)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘1.0.1’.")
|
||||
err = mcCfgV101.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘1.0.1’.")
|
||||
|
||||
console.Infof("Successfully migrated %s from version ‘1.0.0’ to version ‘1.0.1’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
|
||||
// Migrate from config ‘1.0.1’ to ‘2’. Drop semantic versioning and move to integer versioning. No other changes.
|
||||
func migrateConfigV101ToV2() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
mcCfgV101, err := quick.Load(mustGetMcConfigPath(), newConfigV101())
|
||||
fatalIf(err.Trace(), "Unable to load config version ‘1.0.1’.")
|
||||
|
||||
// update to newer version
|
||||
if mcCfgV101.Version() != "1.0.1" {
|
||||
return
|
||||
}
|
||||
|
||||
cfgV2 := newConfigV2()
|
||||
|
||||
// Copy aliases.
|
||||
for k, v := range mcCfgV101.Data().(*configV101).Aliases {
|
||||
cfgV2.Aliases[k] = v
|
||||
}
|
||||
|
||||
// Copy hosts.
|
||||
for k, hostCfgV101 := range mcCfgV101.Data().(*configV101).Hosts {
|
||||
cfgV2.Hosts[k] = hostConfigV2{
|
||||
AccessKeyID: hostCfgV101.AccessKeyID,
|
||||
SecretAccessKey: hostCfgV101.SecretAccessKey,
|
||||
}
|
||||
}
|
||||
|
||||
mcCfgV2, err := quick.New(cfgV2)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘2’.")
|
||||
|
||||
err = mcCfgV2.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘2’.")
|
||||
|
||||
console.Infof("Successfully migrated %s from version ‘1.0.1’ to version ‘2’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
|
||||
// Migrate from config ‘2’ to ‘3’. Use ‘-’ separated names for
|
||||
// hostConfig using struct json tags.
|
||||
func migrateConfigV2ToV3() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
|
||||
mcCfgV2, err := quick.Load(mustGetMcConfigPath(), newConfigV2())
|
||||
fatalIf(err.Trace(), "Unable to load mc config V2.")
|
||||
|
||||
// update to newer version
|
||||
if mcCfgV2.Version() != "2" {
|
||||
return
|
||||
}
|
||||
|
||||
cfgV3 := newConfigV3()
|
||||
|
||||
// Copy aliases.
|
||||
for k, v := range mcCfgV2.Data().(*configV2).Aliases {
|
||||
cfgV3.Aliases[k] = v
|
||||
}
|
||||
|
||||
// Copy hosts.
|
||||
for k, hostCfgV2 := range mcCfgV2.Data().(*configV2).Hosts {
|
||||
// New hostConfV3 uses struct json tags.
|
||||
cfgV3.Hosts[k] = hostConfigV3{
|
||||
AccessKeyID: hostCfgV2.AccessKeyID,
|
||||
SecretAccessKey: hostCfgV2.SecretAccessKey,
|
||||
}
|
||||
}
|
||||
|
||||
mcNewCfgV3, err := quick.New(cfgV3)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘3’.")
|
||||
|
||||
err = mcNewCfgV3.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘3’.")
|
||||
|
||||
console.Infof("Successfully migrated %s from version ‘2’ to version ‘3’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
|
||||
// Migrate from config version ‘3’ to ‘4’. Introduce API Signature
|
||||
// field in host config. Also Use JavaScript notation for field names.
|
||||
func migrateConfigV3ToV4() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
mcCfgV3, err := quick.Load(mustGetMcConfigPath(), newConfigV3())
|
||||
fatalIf(err.Trace(), "Unable to load mc config V2.")
|
||||
|
||||
// update to newer version
|
||||
if mcCfgV3.Version() != "3" {
|
||||
return
|
||||
}
|
||||
|
||||
cfgV4 := newConfigV4()
|
||||
for k, v := range mcCfgV3.Data().(*configV3).Aliases {
|
||||
cfgV4.Aliases[k] = v
|
||||
}
|
||||
// New hostConfig has API signature. All older entries were V4
|
||||
// only. So it is safe to assume V4 as default for all older
|
||||
// entries.
|
||||
// HostConfigV4 als uses JavaScript naming notation for struct JSON tags.
|
||||
for host, hostCfgV3 := range mcCfgV3.Data().(*configV3).Hosts {
|
||||
cfgV4.Hosts[host] = hostConfigV4{
|
||||
AccessKeyID: hostCfgV3.AccessKeyID,
|
||||
SecretAccessKey: hostCfgV3.SecretAccessKey,
|
||||
Signature: "v4",
|
||||
}
|
||||
}
|
||||
|
||||
mcNewCfgV4, err := quick.New(cfgV4)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘4’.")
|
||||
|
||||
err = mcNewCfgV4.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘4’.")
|
||||
|
||||
console.Infof("Successfully migrated %s from version ‘3’ to version ‘4’.\n", mustGetMcConfigPath())
|
||||
|
||||
}
|
||||
|
||||
// Migrate config version ‘4’ to ‘5’. Rename hostConfigV4.Signature -> hostConfigV5.API.
|
||||
func migrateConfigV4ToV5() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
mcCfgV4, err := quick.Load(mustGetMcConfigPath(), newConfigV4())
|
||||
fatalIf(err.Trace(), "Unable to load mc config V4.")
|
||||
|
||||
// update to newer version
|
||||
if mcCfgV4.Version() != "4" {
|
||||
return
|
||||
}
|
||||
|
||||
cfgV5 := newConfigV5()
|
||||
for k, v := range mcCfgV4.Data().(*configV4).Aliases {
|
||||
cfgV5.Aliases[k] = v
|
||||
}
|
||||
for host, hostCfgV4 := range mcCfgV4.Data().(*configV4).Hosts {
|
||||
cfgV5.Hosts[host] = hostConfigV5{
|
||||
AccessKeyID: hostCfgV4.AccessKeyID,
|
||||
SecretAccessKey: hostCfgV4.SecretAccessKey,
|
||||
API: "v4", // Rename from .Signature to .API
|
||||
}
|
||||
}
|
||||
|
||||
mcNewCfgV5, err := quick.New(cfgV5)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘5’.")
|
||||
|
||||
err = mcNewCfgV5.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘5’.")
|
||||
|
||||
console.Infof("Successfully migrated %s from version ‘4’ to version ‘5’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
|
||||
// Migrate config version ‘5’ to ‘6’. Add google cloud storage servers
|
||||
// to host config. Also remove "." from s3 aws glob rule.
|
||||
func migrateConfigV5ToV6() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
mcCfgV5, err := quick.Load(mustGetMcConfigPath(), newConfigV5())
|
||||
fatalIf(err.Trace(), "Unable to load mc config V5.")
|
||||
|
||||
// update to newer version
|
||||
if mcCfgV5.Version() != "5" {
|
||||
return
|
||||
}
|
||||
|
||||
cfgV6 := newConfigV6()
|
||||
|
||||
// Add new Google Cloud Storage alias.
|
||||
cfgV6.Aliases["gcs"] = "https://storage.googleapis.com"
|
||||
|
||||
for k, v := range mcCfgV5.Data().(*configV5).Aliases {
|
||||
cfgV6.Aliases[k] = v
|
||||
}
|
||||
|
||||
// Add defaults.
|
||||
cfgV6.Hosts["*s3*amazonaws.com"] = hostConfigV6{
|
||||
AccessKeyID: "YOUR-ACCESS-KEY-ID-HERE",
|
||||
SecretAccessKey: "YOUR-SECRET-ACCESS-KEY-HERE",
|
||||
API: "S3v4",
|
||||
}
|
||||
cfgV6.Hosts["*storage.googleapis.com"] = hostConfigV6{
|
||||
AccessKeyID: "YOUR-ACCESS-KEY-ID-HERE",
|
||||
SecretAccessKey: "YOUR-SECRET-ACCESS-KEY-HERE",
|
||||
API: "S3v2",
|
||||
}
|
||||
|
||||
for host, hostCfgV5 := range mcCfgV5.Data().(*configV5).Hosts {
|
||||
// Find any matching s3 entry and copy keys from it to newer generalized glob entry.
|
||||
if strings.Contains(host, "s3") {
|
||||
if (hostCfgV5.AccessKeyID == "YOUR-ACCESS-KEY-ID-HERE") ||
|
||||
(hostCfgV5.SecretAccessKey == "YOUR-SECRET-ACCESS-KEY-HERE") ||
|
||||
hostCfgV5.AccessKeyID == "" ||
|
||||
hostCfgV5.SecretAccessKey == "" {
|
||||
continue // Skip defaults.
|
||||
}
|
||||
// Now we have real keys set by the user. Copy
|
||||
// them over to newer glob rule.
|
||||
// Original host entry has "." in the glob rule.
|
||||
host = "*s3*amazonaws.com" // Use this glob entry.
|
||||
}
|
||||
|
||||
cfgV6.Hosts[host] = hostConfigV6{
|
||||
AccessKeyID: hostCfgV5.AccessKeyID,
|
||||
SecretAccessKey: hostCfgV5.SecretAccessKey,
|
||||
API: hostCfgV5.API,
|
||||
}
|
||||
}
|
||||
|
||||
mcNewCfgV6, err := quick.New(cfgV6)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘6’.")
|
||||
|
||||
err = mcNewCfgV6.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘6’.")
|
||||
|
||||
console.Infof("Successfully migrated %s from version ‘5’ to version ‘6’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
|
||||
// Migrate config version ‘6’ to ‘7'. Remove alias map and introduce
|
||||
// named Host config. Also no more glob match for host config entries.
|
||||
func migrateConfigV6ToV7() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
|
||||
mcCfgV6, err := quick.Load(mustGetMcConfigPath(), newConfigV6())
|
||||
fatalIf(err.Trace(), "Unable to load mc config V6.")
|
||||
|
||||
if mcCfgV6.Version() != "6" {
|
||||
return
|
||||
}
|
||||
|
||||
cfgV7 := newConfigV7()
|
||||
aliasIndex := 0
|
||||
|
||||
// old Aliases.
|
||||
oldAliases := mcCfgV6.Data().(*configV6).Aliases
|
||||
|
||||
// We dropped alias support in v7. We only need to migrate host configs.
|
||||
for host, hostCfgV6 := range mcCfgV6.Data().(*configV6).Hosts {
|
||||
// Look through old aliases, if found any matching save those entries.
|
||||
for aliasName, aliasedHost := range oldAliases {
|
||||
if aliasedHost == host {
|
||||
cfgV7.Hosts[aliasName] = hostConfigV7{
|
||||
URL: host,
|
||||
AccessKey: hostCfgV6.AccessKeyID,
|
||||
SecretKey: hostCfgV6.SecretAccessKey,
|
||||
API: hostCfgV6.API,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if hostCfgV6.AccessKeyID == "YOUR-ACCESS-KEY-ID-HERE" ||
|
||||
hostCfgV6.SecretAccessKey == "YOUR-SECRET-ACCESS-KEY-HERE" ||
|
||||
hostCfgV6.AccessKeyID == "" ||
|
||||
hostCfgV6.SecretAccessKey == "" {
|
||||
// Ignore default entries. configV7.loadDefaults() will re-insert them back.
|
||||
} else if host == "https://s3.amazonaws.com" {
|
||||
// Only one entry can exist for "s3" domain.
|
||||
cfgV7.Hosts["s3"] = hostConfigV7{
|
||||
URL: host,
|
||||
AccessKey: hostCfgV6.AccessKeyID,
|
||||
SecretKey: hostCfgV6.SecretAccessKey,
|
||||
API: hostCfgV6.API,
|
||||
}
|
||||
} else if host == "https://storage.googleapis.com" {
|
||||
// Only one entry can exist for "gcs" domain.
|
||||
cfgV7.Hosts["gcs"] = hostConfigV7{
|
||||
URL: host,
|
||||
AccessKey: hostCfgV6.AccessKeyID,
|
||||
SecretKey: hostCfgV6.SecretAccessKey,
|
||||
API: hostCfgV6.API,
|
||||
}
|
||||
} else {
|
||||
// Assign a generic "cloud1", cloud2..." key
|
||||
// for all other entries that has valid keys set.
|
||||
alias := fmt.Sprintf("cloud%d", aliasIndex)
|
||||
aliasIndex++
|
||||
cfgV7.Hosts[alias] = hostConfigV7{
|
||||
URL: host,
|
||||
AccessKey: hostCfgV6.AccessKeyID,
|
||||
SecretKey: hostCfgV6.SecretAccessKey,
|
||||
API: hostCfgV6.API,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load default settings.
|
||||
cfgV7.loadDefaults()
|
||||
mcNewCfgV7, err := quick.New(cfgV7)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘7’.")
|
||||
|
||||
err = mcNewCfgV7.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘7’.")
|
||||
|
||||
console.Infof("Successfully migrated %s from version ‘6’ to version ‘7’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
|
||||
// Migrate config version ‘7’ to ‘8'. Remove hosts
|
||||
// 'play.minio.io:9002' and 'dl.minio.io:9000'.
|
||||
func migrateConfigV7ToV8() {
|
||||
if !isMcConfigExists() {
|
||||
return
|
||||
}
|
||||
|
||||
mcCfgV7, err := quick.Load(mustGetMcConfigPath(), newConfigV7())
|
||||
fatalIf(err.Trace(), "Unable to load mc config V7.")
|
||||
|
||||
if mcCfgV7.Version() != "7" {
|
||||
return
|
||||
}
|
||||
|
||||
cfgV8 := newConfigV8()
|
||||
// We dropped alias support in v7. We only need to migrate host configs.
|
||||
for host, hostCfgV7 := range mcCfgV7.Data().(*configV7).Hosts {
|
||||
// Ignore 'player', 'play' and 'dl' aliases.
|
||||
if host == "player" || host == "dl" || host == "play" {
|
||||
continue
|
||||
}
|
||||
hostCfgV8 := hostConfigV8{}
|
||||
hostCfgV8.URL = hostCfgV7.URL
|
||||
hostCfgV8.AccessKey = hostCfgV7.AccessKey
|
||||
hostCfgV8.SecretKey = hostCfgV7.SecretKey
|
||||
hostCfgV8.API = hostCfgV7.API
|
||||
cfgV8.Hosts[host] = hostCfgV8
|
||||
}
|
||||
// Load default settings.
|
||||
cfgV8.loadDefaults()
|
||||
mcNewCfgV8, err := quick.New(cfgV8)
|
||||
fatalIf(err.Trace(), "Unable to initialize quick config for config version ‘8’.")
|
||||
|
||||
err = mcNewCfgV8.Save(mustGetMcConfigPath())
|
||||
fatalIf(err.Trace(), "Unable to save config version ‘8’.")
|
||||
|
||||
console.Infof("Successfully migrated %s from version ‘7’ to version ‘8’.\n", mustGetMcConfigPath())
|
||||
}
|
||||
244
command/config-old.go
Normal file
244
command/config-old.go
Normal file
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
/////////////////// Config V1 ///////////////////
|
||||
type hostConfigV1 struct {
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
}
|
||||
|
||||
type configV1 struct {
|
||||
Version string
|
||||
Aliases map[string]string
|
||||
Hosts map[string]hostConfigV1
|
||||
}
|
||||
|
||||
// newConfigV1() - get new config version 1.0.0
|
||||
func newConfigV1() *configV1 {
|
||||
conf := new(configV1)
|
||||
conf.Version = "1.0.0"
|
||||
// make sure to allocate map's otherwise Golang
|
||||
// exits silently without providing any errors
|
||||
conf.Aliases = make(map[string]string)
|
||||
conf.Hosts = make(map[string]hostConfigV1)
|
||||
return conf
|
||||
}
|
||||
|
||||
/////////////////// Config V101 ///////////////////
|
||||
type hostConfigV101 hostConfigV1
|
||||
|
||||
type configV101 struct {
|
||||
Version string
|
||||
Aliases map[string]string
|
||||
Hosts map[string]hostConfigV101
|
||||
}
|
||||
|
||||
// newConfigV101() - get new config version 1.0.1
|
||||
func newConfigV101() *configV101 {
|
||||
conf := new(configV101)
|
||||
conf.Version = "1.0.1"
|
||||
conf.Aliases = make(map[string]string)
|
||||
conf.Hosts = make(map[string]hostConfigV101)
|
||||
return conf
|
||||
}
|
||||
|
||||
/////////////////// Config V2 ///////////////////
|
||||
type hostConfigV2 hostConfigV1
|
||||
|
||||
type configV2 struct {
|
||||
Version string
|
||||
Aliases map[string]string
|
||||
Hosts map[string]hostConfigV2
|
||||
}
|
||||
|
||||
// newConfigV2() - get new config version 2
|
||||
func newConfigV2() *configV2 {
|
||||
conf := new(configV2)
|
||||
conf.Version = "2"
|
||||
conf.Aliases = make(map[string]string)
|
||||
conf.Hosts = make(map[string]hostConfigV2)
|
||||
return conf
|
||||
}
|
||||
|
||||
/////////////////// Config V3 ///////////////////
|
||||
type hostConfigV3 struct {
|
||||
AccessKeyID string `json:"access-key-id"`
|
||||
SecretAccessKey string `json:"secret-access-key"`
|
||||
}
|
||||
|
||||
type configV3 struct {
|
||||
Version string `json:"version"`
|
||||
Aliases map[string]string `json:"alias"`
|
||||
Hosts map[string]hostConfigV3 `json:"hosts"`
|
||||
}
|
||||
|
||||
// newConfigV3 - get new config version 3.
|
||||
func newConfigV3() *configV3 {
|
||||
conf := new(configV3)
|
||||
conf.Version = "3"
|
||||
conf.Aliases = make(map[string]string)
|
||||
conf.Hosts = make(map[string]hostConfigV3)
|
||||
return conf
|
||||
}
|
||||
|
||||
/////////////////// Config V4 ///////////////////
|
||||
type hostConfigV4 struct {
|
||||
AccessKeyID string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
type configV4 struct {
|
||||
Version string `json:"version"`
|
||||
Aliases map[string]string `json:"alias"`
|
||||
Hosts map[string]hostConfigV4 `json:"hosts"`
|
||||
}
|
||||
|
||||
func newConfigV4() *configV4 {
|
||||
conf := new(configV4)
|
||||
conf.Version = "4"
|
||||
conf.Aliases = make(map[string]string)
|
||||
conf.Hosts = make(map[string]hostConfigV4)
|
||||
return conf
|
||||
}
|
||||
|
||||
/////////////////// Config V5 ///////////////////
|
||||
type hostConfigV5 struct {
|
||||
AccessKeyID string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
API string `json:"api"`
|
||||
}
|
||||
|
||||
type configV5 struct {
|
||||
Version string `json:"version"`
|
||||
Aliases map[string]string `json:"alias"`
|
||||
Hosts map[string]hostConfigV5 `json:"hosts"`
|
||||
}
|
||||
|
||||
func newConfigV5() *configV5 {
|
||||
conf := new(configV5)
|
||||
conf.Version = "5"
|
||||
conf.Aliases = make(map[string]string)
|
||||
conf.Hosts = make(map[string]hostConfigV5)
|
||||
return conf
|
||||
}
|
||||
|
||||
/////////////////// Config V6 ///////////////////
|
||||
type hostConfigV6 struct {
|
||||
AccessKeyID string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
API string `json:"api"`
|
||||
}
|
||||
|
||||
type configV6 struct {
|
||||
Version string `json:"version"`
|
||||
Aliases map[string]string `json:"alias"`
|
||||
Hosts map[string]hostConfigV6 `json:"hosts"`
|
||||
}
|
||||
|
||||
// newConfigV6 - new config version '6'.
|
||||
func newConfigV6() *configV6 {
|
||||
conf := new(configV6)
|
||||
conf.Version = "6"
|
||||
conf.Aliases = make(map[string]string)
|
||||
conf.Hosts = make(map[string]hostConfigV6)
|
||||
return conf
|
||||
}
|
||||
|
||||
/////////////////// Config V6 ///////////////////
|
||||
// hostConfig configuration of a host - version '7'.
|
||||
type hostConfigV7 struct {
|
||||
URL string `json:"url"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
API string `json:"api"`
|
||||
}
|
||||
|
||||
// configV7 config version.
|
||||
type configV7 struct {
|
||||
Version string `json:"version"`
|
||||
Hosts map[string]hostConfigV7 `json:"hosts"`
|
||||
}
|
||||
|
||||
// newConfigV7 - new config version '7'.
|
||||
func newConfigV7() *configV7 {
|
||||
cfg := new(configV7)
|
||||
cfg.Version = "7"
|
||||
cfg.Hosts = make(map[string]hostConfigV7)
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c *configV7) loadDefaults() {
|
||||
// Minio server running locally.
|
||||
c.setHost("local", hostConfigV7{
|
||||
URL: "http://localhost:9000",
|
||||
AccessKey: "",
|
||||
SecretKey: "",
|
||||
API: "S3v4",
|
||||
})
|
||||
|
||||
// Amazon S3 cloud storage service.
|
||||
c.setHost("s3", hostConfigV7{
|
||||
URL: "https://s3.amazonaws.com",
|
||||
AccessKey: defaultAccessKey,
|
||||
SecretKey: defaultSecretKey,
|
||||
API: "S3v4",
|
||||
})
|
||||
|
||||
// Google cloud storage service.
|
||||
c.setHost("gcs", hostConfigV7{
|
||||
URL: "https://storage.googleapis.com",
|
||||
AccessKey: defaultAccessKey,
|
||||
SecretKey: defaultSecretKey,
|
||||
API: "S3v2",
|
||||
})
|
||||
|
||||
// Minio anonymous server for demo.
|
||||
c.setHost("play", hostConfigV7{
|
||||
URL: "https://play.minio.io:9000",
|
||||
AccessKey: "",
|
||||
SecretKey: "",
|
||||
API: "S3v4",
|
||||
})
|
||||
|
||||
// Minio demo server with public secret and access keys.
|
||||
c.setHost("player", hostConfigV7{
|
||||
URL: "https://play.minio.io:9002",
|
||||
AccessKey: "Q3AM3UQ867SPQQA43P2F",
|
||||
SecretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
|
||||
API: "S3v4",
|
||||
})
|
||||
|
||||
// Minio public download service.
|
||||
c.setHost("dl", hostConfigV7{
|
||||
URL: "https://dl.minio.io:9000",
|
||||
AccessKey: "",
|
||||
SecretKey: "",
|
||||
API: "S3v4",
|
||||
})
|
||||
}
|
||||
|
||||
// SetHost sets host config if not empty.
|
||||
func (c *configV7) setHost(alias string, cfg hostConfigV7) {
|
||||
if _, ok := c.Hosts[alias]; !ok {
|
||||
c.Hosts[alias] = cfg
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////// Config V8 ///////////////////
|
||||
// RESERVED FOR FUTURE
|
||||
67
command/config-utils.go
Normal file
67
command/config-utils.go
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Minio Client (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var validAPIs = []string{"S3v4", "S3v2"}
|
||||
|
||||
// isValidSecretKey - validate secret key.
|
||||
func isValidSecretKey(secretKey string) bool {
|
||||
if secretKey == "" {
|
||||
return true
|
||||
}
|
||||
regex := regexp.MustCompile(`.{8,40}$`)
|
||||
return regex.MatchString(secretKey) && !strings.ContainsAny(secretKey, "$%^~`!|&*#@")
|
||||
}
|
||||
|
||||
// isValidAccessKey - validate access key.
|
||||
func isValidAccessKey(accessKey string) bool {
|
||||
if accessKey == "" {
|
||||
return true
|
||||
}
|
||||
regex := regexp.MustCompile(`.{5,40}$`)
|
||||
return regex.MatchString(accessKey) && !strings.ContainsAny(accessKey, "$%^~`!|&*#@")
|
||||
}
|
||||
|
||||
// isValidHostURL - validate input host url.
|
||||
func isValidHostURL(hostURL string) bool {
|
||||
if strings.TrimSpace(hostURL) == "" {
|
||||
return false
|
||||
}
|
||||
url := newClientURL(hostURL)
|
||||
if url.Scheme != "https" && url.Scheme != "http" {
|
||||
return false
|
||||
}
|
||||
if url.Path != "" && url.Path != "/" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidAPI - Validates if API signature string of supported type.
|
||||
func isValidAPI(api string) bool {
|
||||
switch strings.ToLower(api) {
|
||||
case "s3v2", "s3v4":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
141
command/config-v8.go
Normal file
141
command/config-v8.go
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAccessKey = "YOUR-ACCESS-KEY-HERE"
|
||||
defaultSecretKey = "YOUR-SECRET-KEY-HERE"
|
||||
)
|
||||
|
||||
var (
|
||||
// set once during first load.
|
||||
cacheCfgV8 *configV8
|
||||
// All access to mc config file should be synchronized.
|
||||
cfgMutex = &sync.RWMutex{}
|
||||
)
|
||||
|
||||
// hostConfig configuration of a host.
|
||||
type hostConfigV8 struct {
|
||||
URL string `json:"url"`
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
API string `json:"api"`
|
||||
}
|
||||
|
||||
// configV8 config version.
|
||||
type configV8 struct {
|
||||
Version string `json:"version"`
|
||||
Hosts map[string]hostConfigV8 `json:"hosts"`
|
||||
}
|
||||
|
||||
// newConfigV8 - new config version.
|
||||
func newConfigV8() *configV8 {
|
||||
cfg := new(configV8)
|
||||
cfg.Version = globalMCConfigVersion
|
||||
cfg.Hosts = make(map[string]hostConfigV8)
|
||||
return cfg
|
||||
}
|
||||
|
||||
// SetHost sets host config if not empty.
|
||||
func (c *configV8) setHost(alias string, cfg hostConfigV8) {
|
||||
if _, ok := c.Hosts[alias]; !ok {
|
||||
c.Hosts[alias] = cfg
|
||||
}
|
||||
}
|
||||
|
||||
// load default values for missing entries.
|
||||
func (c *configV8) loadDefaults() {
|
||||
// Minio server running locally.
|
||||
c.setHost("local", hostConfigV8{
|
||||
URL: "http://localhost:9000",
|
||||
AccessKey: "",
|
||||
SecretKey: "",
|
||||
API: "S3v4",
|
||||
})
|
||||
|
||||
// Amazon S3 cloud storage service.
|
||||
c.setHost("s3", hostConfigV8{
|
||||
URL: "https://s3.amazonaws.com",
|
||||
AccessKey: defaultAccessKey,
|
||||
SecretKey: defaultSecretKey,
|
||||
API: "S3v4",
|
||||
})
|
||||
|
||||
// Google cloud storage service.
|
||||
c.setHost("gcs", hostConfigV8{
|
||||
URL: "https://storage.googleapis.com",
|
||||
AccessKey: defaultAccessKey,
|
||||
SecretKey: defaultSecretKey,
|
||||
API: "S3v2",
|
||||
})
|
||||
|
||||
// Minio anonymous server for demo.
|
||||
c.setHost("play", hostConfigV8{
|
||||
URL: "https://play.minio.io:9000",
|
||||
AccessKey: "Q3AM3UQ867SPQQA43P2F",
|
||||
SecretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
|
||||
API: "S3v4",
|
||||
})
|
||||
}
|
||||
|
||||
// loadConfigV8 - loads a new config.
|
||||
func loadConfigV8() (*configV8, *probe.Error) {
|
||||
cfgMutex.RLock()
|
||||
defer cfgMutex.RUnlock()
|
||||
|
||||
// Cached in private global variable.
|
||||
if cacheCfgV8 != nil {
|
||||
return cacheCfgV8, nil
|
||||
}
|
||||
|
||||
if !isMcConfigExists() {
|
||||
return nil, errInvalidArgument().Trace()
|
||||
}
|
||||
|
||||
mcCfgV8, err := quick.Load(mustGetMcConfigPath(), newConfigV8())
|
||||
fatalIf(err.Trace(), "Unable to load mc config file ‘"+mustGetMcConfigPath()+"’.")
|
||||
|
||||
cfgV8 := mcCfgV8.Data().(*configV8)
|
||||
|
||||
// cache it.
|
||||
cacheCfgV8 = cfgV8
|
||||
|
||||
return cfgV8, nil
|
||||
}
|
||||
|
||||
// saveConfigV8 - saves an updated config.
|
||||
func saveConfigV8(cfgV8 *configV8) *probe.Error {
|
||||
cfgMutex.Lock()
|
||||
defer cfgMutex.Unlock()
|
||||
|
||||
qs, err := quick.New(cfgV8)
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
|
||||
// update the cache.
|
||||
cacheCfgV8 = cfgV8
|
||||
|
||||
return qs.Save(mustGetMcConfigPath()).Trace(mustGetMcConfigPath())
|
||||
}
|
||||
78
command/config-validate.go
Normal file
78
command/config-validate.go
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Check if version of the config is valid
|
||||
func validateConfigVersion(config *configV8) (bool, string) {
|
||||
if config.Version != globalMCConfigVersion {
|
||||
return false, fmt.Sprintf("Config version %s does not match GlobalMCConfiguration %s.\n",
|
||||
config.Version, globalMCConfigVersion)
|
||||
}
|
||||
return true, ""
|
||||
}
|
||||
|
||||
// Verifies the config file of the Minio Client
|
||||
func validateConfigFile(config *configV8) (bool, []string) {
|
||||
ok, err := validateConfigVersion(config)
|
||||
var validationSuccessful = true
|
||||
var errors []string
|
||||
if !ok {
|
||||
validationSuccessful = false
|
||||
errors = append(errors, err)
|
||||
}
|
||||
hosts := config.Hosts
|
||||
for _, hostConfig := range hosts {
|
||||
hostConfigHealthOk, hostErrors := validateConfigHost(hostConfig)
|
||||
if !hostConfigHealthOk {
|
||||
validationSuccessful = false
|
||||
errors = append(errors, hostErrors...)
|
||||
}
|
||||
}
|
||||
return validationSuccessful, errors
|
||||
}
|
||||
|
||||
func validateConfigHost(host hostConfigV8) (bool, []string) {
|
||||
var validationSuccessful = true
|
||||
var hostErrors []string
|
||||
api := host.API
|
||||
validAPI := isValidAPI(strings.ToLower(api))
|
||||
if !validAPI {
|
||||
var errorMsg bytes.Buffer
|
||||
errorMsg.WriteString(fmt.Sprintf(
|
||||
"%s API for host %s is not Valid. It is not part of any of the following APIs:\n",
|
||||
api, host.URL))
|
||||
for index, validAPI := range validAPIs {
|
||||
errorMsg.WriteString(fmt.Sprintf("%d. %s\n", index+1, validAPI))
|
||||
}
|
||||
validationSuccessful = false
|
||||
hostErrors = append(hostErrors, errorMsg.String())
|
||||
}
|
||||
url := host.URL
|
||||
validURL := isValidHostURL(url)
|
||||
if !validURL {
|
||||
validationSuccessful = false
|
||||
msg := fmt.Sprintf("URL %s for host %s is not valid. Could not parse it.\n", url, host.URL)
|
||||
hostErrors = append(hostErrors, msg)
|
||||
}
|
||||
return validationSuccessful, hostErrors
|
||||
}
|
||||
194
command/config.go
Normal file
194
command/config.go
Normal file
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
"github.com/minio/go-homedir"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// mcCustomConfigDir contains the whole path to config dir. Only access via get/set functions.
|
||||
var mcCustomConfigDir string
|
||||
|
||||
// setMcConfigDir - set a custom minio client config folder.
|
||||
func setMcConfigDir(configDir string) {
|
||||
mcCustomConfigDir = configDir
|
||||
}
|
||||
|
||||
// getMcConfigDir - construct minio client config folder.
|
||||
func getMcConfigDir() (string, *probe.Error) {
|
||||
if mcCustomConfigDir != "" {
|
||||
return mcCustomConfigDir, nil
|
||||
}
|
||||
homeDir, e := homedir.Dir()
|
||||
if e != nil {
|
||||
return "", probe.NewError(e)
|
||||
}
|
||||
var configDir string
|
||||
// For windows the path is slightly different
|
||||
if runtime.GOOS == "windows" {
|
||||
configDir = filepath.Join(homeDir, globalMCConfigWindowsDir)
|
||||
} else {
|
||||
configDir = filepath.Join(homeDir, globalMCConfigDir)
|
||||
}
|
||||
return configDir, nil
|
||||
}
|
||||
|
||||
// mustGetMcConfigDir - construct minio client config folder or fail
|
||||
func mustGetMcConfigDir() (configDir string) {
|
||||
configDir, err := getMcConfigDir()
|
||||
fatalIf(err.Trace(), "Unable to get mcConfigDir.")
|
||||
|
||||
return configDir
|
||||
}
|
||||
|
||||
// createMcConfigDir - create minio client config folder
|
||||
func createMcConfigDir() *probe.Error {
|
||||
p, err := getMcConfigDir()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
if e := os.MkdirAll(p, 0700); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getMcConfigPath - construct minio client configuration path
|
||||
func getMcConfigPath() (string, *probe.Error) {
|
||||
if mcCustomConfigDir != "" {
|
||||
return filepath.Join(mcCustomConfigDir, globalMCConfigFile), nil
|
||||
}
|
||||
dir, err := getMcConfigDir()
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
}
|
||||
return filepath.Join(dir, globalMCConfigFile), nil
|
||||
}
|
||||
|
||||
// mustGetMcConfigPath - similar to getMcConfigPath, ignores errors
|
||||
func mustGetMcConfigPath() string {
|
||||
path, err := getMcConfigPath()
|
||||
fatalIf(err.Trace(), "Unable to get mcConfigPath.")
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// newMcConfig - initializes a new version '6' config.
|
||||
func newMcConfig() *configV8 {
|
||||
cfg := newConfigV8()
|
||||
cfg.loadDefaults()
|
||||
return cfg
|
||||
}
|
||||
|
||||
// loadMcConfigCached - returns loadMcConfig with a closure for config cache.
|
||||
func loadMcConfigFactory() func() (*configV8, *probe.Error) {
|
||||
// Load once and cache in a closure.
|
||||
cfgCache, err := loadConfigV8()
|
||||
|
||||
// loadMcConfig - reads configuration file and returns config.
|
||||
return func() (*configV8, *probe.Error) {
|
||||
return cfgCache, err
|
||||
}
|
||||
}
|
||||
|
||||
// loadMcConfig - returns configuration, initialized later.
|
||||
var loadMcConfig func() (*configV8, *probe.Error)
|
||||
|
||||
// saveMcConfig - saves configuration file and returns error if any.
|
||||
func saveMcConfig(config *configV8) *probe.Error {
|
||||
if config == nil {
|
||||
return errInvalidArgument().Trace()
|
||||
}
|
||||
|
||||
err := createMcConfigDir()
|
||||
if err != nil {
|
||||
return err.Trace(mustGetMcConfigDir())
|
||||
}
|
||||
|
||||
// Save the config.
|
||||
if err := saveConfigV8(config); err != nil {
|
||||
return err.Trace(mustGetMcConfigPath())
|
||||
}
|
||||
|
||||
// Refresh the config cache.
|
||||
loadMcConfig = loadMcConfigFactory()
|
||||
return nil
|
||||
}
|
||||
|
||||
// isMcConfigExists returns err if config doesn't exist.
|
||||
func isMcConfigExists() bool {
|
||||
configFile, err := getMcConfigPath()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if _, e := os.Stat(configFile); e != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// isValidAlias - Check if alias valid.
|
||||
func isValidAlias(alias string) bool {
|
||||
return regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9-]+$").MatchString(alias)
|
||||
}
|
||||
|
||||
// getHostConfig retrieves host specific configuration such as access keys, signature type.
|
||||
func getHostConfig(alias string) (*hostConfigV8, *probe.Error) {
|
||||
mcCfg, err := loadMcConfig()
|
||||
if err != nil {
|
||||
return nil, err.Trace(alias)
|
||||
}
|
||||
|
||||
// if host is exact return quickly.
|
||||
if _, ok := mcCfg.Hosts[alias]; ok {
|
||||
hostCfg := mcCfg.Hosts[alias]
|
||||
return &hostCfg, nil
|
||||
}
|
||||
|
||||
// return error if cannot be matched.
|
||||
return nil, errNoMatchingHost(alias).Trace(alias)
|
||||
}
|
||||
|
||||
// mustGetHostConfig retrieves host specific configuration such as access keys, signature type.
|
||||
func mustGetHostConfig(alias string) *hostConfigV8 {
|
||||
hostCfg, _ := getHostConfig(alias)
|
||||
return hostCfg
|
||||
}
|
||||
|
||||
// expandAlias expands aliased URL if any match is found, returns as is otherwise.
|
||||
func expandAlias(aliasedURL string) (alias string, urlStr string, hostCfg *hostConfigV8, err *probe.Error) {
|
||||
// Extract alias from the URL.
|
||||
alias, path := url2Alias(aliasedURL)
|
||||
|
||||
// Find the matching alias entry and expand the URL.
|
||||
if hostCfg = mustGetHostConfig(alias); hostCfg != nil {
|
||||
return alias, urlJoinPath(hostCfg.URL, path), hostCfg, nil
|
||||
}
|
||||
return "", aliasedURL, nil, nil // No matching entry found. Return original URL as is.
|
||||
}
|
||||
|
||||
// mustExpandAlias expands aliased URL if any match is found, returns as is otherwise.
|
||||
func mustExpandAlias(aliasedURL string) (alias string, urlStr string, hostCfg *hostConfigV8) {
|
||||
alias, urlStr, hostCfg, _ = expandAlias(aliasedURL)
|
||||
return alias, urlStr, hostCfg
|
||||
}
|
||||
427
command/cp-main.go
Normal file
427
command/cp-main.go
Normal file
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// cp command flags.
|
||||
var (
|
||||
cpFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of cp.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "recursive, r",
|
||||
Usage: "Copy recursively.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Copy command.
|
||||
var cpCmd = cli.Command{
|
||||
Name: "cp",
|
||||
Usage: "Copy one or more objects to a target.",
|
||||
Action: mainCopy,
|
||||
Flags: append(cpFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] SOURCE [SOURCE...] TARGET
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Copy a list of objects from local file system to Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} Music/*.ogg s3/jukebox/
|
||||
|
||||
2. Copy a folder recursively from Minio cloud storage to Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} --recursive play/mybucket/burningman2011/ s3/mybucket/
|
||||
|
||||
3. Copy multiple local folders recursively to Minio cloud storage.
|
||||
$ mc {{.Name}} --recursive backup/2014/ backup/2015/ play/archive/
|
||||
|
||||
4. Copy a bucket recursively from aliased Amazon S3 cloud storage to local filesystem on Windows.
|
||||
$ mc {{.Name}} --recursive s3\documents\2014\ C:\Backups\2014
|
||||
|
||||
5. Copy an object with name containing unicode characters to Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} 本語 s3/andoria/
|
||||
|
||||
6. Copy a local folder with space separated characters to Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} --recursive 'workdir/documents/May 2014/' s3/miniocloud
|
||||
`,
|
||||
}
|
||||
|
||||
// copyMessage container for file copy messages
|
||||
type copyMessage struct {
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
// String colorized copy message
|
||||
func (c copyMessage) String() string {
|
||||
return console.Colorize("Copy", fmt.Sprintf("‘%s’ -> ‘%s’", c.Source, c.Target))
|
||||
}
|
||||
|
||||
// JSON jsonified copy message
|
||||
func (c copyMessage) JSON() string {
|
||||
c.Status = "success"
|
||||
copyMessageBytes, e := json.Marshal(c)
|
||||
fatalIf(probe.NewError(e), "Failed to marshal copy message.")
|
||||
|
||||
return string(copyMessageBytes)
|
||||
}
|
||||
|
||||
// copyStatMessage container for copy accounting message
|
||||
type copyStatMessage struct {
|
||||
Total int64
|
||||
Transferred int64
|
||||
Speed float64
|
||||
}
|
||||
|
||||
const (
|
||||
// 5GiB.
|
||||
fiveGB = 5 * 1024 * 1024 * 1024
|
||||
)
|
||||
|
||||
// copyStatMessage copy accounting message
|
||||
func (c copyStatMessage) String() string {
|
||||
speedBox := pb.Format(int64(c.Speed)).To(pb.U_BYTES).String()
|
||||
if speedBox == "" {
|
||||
speedBox = "0 MB"
|
||||
} else {
|
||||
speedBox = speedBox + "/s"
|
||||
}
|
||||
message := fmt.Sprintf("Total: %s, Transferred: %s, Speed: %s", pb.Format(c.Total).To(pb.U_BYTES),
|
||||
pb.Format(c.Transferred).To(pb.U_BYTES), speedBox)
|
||||
return message
|
||||
}
|
||||
|
||||
// doCopy - Copy a singe file from source to destination
|
||||
func doCopy(cpURLs copyURLs, progressReader *progressBar, accountingReader *accounter) copyURLs {
|
||||
if cpURLs.Error != nil {
|
||||
cpURLs.Error = cpURLs.Error.Trace()
|
||||
return cpURLs
|
||||
}
|
||||
|
||||
if !globalQuiet && !globalJSON {
|
||||
progressReader = progressReader.SetCaption(cpURLs.SourceContent.URL.String() + ": ")
|
||||
}
|
||||
|
||||
sourceAlias := cpURLs.SourceAlias
|
||||
sourceURL := cpURLs.SourceContent.URL
|
||||
targetAlias := cpURLs.TargetAlias
|
||||
targetURL := cpURLs.TargetContent.URL
|
||||
length := cpURLs.SourceContent.Size
|
||||
|
||||
var progress io.Reader
|
||||
if globalQuiet || globalJSON {
|
||||
sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path))
|
||||
targetPath := filepath.ToSlash(filepath.Join(targetAlias, targetURL.Path))
|
||||
printMsg(copyMessage{
|
||||
Source: sourcePath,
|
||||
Target: targetPath,
|
||||
})
|
||||
// Proxy reader to accounting reader only during quiet mode.
|
||||
if globalQuiet || globalJSON {
|
||||
progress = accountingReader
|
||||
}
|
||||
} else {
|
||||
// Set up progress reader.
|
||||
progress = progressReader.ProgressBar
|
||||
}
|
||||
// If source size is <= 5GB and operation is across same server type try to use Copy.
|
||||
if length <= fiveGB && (sourceURL.Type == targetURL.Type) {
|
||||
// FS -> FS Copy includes alias in path.
|
||||
if sourceURL.Type == fileSystem {
|
||||
sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path))
|
||||
err := copySourceStreamFromAlias(targetAlias, targetURL.String(), sourcePath, length, progress)
|
||||
if err != nil {
|
||||
cpURLs.Error = err.Trace(sourceURL.String())
|
||||
return cpURLs
|
||||
}
|
||||
} else if sourceURL.Type == objectStorage {
|
||||
// If source/target are object storage their aliases must be the same.
|
||||
if sourceAlias == targetAlias {
|
||||
// Do not include alias inside path for ObjStore -> ObjStore.
|
||||
err := copySourceStreamFromAlias(targetAlias, targetURL.String(), sourceURL.Path, length, progress)
|
||||
if err != nil {
|
||||
cpURLs.Error = err.Trace(sourceURL.String())
|
||||
return cpURLs
|
||||
}
|
||||
} else {
|
||||
reader, err := getSourceStreamFromAlias(sourceAlias, sourceURL.String())
|
||||
if err != nil {
|
||||
cpURLs.Error = err.Trace(sourceURL.String())
|
||||
return cpURLs
|
||||
}
|
||||
_, err = putTargetStreamFromAlias(targetAlias, targetURL.String(), reader, length, progress)
|
||||
if err != nil {
|
||||
cpURLs.Error = err.Trace(targetURL.String())
|
||||
return cpURLs
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard GET/PUT for size > 5GB.
|
||||
reader, err := getSourceStreamFromAlias(sourceAlias, sourceURL.String())
|
||||
if err != nil {
|
||||
cpURLs.Error = err.Trace(sourceURL.String())
|
||||
return cpURLs
|
||||
}
|
||||
_, err = putTargetStreamFromAlias(targetAlias, targetURL.String(), reader, length, progress)
|
||||
if err != nil {
|
||||
cpURLs.Error = err.Trace(targetURL.String())
|
||||
return cpURLs
|
||||
}
|
||||
}
|
||||
cpURLs.Error = nil // just for safety
|
||||
return cpURLs
|
||||
}
|
||||
|
||||
// doCopyFake - Perform a fake copy to update the progress bar appropriately.
|
||||
func doCopyFake(cpURLs copyURLs, progressReader *progressBar) copyURLs {
|
||||
if !globalQuiet && !globalJSON {
|
||||
progressReader.ProgressBar.Add64(cpURLs.SourceContent.Size)
|
||||
}
|
||||
return cpURLs
|
||||
}
|
||||
|
||||
// doPrepareCopyURLs scans the source URL and prepares a list of objects for copying.
|
||||
func doPrepareCopyURLs(session *sessionV7, trapCh <-chan 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
|
||||
|
||||
var totalBytes int64
|
||||
var totalObjects int
|
||||
|
||||
// Access recursive flag inside the session header.
|
||||
isRecursive := session.Header.CommandBoolFlags["recursive"]
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
URLsCh := prepareCopyURLs(sourceURLs, targetURL, isRecursive)
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case cpURLs, ok := <-URLsCh:
|
||||
if !ok { // Done with URL preparation
|
||||
done = true
|
||||
break
|
||||
}
|
||||
if cpURLs.Error != nil {
|
||||
// Print in new line and adjust to top so that we don't print over the ongoing scan bar
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
if strings.Contains(cpURLs.Error.ToGoError().Error(), " is a folder.") {
|
||||
errorIf(cpURLs.Error.Trace(), "Folder cannot be copied. Please use ‘...’ suffix.")
|
||||
} else {
|
||||
errorIf(cpURLs.Error.Trace(), "Unable to prepare URL for copying.")
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
jsonData, e := json.Marshal(cpURLs)
|
||||
if e != nil {
|
||||
session.Delete()
|
||||
fatalIf(probe.NewError(e), "Unable to prepare URL for copying. Error in JSON marshaling.")
|
||||
}
|
||||
fmt.Fprintln(dataFP, string(jsonData))
|
||||
if !globalQuiet && !globalJSON {
|
||||
scanBar(cpURLs.SourceContent.URL.String())
|
||||
}
|
||||
|
||||
totalBytes += cpURLs.SourceContent.Size
|
||||
totalObjects++
|
||||
case <-trapCh:
|
||||
// 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()
|
||||
}
|
||||
|
||||
func doCopySession(session *sessionV7) {
|
||||
trapCh := signalTrap(os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
if !session.HasData() {
|
||||
doPrepareCopyURLs(session, trapCh)
|
||||
}
|
||||
|
||||
// Enable accounting reader by default.
|
||||
accntReader := newAccounter(session.Header.TotalBytes)
|
||||
|
||||
// Prepare URL scanner from session data file.
|
||||
urlScanner := bufio.NewScanner(session.NewDataReader())
|
||||
// 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)
|
||||
|
||||
// Enable progress bar reader only during default mode.
|
||||
var progressReader *progressBar
|
||||
if !globalQuiet && !globalJSON { // set up progress bar
|
||||
progressReader = newProgressBar(session.Header.TotalBytes)
|
||||
}
|
||||
|
||||
// Wait on status of doCopy() operation.
|
||||
var statusCh = make(chan copyURLs)
|
||||
|
||||
// Add a wait group.
|
||||
var wg = new(sync.WaitGroup)
|
||||
wg.Add(1)
|
||||
|
||||
// Go routine to monitor signal traps if any.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-trapCh:
|
||||
// Receive interrupt notification.
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
session.CloseAndDie()
|
||||
case cpURLs, ok := <-statusCh:
|
||||
// Status channel is closed, we should return.
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if cpURLs.Error == nil {
|
||||
session.Header.LastCopied = cpURLs.SourceContent.URL.String()
|
||||
session.Save()
|
||||
} else {
|
||||
// Print in new line and adjust to top so that we
|
||||
// don't print over the ongoing progress bar.
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
errorIf(cpURLs.Error.Trace(cpURLs.SourceContent.URL.String()),
|
||||
fmt.Sprintf("Failed to copy ‘%s’.", cpURLs.SourceContent.URL.String()))
|
||||
// For all non critical errors we can continue for the
|
||||
// remaining files.
|
||||
switch cpURLs.Error.ToGoError().(type) {
|
||||
// Handle this specifically for filesystem related errors.
|
||||
case BrokenSymlink, TooManyLevelsSymlink, PathNotFound, PathInsufficientPermission:
|
||||
continue
|
||||
// Handle these specifically for object storage related errors.
|
||||
case BucketNameEmpty, ObjectMissing, ObjectAlreadyExists, ObjectAlreadyExistsAsDirectory, BucketDoesNotExist, BucketInvalid, ObjectOnGlacier:
|
||||
continue
|
||||
}
|
||||
// For critical errors we should exit. Session
|
||||
// can be resumed after the user figures out
|
||||
// the problem.
|
||||
session.CloseAndDie()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Loop through all urls.
|
||||
for urlScanner.Scan() {
|
||||
var cpURLs copyURLs
|
||||
// Unmarshal copyURLs from each line.
|
||||
json.Unmarshal([]byte(urlScanner.Text()), &cpURLs)
|
||||
|
||||
// Verify if previously copied, notify progress bar.
|
||||
if isCopied(cpURLs.SourceContent.URL.String()) {
|
||||
statusCh <- doCopyFake(cpURLs, progressReader)
|
||||
} else {
|
||||
statusCh <- doCopy(cpURLs, progressReader, accntReader)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the goroutine.
|
||||
close(statusCh)
|
||||
|
||||
// Wait for the goroutines to finish.
|
||||
wg.Wait()
|
||||
|
||||
if !globalQuiet && !globalJSON {
|
||||
if progressReader.ProgressBar.Get() > 0 {
|
||||
progressReader.ProgressBar.Finish()
|
||||
}
|
||||
} else {
|
||||
accntStat := accntReader.Stat()
|
||||
cpStatMessage := copyStatMessage{
|
||||
Total: accntStat.Total,
|
||||
Transferred: accntStat.Transferred,
|
||||
Speed: accntStat.Speed,
|
||||
}
|
||||
console.Println(console.Colorize("Copy", cpStatMessage.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// mainCopy is the entry point for cp command.
|
||||
func mainCopy(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'copy' cli arguments.
|
||||
checkCopySyntax(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("Copy", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
session := newSessionV7()
|
||||
session.Header.CommandType = "cp"
|
||||
session.Header.CommandBoolFlags["recursive"] = ctx.Bool("recursive")
|
||||
|
||||
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 = ctx.Args()
|
||||
doCopySession(session)
|
||||
session.Delete()
|
||||
}
|
||||
152
command/cp-url-syntax.go
Normal file
152
command/cp-url-syntax.go
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/minio/cli"
|
||||
)
|
||||
|
||||
func checkCopySyntax(ctx *cli.Context) {
|
||||
if len(ctx.Args()) < 2 {
|
||||
cli.ShowCommandHelpAndExit(ctx, "cp", 1) // last argument is exit code.
|
||||
}
|
||||
|
||||
// extract URLs.
|
||||
URLs := ctx.Args()
|
||||
if len(URLs) < 2 {
|
||||
fatalIf(errDummy().Trace(ctx.Args()...), fmt.Sprintf("Unable to parse source and target arguments."))
|
||||
}
|
||||
|
||||
srcURLs := URLs[:len(URLs)-1]
|
||||
tgtURL := URLs[len(URLs)-1]
|
||||
isRecursive := ctx.Bool("recursive")
|
||||
|
||||
/****** Generic Invalid Rules *******/
|
||||
// Verify if source(s) exists.
|
||||
for _, srcURL := range srcURLs {
|
||||
_, _, err := url2Stat(srcURL)
|
||||
if err != nil {
|
||||
fatalIf(err.Trace(srcURL), fmt.Sprintf("Unable to stat '%s'.", srcURL))
|
||||
}
|
||||
}
|
||||
|
||||
// Check if bucket name is passed for URL type arguments.
|
||||
url := newClientURL(tgtURL)
|
||||
if url.Host != "" {
|
||||
// This check is for type URL.
|
||||
if !isURLVirtualHostStyle(url.Host) {
|
||||
if url.Path == string(url.Separator) {
|
||||
fatalIf(errInvalidArgument().Trace(), fmt.Sprintf("Target ‘%s’ does not contain bucket name.", tgtURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Guess CopyURLsType based on source and target URLs.
|
||||
copyURLsType, err := guessCopyURLType(srcURLs, tgtURL, isRecursive)
|
||||
if err != nil {
|
||||
fatalIf(errInvalidArgument().Trace(), "Unable to guess the type of copy operation.")
|
||||
}
|
||||
switch copyURLsType {
|
||||
case copyURLsTypeA: // File -> File.
|
||||
checkCopySyntaxTypeA(srcURLs, tgtURL)
|
||||
case copyURLsTypeB: // File -> Folder.
|
||||
checkCopySyntaxTypeB(srcURLs, tgtURL)
|
||||
case copyURLsTypeC: // Folder... -> Folder.
|
||||
checkCopySyntaxTypeC(srcURLs, tgtURL, isRecursive)
|
||||
case copyURLsTypeD: // File1...FileN -> Folder.
|
||||
checkCopySyntaxTypeD(srcURLs, tgtURL)
|
||||
default:
|
||||
fatalIf(errInvalidArgument().Trace(), "Unable to guess the type of copy operation.")
|
||||
}
|
||||
}
|
||||
|
||||
// checkCopySyntaxTypeA verifies if the source and target are valid file arguments.
|
||||
func checkCopySyntaxTypeA(srcURLs []string, tgtURL string) {
|
||||
// Check source.
|
||||
if len(srcURLs) != 1 {
|
||||
fatalIf(errInvalidArgument().Trace(), "Invalid number of source arguments.")
|
||||
}
|
||||
srcURL := srcURLs[0]
|
||||
_, srcContent, err := url2Stat(srcURL)
|
||||
fatalIf(err.Trace(srcURL), "Unable to stat source ‘"+srcURL+"’.")
|
||||
|
||||
if !srcContent.Type.IsRegular() {
|
||||
fatalIf(errInvalidArgument().Trace(), "Source ‘"+srcURL+"’ is not a file.")
|
||||
}
|
||||
}
|
||||
|
||||
// checkCopySyntaxTypeB verifies if the source is a valid file and target is a valid folder.
|
||||
func checkCopySyntaxTypeB(srcURLs []string, tgtURL string) {
|
||||
// Check source.
|
||||
if len(srcURLs) != 1 {
|
||||
fatalIf(errInvalidArgument().Trace(), "Invalid number of source arguments.")
|
||||
}
|
||||
srcURL := srcURLs[0]
|
||||
_, srcContent, err := url2Stat(srcURL)
|
||||
fatalIf(err.Trace(srcURL), "Unable to stat source ‘"+srcURL+"’.")
|
||||
|
||||
if !srcContent.Type.IsRegular() {
|
||||
fatalIf(errInvalidArgument().Trace(srcURL), "Source ‘"+srcURL+"’ is not a file.")
|
||||
}
|
||||
|
||||
// Check target.
|
||||
if _, tgtContent, err := url2Stat(tgtURL); err == nil {
|
||||
if !tgtContent.Type.IsDir() {
|
||||
fatalIf(errInvalidArgument().Trace(tgtURL), "Target ‘"+tgtURL+"’ is not a folder.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkCopySyntaxTypeC verifies if the source is a valid recursive dir and target is a valid folder.
|
||||
func checkCopySyntaxTypeC(srcURLs []string, tgtURL string, isRecursive bool) {
|
||||
// Check source.
|
||||
if len(srcURLs) != 1 {
|
||||
fatalIf(errInvalidArgument().Trace(), "Invalid number of source arguments.")
|
||||
}
|
||||
|
||||
srcURL := srcURLs[0]
|
||||
_, srcContent, err := url2Stat(srcURL)
|
||||
// incomplete uploads are not necessary for copy operation, no need to verify for them.
|
||||
isIncomplete := false
|
||||
if err != nil && !isURLPrefixExists(srcURL, isIncomplete) {
|
||||
fatalIf(err.Trace(srcURL), "Unable to stat source ‘"+srcURL+"’.")
|
||||
}
|
||||
|
||||
if srcContent.Type.IsDir() && !isRecursive {
|
||||
fatalIf(errInvalidArgument().Trace(srcURL), "To copy a folder requires --recursive option.")
|
||||
}
|
||||
|
||||
// Check target.
|
||||
if _, tgtContent, err := url2Stat(tgtURL); err == nil {
|
||||
if !tgtContent.Type.IsDir() {
|
||||
fatalIf(errInvalidArgument().Trace(tgtURL), "Target ‘"+tgtURL+"’ is not a folder.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkCopySyntaxTypeD verifies if the source is a valid list of files and target is a valid folder.
|
||||
func checkCopySyntaxTypeD(srcURLs []string, tgtURL string) {
|
||||
// Source can be anything: file, dir, dir...
|
||||
// Check target if it is a dir
|
||||
if _, tgtContent, err := url2Stat(tgtURL); err == nil {
|
||||
if !tgtContent.Type.IsDir() {
|
||||
fatalIf(errInvalidArgument().Trace(tgtURL), "Target ‘"+tgtURL+"’ is not a folder.")
|
||||
}
|
||||
}
|
||||
}
|
||||
252
command/cp-url.go
Normal file
252
command/cp-url.go
Normal file
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
type copyURLs struct {
|
||||
SourceAlias string
|
||||
SourceContent *clientContent
|
||||
TargetAlias string
|
||||
TargetContent *clientContent
|
||||
Error *probe.Error `json:"-"`
|
||||
}
|
||||
|
||||
type copyURLsType uint8
|
||||
|
||||
// NOTE: All the parse rules should reduced to A: Copy(Source, Target).
|
||||
//
|
||||
// * VALID RULES
|
||||
// =======================
|
||||
// A: copy(f, f) -> copy(f, f)
|
||||
// B: copy(f, d) -> copy(f, d/f) -> []A
|
||||
// C: copy(d1..., d2) -> []copy(f, d2/d1/f) -> []A
|
||||
// D: copy([]f, d) -> []B
|
||||
|
||||
// * INVALID RULES
|
||||
// =========================
|
||||
// copy(d, f)
|
||||
// copy(d..., f)
|
||||
// copy([](f|d)..., f)
|
||||
|
||||
const (
|
||||
copyURLsTypeInvalid copyURLsType = iota
|
||||
copyURLsTypeA
|
||||
copyURLsTypeB
|
||||
copyURLsTypeC
|
||||
copyURLsTypeD
|
||||
)
|
||||
|
||||
// guessCopyURLType guesses the type of clientURL. This approach all allows prepareURL
|
||||
// functions to accurately report failure causes.
|
||||
func guessCopyURLType(sourceURLs []string, targetURL string, isRecursive bool) (copyURLsType, *probe.Error) {
|
||||
if len(sourceURLs) == 1 { // 1 Source, 1 Target
|
||||
sourceURL := sourceURLs[0]
|
||||
_, sourceContent, err := url2Stat(sourceURL)
|
||||
if err != nil {
|
||||
return copyURLsTypeInvalid, err
|
||||
}
|
||||
|
||||
// If recursion is ON, it is type C.
|
||||
// If source is a folder, it is Type C.
|
||||
if sourceContent.Type.IsDir() || isRecursive {
|
||||
return copyURLsTypeC, nil
|
||||
}
|
||||
|
||||
// If target is a folder, it is Type B.
|
||||
if isTargetURLDir(targetURL) {
|
||||
return copyURLsTypeB, nil
|
||||
}
|
||||
// else Type A.
|
||||
return copyURLsTypeA, nil
|
||||
}
|
||||
|
||||
// Multiple source args and target is a folder. It is Type D.
|
||||
if isTargetURLDir(targetURL) {
|
||||
return copyURLsTypeD, nil
|
||||
}
|
||||
|
||||
return copyURLsTypeInvalid, errInvalidArgument().Trace()
|
||||
}
|
||||
|
||||
// SINGLE SOURCE - Type A: copy(f, f) -> copy(f, f)
|
||||
// prepareCopyURLsTypeA - prepares target and source clientURLs for copying.
|
||||
func prepareCopyURLsTypeA(sourceURL string, targetURL string) copyURLs {
|
||||
// Extract alias before fiddling with the clientURL.
|
||||
sourceAlias, _, _ := mustExpandAlias(sourceURL)
|
||||
// Find alias and expanded clientURL.
|
||||
targetAlias, targetURL, _ := mustExpandAlias(targetURL)
|
||||
|
||||
_, sourceContent, err := url2Stat(sourceURL)
|
||||
if err != nil {
|
||||
// Source does not exist or insufficient privileges.
|
||||
return copyURLs{Error: err.Trace(sourceURL)}
|
||||
}
|
||||
if !sourceContent.Type.IsRegular() {
|
||||
// Source is not a regular file
|
||||
return copyURLs{Error: errInvalidSource(sourceURL).Trace(sourceURL)}
|
||||
}
|
||||
if sourceContent.URL.String() == targetURL {
|
||||
// source and target can not be same
|
||||
return copyURLs{Error: errSourceTargetSame(sourceURL).Trace(sourceURL)}
|
||||
}
|
||||
|
||||
// All OK.. We can proceed. Type A
|
||||
return makeCopyContentTypeA(sourceAlias, sourceContent, targetAlias, targetURL)
|
||||
}
|
||||
|
||||
// prepareCopyContentTypeA - makes CopyURLs content for copying.
|
||||
func makeCopyContentTypeA(sourceAlias string, sourceContent *clientContent, targetAlias string, targetURL string) copyURLs {
|
||||
return copyURLs{
|
||||
SourceAlias: sourceAlias,
|
||||
SourceContent: sourceContent,
|
||||
TargetAlias: targetAlias,
|
||||
TargetContent: &clientContent{URL: *newClientURL(targetURL)},
|
||||
}
|
||||
}
|
||||
|
||||
// SINGLE SOURCE - Type B: copy(f, d) -> copy(f, d/f) -> A
|
||||
// prepareCopyURLsTypeB - prepares target and source clientURLs for copying.
|
||||
func prepareCopyURLsTypeB(sourceURL string, targetURL string) copyURLs {
|
||||
// Extract alias before fiddling with the clientURL.
|
||||
sourceAlias, _, _ := mustExpandAlias(sourceURL)
|
||||
// Find alias and expanded clientURL.
|
||||
targetAlias, targetURL, _ := mustExpandAlias(targetURL)
|
||||
|
||||
_, sourceContent, err := url2Stat(sourceURL)
|
||||
if err != nil {
|
||||
// Source does not exist or insufficient privileges.
|
||||
return copyURLs{Error: err.Trace(sourceURL)}
|
||||
}
|
||||
|
||||
if !sourceContent.Type.IsRegular() {
|
||||
if sourceContent.Type.IsDir() {
|
||||
return copyURLs{Error: errSourceIsDir(sourceURL).Trace(sourceURL)}
|
||||
}
|
||||
// Source is not a regular file.
|
||||
return copyURLs{Error: errInvalidSource(sourceURL).Trace(sourceURL)}
|
||||
}
|
||||
|
||||
// All OK.. We can proceed. Type B: source is a file, target is a folder and exists.
|
||||
return makeCopyContentTypeB(sourceAlias, sourceContent, targetAlias, targetURL)
|
||||
}
|
||||
|
||||
// makeCopyContentTypeB - CopyURLs content for copying.
|
||||
func makeCopyContentTypeB(sourceAlias string, sourceContent *clientContent, targetAlias string, targetURL string) copyURLs {
|
||||
// All OK.. We can proceed. Type B: source is a file, target is a folder and exists.
|
||||
targetURLParse := newClientURL(targetURL)
|
||||
targetURLParse.Path = filepath.ToSlash(filepath.Join(targetURLParse.Path, filepath.Base(sourceContent.URL.Path)))
|
||||
return makeCopyContentTypeA(sourceAlias, sourceContent, targetAlias, targetURLParse.String())
|
||||
}
|
||||
|
||||
// SINGLE SOURCE - Type C: copy(d1..., d2) -> []copy(d1/f, d1/d2/f) -> []A
|
||||
// prepareCopyRecursiveURLTypeC - prepares target and source clientURLs for copying.
|
||||
func prepareCopyURLsTypeC(sourceURL, targetURL string, isRecursive bool) <-chan copyURLs {
|
||||
// Extract alias before fiddling with the clientURL.
|
||||
sourceAlias, _, _ := mustExpandAlias(sourceURL)
|
||||
// Find alias and expanded clientURL.
|
||||
targetAlias, targetURL, _ := mustExpandAlias(targetURL)
|
||||
|
||||
copyURLsCh := make(chan copyURLs)
|
||||
go func(sourceURL, targetURL string, copyURLsCh chan copyURLs) {
|
||||
defer close(copyURLsCh)
|
||||
sourceClient, err := newClient(sourceURL)
|
||||
if err != nil {
|
||||
// Source initialization failed.
|
||||
copyURLsCh <- copyURLs{Error: err.Trace(sourceURL)}
|
||||
return
|
||||
}
|
||||
|
||||
for sourceContent := range sourceClient.List(isRecursive, false) {
|
||||
if sourceContent.Err != nil {
|
||||
// Listing failed.
|
||||
copyURLsCh <- copyURLs{Error: sourceContent.Err.Trace(sourceClient.GetURL().String())}
|
||||
continue
|
||||
}
|
||||
|
||||
if !sourceContent.Type.IsRegular() {
|
||||
// Source is not a regular file. Skip it for copy.
|
||||
continue
|
||||
}
|
||||
|
||||
// All OK.. We can proceed. Type B: source is a file, target is a folder and exists.
|
||||
copyURLsCh <- makeCopyContentTypeC(sourceAlias, sourceClient.GetURL(), sourceContent, targetAlias, targetURL)
|
||||
}
|
||||
}(sourceURL, targetURL, copyURLsCh)
|
||||
return copyURLsCh
|
||||
}
|
||||
|
||||
// makeCopyContentTypeC - CopyURLs content for copying.
|
||||
func makeCopyContentTypeC(sourceAlias string, sourceURL clientURL, sourceContent *clientContent, targetAlias string, targetURL string) copyURLs {
|
||||
newSourceURL := sourceContent.URL
|
||||
pathSeparatorIndex := strings.LastIndex(sourceURL.Path, string(sourceURL.Separator))
|
||||
newSourceSuffix := filepath.ToSlash(newSourceURL.Path)
|
||||
if pathSeparatorIndex > 1 {
|
||||
sourcePrefix := filepath.ToSlash(sourceURL.Path[:pathSeparatorIndex])
|
||||
newSourceSuffix = strings.TrimPrefix(newSourceSuffix, sourcePrefix)
|
||||
}
|
||||
newTargetURL := urlJoinPath(targetURL, newSourceSuffix)
|
||||
return makeCopyContentTypeA(sourceAlias, sourceContent, targetAlias, newTargetURL)
|
||||
}
|
||||
|
||||
// MULTI-SOURCE - Type D: copy([](f|d...), d) -> []B
|
||||
// prepareCopyURLsTypeE - prepares target and source clientURLs for copying.
|
||||
func prepareCopyURLsTypeD(sourceURLs []string, targetURL string, isRecursive bool) <-chan copyURLs {
|
||||
copyURLsCh := make(chan copyURLs)
|
||||
go func(sourceURLs []string, targetURL string, copyURLsCh chan copyURLs) {
|
||||
defer close(copyURLsCh)
|
||||
for _, sourceURL := range sourceURLs {
|
||||
for cpURLs := range prepareCopyURLsTypeC(sourceURL, targetURL, isRecursive) {
|
||||
copyURLsCh <- cpURLs
|
||||
}
|
||||
}
|
||||
}(sourceURLs, targetURL, copyURLsCh)
|
||||
return copyURLsCh
|
||||
}
|
||||
|
||||
// prepareCopyURLs - prepares target and source clientURLs for copying.
|
||||
func prepareCopyURLs(sourceURLs []string, targetURL string, isRecursive bool) <-chan copyURLs {
|
||||
copyURLsCh := make(chan copyURLs)
|
||||
go func(sourceURLs []string, targetURL string, copyURLsCh chan copyURLs) {
|
||||
defer close(copyURLsCh)
|
||||
cpType, err := guessCopyURLType(sourceURLs, targetURL, isRecursive)
|
||||
fatalIf(err.Trace(), "Unable to guess the type of copy operation.")
|
||||
switch cpType {
|
||||
case copyURLsTypeA:
|
||||
copyURLsCh <- prepareCopyURLsTypeA(sourceURLs[0], targetURL)
|
||||
case copyURLsTypeB:
|
||||
copyURLsCh <- prepareCopyURLsTypeB(sourceURLs[0], targetURL)
|
||||
case copyURLsTypeC:
|
||||
for cURLs := range prepareCopyURLsTypeC(sourceURLs[0], targetURL, isRecursive) {
|
||||
copyURLsCh <- cURLs
|
||||
}
|
||||
case copyURLsTypeD:
|
||||
for cURLs := range prepareCopyURLsTypeD(sourceURLs, targetURL, isRecursive) {
|
||||
copyURLsCh <- cURLs
|
||||
}
|
||||
default:
|
||||
copyURLsCh <- copyURLs{Error: errInvalidArgument().Trace(sourceURLs...)}
|
||||
}
|
||||
}(sourceURLs, targetURL, copyURLsCh)
|
||||
|
||||
return copyURLsCh
|
||||
}
|
||||
66
command/damerau-levenshtein.go
Normal file
66
command/damerau-levenshtein.go
Normal file
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Minio Client (C) 2014-2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
// Returns the minimum value of a slice of integers
|
||||
func minimum(integers []int) (minVal int) {
|
||||
minVal = math.MaxInt32
|
||||
for _, v := range integers {
|
||||
if v < minVal {
|
||||
minVal = v
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DamerauLevenshteinDistance calculates distance between two strings using an algorithm
|
||||
// described in https://en.wikipedia.org/wiki/Damerau-Levenshtein_distance
|
||||
func DamerauLevenshteinDistance(a string, b string) int {
|
||||
d := make([][]int, len(a)+1)
|
||||
for i := 1; i <= len(a)+1; i++ {
|
||||
d[i-1] = make([]int, len(b)+1)
|
||||
}
|
||||
for i := 0; i <= len(a); i++ {
|
||||
d[i][0] = i
|
||||
}
|
||||
for j := 0; j <= len(b); j++ {
|
||||
d[0][j] = j
|
||||
}
|
||||
for i := 1; i <= len(a); i++ {
|
||||
for j := 1; j <= len(b); j++ {
|
||||
cost := 0
|
||||
if a[i-1] == b[j-1] {
|
||||
cost = 0
|
||||
} else {
|
||||
cost = 1
|
||||
}
|
||||
d[i][j] = minimum([]int{
|
||||
d[i-1][j] + 1,
|
||||
d[i][j-1] + 1,
|
||||
d[i-1][j-1] + cost,
|
||||
})
|
||||
if i > 1 && j > 1 && a[i-1] == b[j-2] && a[i-2] == b[j-1] {
|
||||
d[i][j] = minimum([]int{d[i][j], d[i-2][j-2] + cost}) // transposition
|
||||
}
|
||||
}
|
||||
}
|
||||
return d[len(a)][len(b)]
|
||||
}
|
||||
204
command/diff-main.go
Normal file
204
command/diff-main.go
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// diff specific flags.
|
||||
var (
|
||||
diffFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of diff.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Compute differences between two files or folders.
|
||||
var diffCmd = cli.Command{
|
||||
Name: "diff",
|
||||
Usage: "Compute differences between two folders.",
|
||||
Description: "Diff only lists missing objects or objects with size differences. It *DOES NOT* compare contents. i.e. Objects of same name and size, but differ in contents are not noticed.",
|
||||
Action: mainDiff,
|
||||
Flags: append(diffFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] FIRST SECOND
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
DESCRIPTION:
|
||||
{{.Description}}
|
||||
|
||||
EXAMPLES:
|
||||
1. Compare a local folder with a folder on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} ~/Photos s3/MyBucket/Photos
|
||||
|
||||
2. Compare two different folders on a local filesystem.
|
||||
$ mc {{.Name}} ~/Photos /Media/Backup/Photos
|
||||
`,
|
||||
}
|
||||
|
||||
// diffMessage json container for diff messages
|
||||
type diffMessage struct {
|
||||
Status string `json:"status"`
|
||||
FirstURL string `json:"first"`
|
||||
SecondURL string `json:"second"`
|
||||
Diff differType `json:"diff"`
|
||||
Error *probe.Error `json:"error,omitempty"`
|
||||
firstContent *clientContent
|
||||
secondContent *clientContent
|
||||
}
|
||||
|
||||
// String colorized diff message
|
||||
func (d diffMessage) String() string {
|
||||
msg := ""
|
||||
switch d.Diff {
|
||||
case differInFirst:
|
||||
msg = console.Colorize("DiffMessage",
|
||||
"‘"+d.FirstURL+"’") + console.Colorize("DiffOnlyInFirst", " - only in first.")
|
||||
case differInSecond:
|
||||
msg = console.Colorize("DiffMessage",
|
||||
"‘"+d.SecondURL+"’") + console.Colorize("DiffOnlyInSecond", " - only in second.")
|
||||
case differInType:
|
||||
msg = console.Colorize("DiffMessage",
|
||||
"‘"+d.FirstURL+"’"+" and "+"‘"+d.SecondURL+"’") + console.Colorize("DiffType", " - differ in type.")
|
||||
case differInSize:
|
||||
msg = console.Colorize("DiffMessage",
|
||||
"‘"+d.FirstURL+"’"+" and "+"‘"+d.SecondURL+"’") + console.Colorize("DiffSize", " - differ in size.")
|
||||
default:
|
||||
fatalIf(errDummy().Trace(d.FirstURL, d.SecondURL),
|
||||
"Unhandled difference between ‘"+d.FirstURL+"’ and ‘"+d.SecondURL+"’.")
|
||||
}
|
||||
return msg
|
||||
|
||||
}
|
||||
|
||||
// JSON jsonified diff message
|
||||
func (d diffMessage) JSON() string {
|
||||
d.Status = "success"
|
||||
diffJSONBytes, e := json.Marshal(d)
|
||||
fatalIf(probe.NewError(e),
|
||||
"Unable to marshal diff message ‘"+d.FirstURL+"’, ‘"+d.SecondURL+"’ and ‘"+string(d.Diff)+"’.")
|
||||
return string(diffJSONBytes)
|
||||
}
|
||||
|
||||
func checkDiffSyntax(ctx *cli.Context) {
|
||||
if len(ctx.Args()) != 2 {
|
||||
cli.ShowCommandHelpAndExit(ctx, "diff", 1) // last argument is exit code
|
||||
}
|
||||
for _, arg := range ctx.Args() {
|
||||
if strings.TrimSpace(arg) == "" {
|
||||
fatalIf(errInvalidArgument().Trace(ctx.Args()...), "Unable to validate empty argument.")
|
||||
}
|
||||
}
|
||||
URLs := ctx.Args()
|
||||
firstURL := URLs[0]
|
||||
secondURL := URLs[1]
|
||||
|
||||
// Diff only works between two directories, verify them below.
|
||||
|
||||
// Verify if firstURL is accessible.
|
||||
_, firstContent, err := url2Stat(firstURL)
|
||||
if err != nil {
|
||||
fatalIf(err.Trace(firstURL), fmt.Sprintf("Unable to stat '%s'.", firstURL))
|
||||
}
|
||||
|
||||
// Verify if its a directory.
|
||||
if !firstContent.Type.IsDir() {
|
||||
fatalIf(errInvalidArgument().Trace(firstURL), fmt.Sprintf("‘%s’ is not a folder.", firstURL))
|
||||
}
|
||||
|
||||
// Verify if secondURL is accessible.
|
||||
_, secondContent, err := url2Stat(secondURL)
|
||||
if err != nil {
|
||||
fatalIf(err.Trace(secondURL), fmt.Sprintf("Unable to stat '%s'.", secondURL))
|
||||
}
|
||||
|
||||
// Verify if its a directory.
|
||||
if !secondContent.Type.IsDir() {
|
||||
fatalIf(errInvalidArgument().Trace(secondURL), fmt.Sprintf("‘%s’ is not a folder.", secondURL))
|
||||
}
|
||||
}
|
||||
|
||||
// doDiffMain runs the diff.
|
||||
func doDiffMain(firstURL, secondURL string) {
|
||||
// Source and targets are always directories
|
||||
sourceSeparator := string(newClientURL(firstURL).Separator)
|
||||
if !strings.HasSuffix(firstURL, sourceSeparator) {
|
||||
firstURL = firstURL + sourceSeparator
|
||||
}
|
||||
targetSeparator := string(newClientURL(secondURL).Separator)
|
||||
if !strings.HasSuffix(secondURL, targetSeparator) {
|
||||
secondURL = secondURL + targetSeparator
|
||||
}
|
||||
|
||||
// Expand aliased urls.
|
||||
firstAlias, firstURL, _ := mustExpandAlias(firstURL)
|
||||
secondAlias, secondURL, _ := mustExpandAlias(secondURL)
|
||||
|
||||
firstClient, err := newClientFromAlias(firstAlias, firstURL)
|
||||
if err != nil {
|
||||
fatalIf(err.Trace(firstAlias, firstURL, secondAlias, secondURL),
|
||||
fmt.Sprintf("Failed to diff '%s' and '%s'", firstURL, secondURL))
|
||||
}
|
||||
|
||||
secondClient, err := newClientFromAlias(secondAlias, secondURL)
|
||||
if err != nil {
|
||||
fatalIf(err.Trace(firstAlias, firstURL, secondAlias, secondURL),
|
||||
fmt.Sprintf("Failed to diff '%s' and '%s'", firstURL, secondURL))
|
||||
}
|
||||
|
||||
// Diff first and second urls.
|
||||
for diffMsg := range objectDifference(firstClient, secondClient, firstURL, secondURL) {
|
||||
printMsg(diffMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// mainDiff main for 'diff'.
|
||||
func mainDiff(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'diff' cli arguments.
|
||||
checkDiffSyntax(ctx)
|
||||
|
||||
// Additional command specific theme customization.
|
||||
console.SetColor("DiffMessage", color.New(color.FgGreen, color.Bold))
|
||||
console.SetColor("DiffOnlyInFirst", color.New(color.FgRed, color.Bold))
|
||||
console.SetColor("DiffType", color.New(color.FgYellow, color.Bold))
|
||||
console.SetColor("DiffSize", color.New(color.FgMagenta, color.Bold))
|
||||
console.SetColor("DiffTime", color.New(color.FgYellow, color.Bold))
|
||||
|
||||
URLs := ctx.Args()
|
||||
firstURL := URLs[0]
|
||||
secondURL := URLs[1]
|
||||
|
||||
doDiffMain(firstURL, secondURL)
|
||||
}
|
||||
191
command/difference.go
Normal file
191
command/difference.go
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// differType difference in type.
|
||||
type differType int
|
||||
|
||||
const (
|
||||
differInNone differType = iota // does not differ
|
||||
differInSize // differs in size
|
||||
differInType // only in source
|
||||
differInFirst // only in target
|
||||
differInSecond // differs in type, exfile/directory
|
||||
)
|
||||
|
||||
func (d differType) String() string {
|
||||
switch d {
|
||||
case differInNone:
|
||||
return ""
|
||||
case differInSize:
|
||||
return "size"
|
||||
case differInType:
|
||||
return "type"
|
||||
case differInFirst:
|
||||
return "only-in-first"
|
||||
case differInSecond:
|
||||
return "only-in-second"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// objectDifference function finds the difference between all objects
|
||||
// recursively in sorted order from source and target.
|
||||
func objectDifference(sourceClnt, targetClnt Client, sourceURL, targetURL string) (diffCh chan diffMessage) {
|
||||
var (
|
||||
srcEOF, tgtEOF bool
|
||||
srcOk, tgtOk bool
|
||||
srcCtnt, tgtCtnt *clientContent
|
||||
srcSuffix, tgtSuffix string
|
||||
)
|
||||
|
||||
// Set default values for listing.
|
||||
isRecursive := true // recursive is always true for diff.
|
||||
isIncomplete := false // we will not compare any incomplete objects.
|
||||
srcCh := sourceClnt.List(isRecursive, isIncomplete)
|
||||
tgtCh := targetClnt.List(isRecursive, isIncomplete)
|
||||
|
||||
diffCh = make(chan diffMessage, 1000)
|
||||
|
||||
go func() {
|
||||
|
||||
srcCtnt, srcOk = <-srcCh
|
||||
tgtCtnt, tgtOk = <-tgtCh
|
||||
|
||||
for {
|
||||
srcEOF = !srcOk
|
||||
tgtEOF = !tgtOk
|
||||
|
||||
// No objects from source AND target: Finish
|
||||
if srcEOF && tgtEOF {
|
||||
close(diffCh)
|
||||
break
|
||||
}
|
||||
|
||||
if !srcEOF && srcCtnt.Err != nil {
|
||||
switch srcCtnt.Err.ToGoError().(type) {
|
||||
// Handle this specifically for filesystem related errors.
|
||||
case BrokenSymlink, TooManyLevelsSymlink, PathNotFound, PathInsufficientPermission:
|
||||
errorIf(srcCtnt.Err.Trace(sourceURL, targetURL), fmt.Sprintf("Failed on '%s'", sourceURL))
|
||||
// Handle these specifically for object storage related errors.
|
||||
case BucketNameEmpty, ObjectMissing, ObjectAlreadyExists, BucketDoesNotExist, BucketInvalid, ObjectOnGlacier:
|
||||
errorIf(srcCtnt.Err.Trace(sourceURL, targetURL), fmt.Sprintf("Failed on '%s'", sourceURL))
|
||||
default:
|
||||
fatalIf(srcCtnt.Err.Trace(sourceURL, targetURL), fmt.Sprintf("Failed on '%s'", sourceURL))
|
||||
}
|
||||
srcCtnt, srcOk = <-srcCh
|
||||
continue
|
||||
}
|
||||
|
||||
if !tgtEOF && tgtCtnt.Err != nil {
|
||||
switch tgtCtnt.Err.ToGoError().(type) {
|
||||
// Handle this specifically for filesystem related errors.
|
||||
case BrokenSymlink, TooManyLevelsSymlink, PathNotFound, PathInsufficientPermission:
|
||||
errorIf(tgtCtnt.Err.Trace(sourceURL, targetURL), fmt.Sprintf("Failed on '%s'", targetURL))
|
||||
// Handle these specifically for object storage related errors.
|
||||
case BucketNameEmpty, ObjectMissing, ObjectAlreadyExists, BucketDoesNotExist, BucketInvalid, ObjectOnGlacier:
|
||||
errorIf(tgtCtnt.Err.Trace(sourceURL, targetURL), fmt.Sprintf("Failed on '%s'", targetURL))
|
||||
default:
|
||||
fatalIf(tgtCtnt.Err.Trace(sourceURL, targetURL), fmt.Sprintf("Failed on '%s'", targetURL))
|
||||
}
|
||||
tgtCtnt, tgtOk = <-tgtCh
|
||||
continue
|
||||
}
|
||||
|
||||
// If source doesn't have objects anymore, comparison becomes obvious
|
||||
if srcEOF {
|
||||
diffCh <- diffMessage{
|
||||
SecondURL: tgtCtnt.URL.String(),
|
||||
Diff: differInSecond,
|
||||
secondContent: tgtCtnt,
|
||||
}
|
||||
tgtCtnt, tgtOk = <-tgtCh
|
||||
continue
|
||||
}
|
||||
|
||||
// The same for target
|
||||
if tgtEOF {
|
||||
diffCh <- diffMessage{
|
||||
FirstURL: srcCtnt.URL.String(),
|
||||
Diff: differInFirst,
|
||||
firstContent: srcCtnt,
|
||||
}
|
||||
srcCtnt, srcOk = <-srcCh
|
||||
continue
|
||||
}
|
||||
|
||||
srcSuffix = strings.TrimPrefix(srcCtnt.URL.String(), sourceURL)
|
||||
tgtSuffix = strings.TrimPrefix(tgtCtnt.URL.String(), targetURL)
|
||||
|
||||
current := urlJoinPath(targetURL, srcSuffix)
|
||||
expected := urlJoinPath(targetURL, tgtSuffix)
|
||||
|
||||
if expected > current {
|
||||
diffCh <- diffMessage{
|
||||
FirstURL: srcCtnt.URL.String(),
|
||||
Diff: differInFirst,
|
||||
firstContent: srcCtnt,
|
||||
}
|
||||
srcCtnt, srcOk = <-srcCh
|
||||
continue
|
||||
}
|
||||
if expected == current {
|
||||
srcType, tgtType := srcCtnt.Type, tgtCtnt.Type
|
||||
srcSize, tgtSize := srcCtnt.Size, tgtCtnt.Size
|
||||
if srcType.IsRegular() && !tgtType.IsRegular() ||
|
||||
!srcType.IsRegular() && tgtType.IsRegular() {
|
||||
// Type differes. Source is never a directory.
|
||||
diffCh <- diffMessage{
|
||||
FirstURL: srcCtnt.URL.String(),
|
||||
SecondURL: tgtCtnt.URL.String(),
|
||||
Diff: differInType,
|
||||
firstContent: srcCtnt,
|
||||
secondContent: tgtCtnt,
|
||||
}
|
||||
} else if (srcType.IsRegular() && tgtType.IsRegular()) && srcSize != tgtSize {
|
||||
// Regular files differing in size.
|
||||
diffCh <- diffMessage{
|
||||
FirstURL: srcCtnt.URL.String(),
|
||||
SecondURL: tgtCtnt.URL.String(),
|
||||
Diff: differInSize,
|
||||
firstContent: srcCtnt,
|
||||
secondContent: tgtCtnt,
|
||||
}
|
||||
}
|
||||
// No differ
|
||||
srcCtnt, srcOk = <-srcCh
|
||||
tgtCtnt, tgtOk = <-tgtCh
|
||||
continue
|
||||
}
|
||||
// Differ in second
|
||||
diffCh <- diffMessage{
|
||||
SecondURL: tgtCtnt.URL.String(),
|
||||
Diff: differInSecond,
|
||||
secondContent: tgtCtnt,
|
||||
}
|
||||
tgtCtnt, tgtOk = <-tgtCh
|
||||
continue
|
||||
}
|
||||
}()
|
||||
|
||||
return diffCh
|
||||
}
|
||||
115
command/error.go
Normal file
115
command/error.go
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// causeMessage container for golang error messages
|
||||
type causeMessage struct {
|
||||
Message string `json:"message"`
|
||||
Error error `json:"error"`
|
||||
}
|
||||
|
||||
// errorMessage container for error messages
|
||||
type errorMessage struct {
|
||||
Message string `json:"message"`
|
||||
Cause causeMessage `json:"cause"`
|
||||
Type string `json:"type"`
|
||||
CallTrace []probe.TracePoint `json:"trace,omitempty"`
|
||||
SysInfo map[string]string `json:"sysinfo"`
|
||||
}
|
||||
|
||||
// fatalIf wrapper function which takes error and selectively prints stack frames if available on debug
|
||||
func fatalIf(err *probe.Error, msg string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if globalJSON {
|
||||
errorMsg := errorMessage{
|
||||
Message: msg,
|
||||
Type: "fatal",
|
||||
Cause: causeMessage{
|
||||
Message: err.ToGoError().Error(),
|
||||
Error: err.ToGoError(),
|
||||
},
|
||||
SysInfo: err.SysInfo,
|
||||
}
|
||||
if globalDebug {
|
||||
errorMsg.CallTrace = err.CallTrace
|
||||
}
|
||||
json, e := json.Marshal(struct {
|
||||
Status string `json:"status"`
|
||||
Error errorMessage `json:"error"`
|
||||
}{
|
||||
Status: "error",
|
||||
Error: errorMsg,
|
||||
})
|
||||
if e != nil {
|
||||
console.Fatalln(probe.NewError(e))
|
||||
}
|
||||
console.Println(string(json))
|
||||
console.Fatalln()
|
||||
}
|
||||
if !globalDebug {
|
||||
console.Fatalln(fmt.Sprintf("%s %s", msg, err.ToGoError()))
|
||||
}
|
||||
console.Fatalln(fmt.Sprintf("%s %s", msg, err))
|
||||
}
|
||||
|
||||
// errorIf synonymous with fatalIf but doesn't exit on error != nil
|
||||
func errorIf(err *probe.Error, msg string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if globalJSON {
|
||||
errorMsg := errorMessage{
|
||||
Message: msg,
|
||||
Type: "error",
|
||||
Cause: causeMessage{
|
||||
Message: err.ToGoError().Error(),
|
||||
Error: err.ToGoError(),
|
||||
},
|
||||
SysInfo: err.SysInfo,
|
||||
}
|
||||
if globalDebug {
|
||||
errorMsg.CallTrace = err.CallTrace
|
||||
}
|
||||
json, e := json.Marshal(struct {
|
||||
Status string `json:"status"`
|
||||
Error errorMessage `json:"error"`
|
||||
}{
|
||||
Status: "error",
|
||||
Error: errorMsg,
|
||||
})
|
||||
if e != nil {
|
||||
console.Fatalln(probe.NewError(e))
|
||||
}
|
||||
console.Println(string(json))
|
||||
return
|
||||
}
|
||||
if !globalDebug {
|
||||
console.Errorln(fmt.Sprintf("%s %s", msg, err.ToGoError()))
|
||||
return
|
||||
}
|
||||
console.Errorln(fmt.Sprintf("%s %s", msg, err))
|
||||
}
|
||||
56
command/flags.go
Normal file
56
command/flags.go
Normal file
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import "github.com/minio/cli"
|
||||
|
||||
// Collection of mc commands currently supported
|
||||
var commands = []cli.Command{}
|
||||
|
||||
// Collection of mc commands currently supported in a trie tree
|
||||
var commandsTree = newTrie()
|
||||
|
||||
// Collection of mc flags currently supported
|
||||
var globalFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config-folder, C",
|
||||
Value: mustGetMcConfigDir(),
|
||||
Usage: "Path to configuration folder.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "quiet, q",
|
||||
Usage: "Suppress chatty console output.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-color",
|
||||
Usage: "Disable color theme.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "Enable json formatted output.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "Enable debugging output.",
|
||||
},
|
||||
}
|
||||
|
||||
// registerCmd registers a cli command
|
||||
func registerCmd(cmd cli.Command) {
|
||||
commands = append(commands, cmd)
|
||||
commandsTree.Insert(cmd.Name)
|
||||
}
|
||||
23
command/fs-pathutils.go
Normal file
23
command/fs-pathutils.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this fs except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
func normalizePath(path string) string {
|
||||
return path
|
||||
}
|
||||
35
command/fs-pathutils_window.go
Normal file
35
command/fs-pathutils_window.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// +build windows
|
||||
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this fs except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func normalizePath(path string) string {
|
||||
if filepath.VolumeName(path) == "" && filepath.HasPrefix(path, "\\") {
|
||||
var err error
|
||||
path, err = syscall.FullPath(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return path
|
||||
}
|
||||
78
command/globals.go
Normal file
78
command/globals.go
Normal file
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This package contains all the global variables and constants. ONLY TO BE ACCESSED VIA GET/SET FUNCTIONS.
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
)
|
||||
|
||||
// mc configuration related constants.
|
||||
const (
|
||||
minGoVersion = ">= 1.6" // mc requires at least Go v1.6
|
||||
)
|
||||
|
||||
const (
|
||||
globalMCConfigVersion = "8"
|
||||
|
||||
globalMCConfigDir = ".mc/"
|
||||
globalMCConfigWindowsDir = "mc\\"
|
||||
globalMCConfigFile = "config.json"
|
||||
|
||||
// session config and shared urls related constants
|
||||
globalSessionDir = "session"
|
||||
globalSharedURLsDataDir = "share"
|
||||
|
||||
// Profile directory for dumping profiler outputs.
|
||||
globalProfileDir = "profile"
|
||||
)
|
||||
|
||||
var (
|
||||
globalQuiet = false // Quiet flag set via command line
|
||||
globalJSON = false // Json flag set via command line
|
||||
globalDebug = false // Debug flag set via command line
|
||||
globalNoColor = false // Debug flag set via command line
|
||||
// WHEN YOU ADD NEXT GLOBAL FLAG, MAKE SURE TO ALSO UPDATE SESSION CODE AND CODE BELOW.
|
||||
)
|
||||
|
||||
// Set global states. NOTE: It is deliberately kept monolithic to ensure we dont miss out any flags.
|
||||
func setGlobals(quiet, debug, json, noColor bool) {
|
||||
globalQuiet = quiet
|
||||
globalDebug = debug
|
||||
globalJSON = json
|
||||
globalNoColor = noColor
|
||||
|
||||
// Enable debug messages if requested.
|
||||
if globalDebug {
|
||||
console.DebugPrint = true
|
||||
}
|
||||
|
||||
// Disable colorified messages if requested.
|
||||
if globalNoColor {
|
||||
console.SetColorOff()
|
||||
}
|
||||
}
|
||||
|
||||
// Set global states. NOTE: It is deliberately kept monolithic to ensure we dont miss out any flags.
|
||||
func setGlobalsFromContext(ctx *cli.Context) {
|
||||
quiet := ctx.Bool("quiet") || ctx.GlobalBool("quiet")
|
||||
debug := ctx.Bool("debug") || ctx.GlobalBool("debug")
|
||||
json := ctx.Bool("json") || ctx.GlobalBool("json")
|
||||
noColor := ctx.Bool("no-color") || ctx.GlobalBool("no-color")
|
||||
setGlobals(quiet, debug, json, noColor)
|
||||
}
|
||||
161
command/ls-main.go
Normal file
161
command/ls-main.go
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
)
|
||||
|
||||
// ls specific flags.
|
||||
var (
|
||||
lsFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of ls.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "recursive, r",
|
||||
Usage: "List recursively.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "incomplete, I",
|
||||
Usage: "List incomplete uploads.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// list files and folders.
|
||||
var lsCmd = cli.Command{
|
||||
Name: "ls",
|
||||
Usage: "List files and folders.",
|
||||
Action: mainList,
|
||||
Flags: append(lsFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] TARGET [TARGET ...]
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. List buckets on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} s3
|
||||
|
||||
2. List buckets and all its contents from Amazon S3 cloud storage recursively.
|
||||
$ mc {{.Name}} --recursive s3
|
||||
|
||||
3. List all contents of mybucket on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} s3/mybucket/
|
||||
|
||||
4. List all contents of mybucket on Amazon S3 cloud storage on Microsoft Windows.
|
||||
$ mc {{.Name}} s3\mybucket\
|
||||
|
||||
5. List files recursively on a local filesystem on Microsoft Windows.
|
||||
$ mc {{.Name}} --recursive C:\Users\Worf\
|
||||
|
||||
6. List incomplete (previously failed) uploads of objects on Amazon S3.
|
||||
$ mc {{.Name}} --incomplete s3/mybucket
|
||||
`,
|
||||
}
|
||||
|
||||
// checkListSyntax - validate all the passed arguments
|
||||
func checkListSyntax(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if !ctx.Args().Present() {
|
||||
args = []string{"."}
|
||||
}
|
||||
for _, arg := range args {
|
||||
if strings.TrimSpace(arg) == "" {
|
||||
fatalIf(errInvalidArgument().Trace(args...), "Unable to validate empty argument.")
|
||||
}
|
||||
}
|
||||
// extract URLs.
|
||||
URLs := ctx.Args()
|
||||
isIncomplete := ctx.Bool("incomplete")
|
||||
|
||||
for _, url := range URLs {
|
||||
_, _, err := url2Stat(url)
|
||||
if err != nil && !isURLPrefixExists(url, isIncomplete) {
|
||||
// Bucket name empty is a valid error for 'ls myminio',
|
||||
// treat it as such.
|
||||
if _, ok := err.ToGoError().(BucketNameEmpty); ok {
|
||||
continue
|
||||
}
|
||||
fatalIf(err.Trace(url), "Unable to stat ‘"+url+"’.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// mainList - is a handler for mc ls command
|
||||
func mainList(ctx *cli.Context) {
|
||||
// Additional command specific theme customization.
|
||||
console.SetColor("File", color.New(color.Bold))
|
||||
console.SetColor("Dir", color.New(color.FgCyan, color.Bold))
|
||||
console.SetColor("Size", color.New(color.FgYellow))
|
||||
console.SetColor("Time", color.New(color.FgGreen))
|
||||
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'ls' cli arguments.
|
||||
checkListSyntax(ctx)
|
||||
|
||||
// Set command flags from context.
|
||||
isRecursive := ctx.Bool("recursive")
|
||||
isIncomplete := ctx.Bool("incomplete")
|
||||
|
||||
args := ctx.Args()
|
||||
// mimic operating system tool behavior.
|
||||
if !ctx.Args().Present() {
|
||||
args = []string{"."}
|
||||
}
|
||||
|
||||
for _, targetURL := range args {
|
||||
var clnt Client
|
||||
clnt, err := newClient(targetURL)
|
||||
fatalIf(err.Trace(targetURL), "Unable to initialize target ‘"+targetURL+"’.")
|
||||
|
||||
var st *clientContent
|
||||
if st, err = clnt.Stat(); err != nil {
|
||||
switch err.ToGoError().(type) {
|
||||
case BucketNameEmpty:
|
||||
// For aliases like ``mc ls s3`` it's acceptable to receive BucketNameEmpty error.
|
||||
// Nothing to do.
|
||||
default:
|
||||
fatalIf(err.Trace(targetURL), "Unable to initialize target ‘"+targetURL+"’.")
|
||||
}
|
||||
} else if st.Type.IsDir() {
|
||||
if !strings.HasSuffix(targetURL, string(clnt.GetURL().Separator)) {
|
||||
targetURL = targetURL + string(clnt.GetURL().Separator)
|
||||
}
|
||||
clnt, err = newClient(targetURL)
|
||||
fatalIf(err.Trace(targetURL), "Unable to initialize target ‘"+targetURL+"’.")
|
||||
}
|
||||
|
||||
err = doList(clnt, isRecursive, isIncomplete)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(clnt.GetURL().String()), "Unable to list target ‘"+clnt.GetURL().String()+"’.")
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
148
command/ls.go
Normal file
148
command/ls.go
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// printDate - human friendly formatted date.
|
||||
const (
|
||||
printDate = "2006-01-02 15:04:05 MST"
|
||||
)
|
||||
|
||||
// contentMessage container for content message structure.
|
||||
type contentMessage struct {
|
||||
Status string `json:"status"`
|
||||
Filetype string `json:"type"`
|
||||
Time time.Time `json:"lastModified"`
|
||||
Size int64 `json:"size"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
// String colorized string message.
|
||||
func (c contentMessage) String() string {
|
||||
message := console.Colorize("Time", fmt.Sprintf("[%s] ", c.Time.Format(printDate)))
|
||||
message = message + console.Colorize("Size", fmt.Sprintf("%6s ", humanize.IBytes(uint64(c.Size))))
|
||||
message = func() string {
|
||||
if c.Filetype == "folder" {
|
||||
return message + console.Colorize("Dir", fmt.Sprintf("%s", c.Key))
|
||||
}
|
||||
return message + console.Colorize("File", fmt.Sprintf("%s", c.Key))
|
||||
}()
|
||||
return message
|
||||
}
|
||||
|
||||
// JSON jsonified content message.
|
||||
func (c contentMessage) JSON() string {
|
||||
c.Status = "success"
|
||||
jsonMessageBytes, e := json.Marshal(c)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(jsonMessageBytes)
|
||||
}
|
||||
|
||||
// parseContent parse client Content container into printer struct.
|
||||
func parseContent(c *clientContent) contentMessage {
|
||||
content := contentMessage{}
|
||||
content.Time = c.Time.Local()
|
||||
|
||||
// guess file type.
|
||||
content.Filetype = func() string {
|
||||
if c.Type.IsDir() {
|
||||
return "folder"
|
||||
}
|
||||
return "file"
|
||||
}()
|
||||
|
||||
content.Size = c.Size
|
||||
// Convert OS Type to match console file printing style.
|
||||
content.Key = func() string {
|
||||
switch {
|
||||
// for windows make sure to print in 'windows' specific style.
|
||||
case runtime.GOOS == "windows":
|
||||
c.URL.Path = strings.Replace(c.URL.Path, "/", "\\", -1)
|
||||
c.URL.Path = strings.TrimSuffix(c.URL.Path, "\\")
|
||||
default:
|
||||
c.URL.Path = strings.TrimSuffix(c.URL.Path, "/")
|
||||
}
|
||||
if c.Type.IsDir() {
|
||||
switch {
|
||||
// for windows make sure to print in 'windows' specific style.
|
||||
case runtime.GOOS == "windows":
|
||||
return fmt.Sprintf("%s\\", c.URL.Path)
|
||||
default:
|
||||
return fmt.Sprintf("%s/", c.URL.Path)
|
||||
}
|
||||
}
|
||||
return c.URL.Path
|
||||
}()
|
||||
return content
|
||||
}
|
||||
|
||||
// doList - list all entities inside a folder.
|
||||
func doList(clnt Client, isRecursive, isIncomplete bool) *probe.Error {
|
||||
prefixPath := clnt.GetURL().Path
|
||||
separator := string(clnt.GetURL().Separator)
|
||||
if !strings.HasSuffix(prefixPath, separator) {
|
||||
prefixPath = prefixPath[:strings.LastIndex(prefixPath, separator)+1]
|
||||
}
|
||||
for content := range clnt.List(isRecursive, isIncomplete) {
|
||||
if content.Err != nil {
|
||||
switch content.Err.ToGoError().(type) {
|
||||
// handle this specifically for filesystem related errors.
|
||||
case BrokenSymlink:
|
||||
errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list broken link.")
|
||||
continue
|
||||
case TooManyLevelsSymlink:
|
||||
errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list too many levels link.")
|
||||
continue
|
||||
case PathNotFound:
|
||||
errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
|
||||
continue
|
||||
case PathInsufficientPermission:
|
||||
errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
|
||||
continue
|
||||
case ObjectOnGlacier:
|
||||
errorIf(content.Err.Trace(clnt.GetURL().String()), "")
|
||||
continue
|
||||
}
|
||||
errorIf(content.Err.Trace(clnt.GetURL().String()), "Unable to list folder.")
|
||||
continue
|
||||
}
|
||||
// Convert any os specific delimiters to "/".
|
||||
contentURL := filepath.ToSlash(content.URL.Path)
|
||||
prefixPath = filepath.ToSlash(prefixPath)
|
||||
|
||||
// Trim prefix path from the content path.
|
||||
contentURL = strings.TrimPrefix(contentURL, prefixPath)
|
||||
content.URL.Path = contentURL
|
||||
parsedContent := parseContent(content)
|
||||
// Print colorized or jsonized content info.
|
||||
printMsg(parsedContent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
17
command/ls_test.go
Normal file
17
command/ls_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
268
command/main.go
Normal file
268
command/main.go
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/pkg/profile"
|
||||
)
|
||||
|
||||
var (
|
||||
// global flags for mc.
|
||||
mcFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Show help.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Help template for mc
|
||||
var mcHelpTemplate = `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} {{if .Flags}}[FLAGS] {{end}}COMMAND{{if .Flags}} [COMMAND FLAGS | -h]{{end}} [ARGUMENTS...]
|
||||
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}{{if .Flags}}
|
||||
GLOBAL FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}{{end}}
|
||||
VERSION:
|
||||
` + mcVersion +
|
||||
`{{ "\n"}}{{range $key, $value := ExtraInfo}}
|
||||
{{$key}}:
|
||||
{{$value}}
|
||||
{{end}}`
|
||||
|
||||
// Main starts mc application
|
||||
func Main() {
|
||||
// Enable profiling supported modes are [cpu, mem, block].
|
||||
// ``MC_PROFILER`` supported options are [cpu, mem, block].
|
||||
switch os.Getenv("MC_PROFILER") {
|
||||
case "cpu":
|
||||
defer profile.Start(profile.CPUProfile, profile.ProfilePath(mustGetProfileDir())).Stop()
|
||||
case "mem":
|
||||
defer profile.Start(profile.MemProfile, profile.ProfilePath(mustGetProfileDir())).Stop()
|
||||
case "block":
|
||||
defer profile.Start(profile.BlockProfile, profile.ProfilePath(mustGetProfileDir())).Stop()
|
||||
}
|
||||
|
||||
probe.Init() // Set project's root source path.
|
||||
probe.SetAppInfo("Release-Tag", mcReleaseTag)
|
||||
probe.SetAppInfo("Commit", mcShortCommitID)
|
||||
|
||||
app := registerApp()
|
||||
app.Before = registerBefore
|
||||
app.ExtraInfo = func() map[string]string {
|
||||
if _, e := pb.GetTerminalWidth(); e != nil {
|
||||
globalQuiet = true
|
||||
}
|
||||
if globalDebug {
|
||||
return getSystemData()
|
||||
}
|
||||
return make(map[string]string)
|
||||
}
|
||||
|
||||
app.RunAndExitOnError()
|
||||
}
|
||||
|
||||
// Function invoked when invalid command is passed.
|
||||
func commandNotFound(ctx *cli.Context, command string) {
|
||||
msg := fmt.Sprintf("‘%s’ is not a mc command. See ‘mc --help’.", command)
|
||||
closestCommands := findClosestCommands(command)
|
||||
if len(closestCommands) > 0 {
|
||||
msg += fmt.Sprintf("\n\nDid you mean one of these?\n")
|
||||
if len(closestCommands) == 1 {
|
||||
cmd := closestCommands[0]
|
||||
msg += fmt.Sprintf(" ‘%s’", cmd)
|
||||
} else {
|
||||
for _, cmd := range closestCommands {
|
||||
msg += fmt.Sprintf(" ‘%s’\n", cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
fatalIf(errDummy().Trace(), msg)
|
||||
}
|
||||
|
||||
// Check for sane config environment early on and gracefully report.
|
||||
func checkConfig() {
|
||||
// Refresh the config once.
|
||||
loadMcConfig = loadMcConfigFactory()
|
||||
// Ensures config file is sane.
|
||||
config, err := loadMcConfig()
|
||||
// Verify if the path is accesible before validating the config
|
||||
fatalIf(err.Trace(mustGetMcConfigPath()), "Unable to access configuration file.")
|
||||
|
||||
// Validate and print error messges
|
||||
ok, errMsgs := validateConfigFile(config)
|
||||
if !ok {
|
||||
var errorMsg bytes.Buffer
|
||||
for index, errMsg := range errMsgs {
|
||||
// Print atmost 10 errors
|
||||
if index > 10 {
|
||||
break
|
||||
}
|
||||
errorMsg.WriteString(errMsg + "\n")
|
||||
}
|
||||
console.Fatalln(errorMsg.String())
|
||||
}
|
||||
}
|
||||
|
||||
func migrate() {
|
||||
// Fix broken config files if any.
|
||||
fixConfig()
|
||||
|
||||
// Migrate config files if any.
|
||||
migrateConfig()
|
||||
|
||||
// Migrate session files if any.
|
||||
migrateSession()
|
||||
|
||||
// Migrate shared urls if any.
|
||||
migrateShare()
|
||||
}
|
||||
|
||||
// Get os/arch/platform specific information.
|
||||
// Returns a map of current os/arch/platform/memstats.
|
||||
func getSystemData() map[string]string {
|
||||
host, e := os.Hostname()
|
||||
fatalIf(probe.NewError(e), "Unable to determine the hostname.")
|
||||
|
||||
memstats := &runtime.MemStats{}
|
||||
runtime.ReadMemStats(memstats)
|
||||
mem := fmt.Sprintf("Used: %s | Allocated: %s | UsedHeap: %s | AllocatedHeap: %s",
|
||||
pb.Format(int64(memstats.Alloc)).To(pb.U_BYTES),
|
||||
pb.Format(int64(memstats.TotalAlloc)).To(pb.U_BYTES),
|
||||
pb.Format(int64(memstats.HeapAlloc)).To(pb.U_BYTES),
|
||||
pb.Format(int64(memstats.HeapSys)).To(pb.U_BYTES))
|
||||
platform := fmt.Sprintf("Host: %s | OS: %s | Arch: %s", host, runtime.GOOS, runtime.GOARCH)
|
||||
goruntime := fmt.Sprintf("Version: %s | CPUs: %s", runtime.Version(), strconv.Itoa(runtime.NumCPU()))
|
||||
return map[string]string{
|
||||
"PLATFORM": platform,
|
||||
"RUNTIME": goruntime,
|
||||
"MEM": mem,
|
||||
}
|
||||
}
|
||||
|
||||
// initMC - initialize 'mc'.
|
||||
func initMC() {
|
||||
// Check if mc config exists.
|
||||
if !isMcConfigExists() {
|
||||
err := saveMcConfig(newMcConfig())
|
||||
fatalIf(err.Trace(), "Unable to save new mc config.")
|
||||
|
||||
console.Infoln("Configuration written to ‘" + mustGetMcConfigPath() + "’. Please update your access credentials.")
|
||||
}
|
||||
|
||||
// Check if mc session folder exists.
|
||||
if !isSessionDirExists() {
|
||||
fatalIf(createSessionDir().Trace(), "Unable to create session config folder.")
|
||||
}
|
||||
|
||||
// Check if mc share folder exists.
|
||||
if !isShareDirExists() {
|
||||
initShareConfig()
|
||||
}
|
||||
}
|
||||
|
||||
func registerBefore(ctx *cli.Context) error {
|
||||
// Check if mc was compiled using a supported version of Golang.
|
||||
checkGoVersion()
|
||||
|
||||
// Set the config folder.
|
||||
setMcConfigDir(ctx.GlobalString("config-folder"))
|
||||
|
||||
// Migrate any old version of config / state files to newer format.
|
||||
migrate()
|
||||
|
||||
// Initialize default config files.
|
||||
initMC()
|
||||
|
||||
// Set global flags.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// Check if config can be read.
|
||||
checkConfig()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findClosestCommands to match a given string with commands trie tree.
|
||||
func findClosestCommands(command string) []string {
|
||||
var closestCommands []string
|
||||
for _, value := range commandsTree.PrefixMatch(command) {
|
||||
closestCommands = append(closestCommands, value.(string))
|
||||
}
|
||||
sort.Strings(closestCommands)
|
||||
// Suggest other close commands - allow missed, wrongly added and even transposed characters
|
||||
for _, value := range commandsTree.walk(commandsTree.root) {
|
||||
if sort.SearchStrings(closestCommands, value.(string)) < len(closestCommands) {
|
||||
continue
|
||||
}
|
||||
// 2 is arbitrary and represents the max allowed number of typed errors
|
||||
if DamerauLevenshteinDistance(command, value.(string)) < 2 {
|
||||
closestCommands = append(closestCommands, value.(string))
|
||||
}
|
||||
}
|
||||
return closestCommands
|
||||
}
|
||||
|
||||
func registerApp() *cli.App {
|
||||
// Register all the commands (refer flags.go)
|
||||
registerCmd(lsCmd) // List contents of a bucket.
|
||||
registerCmd(mbCmd) // Make a bucket.
|
||||
registerCmd(catCmd) // Display contents of a file.
|
||||
registerCmd(pipeCmd) // Write contents of stdin to a file.
|
||||
registerCmd(shareCmd) // Share documents via URL.
|
||||
registerCmd(cpCmd) // Copy objects and files from multiple sources to single destination.
|
||||
registerCmd(mirrorCmd) // Mirror objects and files from single source to multiple destinations.
|
||||
registerCmd(diffCmd) // Computer differences between two files or folders.
|
||||
registerCmd(rmCmd) // Remove a file or bucket
|
||||
registerCmd(policyCmd) // Set policy permissions.
|
||||
registerCmd(sessionCmd) // Manage sessions for copy and mirror.
|
||||
registerCmd(configCmd) // Configure minio client.
|
||||
registerCmd(updateCmd) // Check for new software updates.
|
||||
registerCmd(versionCmd) // Print version.
|
||||
|
||||
app := cli.NewApp()
|
||||
app.Usage = "Minio Client for cloud storage and filesystems."
|
||||
app.Commands = commands
|
||||
app.Author = "Minio.io"
|
||||
app.Flags = append(mcFlags, globalFlags...)
|
||||
app.CustomAppHelpTemplate = mcHelpTemplate
|
||||
app.CommandNotFound = commandNotFound // handler function declared above.
|
||||
return app
|
||||
}
|
||||
|
||||
// mustGetProfilePath must get location that the profile will be written to.
|
||||
func mustGetProfileDir() string {
|
||||
return filepath.Join(mustGetMcConfigDir(), globalProfileDir)
|
||||
}
|
||||
135
command/mb-main.go
Normal file
135
command/mb-main.go
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
mbFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of mb.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "region",
|
||||
Value: "us-east-1",
|
||||
Usage: "Specify bucket region. Defaults to ‘us-east-1’.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// make a bucket or folder.
|
||||
var mbCmd = cli.Command{
|
||||
Name: "mb",
|
||||
Usage: "Make a bucket or folder.",
|
||||
Action: mainMakeBucket,
|
||||
Flags: append(mbFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] TARGET [TARGET...]
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Create a bucket on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} s3/mynewbucket
|
||||
|
||||
2. Create a new bucket on Google Cloud Storage.
|
||||
$ mc {{.Name}} gcs/miniocloud
|
||||
|
||||
4. Create a new bucket on Amazon S3 cloud storage in region ‘us-west-2’.
|
||||
$ mc {{.Name}} --region=us-west-2 s3/myregionbucket
|
||||
|
||||
5. Create a new directory including its missing parents (equivalent to ‘mkdir -p’).
|
||||
$ mc {{.Name}} /tmp/this/new/dir1
|
||||
|
||||
6. Create multiple directories including its missing parents (behavior similar to ‘mkdir -p’).
|
||||
$ mc {{.Name}} /mnt/sdb/mydisk /mnt/sdc/mydisk /mnt/sdd/mydisk
|
||||
`,
|
||||
}
|
||||
|
||||
// makeBucketMessage is container for make bucket success and failure messages.
|
||||
type makeBucketMessage struct {
|
||||
Status string `json:"status"`
|
||||
Bucket string `json:"bucket"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
// String colorized make bucket message.
|
||||
func (s makeBucketMessage) String() string {
|
||||
return console.Colorize("MakeBucket", "Bucket created successfully ‘"+s.Bucket+"’.")
|
||||
}
|
||||
|
||||
// JSON jsonified make bucket message.
|
||||
func (s makeBucketMessage) JSON() string {
|
||||
makeBucketJSONBytes, e := json.Marshal(s)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(makeBucketJSONBytes)
|
||||
}
|
||||
|
||||
// Validate command line arguments.
|
||||
func checkMakeBucketSyntax(ctx *cli.Context) {
|
||||
if !ctx.Args().Present() {
|
||||
cli.ShowCommandHelpAndExit(ctx, "mb", 1) // last argument is exit code
|
||||
}
|
||||
}
|
||||
|
||||
// mainMakeBucket is entry point for mb command.
|
||||
func mainMakeBucket(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'mb' cli arguments.
|
||||
checkMakeBucketSyntax(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("MakeBucket", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
// Save region.
|
||||
region := ctx.String("region")
|
||||
|
||||
for i := range ctx.Args() {
|
||||
targetURL := ctx.Args().Get(i)
|
||||
// Instantiate client for URL.
|
||||
clnt, err := newClient(targetURL)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(targetURL), "Invalid target ‘"+targetURL+"’.")
|
||||
continue
|
||||
}
|
||||
|
||||
// Make bucket.
|
||||
err = clnt.MakeBucket(region)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(targetURL), "Unable to make bucket ‘"+targetURL+"’.")
|
||||
continue
|
||||
}
|
||||
|
||||
// Successfully created a bucket.
|
||||
printMsg(makeBucketMessage{Status: "success", Bucket: targetURL})
|
||||
}
|
||||
}
|
||||
144
command/mc_test.go
Normal file
144
command/mc_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/minio/cli"
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
var customConfigDir string
|
||||
|
||||
func Test(t *testing.T) { TestingT(t) }
|
||||
|
||||
type TestSuite struct{}
|
||||
|
||||
var _ = Suite(&TestSuite{})
|
||||
|
||||
var server *httptest.Server
|
||||
var app *cli.App
|
||||
|
||||
func (s *TestSuite) SetUpSuite(c *C) {
|
||||
}
|
||||
|
||||
func (s *TestSuite) TearDownSuite(c *C) {
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestValidPERMS(c *C) {
|
||||
perms := accessPerms("none")
|
||||
c.Assert(perms.isValidAccessPERM(), Equals, true)
|
||||
c.Assert(string(perms), Equals, "none")
|
||||
perms = accessPerms("both")
|
||||
c.Assert(perms.isValidAccessPERM(), Equals, true)
|
||||
c.Assert(string(perms), Equals, "both")
|
||||
perms = accessPerms("download")
|
||||
c.Assert(perms.isValidAccessPERM(), Equals, true)
|
||||
c.Assert(string(perms), Equals, "download")
|
||||
perms = accessPerms("upload")
|
||||
c.Assert(perms.isValidAccessPERM(), Equals, true)
|
||||
c.Assert(string(perms), Equals, "upload")
|
||||
}
|
||||
|
||||
// Tests valid and invalid secret keys.
|
||||
func (s *TestSuite) TestValidSecretKeys(c *C) {
|
||||
c.Assert(isValidSecretKey("password"), Equals, true)
|
||||
c.Assert(isValidSecretKey("BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF"), Equals, true)
|
||||
|
||||
c.Assert(isValidSecretKey("aaa"), Equals, false)
|
||||
c.Assert(isValidSecretKey("password%%"), Equals, false)
|
||||
}
|
||||
|
||||
// Tests valid and invalid access keys.
|
||||
func (s *TestSuite) TestValidAccessKeys(c *C) {
|
||||
c.Assert(isValidAccessKey("c67W2-r4MAyAYScRl"), Equals, true)
|
||||
c.Assert(isValidAccessKey("EXOb76bfeb1234562iu679f11588"), Equals, true)
|
||||
c.Assert(isValidAccessKey("BYvgJM101sHngl2uzjXS/OBF/aMxAN06JrJ3qJlF"), Equals, true)
|
||||
c.Assert(isValidAccessKey("admin"), Equals, true)
|
||||
|
||||
c.Assert(isValidAccessKey("aaa"), Equals, false)
|
||||
c.Assert(isValidAccessKey("$$%%%%%3333"), Equals, false)
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestInvalidPERMS(c *C) {
|
||||
perms := accessPerms("invalid")
|
||||
c.Assert(perms.isValidAccessPERM(), Equals, false)
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestGetMcConfigDir(c *C) {
|
||||
dir, err := getMcConfigDir()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(dir, Not(Equals), "")
|
||||
c.Assert(mustGetMcConfigDir(), Equals, dir)
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestGetMcConfigPath(c *C) {
|
||||
dir, err := getMcConfigPath()
|
||||
c.Assert(err, IsNil)
|
||||
switch runtime.GOOS {
|
||||
case "linux", "freebsd", "darwin", "solaris":
|
||||
c.Assert(dir, Equals, filepath.Join(mustGetMcConfigDir(), "config.json"))
|
||||
case "windows":
|
||||
c.Assert(dir, Equals, filepath.Join(mustGetMcConfigDir(), "config.json"))
|
||||
default:
|
||||
c.Fail()
|
||||
}
|
||||
c.Assert(mustGetMcConfigPath(), Equals, dir)
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestIsvalidAliasName(c *C) {
|
||||
c.Check(isValidAlias("helloWorld0"), Equals, true)
|
||||
c.Check(isValidAlias("h0SFD2k24Fdsa"), Equals, true)
|
||||
c.Check(isValidAlias("fdslka-4"), Equals, true)
|
||||
c.Check(isValidAlias("fdslka-"), Equals, true)
|
||||
c.Check(isValidAlias("helloWorld$"), Equals, false)
|
||||
c.Check(isValidAlias("h0SFD2k2#Fdsa"), Equals, false)
|
||||
c.Check(isValidAlias("0dslka-4"), Equals, false)
|
||||
c.Check(isValidAlias("-fdslka"), Equals, false)
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestHumanizedTime(c *C) {
|
||||
hTime := timeDurationToHumanizedTime(time.Duration(10) * time.Second)
|
||||
c.Assert(hTime.Minutes, Equals, int64(0))
|
||||
c.Assert(hTime.Hours, Equals, int64(0))
|
||||
c.Assert(hTime.Days, Equals, int64(0))
|
||||
|
||||
hTime = timeDurationToHumanizedTime(time.Duration(10) * time.Minute)
|
||||
c.Assert(hTime.Hours, Equals, int64(0))
|
||||
c.Assert(hTime.Days, Equals, int64(0))
|
||||
|
||||
hTime = timeDurationToHumanizedTime(time.Duration(10) * time.Hour)
|
||||
c.Assert(hTime.Days, Equals, int64(0))
|
||||
|
||||
hTime = timeDurationToHumanizedTime(time.Duration(24) * time.Hour)
|
||||
c.Assert(hTime.Days, Not(Equals), int64(0))
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestVersions(c *C) {
|
||||
v1, e := version.NewVersion("1.6")
|
||||
c.Assert(e, IsNil)
|
||||
v2, e := version.NewConstraint(">= 1.5.0")
|
||||
c.Assert(e, IsNil)
|
||||
c.Assert(v2.Check(v1), Equals, true)
|
||||
}
|
||||
497
command/mirror-main.go
Normal file
497
command/mirror-main.go
Normal file
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* Minio Client, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// mirror specific flags.
|
||||
var (
|
||||
mirrorFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of mirror.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "Force overwrite of an existing target(s).",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "fake",
|
||||
Usage: "Perform a fake mirror operation.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "remove",
|
||||
Usage: "Remove extraneous file(s) on target.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Mirror folders recursively from a single source to many destinations
|
||||
var mirrorCmd = cli.Command{
|
||||
Name: "mirror",
|
||||
Usage: "Mirror folders recursively from a single source to single destination.",
|
||||
Action: mainMirror,
|
||||
Flags: append(mirrorFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] SOURCE TARGET
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Mirror a bucket recursively from Minio cloud storage to a bucket on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} play/photos/2014 s3/backup-photos
|
||||
|
||||
2. Mirror a local folder recursively to Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} backup/ s3/archive
|
||||
|
||||
3. Mirror a bucket from aliased Amazon S3 cloud storage to a folder on Windows.
|
||||
$ mc {{.Name}} s3\documents\2014\ C:\backup\2014
|
||||
|
||||
4. Mirror a bucket from aliased Amazon S3 cloud storage to a local folder use '--force' to overwrite destination.
|
||||
$ mc {{.Name}} --force s3/miniocloud miniocloud-backup
|
||||
|
||||
5. Fake mirror a bucket from Minio cloud storage to a bucket on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} --fake play/photos/2014 s3/backup-photos/2014
|
||||
|
||||
6. Mirror a bucket from Minio cloud storage to a bucket on Amazon S3 cloud storage and remove any extraneous
|
||||
files on Amazon S3 cloud storage. NOTE: '--remove' is only supported with '--force'.
|
||||
$ mc {{.Name}} --force --remove play/photos/2014 s3/backup-photos/2014
|
||||
`,
|
||||
}
|
||||
|
||||
// mirrorMessage container for file mirror messages
|
||||
type mirrorMessage struct {
|
||||
Status string `json:"status"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
}
|
||||
|
||||
// String colorized mirror message
|
||||
func (m mirrorMessage) String() string {
|
||||
return console.Colorize("Mirror", fmt.Sprintf("‘%s’ -> ‘%s’", m.Source, m.Target))
|
||||
}
|
||||
|
||||
// JSON jsonified mirror message
|
||||
func (m mirrorMessage) JSON() string {
|
||||
m.Status = "success"
|
||||
mirrorMessageBytes, e := json.Marshal(m)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(mirrorMessageBytes)
|
||||
}
|
||||
|
||||
// mirrorStatMessage container for mirror accounting message
|
||||
type mirrorStatMessage struct {
|
||||
Total int64
|
||||
Transferred int64
|
||||
Speed float64
|
||||
}
|
||||
|
||||
// mirrorStatMessage mirror accounting message
|
||||
func (c mirrorStatMessage) String() string {
|
||||
speedBox := pb.Format(int64(c.Speed)).To(pb.U_BYTES).String()
|
||||
if speedBox == "" {
|
||||
speedBox = "0 MB"
|
||||
} else {
|
||||
speedBox = speedBox + "/s"
|
||||
}
|
||||
message := fmt.Sprintf("Total: %s, Transferred: %s, Speed: %s", pb.Format(c.Total).To(pb.U_BYTES),
|
||||
pb.Format(c.Transferred).To(pb.U_BYTES), speedBox)
|
||||
return message
|
||||
}
|
||||
|
||||
// doRemove - removes files on target.
|
||||
func doRemove(sURLs mirrorURLs, fakeRemove bool) mirrorURLs {
|
||||
targetAlias := sURLs.TargetAlias
|
||||
targetURL := sURLs.TargetContent.URL
|
||||
|
||||
// We are not removing incomplete uploads.
|
||||
isIncomplete := false
|
||||
|
||||
// Remove extraneous file on target.
|
||||
err := rm(targetAlias, targetURL.String(), isIncomplete, fakeRemove)
|
||||
if err != nil {
|
||||
sURLs.Error = err.Trace(targetAlias, targetURL.String())
|
||||
return sURLs
|
||||
}
|
||||
|
||||
sURLs.Error = nil // just for safety
|
||||
return sURLs
|
||||
}
|
||||
|
||||
// doMirror - Mirror an object to multiple destination. mirrorURLs status contains a copy of sURLs and error if any.
|
||||
func doMirror(sURLs mirrorURLs, progressReader *progressBar, accountingReader *accounter, fakeMirror bool) mirrorURLs {
|
||||
if sURLs.Error != nil { // Errorneous sURLs passed.
|
||||
sURLs.Error = sURLs.Error.Trace()
|
||||
return sURLs
|
||||
}
|
||||
|
||||
sourceAlias := sURLs.SourceAlias
|
||||
sourceURL := sURLs.SourceContent.URL
|
||||
targetAlias := sURLs.TargetAlias
|
||||
targetURL := sURLs.TargetContent.URL
|
||||
length := sURLs.SourceContent.Size
|
||||
|
||||
if !globalQuiet && !globalJSON {
|
||||
progressReader = progressReader.SetCaption(sourceURL.String() + ": ")
|
||||
}
|
||||
|
||||
var progress io.Reader
|
||||
if globalQuiet || globalJSON {
|
||||
sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path))
|
||||
targetPath := filepath.ToSlash(filepath.Join(targetAlias, targetURL.Path))
|
||||
printMsg(mirrorMessage{
|
||||
Source: sourcePath,
|
||||
Target: targetPath,
|
||||
})
|
||||
if globalQuiet || globalJSON {
|
||||
progress = accountingReader
|
||||
}
|
||||
} else {
|
||||
// Set up progress bar.
|
||||
progress = progressReader.ProgressBar
|
||||
}
|
||||
|
||||
// For a fake mirror make sure we update respective progress bars
|
||||
// and accounting readers under relevant conditions.
|
||||
if fakeMirror {
|
||||
if !globalJSON && !globalQuiet {
|
||||
progressReader.ProgressBar.Add64(sURLs.SourceContent.Size)
|
||||
} else {
|
||||
accountingReader.Add(sURLs.SourceContent.Size)
|
||||
}
|
||||
sURLs.Error = nil
|
||||
return sURLs
|
||||
}
|
||||
// If source size is <= 5GB and operation is across same server type try to use Copy.
|
||||
if length <= fiveGB && sourceURL.Type == targetURL.Type {
|
||||
// FS -> FS Copy includes alias in path.
|
||||
if sourceURL.Type == fileSystem {
|
||||
sourcePath := filepath.ToSlash(filepath.Join(sourceAlias, sourceURL.Path))
|
||||
err := copySourceStreamFromAlias(targetAlias, targetURL.String(), sourcePath, length, progress)
|
||||
if err != nil {
|
||||
sURLs.Error = err.Trace(sourceURL.String())
|
||||
return sURLs
|
||||
}
|
||||
} else if sourceURL.Type == objectStorage {
|
||||
if sourceAlias == targetAlias {
|
||||
// If source/target are object storage their aliases must be the same
|
||||
// Do not include alias inside path for ObjStore -> ObjStore.
|
||||
err := copySourceStreamFromAlias(targetAlias, targetURL.String(), sourceURL.Path, length, progress)
|
||||
if err != nil {
|
||||
sURLs.Error = err.Trace(sourceURL.String())
|
||||
return sURLs
|
||||
}
|
||||
} else {
|
||||
reader, err := getSourceStreamFromAlias(sourceAlias, sourceURL.String())
|
||||
if err != nil {
|
||||
sURLs.Error = err.Trace(sourceURL.String())
|
||||
return sURLs
|
||||
}
|
||||
_, err = putTargetStreamFromAlias(targetAlias, targetURL.String(), reader, length, progress)
|
||||
if err != nil {
|
||||
sURLs.Error = err.Trace(targetURL.String())
|
||||
return sURLs
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Standard GET/PUT for size > 5GB
|
||||
reader, err := getSourceStreamFromAlias(sourceAlias, sourceURL.String())
|
||||
if err != nil {
|
||||
sURLs.Error = err.Trace(sourceURL.String())
|
||||
return sURLs
|
||||
}
|
||||
_, err = putTargetStreamFromAlias(targetAlias, targetURL.String(), reader, length, progress)
|
||||
if err != nil {
|
||||
sURLs.Error = err.Trace(targetURL.String())
|
||||
return sURLs
|
||||
}
|
||||
}
|
||||
sURLs.Error = nil // just for safety
|
||||
return sURLs
|
||||
}
|
||||
|
||||
// doPrepareMirrorURLs scans the source URL and prepares a list of objects for mirroring.
|
||||
func doPrepareMirrorURLs(session *sessionV7, isForce bool, isFake bool, isRemove bool, trapCh <-chan bool) {
|
||||
sourceURL := session.Header.CommandArgs[0] // first one is source.
|
||||
targetURL := session.Header.CommandArgs[1]
|
||||
var totalBytes int64
|
||||
var totalObjects int
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
URLsCh := prepareMirrorURLs(sourceURL, targetURL, isForce, isFake, isRemove)
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case sURLs, ok := <-URLsCh:
|
||||
if !ok { // Done with URL prepration
|
||||
done = true
|
||||
break
|
||||
}
|
||||
if sURLs.Error != nil {
|
||||
// Print in new line and adjust to top so that we don't print over the ongoing scan bar
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
errorIf(sURLs.Error.Trace(), "Unable to prepare URLs for mirroring.")
|
||||
break
|
||||
}
|
||||
if sURLs.isEmpty() {
|
||||
break
|
||||
}
|
||||
jsonData, e := json.Marshal(sURLs)
|
||||
if e != nil {
|
||||
session.Delete()
|
||||
fatalIf(probe.NewError(e), "Unable to marshal URLs into JSON.")
|
||||
}
|
||||
fmt.Fprintln(dataFP, string(jsonData))
|
||||
if !globalQuiet && !globalJSON {
|
||||
// Source content is empty if removal is requested,
|
||||
// put targetContent on to scan bar.
|
||||
if sURLs.SourceContent != nil {
|
||||
scanBar(sURLs.SourceContent.URL.String())
|
||||
} else if sURLs.TargetContent != nil && isRemove {
|
||||
scanBar(sURLs.TargetContent.URL.String())
|
||||
}
|
||||
}
|
||||
// Remember totalBytes only for mirror not for removal,
|
||||
if sURLs.SourceContent != nil {
|
||||
totalBytes += sURLs.SourceContent.Size
|
||||
}
|
||||
totalObjects++
|
||||
case <-trapCh:
|
||||
// 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()
|
||||
}
|
||||
|
||||
// Session'fied mirror command.
|
||||
func doMirrorSession(session *sessionV7) {
|
||||
isForce := session.Header.CommandBoolFlags["force"]
|
||||
isFake := session.Header.CommandBoolFlags["fake"]
|
||||
isRemove := session.Header.CommandBoolFlags["remove"]
|
||||
|
||||
// Initialize signal trap.
|
||||
trapCh := signalTrap(os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
if !session.HasData() {
|
||||
doPrepareMirrorURLs(session, isForce, isFake, isRemove, trapCh)
|
||||
}
|
||||
|
||||
// Enable accounting reader by default.
|
||||
accntReader := newAccounter(session.Header.TotalBytes)
|
||||
|
||||
// Set up progress bar.
|
||||
var progressReader *progressBar
|
||||
if !globalQuiet && !globalJSON {
|
||||
progressReader = newProgressBar(session.Header.TotalBytes)
|
||||
}
|
||||
|
||||
// Prepare URL scanner from session data file.
|
||||
urlScanner := bufio.NewScanner(session.NewDataReader())
|
||||
|
||||
// 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)
|
||||
|
||||
// isRemoved returns true if an object has been already removed or
|
||||
// not. This is useful when we resume from a session.
|
||||
isRemoved := isLastFactory(session.Header.LastRemoved)
|
||||
|
||||
// Wait on status of doMirror() operation.
|
||||
var statusCh = make(chan mirrorURLs)
|
||||
|
||||
// Add a wait group for the below go-routine.
|
||||
var wg = new(sync.WaitGroup)
|
||||
wg.Add(1)
|
||||
|
||||
// Go routine to monitor signal traps if any.
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-trapCh:
|
||||
// Receive interrupt notification.
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
session.CloseAndDie()
|
||||
case sURLs, ok := <-statusCh:
|
||||
// Status channel is closed, we should return.
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if sURLs.Error == nil {
|
||||
if sURLs.SourceContent != nil {
|
||||
session.Header.LastCopied = sURLs.SourceContent.URL.String()
|
||||
session.Save()
|
||||
} else if sURLs.TargetContent != nil && isRemove {
|
||||
session.Header.LastRemoved = sURLs.TargetContent.URL.String()
|
||||
session.Save()
|
||||
|
||||
// Construct user facing message and path.
|
||||
targetPath := filepath.ToSlash(filepath.Join(sURLs.TargetAlias, sURLs.TargetContent.URL.Path))
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
printMsg(rmMessage{
|
||||
Status: "success",
|
||||
URL: targetPath,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Print in new line and adjust to top so that we
|
||||
// don't print over the ongoing progress bar.
|
||||
if !globalQuiet && !globalJSON {
|
||||
console.Eraseline()
|
||||
}
|
||||
if sURLs.SourceContent != nil {
|
||||
errorIf(sURLs.Error.Trace(sURLs.SourceContent.URL.String()),
|
||||
fmt.Sprintf("Failed to copy ‘%s’.", sURLs.SourceContent.URL.String()))
|
||||
} else {
|
||||
// When sURLs.SourceContent is nil, we know that we have an error related to removing
|
||||
errorIf(sURLs.Error.Trace(sURLs.TargetContent.URL.String()),
|
||||
fmt.Sprintf("Failed to remove ‘%s’.", sURLs.TargetContent.URL.String()))
|
||||
}
|
||||
// For all non critical errors we can continue for the
|
||||
// remaining files.
|
||||
switch sURLs.Error.ToGoError().(type) {
|
||||
// Handle this specifically for filesystem related errors.
|
||||
case BrokenSymlink, TooManyLevelsSymlink, PathNotFound, PathInsufficientPermission:
|
||||
continue
|
||||
// Handle this specifically for object storage related errors.
|
||||
case BucketNameEmpty, ObjectMissing, ObjectAlreadyExists, BucketDoesNotExist, BucketInvalid, ObjectOnGlacier:
|
||||
continue
|
||||
}
|
||||
// For critical errors we should exit. Session
|
||||
// can be resumed after the user figures out
|
||||
// the problem.
|
||||
session.CloseAndDie()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Loop through all urls and mirror.
|
||||
for urlScanner.Scan() {
|
||||
var sURLs mirrorURLs
|
||||
|
||||
// Unmarshal copyURLs from each line.
|
||||
json.Unmarshal([]byte(urlScanner.Text()), &sURLs)
|
||||
|
||||
if sURLs.SourceContent != nil {
|
||||
// Verify if previously copied or if its a fake mirror, set
|
||||
// fake mirror accordingly.
|
||||
fakeMirror := isCopied(sURLs.SourceContent.URL.String()) || isFake
|
||||
// Perform mirror operation.
|
||||
statusCh <- doMirror(sURLs, progressReader, accntReader, fakeMirror)
|
||||
} else if sURLs.TargetContent != nil && isRemove {
|
||||
fakeRemove := isRemoved(sURLs.TargetContent.URL.String()) || isFake
|
||||
// Perform remove operation.
|
||||
statusCh <- doRemove(sURLs, fakeRemove)
|
||||
}
|
||||
}
|
||||
|
||||
// Close the goroutine.
|
||||
close(statusCh)
|
||||
|
||||
// Wait for the goroutines to finish.
|
||||
wg.Wait()
|
||||
|
||||
if !globalQuiet && !globalJSON {
|
||||
if progressReader.ProgressBar.Get() > 0 {
|
||||
progressReader.ProgressBar.Finish()
|
||||
}
|
||||
} else {
|
||||
accntStat := accntReader.Stat()
|
||||
mrStatMessage := mirrorStatMessage{
|
||||
Total: accntStat.Total,
|
||||
Transferred: accntStat.Transferred,
|
||||
Speed: accntStat.Speed,
|
||||
}
|
||||
console.Println(console.Colorize("Mirror", mrStatMessage.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// Main entry point for mirror command.
|
||||
func mainMirror(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'mirror' cli arguments.
|
||||
checkMirrorSyntax(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("Mirror", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
var e error
|
||||
session := newSessionV7()
|
||||
session.Header.CommandType = "mirror"
|
||||
session.Header.RootPath, e = os.Getwd()
|
||||
if e != nil {
|
||||
session.Delete()
|
||||
fatalIf(probe.NewError(e), "Unable to get current working folder.")
|
||||
}
|
||||
|
||||
// Set command flags from context.
|
||||
isForce := ctx.Bool("force")
|
||||
isFake := ctx.Bool("fake")
|
||||
isRemove := ctx.Bool("remove")
|
||||
session.Header.CommandBoolFlags["force"] = isForce
|
||||
session.Header.CommandBoolFlags["fake"] = isFake
|
||||
session.Header.CommandBoolFlags["remove"] = isRemove
|
||||
|
||||
// extract URLs.
|
||||
session.Header.CommandArgs = ctx.Args()
|
||||
doMirrorSession(session)
|
||||
session.Delete()
|
||||
}
|
||||
193
command/mirror-url.go
Normal file
193
command/mirror-url.go
Normal file
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Minio Client (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
type mirrorURLs struct {
|
||||
SourceAlias string
|
||||
SourceContent *clientContent
|
||||
TargetAlias string
|
||||
TargetContent *clientContent
|
||||
Error *probe.Error `json:"-"`
|
||||
}
|
||||
|
||||
func (m mirrorURLs) isEmpty() bool {
|
||||
if m.SourceContent == nil && m.TargetContent == nil && m.Error == nil {
|
||||
return true
|
||||
}
|
||||
// If remove flag is set then sourceContent is usually nil.
|
||||
if m.SourceContent != nil {
|
||||
if m.SourceContent.Size == 0 && m.TargetContent == nil && m.Error == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//
|
||||
// * MIRROR ARGS - VALID CASES
|
||||
// =========================
|
||||
// mirror(d1..., d2) -> []mirror(d1/f, d2/d1/f)
|
||||
|
||||
// checkMirrorSyntax(URLs []string)
|
||||
func checkMirrorSyntax(ctx *cli.Context) {
|
||||
if len(ctx.Args()) != 2 {
|
||||
cli.ShowCommandHelpAndExit(ctx, "mirror", 1) // last argument is exit code.
|
||||
}
|
||||
|
||||
// extract URLs.
|
||||
URLs := ctx.Args()
|
||||
srcURL := URLs[0]
|
||||
tgtURL := URLs[1]
|
||||
|
||||
/****** Generic rules *******/
|
||||
_, srcContent, err := url2Stat(srcURL)
|
||||
// incomplete uploads are not necessary for copy operation, no need to verify for them.
|
||||
isIncomplete := false
|
||||
if err != nil && !isURLPrefixExists(srcURL, isIncomplete) {
|
||||
fatalIf(err.Trace(srcURL), "Unable to stat source ‘"+srcURL+"’.")
|
||||
}
|
||||
|
||||
if err == nil && !srcContent.Type.IsDir() {
|
||||
fatalIf(errInvalidArgument().Trace(srcContent.URL.String(), srcContent.Type.String()), fmt.Sprintf("Source ‘%s’ is not a folder. Only folders are supported by mirror command.", srcURL))
|
||||
}
|
||||
|
||||
if len(tgtURL) == 0 && tgtURL == "" {
|
||||
fatalIf(errInvalidArgument().Trace(), "Invalid target arguments to mirror command.")
|
||||
}
|
||||
|
||||
url := newClientURL(tgtURL)
|
||||
if url.Host != "" {
|
||||
if !isURLVirtualHostStyle(url.Host) {
|
||||
if url.Path == string(url.Separator) {
|
||||
fatalIf(errInvalidArgument().Trace(tgtURL),
|
||||
fmt.Sprintf("Target ‘%s’ does not contain bucket name.", tgtURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
_, _, err = url2Stat(tgtURL)
|
||||
// we die on any error other than PathNotFound - destination directory need not exist.
|
||||
if _, ok := err.ToGoError().(PathNotFound); !ok {
|
||||
fatalIf(err.Trace(tgtURL), fmt.Sprintf("Unable to stat target ‘%s’.", tgtURL))
|
||||
}
|
||||
}
|
||||
|
||||
func deltaSourceTarget(sourceURL string, targetURL string, isForce bool, isFake bool, isRemove bool, mirrorURLsCh chan<- mirrorURLs) {
|
||||
// source and targets are always directories
|
||||
sourceSeparator := string(newClientURL(sourceURL).Separator)
|
||||
if !strings.HasSuffix(sourceURL, sourceSeparator) {
|
||||
sourceURL = sourceURL + sourceSeparator
|
||||
}
|
||||
targetSeparator := string(newClientURL(targetURL).Separator)
|
||||
if !strings.HasSuffix(targetURL, targetSeparator) {
|
||||
targetURL = targetURL + targetSeparator
|
||||
}
|
||||
|
||||
// Extract alias and expanded URL
|
||||
sourceAlias, sourceURL, _ := mustExpandAlias(sourceURL)
|
||||
targetAlias, targetURL, _ := mustExpandAlias(targetURL)
|
||||
|
||||
defer close(mirrorURLsCh)
|
||||
|
||||
sourceClnt, err := newClientFromAlias(sourceAlias, sourceURL)
|
||||
if err != nil {
|
||||
mirrorURLsCh <- mirrorURLs{Error: err.Trace(sourceAlias, sourceURL)}
|
||||
return
|
||||
}
|
||||
|
||||
targetClnt, err := newClientFromAlias(targetAlias, targetURL)
|
||||
if err != nil {
|
||||
mirrorURLsCh <- mirrorURLs{Error: err.Trace(targetAlias, targetURL)}
|
||||
return
|
||||
}
|
||||
|
||||
// List both source and target, compare and return values through channel.
|
||||
for diffMsg := range objectDifference(sourceClnt, targetClnt, sourceURL, targetURL) {
|
||||
switch diffMsg.Diff {
|
||||
case differInNone:
|
||||
// No difference, continue.
|
||||
continue
|
||||
case differInType:
|
||||
mirrorURLsCh <- mirrorURLs{Error: errInvalidTarget(diffMsg.SecondURL)}
|
||||
continue
|
||||
case differInSize:
|
||||
if !isForce && !isFake {
|
||||
// Size differs and force not set
|
||||
mirrorURLsCh <- mirrorURLs{Error: errOverWriteNotAllowed(diffMsg.SecondURL)}
|
||||
continue
|
||||
}
|
||||
sourceSuffix := strings.TrimPrefix(diffMsg.FirstURL, sourceURL)
|
||||
// Either available only in source or size differs and force is set
|
||||
targetPath := urlJoinPath(targetURL, sourceSuffix)
|
||||
sourceContent := diffMsg.firstContent
|
||||
targetContent := &clientContent{URL: *newClientURL(targetPath)}
|
||||
mirrorURLsCh <- mirrorURLs{
|
||||
SourceAlias: sourceAlias,
|
||||
SourceContent: sourceContent,
|
||||
TargetAlias: targetAlias,
|
||||
TargetContent: targetContent,
|
||||
}
|
||||
continue
|
||||
case differInFirst:
|
||||
sourceSuffix := strings.TrimPrefix(diffMsg.FirstURL, sourceURL)
|
||||
// Either available only in source or size differs and force is set
|
||||
targetPath := urlJoinPath(targetURL, sourceSuffix)
|
||||
sourceContent := diffMsg.firstContent
|
||||
targetContent := &clientContent{URL: *newClientURL(targetPath)}
|
||||
mirrorURLsCh <- mirrorURLs{
|
||||
SourceAlias: sourceAlias,
|
||||
SourceContent: sourceContent,
|
||||
TargetAlias: targetAlias,
|
||||
TargetContent: targetContent,
|
||||
}
|
||||
case differInSecond:
|
||||
if isRemove {
|
||||
if !isForce && !isFake {
|
||||
// Object removal not allowed if force is not set.
|
||||
mirrorURLsCh <- mirrorURLs{
|
||||
Error: errDeleteNotAllowed(diffMsg.SecondURL),
|
||||
}
|
||||
continue
|
||||
}
|
||||
mirrorURLsCh <- mirrorURLs{
|
||||
TargetAlias: targetAlias,
|
||||
TargetContent: diffMsg.secondContent,
|
||||
}
|
||||
}
|
||||
continue
|
||||
default:
|
||||
mirrorURLsCh <- mirrorURLs{
|
||||
Error: errUnrecognizedDiffType(diffMsg.Diff).Trace(diffMsg.FirstURL, diffMsg.SecondURL),
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepares urls that need to be copied or removed based on requested options.
|
||||
func prepareMirrorURLs(sourceURL string, targetURL string, isForce bool, isFake bool, isRemove bool) <-chan mirrorURLs {
|
||||
mirrorURLsCh := make(chan mirrorURLs)
|
||||
go deltaSourceTarget(sourceURL, targetURL, isForce, isFake, isRemove, mirrorURLsCh)
|
||||
return mirrorURLsCh
|
||||
}
|
||||
85
command/notifier.go
Normal file
85
command/notifier.go
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// colorizeUpdateMessage - inspired from Yeoman project npm package https://github.com/yeoman/update-notifier.
|
||||
func colorizeUpdateMessage(updateString string) (string, *probe.Error) {
|
||||
// initialize coloring
|
||||
cyan := color.New(color.FgCyan, color.Bold).SprintFunc()
|
||||
yellow := color.New(color.FgYellow, color.Bold).SprintfFunc()
|
||||
|
||||
// calculate length without color coding, due to ANSI color characters padded to actual
|
||||
// string the final length is wrong than the original string length.
|
||||
line1Str := fmt.Sprintf(" New update available, please execute the following command to update: ")
|
||||
line2Str := fmt.Sprintf(" %s ", updateString)
|
||||
line1Length := len(line1Str)
|
||||
line2Length := len(line2Str)
|
||||
|
||||
// populate lines with color coding.
|
||||
line1InColor := line1Str
|
||||
line2InColor := fmt.Sprintf(" %s ", cyan(updateString))
|
||||
|
||||
// calculate the rectangular box size.
|
||||
maxContentWidth := int(math.Max(float64(line1Length), float64(line2Length)))
|
||||
line1Rest := maxContentWidth - line1Length
|
||||
line2Rest := maxContentWidth - line2Length
|
||||
|
||||
termWidth, e := pb.GetTerminalWidth()
|
||||
if e != nil {
|
||||
return "", probe.NewError(e)
|
||||
}
|
||||
var message string
|
||||
switch {
|
||||
case len(line2Str) > termWidth:
|
||||
message = "\n" + line1InColor + "\n" + line2InColor + "\n"
|
||||
default:
|
||||
// on windows terminal turn off unicode characters.
|
||||
var top, bottom, sideBar string
|
||||
if runtime.GOOS == "windows" {
|
||||
top = yellow("*" + strings.Repeat("*", maxContentWidth) + "*")
|
||||
bottom = yellow("*" + strings.Repeat("*", maxContentWidth) + "*")
|
||||
sideBar = yellow("|")
|
||||
} else {
|
||||
// color the rectangular box, use unicode characters here.
|
||||
top = yellow("┏" + strings.Repeat("━", maxContentWidth) + "┓")
|
||||
bottom = yellow("┗" + strings.Repeat("━", maxContentWidth) + "┛")
|
||||
sideBar = yellow("┃")
|
||||
}
|
||||
// fill spaces to the rest of the area.
|
||||
spacePaddingLine1 := strings.Repeat(" ", line1Rest)
|
||||
spacePaddingLine2 := strings.Repeat(" ", line2Rest)
|
||||
|
||||
// construct the final message.
|
||||
message = "\n" + top + "\n" +
|
||||
sideBar + line1InColor + spacePaddingLine1 + sideBar + "\n" +
|
||||
sideBar + line2InColor + spacePaddingLine2 + sideBar + "\n" +
|
||||
bottom + "\n"
|
||||
}
|
||||
// return the final message
|
||||
return message, nil
|
||||
}
|
||||
111
command/pipe-main.go
Normal file
111
command/pipe-main.go
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Minio Client, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
pipeFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of pipe.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Display contents of a file.
|
||||
var pipeCmd = cli.Command{
|
||||
Name: "pipe",
|
||||
Usage: "Write contents of stdin to one target. When no target is specified, it writes to stdout.",
|
||||
Action: mainPipe,
|
||||
Flags: append(pipeFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] [TARGET]
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Write contents of stdin to a file on local filesystem.
|
||||
$ mc {{.Name}} /tmp/hello-world.go
|
||||
|
||||
2. Write contents of stdin to an object on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} s3/personalbuck/meeting-notes.txt
|
||||
|
||||
3. Copy an ISO image to an object on Amazon S3 cloud storage.
|
||||
$ cat debian-8.2.iso | mc {{.Name}} s3/ferenginar/gnuos.iso
|
||||
|
||||
4. Stream MySQL database dump to Amazon S3 directly.
|
||||
$ mysqldump -u root -p ******* accountsdb | mc {{.Name}} s3/ferenginar/backups/accountsdb-oct-9-2015.sql
|
||||
`,
|
||||
}
|
||||
|
||||
func pipe(targetURL string) *probe.Error {
|
||||
if targetURL == "" {
|
||||
// When no target is specified, pipe cat's stdin to stdout.
|
||||
return catOut(os.Stdin).Trace()
|
||||
}
|
||||
|
||||
// Stream from stdin to multiple objects until EOF.
|
||||
// Ignore size, since os.Stat() would not return proper size all the time
|
||||
// for local filesystem for example /proc files.
|
||||
_, err := putTargetStream(targetURL, os.Stdin, -1)
|
||||
// TODO: See if this check is necessary.
|
||||
switch e := err.ToGoError().(type) {
|
||||
case *os.PathError:
|
||||
if e.Err == syscall.EPIPE {
|
||||
// stdin closed by the user. Gracefully exit.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err.Trace(targetURL)
|
||||
}
|
||||
|
||||
// check pipe input arguments.
|
||||
func checkPipeSyntax(ctx *cli.Context) {
|
||||
if len(ctx.Args()) > 1 {
|
||||
cli.ShowCommandHelpAndExit(ctx, "pipe", 1) // last argument is exit code.
|
||||
}
|
||||
}
|
||||
|
||||
// mainPipe is the main entry point for pipe command.
|
||||
func mainPipe(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// validate pipe input arguments.
|
||||
checkPipeSyntax(ctx)
|
||||
|
||||
if len(ctx.Args()) == 0 {
|
||||
err := pipe("")
|
||||
fatalIf(err.Trace("stdout"), "Unable to write to one or more targets.")
|
||||
} else {
|
||||
// extract URLs.
|
||||
URLs := ctx.Args()
|
||||
err := pipe(URLs[0])
|
||||
fatalIf(err.Trace(URLs[0]), "Unable to write to one or more targets.")
|
||||
}
|
||||
}
|
||||
207
command/policy-main.go
Normal file
207
command/policy-main.go
Normal file
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Minio Client, (C) 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
policyFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of policy.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Set public policy
|
||||
var policyCmd = cli.Command{
|
||||
Name: "policy",
|
||||
Usage: "Set public policy on bucket or prefix.",
|
||||
Action: mainPolicy,
|
||||
Flags: append(policyFlags, globalFlags...),
|
||||
CustomHelpTemplate: `Name:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] PERMISSION TARGET
|
||||
mc {{.Name}} [FLAGS] TARGET
|
||||
|
||||
PERMISSION:
|
||||
Allowed policies are: [none, download, upload, both].
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Set bucket to "download" on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} download s3/burningman2011
|
||||
|
||||
2. Set bucket to "both" on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} both s3/shared
|
||||
|
||||
3. Set bucket to "upload" on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} upload s3/incoming
|
||||
|
||||
4. Set a prefix to "both" on Amazon S3 cloud storage.
|
||||
$ mc {{.Name}} both s3/public-commons/images
|
||||
|
||||
5. Get bucket permissions.
|
||||
$ mc {{.Name}} s3/shared
|
||||
|
||||
`,
|
||||
}
|
||||
|
||||
// policyMessage is container for policy command on bucket success and failure messages.
|
||||
type policyMessage struct {
|
||||
Operation string `json:"operation"`
|
||||
Status string `json:"status"`
|
||||
Bucket string `json:"bucket"`
|
||||
Perms accessPerms `json:"permission"`
|
||||
}
|
||||
|
||||
// String colorized access message.
|
||||
func (s policyMessage) String() string {
|
||||
if s.Operation == "set" {
|
||||
return console.Colorize("Policy",
|
||||
"Access permission for ‘"+s.Bucket+"’ is set to ‘"+string(s.Perms)+"’")
|
||||
}
|
||||
if s.Operation == "get" {
|
||||
return console.Colorize("Policy",
|
||||
"Access permission for ‘"+s.Bucket+"’"+" is ‘"+string(s.Perms)+"’")
|
||||
}
|
||||
// nothing to print
|
||||
return ""
|
||||
}
|
||||
|
||||
// JSON jsonified policy message.
|
||||
func (s policyMessage) JSON() string {
|
||||
policyJSONBytes, e := json.Marshal(s)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(policyJSONBytes)
|
||||
}
|
||||
|
||||
// checkPolicySyntax check for incoming syntax.
|
||||
func checkPolicySyntax(ctx *cli.Context) {
|
||||
if !ctx.Args().Present() {
|
||||
cli.ShowCommandHelpAndExit(ctx, "policy", 1) // last argument is exit code.
|
||||
}
|
||||
if len(ctx.Args()) > 2 {
|
||||
cli.ShowCommandHelpAndExit(ctx, "policy", 1) // last argument is exit code.
|
||||
}
|
||||
if len(ctx.Args()) == 2 {
|
||||
perms := accessPerms(ctx.Args().Get(0))
|
||||
if !perms.isValidAccessPERM() {
|
||||
fatalIf(errDummy().Trace(),
|
||||
"Unrecognized permission ‘"+string(perms)+"’. Allowed values are [none, download, upload, both].")
|
||||
}
|
||||
}
|
||||
if len(ctx.Args()) < 1 {
|
||||
cli.ShowCommandHelpAndExit(ctx, "policy", 1) // last argument is exit code.
|
||||
}
|
||||
}
|
||||
|
||||
// doSetAccess do set access.
|
||||
func doSetAccess(targetURL string, targetPERMS accessPerms) *probe.Error {
|
||||
clnt, err := newClient(targetURL)
|
||||
if err != nil {
|
||||
return err.Trace(targetURL)
|
||||
}
|
||||
policy := ""
|
||||
switch targetPERMS {
|
||||
case accessNone:
|
||||
policy = "none"
|
||||
case accessDownload:
|
||||
policy = "readonly"
|
||||
case accessUpload:
|
||||
policy = "writeonly"
|
||||
case accessBoth:
|
||||
policy = "readwrite"
|
||||
}
|
||||
if err = clnt.SetAccess(policy); err != nil {
|
||||
return err.Trace(targetURL, string(targetPERMS))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// doGetAccess do get access.
|
||||
func doGetAccess(targetURL string) (perms accessPerms, err *probe.Error) {
|
||||
clnt, err := newClient(targetURL)
|
||||
if err != nil {
|
||||
return "", err.Trace(targetURL)
|
||||
}
|
||||
perm, err := clnt.GetAccess()
|
||||
if err != nil {
|
||||
return "", err.Trace(targetURL)
|
||||
}
|
||||
var policy accessPerms
|
||||
switch perm {
|
||||
case "none":
|
||||
policy = accessNone
|
||||
case "readonly":
|
||||
policy = accessDownload
|
||||
case "writeonly":
|
||||
policy = accessUpload
|
||||
case "readwrite":
|
||||
policy = accessBoth
|
||||
}
|
||||
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func mainPolicy(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'policy' cli arguments.
|
||||
checkPolicySyntax(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("Policy", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
perms := accessPerms(ctx.Args().First())
|
||||
if perms.isValidAccessPERM() {
|
||||
targetURL := ctx.Args().Last()
|
||||
err := doSetAccess(targetURL, perms)
|
||||
// Upon error exit.
|
||||
fatalIf(err.Trace(targetURL, string(perms)),
|
||||
"Unable to set policy ‘"+string(perms)+"’ for ‘"+targetURL+"’.")
|
||||
printMsg(policyMessage{
|
||||
Status: "success",
|
||||
Operation: "set",
|
||||
Bucket: targetURL,
|
||||
Perms: perms,
|
||||
})
|
||||
} else {
|
||||
targetURL := ctx.Args().First()
|
||||
perms, err := doGetAccess(targetURL)
|
||||
fatalIf(err.Trace(targetURL), "Unable to get policy for ‘"+targetURL+"’.")
|
||||
printMsg(policyMessage{
|
||||
Status: "success",
|
||||
Operation: "get",
|
||||
Bucket: targetURL,
|
||||
Perms: perms,
|
||||
})
|
||||
}
|
||||
}
|
||||
34
command/print.go
Normal file
34
command/print.go
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import "github.com/minio/mc/pkg/console"
|
||||
|
||||
// message interface for all structured messages implementing JSON(), String() methods.
|
||||
type message interface {
|
||||
JSON() string
|
||||
String() string
|
||||
}
|
||||
|
||||
// printMsg prints message string or JSON structure depending on the type of output console.
|
||||
func printMsg(msg message) {
|
||||
if !globalJSON {
|
||||
console.Println(msg.String())
|
||||
} else {
|
||||
console.Println(msg.JSON())
|
||||
}
|
||||
}
|
||||
151
command/progress-bar.go
Normal file
151
command/progress-bar.go
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
)
|
||||
|
||||
// progress extender.
|
||||
type progressBar struct {
|
||||
ProgressBar *pb.ProgressBar
|
||||
reader io.Reader
|
||||
readerLength int64
|
||||
bytesRead int64
|
||||
isResume bool
|
||||
}
|
||||
|
||||
// newProgressBar - instantiate a progress bar.
|
||||
func newProgressBar(total int64) *progressBar {
|
||||
// Progress bar speific theme customization.
|
||||
console.SetColor("Bar", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
pgbar := progressBar{}
|
||||
|
||||
// get the new original progress bar.
|
||||
bar := pb.New64(total)
|
||||
|
||||
// Set new human friendly print units.
|
||||
bar.SetUnits(pb.U_BYTES)
|
||||
|
||||
// Refresh rate for progress bar is set to 125 milliseconds.
|
||||
bar.SetRefreshRate(time.Millisecond * 125)
|
||||
|
||||
// Do not print a newline by default handled, it is handled manually.
|
||||
bar.NotPrint = true
|
||||
|
||||
// Show current speed is true.
|
||||
bar.ShowSpeed = true
|
||||
|
||||
// Custom callback with colorized bar.
|
||||
bar.Callback = func(s string) {
|
||||
console.Print(console.Colorize("Bar", "\r"+s))
|
||||
}
|
||||
|
||||
// Use different unicodes for Linux, OS X and Windows.
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// Need to add '\x00' as delimiter for unicode characters.
|
||||
bar.Format("┃\x00▓\x00█\x00░\x00┃")
|
||||
case "darwin":
|
||||
// Need to add '\x00' as delimiter for unicode characters.
|
||||
bar.Format(" \x00▓\x00 \x00░\x00 ")
|
||||
default:
|
||||
// Default to non unicode characters.
|
||||
bar.Format("[=> ]")
|
||||
}
|
||||
|
||||
// Start the progress bar.
|
||||
if bar.Total > 0 {
|
||||
bar.Start()
|
||||
}
|
||||
|
||||
// Copy for future
|
||||
pgbar.ProgressBar = bar
|
||||
|
||||
// Return new progress bar here.
|
||||
return &pgbar
|
||||
}
|
||||
|
||||
// Set caption.
|
||||
func (p *progressBar) SetCaption(caption string) *progressBar {
|
||||
caption = fixateBarCaption(caption, getFixedWidth(p.ProgressBar.GetWidth(), 18))
|
||||
p.ProgressBar.Prefix(caption)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *progressBar) Set64(length int64) *progressBar {
|
||||
p.ProgressBar = p.ProgressBar.Set64(length)
|
||||
return p
|
||||
}
|
||||
|
||||
// cursorAnimate - returns a animated rune through read channel for every read.
|
||||
func cursorAnimate() <-chan rune {
|
||||
cursorCh := make(chan rune)
|
||||
var cursors string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// cursors = "➩➪➫➬➭➮➯➱"
|
||||
// cursors = "▁▃▄▅▆▇█▇▆▅▄▃"
|
||||
cursors = "◐◓◑◒"
|
||||
// cursors = "←↖↑↗→↘↓↙"
|
||||
// cursors = "◴◷◶◵"
|
||||
// cursors = "◰◳◲◱"
|
||||
//cursors = "⣾⣽⣻⢿⡿⣟⣯⣷"
|
||||
case "darwin":
|
||||
cursors = "◐◓◑◒"
|
||||
default:
|
||||
cursors = "|/-\\"
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
for _, cursor := range cursors {
|
||||
cursorCh <- cursor
|
||||
}
|
||||
}
|
||||
}()
|
||||
return cursorCh
|
||||
}
|
||||
|
||||
// fixateBarCaption - fancify bar caption based on the terminal width.
|
||||
func fixateBarCaption(caption string, width int) string {
|
||||
switch {
|
||||
case len(caption) > width:
|
||||
// Trim caption to fit within the screen
|
||||
trimSize := len(caption) - width + 3
|
||||
if trimSize < len(caption) {
|
||||
caption = "..." + caption[trimSize:]
|
||||
}
|
||||
case len(caption) < width:
|
||||
caption += strings.Repeat(" ", width-len(caption))
|
||||
}
|
||||
return caption
|
||||
}
|
||||
|
||||
// getFixedWidth - get a fixed width based for a given percentage.
|
||||
func getFixedWidth(width, percent int) int {
|
||||
return width * percent / 100
|
||||
}
|
||||
216
command/rm-main.go
Normal file
216
command/rm-main.go
Normal file
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// rm specific flags.
|
||||
var (
|
||||
rmFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of rm.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "recursive, r",
|
||||
Usage: "Remove recursively.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "force",
|
||||
Usage: "Force a dangerous remove operation.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "incomplete, I",
|
||||
Usage: "Remove an incomplete upload(s).",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "fake",
|
||||
Usage: "Perform a fake remove operation.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// remove a file or folder.
|
||||
var rmCmd = cli.Command{
|
||||
Name: "rm",
|
||||
Usage: "Remove file or bucket [WARNING: Use with care].",
|
||||
Action: mainRm,
|
||||
Flags: append(rmFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] TARGET [TARGET ...]
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Remove a file.
|
||||
$ mc {{.Name}} 1999/old-backup.tgz
|
||||
|
||||
2. Remove contents of a folder, excluding its sub-folders.
|
||||
$ mc {{.Name}} --force s3/jazz-songs/louis/
|
||||
|
||||
3. Remove contents of a folder recursively.
|
||||
$ mc {{.Name}} --force --recursive s3/jazz-songs/louis/
|
||||
|
||||
4. Remove all matching objects with this prefix.
|
||||
$ mc {{.Name}} --force s3/ogg/gunmetal
|
||||
|
||||
5. Drop an incomplete upload of an object.
|
||||
$ mc {{.Name}} --incomplete s3/jazz-songs/louis/file01.mp3
|
||||
|
||||
6. Drop all incomplete uploads recursively matching this prefix.
|
||||
$ mc {{.Name}} --incomplete --force --recursive s3/jazz-songs/louis/
|
||||
`,
|
||||
}
|
||||
|
||||
// Structured message depending on the type of console.
|
||||
type rmMessage struct {
|
||||
Status string `json:"status"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// Colorized message for console printing.
|
||||
func (r rmMessage) String() string {
|
||||
return console.Colorize("Remove", fmt.Sprintf("Removed ‘%s’.", r.URL))
|
||||
}
|
||||
|
||||
// JSON'ified message for scripting.
|
||||
func (r rmMessage) JSON() string {
|
||||
msgBytes, e := json.Marshal(r)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
return string(msgBytes)
|
||||
}
|
||||
|
||||
// Validate command line arguments.
|
||||
func checkRmSyntax(ctx *cli.Context) {
|
||||
// Set command flags from context.
|
||||
isForce := ctx.Bool("force")
|
||||
isRecursive := ctx.Bool("recursive")
|
||||
isFake := ctx.Bool("fake")
|
||||
|
||||
if !ctx.Args().Present() {
|
||||
exitCode := 1
|
||||
cli.ShowCommandHelpAndExit(ctx, "rm", exitCode)
|
||||
}
|
||||
|
||||
// For all recursive operations make sure to check for 'force' flag.
|
||||
if isRecursive && !isForce && !isFake {
|
||||
fatalIf(errDummy().Trace(),
|
||||
"Recursive removal requires --force option. Please review carefully before performing this *DANGEROUS* operation.")
|
||||
}
|
||||
}
|
||||
|
||||
// Remove a single object.
|
||||
func rm(targetAlias, targetURL string, isIncomplete, isFake bool) *probe.Error {
|
||||
clnt, err := newClientFromAlias(targetAlias, targetURL)
|
||||
if err != nil {
|
||||
return err.Trace(targetURL)
|
||||
}
|
||||
|
||||
if isFake { // It is a fake remove. Return success.
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = clnt.Remove(isIncomplete); err != nil {
|
||||
return err.Trace(targetURL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove all objects recursively.
|
||||
func rmAll(targetAlias, targetURL string, isRecursive, isIncomplete, isFake bool) {
|
||||
// Initialize new client.
|
||||
clnt, err := newClientFromAlias(targetAlias, targetURL)
|
||||
if err != nil {
|
||||
errorIf(err.Trace(targetURL), "Invalid URL ‘"+targetURL+"’.")
|
||||
return // End of journey.
|
||||
}
|
||||
|
||||
/* Disable recursion and only list this folder's contents. We
|
||||
perform manual depth-first recursion ourself here. */
|
||||
nonRecursive := false
|
||||
for entry := range clnt.List(nonRecursive, isIncomplete) {
|
||||
if entry.Err != nil {
|
||||
errorIf(entry.Err.Trace(targetURL), "Unable to list ‘"+targetURL+"’.")
|
||||
return // End of journey.
|
||||
}
|
||||
|
||||
if entry.Type.IsDir() && isRecursive {
|
||||
// Add separator at the end to remove all its contents.
|
||||
url := entry.URL
|
||||
url.Path = strings.TrimSuffix(entry.URL.Path, string(entry.URL.Separator)) + string(entry.URL.Separator)
|
||||
|
||||
// Recursively remove contents of this directory.
|
||||
rmAll(targetAlias, url.String(), isRecursive, isIncomplete, isFake)
|
||||
}
|
||||
|
||||
// Regular type.
|
||||
if err = rm(targetAlias, entry.URL.String(), isIncomplete, isFake); err != nil {
|
||||
errorIf(err.Trace(entry.URL.String()), "Unable to remove ‘"+entry.URL.String()+"’.")
|
||||
continue
|
||||
}
|
||||
// Construct user facing message and path.
|
||||
entryPath := filepath.ToSlash(filepath.Join(targetAlias, entry.URL.Path))
|
||||
printMsg(rmMessage{Status: "success", URL: entryPath})
|
||||
}
|
||||
}
|
||||
|
||||
// main for rm command.
|
||||
func mainRm(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'rm' cli arguments.
|
||||
checkRmSyntax(ctx)
|
||||
|
||||
// rm specific flags.
|
||||
isForce := ctx.Bool("force")
|
||||
isIncomplete := ctx.Bool("incomplete")
|
||||
isRecursive := ctx.Bool("recursive")
|
||||
isFake := ctx.Bool("fake")
|
||||
|
||||
// Set color.
|
||||
console.SetColor("Remove", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
// Support multiple targets.
|
||||
for _, url := range ctx.Args() {
|
||||
targetAlias, targetURL, _ := mustExpandAlias(url)
|
||||
if isRecursive && isForce || isFake {
|
||||
rmAll(targetAlias, targetURL, isRecursive, isIncomplete, isFake)
|
||||
} else {
|
||||
if err := rm(targetAlias, targetURL, isIncomplete, isFake); err != nil {
|
||||
errorIf(err.Trace(url), "Unable to remove ‘"+url+"’.")
|
||||
continue
|
||||
}
|
||||
printMsg(rmMessage{Status: "success", URL: url})
|
||||
}
|
||||
}
|
||||
}
|
||||
53
command/runtime-checks.go
Normal file
53
command/runtime-checks.go
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015, 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
)
|
||||
|
||||
// check if minimum Go version is met.
|
||||
func checkGoVersion() {
|
||||
runtimeVersion := runtime.Version()
|
||||
|
||||
// Checking version is always successful with go tip
|
||||
if strings.HasPrefix(runtimeVersion, "devel") {
|
||||
return
|
||||
}
|
||||
|
||||
// Parsing golang version
|
||||
curVersion, e := version.NewVersion(runtimeVersion[2:])
|
||||
if e != nil {
|
||||
console.Fatalln("Unable to determine current go version.", e)
|
||||
}
|
||||
|
||||
// Prepare version constraint.
|
||||
constraints, e := version.NewConstraint(minGoVersion)
|
||||
if e != nil {
|
||||
console.Fatalln("Unable to check go version.")
|
||||
}
|
||||
|
||||
// Check for minimum version.
|
||||
if !constraints.Check(curVersion) {
|
||||
console.Fatalln(fmt.Sprintf("Please recompile Minio with Golang version %s.", minGoVersion))
|
||||
}
|
||||
}
|
||||
61
command/scan-bar.go
Normal file
61
command/scan-bar.go
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Minio Client (C) 2014-2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cheggaaa/pb"
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// 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
|
||||
termWidth, e := pb.GetTerminalWidth()
|
||||
fatalIf(probe.NewError(e), "Unable to get terminal size. Please use --quiet option.")
|
||||
|
||||
// Cursor animate channel.
|
||||
cursorCh := cursorAnimate()
|
||||
return func(source string) {
|
||||
scanPrefix := fmt.Sprintf("[%s] %s ", humanize.Comma(int64(fileCount)), string(<-cursorCh))
|
||||
source = fixateScanBar(source, termWidth-len([]rune(scanPrefix)))
|
||||
barText := scanPrefix + source
|
||||
console.PrintC("\r" + barText + "\r")
|
||||
fileCount++
|
||||
}
|
||||
}
|
||||
276
command/session-main.go
Normal file
276
command/session-main.go
Normal file
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Minio Client, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
sessionFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of session.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Manage sessions for cp and mirror.
|
||||
var sessionCmd = cli.Command{
|
||||
Name: "session",
|
||||
Usage: "Manage saved sessions of cp and mirror operations.",
|
||||
Action: mainSession,
|
||||
Flags: append(sessionFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS] OPERATION [ARG]
|
||||
|
||||
OPERATION:
|
||||
resume Resume a previously saved session.
|
||||
clear Clear a previously saved session.
|
||||
list List all previously saved sessions.
|
||||
|
||||
SESSION-ID:
|
||||
SESSION - Session can either be $SESSION-ID or "all".
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. List sessions.
|
||||
$ mc {{.Name}} list
|
||||
|
||||
2. Resume session.
|
||||
$ mc {{.Name}} resume ygVIpSJs
|
||||
|
||||
3. Clear session.
|
||||
$ mc {{.Name}} clear ygVIpSJs
|
||||
|
||||
4. Clear all sessions.
|
||||
$ mc {{.Name}} clear all
|
||||
`,
|
||||
}
|
||||
|
||||
// bySessionWhen is a type for sorting session metadata by time.
|
||||
type bySessionWhen []*sessionV7
|
||||
|
||||
func (b bySessionWhen) Len() int { return len(b) }
|
||||
func (b bySessionWhen) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
|
||||
func (b bySessionWhen) Less(i, j int) bool { return b[i].Header.When.Before(b[j].Header.When) }
|
||||
|
||||
// listSessions list all current sessions.
|
||||
func listSessions() *probe.Error {
|
||||
var bySessions []*sessionV7
|
||||
for _, sid := range getSessionIDs() {
|
||||
session, err := loadSessionV7(sid)
|
||||
if err != nil {
|
||||
continue // Skip 'broken' session during listing
|
||||
}
|
||||
session.Close() // Session close right here.
|
||||
bySessions = append(bySessions, session)
|
||||
}
|
||||
// sort sessions based on time.
|
||||
sort.Sort(bySessionWhen(bySessions))
|
||||
for _, session := range bySessions {
|
||||
printMsg(session)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearSessionMessage container for clearing session messages.
|
||||
type clearSessionMessage struct {
|
||||
Status string `json:"success"`
|
||||
SessionID string `json:"sessionId"`
|
||||
}
|
||||
|
||||
// String colorized clear session message.
|
||||
func (c clearSessionMessage) String() string {
|
||||
msg := "Session ‘" + c.SessionID + "’"
|
||||
var colorizedMsg string
|
||||
switch c.Status {
|
||||
case "success":
|
||||
colorizedMsg = console.Colorize("ClearSession", msg+" cleared successfully.")
|
||||
case "forced":
|
||||
colorizedMsg = console.Colorize("ClearSession", msg+" cleared forcefully.")
|
||||
}
|
||||
return colorizedMsg
|
||||
}
|
||||
|
||||
// JSON jsonified clear session message.
|
||||
func (c clearSessionMessage) JSON() string {
|
||||
clearSessionJSONBytes, e := json.Marshal(c)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(clearSessionJSONBytes)
|
||||
}
|
||||
|
||||
// clearSession clear sessions.
|
||||
func clearSession(sid string) {
|
||||
if sid == "all" {
|
||||
for _, sid := range getSessionIDs() {
|
||||
session, err := loadSessionV7(sid)
|
||||
fatalIf(err.Trace(sid), "Unable to load session ‘"+sid+"’.")
|
||||
|
||||
fatalIf(session.Delete().Trace(sid), "Unable to load session ‘"+sid+"’.")
|
||||
|
||||
printMsg(clearSessionMessage{Status: "success", SessionID: sid})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !isSessionExists(sid) {
|
||||
fatalIf(errDummy().Trace(sid), "Session ‘"+sid+"’ not found.")
|
||||
}
|
||||
|
||||
session, err := loadSessionV7(sid)
|
||||
if err != nil {
|
||||
// `mc session clear <broken-session-id>` assumes that user is aware that the session is unuseful
|
||||
// and wants the associated session files to be removed
|
||||
removeSessionFile(sid)
|
||||
removeSessionDataFile(sid)
|
||||
printMsg(clearSessionMessage{Status: "forced", SessionID: sid})
|
||||
return
|
||||
}
|
||||
|
||||
if session != nil {
|
||||
fatalIf(session.Delete().Trace(sid), "Unable to load session ‘"+sid+"’.")
|
||||
printMsg(clearSessionMessage{Status: "success", SessionID: sid})
|
||||
}
|
||||
}
|
||||
|
||||
func sessionExecute(s *sessionV7) {
|
||||
switch s.Header.CommandType {
|
||||
case "cp":
|
||||
doCopySession(s)
|
||||
case "mirror":
|
||||
doMirrorSession(s)
|
||||
}
|
||||
}
|
||||
|
||||
func checkSessionSyntax(ctx *cli.Context) {
|
||||
if len(ctx.Args()) < 1 {
|
||||
cli.ShowCommandHelpAndExit(ctx, "session", 1) // last argument is exit code
|
||||
}
|
||||
if strings.TrimSpace(ctx.Args().First()) == "" {
|
||||
cli.ShowCommandHelpAndExit(ctx, "session", 1) // last argument is exit code
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(ctx.Args().First()) {
|
||||
case "list":
|
||||
case "resume":
|
||||
if strings.TrimSpace(ctx.Args().Tail().First()) == "" {
|
||||
fatalIf(errInvalidArgument().Trace(ctx.Args()...), "Unable to validate empty argument.")
|
||||
}
|
||||
case "clear":
|
||||
if strings.TrimSpace(ctx.Args().Tail().First()) == "" {
|
||||
fatalIf(errInvalidArgument().Trace(ctx.Args()...), "Unable to validate empty argument.")
|
||||
}
|
||||
default:
|
||||
cli.ShowCommandHelpAndExit(ctx, "session", 1) // last argument is exit code
|
||||
}
|
||||
}
|
||||
|
||||
// findClosestSessions to match a given string with sessions trie tree.
|
||||
func findClosestSessions(session string) []string {
|
||||
sessionsTree := newTrie() // Allocate a new trie for sessions strings.
|
||||
for _, sid := range getSessionIDs() {
|
||||
sessionsTree.Insert(sid)
|
||||
}
|
||||
var closestSessions []string
|
||||
for _, value := range sessionsTree.PrefixMatch(session) {
|
||||
closestSessions = append(closestSessions, value.(string))
|
||||
}
|
||||
sort.Strings(closestSessions)
|
||||
return closestSessions
|
||||
}
|
||||
|
||||
func mainSession(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check 'session' cli arguments.
|
||||
checkSessionSyntax(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("Command", color.New(color.FgWhite, color.Bold))
|
||||
console.SetColor("SessionID", color.New(color.FgYellow, color.Bold))
|
||||
console.SetColor("SessionTime", color.New(color.FgGreen))
|
||||
console.SetColor("ClearSession", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
if !isSessionDirExists() {
|
||||
fatalIf(createSessionDir().Trace(), "Unable to create session folder.")
|
||||
}
|
||||
|
||||
switch strings.TrimSpace(ctx.Args().First()) {
|
||||
// list all resumable sessions.
|
||||
case "list":
|
||||
fatalIf(listSessions().Trace(ctx.Args()...), "Unable to list sessions.")
|
||||
case "resume":
|
||||
sid := strings.TrimSpace(ctx.Args().Tail().First())
|
||||
if !isSessionExists(sid) {
|
||||
closestSessions := findClosestSessions(sid)
|
||||
errorMsg := "Session ‘" + sid + "’ not found."
|
||||
if len(closestSessions) > 0 {
|
||||
errorMsg += fmt.Sprintf("\n\nDid you mean?\n")
|
||||
for _, session := range closestSessions {
|
||||
errorMsg += fmt.Sprintf(" ‘mc resume session %s’", session)
|
||||
// break on the first one, it is good enough.
|
||||
break
|
||||
}
|
||||
}
|
||||
fatalIf(errDummy().Trace(sid), errorMsg)
|
||||
}
|
||||
s, err := loadSessionV7(sid)
|
||||
fatalIf(err.Trace(sid), "Unable to load session.")
|
||||
|
||||
// Restore the state of global variables from this previous session.
|
||||
s.restoreGlobals()
|
||||
|
||||
savedCwd, e := os.Getwd()
|
||||
fatalIf(probe.NewError(e), "Unable to determine current working folder.")
|
||||
|
||||
if s.Header.RootPath != "" {
|
||||
// change folder to RootPath.
|
||||
e = os.Chdir(s.Header.RootPath)
|
||||
fatalIf(probe.NewError(e), "Unable to change working folder to root path while resuming session.")
|
||||
}
|
||||
sessionExecute(s)
|
||||
err = s.Close()
|
||||
fatalIf(err.Trace(), "Unable to close session file properly.")
|
||||
|
||||
err = s.Delete()
|
||||
fatalIf(err.Trace(), "Unable to clear session files properly.")
|
||||
|
||||
// change folder back to saved path.
|
||||
e = os.Chdir(savedCwd)
|
||||
fatalIf(probe.NewError(e), "Unable to change working folder to saved path ‘"+savedCwd+"’.")
|
||||
// purge a requested pending session, if "all" purge everything.
|
||||
case "clear":
|
||||
clearSession(strings.TrimSpace(ctx.Args().Tail().First()))
|
||||
}
|
||||
}
|
||||
104
command/session-migrate.go
Normal file
104
command/session-migrate.go
Normal file
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Minio Client (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
// 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 && !os.IsNotExist(err.ToGoError()) {
|
||||
fatalIf(err.Trace(sid), "Unable to load version ‘6’. Migration failed please report this issue at https://github.com/minio/mc/issues.")
|
||||
}
|
||||
if sV6Header.Version == "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.
|
||||
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, err := quick.New(sV7Header)
|
||||
fatalIf(err.Trace(sid), "Unable to initialize quick config for session '7' header.")
|
||||
|
||||
err = qs.Save(sessionFile)
|
||||
fatalIf(err.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 && !os.IsNotExist(err.ToGoError()) {
|
||||
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+"’.")
|
||||
}
|
||||
}
|
||||
}
|
||||
76
command/session-old.go
Normal file
76
command/session-old.go
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Minio Client (C) 2016 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/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, err := quick.New(sV6Header)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sid, sV6Header.Version)
|
||||
}
|
||||
err = qs.Load(sessionFile)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sid, sV6Header.Version)
|
||||
}
|
||||
|
||||
sV6Header = qs.Data().(*sessionV6Header)
|
||||
return sV6Header, nil
|
||||
}
|
||||
|
||||
/////////////////// Session V7 ///////////////////
|
||||
// RESERVED FOR FUTURE
|
||||
371
command/session-v7.go
Normal file
371
command/session-v7.go
Normal file
@@ -0,0 +1,371 @@
|
||||
/*
|
||||
* Minio Client, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// Session V2 - Version 2 stores session header and session data in
|
||||
// two separate files. Session data contains fully prepared URL list.
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// sessionV7 resumable session container.
|
||||
type sessionV7 struct {
|
||||
Header *sessionV7Header
|
||||
SessionID string
|
||||
mutex *sync.Mutex
|
||||
DataFP *sessionDataFP
|
||||
sigCh bool
|
||||
}
|
||||
|
||||
// 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 sessionV7) 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 sessionV7) 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.Marshal(sessionMsg)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(sessionBytes)
|
||||
}
|
||||
|
||||
// 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, err := quick.New(s.Header)
|
||||
if err != nil {
|
||||
return nil, err.Trace(sid, s.Header.Version)
|
||||
}
|
||||
err = qs.Load(sessionFile)
|
||||
if err != nil {
|
||||
return nil, err.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
|
||||
}
|
||||
|
||||
// newSessionV7 provides a new session.
|
||||
func newSessionV7() *sessionV7 {
|
||||
s := &sessionV7{}
|
||||
s.Header = &sessionV7Header{}
|
||||
s.Header.Version = "7"
|
||||
// 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.When = time.Now().UTC()
|
||||
s.mutex = new(sync.Mutex)
|
||||
s.SessionID = newRandomID(8)
|
||||
|
||||
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 sessionV7) HasData() bool {
|
||||
return s.Header.LastCopied != "" || s.Header.LastRemoved != ""
|
||||
}
|
||||
|
||||
// NewDataReader provides reader interface to session data file.
|
||||
func (s *sessionV7) NewDataReader() io.Reader {
|
||||
// DataFP is always intitialized, either via new or load functions.
|
||||
s.DataFP.Seek(0, os.SEEK_SET)
|
||||
return io.Reader(s.DataFP)
|
||||
}
|
||||
|
||||
// NewDataReader provides writer interface to session data file.
|
||||
func (s *sessionV7) NewDataWriter() io.Writer {
|
||||
// DataFP is always intitialized, either via new or load functions.
|
||||
s.DataFP.Seek(0, os.SEEK_SET)
|
||||
return io.Writer(s.DataFP)
|
||||
}
|
||||
|
||||
// Save this session.
|
||||
func (s *sessionV7) 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, err := quick.New(s.Header)
|
||||
if err != nil {
|
||||
return err.Trace(s.SessionID)
|
||||
}
|
||||
|
||||
sessionFile, err := getSessionFile(s.SessionID)
|
||||
if err != nil {
|
||||
return err.Trace(s.SessionID)
|
||||
}
|
||||
return qs.Save(sessionFile).Trace(sessionFile)
|
||||
}
|
||||
|
||||
// setGlobals captures the state of global variables into session header.
|
||||
// Used by newSession.
|
||||
func (s *sessionV7) setGlobals() {
|
||||
s.Header.GlobalBoolFlags["quiet"] = globalQuiet
|
||||
s.Header.GlobalBoolFlags["debug"] = globalDebug
|
||||
s.Header.GlobalBoolFlags["json"] = globalJSON
|
||||
s.Header.GlobalBoolFlags["noColor"] = globalNoColor
|
||||
}
|
||||
|
||||
// RestoreGlobals restores the state of global variables.
|
||||
// Used by resumeSession.
|
||||
func (s sessionV7) restoreGlobals() {
|
||||
quiet := s.Header.GlobalBoolFlags["quiet"]
|
||||
debug := s.Header.GlobalBoolFlags["debug"]
|
||||
json := s.Header.GlobalBoolFlags["json"]
|
||||
noColor := s.Header.GlobalBoolFlags["noColor"]
|
||||
setGlobals(quiet, debug, json, noColor)
|
||||
}
|
||||
|
||||
// IsModified - returns if in memory session header has changed from
|
||||
// its on disk value.
|
||||
func (s *sessionV7) isModified(sessionFile string) (bool, *probe.Error) {
|
||||
qs, err := quick.New(s.Header)
|
||||
if err != nil {
|
||||
return false, err.Trace(s.SessionID)
|
||||
}
|
||||
|
||||
var currentHeader = &sessionV7Header{}
|
||||
currentQS, err := quick.Load(sessionFile, currentHeader)
|
||||
if err != nil {
|
||||
// If session does not exist for the first, return modified to
|
||||
// be true.
|
||||
if os.IsNotExist(err.ToGoError()) {
|
||||
return true, nil
|
||||
}
|
||||
// For all other errors return.
|
||||
return false, err.Trace(s.SessionID)
|
||||
}
|
||||
|
||||
changedFields, err := qs.DeepDiff(currentQS)
|
||||
if err != nil {
|
||||
return false, err.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 *sessionV7) 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, err := quick.New(s.Header)
|
||||
if err != nil {
|
||||
return err.Trace(s.SessionID)
|
||||
}
|
||||
// Save an return.
|
||||
return qs.Save(sessionFile).Trace(sessionFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close ends this session and removes all associated session files.
|
||||
func (s *sessionV7) 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 *sessionV7) 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 sessionV7) CloseAndDie() {
|
||||
s.Close()
|
||||
console.Fatalln("Session safely terminated. To resume session ‘mc session resume " + s.SessionID + "’")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
135
command/session.go
Normal file
135
command/session.go
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Minio Client, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/minio/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()
|
||||
}
|
||||
|
||||
// createSessionDir - create session directory.
|
||||
func createSessionDir() *probe.Error {
|
||||
sessionDir, err := getSessionDir()
|
||||
if err != nil {
|
||||
return err.Trace()
|
||||
}
|
||||
|
||||
if e := os.MkdirAll(sessionDir, 0700); 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
|
||||
}
|
||||
|
||||
// removeSessionFile - remove the session file, ending with .json
|
||||
func removeSessionFile(sid string) {
|
||||
sessionFile, err := getSessionFile(sid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.Remove(sessionFile)
|
||||
}
|
||||
|
||||
// removeSessionDataFile - remove the session data file, ending with .data
|
||||
func removeSessionDataFile(sid string) {
|
||||
dataFile, err := getSessionDataFile(sid)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.Remove(dataFile)
|
||||
}
|
||||
60
command/session_test.go
Normal file
60
command/session_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func (s *TestSuite) TestValidSessionID(c *C) {
|
||||
validSid := regexp.MustCompile("^[a-zA-Z]+$")
|
||||
sid := newRandomID(8)
|
||||
c.Assert(len(sid), Equals, 8)
|
||||
c.Assert(validSid.MatchString(sid), Equals, true)
|
||||
}
|
||||
|
||||
func (s *TestSuite) TestSession(c *C) {
|
||||
err := createSessionDir()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(isSessionDirExists(), Equals, true)
|
||||
|
||||
session := newSessionV7()
|
||||
c.Assert(session.Header.CommandArgs, IsNil)
|
||||
c.Assert(len(session.SessionID), Equals, 8)
|
||||
_, e := os.Stat(session.DataFP.Name())
|
||||
c.Assert(e, IsNil)
|
||||
|
||||
err = session.Close()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(isSessionExists(session.SessionID), Equals, true)
|
||||
|
||||
savedSession, err := loadSessionV7(session.SessionID)
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(session.SessionID, Equals, savedSession.SessionID)
|
||||
|
||||
err = savedSession.Close()
|
||||
c.Assert(err, IsNil)
|
||||
|
||||
err = savedSession.Delete()
|
||||
c.Assert(err, IsNil)
|
||||
c.Assert(isSessionExists(session.SessionID), Equals, false)
|
||||
_, e = os.Stat(session.DataFP.Name())
|
||||
c.Assert(e, NotNil)
|
||||
}
|
||||
134
command/share-db-v1.go
Normal file
134
command/share-db-v1.go
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
"github.com/minio/minio/pkg/quick"
|
||||
)
|
||||
|
||||
// shareEntryV1 - container for each download/upload entries.
|
||||
type shareEntryV1 struct {
|
||||
URL string `json:"share"` // Object URL.
|
||||
Date time.Time `json:"date"`
|
||||
Expiry time.Duration `json:"expiry"`
|
||||
ContentType string `json:"contentType,omitempty"` // Only used by upload cmd.
|
||||
}
|
||||
|
||||
// JSON file to persist previously shared uploads.
|
||||
type shareDBV1 struct {
|
||||
Version string `json:"version"`
|
||||
mutex *sync.Mutex
|
||||
|
||||
// key is unique share URL.
|
||||
Shares map[string]shareEntryV1 `json:"shares"`
|
||||
}
|
||||
|
||||
// Instantiate a new uploads structure for persistence.
|
||||
func newShareDBV1() *shareDBV1 {
|
||||
s := &shareDBV1{
|
||||
Version: "1",
|
||||
}
|
||||
s.Shares = make(map[string]shareEntryV1)
|
||||
s.mutex = &sync.Mutex{}
|
||||
return s
|
||||
}
|
||||
|
||||
// Set upload info for each share.
|
||||
func (s *shareDBV1) Set(objectURL string, shareURL string, expiry time.Duration, contentType string) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
s.Shares[shareURL] = shareEntryV1{
|
||||
URL: objectURL,
|
||||
Date: time.Now().UTC(),
|
||||
Expiry: expiry,
|
||||
ContentType: contentType,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete upload info if it exists.
|
||||
func (s *shareDBV1) Delete(objectURL string) {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
delete(s.Shares, objectURL)
|
||||
}
|
||||
|
||||
// Delete all expired uploads.
|
||||
func (s *shareDBV1) deleteAllExpired() {
|
||||
for shareURL, share := range s.Shares {
|
||||
if (share.Expiry - time.Since(share.Date)) <= 0 {
|
||||
// Expired entry. Safe to drop.
|
||||
delete(s.Shares, shareURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load shareDB entries from disk. Any entries held in memory are reset.
|
||||
func (s *shareDBV1) Load(filename string) *probe.Error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
// Check if the db file exist.
|
||||
if _, e := os.Stat(filename); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
|
||||
// Initialize and load using quick package.
|
||||
qs, err := quick.New(newShareDBV1())
|
||||
if err != nil {
|
||||
return err.Trace(filename)
|
||||
}
|
||||
err = qs.Load(filename)
|
||||
if err != nil {
|
||||
return err.Trace(filename)
|
||||
}
|
||||
|
||||
// Copy map over.
|
||||
for k, v := range qs.Data().(*shareDBV1).Shares {
|
||||
s.Shares[k] = v
|
||||
}
|
||||
|
||||
// Filter out expired entries and save changes back to disk.
|
||||
s.deleteAllExpired()
|
||||
s.save(filename)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Persist share uploads to disk.
|
||||
func (s shareDBV1) save(filename string) *probe.Error {
|
||||
// Initialize a new quick file.
|
||||
qs, err := quick.New(s)
|
||||
if err != nil {
|
||||
return err.Trace(filename)
|
||||
}
|
||||
return qs.Save(filename).Trace(filename)
|
||||
}
|
||||
|
||||
// Persist share uploads to disk.
|
||||
func (s shareDBV1) Save(filename string) *probe.Error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
|
||||
return s.save(filename)
|
||||
}
|
||||
184
command/share-download-main.go
Normal file
184
command/share-download-main.go
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
shareDownloadFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of share download",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "recursive, r",
|
||||
Usage: "Share all objects recursively.",
|
||||
},
|
||||
shareFlagExpire,
|
||||
}
|
||||
)
|
||||
|
||||
// Share documents via URL.
|
||||
var shareDownload = cli.Command{
|
||||
Name: "download",
|
||||
Usage: "Generate URLs for download access.",
|
||||
Action: mainShareDownload,
|
||||
Flags: append(shareDownloadFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc share {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc share {{.Name}} [OPTIONS] TARGET [TARGET...]
|
||||
|
||||
OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Share this object with 7 days default expiry.
|
||||
$ mc share {{.Name}} s3/backup/2006-Mar-1/backup.tar.gz
|
||||
|
||||
2. Share this object with 10 minutes expiry.
|
||||
$ mc share {{.Name}} --expire=10m s3/backup/2006-Mar-1/backup.tar.gz
|
||||
|
||||
3. Share all objects under this folder with 5 days expiry.
|
||||
$ mc share {{.Name}} --expire=120h s3/backup/
|
||||
|
||||
4. Share all objects under this folder and all its sub-folders with 5 days expiry.
|
||||
$ mc share {{.Name}} --recursive --expire=120h s3/backup/
|
||||
`,
|
||||
}
|
||||
|
||||
// checkShareDownloadSyntax - validate command-line args.
|
||||
func checkShareDownloadSyntax(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if !args.Present() {
|
||||
cli.ShowCommandHelpAndExit(ctx, "download", 1) // last argument is exit code.
|
||||
}
|
||||
|
||||
// Parse expiry.
|
||||
expiry := shareDefaultExpiry
|
||||
expireArg := ctx.String("expire")
|
||||
if expireArg != "" {
|
||||
var e error
|
||||
expiry, e = time.ParseDuration(expireArg)
|
||||
fatalIf(probe.NewError(e), "Unable to parse expire=‘"+expireArg+"’.")
|
||||
}
|
||||
|
||||
// Validate expiry.
|
||||
if expiry.Seconds() < 1 {
|
||||
fatalIf(errDummy().Trace(expiry.String()), "Expiry cannot be lesser than 1 second.")
|
||||
}
|
||||
if expiry.Seconds() > 604800 {
|
||||
fatalIf(errDummy().Trace(expiry.String()), "Expiry cannot be larger than 7 days.")
|
||||
}
|
||||
|
||||
for _, url := range ctx.Args() {
|
||||
_, _, err := url2Stat(url)
|
||||
fatalIf(err.Trace(url), "Unable to stat ‘"+url+"’.")
|
||||
}
|
||||
}
|
||||
|
||||
// doShareURL share files from target.
|
||||
func doShareDownloadURL(targetURL string, isRecursive bool, expiry time.Duration) *probe.Error {
|
||||
targetAlias, targetURLFull, _, err := expandAlias(targetURL)
|
||||
if err != nil {
|
||||
return err.Trace(targetURL)
|
||||
}
|
||||
clnt, err := newClientFromAlias(targetAlias, targetURLFull)
|
||||
if err != nil {
|
||||
return err.Trace(targetURL)
|
||||
}
|
||||
|
||||
// Load previously saved upload-shares. Add new entries and write it back.
|
||||
shareDB := newShareDBV1()
|
||||
shareDownloadsFile := getShareDownloadsFile()
|
||||
err = shareDB.Load(shareDownloadsFile)
|
||||
if err != nil {
|
||||
return err.Trace(shareDownloadsFile)
|
||||
}
|
||||
|
||||
// Generate share URL for each target.
|
||||
incomplete := false
|
||||
for content := range clnt.List(isRecursive, incomplete) {
|
||||
if content.Err != nil {
|
||||
return content.Err.Trace(clnt.GetURL().String())
|
||||
}
|
||||
// if any incoming directories, we don't need to calculate.
|
||||
if content.Type.IsDir() {
|
||||
continue
|
||||
}
|
||||
objectURL := content.URL.String()
|
||||
newClnt, err := newClientFromAlias(targetAlias, objectURL)
|
||||
if err != nil {
|
||||
return err.Trace(objectURL)
|
||||
}
|
||||
|
||||
// Generate share URL.
|
||||
shareURL, err := newClnt.ShareDownload(expiry)
|
||||
if err != nil {
|
||||
// add objectURL and expiry as part of the trace arguments.
|
||||
return err.Trace(objectURL, "expiry="+expiry.String())
|
||||
}
|
||||
|
||||
// Make new entries to shareDB.
|
||||
contentType := "" // Not useful for download shares.
|
||||
shareDB.Set(objectURL, shareURL, expiry, contentType)
|
||||
printMsg(shareMesssage{
|
||||
ObjectURL: objectURL,
|
||||
ShareURL: shareURL,
|
||||
TimeLeft: expiry,
|
||||
ContentType: contentType,
|
||||
})
|
||||
}
|
||||
|
||||
// Save downloads and return.
|
||||
return shareDB.Save(shareDownloadsFile)
|
||||
}
|
||||
|
||||
// main for share download.
|
||||
func mainShareDownload(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check input arguments.
|
||||
checkShareDownloadSyntax(ctx)
|
||||
|
||||
// Initialize share config folder.
|
||||
initShareConfig()
|
||||
|
||||
// Additional command speific theme customization.
|
||||
shareSetColor()
|
||||
|
||||
// Set command flags from context.
|
||||
isRecursive := ctx.Bool("recursive")
|
||||
expiry := shareDefaultExpiry
|
||||
if ctx.String("expire") != "" {
|
||||
var e error
|
||||
expiry, e = time.ParseDuration(ctx.String("expire"))
|
||||
fatalIf(probe.NewError(e), "Unable to parse expire=‘"+ctx.String("expire")+"’.")
|
||||
}
|
||||
|
||||
for _, targetURL := range ctx.Args() {
|
||||
err := doShareDownloadURL(targetURL, isRecursive, expiry)
|
||||
fatalIf(err.Trace(targetURL), "Unable to share target ‘"+targetURL+"’.")
|
||||
}
|
||||
}
|
||||
123
command/share-list-main.go
Normal file
123
command/share-list-main.go
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
shareListFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of share list.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Share documents via URL.
|
||||
var shareList = cli.Command{
|
||||
Name: "list",
|
||||
Usage: "List previously shared objects and folders.",
|
||||
Action: mainShareList,
|
||||
Flags: append(shareListFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc share {{.Name}} COMMAND - {{.Usage}}
|
||||
|
||||
COMMAND:
|
||||
upload: list previously shared access to uploads.
|
||||
download: list previously shared access to downloads.
|
||||
|
||||
USAGE:
|
||||
mc share {{.Name}}
|
||||
|
||||
EXAMPLES:
|
||||
1. List previously shared downloads, that haven't expired yet.
|
||||
$ mc share {{.Name}} download
|
||||
2. List previously shared uploads, that haven't expired yet.
|
||||
$ mc share {{.Name}} upload
|
||||
`,
|
||||
}
|
||||
|
||||
// validate command-line args.
|
||||
func checkShareListSyntax(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if !args.Present() || (args.First() != "upload" && args.First() != "download") {
|
||||
cli.ShowCommandHelpAndExit(ctx, "list", 1) // last argument is exit code.
|
||||
}
|
||||
}
|
||||
|
||||
// doShareList list shared url's.
|
||||
func doShareList(cmd string) *probe.Error {
|
||||
if cmd != "upload" && cmd != "download" {
|
||||
return probe.NewError(fmt.Errorf("Unknown argument ‘%s’ passed.", cmd))
|
||||
}
|
||||
|
||||
// Fetch defaults.
|
||||
uploadsFile := getShareUploadsFile()
|
||||
downloadsFile := getShareDownloadsFile()
|
||||
|
||||
// Load previously saved upload-shares.
|
||||
shareDB := newShareDBV1()
|
||||
|
||||
// if upload - read uploads file.
|
||||
if cmd == "upload" {
|
||||
if err := shareDB.Load(uploadsFile); err != nil {
|
||||
return err.Trace(uploadsFile)
|
||||
}
|
||||
}
|
||||
|
||||
// if download - read downloads file.
|
||||
if cmd == "download" {
|
||||
if err := shareDB.Load(downloadsFile); err != nil {
|
||||
return err.Trace(downloadsFile)
|
||||
}
|
||||
}
|
||||
|
||||
// Print previously shared entries.
|
||||
for shareURL, share := range shareDB.Shares {
|
||||
printMsg(shareMesssage{
|
||||
ObjectURL: share.URL,
|
||||
ShareURL: shareURL,
|
||||
TimeLeft: share.Expiry - time.Since(share.Date),
|
||||
ContentType: share.ContentType,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// main entry point for share list.
|
||||
func mainShareList(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// validate command-line args.
|
||||
checkShareListSyntax(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
shareSetColor()
|
||||
|
||||
// Initialize share config folder.
|
||||
initShareConfig()
|
||||
|
||||
// List shares.
|
||||
fatalIf(doShareList(ctx.Args().First()).Trace(), "Unable to list previously shared URLs.")
|
||||
}
|
||||
91
command/share-main.go
Normal file
91
command/share-main.go
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
shareFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of share.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Share documents via URL.
|
||||
var shareCmd = cli.Command{
|
||||
Name: "share",
|
||||
Usage: "Generate URL for sharing.",
|
||||
Action: mainShare,
|
||||
Flags: append(shareFlags, globalFlags...),
|
||||
Subcommands: []cli.Command{
|
||||
shareDownload,
|
||||
shareUpload,
|
||||
shareList,
|
||||
},
|
||||
CustomHelpTemplate: `NAME:
|
||||
{{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
{{.Name}} [FLAGS] COMMAND
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
COMMANDS:
|
||||
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
|
||||
{{end}}
|
||||
`,
|
||||
}
|
||||
|
||||
// migrateShare migrate to newest version sequentially.
|
||||
func migrateShare() {
|
||||
if !isShareDirExists() {
|
||||
return
|
||||
}
|
||||
|
||||
// Shared URLs are now managed by sub-commands. So delete any old URLs file if found.
|
||||
oldShareFile := filepath.Join(mustGetShareDir(), "urls.json")
|
||||
if _, e := os.Stat(oldShareFile); e == nil {
|
||||
// Old file exits.
|
||||
e := os.Remove(oldShareFile)
|
||||
fatalIf(probe.NewError(e), "Unable to delete old ‘"+oldShareFile+"’.")
|
||||
console.Infof("Removed older version of share ‘%s’ file.\n", oldShareFile)
|
||||
}
|
||||
}
|
||||
|
||||
// mainShare - main handler for mc share command.
|
||||
func mainShare(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
if ctx.Args().First() != "" { // command help.
|
||||
cli.ShowCommandHelp(ctx, ctx.Args().First())
|
||||
} else { // mc help.
|
||||
cli.ShowAppHelp(ctx)
|
||||
}
|
||||
|
||||
// Sub-commands like "upload" and "download" have their own main.
|
||||
}
|
||||
211
command/share-upload-main.go
Normal file
211
command/share-upload-main.go
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
shareUploadFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help of share download.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "recursive, r",
|
||||
Usage: "Recursively upload any object matching the prefix.",
|
||||
},
|
||||
shareFlagExpire,
|
||||
shareFlagContentType,
|
||||
}
|
||||
)
|
||||
|
||||
// Share documents via URL.
|
||||
var shareUpload = cli.Command{
|
||||
Name: "upload",
|
||||
Usage: "Generate ‘curl’ command to upload objects without requiring access/secret keys.",
|
||||
Action: mainShareUpload,
|
||||
Flags: append(shareUploadFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc share {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc share {{.Name}} [OPTIONS] TARGET [TARGET...]
|
||||
|
||||
OPTIONS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Generate a curl command to allow upload access for a single object. Command expires in 7 days (default).
|
||||
$ mc share {{.Name}} s3/backup/2006-Mar-1/backup.tar.gz
|
||||
|
||||
2. Generate a curl command to allow upload access to a folder. Command expires in 120 hours.
|
||||
$ mc share {{.Name}} --expire=120h s3/backup/2007-Mar-2/
|
||||
|
||||
3. Generate a curl command to allow upload access of only '.png' images to a folder. Command expires in 2 hours.
|
||||
$ mc share {{.Name}} --expire=2h --content-type=image/png s3/backup/2007-Mar-2/
|
||||
|
||||
4. Generate a curl command to allow upload access to any objects matching the key prefix 'backup/'. Command expires in 2 hours.
|
||||
$ mc share {{.Name}} --recursive --expire=2h s3/backup/2007-Mar-2/backup/
|
||||
`,
|
||||
}
|
||||
|
||||
// checkShareUploadSyntax - validate command-line args.
|
||||
func checkShareUploadSyntax(ctx *cli.Context) {
|
||||
args := ctx.Args()
|
||||
if !args.Present() {
|
||||
cli.ShowCommandHelpAndExit(ctx, "upload", 1) // last argument is exit code.
|
||||
}
|
||||
|
||||
// Set command flags from context.
|
||||
isRecursive := ctx.Bool("recursive")
|
||||
expireArg := ctx.String("expire")
|
||||
|
||||
// Parse expiry.
|
||||
expiry := shareDefaultExpiry
|
||||
if expireArg != "" {
|
||||
var e error
|
||||
expiry, e = time.ParseDuration(expireArg)
|
||||
fatalIf(probe.NewError(e), "Unable to parse expire=‘"+expireArg+"’.")
|
||||
}
|
||||
|
||||
// Validate expiry.
|
||||
if expiry.Seconds() < 1 {
|
||||
fatalIf(errDummy().Trace(expiry.String()),
|
||||
"Expiry cannot be lesser than 1 second.")
|
||||
}
|
||||
if expiry.Seconds() > 604800 {
|
||||
fatalIf(errDummy().Trace(expiry.String()),
|
||||
"Expiry cannot be larger than 7 days.")
|
||||
}
|
||||
|
||||
for _, targetURL := range ctx.Args() {
|
||||
url := newClientURL(targetURL)
|
||||
if strings.HasSuffix(targetURL, string(url.Separator)) && !isRecursive {
|
||||
fatalIf(errInvalidArgument().Trace(targetURL),
|
||||
"Use --recursive option to generate curl command for prefixes.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// makeCurlCmd constructs curl command-line.
|
||||
func makeCurlCmd(key string, isRecursive bool, uploadInfo map[string]string) string {
|
||||
URL := newClientURL(key)
|
||||
postURL := URL.Scheme + URL.SchemeSeparator + URL.Host + string(URL.Separator)
|
||||
if !isBucketVirtualStyle(URL.Host) {
|
||||
postURL = postURL + uploadInfo["bucket"]
|
||||
}
|
||||
postURL += " "
|
||||
curlCommand := "curl " + postURL
|
||||
for k, v := range uploadInfo {
|
||||
if k == "key" {
|
||||
key = v
|
||||
continue
|
||||
}
|
||||
curlCommand += fmt.Sprintf("-F %s=%s ", k, v)
|
||||
}
|
||||
// If key starts with is enabled prefix it with the output.
|
||||
if isRecursive {
|
||||
curlCommand += fmt.Sprintf("-F key=%s<NAME> ", key) // Object name.
|
||||
} else {
|
||||
curlCommand += fmt.Sprintf("-F key=%s ", key) // Object name.
|
||||
}
|
||||
curlCommand += "-F file=@<FILE>" // File to upload.
|
||||
return curlCommand
|
||||
}
|
||||
|
||||
// save shared URL to disk.
|
||||
func saveSharedURL(objectURL string, shareURL string, expiry time.Duration, contentType string) *probe.Error {
|
||||
// Load previously saved upload-shares.
|
||||
shareDB := newShareDBV1()
|
||||
if err := shareDB.Load(getShareUploadsFile()); err != nil {
|
||||
return err.Trace(getShareUploadsFile())
|
||||
}
|
||||
|
||||
// Make new entries to uploadsDB.
|
||||
shareDB.Set(objectURL, shareURL, expiry, contentType)
|
||||
shareDB.Save(getShareUploadsFile())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// doShareUploadURL uploads files to the target.
|
||||
func doShareUploadURL(objectURL string, isRecursive bool, expiry time.Duration, contentType string) *probe.Error {
|
||||
clnt, err := newClient(objectURL)
|
||||
if err != nil {
|
||||
return err.Trace(objectURL)
|
||||
}
|
||||
|
||||
// Generate pre-signed access info.
|
||||
uploadInfo, err := clnt.ShareUpload(isRecursive, expiry, contentType)
|
||||
if err != nil {
|
||||
return err.Trace(objectURL, "expiry="+expiry.String(), "contentType="+contentType)
|
||||
}
|
||||
|
||||
// Get the new expanded url.
|
||||
objectURL = clnt.GetURL().String()
|
||||
|
||||
// Generate curl command.
|
||||
curlCmd := makeCurlCmd(objectURL, isRecursive, uploadInfo)
|
||||
|
||||
printMsg(shareMesssage{
|
||||
ObjectURL: objectURL,
|
||||
ShareURL: curlCmd,
|
||||
TimeLeft: expiry,
|
||||
ContentType: contentType,
|
||||
})
|
||||
|
||||
// save shared URL to disk.
|
||||
return saveSharedURL(objectURL, curlCmd, expiry, contentType)
|
||||
}
|
||||
|
||||
// main for share upload command.
|
||||
func mainShareUpload(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// check input arguments.
|
||||
checkShareUploadSyntax(ctx)
|
||||
|
||||
// Initialize share config folder.
|
||||
initShareConfig()
|
||||
|
||||
// Additional command speific theme customization.
|
||||
shareSetColor()
|
||||
|
||||
// Set command flags from context.
|
||||
isRecursive := ctx.Bool("recursive")
|
||||
expireArg := ctx.String("expire")
|
||||
expiry := shareDefaultExpiry
|
||||
contentType := ctx.String("content-type")
|
||||
if expireArg != "" {
|
||||
var e error
|
||||
expiry, e = time.ParseDuration(expireArg)
|
||||
fatalIf(probe.NewError(e), "Unable to parse expire=‘"+expireArg+"’.")
|
||||
}
|
||||
|
||||
for _, targetURL := range ctx.Args() {
|
||||
err := doShareUploadURL(targetURL, isRecursive, expiry, contentType)
|
||||
fatalIf(err.Trace(targetURL), "Unable to generate curl command for upload ‘"+targetURL+"’.")
|
||||
}
|
||||
}
|
||||
197
command/share.go
Normal file
197
command/share.go
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Default expiry is 7 days (168h).
|
||||
shareDefaultExpiry = time.Duration(604800) * time.Second
|
||||
)
|
||||
|
||||
// Upload specific flags.
|
||||
var (
|
||||
shareFlagContentType = cli.StringFlag{
|
||||
Name: "content-type, T",
|
||||
Usage: "Speific content-type to allow.",
|
||||
}
|
||||
shareFlagExpire = cli.StringFlag{
|
||||
Name: "expire, E",
|
||||
Value: "168h",
|
||||
Usage: "Set expiry in NN[h|m|s].",
|
||||
}
|
||||
)
|
||||
|
||||
// Structured share command message.
|
||||
type shareMesssage struct {
|
||||
Status string `json:"status"`
|
||||
ObjectURL string `json:"url"`
|
||||
ShareURL string `json:"share"`
|
||||
TimeLeft time.Duration `json:"timeLeft"`
|
||||
ContentType string `json:"contentType,omitempty"` // Only used by upload cmd.
|
||||
}
|
||||
|
||||
// String - Themefied string message for console printing.
|
||||
func (s shareMesssage) String() string {
|
||||
msg := console.Colorize("URL", fmt.Sprintf("URL: %s\n", s.ObjectURL))
|
||||
msg += console.Colorize("Expire", fmt.Sprintf("Expire: %s\n", timeDurationToHumanizedTime(s.TimeLeft)))
|
||||
if s.ContentType != "" {
|
||||
msg += console.Colorize("Content-type", fmt.Sprintf("Content-Type: %s\n", s.ContentType))
|
||||
}
|
||||
|
||||
// Highlight <FILE> specifically. "share upload" sub-commands use this identifier.
|
||||
shareURL := strings.Replace(s.ShareURL, "<FILE>", console.Colorize("File", "<FILE>"), 1)
|
||||
// Highlight <KEY> specifically for recursive operation.
|
||||
shareURL = strings.Replace(shareURL, "<NAME>", console.Colorize("File", "<NAME>"), 1)
|
||||
|
||||
msg += console.Colorize("Share", fmt.Sprintf("Share: %s\n", shareURL))
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
// JSON - JSONified message for scripting.
|
||||
func (s shareMesssage) JSON() string {
|
||||
s.Status = "success"
|
||||
shareMessageBytes, e := json.Marshal(s)
|
||||
fatalIf(probe.NewError(e), "Failed to marshal into JSON.")
|
||||
|
||||
// JSON encoding escapes ampersand into its unicode character
|
||||
// which is not usable directly for share and fails with cloud
|
||||
// storage. convert them back so that they are usable.
|
||||
shareMessageBytes = bytes.Replace(shareMessageBytes, []byte("\\u0026"), []byte("&"), -1)
|
||||
shareMessageBytes = bytes.Replace(shareMessageBytes, []byte("\\u003c"), []byte("<"), -1)
|
||||
shareMessageBytes = bytes.Replace(shareMessageBytes, []byte("\\u003e"), []byte(">"), -1)
|
||||
|
||||
return string(shareMessageBytes)
|
||||
}
|
||||
|
||||
// shareSetColor sets colors share sub-commands.
|
||||
func shareSetColor() {
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("URL", color.New(color.Bold))
|
||||
console.SetColor("Expire", color.New(color.FgCyan))
|
||||
console.SetColor("Content-type", color.New(color.FgBlue))
|
||||
console.SetColor("Share", color.New(color.FgGreen))
|
||||
console.SetColor("File", color.New(color.FgRed, color.Bold))
|
||||
}
|
||||
|
||||
// Get share dir name.
|
||||
func getShareDir() (string, *probe.Error) {
|
||||
configDir, err := getMcConfigDir()
|
||||
if err != nil {
|
||||
return "", err.Trace()
|
||||
}
|
||||
|
||||
sharedURLsDataDir := filepath.Join(configDir, globalSharedURLsDataDir)
|
||||
return sharedURLsDataDir, nil
|
||||
}
|
||||
|
||||
// Get share dir name or die. (NOTE: This ‘Die’ approach is only OK for mc like tools.).
|
||||
func mustGetShareDir() string {
|
||||
shareDir, err := getShareDir()
|
||||
fatalIf(err.Trace(), "Unable to determine share folder.")
|
||||
return shareDir
|
||||
}
|
||||
|
||||
// Check if the share dir exists.
|
||||
func isShareDirExists() bool {
|
||||
if _, e := os.Stat(mustGetShareDir()); e != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Create config share dir.
|
||||
func createShareDir() *probe.Error {
|
||||
if e := os.MkdirAll(mustGetShareDir(), 0700); e != nil {
|
||||
return probe.NewError(e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get share uploads file.
|
||||
func getShareUploadsFile() string {
|
||||
return filepath.Join(mustGetShareDir(), "uploads.json")
|
||||
}
|
||||
|
||||
// Get share downloads file.
|
||||
func getShareDownloadsFile() string {
|
||||
return filepath.Join(mustGetShareDir(), "downloads.json")
|
||||
}
|
||||
|
||||
// Check if share uploads file exists?.
|
||||
func isShareUploadsExists() bool {
|
||||
if _, e := os.Stat(getShareUploadsFile()); e != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if share downloads file exists?.
|
||||
func isShareDownloadsExists() bool {
|
||||
if _, e := os.Stat(getShareDownloadsFile()); e != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Initialize share uploads file.
|
||||
func initShareUploadsFile() *probe.Error {
|
||||
return newShareDBV1().Save(getShareUploadsFile())
|
||||
}
|
||||
|
||||
// Initialize share downloads file.
|
||||
func initShareDownloadsFile() *probe.Error {
|
||||
return newShareDBV1().Save(getShareDownloadsFile())
|
||||
}
|
||||
|
||||
// Initialize share directory, if not done already.
|
||||
func initShareConfig() {
|
||||
// Share directory.
|
||||
if !isShareDirExists() {
|
||||
fatalIf(createShareDir().Trace(mustGetShareDir()),
|
||||
"Failed to create share ‘"+mustGetShareDir()+"’ folder.")
|
||||
console.Infof("Successfully created ‘%s’.\n", mustGetShareDir())
|
||||
}
|
||||
|
||||
// Uploads share file.
|
||||
if !isShareUploadsExists() {
|
||||
fatalIf(initShareUploadsFile().Trace(getShareUploadsFile()),
|
||||
"Failed to initialize share uploads ‘"+getShareUploadsFile()+"’ file.")
|
||||
console.Infof("Initialized share uploads ‘%s’ file.\n", getShareUploadsFile())
|
||||
}
|
||||
|
||||
// Downloads share file.
|
||||
if !isShareDownloadsExists() {
|
||||
fatalIf(initShareDownloadsFile().Trace(getShareDownloadsFile()),
|
||||
"Failed to initialize share downloads ‘"+getShareDownloadsFile()+"’ file.")
|
||||
console.Infof("Initialized share downloads ‘%s’ file.\n", getShareDownloadsFile())
|
||||
}
|
||||
}
|
||||
49
command/signals.go
Normal file
49
command/signals.go
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Minio Client, (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
)
|
||||
|
||||
// signalTrap traps the registered signals and notifies the caller.
|
||||
func signalTrap(sig ...os.Signal) <-chan bool {
|
||||
// channel to notify the caller.
|
||||
trapCh := make(chan bool, 1)
|
||||
|
||||
go func(chan<- bool) {
|
||||
// channel to receive signals.
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
defer close(sigCh)
|
||||
|
||||
// `signal.Notify` registers the given channel to
|
||||
// receive notifications of the specified signals.
|
||||
signal.Notify(sigCh, sig...)
|
||||
|
||||
// Wait for the signal.
|
||||
<-sigCh
|
||||
|
||||
// Once signal has been received stop signal Notify handler.
|
||||
signal.Stop(sigCh)
|
||||
|
||||
// Notify the caller.
|
||||
trapCh <- true
|
||||
}(trapCh)
|
||||
|
||||
return trapCh
|
||||
}
|
||||
76
command/time.go
Normal file
76
command/time.go
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// humanizedTime container to capture humanized time.
|
||||
type humanizedTime struct {
|
||||
Days int64 `json:"days,omitempty"`
|
||||
Hours int64 `json:"hours,omitempty"`
|
||||
Minutes int64 `json:"minutes,omitempty"`
|
||||
Seconds int64 `json:"seconds,omitempty"`
|
||||
}
|
||||
|
||||
// String() humanizes humanizedTime to human readable,
|
||||
func (r humanizedTime) String() string {
|
||||
if r.Days == 0 && r.Hours == 0 && r.Minutes == 0 {
|
||||
return fmt.Sprintf("%d seconds", r.Seconds)
|
||||
}
|
||||
if r.Days == 0 && r.Hours == 0 {
|
||||
return fmt.Sprintf("%d minutes %d seconds", r.Minutes, r.Seconds)
|
||||
}
|
||||
if r.Days == 0 {
|
||||
return fmt.Sprintf("%d hours %d minutes %d seconds", r.Hours, r.Minutes, r.Seconds)
|
||||
}
|
||||
return fmt.Sprintf("%d days %d hours %d minutes %d seconds", r.Days, r.Hours, r.Minutes, r.Seconds)
|
||||
}
|
||||
|
||||
// timeDurationToHumanizedTime convert golang time.Duration to a custom more readable humanizedTime.
|
||||
func timeDurationToHumanizedTime(duration time.Duration) humanizedTime {
|
||||
r := humanizedTime{}
|
||||
if duration.Seconds() < 60.0 {
|
||||
r.Seconds = int64(duration.Seconds())
|
||||
return r
|
||||
}
|
||||
if duration.Minutes() < 60.0 {
|
||||
remainingSeconds := math.Mod(duration.Seconds(), 60)
|
||||
r.Seconds = int64(remainingSeconds)
|
||||
r.Minutes = int64(duration.Minutes())
|
||||
return r
|
||||
}
|
||||
if duration.Hours() < 24.0 {
|
||||
remainingMinutes := math.Mod(duration.Minutes(), 60)
|
||||
remainingSeconds := math.Mod(duration.Seconds(), 60)
|
||||
r.Seconds = int64(remainingSeconds)
|
||||
r.Minutes = int64(remainingMinutes)
|
||||
r.Hours = int64(duration.Hours())
|
||||
return r
|
||||
}
|
||||
remainingHours := math.Mod(duration.Hours(), 24)
|
||||
remainingMinutes := math.Mod(duration.Minutes(), 60)
|
||||
remainingSeconds := math.Mod(duration.Seconds(), 60)
|
||||
r.Hours = int64(remainingHours)
|
||||
r.Minutes = int64(remainingMinutes)
|
||||
r.Seconds = int64(remainingSeconds)
|
||||
r.Days = int64(duration.Hours() / 24)
|
||||
return r
|
||||
}
|
||||
114
command/trie.go
Normal file
114
command/trie.go
Normal file
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This file implements a simple trie tree to be used for 'mc' cli commands.
|
||||
package command
|
||||
|
||||
// This package borrows idea from - https://godoc.org/golang.org/x/text/internal/triegen.
|
||||
|
||||
// Trie is a trie container.
|
||||
type Trie struct {
|
||||
root *trieNode
|
||||
size int
|
||||
}
|
||||
|
||||
// newTrie create a new trie.
|
||||
func newTrie() *Trie {
|
||||
return &Trie{
|
||||
root: newTrieNode(),
|
||||
size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// trieNode trie tree node container carries value and children.
|
||||
type trieNode struct {
|
||||
exists bool
|
||||
value interface{}
|
||||
child map[rune]*trieNode // runes as child.
|
||||
}
|
||||
|
||||
// newTrieNode create a new trie node.
|
||||
func newTrieNode() *trieNode {
|
||||
return &trieNode{
|
||||
exists: false,
|
||||
value: nil,
|
||||
child: make(map[rune]*trieNode),
|
||||
}
|
||||
}
|
||||
|
||||
// Insert insert a key.
|
||||
func (t *Trie) Insert(key string) {
|
||||
curNode := t.root
|
||||
for _, v := range key {
|
||||
if curNode.child[v] == nil {
|
||||
curNode.child[v] = newTrieNode()
|
||||
}
|
||||
curNode = curNode.child[v]
|
||||
}
|
||||
|
||||
if !curNode.exists {
|
||||
// increment when new rune child is added.
|
||||
t.size++
|
||||
curNode.exists = true
|
||||
}
|
||||
// value is stored for retrieval in future.
|
||||
curNode.value = key
|
||||
}
|
||||
|
||||
// PrefixMatch - prefix match.
|
||||
func (t *Trie) PrefixMatch(key string) []interface{} {
|
||||
node, _ := t.findNode(key)
|
||||
if node != nil {
|
||||
return t.walk(node)
|
||||
}
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
// walk the tree.
|
||||
func (t *Trie) walk(node *trieNode) (ret []interface{}) {
|
||||
if node.exists {
|
||||
ret = append(ret, node.value)
|
||||
}
|
||||
for _, v := range node.child {
|
||||
ret = append(ret, t.walk(v)...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// find nodes corresponding to key.
|
||||
func (t *Trie) findNode(key string) (node *trieNode, index int) {
|
||||
curNode := t.root
|
||||
f := false
|
||||
for k, v := range key {
|
||||
if f {
|
||||
index = k
|
||||
f = false
|
||||
}
|
||||
if curNode.child[v] == nil {
|
||||
return nil, index
|
||||
}
|
||||
curNode = curNode.child[v]
|
||||
if curNode.exists {
|
||||
f = true
|
||||
}
|
||||
}
|
||||
|
||||
if curNode.exists {
|
||||
index = len(key)
|
||||
}
|
||||
|
||||
return curNode, index
|
||||
}
|
||||
68
command/typed-errors.go
Normal file
68
command/typed-errors.go
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
errDummy = func() *probe.Error {
|
||||
return probe.NewError(errors.New("")).Untrace()
|
||||
}
|
||||
|
||||
errInvalidArgument = func() *probe.Error {
|
||||
return probe.NewError(errors.New("Invalid arguments provided, cannot proceed.")).Untrace()
|
||||
}
|
||||
|
||||
errUnrecognizedDiffType = func(diff differType) *probe.Error {
|
||||
return probe.NewError(errors.New("Unrecognized diffType: " + diff.String() + " provided.")).Untrace()
|
||||
}
|
||||
|
||||
errInvalidAliasedURL = func(URL string) *probe.Error {
|
||||
return probe.NewError(errors.New("Use ‘mc config host add mycloud " + URL + " ...’ to add an alias. Use the alias for S3 operations.")).Untrace()
|
||||
}
|
||||
|
||||
errNoMatchingHost = func(URL string) *probe.Error {
|
||||
return probe.NewError(errors.New("No matching host found for the given URL ‘" + URL + "’.")).Untrace()
|
||||
}
|
||||
|
||||
errInvalidSource = func(URL string) *probe.Error {
|
||||
return probe.NewError(errors.New("Invalid source ‘" + URL + "’.")).Untrace()
|
||||
}
|
||||
|
||||
errInvalidTarget = func(URL string) *probe.Error {
|
||||
return probe.NewError(errors.New("Invalid target ‘" + URL + "’.")).Untrace()
|
||||
}
|
||||
|
||||
errOverWriteNotAllowed = func(URL string) *probe.Error {
|
||||
return probe.NewError(errors.New("Overwrite not allowed for ‘" + URL + "’. Use ‘--force’ to override this behavior."))
|
||||
}
|
||||
|
||||
errDeleteNotAllowed = func(URL string) *probe.Error {
|
||||
return probe.NewError(errors.New("Delete not allowed for ‘" + URL + "’. Use ‘--force’ to override this behavior."))
|
||||
}
|
||||
errSourceIsDir = func(URL string) *probe.Error {
|
||||
return probe.NewError(errors.New("Source ‘" + URL + "’ is a folder.")).Untrace()
|
||||
}
|
||||
|
||||
errSourceTargetSame = func(URL string) *probe.Error {
|
||||
return probe.NewError(errors.New("Source and target URL can not be same : " + URL)).Untrace()
|
||||
}
|
||||
)
|
||||
215
command/update-main.go
Normal file
215
command/update-main.go
Normal file
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
// command specific flags.
|
||||
var (
|
||||
updateFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help for update.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "experimental, E",
|
||||
Usage: "Check experimental update.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Check for new software updates.
|
||||
var updateCmd = cli.Command{
|
||||
Name: "update",
|
||||
Usage: "Check for a new software update.",
|
||||
Action: mainUpdate,
|
||||
Flags: append(updateFlags, globalFlags...),
|
||||
CustomHelpTemplate: `Name:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS]
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
EXAMPLES:
|
||||
1. Check for any new official release.
|
||||
$ mc {{.Name}}
|
||||
|
||||
2. Check for any new experimental release.
|
||||
$ mc {{.Name}} --experimental
|
||||
`,
|
||||
}
|
||||
|
||||
// update URL endpoints.
|
||||
const (
|
||||
mcUpdateStableURL = "https://dl.minio.io/client/mc/release"
|
||||
mcUpdateExperimentalURL = "https://dl.minio.io/client/mc/experimental"
|
||||
)
|
||||
|
||||
// updateMessage container to hold update messages.
|
||||
type updateMessage struct {
|
||||
Status string `json:"status"`
|
||||
Update bool `json:"update"`
|
||||
Download string `json:"downloadURL"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// String colorized update message.
|
||||
func (u updateMessage) String() string {
|
||||
if !u.Update {
|
||||
return console.Colorize("Update", "You are already running the most recent version of ‘mc’.")
|
||||
}
|
||||
var msg string
|
||||
if runtime.GOOS == "windows" {
|
||||
msg = "Download " + u.Download
|
||||
} else {
|
||||
msg = "Download " + u.Download
|
||||
}
|
||||
msg, err := colorizeUpdateMessage(msg)
|
||||
fatalIf(err.Trace(msg), "Unable to colorize experimental update notification string ‘"+msg+"’.")
|
||||
return msg
|
||||
}
|
||||
|
||||
// JSON jsonified update message.
|
||||
func (u updateMessage) JSON() string {
|
||||
u.Status = "success"
|
||||
updateMessageJSONBytes, e := json.Marshal(u)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
|
||||
return string(updateMessageJSONBytes)
|
||||
}
|
||||
|
||||
func parseReleaseData(data string) (time.Time, *probe.Error) {
|
||||
releaseStr := strings.Fields(data)
|
||||
if len(releaseStr) < 2 {
|
||||
return time.Time{}, probe.NewError(errors.New("Update data malformed"))
|
||||
}
|
||||
releaseDate := releaseStr[1]
|
||||
releaseDateSplits := strings.SplitN(releaseDate, ".", 3)
|
||||
if len(releaseDateSplits) < 3 {
|
||||
return time.Time{}, probe.NewError(errors.New("Update data malformed"))
|
||||
}
|
||||
if releaseDateSplits[0] != "mc" {
|
||||
return time.Time{}, probe.NewError(errors.New("Update data malformed, missing mc tag"))
|
||||
}
|
||||
// "OFFICIAL" tag is still kept for backward compatibility, we should remove this for the next release.
|
||||
if releaseDateSplits[1] != "RELEASE" && releaseDateSplits[1] != "OFFICIAL" {
|
||||
return time.Time{}, probe.NewError(errors.New("Update data malformed, missing RELEASE tag"))
|
||||
}
|
||||
dateSplits := strings.SplitN(releaseDateSplits[2], "T", 2)
|
||||
if len(dateSplits) < 2 {
|
||||
return time.Time{}, probe.NewError(errors.New("Update data malformed, not in modified RFC3359 form"))
|
||||
}
|
||||
dateSplits[1] = strings.Replace(dateSplits[1], "-", ":", -1)
|
||||
date := strings.Join(dateSplits, "T")
|
||||
|
||||
parsedDate, e := time.Parse(time.RFC3339, date)
|
||||
if e != nil {
|
||||
return time.Time{}, probe.NewError(e)
|
||||
}
|
||||
return parsedDate, nil
|
||||
}
|
||||
|
||||
// verify updates for releases.
|
||||
func getReleaseUpdate(updateURL string) {
|
||||
// Construct a new update url.
|
||||
newUpdateURLPrefix := updateURL + "/" + runtime.GOOS + "-" + runtime.GOARCH
|
||||
newUpdateURL := newUpdateURLPrefix + "/mc.shasum"
|
||||
|
||||
// Instantiate a new client with 3 sec timeout.
|
||||
client := &http.Client{
|
||||
Timeout: 3000 * time.Millisecond,
|
||||
}
|
||||
|
||||
// Get the downloadURL.
|
||||
var downloadURL string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// For windows and darwin.
|
||||
downloadURL = newUpdateURLPrefix + "/mc.exe"
|
||||
default:
|
||||
// For all other operating systems.
|
||||
downloadURL = newUpdateURLPrefix + "/mc"
|
||||
}
|
||||
|
||||
data, e := client.Get(newUpdateURL)
|
||||
fatalIf(probe.NewError(e), "Unable to read from update URL ‘"+newUpdateURL+"’.")
|
||||
|
||||
if strings.HasPrefix(mcVersion, "DEVELOPMENT.GOGET") {
|
||||
fatalIf(errDummy().Trace(newUpdateURL),
|
||||
"Update mechanism is not supported for ‘go get’ based binary builds. Please download official releases from https://minio.io/#minio")
|
||||
}
|
||||
|
||||
current, e := time.Parse(time.RFC3339, mcVersion)
|
||||
fatalIf(probe.NewError(e), "Unable to parse version string as time.")
|
||||
|
||||
if current.IsZero() {
|
||||
fatalIf(errDummy().Trace(newUpdateURL),
|
||||
"Updates not supported for custom builds. Version field is empty. Please download official releases from https://minio.io/#minio")
|
||||
}
|
||||
|
||||
body, e := ioutil.ReadAll(data.Body)
|
||||
fatalIf(probe.NewError(e), "Fetching updates failed. Please try again.")
|
||||
|
||||
latest, err := parseReleaseData(string(body))
|
||||
fatalIf(err.Trace(newUpdateURL), "Please report this issue at https://github.com/minio/mc/issues.")
|
||||
|
||||
if latest.IsZero() {
|
||||
fatalIf(errDummy().Trace(newUpdateURL),
|
||||
"Unable to validate any update available at this time. Please open an issue at https://github.com/minio/mc/issues")
|
||||
}
|
||||
|
||||
updateMsg := updateMessage{
|
||||
Download: downloadURL,
|
||||
Version: mcVersion,
|
||||
}
|
||||
if latest.After(current) {
|
||||
updateMsg.Update = true
|
||||
}
|
||||
printMsg(updateMsg)
|
||||
}
|
||||
|
||||
// main entry point for update command.
|
||||
func mainUpdate(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("Update", color.New(color.FgGreen, color.Bold))
|
||||
|
||||
// Check for update.
|
||||
if ctx.Bool("experimental") {
|
||||
getReleaseUpdate(mcUpdateExperimentalURL)
|
||||
} else {
|
||||
getReleaseUpdate(mcUpdateStableURL)
|
||||
}
|
||||
}
|
||||
42
command/utils.go
Normal file
42
command/utils.go
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
|
||||
// newRandomID generates a random id of regular lower case and uppercase english characters.
|
||||
func newRandomID(n int) string {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
sid := make([]rune, n)
|
||||
for i := range sid {
|
||||
sid[i] = letters[rand.Intn(len(letters))]
|
||||
}
|
||||
return string(sid)
|
||||
}
|
||||
|
||||
// isBucketVirtualStyle is host virtual bucket style?.
|
||||
func isBucketVirtualStyle(host string) bool {
|
||||
s3Virtual, _ := filepath.Match("*.s3*.amazonaws.com", host)
|
||||
googleVirtual, _ := filepath.Match("*.storage.googleapis.com", host)
|
||||
return s3Virtual || googleVirtual
|
||||
}
|
||||
98
command/version-main.go
Normal file
98
command/version-main.go
Normal file
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Minio Client (C) 2014, 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/minio/cli"
|
||||
"github.com/minio/mc/pkg/console"
|
||||
"github.com/minio/minio/pkg/probe"
|
||||
)
|
||||
|
||||
var (
|
||||
versionFlags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "help, h",
|
||||
Usage: "Help for version.",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Print version.
|
||||
var versionCmd = cli.Command{
|
||||
Name: "version",
|
||||
Usage: "Print version.",
|
||||
Action: mainVersion,
|
||||
Flags: append(versionFlags, globalFlags...),
|
||||
CustomHelpTemplate: `NAME:
|
||||
mc {{.Name}} - {{.Usage}}
|
||||
|
||||
USAGE:
|
||||
mc {{.Name}} [FLAGS]
|
||||
|
||||
FLAGS:
|
||||
{{range .Flags}}{{.}}
|
||||
{{end}}
|
||||
`,
|
||||
}
|
||||
|
||||
// Structured message depending on the type of console.
|
||||
type versionMessage struct {
|
||||
Status string `json:"status"`
|
||||
Version struct {
|
||||
Value string `json:"value"`
|
||||
Format string `json:"format"`
|
||||
} `json:"version"`
|
||||
ReleaseTag string `json:"releaseTag"`
|
||||
CommitID string `json:"commitID"`
|
||||
}
|
||||
|
||||
// Colorized message for console printing.
|
||||
func (v versionMessage) String() string {
|
||||
return console.Colorize("Version", fmt.Sprintf("Version: %s\n", v.Version.Value)) +
|
||||
console.Colorize("ReleaseTag", fmt.Sprintf("Release-tag: %s\n", v.ReleaseTag)) +
|
||||
console.Colorize("CommitID", fmt.Sprintf("Commit-id: %s", v.CommitID))
|
||||
}
|
||||
|
||||
// JSON'ified message for scripting.
|
||||
func (v versionMessage) JSON() string {
|
||||
v.Status = "success"
|
||||
msgBytes, e := json.Marshal(v)
|
||||
fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
|
||||
return string(msgBytes)
|
||||
}
|
||||
|
||||
func mainVersion(ctx *cli.Context) {
|
||||
// Set global flags from context.
|
||||
setGlobalsFromContext(ctx)
|
||||
|
||||
// Additional command speific theme customization.
|
||||
console.SetColor("Version", color.New(color.FgGreen, color.Bold))
|
||||
console.SetColor("ReleaseTag", color.New(color.FgGreen))
|
||||
console.SetColor("CommitID", color.New(color.FgGreen))
|
||||
|
||||
verMsg := versionMessage{}
|
||||
verMsg.CommitID = mcCommitID
|
||||
verMsg.ReleaseTag = mcReleaseTag
|
||||
verMsg.Version.Value = mcVersion
|
||||
verMsg.Version.Format = "RFC3339"
|
||||
|
||||
printMsg(verMsg)
|
||||
}
|
||||
29
command/version_test.go
Normal file
29
command/version_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Minio Client (C) 2015 Minio, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package command
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
. "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func (s *TestSuite) TestVersion(c *C) {
|
||||
_, e := time.Parse(mcVersion, http.TimeFormat)
|
||||
c.Assert(e, NotNil)
|
||||
}
|
||||
Reference in New Issue
Block a user