master
go 191 lines 5.29 KB
Raw
1 package migrations
2
3 import (
4 "bufio"
5 "bytes"
6 "context"
7 "fmt"
8 "io"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "runtime"
13 "strings"
14 )
15
16 // DownloadDirectory can be set as the location for FetchBinary to save the
17 // downloaded archive file in. If not set, then FetchBinary saves the archive
18 // in a temporary directory that is removed after the contents of the archive
19 // is extracted.
20 var DownloadDirectory string
21
22 // FetchBinary downloads an archive from the distribution site and unpacks it.
23 //
24 // The base name of the binary inside the archive may differ from the base
25 // archive name. If it does, then specify binName. For example, the following
26 // is needed because the archive "go-ipfs_v0.7.0_linux-amd64.tar.gz" contains a
27 // binary named "ipfs"
28 //
29 // FetchBinary(ctx, fetcher, "go-ipfs", "v0.7.0", "ipfs", tmpDir)
30 //
31 // If out is a directory, then the binary is written to that directory with the
32 // same name it has inside the archive. Otherwise, the binary file is written
33 // to the file named by out.
34 func FetchBinary(ctx context.Context, fetcher Fetcher, dist, ver, binName, out string) (string, error) {
35 // The archive file name is the base of dist. This is to support a possible subdir in
36 // dist, for example: "ipfs-repo-migrations/fs-repo-11-to-12"
37 arcName := filepath.Base(dist)
38 // If binary base name is not specified, then it is same as archive base name.
39 if binName == "" {
40 binName = arcName
41 }
42
43 // Name of binary that exists inside archive
44 binName = ExeName(binName)
45
46 // Return error if file exists or stat fails for reason other than not
47 // exists. If out is a directory, then write extracted binary to that dir.
48 fi, err := os.Stat(out)
49 if !os.IsNotExist(err) {
50 if err != nil {
51 return "", err
52 }
53 if !fi.IsDir() {
54 return "", &os.PathError{
55 Op: "FetchBinary",
56 Path: out,
57 Err: os.ErrExist,
58 }
59 }
60 // out exists and is a directory, so compose final name
61 out = filepath.Join(out, binName)
62 // Check if the binary already exists in the directory
63 _, err = os.Stat(out)
64 if !os.IsNotExist(err) {
65 if err != nil {
66 return "", err
67 }
68 return "", &os.PathError{
69 Op: "FetchBinary",
70 Path: out,
71 Err: os.ErrExist,
72 }
73 }
74 }
75
76 tmpDir := DownloadDirectory
77 if tmpDir != "" {
78 fi, err = os.Stat(tmpDir)
79 if err != nil {
80 return "", err
81 }
82 if !fi.IsDir() {
83 return "", &os.PathError{
84 Op: "FetchBinary",
85 Path: tmpDir,
86 Err: os.ErrExist,
87 }
88 }
89 } else {
90 // Create temp directory to store download
91 tmpDir, err = os.MkdirTemp("", arcName)
92 if err != nil {
93 return "", err
94 }
95 defer os.RemoveAll(tmpDir)
96 }
97
98 atype := "tar.gz"
99 if runtime.GOOS == "windows" {
100 atype = "zip"
101 }
102
103 arcDistPath, arcFullName := makeArchivePath(dist, arcName, ver, atype)
104
105 // Create a file to write the archive data to
106 arcPath := filepath.Join(tmpDir, arcFullName)
107 arcFile, err := os.Create(arcPath)
108 if err != nil {
109 return "", err
110 }
111 defer arcFile.Close()
112
113 // Open connection to download archive from ipfs path and write to file
114 arcBytes, err := fetcher.Fetch(ctx, arcDistPath)
115 if err != nil {
116 return "", err
117 }
118
119 // Write download data
120 _, err = io.Copy(arcFile, bytes.NewReader(arcBytes))
121 if err != nil {
122 return "", err
123 }
124 arcFile.Close()
125
126 // Unpack the archive and write binary to out
127 err = unpackArchive(arcPath, atype, dist, binName, out)
128 if err != nil {
129 return "", err
130 }
131
132 // Set mode of binary to executable
133 err = os.Chmod(out, 0o755)
134 if err != nil {
135 return "", err
136 }
137
138 return out, nil
139 }
140
141 // osWithVariant returns the OS name with optional variant.
142 // Currently returns either runtime.GOOS, or "linux-musl".
143 func osWithVariant() (string, error) {
144 if runtime.GOOS != "linux" {
145 return runtime.GOOS, nil
146 }
147
148 // ldd outputs the system's kind of libc.
149 // - on standard ubuntu: ldd (Ubuntu GLIBC 2.23-0ubuntu5) 2.23
150 // - on alpine: musl libc (x86_64)
151 //
152 // we use the combined stdout+stderr,
153 // because ldd --version prints differently on different OSes.
154 // - on standard ubuntu: stdout
155 // - on alpine: stderr (it probably doesn't know the --version flag)
156 //
157 // we suppress non-zero exit codes (see last point about alpine).
158 out, err := exec.Command("sh", "-c", "ldd --version || true").CombinedOutput()
159 if err != nil {
160 return "", err
161 }
162
163 // now just see if we can find "musl" somewhere in the output
164 scan := bufio.NewScanner(bytes.NewBuffer(out))
165 for scan.Scan() {
166 if strings.Contains(scan.Text(), "musl") {
167 return "linux-musl", nil
168 }
169 }
170
171 return "linux", nil
172 }
173
174 // makeArchivePath composes the path, relative to the distribution site, from which to
175 // download a binary. The path returned does not contain the distribution site path,
176 // e.g. "/ipns/dist.ipfs.tech/", since that is know to the fetcher.
177 //
178 // Returns the archive path and the base name.
179 //
180 // The ipfs path format is: distribution/version/archiveName
181 // - distribution is the name of a distribution, such as "go-ipfs"
182 // - version is the version to fetch, such as "v0.8.0-rc2"
183 // - archiveName is formatted as name_version_osv-GOARCH.atype, such as
184 // "go-ipfs_v0.8.0-rc2_linux-amd64.tar.gz"
185 //
186 // This would form the path:
187 // go-ipfs/v0.8.0/go-ipfs_v0.8.0_linux-amd64.tar.gz
188 func makeArchivePath(dist, name, ver, atype string) (string, string) {
189 arcName := fmt.Sprintf("%s_%s_%s-%s.%s", name, ver, runtime.GOOS, runtime.GOARCH, atype)
190 return fmt.Sprintf("%s/%s/%s", dist, ver, arcName), arcName
191 }