diff --git a/Makefile b/Makefile index fbd531bdc..45ef703a8 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ checkdeps: checkgopath: @echo "Checking if project is at ${GOPATH}" - @for miniofspath in $(echo ${GOPATH} | sed 's/:/\n/g'); do if [ ! -d ${mcpath}/src/github.com/minio/minio ]; then echo "Project not found in ${miniofspath}, please follow instructions provided at https://github.com/minio/minio/blob/master/CONTRIBUTING.md#setup-your-minio-github-repository" && exit 1; fi done + @for miniopath in $(echo ${GOPATH} | sed 's/:/\n/g'); do if [ ! -d ${miniopath}/src/github.com/minio/minio ]; then echo "Project not found in ${miniopath}, please follow instructions provided at https://github.com/minio/minio/blob/master/CONTRIBUTING.md#setup-your-minio-github-repository" && exit 1; fi done getdeps: checkdeps checkgopath @go get github.com/golang/lint/golint && echo "Installed golint:" @@ -69,3 +69,5 @@ clean: @echo "Cleaning up all the generated files:" @rm -fv cover.out @rm -fv minio + @rm -fv minio.test + @rm -fv pkg/fs/fs.test diff --git a/main.go b/main.go index 44e2d8096..e45874e37 100644 --- a/main.go +++ b/main.go @@ -91,6 +91,7 @@ func registerApp() *cli.App { // register all commands registerCommand(serverCmd) registerCommand(versionCmd) + registerCommand(updateCmd) // register all flags registerFlag(addressFlag) diff --git a/notifier.go b/notifier.go new file mode 100644 index 000000000..f6f829df5 --- /dev/null +++ b/notifier.go @@ -0,0 +1,86 @@ +/* + * Minio Cloud Storage, (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 main + +import ( + "fmt" + "math" + "runtime" + "strings" + + "github.com/fatih/color" + "github.com/minio/minio-xl/pkg/probe" + "github.com/olekukonko/ts" +) + +// 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(" Update available: ") + line2Str := fmt.Sprintf(" Run \"%s\" to update. ", updateString) + line1Length := len(line1Str) + line2Length := len(line2Str) + + // populate lines with color coding + line1InColor := line1Str + line2InColor := fmt.Sprintf(" Run \"%s\" to update. ", cyan(updateString)) + + // calculate the rectangular box size + maxContentWidth := int(math.Max(float64(line1Length), float64(line2Length))) + line1Rest := maxContentWidth - line1Length + line2Rest := maxContentWidth - line2Length + + terminal, err := ts.GetSize() + if err != nil { + return "", probe.NewError(err) + } + + var message string + switch { + case len(line2Str) > terminal.Col(): + 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" + } + // finally print the message + return message, nil +} diff --git a/typed-errors.go b/typed-errors.go index 0d86c55df..86ef26b8c 100644 --- a/typed-errors.go +++ b/typed-errors.go @@ -18,6 +18,9 @@ package main import "errors" +// errInvalidArgument means that input argument is invalid +var errInvalidArgument = errors.New("Invalid Argument") + // errMissingAuthHeader means that Authorization header // has missing value or it is empty. var errMissingAuthHeaderValue = errors.New("Missing auth header value") diff --git a/update-main.go b/update-main.go new file mode 100644 index 000000000..b9309b4cd --- /dev/null +++ b/update-main.go @@ -0,0 +1,203 @@ +/* + * Minio Cloud Storage, (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 main + +import ( + "encoding/json" + "errors" + "net/http" + "net/url" + "runtime" + "strings" + "time" + + "github.com/fatih/color" + "github.com/minio/cli" + "github.com/minio/minio-xl/pkg/probe" +) + +// Check for new software updates. +var updateCmd = cli.Command{ + Name: "update", + Usage: "Check for new software updates.", + Action: mainUpdate, + CustomHelpTemplate: `Name: + minio {{.Name}} - {{.Usage}} + +USAGE: + minio {{.Name}} release + minio {{.Name}} experimental + +EXAMPLES: + 1. Check for new official releases + $ minio {{.Name}} release + + 2. Check for new experimental releases + $ minio {{.Name}} experimental +`, +} + +// updates container to hold updates json +type updates struct { + BuildDate string + Platforms map[string]string +} + +// updateMessage container to hold update messages +type updateMessage struct { + Update bool `json:"update"` + Download string `json:"downloadURL"` + Version string `json:"version"` +} + +// String colorized update message +func (u updateMessage) String() string { + if u.Update { + 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+"’.", nil) + return msg + } + updateMessage := color.New(color.FgGreen, color.Bold).SprintfFunc() + return updateMessage("You are already running the most recent version of ‘minio’.") +} + +// JSON jsonified update message +func (u updateMessage) JSON() string { + updateMessageJSONBytes, err := json.Marshal(u) + fatalIf(probe.NewError(err), "Unable to marshal into JSON.", nil) + + return string(updateMessageJSONBytes) +} + +func getExperimentalUpdate() { + current, e := time.Parse(http.TimeFormat, minioVersion) + fatalIf(probe.NewError(e), "Unable to parse Version string as time.", nil) + + if current.IsZero() { + fatalIf(probe.NewError(errors.New("")), "Experimental updates are not supported for custom build. Version field is empty. Please download official releases from https://dl.minio.io:9000", nil) + } + + resp, err := http.Get(minioExperimentalURL) + fatalIf(probe.NewError(err), "Unable to initalize experimental URL.", nil) + + var experimentals updates + decoder := json.NewDecoder(resp.Body) + defer resp.Body.Close() + e = decoder.Decode(&experimentals) + fatalIf(probe.NewError(e), "Unable to decode experimental update notification.", nil) + + latest, e := time.Parse(http.TimeFormat, experimentals.BuildDate) + fatalIf(probe.NewError(e), "Unable to parse BuildDate.", nil) + + if latest.IsZero() { + fatalIf(probe.NewError(errors.New("")), "Unable to validate any experimental update available at this time. Please open an issue at https://github.com/minio/minio/issues", nil) + } + + minioExperimentalURLParse, err := url.Parse(minioExperimentalURL) + if err != nil { + fatalIf(probe.NewError(err), "Unable to parse URL: "+minioExperimentalURL, nil) + } + downloadURL := minioExperimentalURLParse.Scheme + "://" + minioExperimentalURLParse.Host + "/" + experimentals.Platforms[runtime.GOOS] + updateMessage := updateMessage{ + Download: downloadURL, + Version: minioVersion, + } + if latest.After(current) { + updateMessage.Update = true + } + if globalJSONFlag { + Println(updateMessage.JSON()) + } else { + Println(updateMessage) + } +} + +func getReleaseUpdate() { + current, e := time.Parse(http.TimeFormat, minioVersion) + fatalIf(probe.NewError(e), "Unable to parse Version string as time.", nil) + + if current.IsZero() { + fatalIf(probe.NewError(errors.New("")), "Updates not supported for custom build. Version field is empty. Please download official releases from https://dl.minio.io:9000", nil) + } + + resp, err := http.Get(minioUpdateURL) + fatalIf(probe.NewError(err), "Unable to initalize experimental URL.", nil) + + var releases updates + decoder := json.NewDecoder(resp.Body) + e = decoder.Decode(&releases) + fatalIf(probe.NewError(e), "Unable to decode update notification.", nil) + + latest, e := time.Parse(http.TimeFormat, releases.BuildDate) + fatalIf(probe.NewError(e), "Unable to parse BuildDate.", nil) + + if latest.IsZero() { + fatalIf(probe.NewError(errors.New("")), "Unable to validate any update available at this time. Please open an issue at https://github.com/minio/minio/issues", nil) + } + + minioUpdateURLParse, err := url.Parse(minioUpdateURL) + if err != nil { + fatalIf(probe.NewError(err), "Unable to parse URL: "+minioUpdateURL, nil) + } + downloadURL := minioUpdateURLParse.Scheme + "://" + minioUpdateURLParse.Host + "/" + releases.Platforms[runtime.GOOS] + updateMessage := updateMessage{ + Download: downloadURL, + Version: minioVersion, + } + if latest.After(current) { + updateMessage.Update = true + } + + if globalJSONFlag { + Println(updateMessage.JSON()) + } else { + Println(updateMessage) + } +} + +const ( + minioUpdateURL = "https://dl.minio.io:9000/updates/minio/updates.json" + minioExperimentalURL = "https://dl.minio.io:9000/updates/minio/experimental.json" +) + +func checkUpdateSyntax(ctx *cli.Context) { + if ctx.Args().First() == "help" || !ctx.Args().Present() { + cli.ShowCommandHelpAndExit(ctx, "update", 1) // last argument is exit code + } + arg := strings.TrimSpace(ctx.Args().First()) + if arg != "release" && arg != "experimental" { + fatalIf(probe.NewError(errInvalidArgument), "Unrecognized argument provided.", nil) + } +} + +// mainUpdate - +func mainUpdate(ctx *cli.Context) { + checkUpdateSyntax(ctx) + arg := strings.TrimSpace(ctx.Args().First()) + switch arg { + case "release": + getReleaseUpdate() + case "experimental": + getExperimentalUpdate() + } +} diff --git a/verify-runtime.go b/verify-runtime.go index b611a06c8..c64d99189 100644 --- a/verify-runtime.go +++ b/verify-runtime.go @@ -90,6 +90,6 @@ func checkGolangRuntimeVersion() { v1 := newVersion(getNormalizedGolangVersion()) v2 := newVersion(minGolangRuntimeVersion) if v1.LessThan(v2) { - Fatalln("Old Golang runtime version ‘" + v1.String() + "’ detected., ‘mc’ requires minimum go1.5.1 or later.") + Fatalln("Old Golang runtime version ‘" + v1.String() + "’ detected., ‘minio’ requires minimum go1.5.1 or later.") } } diff --git a/version-main.go b/version-main.go index d3866e9e2..febe09c76 100644 --- a/version-main.go +++ b/version-main.go @@ -23,13 +23,10 @@ var versionCmd = cli.Command{ Usage: "Print version", Action: mainVersion, CustomHelpTemplate: `NAME: - mc {{.Name}} - {{.Usage}} + minio {{.Name}} - {{.Usage}} USAGE: - mc {{.Name}} {{if .Description}} - -EXAMPLES: - + minio {{.Name}} {{if .Description}} `, }