From deb9aebc482969555143f42964170406d3fd20e6 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 18 Oct 2014 13:31:24 +0200 Subject: [PATCH 01/17] vendor inconshreveable/go-update and it's deps --- Godeps/Godeps.json | 13 + .../src/bitbucket.org/kardianos/osext/LICENSE | 27 + .../bitbucket.org/kardianos/osext/osext.go | 32 ++ .../kardianos/osext/osext_plan9.go | 20 + .../kardianos/osext/osext_procfs.go | 28 + .../kardianos/osext/osext_sysctl.go | 79 +++ .../kardianos/osext/osext_test.go | 79 +++ .../kardianos/osext/osext_windows.go | 34 ++ .../inconshreveable/go-update/LICENSE | 13 + .../inconshreveable/go-update/README.md | 37 ++ .../inconshreveable/go-update/check/check.go | 209 ++++++++ .../go-update/download/download.go | 230 +++++++++ .../inconshreveable/go-update/hide_noop.go | 7 + .../inconshreveable/go-update/hide_windows.go | 19 + .../inconshreveable/go-update/update.go | 486 ++++++++++++++++++ .../inconshreveable/go-update/update_test.go | 380 ++++++++++++++ .../github.com/jbenet/go-multihash/LICENSE | 21 + .../src/github.com/kr/binarydist/.gitignore | 1 + .../src/github.com/kr/binarydist/License | 22 + .../src/github.com/kr/binarydist/Readme.md | 7 + .../src/github.com/kr/binarydist/bzip2.go | 40 ++ .../github.com/kr/binarydist/common_test.go | 93 ++++ .../src/github.com/kr/binarydist/diff.go | 408 +++++++++++++++ .../src/github.com/kr/binarydist/diff_test.go | 67 +++ .../src/github.com/kr/binarydist/doc.go | 24 + .../src/github.com/kr/binarydist/encoding.go | 53 ++ .../src/github.com/kr/binarydist/patch.go | 109 ++++ .../github.com/kr/binarydist/patch_test.go | 62 +++ .../src/github.com/kr/binarydist/seek.go | 43 ++ .../src/github.com/kr/binarydist/sort_test.go | 33 ++ .../kr/binarydist/testdata/sample.new | Bin 0 -> 10000 bytes .../kr/binarydist/testdata/sample.old | Bin 0 -> 11000 bytes .../kr/binarydist/testdata/sample.patch | Bin 0 -> 1090 bytes 33 files changed, 2676 insertions(+) create mode 100644 Godeps/_workspace/src/bitbucket.org/kardianos/osext/LICENSE create mode 100644 Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext.go create mode 100644 Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_plan9.go create mode 100644 Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_procfs.go create mode 100644 Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_sysctl.go create mode 100644 Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_test.go create mode 100644 Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_windows.go create mode 100644 Godeps/_workspace/src/github.com/inconshreveable/go-update/LICENSE create mode 100644 Godeps/_workspace/src/github.com/inconshreveable/go-update/README.md create mode 100644 Godeps/_workspace/src/github.com/inconshreveable/go-update/check/check.go create mode 100644 Godeps/_workspace/src/github.com/inconshreveable/go-update/download/download.go create mode 100644 Godeps/_workspace/src/github.com/inconshreveable/go-update/hide_noop.go create mode 100644 Godeps/_workspace/src/github.com/inconshreveable/go-update/hide_windows.go create mode 100644 Godeps/_workspace/src/github.com/inconshreveable/go-update/update.go create mode 100644 Godeps/_workspace/src/github.com/inconshreveable/go-update/update_test.go create mode 100644 Godeps/_workspace/src/github.com/jbenet/go-multihash/LICENSE create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/.gitignore create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/License create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/Readme.md create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/bzip2.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/common_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/diff.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/diff_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/doc.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/encoding.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/patch.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/patch_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/seek.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/sort_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/testdata/sample.new create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/testdata/sample.old create mode 100644 Godeps/_workspace/src/github.com/kr/binarydist/testdata/sample.patch diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 51e93adfafe..ecf80877dc9 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -9,6 +9,11 @@ "ImportPath": "bazil.org/fuse", "Rev": "a04507d54fc3610d38ee951402d8c4acab56c7b1" }, + { + "ImportPath": "bitbucket.org/kardianos/osext", + "Comment": "null-15", + "Rev": "44140c5fc69ecf1102c5ef451d73cd98ef59b178" + }, { "ImportPath": "code.google.com/p/go-uuid/uuid", "Comment": "null-12", @@ -62,6 +67,10 @@ "ImportPath": "github.com/gorilla/mux", "Rev": "4b8fbc56f3b2400a7c7ea3dba9b3539787c486b6" }, + { + "ImportPath": "github.com/inconshreveable/go-update", + "Rev": "221d034a558b4c21b0624b2a450c076913854a57" + }, { "ImportPath": "github.com/jbenet/commander", "Rev": "e0cf317891f0ab6f1ac64dfcb754b4fb5e69f7df" @@ -92,6 +101,10 @@ "Comment": "0.1.0-5-g1976046", "Rev": "1976046c2b0db0b668791b3e541d76a38b7c1af7" }, + { + "ImportPath": "github.com/kr/binarydist", + "Rev": "9955b0ab8708602d411341e55fffd7e0700f86bd" + }, { "ImportPath": "github.com/mitchellh/go-homedir", "Rev": "7d2d8c8a4e078ce3c58736ab521a40b37a504c52" diff --git a/Godeps/_workspace/src/bitbucket.org/kardianos/osext/LICENSE b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/LICENSE new file mode 100644 index 00000000000..74487567632 --- /dev/null +++ b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext.go b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext.go new file mode 100644 index 00000000000..37efbb22108 --- /dev/null +++ b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext.go @@ -0,0 +1,32 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Extensions to the standard "os" package. +package osext + +import "path/filepath" + +// Executable returns an absolute path that can be used to +// re-invoke the current program. +// It may not be valid after the current program exits. +func Executable() (string, error) { + p, err := executable() + return filepath.Clean(p), err +} + +// Returns same path as Executable, returns just the folder +// path. Excludes the executable name. +func ExecutableFolder() (string, error) { + p, err := Executable() + if err != nil { + return "", err + } + folder, _ := filepath.Split(p) + return folder, nil +} + +// Depricated. Same as Executable(). +func GetExePath() (exePath string, err error) { + return Executable() +} diff --git a/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_plan9.go b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_plan9.go new file mode 100644 index 00000000000..655750c5426 --- /dev/null +++ b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_plan9.go @@ -0,0 +1,20 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package osext + +import ( + "os" + "strconv" + "syscall" +) + +func executable() (string, error) { + f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text") + if err != nil { + return "", err + } + defer f.Close() + return syscall.Fd2path(int(f.Fd())) +} diff --git a/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_procfs.go b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_procfs.go new file mode 100644 index 00000000000..9c677c53553 --- /dev/null +++ b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_procfs.go @@ -0,0 +1,28 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build linux netbsd openbsd solaris + +package osext + +import ( + "errors" + "fmt" + "os" + "runtime" +) + +func executable() (string, error) { + switch runtime.GOOS { + case "linux": + return os.Readlink("/proc/self/exe") + case "netbsd": + return os.Readlink("/proc/curproc/exe") + case "openbsd": + return os.Readlink("/proc/curproc/file") + case "solaris": + return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid())) + } + return "", errors.New("ExecPath not implemented for " + runtime.GOOS) +} diff --git a/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_sysctl.go b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_sysctl.go new file mode 100644 index 00000000000..b66cac878c4 --- /dev/null +++ b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_sysctl.go @@ -0,0 +1,79 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin freebsd + +package osext + +import ( + "os" + "path/filepath" + "runtime" + "syscall" + "unsafe" +) + +var initCwd, initCwdErr = os.Getwd() + +func executable() (string, error) { + var mib [4]int32 + switch runtime.GOOS { + case "freebsd": + mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1} + case "darwin": + mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1} + } + + n := uintptr(0) + // Get length. + _, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0) + if errNum != 0 { + return "", errNum + } + if n == 0 { // This shouldn't happen. + return "", nil + } + buf := make([]byte, n) + _, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0) + if errNum != 0 { + return "", errNum + } + if n == 0 { // This shouldn't happen. + return "", nil + } + for i, v := range buf { + if v == 0 { + buf = buf[:i] + break + } + } + var err error + execPath := string(buf) + // execPath will not be empty due to above checks. + // Try to get the absolute path if the execPath is not rooted. + if execPath[0] != '/' { + execPath, err = getAbs(execPath) + if err != nil { + return execPath, err + } + } + // For darwin KERN_PROCARGS may return the path to a symlink rather than the + // actual executable. + if runtime.GOOS == "darwin" { + if execPath, err = filepath.EvalSymlinks(execPath); err != nil { + return execPath, err + } + } + return execPath, nil +} + +func getAbs(execPath string) (string, error) { + if initCwdErr != nil { + return execPath, initCwdErr + } + // The execPath may begin with a "../" or a "./" so clean it first. + // Join the two paths, trailing and starting slashes undetermined, so use + // the generic Join function. + return filepath.Join(initCwd, filepath.Clean(execPath)), nil +} diff --git a/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_test.go b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_test.go new file mode 100644 index 00000000000..dc661dbc213 --- /dev/null +++ b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_test.go @@ -0,0 +1,79 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build darwin linux freebsd netbsd windows + +package osext + +import ( + "fmt" + "os" + oexec "os/exec" + "path/filepath" + "runtime" + "testing" +) + +const execPath_EnvVar = "OSTEST_OUTPUT_EXECPATH" + +func TestExecPath(t *testing.T) { + ep, err := Executable() + if err != nil { + t.Fatalf("ExecPath failed: %v", err) + } + // we want fn to be of the form "dir/prog" + dir := filepath.Dir(filepath.Dir(ep)) + fn, err := filepath.Rel(dir, ep) + if err != nil { + t.Fatalf("filepath.Rel: %v", err) + } + cmd := &oexec.Cmd{} + // make child start with a relative program path + cmd.Dir = dir + cmd.Path = fn + // forge argv[0] for child, so that we can verify we could correctly + // get real path of the executable without influenced by argv[0]. + cmd.Args = []string{"-", "-test.run=XXXX"} + cmd.Env = []string{fmt.Sprintf("%s=1", execPath_EnvVar)} + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("exec(self) failed: %v", err) + } + outs := string(out) + if !filepath.IsAbs(outs) { + t.Fatalf("Child returned %q, want an absolute path", out) + } + if !sameFile(outs, ep) { + t.Fatalf("Child returned %q, not the same file as %q", out, ep) + } +} + +func sameFile(fn1, fn2 string) bool { + fi1, err := os.Stat(fn1) + if err != nil { + return false + } + fi2, err := os.Stat(fn2) + if err != nil { + return false + } + return os.SameFile(fi1, fi2) +} + +func init() { + if e := os.Getenv(execPath_EnvVar); e != "" { + // first chdir to another path + dir := "/" + if runtime.GOOS == "windows" { + dir = filepath.VolumeName(".") + } + os.Chdir(dir) + if ep, err := Executable(); err != nil { + fmt.Fprint(os.Stderr, "ERROR: ", err) + } else { + fmt.Fprint(os.Stderr, ep) + } + os.Exit(0) + } +} diff --git a/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_windows.go b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_windows.go new file mode 100644 index 00000000000..72d282cf8c0 --- /dev/null +++ b/Godeps/_workspace/src/bitbucket.org/kardianos/osext/osext_windows.go @@ -0,0 +1,34 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package osext + +import ( + "syscall" + "unicode/utf16" + "unsafe" +) + +var ( + kernel = syscall.MustLoadDLL("kernel32.dll") + getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW") +) + +// GetModuleFileName() with hModule = NULL +func executable() (exePath string, err error) { + return getModuleFileName() +} + +func getModuleFileName() (string, error) { + var n uint32 + b := make([]uint16, syscall.MAX_PATH) + size := uint32(len(b)) + + r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size)) + n = uint32(r0) + if n == 0 { + return "", e1 + } + return string(utf16.Decode(b[0:n])), nil +} diff --git a/Godeps/_workspace/src/github.com/inconshreveable/go-update/LICENSE b/Godeps/_workspace/src/github.com/inconshreveable/go-update/LICENSE new file mode 100644 index 00000000000..5f0d1fb6a7b --- /dev/null +++ b/Godeps/_workspace/src/github.com/inconshreveable/go-update/LICENSE @@ -0,0 +1,13 @@ +Copyright 2014 Alan Shreve + +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. diff --git a/Godeps/_workspace/src/github.com/inconshreveable/go-update/README.md b/Godeps/_workspace/src/github.com/inconshreveable/go-update/README.md new file mode 100644 index 00000000000..f070062c226 --- /dev/null +++ b/Godeps/_workspace/src/github.com/inconshreveable/go-update/README.md @@ -0,0 +1,37 @@ +# go-update: Automatically update Go programs from the internet + +go-update allows a program to update itself by replacing its executable file +with a new version. It provides the flexibility to implement different updating user experiences +like auto-updating, or manual user-initiated updates. It also boasts +advanced features like binary patching and code signing verification. + +Updating your program to a new version is as easy as: + + err, errRecover := update.New().FromUrl("http://release.example.com/2.0/myprogram") + if err != nil { + fmt.Printf("Update failed: %v\n", err) + } + +## Documentation and API Reference + +Comprehensive API documentation and code examples are available in the code documentation available on godoc.org: + +[![GoDoc](https://godoc.org/github.com/inconshreveable/go-update?status.svg)](https://godoc.org/github.com/inconshreveable/go-update) + +## Features + +- Cross platform support (Windows too!) +- Binary patch application +- Checksum verification +- Code signing verification +- Support for updating arbitrary files + +## [equinox.io](https://equinox.io) +go-update provides the primitives for building self-updating applications, but there a number of other challenges +involved in a complete updating solution such as hosting, code signing, update channels, gradual rollout, +dynamically computing binary patches, tracking update metrics like versions and failures, plus more. + +I provide this service, a complete solution, free for open source projects, at [equinox.io](https://equinox.io). + +## License +Apache diff --git a/Godeps/_workspace/src/github.com/inconshreveable/go-update/check/check.go b/Godeps/_workspace/src/github.com/inconshreveable/go-update/check/check.go new file mode 100644 index 00000000000..e8f7981f248 --- /dev/null +++ b/Godeps/_workspace/src/github.com/inconshreveable/go-update/check/check.go @@ -0,0 +1,209 @@ +package check + +import ( + "bytes" + _ "crypto/sha512" // for tls cipher support + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "runtime" + + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/bitbucket.org/kardianos/osext" + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update" +) + +type Initiative string + +const ( + INITIATIVE_NEVER Initiative = "never" + INITIATIVE_AUTO = "auto" + INITIATIVE_MANUAL = "manual" +) + +var NoUpdateAvailable error = fmt.Errorf("No update available") + +type Params struct { + // protocol version + Version int `json:"version"` + // identifier of the application to update + AppId string `json:"app_id"` + // version of the application updating itself + AppVersion string `json:"app_version"` + // operating system of target platform + OS string `json:"-"` + // hardware architecture of target platform + Arch string `json:"-"` + // application-level user identifier + UserId string `json:"user_id"` + // checksum of the binary to replace (used for returning diff patches) + Checksum string `json:"checksum"` + // release channel (empty string means 'stable') + Channel string `json:"-"` + // tags for custom update channels + Tags map[string]string `json:"tags"` +} + +type Result struct { + up *update.Update + + // should the update be applied automatically/manually + Initiative Initiative `json:"initiative"` + // url where to download the updated application + Url string `json:"url"` + // a URL to a patch to apply + PatchUrl string `json:"patch_url"` + // the patch format (only bsdiff supported at the moment) + PatchType update.PatchType `json:"patch_type"` + // version of the new application + Version string `json:"version"` + // expected checksum of the new application + Checksum string `json:"checksum"` + // signature for verifying update authenticity + Signature string `json:"signature"` +} + +// CheckForUpdate makes an HTTP post to a URL with the JSON serialized +// representation of Params. It returns the deserialized result object +// returned by the remote endpoint or an error. If you do not set +// OS/Arch, CheckForUpdate will populate them for you. Similarly, if +// Version is 0, it will be set to 1. Lastly, if Checksum is the empty +// string, it will be automatically be computed for the running program's +// executable file. +func (p *Params) CheckForUpdate(url string, up *update.Update) (*Result, error) { + if p.Tags == nil { + p.Tags = make(map[string]string) + } + + if p.Channel == "" { + p.Channel = "stable" + } + + if p.OS == "" { + p.OS = runtime.GOOS + } + + if p.Arch == "" { + p.Arch = runtime.GOARCH + } + + if p.Version == 0 { + p.Version = 1 + } + + // ignore errors auto-populating the checksum + // if it fails, you just won't be able to patch + if up.TargetPath == "" { + p.Checksum = defaultChecksum() + } else { + checksum, err := update.ChecksumForFile(up.TargetPath) + if err != nil { + return nil, err + } + p.Checksum = hex.EncodeToString(checksum) + } + + p.Tags["os"] = p.OS + p.Tags["arch"] = p.Arch + p.Tags["channel"] = p.Channel + + body, err := json.Marshal(p) + if err != nil { + return nil, err + } + + resp, err := http.Post(url, "application/json", bytes.NewReader(body)) + if err != nil { + return nil, err + } + + // no content means no available update + if resp.StatusCode == 204 { + return nil, NoUpdateAvailable + } + + defer resp.Body.Close() + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + result := &Result{up: up} + if err := json.Unmarshal(respBytes, result); err != nil { + return nil, err + } + + return result, nil +} + +func (p *Params) CheckAndApplyUpdate(url string, up *update.Update) (result *Result, err error, errRecover error) { + // check for an update + result, err = p.CheckForUpdate(url, up) + if err != nil { + return + } + + // run the available update + err, errRecover = result.Update() + return +} + +func (r *Result) Update() (err error, errRecover error) { + if r.Checksum != "" { + r.up.Checksum, err = hex.DecodeString(r.Checksum) + if err != nil { + return + } + } + + if r.Signature != "" { + r.up.Signature, err = hex.DecodeString(r.Signature) + if err != nil { + return + } + } + + if r.PatchType != "" { + r.up.PatchType = r.PatchType + } + + if r.Url == "" && r.PatchUrl == "" { + err = fmt.Errorf("Result does not contain an update url or patch update url") + return + } + + if r.PatchUrl != "" { + err, errRecover = r.up.FromUrl(r.PatchUrl) + if err == nil { + // success! + return + } else { + // failed to update from patch URL, try with the whole thing + if r.Url == "" || errRecover != nil { + // we can't try updating from a URL with the full contents + // in these cases, so fail + return + } else { + r.up.PatchType = update.PATCHTYPE_NONE + } + } + } + + // try updating from a URL with the full contents + return r.up.FromUrl(r.Url) +} + +func defaultChecksum() string { + path, err := osext.Executable() + if err != nil { + return "" + } + + checksum, err := update.ChecksumForFile(path) + if err != nil { + return "" + } + + return hex.EncodeToString(checksum) +} diff --git a/Godeps/_workspace/src/github.com/inconshreveable/go-update/download/download.go b/Godeps/_workspace/src/github.com/inconshreveable/go-update/download/download.go new file mode 100644 index 00000000000..b4a24b1b8be --- /dev/null +++ b/Godeps/_workspace/src/github.com/inconshreveable/go-update/download/download.go @@ -0,0 +1,230 @@ +package download + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "net/http" + "os" + "runtime" +) + +type roundTripper struct { + RoundTripFn func(*http.Request) (*http.Response, error) +} + +func (rt *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + return rt.RoundTripFn(r) +} + +// Download encapsulates the state and parameters to download content +// from a URL which: +// +// - Publishes the percentage of the download completed to a channel. +// - May resume a previous download that was partially completed. +// +// Create an instance with the New() factory function. +type Download struct { + // net/http.Client to use when downloading the update. + // If nil, a default http.Client is used + HttpClient *http.Client + + // As bytes are downloaded, they are written to Target. + // Download also uses the Target's Seek method to determine + // the size of partial-downloads so that it may properly + // request the remaining bytes to resume the download. + Target Target + + // Progress returns the percentage of the download + // completed as an integer between 0 and 100 + Progress chan (int) + + // HTTP Method to use in the download request. Default is "GET" + Method string + + // HTTP URL to issue the download request to + Url string +} + +// New initializes a new Download object which will download +// the content from url into target. +func New(url string, target Target) *Download { + return &Download{ + HttpClient: new(http.Client), + Progress: make(chan int), + Method: "GET", + Url: url, + Target: target, + } +} + +// Get() downloads the content of a url to a target destination. +// +// Only HTTP/1.1 servers that implement the Range header support resuming a +// partially completed download. +// +// On success, the server must return 200 and the content, or 206 when resuming a partial download. +// If the HTTP server returns a 3XX redirect, it will be followed according to d.HttpClient's redirect policy. +// +func (d *Download) Get() (err error) { + // Close the progress channel whenever this function completes + defer close(d.Progress) + + // determine the size of the download target to determine if we're resuming a partial download + offset, err := d.Target.Size() + if err != nil { + return + } + + // create the download request + req, err := http.NewRequest(d.Method, d.Url, nil) + if err != nil { + return + } + + // we have to add headers like this so they get used across redirects + trans := d.HttpClient.Transport + if trans == nil { + trans = http.DefaultTransport + } + + d.HttpClient.Transport = &roundTripper{ + RoundTripFn: func(r *http.Request) (*http.Response, error) { + // add header for download continuation + if offset > 0 { + r.Header.Add("Range", fmt.Sprintf("%d-", offset)) + } + + // ask for gzipped content so that net/http won't unzip it for us + // and destroy the content length header we need for progress calculations + r.Header.Add("Accept-Encoding", "gzip") + + return trans.RoundTrip(r) + }, + } + + // issue the download request + resp, err := d.HttpClient.Do(req) + if err != nil { + return + } + defer resp.Body.Close() + + switch resp.StatusCode { + // ok + case 200, 206: + + // server error + default: + err = fmt.Errorf("Non 2XX response when downloading update: %s", resp.Status) + return + } + + // Determine how much we have to download + // net/http sets this to -1 when it is unknown + clength := resp.ContentLength + + // Read the content from the response body + rd := resp.Body + + // meter the rate at which we download content for + // progress reporting if we know how much to expect + if clength > 0 { + rd = &meteredReader{rd: rd, totalSize: clength, progress: d.Progress} + } + + // Decompress the content if necessary + if resp.Header.Get("Content-Encoding") == "gzip" { + rd, err = gzip.NewReader(rd) + if err != nil { + return + } + } + + // Download the update + _, err = io.Copy(d.Target, rd) + if err != nil { + return + } + + return +} + +// meteredReader wraps a ReadCloser. Calls to a meteredReader's Read() method +// publish updates to a progress channel with the percentage read so far. +type meteredReader struct { + rd io.ReadCloser + totalSize int64 + progress chan int + totalRead int64 + ticks int64 +} + +func (m *meteredReader) Close() error { + return m.rd.Close() +} + +func (m *meteredReader) Read(b []byte) (n int, err error) { + chunkSize := (m.totalSize / 100) + 1 + lenB := int64(len(b)) + + var nChunk int + for start := int64(0); start < lenB; start += int64(nChunk) { + end := start + chunkSize + if end > lenB { + end = lenB + } + + nChunk, err = m.rd.Read(b[start:end]) + + n += nChunk + m.totalRead += int64(nChunk) + + if m.totalRead > (m.ticks * chunkSize) { + m.ticks += 1 + // try to send on channel, but don't block if it's full + select { + case m.progress <- int(m.ticks + 1): + default: + } + + // give the progress channel consumer a chance to run + runtime.Gosched() + } + + if err != nil { + return + } + } + + return +} + +// A Target is what you can supply to Download, +// it's just an io.Writer with a Size() method so that +// the a Download can "resume" an interrupted download +type Target interface { + io.Writer + Size() (int, error) +} + +type FileTarget struct { + *os.File +} + +func (t *FileTarget) Size() (int, error) { + if fi, err := t.File.Stat(); err != nil { + return 0, err + } else { + return int(fi.Size()), nil + } +} + +type MemoryTarget struct { + bytes.Buffer +} + +func (t *MemoryTarget) Size() (int, error) { + return t.Buffer.Len(), nil +} diff --git a/Godeps/_workspace/src/github.com/inconshreveable/go-update/hide_noop.go b/Godeps/_workspace/src/github.com/inconshreveable/go-update/hide_noop.go new file mode 100644 index 00000000000..3707756087d --- /dev/null +++ b/Godeps/_workspace/src/github.com/inconshreveable/go-update/hide_noop.go @@ -0,0 +1,7 @@ +// +build !windows + +package update + +func hideFile(path string) error { + return nil +} diff --git a/Godeps/_workspace/src/github.com/inconshreveable/go-update/hide_windows.go b/Godeps/_workspace/src/github.com/inconshreveable/go-update/hide_windows.go new file mode 100644 index 00000000000..c368b9cc45b --- /dev/null +++ b/Godeps/_workspace/src/github.com/inconshreveable/go-update/hide_windows.go @@ -0,0 +1,19 @@ +package update + +import ( + "syscall" + "unsafe" +) + +func hideFile(path string) error { + kernel32 := syscall.NewLazyDLL("kernel32.dll") + setFileAttributes := kernel32.NewProc("SetFileAttributesW") + + r1, _, err := setFileAttributes.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(path))), 2) + + if r1 == 0 { + return err + } else { + return nil + } +} diff --git a/Godeps/_workspace/src/github.com/inconshreveable/go-update/update.go b/Godeps/_workspace/src/github.com/inconshreveable/go-update/update.go new file mode 100644 index 00000000000..e877a82caab --- /dev/null +++ b/Godeps/_workspace/src/github.com/inconshreveable/go-update/update.go @@ -0,0 +1,486 @@ +/* +go-update allows a program to update itself by replacing its executable file +with a new version. It provides the flexibility to implement different updating user experiences +like auto-updating, or manual user-initiated updates. It also boasts +advanced features like binary patching and code signing verification. + +Updating your program to a new version is as easy as: + + err, errRecover := update.New().FromUrl("http://release.example.com/2.0/myprogram") + if err != nil { + fmt.Printf("Update failed: %v\n", err) + } + +You may also choose to update from other data sources such as a file or an io.Reader: + + err, errRecover := update.New().FromFile("/path/to/update") + +Binary Diff Patching + +Binary diff updates are supported and easy to use: + + up := update.New().ApplyPatch(update.PATCHTYPE_BSDIFF) + err, errRecover := up.FromUrl("http://release.example.com/2.0/mypatch") + +Checksum Verification + +You should also verify the checksum of new updates as well as verify +the digital signature of an update. Note that even when you choose to apply +a patch, the checksum is verified against the complete update after that patch +has been applied. + + up := update.New().ApplyPatch(update.PATCHTYPE_BSDIFF).VerifyChecksum(checksum) + err, errRecover := up.FromUrl("http://release.example.com/2.0/mypatch") + +Updating other files + +Updating arbitrary files is also supported. You may update files which are +not the currently running program: + + up := update.New().Target("/usr/local/bin/some-program") + err, errRecover := up.FromUrl("http://release.example.com/2.0/some-program") + +Code Signing + +Truly secure updates use code signing to verify that the update was issued by a trusted party. +To do this, you'll need to generate a public/private key pair. You can do this with openssl, +or the equinox.io client (https://equinox.io/client) can easily generate one for you: + + # with equinox client + equinox genkey --private-key=private.pem --public-key=public.pem + + # with openssl + openssl genrsa -out private.pem 2048 + openssl rsa -in private.pem -out public.pem -pubout + +Once you have your key pair, you can instruct your program to validate its updates +with the public key: + + const publicKey = `-----BEGIN PUBLIC KEY----- + ... + -----END PUBLIC KEY-----` + + up, err := update.New().VerifySignatureWithPEM(publicKey) + if err != nil { + return fmt.Errorf("Bad public key: '%v': %v", publicKey, err) + } + +Once you've configured your program this way, it will disallow all updates unless they +are properly signed. You must now pass in the signature to verify with: + + up.VerifySignature(signature).FromUrl("http://dl.example.com/update") + +Error Handling and Recovery + +To perform an update, the process must be able to read its executable file and to write +to the directory that contains its executable file. It can be useful to check whether the process +has the necessary permissions to perform an update before trying to apply one. Use the +CanUpdate call to provide a useful message to the user if the update can't proceed without +elevated permissions: + + up := update.New().Target("/etc/hosts") + err := up.CanUpdate() + if err != nil { + fmt.Printf("Can't update because: '%v'. Try as root or Administrator\n", err) + return + } + err, errRecover := up.FromUrl("https://example.com/new/hosts") + +Although exceedingly unlikely, the update operation itself is not atomic and can fail +in such a way that a user's computer is left in an inconsistent state. If that happens, +go-update attempts to recover to leave the system in a good state. If the recovery step +fails (even more unlikely), a second error, referred to as "errRecover" will be non-nil +so that you may inform your users of the bad news. You should handle this case as shown +here: + + err, errRecover := up.FromUrl("https://example.com/update") + if err != nil { + fmt.Printf("Update failed: %v\n", err) + if errRecover != nil { + fmt.Printf("Failed to recover bad update: %v!\n", errRecover) + fmt.Printf("Program exectuable may be missing!\n") + } + } + +Subpackages + +Sub-package check contains the client functionality for a simple protocol for negotiating +whether a new update is available, where it is, and the metadata needed for verifying it. + +Sub-package download contains functionality for downloading from an HTTP endpoint +while outputting a progress meter and supports resuming partial downloads. +*/ +package update + +import ( + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/bitbucket.org/kardianos/osext" + "bytes" + "crypto" + "crypto/rsa" + "crypto/sha256" + _ "crypto/sha512" // for tls cipher support + "crypto/x509" + "encoding/pem" + "fmt" + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/download" + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/kr/binarydist" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +// The type of a binary patch, if any. Only bsdiff is supported +type PatchType string + +const ( + PATCHTYPE_BSDIFF PatchType = "bsdiff" + PATCHTYPE_NONE = "" +) + +type Update struct { + // empty string means "path of the current executable" + TargetPath string + + // type of patch to apply. PATCHTYPE_NONE means "not a patch" + PatchType + + // sha256 checksum of the new binary to verify against + Checksum []byte + + // public key to use for signature verification + PublicKey *rsa.PublicKey + + // signature to use for signature verification + Signature []byte +} + +func (u *Update) getPath() (string, error) { + if u.TargetPath == "" { + return osext.Executable() + } else { + return u.TargetPath, nil + } +} + +// New creates a new Update object. +// A default update object assumes the complete binary +// content will be used for update (not a patch) and that +// the intended target is the running executable. +// +// Use this as the start of a chain of calls on the Update +// object to build up your configuration. Example: +// +// up := update.New().ApplyPatch(update.PATCHTYPE_BSDIFF).VerifyChecksum(checksum) +// +func New() *Update { + return &Update{ + TargetPath: "", + PatchType: PATCHTYPE_NONE, + } +} + +// Target configures the update to update the file at the given path. +// The emptry string means 'the executable file of the running program'. +func (u *Update) Target(path string) *Update { + u.TargetPath = path + return u +} + +// ApplyPatch configures the update to treat the contents of the update +// as a patch to apply to the existing to target. You must specify the +// format of the patch. Only PATCHTYPE_BSDIFF is supported at the moment. +func (u *Update) ApplyPatch(patchType PatchType) *Update { + u.PatchType = patchType + return u +} + +// VerifyChecksum configures the update to verify that the +// the update has the given sha256 checksum. +func (u *Update) VerifyChecksum(checksum []byte) *Update { + u.Checksum = checksum + return u +} + +// VerifySignature configures the update to verify the given +// signature of the update. You must also call one of the +// VerifySignatureWith* functions to specify a public key +// to use for verification. +func (u *Update) VerifySignature(signature []byte) *Update { + u.Signature = signature + return u +} + +// VerifySignatureWith configures the update to use the given RSA +// public key to verify the update's signature. You must also call +// VerifySignature() with a signature to check. +// +// You'll probably want to use VerifySignatureWithPEM instead of +// parsing the public key yourself. +func (u *Update) VerifySignatureWith(publicKey *rsa.PublicKey) *Update { + u.PublicKey = publicKey + return u +} + +// VerifySignatureWithPEM configures the update to use the given PEM-formatted +// RSA public key to verify the update's signature. You must also call +// VerifySignature() with a signature to check. +// +// A PEM formatted public key typically begins with +// -----BEGIN PUBLIC KEY----- +func (u *Update) VerifySignatureWithPEM(publicKeyPEM []byte) (*Update, error) { + block, _ := pem.Decode(publicKeyPEM) + if block == nil { + return u, fmt.Errorf("Couldn't parse PEM data") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return u, err + } + + var ok bool + u.PublicKey, ok = pub.(*rsa.PublicKey) + if !ok { + return u, fmt.Errorf("Public key isn't an RSA public key") + } + + return u, nil +} + +// FromUrl updates the target with the contents of the given URL. +func (u *Update) FromUrl(url string) (err error, errRecover error) { + target := new(download.MemoryTarget) + err = download.New(url, target).Get() + if err != nil { + return + } + + return u.FromStream(target) +} + +// FromFile updates the target the contents of the given file. +func (u *Update) FromFile(path string) (err error, errRecover error) { + // open the new updated contents + fp, err := os.Open(path) + if err != nil { + return + } + defer fp.Close() + + // do the update + return u.FromStream(fp) +} + +// FromStream updates the target file with the contents of the supplied io.Reader. +// +// FromStream performs the following actions to ensure a safe cross-platform update: +// +// 1. If configured, applies the contents of the io.Reader as a binary patch. +// +// 2. If configured, computes the sha256 checksum and verifies it matches. +// +// 3. If configured, verifies the RSA signature with a public key. +// +// 4. Creates a new file, /path/to/.target.new with mode 0755 with the contents of the updated file +// +// 5. Renames /path/to/target to /path/to/.target.old +// +// 6. Renames /path/to/.target.new to /path/to/target +// +// 7. If the rename is successful, deletes /path/to/.target.old, returns no error +// +// 8. If the rename fails, attempts to rename /path/to/.target.old back to /path/to/target +// If this operation fails, it is reported in the errRecover return value so as not to +// mask the original error that caused the recovery attempt. +// +// On Windows, the removal of /path/to/.target.old always fails, so instead, +// we just make the old file hidden instead. +func (u *Update) FromStream(updateWith io.Reader) (err error, errRecover error) { + updatePath, err := u.getPath() + if err != nil { + return + } + + var newBytes []byte + // apply a patch if requested + switch u.PatchType { + case PATCHTYPE_BSDIFF: + newBytes, err = applyPatch(updateWith, updatePath) + if err != nil { + return + } + case PATCHTYPE_NONE: + // no patch to apply, go on through + newBytes, err = ioutil.ReadAll(updateWith) + if err != nil { + return + } + default: + err = fmt.Errorf("Unrecognized patch type: %s", u.PatchType) + return + } + + // verify checksum if requested + if u.Checksum != nil { + if err = verifyChecksum(newBytes, u.Checksum); err != nil { + return + } + } + + // verify signature if requested + if u.Signature != nil || u.PublicKey != nil { + if u.Signature == nil { + err = fmt.Errorf("No public key specified to verify signature") + return + } + + if u.PublicKey == nil { + err = fmt.Errorf("No signature to verify!") + return + } + + if err = verifySignature(newBytes, u.Signature, u.PublicKey); err != nil { + return + } + } + + // get the directory the executable exists in + updateDir := filepath.Dir(updatePath) + filename := filepath.Base(updatePath) + + // Copy the contents of of newbinary to a the new executable file + newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename)) + fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return + } + defer fp.Close() + _, err = io.Copy(fp, bytes.NewReader(newBytes)) + + // if we don't call fp.Close(), windows won't let us move the new executable + // because the file will still be "in use" + fp.Close() + + // this is where we'll move the executable to so that we can swap in the updated replacement + oldPath := filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename)) + + // delete any existing old exec file - this is necessary on Windows for two reasons: + // 1. after a successful update, Windows can't remove the .old file because the process is still running + // 2. windows rename operations fail if the destination file already exists + _ = os.Remove(oldPath) + + // move the existing executable to a new file in the same directory + err = os.Rename(updatePath, oldPath) + if err != nil { + return + } + + // move the new exectuable in to become the new program + err = os.Rename(newPath, updatePath) + + if err != nil { + // copy unsuccessful + errRecover = os.Rename(oldPath, updatePath) + } else { + // copy successful, remove the old binary + errRemove := os.Remove(oldPath) + + // windows has trouble with removing old binaries, so hide it instead + if errRemove != nil { + _ = hideFile(oldPath) + } + } + + return +} + +// CanUpdate() determines whether the process has the correct permissions to +// perform the requested update. If the update can proceed, it returns nil, otherwise +// it returns the error that would occur if an update were attempted. +func (u *Update) CanUpdate() (err error) { + // get the directory the file exists in + path, err := u.getPath() + if err != nil { + return + } + + fileDir := filepath.Dir(path) + fileName := filepath.Base(path) + + // attempt to open a file in the file's directory + newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.new", fileName)) + fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0755) + if err != nil { + return + } + fp.Close() + + _ = os.Remove(newPath) + return +} + +func applyPatch(patch io.Reader, updatePath string) ([]byte, error) { + // open the file to update + old, err := os.Open(updatePath) + if err != nil { + return nil, err + } + defer old.Close() + + // apply the patch + applied := new(bytes.Buffer) + if err = binarydist.Patch(old, applied, patch); err != nil { + return nil, err + } + + return applied.Bytes(), nil +} + +func verifyChecksum(updated []byte, expectedChecksum []byte) error { + checksum, err := ChecksumForBytes(updated) + if err != nil { + return err + } + + if !bytes.Equal(expectedChecksum, checksum) { + return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", expectedChecksum, checksum) + } + + return nil +} + +// ChecksumForFile returns the sha256 checksum for the given file +func ChecksumForFile(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + + return ChecksumForReader(f) +} + +// ChecksumForReader returns the sha256 checksum for the entire +// contents of the given reader. +func ChecksumForReader(rd io.Reader) ([]byte, error) { + h := sha256.New() + if _, err := io.Copy(h, rd); err != nil { + return nil, err + } + return h.Sum(nil), nil +} + +// ChecksumForBytes returns the sha256 checksum for the given bytes +func ChecksumForBytes(source []byte) ([]byte, error) { + return ChecksumForReader(bytes.NewReader(source)) +} + +func verifySignature(source, signature []byte, publicKey *rsa.PublicKey) error { + checksum, err := ChecksumForBytes(source) + if err != nil { + return err + } + + return rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, checksum, signature) +} diff --git a/Godeps/_workspace/src/github.com/inconshreveable/go-update/update_test.go b/Godeps/_workspace/src/github.com/inconshreveable/go-update/update_test.go new file mode 100644 index 00000000000..ba6611da0db --- /dev/null +++ b/Godeps/_workspace/src/github.com/inconshreveable/go-update/update_test.go @@ -0,0 +1,380 @@ +package update + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/kr/binarydist" + "io/ioutil" + "net" + "net/http" + "os" + "testing" +) + +var ( + oldFile = []byte{0xDE, 0xAD, 0xBE, 0xEF} + newFile = []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06} +) + +func cleanup(path string) { + os.Remove(path) +} + +// we write with a separate name for each test so that we can run them in parallel +func writeOldFile(path string, t *testing.T) { + if err := ioutil.WriteFile(path, oldFile, 0777); err != nil { + t.Fatalf("Failed to write file for testing preparation: %v", err) + } +} + +func validateUpdate(path string, err error, t *testing.T) { + if err != nil { + t.Fatalf("Failed to update: %v", err) + } + + buf, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("Failed to read file post-update: %v", err) + } + + if !bytes.Equal(buf, newFile) { + t.Fatalf("File was not updated! Bytes read: %v, Bytes expected: %v", buf, newFile) + } +} + +func TestFromStream(t *testing.T) { + t.Parallel() + + fName := "TestFromStream" + defer cleanup(fName) + writeOldFile(fName, t) + + err, _ := New().Target(fName).FromStream(bytes.NewReader(newFile)) + validateUpdate(fName, err, t) +} + +func TestFromFile(t *testing.T) { + t.Parallel() + + fName := "TestFromFile" + newFName := "NewTestFromFile" + defer cleanup(fName) + defer cleanup(newFName) + writeOldFile(fName, t) + + if err := ioutil.WriteFile(newFName, newFile, 0777); err != nil { + t.Fatalf("Failed to write file to update from: %v", err) + } + + err, _ := New().Target(fName).FromFile(newFName) + validateUpdate(fName, err, t) +} + +func TestFromUrl(t *testing.T) { + t.Parallel() + + fName := "TestFromUrl" + defer cleanup(fName) + writeOldFile(fName, t) + + l, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("Couldn't bind listener: %v", err) + } + addr := l.Addr().String() + + go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(newFile) + })) + + err, _ = New().Target(fName).FromUrl("http://" + addr) + validateUpdate(fName, err, t) +} + +func TestVerifyChecksum(t *testing.T) { + t.Parallel() + + fName := "TestVerifyChecksum" + defer cleanup(fName) + writeOldFile(fName, t) + + checksum, err := ChecksumForBytes(newFile) + if err != nil { + t.Fatalf("Failed to compute checksum: %v", err) + } + + err, _ = New().Target(fName).VerifyChecksum(checksum).FromStream(bytes.NewReader(newFile)) + validateUpdate(fName, err, t) +} + +func TestVerifyChecksumNegative(t *testing.T) { + t.Parallel() + + fName := "TestVerifyChecksumNegative" + defer cleanup(fName) + writeOldFile(fName, t) + + badChecksum := []byte{0x0A, 0x0B, 0x0C, 0xFF} + err, _ := New().Target(fName).VerifyChecksum(badChecksum).FromStream(bytes.NewReader(newFile)) + if err == nil { + t.Fatalf("Failed to detect bad checksum!") + } +} + +func TestApplyPatch(t *testing.T) { + t.Parallel() + + fName := "TestApplyPatch" + defer cleanup(fName) + writeOldFile(fName, t) + + patch := new(bytes.Buffer) + err := binarydist.Diff(bytes.NewReader(oldFile), bytes.NewReader(newFile), patch) + if err != nil { + t.Fatalf("Failed to create patch: %v", err) + } + + up := New().Target(fName).ApplyPatch(PATCHTYPE_BSDIFF) + err, _ = up.FromStream(bytes.NewReader(patch.Bytes())) + validateUpdate(fName, err, t) +} + +func TestCorruptPatch(t *testing.T) { + t.Parallel() + + fName := "TestCorruptPatch" + defer cleanup(fName) + writeOldFile(fName, t) + + badPatch := []byte{0x44, 0x38, 0x86, 0x3c, 0x4f, 0x8d, 0x26, 0x54, 0xb, 0x11, 0xce, 0xfe, 0xc1, 0xc0, 0xf8, 0x31, 0x38, 0xa0, 0x12, 0x1a, 0xa2, 0x57, 0x2a, 0xe1, 0x3a, 0x48, 0x62, 0x40, 0x2b, 0x81, 0x12, 0xb1, 0x21, 0xa5, 0x16, 0xed, 0x73, 0xd6, 0x54, 0x84, 0x29, 0xa6, 0xd6, 0xb2, 0x1b, 0xfb, 0xe6, 0xbe, 0x7b, 0x70} + up := New().Target(fName).ApplyPatch(PATCHTYPE_BSDIFF) + err, _ := up.FromStream(bytes.NewReader(badPatch)) + if err == nil { + t.Fatalf("Failed to detect corrupt patch!") + } +} + +func TestVerifyChecksumPatchNegative(t *testing.T) { + t.Parallel() + + fName := "TestVerifyChecksumPatchNegative" + defer cleanup(fName) + writeOldFile(fName, t) + + checksum, err := ChecksumForBytes(newFile) + if err != nil { + t.Fatalf("Failed to compute checksum: %v", err) + } + + patch := new(bytes.Buffer) + anotherFile := []byte{0x77, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66} + err = binarydist.Diff(bytes.NewReader(oldFile), bytes.NewReader(anotherFile), patch) + if err != nil { + t.Fatalf("Failed to create patch: %v", err) + } + + up := New().Target(fName).ApplyPatch(PATCHTYPE_BSDIFF).VerifyChecksum(checksum) + err, _ = up.FromStream(bytes.NewReader(patch.Bytes())) + if err == nil { + t.Fatalf("Failed to detect patch to wrong file!") + } +} + +const publicKey = `-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxSWmu7trWKAwDFjiCN2D +Tk2jj2sgcr/CMlI4cSSiIOHrXCFxP1I8i9PvQkd4hasXQrLbT5WXKrRGv1HKUKab +b9ead+kD0kxk7i2bFYvKX43oq66IW0mOLTQBO7I9UyT4L7svcMD+HUQ2BqHoaQe4 +y20C59dPr9Dpcz8DZkdLsBV6YKF6Ieb3iGk8oRLMWNaUqPa8f1BGgxAkvPHcqDjT +x4xRnjgTRRRlZvRtALHMUkIChgxDOhoEzKpGiqnX7HtMJfrhV6h0PAXNA4h9Kjv5 +5fhJ08Rz7mmZmtH5JxTK5XTquo59sihSajR4bSjZbbkQ1uLkeFlY3eli3xdQ7Nrf +fQIDAQAB +-----END PUBLIC KEY-----` + +const privateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAxSWmu7trWKAwDFjiCN2DTk2jj2sgcr/CMlI4cSSiIOHrXCFx +P1I8i9PvQkd4hasXQrLbT5WXKrRGv1HKUKabb9ead+kD0kxk7i2bFYvKX43oq66I +W0mOLTQBO7I9UyT4L7svcMD+HUQ2BqHoaQe4y20C59dPr9Dpcz8DZkdLsBV6YKF6 +Ieb3iGk8oRLMWNaUqPa8f1BGgxAkvPHcqDjTx4xRnjgTRRRlZvRtALHMUkIChgxD +OhoEzKpGiqnX7HtMJfrhV6h0PAXNA4h9Kjv55fhJ08Rz7mmZmtH5JxTK5XTquo59 +sihSajR4bSjZbbkQ1uLkeFlY3eli3xdQ7NrffQIDAQABAoIBAAkN+6RvrTR61voa +Mvd5RQiZpEN4Bht/Fyo8gH8h0Zh1B9xJZOwlmMZLS5fdtHlfLEhR8qSrGDBL61vq +I8KkhEsUufF78EL+YzxVN+Q7cWYGHIOWFokqza7hzpSxUQO6lPOMQ1eIZaNueJTB +Zu07/47ISPPg/bXzgGVcpYlTCPTjUwKjtfyMqvX9AD7fIyYRm6zfE7EHj1J2sBFt +Yz1OGELg6HfJwXfpnPfBvftD0hWGzJ78Bp71fPJe6n5gnqmSqRvrcXNWFnH/yqkN +d6vPIxD6Z3LjvyZpkA7JillLva2L/zcIFhg4HZvQnWd8/PpDnUDonu36hcj4SC5j +W4aVPLkCgYEA4XzNKWxqYcajzFGZeSxlRHupSAl2MT7Cc5085MmE7dd31wK2T8O4 +n7N4bkm/rjTbX85NsfWdKtWb6mpp8W3VlLP0rp4a/12OicVOkg4pv9LZDmY0sRlE +YuDJk1FeCZ50UrwTZI3rZ9IhZHhkgVA6uWAs7tYndONkxNHG0pjqs4sCgYEA39MZ +JwMqo3qsPntpgP940cCLflEsjS9hYNO3+Sv8Dq3P0HLVhBYajJnotf8VuU0fsQZG +grmtVn1yThFbMq7X1oY4F0XBA+paSiU18c4YyUnwax2u4sw9U/Q9tmQUZad5+ueT +qriMBwGv+ewO+nQxqvAsMUmemrVzrfwA5Oct+hcCgYAfiyXoNZJsOy2O15twqBVC +j0oPGcO+/9iT89sg5lACNbI+EdMPNYIOVTzzsL1v0VUfAe08h++Enn1BPcG0VHkc +ZFBGXTfJoXzfKQrkw7ZzbzuOGB4m6DH44xlP0oIlNlVvfX/5ASF9VJf3RiBJNsAA +TsP6ZVr/rw/ZuL7nlxy+IQKBgDhL/HOXlE3yOQiuOec8WsNHTs7C1BXe6PtVxVxi +988pYK/pclL6zEq5G5NLSceF4obAMVQIJ9UtUGbabrncyGUo9UrFPLsjYvprSZo8 +YHegpVwL50UcYgCP2kXZ/ldjPIcjYDz8lhvdDMor2cidGTEJn9P11HLNWP9V91Ob +4jCZAoGAPNRSC5cC8iP/9j+s2/kdkfWJiNaolPYAUrmrkL6H39PYYZM5tnhaIYJV +Oh9AgABamU0eb3p3vXTISClVgV7ifq1HyZ7BSUhMfaY2Jk/s3sUHCWFxPZe9sgEG +KinIY/373KIkIV/5g4h2v1w330IWcfptxKcY/Er3DJr38f695GE= +-----END RSA PRIVATE KEY-----` + +func sign(privatePEM string, source []byte, t *testing.T) []byte { + block, _ := pem.Decode([]byte(privatePEM)) + if block == nil { + t.Fatalf("Failed to parse private key PEM") + } + + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + t.Fatalf("Failed to parse private key DER") + } + + checksum, err := ChecksumForBytes(source) + if err != nil { + t.Fatalf("Failed to make checksum") + } + + sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, checksum) + if err != nil { + t.Fatalf("Failed to sign: %v", sig) + } + + return sig +} + +func TestVerifySignature(t *testing.T) { + t.Parallel() + + fName := "TestVerifySignature" + defer cleanup(fName) + writeOldFile(fName, t) + + up, err := New().Target(fName).VerifySignatureWithPEM([]byte(publicKey)) + if err != nil { + t.Fatalf("Could not parse public key: %v", err) + } + + signature := sign(privateKey, newFile, t) + err, _ = up.VerifySignature(signature).FromStream(bytes.NewReader(newFile)) + validateUpdate(fName, err, t) +} + +func TestVerifyFailBadSignature(t *testing.T) { + t.Parallel() + + fName := "TestVerifyFailBadSignature" + defer cleanup(fName) + writeOldFile(fName, t) + + up, err := New().Target(fName).VerifySignatureWithPEM([]byte(publicKey)) + if err != nil { + t.Fatalf("Could not parse public key: %v", err) + } + + badSig := []byte{0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA} + err, _ = up.VerifySignature(badSig).FromStream(bytes.NewReader(newFile)) + if err == nil { + t.Fatalf("Did not fail with bad signature") + } +} + +func TestVerifyFailNoSignature(t *testing.T) { + t.Parallel() + + fName := "TestVerifySignatureWithPEM" + defer cleanup(fName) + writeOldFile(fName, t) + + up, err := New().Target(fName).VerifySignatureWithPEM([]byte(publicKey)) + if err != nil { + t.Fatalf("Could not parse public key: %v", err) + } + + err, _ = up.VerifySignature([]byte{}).FromStream(bytes.NewReader(newFile)) + if err == nil { + t.Fatalf("Did not fail with empty signature") + } +} + +const wrongKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArKqjT+xOFJILe0CX7lKfQy52YwWLF9devYtLeUHTbPOueGLy +6CjrXJBrWIxNBxRd53y4dtgiMqCX6Gmmvuy8HnfbBuJjR2mcdEYo8UDy+aSVBQ6T +/ND7Fd7KSzOruEFFzl2QFnZ/SrW/nsXdGyuF8l+YIwjZJRyV6StZkZ4ydOzOqUk9 +FXTeIkhX/Q7/jTETw7L3wxMyLgJAlV3lxDsPkMjxymngtbAIjwEjLsVeU+Prcz2e +Ww34SZQ8qwzAdXieuDPryMwEsCcgc5NAKJFNL8TppYGDXOHI7CRXqHfNiJq2R+kQ +LdxRvmfx8/iu4xM2hBzk4uSDS6TTn2AnWBm+cQIDAQABAoIBAFp//aUwaCRj/9yU +GI3zhEJEIgz4pNTUL3YNgnuFwvlCJ9o1kreYavRTRdBdiSoCxM1GE7FGy3XZsoVA +iwNbNaaKj6RmGD8f3b8b3u3EaxXp66mA4JQMPO5TnZgY9xJWM+5cH9+GMGXKKStg +7ekFwOkuraD/TEElYHWcIRAv6KZbc/YOIa6YDKi+1Gc7u0MeIvwqN7nwaBAoJKUE +ZrJIfYKIViD/ZrCpgWN47C9x8w3ne7iiDrYoYct+0reC9LFlqwVBtDnyVx/q3upW +zzczbNQagu3w0QgprDGhy0ZhDNxuylV3XBWTB+xBrFQgz6rD3LzUPywlbt0N7ZmD +936MVSECgYEA1IElCahF/+hC/OxFgy98DubAUDGmrvxWeZF3bvTseWZQp/gzxVS+ +SYumYyd2Ysx5+UjXQlVgR6BbDG13+DpSpZm6+MeWHBAR+KA2qCg009SDFv7l26/d +xMT7lvIWz7ckQDb/+jvhF9HL2llyTN1Zex+n3XBeAMKNrPaubdEBFsUCgYEA0AIO +tZMtzOpioAR1lGbwIguq04msDdrJNaY2TKrLeviJuQUw94fgL+3ULAPsiyxaU/Gv +vln11R7aIp1SJ09T2UoFRbty+6SGRC56+Wh0pn5VnAi7aT6qdkYWhEjhqRHuXosf +PYboXBuMwA0FBUTxWQL/lux2PZgvBkniYh5jI70CgYEAk9KmhhpFX2gdOT3OeRxO +CzufaemwDqfAK97yGwBLg4OV9dJliQ6TNCvt+amY489jxfJSs3UafZjh3TpFKyq/ +FS1kb+y+0hSnu7EPdFhLr1N0QUndcb3b4iY48V7EWYgHspfP5y1CPsSVLvXr2eZc +eZaiuhqReavczAXpfsDWJhUCgYEAwmUp2gfyhc+G3IVOXaLWSPseaxP+9/PAl6L+ +nCgCgqpEC+YOHUee/SwHXhtMtcR9pnX5CKyKUuLCehcM8C/y7N+AjerhSsw3rwDB +bNVyLydiWrDOdU1bga1+3aI/QwK/AxyB1b5+6ZXVtKZ2SrZj2Aw1UZcr6eSQDhB+ +wbQkcwECgYBF13FMA6OOon992t9H3I+4KDgmz6G6mz3bVXSoFWfO1p/yXP04BzJl +jtLFvFVTZdMs2o/wTd4SL6gYjx9mlOWwM8FblmjfiNSUVIyye33fRntEAr1n+FYI +Xhv6aVnNdaGehGIqQxXFoGyiJxG3RYNkSwaTOamxY1V+ceLuO26n2Q== +-----END RSA PRIVATE KEY-----` + +func TestVerifyFailWrongSignature(t *testing.T) { + t.Parallel() + + fName := "TestVerifyFailWrongSignature" + defer cleanup(fName) + writeOldFile(fName, t) + + up, err := New().Target(fName).VerifySignatureWithPEM([]byte(publicKey)) + if err != nil { + t.Fatalf("Could not parse public key: %v", err) + } + + signature := sign(wrongKey, newFile, t) + err, _ = up.VerifySignature(signature).FromStream(bytes.NewReader(newFile)) + if err == nil { + t.Fatalf("Verified an update that was signed by an untrusted key!") + } +} + +func TestSignatureButNoPublicKey(t *testing.T) { + t.Parallel() + + fName := "TestSignatureButNoPublicKey" + defer cleanup(fName) + writeOldFile(fName, t) + + sig := sign(privateKey, newFile, t) + err, _ := New().Target(fName).VerifySignature(sig).FromStream(bytes.NewReader(newFile)) + if err == nil { + t.Fatalf("Allowed an update with a signautre verification when no public key was specified!") + } +} + +func TestPublicKeyButNoSignature(t *testing.T) { + t.Parallel() + + fName := "TestPublicKeyButNoSignature" + defer cleanup(fName) + writeOldFile(fName, t) + + up, err := New().Target(fName).VerifySignatureWithPEM([]byte(publicKey)) + if err != nil { + t.Fatalf("Could not parse public key: %v", err) + } + + err, _ = up.FromStream(bytes.NewReader(newFile)) + if err == nil { + t.Fatalf("Allowed an update with no signautre when a public key was specified!") + } +} diff --git a/Godeps/_workspace/src/github.com/jbenet/go-multihash/LICENSE b/Godeps/_workspace/src/github.com/jbenet/go-multihash/LICENSE new file mode 100644 index 00000000000..c7386b3c940 --- /dev/null +++ b/Godeps/_workspace/src/github.com/jbenet/go-multihash/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Juan Batiz-Benet + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/.gitignore b/Godeps/_workspace/src/github.com/kr/binarydist/.gitignore new file mode 100644 index 00000000000..653f1601457 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/.gitignore @@ -0,0 +1 @@ +test.* diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/License b/Godeps/_workspace/src/github.com/kr/binarydist/License new file mode 100644 index 00000000000..183c3898c36 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/License @@ -0,0 +1,22 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/Readme.md b/Godeps/_workspace/src/github.com/kr/binarydist/Readme.md new file mode 100644 index 00000000000..dadc3683de5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/Readme.md @@ -0,0 +1,7 @@ +# binarydist + +Package binarydist implements binary diff and patch as described on +. It reads and writes files +compatible with the tools there. + +Documentation at . diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/bzip2.go b/Godeps/_workspace/src/github.com/kr/binarydist/bzip2.go new file mode 100644 index 00000000000..a2516b81df2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/bzip2.go @@ -0,0 +1,40 @@ +package binarydist + +import ( + "io" + "os/exec" +) + +type bzip2Writer struct { + c *exec.Cmd + w io.WriteCloser +} + +func (w bzip2Writer) Write(b []byte) (int, error) { + return w.w.Write(b) +} + +func (w bzip2Writer) Close() error { + if err := w.w.Close(); err != nil { + return err + } + return w.c.Wait() +} + +// Package compress/bzip2 implements only decompression, +// so we'll fake it by running bzip2 in another process. +func newBzip2Writer(w io.Writer) (wc io.WriteCloser, err error) { + var bw bzip2Writer + bw.c = exec.Command("bzip2", "-c") + bw.c.Stdout = w + + if bw.w, err = bw.c.StdinPipe(); err != nil { + return nil, err + } + + if err = bw.c.Start(); err != nil { + return nil, err + } + + return bw, nil +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/common_test.go b/Godeps/_workspace/src/github.com/kr/binarydist/common_test.go new file mode 100644 index 00000000000..af5161668d7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/common_test.go @@ -0,0 +1,93 @@ +package binarydist + +import ( + "crypto/rand" + "io" + "io/ioutil" + "os" +) + +func mustOpen(path string) *os.File { + f, err := os.Open(path) + if err != nil { + panic(err) + } + + return f +} + +func mustReadAll(r io.Reader) []byte { + b, err := ioutil.ReadAll(r) + if err != nil { + panic(err) + } + return b +} + +func fileCmp(a, b *os.File) int64 { + sa, err := a.Seek(0, 2) + if err != nil { + panic(err) + } + + sb, err := b.Seek(0, 2) + if err != nil { + panic(err) + } + + if sa != sb { + return sa + } + + _, err = a.Seek(0, 0) + if err != nil { + panic(err) + } + + _, err = b.Seek(0, 0) + if err != nil { + panic(err) + } + + pa, err := ioutil.ReadAll(a) + if err != nil { + panic(err) + } + + pb, err := ioutil.ReadAll(b) + if err != nil { + panic(err) + } + + for i := range pa { + if pa[i] != pb[i] { + return int64(i) + } + } + return -1 +} + +func mustWriteRandFile(path string, size int) *os.File { + p := make([]byte, size) + _, err := rand.Read(p) + if err != nil { + panic(err) + } + + f, err := os.Create(path) + if err != nil { + panic(err) + } + + _, err = f.Write(p) + if err != nil { + panic(err) + } + + _, err = f.Seek(0, 0) + if err != nil { + panic(err) + } + + return f +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/diff.go b/Godeps/_workspace/src/github.com/kr/binarydist/diff.go new file mode 100644 index 00000000000..1d2d951bb4d --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/diff.go @@ -0,0 +1,408 @@ +package binarydist + +import ( + "bytes" + "encoding/binary" + "io" + "io/ioutil" +) + +func swap(a []int, i, j int) { a[i], a[j] = a[j], a[i] } + +func split(I, V []int, start, length, h int) { + var i, j, k, x, jj, kk int + + if length < 16 { + for k = start; k < start+length; k += j { + j = 1 + x = V[I[k]+h] + for i = 1; k+i < start+length; i++ { + if V[I[k+i]+h] < x { + x = V[I[k+i]+h] + j = 0 + } + if V[I[k+i]+h] == x { + swap(I, k+i, k+j) + j++ + } + } + for i = 0; i < j; i++ { + V[I[k+i]] = k + j - 1 + } + if j == 1 { + I[k] = -1 + } + } + return + } + + x = V[I[start+length/2]+h] + jj = 0 + kk = 0 + for i = start; i < start+length; i++ { + if V[I[i]+h] < x { + jj++ + } + if V[I[i]+h] == x { + kk++ + } + } + jj += start + kk += jj + + i = start + j = 0 + k = 0 + for i < jj { + if V[I[i]+h] < x { + i++ + } else if V[I[i]+h] == x { + swap(I, i, jj+j) + j++ + } else { + swap(I, i, kk+k) + k++ + } + } + + for jj+j < kk { + if V[I[jj+j]+h] == x { + j++ + } else { + swap(I, jj+j, kk+k) + k++ + } + } + + if jj > start { + split(I, V, start, jj-start, h) + } + + for i = 0; i < kk-jj; i++ { + V[I[jj+i]] = kk - 1 + } + if jj == kk-1 { + I[jj] = -1 + } + + if start+length > kk { + split(I, V, kk, start+length-kk, h) + } +} + +func qsufsort(obuf []byte) []int { + var buckets [256]int + var i, h int + I := make([]int, len(obuf)+1) + V := make([]int, len(obuf)+1) + + for _, c := range obuf { + buckets[c]++ + } + for i = 1; i < 256; i++ { + buckets[i] += buckets[i-1] + } + copy(buckets[1:], buckets[:]) + buckets[0] = 0 + + for i, c := range obuf { + buckets[c]++ + I[buckets[c]] = i + } + + I[0] = len(obuf) + for i, c := range obuf { + V[i] = buckets[c] + } + + V[len(obuf)] = 0 + for i = 1; i < 256; i++ { + if buckets[i] == buckets[i-1]+1 { + I[buckets[i]] = -1 + } + } + I[0] = -1 + + for h = 1; I[0] != -(len(obuf) + 1); h += h { + var n int + for i = 0; i < len(obuf)+1; { + if I[i] < 0 { + n -= I[i] + i -= I[i] + } else { + if n != 0 { + I[i-n] = -n + } + n = V[I[i]] + 1 - i + split(I, V, i, n, h) + i += n + n = 0 + } + } + if n != 0 { + I[i-n] = -n + } + } + + for i = 0; i < len(obuf)+1; i++ { + I[V[i]] = i + } + return I +} + +func matchlen(a, b []byte) (i int) { + for i < len(a) && i < len(b) && a[i] == b[i] { + i++ + } + return i +} + +func search(I []int, obuf, nbuf []byte, st, en int) (pos, n int) { + if en-st < 2 { + x := matchlen(obuf[I[st]:], nbuf) + y := matchlen(obuf[I[en]:], nbuf) + + if x > y { + return I[st], x + } else { + return I[en], y + } + } + + x := st + (en-st)/2 + if bytes.Compare(obuf[I[x]:], nbuf) < 0 { + return search(I, obuf, nbuf, x, en) + } else { + return search(I, obuf, nbuf, st, x) + } + panic("unreached") +} + +// Diff computes the difference between old and new, according to the bsdiff +// algorithm, and writes the result to patch. +func Diff(old, new io.Reader, patch io.Writer) error { + obuf, err := ioutil.ReadAll(old) + if err != nil { + return err + } + + nbuf, err := ioutil.ReadAll(new) + if err != nil { + return err + } + + pbuf, err := diffBytes(obuf, nbuf) + if err != nil { + return err + } + + _, err = patch.Write(pbuf) + return err +} + +func diffBytes(obuf, nbuf []byte) ([]byte, error) { + var patch seekBuffer + err := diff(obuf, nbuf, &patch) + if err != nil { + return nil, err + } + return patch.buf, nil +} + +func diff(obuf, nbuf []byte, patch io.WriteSeeker) error { + var lenf int + I := qsufsort(obuf) + db := make([]byte, len(nbuf)) + eb := make([]byte, len(nbuf)) + var dblen, eblen int + + var hdr header + hdr.Magic = magic + hdr.NewSize = int64(len(nbuf)) + err := binary.Write(patch, signMagLittleEndian{}, &hdr) + if err != nil { + return err + } + + // Compute the differences, writing ctrl as we go + pfbz2, err := newBzip2Writer(patch) + if err != nil { + return err + } + var scan, pos, length int + var lastscan, lastpos, lastoffset int + for scan < len(nbuf) { + var oldscore int + scan += length + for scsc := scan; scan < len(nbuf); scan++ { + pos, length = search(I, obuf, nbuf[scan:], 0, len(obuf)) + + for ; scsc < scan+length; scsc++ { + if scsc+lastoffset < len(obuf) && + obuf[scsc+lastoffset] == nbuf[scsc] { + oldscore++ + } + } + + if (length == oldscore && length != 0) || length > oldscore+8 { + break + } + + if scan+lastoffset < len(obuf) && obuf[scan+lastoffset] == nbuf[scan] { + oldscore-- + } + } + + if length != oldscore || scan == len(nbuf) { + var s, Sf int + lenf = 0 + for i := 0; lastscan+i < scan && lastpos+i < len(obuf); { + if obuf[lastpos+i] == nbuf[lastscan+i] { + s++ + } + i++ + if s*2-i > Sf*2-lenf { + Sf = s + lenf = i + } + } + + lenb := 0 + if scan < len(nbuf) { + var s, Sb int + for i := 1; (scan >= lastscan+i) && (pos >= i); i++ { + if obuf[pos-i] == nbuf[scan-i] { + s++ + } + if s*2-i > Sb*2-lenb { + Sb = s + lenb = i + } + } + } + + if lastscan+lenf > scan-lenb { + overlap := (lastscan + lenf) - (scan - lenb) + s := 0 + Ss := 0 + lens := 0 + for i := 0; i < overlap; i++ { + if nbuf[lastscan+lenf-overlap+i] == obuf[lastpos+lenf-overlap+i] { + s++ + } + if nbuf[scan-lenb+i] == obuf[pos-lenb+i] { + s-- + } + if s > Ss { + Ss = s + lens = i + 1 + } + } + + lenf += lens - overlap + lenb -= lens + } + + for i := 0; i < lenf; i++ { + db[dblen+i] = nbuf[lastscan+i] - obuf[lastpos+i] + } + for i := 0; i < (scan-lenb)-(lastscan+lenf); i++ { + eb[eblen+i] = nbuf[lastscan+lenf+i] + } + + dblen += lenf + eblen += (scan - lenb) - (lastscan + lenf) + + err = binary.Write(pfbz2, signMagLittleEndian{}, int64(lenf)) + if err != nil { + pfbz2.Close() + return err + } + + val := (scan - lenb) - (lastscan + lenf) + err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val)) + if err != nil { + pfbz2.Close() + return err + } + + val = (pos - lenb) - (lastpos + lenf) + err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val)) + if err != nil { + pfbz2.Close() + return err + } + + lastscan = scan - lenb + lastpos = pos - lenb + lastoffset = pos - scan + } + } + err = pfbz2.Close() + if err != nil { + return err + } + + // Compute size of compressed ctrl data + l64, err := patch.Seek(0, 1) + if err != nil { + return err + } + hdr.CtrlLen = int64(l64 - 32) + + // Write compressed diff data + pfbz2, err = newBzip2Writer(patch) + if err != nil { + return err + } + n, err := pfbz2.Write(db[:dblen]) + if err != nil { + pfbz2.Close() + return err + } + if n != dblen { + pfbz2.Close() + return io.ErrShortWrite + } + err = pfbz2.Close() + if err != nil { + return err + } + + // Compute size of compressed diff data + n64, err := patch.Seek(0, 1) + if err != nil { + return err + } + hdr.DiffLen = n64 - l64 + + // Write compressed extra data + pfbz2, err = newBzip2Writer(patch) + if err != nil { + return err + } + n, err = pfbz2.Write(eb[:eblen]) + if err != nil { + pfbz2.Close() + return err + } + if n != eblen { + pfbz2.Close() + return io.ErrShortWrite + } + err = pfbz2.Close() + if err != nil { + return err + } + + // Seek to the beginning, write the header, and close the file + _, err = patch.Seek(0, 0) + if err != nil { + return err + } + err = binary.Write(patch, signMagLittleEndian{}, &hdr) + if err != nil { + return err + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/diff_test.go b/Godeps/_workspace/src/github.com/kr/binarydist/diff_test.go new file mode 100644 index 00000000000..9baa4926d8c --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/diff_test.go @@ -0,0 +1,67 @@ +package binarydist + +import ( + "bytes" + "io/ioutil" + "os" + "os/exec" + "testing" +) + +var diffT = []struct { + old *os.File + new *os.File +}{ + { + old: mustWriteRandFile("test.old", 1e3), + new: mustWriteRandFile("test.new", 1e3), + }, + { + old: mustOpen("testdata/sample.old"), + new: mustOpen("testdata/sample.new"), + }, +} + +func TestDiff(t *testing.T) { + for _, s := range diffT { + got, err := ioutil.TempFile("/tmp", "bspatch.") + if err != nil { + panic(err) + } + os.Remove(got.Name()) + + exp, err := ioutil.TempFile("/tmp", "bspatch.") + if err != nil { + panic(err) + } + + cmd := exec.Command("bsdiff", s.old.Name(), s.new.Name(), exp.Name()) + cmd.Stdout = os.Stdout + err = cmd.Run() + os.Remove(exp.Name()) + if err != nil { + panic(err) + } + + err = Diff(s.old, s.new, got) + if err != nil { + t.Fatal("err", err) + } + + _, err = got.Seek(0, 0) + if err != nil { + panic(err) + } + gotBuf := mustReadAll(got) + expBuf := mustReadAll(exp) + + if !bytes.Equal(gotBuf, expBuf) { + t.Fail() + t.Logf("diff %s %s", s.old.Name(), s.new.Name()) + t.Logf("%s: len(got) = %d", got.Name(), len(gotBuf)) + t.Logf("%s: len(exp) = %d", exp.Name(), len(expBuf)) + i := matchlen(gotBuf, expBuf) + t.Logf("produced different output at pos %d; %d != %d", i, gotBuf[i], expBuf[i]) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/doc.go b/Godeps/_workspace/src/github.com/kr/binarydist/doc.go new file mode 100644 index 00000000000..3c92d875005 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/doc.go @@ -0,0 +1,24 @@ +// Package binarydist implements binary diff and patch as described on +// http://www.daemonology.net/bsdiff/. It reads and writes files +// compatible with the tools there. +package binarydist + +var magic = [8]byte{'B', 'S', 'D', 'I', 'F', 'F', '4', '0'} + +// File format: +// 0 8 "BSDIFF40" +// 8 8 X +// 16 8 Y +// 24 8 sizeof(newfile) +// 32 X bzip2(control block) +// 32+X Y bzip2(diff block) +// 32+X+Y ??? bzip2(extra block) +// with control block a set of triples (x,y,z) meaning "add x bytes +// from oldfile to x bytes from the diff block; copy y bytes from the +// extra block; seek forwards in oldfile by z bytes". +type header struct { + Magic [8]byte + CtrlLen int64 + DiffLen int64 + NewSize int64 +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/encoding.go b/Godeps/_workspace/src/github.com/kr/binarydist/encoding.go new file mode 100644 index 00000000000..75ba5856a62 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/encoding.go @@ -0,0 +1,53 @@ +package binarydist + +// SignMagLittleEndian is the numeric encoding used by the bsdiff tools. +// It implements binary.ByteOrder using a sign-magnitude format +// and little-endian byte order. Only methods Uint64 and String +// have been written; the rest panic. +type signMagLittleEndian struct{} + +func (signMagLittleEndian) Uint16(b []byte) uint16 { panic("unimplemented") } + +func (signMagLittleEndian) PutUint16(b []byte, v uint16) { panic("unimplemented") } + +func (signMagLittleEndian) Uint32(b []byte) uint32 { panic("unimplemented") } + +func (signMagLittleEndian) PutUint32(b []byte, v uint32) { panic("unimplemented") } + +func (signMagLittleEndian) Uint64(b []byte) uint64 { + y := int64(b[0]) | + int64(b[1])<<8 | + int64(b[2])<<16 | + int64(b[3])<<24 | + int64(b[4])<<32 | + int64(b[5])<<40 | + int64(b[6])<<48 | + int64(b[7]&0x7f)<<56 + + if b[7]&0x80 != 0 { + y = -y + } + return uint64(y) +} + +func (signMagLittleEndian) PutUint64(b []byte, v uint64) { + x := int64(v) + neg := x < 0 + if neg { + x = -x + } + + b[0] = byte(x) + b[1] = byte(x >> 8) + b[2] = byte(x >> 16) + b[3] = byte(x >> 24) + b[4] = byte(x >> 32) + b[5] = byte(x >> 40) + b[6] = byte(x >> 48) + b[7] = byte(x >> 56) + if neg { + b[7] |= 0x80 + } +} + +func (signMagLittleEndian) String() string { return "signMagLittleEndian" } diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/patch.go b/Godeps/_workspace/src/github.com/kr/binarydist/patch.go new file mode 100644 index 00000000000..eb0322578be --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/patch.go @@ -0,0 +1,109 @@ +package binarydist + +import ( + "bytes" + "compress/bzip2" + "encoding/binary" + "errors" + "io" + "io/ioutil" +) + +var ErrCorrupt = errors.New("corrupt patch") + +// Patch applies patch to old, according to the bspatch algorithm, +// and writes the result to new. +func Patch(old io.Reader, new io.Writer, patch io.Reader) error { + var hdr header + err := binary.Read(patch, signMagLittleEndian{}, &hdr) + if err != nil { + return err + } + if hdr.Magic != magic { + return ErrCorrupt + } + if hdr.CtrlLen < 0 || hdr.DiffLen < 0 || hdr.NewSize < 0 { + return ErrCorrupt + } + + ctrlbuf := make([]byte, hdr.CtrlLen) + _, err = io.ReadFull(patch, ctrlbuf) + if err != nil { + return err + } + cpfbz2 := bzip2.NewReader(bytes.NewReader(ctrlbuf)) + + diffbuf := make([]byte, hdr.DiffLen) + _, err = io.ReadFull(patch, diffbuf) + if err != nil { + return err + } + dpfbz2 := bzip2.NewReader(bytes.NewReader(diffbuf)) + + // The entire rest of the file is the extra block. + epfbz2 := bzip2.NewReader(patch) + + obuf, err := ioutil.ReadAll(old) + if err != nil { + return err + } + + nbuf := make([]byte, hdr.NewSize) + + var oldpos, newpos int64 + for newpos < hdr.NewSize { + var ctrl struct{ Add, Copy, Seek int64 } + err = binary.Read(cpfbz2, signMagLittleEndian{}, &ctrl) + if err != nil { + return err + } + + // Sanity-check + if newpos+ctrl.Add > hdr.NewSize { + return ErrCorrupt + } + + // Read diff string + _, err = io.ReadFull(dpfbz2, nbuf[newpos:newpos+ctrl.Add]) + if err != nil { + return ErrCorrupt + } + + // Add old data to diff string + for i := int64(0); i < ctrl.Add; i++ { + if oldpos+i >= 0 && oldpos+i < int64(len(obuf)) { + nbuf[newpos+i] += obuf[oldpos+i] + } + } + + // Adjust pointers + newpos += ctrl.Add + oldpos += ctrl.Add + + // Sanity-check + if newpos+ctrl.Copy > hdr.NewSize { + return ErrCorrupt + } + + // Read extra string + _, err = io.ReadFull(epfbz2, nbuf[newpos:newpos+ctrl.Copy]) + if err != nil { + return ErrCorrupt + } + + // Adjust pointers + newpos += ctrl.Copy + oldpos += ctrl.Seek + } + + // Write the new file + for len(nbuf) > 0 { + n, err := new.Write(nbuf) + if err != nil { + return err + } + nbuf = nbuf[n:] + } + + return nil +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/patch_test.go b/Godeps/_workspace/src/github.com/kr/binarydist/patch_test.go new file mode 100644 index 00000000000..840a919e209 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/patch_test.go @@ -0,0 +1,62 @@ +package binarydist + +import ( + "io/ioutil" + "os" + "os/exec" + "testing" +) + +func TestPatch(t *testing.T) { + mustWriteRandFile("test.old", 1e3) + mustWriteRandFile("test.new", 1e3) + + got, err := ioutil.TempFile("/tmp", "bspatch.") + if err != nil { + panic(err) + } + os.Remove(got.Name()) + + err = exec.Command("bsdiff", "test.old", "test.new", "test.patch").Run() + if err != nil { + panic(err) + } + + err = Patch(mustOpen("test.old"), got, mustOpen("test.patch")) + if err != nil { + t.Fatal("err", err) + } + + ref, err := got.Seek(0, 2) + if err != nil { + panic(err) + } + + t.Logf("got %d bytes", ref) + if n := fileCmp(got, mustOpen("test.new")); n > -1 { + t.Fatalf("produced different output at pos %d", n) + } +} + +func TestPatchHk(t *testing.T) { + got, err := ioutil.TempFile("/tmp", "bspatch.") + if err != nil { + panic(err) + } + os.Remove(got.Name()) + + err = Patch(mustOpen("testdata/sample.old"), got, mustOpen("testdata/sample.patch")) + if err != nil { + t.Fatal("err", err) + } + + ref, err := got.Seek(0, 2) + if err != nil { + panic(err) + } + + t.Logf("got %d bytes", ref) + if n := fileCmp(got, mustOpen("testdata/sample.new")); n > -1 { + t.Fatalf("produced different output at pos %d", n) + } +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/seek.go b/Godeps/_workspace/src/github.com/kr/binarydist/seek.go new file mode 100644 index 00000000000..96c03461e4e --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/seek.go @@ -0,0 +1,43 @@ +package binarydist + +import ( + "errors" +) + +type seekBuffer struct { + buf []byte + pos int +} + +func (b *seekBuffer) Write(p []byte) (n int, err error) { + n = copy(b.buf[b.pos:], p) + if n == len(p) { + b.pos += n + return n, nil + } + b.buf = append(b.buf, p[n:]...) + b.pos += len(p) + return len(p), nil +} + +func (b *seekBuffer) Seek(offset int64, whence int) (ret int64, err error) { + var abs int64 + switch whence { + case 0: + abs = offset + case 1: + abs = int64(b.pos) + offset + case 2: + abs = int64(len(b.buf)) + offset + default: + return 0, errors.New("binarydist: invalid whence") + } + if abs < 0 { + return 0, errors.New("binarydist: negative position") + } + if abs >= 1<<31 { + return 0, errors.New("binarydist: position out of range") + } + b.pos = int(abs) + return abs, nil +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/sort_test.go b/Godeps/_workspace/src/github.com/kr/binarydist/sort_test.go new file mode 100644 index 00000000000..be483c3a262 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/binarydist/sort_test.go @@ -0,0 +1,33 @@ +package binarydist + +import ( + "bytes" + "crypto/rand" + "testing" +) + +var sortT = [][]byte{ + mustRandBytes(1000), + mustReadAll(mustOpen("test.old")), + []byte("abcdefabcdef"), +} + +func TestQsufsort(t *testing.T) { + for _, s := range sortT { + I := qsufsort(s) + for i := 1; i < len(I); i++ { + if bytes.Compare(s[I[i-1]:], s[I[i]:]) > 0 { + t.Fatalf("unsorted at %d", i) + } + } + } +} + +func mustRandBytes(n int) []byte { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return b +} diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/testdata/sample.new b/Godeps/_workspace/src/github.com/kr/binarydist/testdata/sample.new new file mode 100644 index 0000000000000000000000000000000000000000..592cdbe2dff6a7f7f9bcdbd80fdf30fb775d8e13 GIT binary patch literal 10000 zcmeHMeQZ_b9X@=y>*(kWxTVG-mw38$g7hlQavh!Aj+VWQZdM_)&gw0NN-LN4roEMR zW3gS!!tt~ln`Y^>$;yZencX7JWXuZizM6ROd#U6C{~?&p5ObP`_Bx4X=^A1t0d3Y!rf3aAyp(CmL`aOqU$jtq0 zq3-zf&9F7s@8~ubYwO%z-x80tCEM$-pGLl?HJ$zbw#N-!^B$OMb+LPyd8eKcfESBZ zly59IgKVm`e$mV4XjP1U!CN%a)RQ*g!(S}6Wm9sK?(6sI>4mEGejl&&j_h76)(joB zs)=Ol6!}6!VcBrrYzyrPr!eOb^(!=UY+|wcwzjPIZ25wFHNEi2EZ=k)tJD7F52pRB zYTeh@S5~a^eWdON*&m=2@E2SyjsNcBCWH8gbdF*N@V~A6*0uid)YjsuS{otfS-f2*PAzvi|+y?9e|1LtOvZ|ID+=dp{VcX#Wi?Ibu8y(cO) z{PDgUaz)B|_vLG9zPp7JGtoP@hKO5M=wzw1GyUV!OSIQFL&CP1=&k*lmhV%aDVvFu zjAzhGb|z+~cklsCX2a&2E{fi!)=kY@8`@^nH~eW$=8AVOj^5TS^|7|itxYrOixeZj ziahPd=5VsJkC)f~_G`T6eZ zjIrEq5c|KFJBW8iZ3s^uo|5*q){=NreaV*2_!hkL$MFark%a^1ms*SU8#>#P4ck|I zc?I@0)$0Y^*NA7zKE1f4s;RzpQ)_3*TE^Xumey@;C2Lw*8`ie8HzQ#;wKT7gh~!Mk zHyT>oFw$01p0ft@$*1nWE0zhAS$dSc)2OLxrkI$xe;sqV8~P;`8*&BL7P zueAfpU0yWs)v%?UW#|o|=WGjbra-yF_5hb*X4oD=4>R7mqaf3tw2M{$PCKX`!Jqy@ zn`?Kc*6z`3xgw;lA5)L2N4-04nQJMx)($JD)>dQ?QSLJ3#O*ref+ISO9IaD5tE^;# zdlP0ahZEx!b`8%qXlvP$OtofB63#N^k^)71BXnW^?aX)91iTd+wVI>_ zRhMI3Y0;kbOCZortQWXSi!OXu4-{~~TO6KdPvfkdJtu-8A)@#Et;FBs4BGpNNB7|_ zG>6hw@2{{gs8zFI`uIy?dNGhOmN6{tHiT`0+b6fo28UYUrVUdKyTB|p?zTKMwVVo> zsW8+Nn?*PpPTQ;_!)6_@Rim+4+_zbsZ!@4HHj4=Fw_haU#%Az{ESu51Fjk|sS)4X| z?wqz6%vozkVU!xW71~;~RSKLFP|H$q^^y0lvb+Z$2Lx^f{XK3+l~asHIgJ4{RZe5x zc$6nGpID?^-CU{DBNV zIRDni+H~s**tGjF&6HtA3EsUgLj0byQA!V$;iBWenO%SJ zN(wFSCkHZAFV?Duq54#vBivJPSB}mw5OuCNK2>KE3*L?72k^7Jeu}n7KerSj??h&y z*LPkAZF>$!b5(unq^PR#zvC)j$zi*TOz1OyszInXpQKQdtmZ3(=l!*I@$OfFXqj<|h##O>7y3vg9qT+Cb}9R%!9tu2y9b-=>Ilrvu;z6nq$QK+3_ zzetzFB$Sd==q-7kEUFkM6-bdz6|IHL#W7?e>DAgDs^<_;)fBTt9HJ!dgLap+eOg3@ zth13xZV@RqDrbGbE1R$^ccUG|M5KH2I|aq%?K@KD10v$+*piVK2Vg;LLEa@`&MN>b zw0ok=O5UWLqrgEv)3~<>-KM~eXLc_3N4UHLepjY8pF^xF7%g$gEe|X!4??xFGp~0~ z-uvX{)9t6pzB>PEF~~IRucQNM))T_|m9F=!ge?@ew^U|CJjnSisE`YcNg%QJ39 zTI;nVj8VyKKRN+t={~C@;0lXQMOnxI7lMypPwBY<@0xdXt_bo~N)KuqO3$!;LSSYl z8@7R`_=}8*zqm586Ts^lMn|g~@3k*r9KvDEs|`c6tnz$Gu9qcZ4?FQ{aU+ z%(8C3UF{mn-!*{GPRt_8y))ST{tvM)f7cI0@=?2(RD4|rCt15RJgCk3Jow?=Ra|b8q9sF>~=ils^oSol$l?*f1T;;%?u;pWyg)(fpv>2J%Bw6Ixo(ssO$h#P0 zVv=Lex3*pelOY`;&xy$yASrVV+I$5HAvE~Qu5G#mqyYUiAk1CPBIdZ;0CLKSWKkN5 zx)LNBI6)xrJWF$7QFtF?pOxyN@zl05+t1)Q)C_Az0t6+|Ku{0@@vFb^^fM@4#q&&l z?{*`WF8)Nl=LpU_KJq=!fsN=3KM5(v6c)!XEWLIR1o)$FCyt;LFZcyZ@kT26vUZwc zOw(x~To+e5*Rs1?y7yq&gF=L3B*x0G`r~$>3gwILU*%N7O?&M+-4A%{_USVmG8_sb zGh3g|avz)FvQOH*HgNatbj);Vr!k5}qeGk%?B`^-S$po{QEbn-&y*Gwyu#($d%R!&9pwfbVB>V!iw2SQFxa7M%uEZbbAo$rBY~Jzi|KKyQGG!)z)V5bPZ(xxKZ93qous z4;AK=4E5gO$ui5^ad3xykVfkWbll!Tl$-uU_sC79y<$x=M_~Q|jh16j{2*JDo+EOr z5)HlI9M(!p#M7i15+&Z7BGIs>Ck{aO(GEa<6z%wHS~QhP8@0BAJiCoN==PZ|?a+mO zu4th&QiYkK$=Wnn!P%i)u>9!bO_vO{XyixHRvOV7Gem2IJY$$QsDWJyHCD7-(ZXrb zx({L6FO9M`E!z3Fr$J5D2x?@gMI%3oHtC!!cX$ZIGv$xd4Z21QDV$inb0Zt0?ji*6C|4(;{w4 z)!ZH&P2=tT_+eed^kE$ZFJyLf=uB#y1QR&Gud><~kg!m27rE;p&Zth@X*1B35Z4K@ zL4}=UOVJvcfg0t2mKCNTh>FVE#|0=^ah~=W#0jDrx08f4zmalEK0<2wLMPt34->#? z0d9FF1u$*ib=@M%``$i{6UqbcHUJ_s;qiUK(29m=!gbTid&lP{&n=;U#uKbt@ ze_$@2(8=fv;zvg-@dlWT?7ZPA|r_ zD4ll(x3Am-jC}V%1mAZayX3?n7o7k^l8X)xD$eVaqN@QPOvVrDnMZ)vQLLGZ2i;?) zOX$YXSMC$qrF)Kq7`b|EwdxrU>%)3!k$PiF&<*Q#f)0smbV!@%d{Cd65+82E8z>jR Jc&U;-_-}XSHje-R literal 0 HcmV?d00001 diff --git a/Godeps/_workspace/src/github.com/kr/binarydist/testdata/sample.old b/Godeps/_workspace/src/github.com/kr/binarydist/testdata/sample.old new file mode 100644 index 0000000000000000000000000000000000000000..7bc64dacb0a93810ef51ba9bba355b1755738734 GIT binary patch literal 11000 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 F1^_f>!+rn& literal 0 HcmV?d00001 From 1e623f8823157e2e7373bd662d043a66c4b375e7 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 18 Oct 2014 14:32:37 +0200 Subject: [PATCH 02/17] added test to not commit bad version numbers --- updates/updates.go | 7 +++++-- updates/updates_test.go | 11 +++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 updates/updates_test.go diff --git a/updates/updates.go b/updates/updates.go index b8736b4781d..f2f4f93e8e9 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -35,13 +35,16 @@ var currentVersion *semver.Version func init() { var err error - currentVersion, err = semver.NewVersion(Version) + currentVersion, err = parseVersion() if err != nil { - log.Error("The const Version literal in version.go needs to be in semver format: %s \n", Version) + log.Error("illegal version number in code: %q\n", Version) os.Exit(1) } } +func parseVersion() (*semver.Version, error) { + return semver.NewVersion(Version) +} func CheckForUpdates() error { resp, err := http.Get(EndpointURLLatestReleases) if err != nil { diff --git a/updates/updates_test.go b/updates/updates_test.go new file mode 100644 index 00000000000..4b258fe5dca --- /dev/null +++ b/updates/updates_test.go @@ -0,0 +1,11 @@ +package updates + +import "testing" + +// TestParseVersion just makes sure that we dont commit a bad version number +func TestParseVersion(t *testing.T) { + _, err := parseVersion() + if err != nil { + t.Fatal(err) + } +} From 2806260f5c54b5c61730a592ac58d6d977c5850f Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 18 Oct 2014 14:33:13 +0200 Subject: [PATCH 03/17] use inconshreveable's go-update to check for updates --- cmd/ipfs/ipfs.go | 20 +++++++------- updates/updates.go | 69 +++++++++------------------------------------- 2 files changed, 23 insertions(+), 66 deletions(-) diff --git a/cmd/ipfs/ipfs.go b/cmd/ipfs/ipfs.go index a79a49d0cd7..2d3f79a3c0a 100644 --- a/cmd/ipfs/ipfs.go +++ b/cmd/ipfs/ipfs.go @@ -7,6 +7,7 @@ import ( "runtime/pprof" flag "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/gonuts/flag" + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/check" commander "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/commander" ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr" @@ -126,18 +127,17 @@ func localNode(confdir string, online bool) (*core.IpfsNode, error) { } if cfg.Version.ShouldCheckForUpdate() { - obsolete := updates.CheckForUpdates() - if obsolete != nil { - if cfg.Version.Check == config.CheckError { - return nil, obsolete - } + _, err := updates.CheckForUpdate() + if err != nil { + if err != check.NoUpdateAvailable { + log.Error("Error while checking for update: %v\n", err) + return nil, err - // when "warn" version.check mode we just show warning message - log.Warning(fmt.Sprintf("%v", obsolete)) - } else { - // update most recent check timestamp in config - config.RecordUpdateCheck(cfg, filename) + } + log.Notice("No update available") } + + config.RecordUpdateCheck(cfg, filename) } return core.NewIpfsNode(cfg, online) diff --git a/updates/updates.go b/updates/updates.go index f2f4f93e8e9..4b6480ed640 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -1,32 +1,18 @@ package updates import ( - "encoding/json" - "fmt" - "net/http" "os" "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/coreos/go-semver/semver" + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update" + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/check" 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 - - ` + Version = "0.1.0" // actual current application's version literal + UpdateEndpointURL = "https://api.equinox.io/1/Updates" + UpdateAppID = "ap_ywkPmAR40q4EfdikN9Jh2hgIHi" ) var log = u.Logger("updates") @@ -45,42 +31,13 @@ func init() { func parseVersion() (*semver.Version, error) { return semver.NewVersion(Version) } -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) - } + +func CheckForUpdate() (*check.Result, error) { + param := check.Params{ + AppVersion: Version, + AppId: UpdateAppID, + Channel: "stable", } - return nil + + return param.CheckForUpdate(UpdateEndpointURL, update.New()) } From ee8bcd55972677ac200edad8aa19fedce99cf234 Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 18 Oct 2014 14:34:08 +0200 Subject: [PATCH 04/17] added AutoUpdate to config --- config/version.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/version.go b/config/version.go index 3c08cd372db..417be5bd845 100644 --- a/config/version.go +++ b/config/version.go @@ -22,6 +22,13 @@ type Version struct { // CheckPeriod is the time duration over which the update check will not be performed // (Note: cannot use time.Duration because marshalling with json breaks it) CheckPeriod string + + // AutoUpdate is optional and has these these options: + // - "never" do not auto-update + // - "patch" auto-update on new patch versions + // - "minor" auto-update on new minor (or patch) versions (Default) + // - "major" auto-update on any new version + AutoUpdate string } // supported Version.Check values @@ -36,6 +43,14 @@ const ( CheckIgnore = "ignore" ) +// supported Version.AutoUpdate values +const ( + UpdateNever = "never" + UpdatePatch = "patch" + UpdateMinor = "minor" + UpdateMajor = "major" +) + // defaultCheckPeriod governs h var defaultCheckPeriod = time.Hour * 48 From 5fa3053e193dcef57c6c86c0c7f9a6b2701e61ce Mon Sep 17 00:00:00 2001 From: Henry Date: Sat, 18 Oct 2014 21:10:52 +0200 Subject: [PATCH 05/17] added command stubs --- cmd/ipfs/ipfs.go | 1 + cmd/ipfs/update.go | 58 +++++++++++++++++++++++++++++++++++++++++ core/commands/update.go | 22 ++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 cmd/ipfs/update.go create mode 100644 core/commands/update.go diff --git a/cmd/ipfs/ipfs.go b/cmd/ipfs/ipfs.go index 2d3f79a3c0a..fd28b8f5a3d 100644 --- a/cmd/ipfs/ipfs.go +++ b/cmd/ipfs/ipfs.go @@ -68,6 +68,7 @@ Use "ipfs help " for more information about a command. cmdIpfsDiag, cmdIpfsBlock, cmdIpfsObject, + cmdIpfsUpdate, cmdIpfsLog, cmdIpfsPin, }, diff --git a/cmd/ipfs/update.go b/cmd/ipfs/update.go new file mode 100644 index 00000000000..0391df98aaa --- /dev/null +++ b/cmd/ipfs/update.go @@ -0,0 +1,58 @@ +package main + +import ( + flag "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/gonuts/flag" + commander "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/commander" + "github.com/jbenet/go-ipfs/core/commands" +) + +var cmdIpfsUpdate = &commander.Command{ + UsageLine: "update", + Short: "check for updates and apply them", + Long: `ipfs update - check for updates and apply them + + ipfs update - apply + ipfs update check - just check + ipfs update log - list the changelogs + +ipfs update is a utility command used to check for updates and apply them. +I wont even try, @jbenet. You do this much better :)`, + Run: makeCommand(command{ + name: "updateApply", + args: 0, + flags: nil, + online: true, + cmdFn: commands.UpdateApply, + }), + Subcommands: []*commander.Command{ + cmdIpfsUpdateCheck, + cmdIpfsUpdateLog, + }, + Flag: *flag.NewFlagSet("ipfs-update", flag.ExitOnError), +} + +var cmdIpfsUpdateCheck = &commander.Command{ + UsageLine: "check", + Short: "", + Long: `ipfs update check `, + Run: makeCommand(command{ + name: "updateCheck", + args: 0, + flags: nil, + online: false, + cmdFn: commands.UpdateCheck, + }), +} + +var cmdIpfsUpdateLog = &commander.Command{ + UsageLine: "log", + Short: "list the last versions and their changelog", + Long: `ipfs updage log - list the last versions and their changelog`, + Run: makeCommand(command{ + name: "updateLog", + args: 0, + flags: nil, + online: false, + cmdFn: commands.UpdateCheck, + }), +} diff --git a/core/commands/update.go b/core/commands/update.go new file mode 100644 index 00000000000..f2b72c9692e --- /dev/null +++ b/core/commands/update.go @@ -0,0 +1,22 @@ +package commands + +import ( + "errors" + "io" + + "github.com/jbenet/go-ipfs/core" +) + +// UpdateApply applys an update of the ipfs binary and shuts down the node if successful +func UpdateApply(n *core.IpfsNode, args []string, opts map[string]interface{}, out io.Writer) error { + return errors.New("TODOUpdateApply") +} + +// UpdateCheck checks wether there is an update available +func UpdateCheck(n *core.IpfsNode, args []string, opts map[string]interface{}, out io.Writer) error { + return errors.New("TODOUpdateCheck") +} + +func UpdateLog(n *core.IpfsNode, args []string, opts map[string]interface{}, out io.Writer) error { + return errors.New("TODOUpdateLog") +} From 7ddf3836d07665938c202a4c8f27daeec0571242 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 15:33:46 +0200 Subject: [PATCH 06/17] implemented manual check and update (with signature verification) --- cmd/ipfs/equinox.yaml | 6 +++++ core/commands/update.go | 49 ++++++++++++++++++++++++++++++++++++++--- daemon/daemon.go | 2 ++ updates/updates.go | 31 +++++++++++++++++++++----- 4 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 cmd/ipfs/equinox.yaml diff --git a/cmd/ipfs/equinox.yaml b/cmd/ipfs/equinox.yaml new file mode 100644 index 00000000000..9e764e05c8b --- /dev/null +++ b/cmd/ipfs/equinox.yaml @@ -0,0 +1,6 @@ +--- +equinox-account: CHANGEME +equinox-secret: CHANGEME +equinox-app: CHANGEME +channel: stable +private-key: equinox-priv diff --git a/core/commands/update.go b/core/commands/update.go index f2b72c9692e..16e2f82d420 100644 --- a/core/commands/update.go +++ b/core/commands/update.go @@ -2,21 +2,64 @@ package commands import ( "errors" + "fmt" "io" + "os" "github.com/jbenet/go-ipfs/core" + "github.com/jbenet/go-ipfs/updates" ) // UpdateApply applys an update of the ipfs binary and shuts down the node if successful func UpdateApply(n *core.IpfsNode, args []string, opts map[string]interface{}, out io.Writer) error { - return errors.New("TODOUpdateApply") + fmt.Fprintln(out, "Current Version:", updates.Version) + u, err := updates.CheckForUpdate() + if err != nil { + return err + } + + if u == nil { + fmt.Fprintln(out, "No update available") + return nil + } + fmt.Fprintln(out, "New Version:", u.Version) + + if err = updates.AbleToApply(); err != nil { + return fmt.Errorf("Can't apply update: %v", err) + } + + if err, errRecover := u.Update(); err != nil { + err = fmt.Errorf("Update failed: %v\n", err) + if errRecover != nil { + err = fmt.Errorf("%s\nRecovery failed! Cause: %v\nYou may need to recover manually", err, errRecover) + } + fmt.Fprint(out, err.Error()) + return err + } + + fmt.Fprintln(out, "Updated applied! Shutting down.") + os.Exit(0) + return nil } // UpdateCheck checks wether there is an update available func UpdateCheck(n *core.IpfsNode, args []string, opts map[string]interface{}, out io.Writer) error { - return errors.New("TODOUpdateCheck") + fmt.Fprintln(out, "Current Version:", updates.Version) + u, err := updates.CheckForUpdate() + if err != nil { + return err + } + + if u == nil { + fmt.Fprintln(out, "No update available") + return nil + } + + fmt.Fprintln(out, "New Version:", u.Version) + return nil } +// UpdateLog lists the version available online func UpdateLog(n *core.IpfsNode, args []string, opts map[string]interface{}, out io.Writer) error { - return errors.New("TODOUpdateLog") + return errors.New("Not yet implemented") } diff --git a/daemon/daemon.go b/daemon/daemon.go index 9e3ef370dc5..b4241e16b77 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -141,6 +141,8 @@ func (dl *DaemonListener) handleConnection(conn manet.Conn) { err = commands.Log(dl.node, command.Args, command.Opts, conn) case "unpin": err = commands.Unpin(dl.node, command.Args, command.Opts, conn) + case "updateApply": + err = commands.UpdateApply(dl.node, command.Args, command.Opts, conn) default: err = fmt.Errorf("Invalid Command: '%s'", command.Command) } diff --git a/updates/updates.go b/updates/updates.go index 4b6480ed640..754f559ab4a 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -1,18 +1,26 @@ package updates import ( + "fmt" "os" + u "github.com/jbenet/go-ipfs/util" + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/coreos/go-semver/semver" "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update" "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/check" - u "github.com/jbenet/go-ipfs/util" ) const ( - Version = "0.1.0" // actual current application's version literal - UpdateEndpointURL = "https://api.equinox.io/1/Updates" - UpdateAppID = "ap_ywkPmAR40q4EfdikN9Jh2hgIHi" + // Version is the current application's version literal + Version = "0.1.1" + + updateEndpointURL = "https://api.equinox.io/1/Updates" + updateAppID = "CHANGEME" + + updatePubKey = `-----BEGIN RSA PUBLIC KEY----- +CHANGEME +-----END RSA PUBLIC KEY-----` ) var log = u.Logger("updates") @@ -32,12 +40,23 @@ func parseVersion() (*semver.Version, error) { return semver.NewVersion(Version) } +// CheckForUpdate checks the equinox.io api if there is an update available func CheckForUpdate() (*check.Result, error) { param := check.Params{ AppVersion: Version, - AppId: UpdateAppID, + AppId: updateAppID, Channel: "stable", } - return param.CheckForUpdate(UpdateEndpointURL, update.New()) + up, err := update.New().VerifySignatureWithPEM([]byte(updatePubKey)) + if err != nil { + return nil, fmt.Errorf("Failed to parse public key: %v", err) + } + + return param.CheckForUpdate(updateEndpointURL, up) +} + +// AbleToApply cheks if the running process is able to update itself +func AbleToApply() error { + return update.New().CanUpdate() } From 8f1fd2fcd9983a5ecc79e6a1ed0e8b89737a903b Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 17:10:12 +0200 Subject: [PATCH 07/17] ShouldAutoUpdate function --- config/version.go | 1 + updates/updates.go | 49 ++++++++++++++++++++++++++++++++++++++++ updates/updates_test.go | 50 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/config/version.go b/config/version.go index 417be5bd845..1bd641ad4d0 100644 --- a/config/version.go +++ b/config/version.go @@ -44,6 +44,7 @@ const ( ) // supported Version.AutoUpdate values +// BUG(cryptix): make this a custom type that implements json.Unmarshaller() to verify values const ( UpdateNever = "never" UpdatePatch = "patch" diff --git a/updates/updates.go b/updates/updates.go index 754f559ab4a..c6cf45783a9 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/jbenet/go-ipfs/config" u "github.com/jbenet/go-ipfs/util" "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/coreos/go-semver/semver" @@ -60,3 +61,51 @@ func CheckForUpdate() (*check.Result, error) { func AbleToApply() error { return update.New().CanUpdate() } + +// ShouldAutoUpdate decides wether a new version should be applied +// checks against config setting and new version string. returns false in case of error +func ShouldAutoUpdate(setting, newVer string) bool { + if setting == config.UpdateNever { + return false + } + + nv, err := semver.NewVersion(newVer) + if err != nil { + log.Error("could not parse version string: %s", err) + return false + } + + n := nv.Slice() + c := currentVersion.Slice() + + switch setting { + + case config.UpdatePatch: + if n[0] < c[0] { + return false + } + + if n[1] < c[1] { + return false + } + + return n[2] > c[2] + + case config.UpdateMinor: + if n[0] != c[0] { + return false + } + + return n[1] > c[1] || (n[1] == c[1] && n[2] > c[2]) + + case config.UpdateMajor: + for i := 0; i < 3; i++ { + if n[i] < c[i] { + return false + } + } + return true + } + + return false +} diff --git a/updates/updates_test.go b/updates/updates_test.go index 4b258fe5dca..02d7b9e9ad2 100644 --- a/updates/updates_test.go +++ b/updates/updates_test.go @@ -1,6 +1,11 @@ package updates -import "testing" +import ( + "testing" + + "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/coreos/go-semver/semver" + "github.com/jbenet/go-ipfs/config" +) // TestParseVersion just makes sure that we dont commit a bad version number func TestParseVersion(t *testing.T) { @@ -9,3 +14,46 @@ func TestParseVersion(t *testing.T) { t.Fatal(err) } } + +func TestShouldAutoUpdate(t *testing.T) { + tests := []struct { + setting, currV, newV string + should bool + }{ + {config.UpdateNever, "0.0.1", "1.0.0", false}, + {config.UpdateNever, "0.0.1", "0.1.0", false}, + {config.UpdateNever, "0.0.1", "0.0.1", false}, + {config.UpdateNever, "0.0.1", "0.0.2", false}, + + {config.UpdatePatch, "0.0.1", "1.0.0", false}, + {config.UpdatePatch, "0.0.1", "0.1.0", false}, + {config.UpdatePatch, "0.0.1", "0.0.1", false}, + {config.UpdatePatch, "0.0.2", "0.0.1", false}, + {config.UpdatePatch, "0.0.1", "0.0.2", true}, + + {config.UpdateMinor, "0.1.1", "1.0.0", false}, + {config.UpdateMinor, "0.1.1", "0.2.0", true}, + {config.UpdateMinor, "0.1.1", "0.1.2", true}, + {config.UpdateMinor, "0.2.1", "0.1.9", false}, + {config.UpdateMinor, "0.1.2", "0.1.1", false}, + + {config.UpdateMajor, "1.0.0", "2.0.0", true}, + {config.UpdateMajor, "1.0.0", "1.1.0", true}, + {config.UpdateMajor, "1.0.0", "1.0.1", true}, + {config.UpdateMajor, "2.0.0", "1.0.0", false}, // don't downgrade + {config.UpdateMajor, "2.5.0", "2.4.0", false}, + {config.UpdateMajor, "2.0.2", "2.0.1", false}, + } + + for i, tc := range tests { + var err error + currentVersion, err = semver.NewVersion(tc.currV) + if err != nil { + t.Fatalf("Could not parse test version: %v", err) + } + + if tc.should != ShouldAutoUpdate(tc.setting, tc.newV) { + t.Fatalf("#%d failed for %+v", i, tc) + } + } +} From 8d41021b898df35b4a5de6a0752294489f6773a2 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 17:14:24 +0200 Subject: [PATCH 08/17] check AutoUpdate settings and apply --- cmd/ipfs/ipfs.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/cmd/ipfs/ipfs.go b/cmd/ipfs/ipfs.go index fd28b8f5a3d..93cd41eacf7 100644 --- a/cmd/ipfs/ipfs.go +++ b/cmd/ipfs/ipfs.go @@ -128,7 +128,8 @@ func localNode(confdir string, online bool) (*core.IpfsNode, error) { } if cfg.Version.ShouldCheckForUpdate() { - _, err := updates.CheckForUpdate() + log.Info("checking for update") + u, err := updates.CheckForUpdate() if err != nil { if err != check.NoUpdateAvailable { log.Error("Error while checking for update: %v\n", err) @@ -139,6 +140,29 @@ func localNode(confdir string, online bool) (*core.IpfsNode, error) { } config.RecordUpdateCheck(cfg, filename) + + if u != nil && cfg.Version.AutoUpdate != config.UpdateNever { + if updates.ShouldAutoUpdate(cfg.Version.AutoUpdate, u.Version) { + log.Notice("Applying update %s", u.Version) + + if err = updates.AbleToApply(); err != nil { + log.Error("Can't apply update: %v", err) + return nil, err + } + + if err, errRecover := u.Update(); err != nil { + err = fmt.Errorf("Update failed: %v\n", err) + if errRecover != nil { + err = fmt.Errorf("%s\nRecovery failed! Cause: %v\nYou may need to recover manually", err, errRecover) + } + log.Error(err.Error()) + return nil, err + } + // BUG(cryptix): no good way to restart yet. - tracking https://github.com/inconshreveable/go-update/issues/5 + fmt.Println("update %v applied. please restart.", u.Version) + os.Exit(0) + } + } } return core.NewIpfsNode(cfg, online) From 24e1e6d1e1cb1ab128976fe928b5b2f16c8f8c45 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 18:27:40 +0200 Subject: [PATCH 09/17] config: custom AutoUpdate type for validity --- config/version.go | 71 +++++++++++++++++++++++++++++++++++------- config/version_test.go | 36 +++++++++++++++++++++ updates/updates.go | 2 +- 3 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 config/version_test.go diff --git a/config/version.go b/config/version.go index 1bd641ad4d0..88534ebc7bc 100644 --- a/config/version.go +++ b/config/version.go @@ -1,7 +1,9 @@ package config import ( + "errors" "strconv" + "strings" "time" ) @@ -23,12 +25,8 @@ type Version struct { // (Note: cannot use time.Duration because marshalling with json breaks it) CheckPeriod string - // AutoUpdate is optional and has these these options: - // - "never" do not auto-update - // - "patch" auto-update on new patch versions - // - "minor" auto-update on new minor (or patch) versions (Default) - // - "major" auto-update on any new version - AutoUpdate string + // AutoUpdate is optional + AutoUpdate AutoUpdateSetting } // supported Version.Check values @@ -43,13 +41,62 @@ const ( CheckIgnore = "ignore" ) -// supported Version.AutoUpdate values -// BUG(cryptix): make this a custom type that implements json.Unmarshaller() to verify values +// AutoUpdateSetting implements json.Unmarshaler to check values in config +// supported values: +// "never" - do not auto-update +// "patch" - auto-update on new patch versions +// "minor" - auto-update on new minor (or patch) versions (Default) +// "major" - auto-update on any new version +type AutoUpdateSetting int + +// UnmarshalJSON checks the input against known strings +func (s *AutoUpdateSetting) UnmarshalJSON(in []byte) error { + + switch strings.ToLower(string(in)) { + case `"never"`: + *s = UpdateNever + case `"major"`: + *s = UpdateMajor + case `"minor"`: + *s = UpdateMinor + case `"patch"`: + *s = UpdatePatch + default: + *s = UpdateMinor + return ErrUnknownAutoUpdateSetting + } + return nil +} + +// MarshalJSON converts the value back to JSON string +func (s AutoUpdateSetting) MarshalJSON() ([]byte, error) { + return []byte(`"` + s.String() + `"`), nil +} + +// String converts valye to human readable string +func (s AutoUpdateSetting) String() string { + switch s { + case UpdateNever: + return "never" + case UpdateMajor: + return "major" + case UpdateMinor: + return "minor" + case UpdatePatch: + return "patch" + default: + return ErrUnknownAutoUpdateSetting.Error() + } +} + +// ErrUnknownAutoUpdateSetting is returned when an unknown value is read from the config +var ErrUnknownAutoUpdateSetting = errors.New("unknown value for AutoUpdate") + const ( - UpdateNever = "never" - UpdatePatch = "patch" - UpdateMinor = "minor" - UpdateMajor = "major" + UpdateMinor AutoUpdateSetting = iota // first value so that it is the zero value and thus the default + UpdatePatch + UpdateMajor + UpdateNever ) // defaultCheckPeriod governs h diff --git a/config/version_test.go b/config/version_test.go new file mode 100644 index 00000000000..01b1a643395 --- /dev/null +++ b/config/version_test.go @@ -0,0 +1,36 @@ +package config + +import ( + "strings" + "testing" +) + +func TestAutoUpdateValues(t *testing.T) { + var tval struct { + AutoUpdate AutoUpdateSetting + } + tests := []struct { + input string + val AutoUpdateSetting + err error + }{ + {`{"hello":123}`, UpdateMinor, nil}, // default + {`{"AutoUpdate": "never"}`, UpdateNever, nil}, + {`{"AutoUpdate": "patch"}`, UpdatePatch, nil}, + {`{"AutoUpdate": "minor"}`, UpdateMinor, nil}, + {`{"AutoUpdate": "major"}`, UpdateMajor, nil}, + {`{"AutoUpdate": "blarg"}`, UpdateMinor, ErrUnknownAutoUpdateSetting}, + } + + for i, tc := range tests { + err := Decode(strings.NewReader(tc.input), &tval) + if err != tc.err { + t.Fatalf("%d failed - got err %q wanted %v", i, err, tc.err) + } + + if tval.AutoUpdate != tc.val { + t.Fatalf("%d failed - got val %q where we wanted %q", i, tval.AutoUpdate, tc.val) + } + } + +} diff --git a/updates/updates.go b/updates/updates.go index c6cf45783a9..443bf461a66 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -64,7 +64,7 @@ func AbleToApply() error { // ShouldAutoUpdate decides wether a new version should be applied // checks against config setting and new version string. returns false in case of error -func ShouldAutoUpdate(setting, newVer string) bool { +func ShouldAutoUpdate(setting config.AutoUpdateSetting, newVer string) bool { if setting == config.UpdateNever { return false } From 81f84d6ceaa297fda589732bf9cc00925adcc139 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 18:53:53 +0200 Subject: [PATCH 10/17] updated error messages and explicit import names --- cmd/ipfs/ipfs.go | 6 +++--- updates/updates.go | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/ipfs/ipfs.go b/cmd/ipfs/ipfs.go index 93cd41eacf7..63c476f2f4c 100644 --- a/cmd/ipfs/ipfs.go +++ b/cmd/ipfs/ipfs.go @@ -5,9 +5,10 @@ import ( "fmt" "os" "runtime/pprof" + "time" flag "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/gonuts/flag" - "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/check" + check "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/check" commander "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/commander" ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr" @@ -134,9 +135,8 @@ func localNode(confdir string, online bool) (*core.IpfsNode, error) { if err != check.NoUpdateAvailable { log.Error("Error while checking for update: %v\n", err) return nil, err - } - log.Notice("No update available") + log.Notice("No update available, checked on %s", time.Now()) } config.RecordUpdateCheck(cfg, filename) diff --git a/updates/updates.go b/updates/updates.go index 443bf461a66..eb11ad186e8 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -7,9 +7,9 @@ import ( "github.com/jbenet/go-ipfs/config" u "github.com/jbenet/go-ipfs/util" - "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/coreos/go-semver/semver" - "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update" - "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/check" + semver "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/coreos/go-semver/semver" + update "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update" + check "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/check" ) const ( @@ -32,7 +32,7 @@ func init() { var err error currentVersion, err = parseVersion() if err != nil { - log.Error("illegal version number in code: %q\n", Version) + log.Error("invalid version number in code (must be semver): %q\n", Version) os.Exit(1) } } From 46dc322f822c687206c5b8f3ee3510c4cc6a8b1f Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 18:58:22 +0200 Subject: [PATCH 11/17] fixed tests --- updates/updates_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/updates/updates_test.go b/updates/updates_test.go index 02d7b9e9ad2..5e3ce932390 100644 --- a/updates/updates_test.go +++ b/updates/updates_test.go @@ -17,8 +17,9 @@ func TestParseVersion(t *testing.T) { func TestShouldAutoUpdate(t *testing.T) { tests := []struct { - setting, currV, newV string - should bool + setting config.AutoUpdateSetting + currV, newV string + should bool }{ {config.UpdateNever, "0.0.1", "1.0.0", false}, {config.UpdateNever, "0.0.1", "0.1.0", false}, From cb73ea7ba48bdb1ca80a2d19992579dd9f5c673f Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 19:01:50 +0200 Subject: [PATCH 12/17] removed duplicate logic --- cmd/ipfs/ipfs.go | 12 ++---------- core/commands/update.go | 12 ++---------- updates/updates.go | 19 ++++++++++++++++--- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/cmd/ipfs/ipfs.go b/cmd/ipfs/ipfs.go index 63c476f2f4c..c2d9984c82f 100644 --- a/cmd/ipfs/ipfs.go +++ b/cmd/ipfs/ipfs.go @@ -145,19 +145,11 @@ func localNode(confdir string, online bool) (*core.IpfsNode, error) { if updates.ShouldAutoUpdate(cfg.Version.AutoUpdate, u.Version) { log.Notice("Applying update %s", u.Version) - if err = updates.AbleToApply(); err != nil { - log.Error("Can't apply update: %v", err) - return nil, err - } - - if err, errRecover := u.Update(); err != nil { - err = fmt.Errorf("Update failed: %v\n", err) - if errRecover != nil { - err = fmt.Errorf("%s\nRecovery failed! Cause: %v\nYou may need to recover manually", err, errRecover) - } + if err = updates.Apply(u); err != nil { log.Error(err.Error()) return nil, err } + // BUG(cryptix): no good way to restart yet. - tracking https://github.com/inconshreveable/go-update/issues/5 fmt.Println("update %v applied. please restart.", u.Version) os.Exit(0) diff --git a/core/commands/update.go b/core/commands/update.go index 16e2f82d420..95a4f8a44d3 100644 --- a/core/commands/update.go +++ b/core/commands/update.go @@ -24,17 +24,9 @@ func UpdateApply(n *core.IpfsNode, args []string, opts map[string]interface{}, o } fmt.Fprintln(out, "New Version:", u.Version) - if err = updates.AbleToApply(); err != nil { - return fmt.Errorf("Can't apply update: %v", err) - } - - if err, errRecover := u.Update(); err != nil { - err = fmt.Errorf("Update failed: %v\n", err) - if errRecover != nil { - err = fmt.Errorf("%s\nRecovery failed! Cause: %v\nYou may need to recover manually", err, errRecover) - } + if err = updates.Apply(u); err != nil { fmt.Fprint(out, err.Error()) - return err + return fmt.Errorf("Couldn't apply update: %v", err) } fmt.Fprintln(out, "Updated applied! Shutting down.") diff --git a/updates/updates.go b/updates/updates.go index eb11ad186e8..d016afc2d7a 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -57,9 +57,22 @@ func CheckForUpdate() (*check.Result, error) { return param.CheckForUpdate(updateEndpointURL, up) } -// AbleToApply cheks if the running process is able to update itself -func AbleToApply() error { - return update.New().CanUpdate() +// Apply cheks if the running process is able to update itself +// and than updates to the passed release +func Apply(rel *check.Result) error { + if err := update.New().CanUpdate(); err != nil { + return err + } + + if err, errRecover := rel.Update(); err != nil { + err = fmt.Errorf("Update failed: %v\n", err) + if errRecover != nil { + err = fmt.Errorf("%s\nRecovery failed! Cause: %v\nYou may need to recover manually", err, errRecover) + } + return err + } + + return nil } // ShouldAutoUpdate decides wether a new version should be applied From 0115d1e0de9df033ff37a812cb361b546f54f633 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 19:24:01 +0200 Subject: [PATCH 13/17] respect cfg.Version.Check --- cmd/ipfs/ipfs.go | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/cmd/ipfs/ipfs.go b/cmd/ipfs/ipfs.go index c2d9984c82f..7c67742a9f2 100644 --- a/cmd/ipfs/ipfs.go +++ b/cmd/ipfs/ipfs.go @@ -133,26 +133,30 @@ func localNode(confdir string, online bool) (*core.IpfsNode, error) { u, err := updates.CheckForUpdate() if err != nil { if err != check.NoUpdateAvailable { - log.Error("Error while checking for update: %v\n", err) - return nil, err - } - log.Notice("No update available, checked on %s", time.Now()) - } - - config.RecordUpdateCheck(cfg, filename) - - if u != nil && cfg.Version.AutoUpdate != config.UpdateNever { - if updates.ShouldAutoUpdate(cfg.Version.AutoUpdate, u.Version) { - log.Notice("Applying update %s", u.Version) - - if err = updates.Apply(u); err != nil { - log.Error(err.Error()) + if cfg.Version.Check == config.CheckError { + log.Error("Error while checking for update: %v\n", err) return nil, err } - - // BUG(cryptix): no good way to restart yet. - tracking https://github.com/inconshreveable/go-update/issues/5 - fmt.Println("update %v applied. please restart.", u.Version) - os.Exit(0) + // when "warn" version.check mode we just show a warning message + log.Warning(err.Error()) + } else { // err == check.NoUpdateAvailable + log.Notice("No update available, checked on %s", time.Now()) + config.RecordUpdateCheck(cfg, filename) + } + } else { // update avail + if cfg.Version.AutoUpdate != config.UpdateNever { + if updates.ShouldAutoUpdate(cfg.Version.AutoUpdate, u.Version) { + log.Notice("Applying update %s", u.Version) + + if err = updates.Apply(u); err != nil { + log.Error(err.Error()) + return nil, err + } + + // BUG(cryptix): no good way to restart yet. - tracking https://github.com/inconshreveable/go-update/issues/5 + fmt.Println("update %v applied. please restart.", u.Version) + os.Exit(0) + } } } } From 0a0c38fb91d25ac2747ef9c337c525257738dd07 Mon Sep 17 00:00:00 2001 From: Henry Date: Mon, 20 Oct 2014 20:01:31 +0200 Subject: [PATCH 14/17] using @jbenet's industrial lipstick to clean this up --- cmd/ipfs/ipfs.go | 35 ++-------------------- updates/updates.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 33 deletions(-) diff --git a/cmd/ipfs/ipfs.go b/cmd/ipfs/ipfs.go index 7c67742a9f2..666699b0293 100644 --- a/cmd/ipfs/ipfs.go +++ b/cmd/ipfs/ipfs.go @@ -5,10 +5,8 @@ import ( "fmt" "os" "runtime/pprof" - "time" flag "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/gonuts/flag" - check "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/inconshreveable/go-update/check" commander "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/commander" ma "github.com/jbenet/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr" @@ -128,37 +126,8 @@ func localNode(confdir string, online bool) (*core.IpfsNode, error) { return nil, err } - if cfg.Version.ShouldCheckForUpdate() { - log.Info("checking for update") - u, err := updates.CheckForUpdate() - if err != nil { - if err != check.NoUpdateAvailable { - if cfg.Version.Check == config.CheckError { - log.Error("Error while checking for update: %v\n", err) - return nil, err - } - // when "warn" version.check mode we just show a warning message - log.Warning(err.Error()) - } else { // err == check.NoUpdateAvailable - log.Notice("No update available, checked on %s", time.Now()) - config.RecordUpdateCheck(cfg, filename) - } - } else { // update avail - if cfg.Version.AutoUpdate != config.UpdateNever { - if updates.ShouldAutoUpdate(cfg.Version.AutoUpdate, u.Version) { - log.Notice("Applying update %s", u.Version) - - if err = updates.Apply(u); err != nil { - log.Error(err.Error()) - return nil, err - } - - // BUG(cryptix): no good way to restart yet. - tracking https://github.com/inconshreveable/go-update/issues/5 - fmt.Println("update %v applied. please restart.", u.Version) - os.Exit(0) - } - } - } + if err := updates.CliCheckForUpdates(cfg, filename); err != nil { + return nil, err } return core.NewIpfsNode(cfg, online) diff --git a/updates/updates.go b/updates/updates.go index d016afc2d7a..ba9fce1b3d3 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -3,6 +3,7 @@ package updates import ( "fmt" "os" + "time" "github.com/jbenet/go-ipfs/config" u "github.com/jbenet/go-ipfs/util" @@ -122,3 +123,77 @@ func ShouldAutoUpdate(setting config.AutoUpdateSetting, newVer string) bool { return false } + +func CliCheckForUpdates(cfg *config.Config, confFile string) error { + + // if config says not to, don't check for updates + if !cfg.Version.ShouldCheckForUpdate() { + log.Info("update checking disabled.") + return nil + } + + log.Info("checking for update") + u, err := CheckForUpdate() + // if there is no update available, record it, and exit. + if err == check.NoUpdateAvailable { + log.Notice("No update available, checked on %s", time.Now()) + config.RecordUpdateCheck(cfg, confFile) // only record if we checked successfully. + return nil + } + + // if another, unexpected error occurred, note it. + if err != nil { + if cfg.Version.Check == config.CheckError { + log.Error("Error while checking for update: %v\n", err) + return nil + } + // when "warn" version.check mode we just show a warning message + log.Warning(err.Error()) + return nil + } + + // there is an update available + + // if we autoupdate + if cfg.Version.AutoUpdate != config.UpdateNever { + // and we should auto update + if ShouldAutoUpdate(cfg.Version.AutoUpdate, u.Version) { + log.Notice("Applying update %s", u.Version) + + if err = Apply(u); err != nil { + log.Error(err.Error()) + return nil + } + + // BUG(cryptix): no good way to restart yet. - tracking https://github.com/inconshreveable/go-update/issues/5 + fmt.Println("update %v applied. please restart.", u.Version) + os.Exit(0) + } + } + + // autoupdate did not exit, so regular notices. + switch cfg.Version.Check { + case config.CheckError: + return fmt.Errorf(errShouldUpdate, Version, u.Version) + case config.CheckWarn: + // print the warning + fmt.Printf("New version available: %s", u.Version) + default: // ignore + } + return nil +} + +var errShouldUpdate = ` +Your go-ipfs version is: %s +There is a new version available: %s +Since this is alpha software, it is strongly recommended you update. + +To update, run: + + ipfs update apply + +To disable this notice, run: + + ipfs config Version.Check warn + +` From 3e4078818884174e362565e8bdd9cb7d04dc27ef Mon Sep 17 00:00:00 2001 From: Henry Date: Tue, 21 Oct 2014 13:02:25 +0200 Subject: [PATCH 15/17] updateApply: force shutdown check on daemon (TODO: force flag in cli) --- core/commands/update.go | 23 +++++++++++++++++++++-- daemon/daemon.go | 1 + 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/core/commands/update.go b/core/commands/update.go index 95a4f8a44d3..67a1f6285af 100644 --- a/core/commands/update.go +++ b/core/commands/update.go @@ -24,13 +24,32 @@ func UpdateApply(n *core.IpfsNode, args []string, opts map[string]interface{}, o } fmt.Fprintln(out, "New Version:", u.Version) + _, onDaemon := opts["onDaemon"] + _, force := opts["force"] + if onDaemon && !force { + return fmt.Errorf(`Error: update must stop running ipfs service. +You may want to abort the update, or shut the service down manually. +To shut it down automatically, run: + + ipfs update apply -f +`) + } + if err = updates.Apply(u); err != nil { fmt.Fprint(out, err.Error()) return fmt.Errorf("Couldn't apply update: %v", err) } - fmt.Fprintln(out, "Updated applied! Shutting down.") - os.Exit(0) + fmt.Fprintln(out, "Updated applied!") + if onDaemon { + if force { + fmt.Fprintln(out, "Shutting down ipfs service.") + os.Exit(1) // is there a cleaner shutdown routine? + } else { + fmt.Fprintln(out, "You can now restart the ipfs service.") + } + } + return nil } diff --git a/daemon/daemon.go b/daemon/daemon.go index b4241e16b77..a3790971ebc 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -142,6 +142,7 @@ func (dl *DaemonListener) handleConnection(conn manet.Conn) { case "unpin": err = commands.Unpin(dl.node, command.Args, command.Opts, conn) case "updateApply": + command.Opts["onDaemon"] = true err = commands.UpdateApply(dl.node, command.Args, command.Opts, conn) default: err = fmt.Errorf("Invalid Command: '%s'", command.Command) From d9552fea185937b603a297e47ce9d31799840215 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Oct 2014 12:34:00 +0200 Subject: [PATCH 16/17] updates: printing fixes --- updates/updates.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/updates/updates.go b/updates/updates.go index ba9fce1b3d3..3d560f06d57 100644 --- a/updates/updates.go +++ b/updates/updates.go @@ -36,6 +36,7 @@ func init() { log.Error("invalid version number in code (must be semver): %q\n", Version) os.Exit(1) } + log.Info("go-ipfs Version: %s", currentVersion) } func parseVersion() (*semver.Version, error) { @@ -166,7 +167,7 @@ func CliCheckForUpdates(cfg *config.Config, confFile string) error { } // BUG(cryptix): no good way to restart yet. - tracking https://github.com/inconshreveable/go-update/issues/5 - fmt.Println("update %v applied. please restart.", u.Version) + fmt.Printf("update %v applied. please restart.\n", u.Version) os.Exit(0) } } @@ -177,7 +178,7 @@ func CliCheckForUpdates(cfg *config.Config, confFile string) error { return fmt.Errorf(errShouldUpdate, Version, u.Version) case config.CheckWarn: // print the warning - fmt.Printf("New version available: %s", u.Version) + fmt.Printf("New version available: %s\n", u.Version) default: // ignore } return nil From 4f42a26836fe9e38325ff8c2ec20829a04a1b259 Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 22 Oct 2014 12:42:13 +0200 Subject: [PATCH 17/17] fixed --force flag for shutdown after update --- cmd/ipfs/update.go | 6 +++++- core/commands/update.go | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/ipfs/update.go b/cmd/ipfs/update.go index 0391df98aaa..e6d078253e0 100644 --- a/cmd/ipfs/update.go +++ b/cmd/ipfs/update.go @@ -6,6 +6,10 @@ import ( "github.com/jbenet/go-ipfs/core/commands" ) +func init() { + cmdIpfsUpdate.Flag.Bool("force", false, "force shutdown of daemon when updating") +} + var cmdIpfsUpdate = &commander.Command{ UsageLine: "update", Short: "check for updates and apply them", @@ -20,7 +24,7 @@ I wont even try, @jbenet. You do this much better :)`, Run: makeCommand(command{ name: "updateApply", args: 0, - flags: nil, + flags: []string{"force"}, online: true, cmdFn: commands.UpdateApply, }), diff --git a/core/commands/update.go b/core/commands/update.go index 67a1f6285af..68f9456dddb 100644 --- a/core/commands/update.go +++ b/core/commands/update.go @@ -25,13 +25,13 @@ func UpdateApply(n *core.IpfsNode, args []string, opts map[string]interface{}, o fmt.Fprintln(out, "New Version:", u.Version) _, onDaemon := opts["onDaemon"] - _, force := opts["force"] + force := opts["force"].(bool) if onDaemon && !force { return fmt.Errorf(`Error: update must stop running ipfs service. You may want to abort the update, or shut the service down manually. To shut it down automatically, run: - ipfs update apply -f + ipfs update --force `) }