Commit 16b53456 authored by Juan Batiz-Benet's avatar Juan Batiz-Benet

Merge branch 'dborzov-update-warning'

parents 555bc871 afec2d3d
# ipfs can generate profiling dump files
cpu.prof
*.swp
.ipfsconfig
*.out
......
......@@ -46,6 +46,10 @@
"ImportPath": "github.com/camlistore/lock",
"Rev": "ae27720f340952636b826119b58130b9c1a847a0"
},
{
"ImportPath": "github.com/coreos/go-semver/semver",
"Rev": "6fe83ccda8fb9b7549c9ab4ba47f47858bc950aa"
},
{
"ImportPath": "github.com/gonuts/flag",
"Rev": "741a6cbd37a30dedc93f817e7de6aaf0ca38a493"
......
package semver
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
)
type Version struct {
Major int64
Minor int64
Patch int64
PreRelease PreRelease
Metadata string
}
type PreRelease string
func splitOff(input *string, delim string) (val string) {
parts := strings.SplitN(*input, delim, 2)
if len(parts) == 2 {
*input = parts[0]
val = parts[1]
}
return val
}
func NewVersion(version string) (*Version, error) {
v := Version{}
dotParts := strings.SplitN(version, ".", 3)
if len(dotParts) != 3 {
return nil, errors.New(fmt.Sprintf("%s is not in dotted-tri format", version))
}
v.Metadata = splitOff(&dotParts[2], "+")
v.PreRelease = PreRelease(splitOff(&dotParts[2], "-"))
parsed := make([]int64, 3, 3)
for i, v := range dotParts[:3] {
val, err := strconv.ParseInt(v, 10, 64)
parsed[i] = val
if err != nil {
return nil, err
}
}
v.Major = parsed[0]
v.Minor = parsed[1]
v.Patch = parsed[2]
return &v, nil
}
func (v *Version) String() string {
var buffer bytes.Buffer
base := fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
buffer.WriteString(base)
if v.PreRelease != "" {
buffer.WriteString(fmt.Sprintf("-%s", v.PreRelease))
}
if v.Metadata != "" {
buffer.WriteString(fmt.Sprintf("+%s", v.Metadata))
}
return buffer.String()
}
func (v *Version) LessThan(versionB Version) bool {
versionA := *v
cmp := recursiveCompare(versionA.Slice(), versionB.Slice())
if cmp == 0 {
cmp = preReleaseCompare(versionA, versionB)
}
if cmp == -1 {
return true
}
return false
}
/* Slice converts the comparable parts of the semver into a slice of strings */
func (v *Version) Slice() []int64 {
return []int64{v.Major, v.Minor, v.Patch}
}
func (p *PreRelease) Slice() []string {
preRelease := string(*p)
return strings.Split(preRelease, ".")
}
func preReleaseCompare(versionA Version, versionB Version) int {
a := versionA.PreRelease
b := versionB.PreRelease
/* Handle the case where if two versions are otherwise equal it is the
* one without a PreRelease that is greater */
if len(a) == 0 && (len(b) > 0) {
return 1
} else if len(b) == 0 && (len(a) > 0) {
return -1
}
// If there is a prelease, check and compare each part.
return recursivePreReleaseCompare(a.Slice(), b.Slice())
}
func recursiveCompare(versionA []int64, versionB []int64) int {
if len(versionA) == 0 {
return 0
}
a := versionA[0]
b := versionB[0]
if a > b {
return 1
} else if a < b {
return -1
}
return recursiveCompare(versionA[1:], versionB[1:])
}
func recursivePreReleaseCompare(versionA []string, versionB []string) int {
// Handle slice length disparity.
if len(versionA) == 0 {
// Nothing to compare too, so we return 0
return 0
} else if len(versionB) == 0 {
// We're longer than versionB so return 1.
return 1
}
a := versionA[0]
b := versionB[0]
aInt := false; bInt := false
aI, err := strconv.Atoi(versionA[0])
if err == nil {
aInt = true
}
bI, err := strconv.Atoi(versionB[0])
if err == nil {
bInt = true
}
// Handle Integer Comparison
if aInt && bInt {
if aI > bI {
return 1
} else if aI < bI {
return -1
}
}
// Handle String Comparison
if a > b {
return 1
} else if a < b {
return -1
}
return recursivePreReleaseCompare(versionA[1:], versionB[1:])
}
// BumpMajor increments the Major field by 1 and resets all other fields to their default values
func (v *Version) BumpMajor() {
v.Major += 1
v.Minor = 0
v.Patch = 0
v.PreRelease = PreRelease("")
v.Metadata = ""
}
// BumpMinor increments the Minor field by 1 and resets all other fields to their default values
func (v *Version) BumpMinor() {
v.Minor += 1
v.Patch = 0
v.PreRelease = PreRelease("")
v.Metadata = ""
}
// BumpPatch increments the Patch field by 1 and resets all other fields to their default values
func (v *Version) BumpPatch() {
v.Patch += 1
v.PreRelease = PreRelease("")
v.Metadata = ""
}
package semver
import (
"math/rand"
"testing"
"time"
)
type fixture struct {
greaterVersion string
lesserVersion string
}
var fixtures = []fixture{
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"0.0.0", "0.0.0-foo"},
fixture{"0.0.1", "0.0.0"},
fixture{"1.0.0", "0.9.9"},
fixture{"0.10.0", "0.9.0"},
fixture{"0.99.0", "0.10.0"},
fixture{"2.0.0", "1.2.3"},
fixture{"1.2.3", "1.2.3-asdf"},
fixture{"1.2.3", "1.2.3-4"},
fixture{"1.2.3", "1.2.3-4-foo"},
fixture{"1.2.3-5-foo", "1.2.3-5"},
fixture{"1.2.3-5", "1.2.3-4"},
fixture{"1.2.3-5-foo", "1.2.3-5-Foo"},
fixture{"3.0.0", "2.7.2+asdf"},
fixture{"3.0.0+foobar", "2.7.2"},
fixture{"1.2.3-a.10", "1.2.3-a.5"},
fixture{"1.2.3-a.b", "1.2.3-a.5"},
fixture{"1.2.3-a.b", "1.2.3-a"},
fixture{"1.2.3-a.b.c.10.d.5", "1.2.3-a.b.c.5.d.100"},
fixture{"1.0.0", "1.0.0-rc.1"},
fixture{"1.0.0-rc.2", "1.0.0-rc.1"},
fixture{"1.0.0-rc.1", "1.0.0-beta.11"},
fixture{"1.0.0-beta.11", "1.0.0-beta.2"},
fixture{"1.0.0-beta.2", "1.0.0-beta"},
fixture{"1.0.0-beta", "1.0.0-alpha.beta"},
fixture{"1.0.0-alpha.beta", "1.0.0-alpha.1"},
fixture{"1.0.0-alpha.1", "1.0.0-alpha"},
}
func TestCompare(t *testing.T) {
for _, v := range fixtures {
gt, err := NewVersion(v.greaterVersion)
if err != nil {
t.Error(err)
}
lt, err := NewVersion(v.lesserVersion)
if err != nil {
t.Error(err)
}
if gt.LessThan(*lt) == true {
t.Errorf("%s should not be less than %s", gt, lt)
}
}
}
func testString(t *testing.T, orig string, version *Version) {
if orig != version.String() {
t.Errorf("%s != %s", orig, version)
}
}
func TestString(t *testing.T) {
for _, v := range fixtures {
gt, err := NewVersion(v.greaterVersion)
if err != nil {
t.Error(err)
}
testString(t, v.greaterVersion, gt)
lt, err := NewVersion(v.lesserVersion)
if err != nil {
t.Error(err)
}
testString(t, v.lesserVersion, lt)
}
}
func shuffleStringSlice(src []string) []string {
dest := make([]string, len(src))
rand.Seed(time.Now().Unix())
perm := rand.Perm(len(src))
for i, v := range perm {
dest[v] = src[i]
}
return dest
}
func TestSort(t *testing.T) {
sortedVersions := []string{"1.0.0", "1.0.2", "1.2.0", "3.1.1"}
unsortedVersions := shuffleStringSlice(sortedVersions)
semvers := []*Version{}
for _, v := range unsortedVersions {
sv, err := NewVersion(v)
if err != nil {
t.Fatal(err)
}
semvers = append(semvers, sv)
}
Sort(semvers)
for idx, sv := range semvers {
if sv.String() != sortedVersions[idx] {
t.Fatalf("incorrect sort at index %v", idx)
}
}
}
func TestBumpMajor(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpMajor()
if version.Major != 2 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.5.2")
version.BumpMajor()
if version.Minor != 0 && version.Patch != 0 {
t.Fatalf("bumping major on 1.5.2 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpMajor()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}
func TestBumpMinor(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpMinor()
if version.Major != 1 {
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
}
if version.Minor != 1 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpMinor()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}
func TestBumpPatch(t *testing.T) {
version, _ := NewVersion("1.0.0")
version.BumpPatch()
if version.Major != 1 {
t.Fatalf("bumping minor on 1.0.0 resulted in %v", version)
}
if version.Minor != 0 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
if version.Patch != 1 {
t.Fatalf("bumping major on 1.0.0 resulted in %v", version)
}
version, _ = NewVersion("1.0.0+build.1-alpha.1")
version.BumpPatch()
if version.PreRelease != "" && version.PreRelease != "" {
t.Fatalf("bumping major on 1.0.0+build.1-alpha.1 resulted in %v", version)
}
}
package semver
import (
"sort"
)
type Versions []*Version
func (s Versions) Len() int {
return len(s)
}
func (s Versions) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s Versions) Less(i, j int) bool {
return s[i].LessThan(*s[j])
}
// Sort sorts the given slice of Version
func Sort(versions []*Version) {
sort.Sort(Versions(versions))
}
......@@ -11,6 +11,7 @@ import (
config "github.com/jbenet/go-ipfs/config"
ci "github.com/jbenet/go-ipfs/crypto"
spipe "github.com/jbenet/go-ipfs/crypto/spipe"
updates "github.com/jbenet/go-ipfs/updates"
u "github.com/jbenet/go-ipfs/util"
)
......@@ -135,6 +136,12 @@ func initCmd(c *commander.Command, inp []string) error {
},
}
// tracking ipfs version used to generate the init folder and adding update checker default setting.
cfg.Version = config.Version{
Check: "error",
Current: updates.Version,
}
err = config.WriteConfigFile(filename, cfg)
if err != nil {
return err
......
......@@ -13,6 +13,7 @@ import (
config "github.com/jbenet/go-ipfs/config"
core "github.com/jbenet/go-ipfs/core"
daemon "github.com/jbenet/go-ipfs/daemon"
updates "github.com/jbenet/go-ipfs/updates"
u "github.com/jbenet/go-ipfs/util"
)
......@@ -24,6 +25,7 @@ var CmdIpfs = &commander.Command{
Basic commands:
init Initialize ipfs local configuration.
add <path> Add an object to ipfs.
cat <ref> Show ipfs object data.
ls <ref> List links from an object.
......@@ -67,7 +69,8 @@ var log = u.Logger("cmd/ipfs")
func init() {
config, err := config.PathRoot()
if err != nil {
config = ""
u.POut("Failure initializing the default Config Directory: ", err)
os.Exit(1)
}
CmdIpfs.Flag.String("c", config, "specify config directory")
}
......@@ -117,6 +120,19 @@ func localNode(confdir string, online bool) (*core.IpfsNode, error) {
return nil, err
}
if cfg.Version.EligibleForUpdateCheck() {
obsolete := updates.CheckForUpdates()
if obsolete != nil {
if cfg.Version.Check == config.CheckError {
return nil, obsolete
}
log.Warning(fmt.Sprintf("%v", obsolete)) // when "warn" version.check mode we just show warning message
} else {
// update most recent check timestamp in config
cfg.RecordCurrentUpdateCheck(filename)
}
}
return core.NewIpfsNode(cfg, online)
}
......
......@@ -2,12 +2,10 @@ package main
import (
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/commander"
updates "github.com/jbenet/go-ipfs/updates"
u "github.com/jbenet/go-ipfs/util"
)
// The IPFS version.
const Version = "0.1.0"
var cmdIpfsVersion = &commander.Command{
UsageLine: "version",
Short: "Show ipfs version information.",
......@@ -27,6 +25,6 @@ func versionCmd(c *commander.Command, _ []string) error {
if !number {
u.POut("ipfs version ")
}
u.POut("%s\n", Version)
u.POut("%s\n", updates.Version)
return nil
}
......@@ -51,6 +51,7 @@ type Config struct {
Datastore Datastore // local node's storage
Addresses Addresses // local node's addresses
Mounts Mounts // local node's mount points
Version Version // local node's version management
Bootstrap []*BootstrapPeer // local nodes's bootstrap peers
}
......
package config
import "time"
// Version regulates checking if the most recent version is run
type Version struct {
Check string // "ignore" for do not check, "warn" and "error" for reacting when obsolete
Current string // ipfs version for which config was generated
UpdateCheckedTime time.Time // timestamp for the last time API endpoint was checked for updates
UpdateCheckPeriod time.Duration // time duration over which the update check will not be performed
}
// supported Version.Check values
const (
CheckError = "error" // value for Version.Check to raise error and exit if version is obsolete
CheckWarn = "warn" // value for Version.Check to show warning message if version is obsolete
CheckIgnore = "ignore" // value for Version.Check to not perform update check
)
var defaultUpdateCheckPeriod = time.Hour * 48
// EligibleForUpdateCheck returns if update check API endpoint is needed for this specific runtime
func (v *Version) EligibleForUpdateCheck() bool {
if v.Check == CheckIgnore || v.UpdateCheckedTime.Add(v.UpdateCheckPeriod).After(time.Now()) {
return false
}
return true
}
// RecordCurrentUpdateCheck is called to record that update check was performed and showed that the running version is the most recent one
func (cfg *Config) RecordCurrentUpdateCheck(filename string) {
cfg.Version.UpdateCheckedTime = time.Now()
if cfg.Version.UpdateCheckPeriod == time.Duration(0) {
// UpdateCheckPeriod was not initialized for some reason (e.g. config file used is broken)
cfg.Version.UpdateCheckPeriod = defaultUpdateCheckPeriod
}
WriteConfigFile(filename, cfg)
}
package updates
import (
"encoding/json"
"fmt"
"net/http"
"os"
"github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/coreos/go-semver/semver"
u "github.com/jbenet/go-ipfs/util"
)
const (
Version = "0.1.0" // actual current application's version literal
EndpointURLLatestReleases = "https://api.github.com/repos/jbenet/go-ipfs/tags"
VersionErrorShort = `Warning: You are running version %s of go-ipfs. The latest version is %s.`
VersionErrorLong = `
Warning: You are running version %s of go-ipfs. The latest version is %s.
Since this is alpha software, it is strongly recommended you update.
You can update go-ipfs by running
ipfs version update
You can silence this message by running
ipfs config update.check ignore
`
)
var log = u.Logger("updates")
var currentVersion *semver.Version
func init() {
var err error
currentVersion, err = semver.NewVersion(Version)
if err != nil {
u.PErr("The const Version literal in version.go needs to be in semver format: %s \n", Version)
os.Exit(1)
}
}
func CheckForUpdates() error {
resp, err := http.Get(EndpointURLLatestReleases)
if err != nil {
// can't reach the endpoint, coud be firewall, or no internet connection or something else
log.Error("update check: error connecting to API endpoint for newer versions: %v", err)
return nil
}
var body interface{}
_ = json.NewDecoder(resp.Body).Decode(&body)
releases, ok := body.([]interface{})
if !ok {
// the response body does not seem to meet specified Github API format
// https://developer.github.com/v3/repos/#list-tags
log.Error("update check: API endpoint for newer versions does not seem to be in Github API specified format")
return nil
}
for _, r := range releases {
release, ok := r.(map[string]interface{})
if !ok {
continue
}
tagName, ok := release["name"].(string)
if !ok {
continue
}
if len(tagName) > 0 && tagName[0] == 'v' {
// both 'v0.1.0' and '0.1.0' semver tagname conventions can be encountered
tagName = tagName[1:]
}
releaseVersion, err := semver.NewVersion(tagName)
if err != nil {
continue
}
if currentVersion.LessThan(*releaseVersion) {
return fmt.Errorf(VersionErrorLong, Version, tagName)
}
}
return nil
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment