diff --git a/cmd/humanized-duration.go b/cmd/humanized-duration.go new file mode 100644 index 000000000..6abd764f9 --- /dev/null +++ b/cmd/humanized-duration.go @@ -0,0 +1,88 @@ +/* + * Minio Cloud Storage, (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 cmd + +import ( + "fmt" + "math" + "time" +) + +// humanizedDuration container to capture humanized time. +type humanizedDuration struct { + Days int64 `json:"days,omitempty"` + Hours int64 `json:"hours,omitempty"` + Minutes int64 `json:"minutes,omitempty"` + Seconds int64 `json:"seconds,omitempty"` +} + +// StringShort() humanizes humanizedDuration to human readable short format. +// This does not print at seconds. +func (r humanizedDuration) StringShort() string { + if r.Days == 0 && r.Hours == 0 { + return fmt.Sprintf("%d minutes", r.Minutes) + } + if r.Days == 0 { + return fmt.Sprintf("%d hours %d minutes", r.Hours, r.Minutes) + } + return fmt.Sprintf("%d days %d hours %d minutes", r.Days, r.Hours, r.Minutes) +} + +// String() humanizes humanizedDuration to human readable, +func (r humanizedDuration) 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) +} + +// timeDurationToHumanizedDuration convert golang time.Duration to a custom more readable humanizedDuration. +func timeDurationToHumanizedDuration(duration time.Duration) humanizedDuration { + r := humanizedDuration{} + 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 +} diff --git a/cmd/humanized-duration_test.go b/cmd/humanized-duration_test.go new file mode 100644 index 000000000..9f51b1296 --- /dev/null +++ b/cmd/humanized-duration_test.go @@ -0,0 +1,76 @@ +/* + * Minio Cloud Storage, (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 cmd + +import ( + "strings" + "testing" + "time" +) + +// Test humanized duration. +func TestHumanizedDuration(t *testing.T) { + duration := time.Duration(90487000000000) + humanDuration := timeDurationToHumanizedDuration(duration) + if !strings.HasSuffix(humanDuration.String(), "seconds") { + t.Fatal("Stringer method for humanized duration should have seconds.", humanDuration.String()) + } + if strings.HasSuffix(humanDuration.StringShort(), "seconds") { + t.Fatal("StringShorter method for humanized duration should not have seconds.", humanDuration.StringShort()) + } + + // Test humanized duration for seconds. + humanSecDuration := timeDurationToHumanizedDuration(time.Duration(5 * time.Second)) + expectedHumanSecDuration := humanizedDuration{ + Seconds: 5, + } + if humanSecDuration != expectedHumanSecDuration { + t.Fatalf("Expected %#v, got %#v incorrect conversion of duration to humanized form", + expectedHumanSecDuration, humanSecDuration) + } + if strings.HasSuffix(humanSecDuration.String(), "days") || + strings.HasSuffix(humanSecDuration.String(), "hours") || + strings.HasSuffix(humanSecDuration.String(), "minutes") { + t.Fatal("Stringer method for humanized duration should have only seconds.", humanSecDuration.String()) + } + + // Test humanized duration for minutes. + humanMinDuration := timeDurationToHumanizedDuration(10 * time.Minute) + expectedHumanMinDuration := humanizedDuration{ + Minutes: 10, + } + if humanMinDuration != expectedHumanMinDuration { + t.Fatalf("Expected %#v, got %#v incorrect conversion of duration to humanized form", + expectedHumanMinDuration, humanMinDuration) + } + if strings.HasSuffix(humanMinDuration.String(), "hours") { + t.Fatal("Stringer method for humanized duration should have only minutes.", humanMinDuration.String()) + } + + // Test humanized duration for hours. + humanHourDuration := timeDurationToHumanizedDuration(10 * time.Hour) + expectedHumanHourDuration := humanizedDuration{ + Hours: 10, + } + if humanHourDuration != expectedHumanHourDuration { + t.Fatalf("Expected %#v, got %#v incorrect conversion of duration to humanized form", + expectedHumanHourDuration, humanHourDuration) + } + if strings.HasSuffix(humanHourDuration.String(), "days") { + t.Fatal("Stringer method for humanized duration should have hours.", humanHourDuration.String()) + } +} diff --git a/cmd/main.go b/cmd/main.go index b95a77e91..d76e084f3 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -21,7 +21,6 @@ import ( "fmt" "os" "sort" - "strings" "time" "github.com/minio/cli" @@ -201,7 +200,7 @@ func Main() { // Do not print update messages, if quiet flag is set. if !globalQuiet { - if strings.HasPrefix(ReleaseTag, "RELEASE.") && c.Args().Get(0) != "update" { + if c.Args().Get(0) != "update" { updateMsg, _, err := getReleaseUpdate(minioUpdateStableURL, 1*time.Second) if err != nil { // Ignore any errors during getReleaseUpdate(), possibly diff --git a/cmd/update-main.go b/cmd/update-main.go index fbf4c317c..f88d24edd 100644 --- a/cmd/update-main.go +++ b/cmd/update-main.go @@ -18,7 +18,6 @@ package cmd import ( "bytes" - "encoding/json" "errors" "io/ioutil" "net/http" @@ -74,10 +73,9 @@ const ( // 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"` + Update bool `json:"update"` + Download string `json:"downloadURL"` + NewerThan time.Duration `json:"newerThan"` } // String colorized update message. @@ -86,19 +84,10 @@ func (u updateMessage) String() string { updateMessage := color.New(color.FgGreen, color.Bold).SprintfFunc() return updateMessage("You are already running the most recent version of ‘minio’.") } - msg := colorizeUpdateMessage(u.Download) + msg := colorizeUpdateMessage(u.Download, u.NewerThan) return msg } -// JSON jsonified update message. -func (u updateMessage) JSON() string { - u.Status = "success" - updateMessageJSONBytes, err := json.Marshal(u) - fatalIf((err), "Unable to marshal into JSON.") - - return string(updateMessageJSONBytes) -} - func parseReleaseData(data string) (time.Time, error) { releaseStr := strings.Fields(data) if len(releaseStr) < 2 { @@ -149,6 +138,34 @@ func isDocker() bool { return bytes.Contains(cgroup, []byte("docker")) } +// Check if the minio server binary was built with source. +func isSourceBuild() bool { + return Version == "DEVELOPMENT.GOGET" +} + +// Fetch the current version of the Minio server binary. +func getCurrentMinioVersion() (current time.Time, err error) { + // For development builds we check for binary modTime + // to validate against latest minio server release. + if Version != "DEVELOPMENT.GOGET" { + // Parse current minio version into RFC3339. + current, err = time.Parse(time.RFC3339, Version) + if err != nil { + return time.Time{}, err + } + return current, nil + } // else { + // For all development builds through `go get`. + // fall back to looking for version of the build + // date of the binary itself. + var fi os.FileInfo + fi, err = os.Stat(os.Args[0]) + if err != nil { + return time.Time{}, err + } + return fi.ModTime(), nil +} + // verify updates for releases. func getReleaseUpdate(updateURL string, duration time.Duration) (updateMsg updateMessage, errMsg string, err error) { // Construct a new update url. @@ -157,19 +174,22 @@ func getReleaseUpdate(updateURL string, duration time.Duration) (updateMsg updat // Get the downloadURL. var downloadURL string - switch runtime.GOOS { - case "windows": - // For windows. - downloadURL = newUpdateURLPrefix + "/minio.exe" - default: - // For all other operating systems. - downloadURL = newUpdateURLPrefix + "/minio" + if isDocker() { + downloadURL = "docker pull minio/minio" + } else { + switch runtime.GOOS { + case "windows": + // For windows. + downloadURL = newUpdateURLPrefix + "/minio.exe" + default: + // For all other operating systems. + downloadURL = newUpdateURLPrefix + "/minio" + } } // Initialize update message. updateMsg = updateMessage{ Download: downloadURL, - Version: Version, } // Instantiate a new client with 3 sec timeout. @@ -177,17 +197,9 @@ func getReleaseUpdate(updateURL string, duration time.Duration) (updateMsg updat Timeout: duration, } - // Parse current minio version into RFC3339. - current, err := time.Parse(time.RFC3339, Version) + current, err := getCurrentMinioVersion() if err != nil { - errMsg = "Unable to parse version string as time." - return - } - - // Verify if current minio version is zero. - if current.IsZero() { - err = errors.New("date should not be zero") - errMsg = "Updates mechanism is not supported for custom builds. Please download official releases from https://minio.io/#minio" + errMsg = "Unable to fetch the current version of Minio server." return } @@ -198,10 +210,22 @@ func getReleaseUpdate(updateURL string, duration time.Duration) (updateMsg updat } userAgentPrefix := func() string { + prefix := "Minio (" + runtime.GOOS + "; " + runtime.GOARCH + // if its a source build. + if isSourceBuild() { + if isDocker() { + prefix = prefix + "; " + "docker; source) " + } else { + prefix = prefix + "; " + "source) " + } + return prefix + } // else { not source. if isDocker() { - return "Minio (" + runtime.GOOS + "; " + runtime.GOARCH + "; " + "docker) " + prefix = prefix + "; " + "docker) " + } else { + prefix = prefix + ") " } - return "Minio (" + runtime.GOOS + "; " + runtime.GOARCH + ") " + return prefix }() // Set user agent. @@ -239,13 +263,14 @@ func getReleaseUpdate(updateURL string, duration time.Duration) (updateMsg updat // Verify if the date is not zero. if latest.IsZero() { - err = errors.New("date should not be zero") + err = errors.New("Release date cannot be zero. Please report this issue at https://github.com/minio/minio/issues") return } // Is the update latest?. if latest.After(current) { updateMsg.Update = true + updateMsg.NewerThan = latest.Sub(current) } // Return update message. @@ -254,11 +279,6 @@ func getReleaseUpdate(updateURL string, duration time.Duration) (updateMsg updat // main entry point for update command. func mainUpdate(ctx *cli.Context) { - // Error out if 'update' command is issued for development based builds. - if Version == "DEVELOPMENT.GOGET" { - fatalIf(errors.New(""), "Update mechanism is not supported for ‘go get’ based binary builds. Please download official releases from https://minio.io/#minio") - } - // Check for update. var updateMsg updateMessage var errMsg string diff --git a/cmd/update-main_nix_test.go b/cmd/update-main_nix_test.go index 0f4a92726..11e1a0b77 100644 --- a/cmd/update-main_nix_test.go +++ b/cmd/update-main_nix_test.go @@ -47,9 +47,9 @@ func TestReleaseUpdateVersion(t *testing.T) { { updateURL: ts.URL, updateMsg: updateMessage{ - Download: ts.URL + "/" + runtime.GOOS + "-" + runtime.GOARCH + "/minio", - Version: "2016-10-06T00:08:32Z", - Update: true, + Download: ts.URL + "/" + runtime.GOOS + "-" + runtime.GOARCH + "/minio", + Update: true, + NewerThan: 90487000000000, }, errMsg: "", shouldPass: true, @@ -87,9 +87,8 @@ func TestReleaseUpdate(t *testing.T) { updateURL: ts.URL, updateMsg: updateMessage{ Download: ts.URL + "/" + runtime.GOOS + "-" + runtime.GOARCH + "/minio", - Version: "DEVELOPMENT.GOGET", }, - errMsg: "Unable to parse version string as time.", + errMsg: "Failed to retrieve update notice. Please try again later. Please report this issue at https://github.com/minio/minio/issues", shouldPass: false, }, } diff --git a/cmd/update-main_windows_test.go b/cmd/update-main_windows_test.go index 7f271be72..9900be556 100644 --- a/cmd/update-main_windows_test.go +++ b/cmd/update-main_windows_test.go @@ -47,9 +47,9 @@ func TestReleaseUpdateVersion(t *testing.T) { { updateURL: ts.URL, updateMsg: updateMessage{ - Download: ts.URL + "/" + runtime.GOOS + "-" + runtime.GOARCH + "/minio.exe", - Version: "2016-10-06T00:08:32Z", - Update: true, + Download: ts.URL + "/" + runtime.GOOS + "-" + runtime.GOARCH + "/minio.exe", + Update: true, + NewerThan: 90487000000000, }, errMsg: "", shouldPass: true, @@ -87,9 +87,8 @@ func TestReleaseUpdate(t *testing.T) { updateURL: ts.URL, updateMsg: updateMessage{ Download: ts.URL + "/" + runtime.GOOS + "-" + runtime.GOARCH + "/minio.exe", - Version: "DEVELOPMENT.GOGET", }, - errMsg: "Unable to parse version string as time.", + errMsg: "Failed to retrieve update notice. Please try again later. Please report this issue at https://github.com/minio/minio/issues", shouldPass: false, }, } diff --git a/cmd/update-notifier.go b/cmd/update-notifier.go index 87f4d988c..c54a2cfc7 100644 --- a/cmd/update-notifier.go +++ b/cmd/update-notifier.go @@ -18,15 +18,17 @@ package cmd import ( "fmt" + "math" "runtime" "strings" + "time" "github.com/cheggaaa/pb" "github.com/fatih/color" ) // colorizeUpdateMessage - inspired from Yeoman project npm package https://github.com/yeoman/update-notifier -func colorizeUpdateMessage(updateString string) string { +func colorizeUpdateMessage(updateString string, newerThan time.Duration) string { // Initialize coloring. cyan := color.New(color.FgCyan, color.Bold).SprintFunc() yellow := color.New(color.FgYellow, color.Bold).SprintfFunc() @@ -34,15 +36,20 @@ func colorizeUpdateMessage(updateString string) string { // 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: %s ", updateString) + hTime := timeDurationToHumanizedDuration(newerThan) + line1Str := fmt.Sprintf(" Minio is %s old ", hTime.StringShort()) + line2Str := fmt.Sprintf(" Update: %s ", updateString) line1Length := len(line1Str) + line2Length := len(line2Str) // Populate lines with color coding. - line1InColor := fmt.Sprintf(" New update: %s ", cyan(updateString)) + line1InColor := fmt.Sprintf(" Minio is %s old ", yellow(hTime.StringShort())) + line2InColor := fmt.Sprintf(" Update: %s ", cyan(updateString)) - // Calculate the rectangular box size. - maxContentWidth := line1Length + // calculate the rectangular box size. + maxContentWidth := int(math.Max(float64(line1Length), float64(line2Length))) line1Rest := maxContentWidth - line1Length + line2Rest := maxContentWidth - line2Length // termWidth is set to a default one to use when we are // not able to calculate terminal width via OS syscalls @@ -54,27 +61,29 @@ func colorizeUpdateMessage(updateString string) string { var message string switch { - case len(line1Str) > termWidth: - message = "\n" + line1InColor + "\n" + case len(line2Str) > termWidth: + message = "\n" + line1InColor + "\n" + line2InColor + "\n" default: - // On windows terminal turn off unicode characters. + // 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. + // 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. + // fill spaces to the rest of the area. spacePaddingLine1 := strings.Repeat(" ", line1Rest) + spacePaddingLine2 := strings.Repeat(" ", line2Rest) - // Construct the final message. + // construct the final message. message = "\n" + top + "\n" + sideBar + line1InColor + spacePaddingLine1 + sideBar + "\n" + + sideBar + line2InColor + spacePaddingLine2 + sideBar + "\n" + bottom + "\n" } // Return the final message. diff --git a/cmd/update-notifier_test.go b/cmd/update-notifier_test.go new file mode 100644 index 000000000..b6c83e567 --- /dev/null +++ b/cmd/update-notifier_test.go @@ -0,0 +1,35 @@ +/* + * 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 cmd + +import ( + "strings" + "testing" + "time" +) + +// Tests update notifier string builder. +func TestUpdateNotifier(t *testing.T) { + updateMsg := minioUpdateStableURL + colorUpdateMsg := colorizeUpdateMessage(updateMsg, time.Duration(72*time.Hour)) + if strings.Index(colorUpdateMsg, "minutes") == -1 { + t.Fatal("Duration string not found in colorized update message", colorUpdateMsg) + } + if strings.Index(colorUpdateMsg, updateMsg) == -1 { + t.Fatal("Update message not found in colorized update message", updateMsg) + } +}