migrations.go 5.87 KB
Newer Older
1 2 3 4
package mfsr

import (
	"bufio"
5
	"bytes"
6 7 8 9 10 11 12 13 14 15 16 17
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
)

18
var DistPath = "https://ipfs.io/ipfs/QmUgY7wRGB7bEKREE5BNrSwjzrRZddkRV4bu64bP2qgv7f"
19

Jeromy's avatar
Jeromy committed
20 21 22 23 24 25
func init() {
	if dist := os.Getenv("IPFS_DIST_PATH"); dist != "" {
		DistPath = dist
	}
}

26 27
const migrations = "fs-repo-migrations"

28 29 30 31 32 33 34 35 36
func migrationsBinName() string {
	switch runtime.GOOS {
	case "windows":
		return migrations + ".exe"
	default:
		return migrations
	}
}

37
func RunMigration(newv int) error {
38 39
	migrateBin := migrationsBinName()

Lars Gierth's avatar
Lars Gierth committed
40
	fmt.Println("  => Looking for suitable fs-repo-migrations binary.")
41 42 43

	var err error
	migrateBin, err = exec.LookPath(migrateBin)
44 45 46 47 48 49
	if err == nil {
		// check to make sure migrations binary supports our target version
		err = verifyMigrationSupportsVersion(migrateBin, newv)
	}

	if err != nil {
Lars Gierth's avatar
Lars Gierth committed
50 51
		fmt.Println("  => None found, downloading.")

52 53
		loc, err := GetMigrations()
		if err != nil {
Lars Gierth's avatar
Lars Gierth committed
54
			fmt.Println("  => Failed to download fs-repo-migrations.")
55 56 57 58 59
			return err
		}

		err = verifyMigrationSupportsVersion(loc, newv)
		if err != nil {
Lars Gierth's avatar
Lars Gierth committed
60
			return fmt.Errorf("no fs-repo-migration binary found for version %d: %s", newv, err)
61 62 63 64 65 66 67 68 69
		}

		migrateBin = loc
	}

	cmd := exec.Command(migrateBin, "-to", fmt.Sprint(newv), "-y")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

Lars Gierth's avatar
Lars Gierth committed
70
	fmt.Printf("  => Running: %s -to %d -y\n", migrateBin, newv)
71 72 73

	err = cmd.Run()
	if err != nil {
Lars Gierth's avatar
Lars Gierth committed
74
		fmt.Printf("  => Failed: %s -to %d -y\n", migrateBin, newv)
75 76 77
		return fmt.Errorf("migration failed: %s", err)
	}

Lars Gierth's avatar
Lars Gierth committed
78
	fmt.Printf("  => Success: fs-repo has been migrated to version %d.\n", newv)
79 80 81 82 83 84 85

	return nil
}

func GetMigrations() (string, error) {
	latest, err := GetLatestVersion(DistPath, migrations)
	if err != nil {
Lars Gierth's avatar
Lars Gierth committed
86
		return "", fmt.Errorf("failed to find latest fs-repo-migrations: %s", err)
87 88 89 90
	}

	dir, err := ioutil.TempDir("", "go-ipfs-migrate")
	if err != nil {
Lars Gierth's avatar
Lars Gierth committed
91
		return "", fmt.Errorf("failed to create fs-repo-migrations tempdir: %s", err)
92 93
	}

94
	out := filepath.Join(dir, migrationsBinName())
95 96 97

	err = GetBinaryForVersion(migrations, migrations, DistPath, latest, out)
	if err != nil {
Lars Gierth's avatar
Lars Gierth committed
98
		return "", fmt.Errorf("failed to download latest fs-repo-migrations: %s", err)
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
	}

	err = os.Chmod(out, 0755)
	if err != nil {
		return "", err
	}

	return out, nil
}

func verifyMigrationSupportsVersion(fsrbin string, vn int) error {
	sn, err := migrationsVersion(fsrbin)
	if err != nil {
		return err
	}

	if sn >= vn {
		return nil
	}

119
	return fmt.Errorf("migrations binary doesnt support version %d: %s", vn, fsrbin)
120 121 122 123 124 125 126 127 128 129 130
}

func migrationsVersion(bin string) (int, error) {
	out, err := exec.Command(bin, "-v").CombinedOutput()
	if err != nil {
		return 0, fmt.Errorf("failed to check migrations version: %s", err)
	}

	vs := strings.Trim(string(out), " \n\t")
	vn, err := strconv.Atoi(vs)
	if err != nil {
131
		return 0, fmt.Errorf("migrations binary version check did not return a number: %s", err)
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
	}

	return vn, nil
}

func GetVersions(ipfspath, dist string) ([]string, error) {
	rc, err := httpFetch(ipfspath + "/" + dist + "/versions")
	if err != nil {
		return nil, err
	}
	defer rc.Close()

	var out []string
	scan := bufio.NewScanner(rc)
	for scan.Scan() {
		out = append(out, scan.Text())
	}

	return out, nil
}

func GetLatestVersion(ipfspath, dist string) (string, error) {
	vs, err := GetVersions(ipfspath, dist)
	if err != nil {
		return "", err
	}
	var latest string
	for i := len(vs) - 1; i >= 0; i-- {
		if !strings.Contains(vs[i], "-dev") {
			latest = vs[i]
			break
		}
	}
	if latest == "" {
		return "", fmt.Errorf("couldnt find a non dev version in the list")
	}
	return vs[len(vs)-1], nil
}

func httpGet(url string) (*http.Response, error) {
	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, fmt.Errorf("http.NewRequest error: %s", err)
	}

	req.Header.Set("User-Agent", "go-ipfs")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("http.DefaultClient.Do error: %s", err)
	}

	return resp, nil
}

func httpFetch(url string) (io.ReadCloser, error) {
	resp, err := httpGet(url)
	if err != nil {
		return nil, err
	}

	if resp.StatusCode >= 400 {
		mes, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return nil, fmt.Errorf("error reading error body: %s", err)
		}

Lars Gierth's avatar
Lars Gierth committed
199
		return nil, fmt.Errorf("GET %s error: %s: %s", url, resp.Status, string(mes))
200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
	}

	return resp.Body, nil
}

func GetBinaryForVersion(distname, binnom, root, vers, out string) error {
	dir, err := ioutil.TempDir("", "go-ipfs-auto-migrate")
	if err != nil {
		return err
	}

	var archive string
	switch runtime.GOOS {
	case "windows":
		archive = "zip"
215
		binnom += ".exe"
216 217 218
	default:
		archive = "tar.gz"
	}
219 220 221 222 223
	osv, err := osWithVariant()
	if err != nil {
		return err
	}
	finame := fmt.Sprintf("%s_%s_%s-%s.%s", distname, vers, osv, runtime.GOARCH, archive)
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
	distpath := fmt.Sprintf("%s/%s/%s/%s", root, distname, vers, finame)

	data, err := httpFetch(distpath)
	if err != nil {
		return err
	}

	arcpath := filepath.Join(dir, finame)
	fi, err := os.Create(arcpath)
	if err != nil {
		return err
	}

	_, err = io.Copy(fi, data)
	if err != nil {
		return err
	}
	fi.Close()

	return unpackArchive(distname, binnom, arcpath, out, archive)
}
245 246 247 248 249 250 251 252 253 254 255

func osWithVariant() (string, error) {
	if runtime.GOOS != "linux" {
		return runtime.GOOS, nil
	}

	bin, err := exec.LookPath(filepath.Base(os.Args[0]))
	if err != nil {
		return "", fmt.Errorf("failed to resolve go-ipfs: %s", err)
	}

256 257 258 259
	// ldd outputs the system's kind of libc
	// - on standard ubuntu: ldd (Ubuntu GLIBC 2.23-0ubuntu5) 2.23
	// - on alpine: musl libc (x86_64)
	cmd := exec.Command("ldd --version", bin)
260 261
	buf := new(bytes.Buffer)
	cmd.Stdout = buf
262 263 264
	// we throw away the error, this code path must not fail because of
	// a silly issue such as missing/broken ldd. we'll assume glibc in that case.
	_ = cmd.Run()
265 266 267

	scan := bufio.NewScanner(buf)
	for scan.Scan() {
268
		if strings.Contains(scan.Text(), "musl") {
269 270 271 272 273 274
			return "linux-musl", nil
		}
	}

	return "linux", nil
}