1
0
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:
Remco Verhoef
2016-08-12 00:29:04 +02:00
committed by Harshavardhana
parent c9ecd023fa
commit b51fb32054
75 changed files with 350 additions and 318 deletions

37
command/access-perms.go Normal file
View 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")
)

View 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
}

View 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)
}
}

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

344
command/client-fs_test.go Normal file
View 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)
}

View 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
}

View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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())
}

View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}

View 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
View 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
View 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.
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}