Expose additional migration APIs (#8153)
* Expose additional migration APIs Expose migration APIs for reading migration config and creating migration fetchers. This allows implementation of commands and external applications that want to retrieve migrations according to the Migration portion of the IPFS config. This change also moves some functionality that is specific to fetching migrations via IPFS into the `ipfsfetcher` package.
Andrew Gillis committed
Jul 30, 2021 at 11:27 UTC
3b6f57788bf9d0be3e7b45d9c52813d17d71732b
10 files changed
+526
-511
cmd/ipfs/add_migrations.go
renamed
+1
-138
@@ -2,17 +2,13 @@ package main
2
3
import (
4
"context"
5
- "encoding/json"
5
"errors"
6
"fmt"
7
"io"
8
"io/ioutil"
10
- "net/url"
9
"os"
10
"path/filepath"
13
- "strings"
11
15
- config "github.com/ipfs/go-ipfs-config"
12
"github.com/ipfs/go-ipfs-files"
13
"github.com/ipfs/go-ipfs/core"
14
"github.com/ipfs/go-ipfs/core/coreapi"
@@ -24,140 +20,7 @@ import (
20
"github.com/libp2p/go-libp2p-core/peer"
21
)
22
27
-// readMigrationConfig reads the migration config out of the config, avoiding
28
-// reading anything other than the migration section. That way, we're free to
29
-// make arbitrary changes to all _other_ sections in migrations.
30
-func readMigrationConfig(repoRoot string) (*config.Migration, error) {
31
- var cfg struct {
32
- Migration config.Migration
33
- }
34
-
35
- cfgPath, err := config.Filename(repoRoot)
36
- if err != nil {
37
- return nil, err
38
- }
39
-
40
- cfgFile, err := os.Open(cfgPath)
41
- if err != nil {
42
- return nil, err
43
- }
44
- defer cfgFile.Close()
45
-
46
- err = json.NewDecoder(cfgFile).Decode(&cfg)
47
- if err != nil {
48
- return nil, err
49
- }
50
-
51
- switch cfg.Migration.Keep {
52
- case "":
53
- cfg.Migration.Keep = config.DefaultMigrationKeep
54
- case "discard", "cache", "keep":
55
- default:
56
- return nil, errors.New("unknown config value, Migrations.Keep must be 'cache', 'pin', or 'discard'")
57
- }
58
-
59
- if len(cfg.Migration.DownloadSources) == 0 {
60
- cfg.Migration.DownloadSources = config.DefaultMigrationDownloadSources
61
- }
62
-
63
- return &cfg.Migration, nil
64
-}
65
-
66
-func readIpfsConfig(repoRoot *string) (bootstrap []string, peers []peer.AddrInfo) {
67
- if repoRoot == nil {
68
- return
69
- }
70
-
71
- cfgPath, err := config.Filename(*repoRoot)
72
- if err != nil {
73
- fmt.Fprintln(os.Stderr, err)
74
- return
75
- }
76
-
77
- cfgFile, err := os.Open(cfgPath)
78
- if err != nil {
79
- fmt.Fprintln(os.Stderr, err)
80
- return
81
- }
82
- defer cfgFile.Close()
83
-
84
- // Attempt to read bootstrap addresses
85
- var bootstrapCfg struct {
86
- Bootstrap []string
87
- }
88
- err = json.NewDecoder(cfgFile).Decode(&bootstrapCfg)
89
- if err != nil {
90
- fmt.Fprintln(os.Stderr, "cannot read bootstrap peers from config")
91
- } else {
92
- bootstrap = bootstrapCfg.Bootstrap
93
- }
94
-
95
- if _, err = cfgFile.Seek(0, 0); err != nil {
96
- fmt.Fprintln(os.Stderr, err)
97
- }
98
-
99
- // Attempt to read peers
100
- var peeringCfg struct {
101
- Peering config.Peering
102
- }
103
- err = json.NewDecoder(cfgFile).Decode(&peeringCfg)
104
- if err != nil {
105
- fmt.Fprintln(os.Stderr, "cannot read peering from config")
106
- } else {
107
- peers = peeringCfg.Peering.Peers
108
- }
109
-
110
- return
111
-}
112
-
113
-// getMigrationFetcher creates one or more fetchers according to
114
-// config.Migration.DownloadSources. If an IpfsFetcher is required, then
115
-// bootstrap and peer information in read from the config file in repoRoot,
116
-// unless repoRoot is nil.
117
-func getMigrationFetcher(cfg *config.Migration, repoRoot *string) (migrations.Fetcher, error) {
118
- const httpUserAgent = "go-ipfs"
119
-
120
- // Fetch migrations from current distribution, or location from environ
121
- fetchDistPath := migrations.GetDistPathEnv(migrations.CurrentIpfsDist)
122
-
123
- var fetchers []migrations.Fetcher
124
- for _, src := range cfg.DownloadSources {
125
- src := strings.TrimSpace(src)
126
- switch src {
127
- case "IPFS", "ipfs":
128
- bootstrap, peers := readIpfsConfig(repoRoot)
129
- fetchers = append(fetchers, ipfsfetcher.NewIpfsFetcher(fetchDistPath, 0, bootstrap, peers))
130
- case "HTTPS", "https", "HTTP", "http":
131
- fetchers = append(fetchers, migrations.NewHttpFetcher(fetchDistPath, "", httpUserAgent, 0))
132
- default:
133
- u, err := url.Parse(src)
134
- if err != nil {
135
- return nil, fmt.Errorf("bad gateway address: %s", err)
136
- }
137
- switch u.Scheme {
138
- case "":
139
- u.Scheme = "https"
140
- case "https", "http":
141
- default:
142
- return nil, errors.New("bad gateway address: url scheme must be http or https")
143
- }
144
- fetchers = append(fetchers, migrations.NewHttpFetcher(fetchDistPath, u.String(), httpUserAgent, 0))
145
- case "":
146
- // Ignore empty string
147
- }
148
- }
149
- if len(fetchers) == 0 {
150
- return nil, errors.New("no sources specified")
151
- }
152
-
153
- if len(fetchers) == 1 {
154
- return fetchers[0], nil
155
- }
156
-
157
- // Wrap fetchers in a MultiFetcher to try them in order
158
- return migrations.NewMultiFetcher(fetchers...), nil
159
-}
160
-
23
+// addMigrations adds any migration downloaded by the fetcher to the IPFS node
24
func addMigrations(ctx context.Context, node *core.IpfsNode, fetcher migrations.Fetcher, pin bool) error {
25
var fetchers []migrations.Fetcher
26
if mf, ok := fetcher.(*migrations.MultiFetcher); ok {
cmd/ipfs/daemon.go
+17
-2
@@ -30,6 +30,7 @@ import (
30
nodeMount "github.com/ipfs/go-ipfs/fuse/node"
31
fsrepo "github.com/ipfs/go-ipfs/repo/fsrepo"
32
"github.com/ipfs/go-ipfs/repo/fsrepo/migrations"
33
+ "github.com/ipfs/go-ipfs/repo/fsrepo/migrations/ipfsfetcher"
34
sockets "github.com/libp2p/go-socket-activation"
35
36
cmds "github.com/ipfs/go-ipfs-cmds"
@@ -294,12 +295,26 @@ func daemonFunc(req *cmds.Request, re cmds.ResponseEmitter, env cmds.Environment
295
return fmt.Errorf("fs-repo requires migration")
296
}
297
297
- migrationCfg, err := readMigrationConfig(cctx.ConfigRoot)
298
+ // Read Migration section of IPFS config
299
+ migrationCfg, err := migrations.ReadMigrationConfig(cctx.ConfigRoot)
300
if err != nil {
301
return err
302
}
303
302
- fetcher, err = getMigrationFetcher(migrationCfg, &cctx.ConfigRoot)
304
+ // Define function to create IPFS fetcher. Do not supply an
305
+ // already-constructed IPFS fetcher, because this may be expensive and
306
+ // not needed according to migration config. Instead, supply a function
307
+ // to construct the particular IPFS fetcher implementation used here,
308
+ // which is called only if an IPFS fetcher is needed.
309
+ newIpfsFetcher := func(distPath string) migrations.Fetcher {
310
+ return ipfsfetcher.NewIpfsFetcher(distPath, 0, &cctx.ConfigRoot)
311
+ }
312
+
313
+ // Fetch migrations from current distribution, or location from environ
314
+ fetchDistPath := migrations.GetDistPathEnv(migrations.CurrentIpfsDist)
315
+
316
+ // Create fetchers according to migrationCfg.DownloadSources
317
+ fetcher, err = migrations.GetMigrationFetcher(migrationCfg.DownloadSources, fetchDistPath, newIpfsFetcher)
318
if err != nil {
319
return err
320
}
cmd/ipfs/migration_test.go
deleted
-312
@@ -1,312 +0,0 @@
1
-package main
2
-
3
-import (
4
- "io/ioutil"
5
- "os"
6
- "path/filepath"
7
- "strings"
8
- "testing"
9
-
10
- config "github.com/ipfs/go-ipfs-config"
11
- "github.com/ipfs/go-ipfs/repo/fsrepo/migrations"
12
- "github.com/ipfs/go-ipfs/repo/fsrepo/migrations/ipfsfetcher"
13
-)
14
-
15
-var testConfig = `
16
-{
17
- "Bootstrap": [
18
- "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
19
- "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
20
- ],
21
- "Migration": {
22
- "DownloadSources": ["IPFS", "HTTP", "127.0.0.1", "https://127.0.1.1"],
23
- "Keep": "cache"
24
- },
25
- "Peering": {
26
- "Peers": [
27
- {
28
- "ID": "12D3KooWGC6TvWhfapngX6wvJHMYvKpDMXPb3ZnCZ6dMoaMtimQ5",
29
- "Addrs": ["/ip4/127.0.0.1/tcp/4001", "/ip4/127.0.0.1/udp/4001/quic"]
30
- }
31
- ]
32
- }
33
-}
34
-`
35
-
36
-func TestReadMigrationConfigDefaults(t *testing.T) {
37
- tmpDir := makeConfig("{}")
38
- defer os.RemoveAll(tmpDir)
39
-
40
- cfg, err := readMigrationConfig(tmpDir)
41
- if err != nil {
42
- t.Fatal(err)
43
- }
44
-
45
- if cfg.Keep != config.DefaultMigrationKeep {
46
- t.Error("expected default value for Keep")
47
- }
48
-
49
- if len(cfg.DownloadSources) != len(config.DefaultMigrationDownloadSources) {
50
- t.Fatal("expected default number of download sources")
51
- }
52
- for i, src := range config.DefaultMigrationDownloadSources {
53
- if cfg.DownloadSources[i] != src {
54
- t.Errorf("wrong DownloadSource: %s", cfg.DownloadSources[i])
55
- }
56
- }
57
-}
58
-
59
-func TestReadMigrationConfigErrors(t *testing.T) {
60
- tmpDir := makeConfig(`{"Migration": {"Keep": "badvalue"}}`)
61
- defer os.RemoveAll(tmpDir)
62
-
63
- _, err := readMigrationConfig(tmpDir)
64
- if err == nil {
65
- t.Fatal("expected error")
66
- }
67
- if !strings.HasPrefix(err.Error(), "unknown") {
68
- t.Fatal("did not get expected error:", err)
69
- }
70
-
71
- os.RemoveAll(tmpDir)
72
- _, err = readMigrationConfig(tmpDir)
73
- if err == nil {
74
- t.Fatal("expected error")
75
- }
76
-
77
- bootstrap, peers := readIpfsConfig(&tmpDir)
78
- if bootstrap != nil {
79
- t.Error("expected nil bootstrap")
80
- }
81
- if peers != nil {
82
- t.Error("expected nil peers")
83
- }
84
-
85
- tmpDir = makeConfig(`}{`)
86
- defer os.RemoveAll(tmpDir)
87
- _, err = readMigrationConfig(tmpDir)
88
- if err == nil {
89
- t.Fatal("expected error")
90
- }
91
-}
92
-
93
-func TestReadMigrationConfig(t *testing.T) {
94
- tmpDir := makeConfig(testConfig)
95
- defer os.RemoveAll(tmpDir)
96
-
97
- cfg, err := readMigrationConfig(tmpDir)
98
- if err != nil {
99
- t.Fatal(err)
100
- }
101
-
102
- if len(cfg.DownloadSources) != 4 {
103
- t.Fatal("wrong number of DownloadSources")
104
- }
105
- expect := []string{"IPFS", "HTTP", "127.0.0.1", "https://127.0.1.1"}
106
- for i := range expect {
107
- if cfg.DownloadSources[i] != expect[i] {
108
- t.Errorf("wrong DownloadSource at %d", i)
109
- }
110
- }
111
-
112
- if cfg.Keep != "cache" {
113
- t.Error("wrong value for Keep")
114
- }
115
-}
116
-
117
-func TestReadIpfsConfig(t *testing.T) {
118
- tmpDir := makeConfig(testConfig)
119
- defer os.RemoveAll(tmpDir)
120
-
121
- bootstrap, peers := readIpfsConfig(nil)
122
- if bootstrap != nil || peers != nil {
123
- t.Fatal("expected nil ipfs config items")
124
- }
125
-
126
- bootstrap, peers = readIpfsConfig(&tmpDir)
127
- if len(bootstrap) != 2 {
128
- t.Fatal("wrong number of bootstrap addresses")
129
- }
130
- if bootstrap[0] != "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt" {
131
- t.Fatal("wrong bootstrap address")
132
- }
133
-
134
- if len(peers) != 1 {
135
- t.Fatal("wrong number of peers")
136
- }
137
-
138
- peer := peers[0]
139
- if peer.ID.String() != "12D3KooWGC6TvWhfapngX6wvJHMYvKpDMXPb3ZnCZ6dMoaMtimQ5" {
140
- t.Errorf("wrong ID for first peer")
141
- }
142
- if len(peer.Addrs) != 2 {
143
- t.Error("wrong number of addrs for first peer")
144
- }
145
-}
146
-
147
-func TestReadPartialIpfsConfig(t *testing.T) {
148
- const (
149
- configBadBootstrap = `
150
-{
151
- "Bootstrap": "unreadable",
152
- "Migration": {
153
- "DownloadSources": ["IPFS", "HTTP", "127.0.0.1"],
154
- "Keep": "cache"
155
- },
156
- "Peering": {
157
- "Peers": [
158
- {
159
- "ID": "12D3KooWGC6TvWhfapngX6wvJHMYvKpDMXPb3ZnCZ6dMoaMtimQ5",
160
- "Addrs": ["/ip4/127.0.0.1/tcp/4001", "/ip4/127.0.0.1/udp/4001/quic"]
161
- }
162
- ]
163
- }
164
-}
165
-`
166
- configBadPeers = `
167
-{
168
- "Bootstrap": [
169
- "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
170
- "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
171
- ],
172
- "Migration": {
173
- "DownloadSources": ["IPFS", "HTTP", "127.0.0.1"],
174
- "Keep": "cache"
175
- },
176
- "Peering": "Unreadable-data"
177
-}
178
-`
179
- )
180
-
181
- tmpDir := makeConfig(configBadBootstrap)
182
- defer os.RemoveAll(tmpDir)
183
-
184
- bootstrap, peers := readIpfsConfig(&tmpDir)
185
- if bootstrap != nil {
186
- t.Fatal("expected nil bootstrap")
187
- }
188
- if len(peers) != 1 {
189
- t.Fatal("wrong number of peers")
190
- }
191
- if len(peers[0].Addrs) != 2 {
192
- t.Error("wrong number of addrs for first peer")
193
- }
194
- os.RemoveAll(tmpDir)
195
-
196
- tmpDir = makeConfig(configBadPeers)
197
- defer os.RemoveAll(tmpDir)
198
-
199
- bootstrap, peers = readIpfsConfig(&tmpDir)
200
- if peers != nil {
201
- t.Fatal("expected nil peers")
202
- }
203
- if len(bootstrap) != 2 {
204
- t.Fatal("wrong number of bootstrap addresses")
205
- }
206
- if bootstrap[0] != "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt" {
207
- t.Fatal("wrong bootstrap address")
208
- }
209
-}
210
-
211
-func makeConfig(configData string) string {
212
- tmpDir, err := ioutil.TempDir("", "migration_test")
213
- if err != nil {
214
- panic(err)
215
- }
216
-
217
- cfgFile, err := os.Create(filepath.Join(tmpDir, "config"))
218
- if err != nil {
219
- panic(err)
220
- }
221
- if _, err = cfgFile.Write([]byte(configData)); err != nil {
222
- panic(err)
223
- }
224
- if err = cfgFile.Close(); err != nil {
225
- panic(err)
226
- }
227
- return tmpDir
228
-}
229
-
230
-func TestGetMigrationFetcher(t *testing.T) {
231
- var f migrations.Fetcher
232
- var err error
233
-
234
- cfg := &config.Migration{}
235
-
236
- cfg.DownloadSources = []string{"ftp://bad.gateway.io"}
237
- _, err = getMigrationFetcher(cfg, nil)
238
- if err == nil || !strings.HasPrefix(err.Error(), "bad gateway addr") {
239
- t.Fatal("Expected bad gateway address error, got:", err)
240
- }
241
-
242
- cfg.DownloadSources = []string{"::bad.gateway.io"}
243
- _, err = getMigrationFetcher(cfg, nil)
244
- if err == nil || !strings.HasPrefix(err.Error(), "bad gateway addr") {
245
- t.Fatal("Expected bad gateway address error, got:", err)
246
- }
247
-
248
- cfg.DownloadSources = []string{"http://localhost"}
249
- f, err = getMigrationFetcher(cfg, nil)
250
- if err != nil {
251
- t.Fatal(err)
252
- }
253
- if _, ok := f.(*migrations.HttpFetcher); !ok {
254
- t.Fatal("expected HttpFetcher")
255
- }
256
-
257
- cfg.DownloadSources = []string{"ipfs"}
258
- f, err = getMigrationFetcher(cfg, nil)
259
- if err != nil {
260
- t.Fatal(err)
261
- }
262
- if _, ok := f.(*ipfsfetcher.IpfsFetcher); !ok {
263
- t.Fatal("expected IpfsFetcher")
264
- }
265
-
266
- cfg.DownloadSources = []string{"http"}
267
- f, err = getMigrationFetcher(cfg, nil)
268
- if err != nil {
269
- t.Fatal(err)
270
- }
271
- if _, ok := f.(*migrations.HttpFetcher); !ok {
272
- t.Fatal("expected HttpFetcher")
273
- }
274
-
275
- cfg.DownloadSources = []string{"IPFS", "HTTPS"}
276
- f, err = getMigrationFetcher(cfg, nil)
277
- if err != nil {
278
- t.Fatal(err)
279
- }
280
- mf, ok := f.(*migrations.MultiFetcher)
281
- if !ok {
282
- t.Fatal("expected MultiFetcher")
283
- }
284
- if mf.Len() != 2 {
285
- t.Fatal("expected 2 fetchers in MultiFetcher")
286
- }
287
-
288
- cfg.DownloadSources = []string{"ipfs", "https", "some.domain.io"}
289
- f, err = getMigrationFetcher(cfg, nil)
290
- if err != nil {
291
- t.Fatal(err)
292
- }
293
- mf, ok = f.(*migrations.MultiFetcher)
294
- if !ok {
295
- t.Fatal("expected MultiFetcher")
296
- }
297
- if mf.Len() != 3 {
298
- t.Fatal("expected 3 fetchers in MultiFetcher")
299
- }
300
-
301
- cfg.DownloadSources = nil
302
- _, err = getMigrationFetcher(cfg, nil)
303
- if err == nil {
304
- t.Fatal("expected error when no sources specified")
305
- }
306
-
307
- cfg.DownloadSources = []string{"", ""}
308
- _, err = getMigrationFetcher(cfg, nil)
309
- if err == nil {
310
- t.Fatal("expected error when empty string fetchers specified")
311
- }
312
-}
repo/fsrepo/migrations/fetch_test.go
+1
-5
@@ -127,11 +127,7 @@ func TestHttpFetch(t *testing.T) {
127
}
128
129
func TestFetchBinary(t *testing.T) {
130
- tmpDir, err := ioutil.TempDir("", "fetchtest")
131
- if err != nil {
132
- panic(err)
133
- }
134
- defer os.RemoveAll(tmpDir)
130
+ tmpDir := t.TempDir()
131
132
ctx, cancel := context.WithCancel(context.Background())
133
defer cancel()
repo/fsrepo/migrations/ipfsdir_test.go
+1
-7
@@ -13,13 +13,7 @@ var (
13
)
14
15
func TestRepoDir(t *testing.T) {
16
- var err error
17
- fakeHome, err = ioutil.TempDir("", "testhome")
18
- if err != nil {
19
- panic(err)
20
- }
21
- defer os.RemoveAll(fakeHome)
22
-
16
+ fakeHome = t.TempDir()
17
os.Setenv("HOME", fakeHome)
18
fakeIpfs = filepath.Join(fakeHome, ".ipfs")
19
repo/fsrepo/migrations/ipfsfetcher/ipfsfetcher.go
+63
-10
@@ -2,6 +2,7 @@ package ipfsfetcher
2
3
import (
4
"context"
5
+ "encoding/json"
6
"fmt"
7
"io"
8
"io/ioutil"
@@ -32,10 +33,9 @@ const (
33
)
34
35
type IpfsFetcher struct {
35
- distPath string
36
- limit int64
37
- bootstrap []string
38
- peers []peer.AddrInfo
36
+ distPath string
37
+ limit int64
38
+ repoRoot *string
39
40
openOnce sync.Once
41
openErr error
@@ -56,12 +56,15 @@ type IpfsFetcher struct {
56
//
57
// Specifying "" for distPath sets the default IPNS path.
58
// Specifying 0 for fetchLimit sets the default, -1 means no limit.
59
-func NewIpfsFetcher(distPath string, fetchLimit int64, bootstrap []string, peers []peer.AddrInfo) *IpfsFetcher {
59
+//
60
+// Bootstrap and peer information in read from the IPFS config file in
61
+// repoRoot, unless repoRoot is nil. If repoRoot is empty (""), then read the
62
+// config from the default IPFS directory.
63
+func NewIpfsFetcher(distPath string, fetchLimit int64, repoRoot *string) *IpfsFetcher {
64
f := &IpfsFetcher{
61
- limit: defaultFetchLimit,
62
- distPath: migrations.LatestIpfsDist,
63
- bootstrap: bootstrap,
64
- peers: peers,
65
+ limit: defaultFetchLimit,
66
+ distPath: migrations.LatestIpfsDist,
67
+ repoRoot: repoRoot,
68
}
69
70
if distPath != "" {
@@ -88,7 +91,8 @@ func (f *IpfsFetcher) Fetch(ctx context.Context, filePath string) (io.ReadCloser
91
// Initialize and start IPFS node on first call to Fetch, since the fetcher
92
// may be created by not used.
93
f.openOnce.Do(func() {
91
- f.ipfsTmpDir, f.openErr = initTempNode(ctx, f.bootstrap, f.peers)
94
+ bootstrap, peers := readIpfsConfig(f.repoRoot)
95
+ f.ipfsTmpDir, f.openErr = initTempNode(ctx, bootstrap, peers)
96
if f.openErr != nil {
97
return
98
}
@@ -277,3 +281,52 @@ func parsePath(fetchPath string) (ipath.Path, error) {
281
}
282
return ipfsPath, ipfsPath.IsValid()
283
}
284
+
285
+func readIpfsConfig(repoRoot *string) (bootstrap []string, peers []peer.AddrInfo) {
286
+ if repoRoot == nil {
287
+ return
288
+ }
289
+
290
+ cfgPath, err := config.Filename(*repoRoot)
291
+ if err != nil {
292
+ fmt.Fprintln(os.Stderr, err)
293
+ return
294
+ }
295
+
296
+ cfgFile, err := os.Open(cfgPath)
297
+ if err != nil {
298
+ fmt.Fprintln(os.Stderr, err)
299
+ return
300
+ }
301
+ defer cfgFile.Close()
302
+
303
+ // Attempt to read bootstrap addresses
304
+ var bootstrapCfg struct {
305
+ Bootstrap []string
306
+ }
307
+ err = json.NewDecoder(cfgFile).Decode(&bootstrapCfg)
308
+ if err != nil {
309
+ fmt.Fprintln(os.Stderr, "cannot read bootstrap peers from config")
310
+ } else {
311
+ bootstrap = bootstrapCfg.Bootstrap
312
+ }
313
+
314
+ if _, err = cfgFile.Seek(0, 0); err != nil {
315
+ // If Seek fails, only log the error and continue on to try to read the
316
+ // peering config anyway as it might still be readable
317
+ fmt.Fprintln(os.Stderr, err)
318
+ }
319
+
320
+ // Attempt to read peers
321
+ var peeringCfg struct {
322
+ Peering config.Peering
323
+ }
324
+ err = json.NewDecoder(cfgFile).Decode(&peeringCfg)
325
+ if err != nil {
326
+ fmt.Fprintln(os.Stderr, "cannot read peering from config")
327
+ } else {
328
+ peers = peeringCfg.Peering.Peers
329
+ }
330
+
331
+ return
332
+}
repo/fsrepo/migrations/ipfsfetcher/ipfsfetcher_test.go
+141
-3
@@ -25,7 +25,7 @@ func TestIpfsFetcher(t *testing.T) {
25
ctx, cancel := context.WithCancel(context.Background())
26
defer cancel()
27
28
- fetcher := NewIpfsFetcher("", 0, nil, nil)
28
+ fetcher := NewIpfsFetcher("", 0, nil)
29
defer fetcher.Close()
30
31
rc, err := fetcher.Fetch(ctx, "go-ipfs/versions")
@@ -63,11 +63,11 @@ func TestInitIpfsFetcher(t *testing.T) {
63
ctx, cancel := context.WithCancel(context.Background())
64
defer cancel()
65
66
- f := NewIpfsFetcher("", 0, nil, nil)
66
+ f := NewIpfsFetcher("", 0, nil)
67
defer f.Close()
68
69
// Init ipfs repo
70
- f.ipfsTmpDir, f.openErr = initTempNode(ctx, f.bootstrap, f.peers)
70
+ f.ipfsTmpDir, f.openErr = initTempNode(ctx, nil, nil)
71
if f.openErr != nil {
72
t.Fatalf("failed to initialize ipfs node: %s", f.openErr)
73
}
@@ -110,6 +110,144 @@ func TestInitIpfsFetcher(t *testing.T) {
110
}
111
}
112
113
+func TestReadIpfsConfig(t *testing.T) {
114
+ var testConfig = `
115
+{
116
+ "Bootstrap": [
117
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
118
+ "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
119
+ ],
120
+ "Migration": {
121
+ "DownloadSources": ["IPFS", "HTTP", "127.0.0.1", "https://127.0.1.1"],
122
+ "Keep": "cache"
123
+ },
124
+ "Peering": {
125
+ "Peers": [
126
+ {
127
+ "ID": "12D3KooWGC6TvWhfapngX6wvJHMYvKpDMXPb3ZnCZ6dMoaMtimQ5",
128
+ "Addrs": ["/ip4/127.0.0.1/tcp/4001", "/ip4/127.0.0.1/udp/4001/quic"]
129
+ }
130
+ ]
131
+ }
132
+}
133
+`
134
+
135
+ noSuchDir := "no_such_dir-5953aa51-1145-4efd-afd1-a069075fcf76"
136
+ bootstrap, peers := readIpfsConfig(&noSuchDir)
137
+ if bootstrap != nil {
138
+ t.Error("expected nil bootstrap")
139
+ }
140
+ if peers != nil {
141
+ t.Error("expected nil peers")
142
+ }
143
+
144
+ tmpDir := makeConfig(t, testConfig)
145
+
146
+ bootstrap, peers = readIpfsConfig(nil)
147
+ if bootstrap != nil || peers != nil {
148
+ t.Fatal("expected nil ipfs config items")
149
+ }
150
+
151
+ bootstrap, peers = readIpfsConfig(&tmpDir)
152
+ if len(bootstrap) != 2 {
153
+ t.Fatal("wrong number of bootstrap addresses")
154
+ }
155
+ if bootstrap[0] != "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt" {
156
+ t.Fatal("wrong bootstrap address")
157
+ }
158
+
159
+ if len(peers) != 1 {
160
+ t.Fatal("wrong number of peers")
161
+ }
162
+
163
+ peer := peers[0]
164
+ if peer.ID.String() != "12D3KooWGC6TvWhfapngX6wvJHMYvKpDMXPb3ZnCZ6dMoaMtimQ5" {
165
+ t.Errorf("wrong ID for first peer")
166
+ }
167
+ if len(peer.Addrs) != 2 {
168
+ t.Error("wrong number of addrs for first peer")
169
+ }
170
+}
171
+
172
+func TestBadBootstrappingIpfsConfig(t *testing.T) {
173
+ const configBadBootstrap = `
174
+{
175
+ "Bootstrap": "unreadable",
176
+ "Migration": {
177
+ "DownloadSources": ["IPFS", "HTTP", "127.0.0.1"],
178
+ "Keep": "cache"
179
+ },
180
+ "Peering": {
181
+ "Peers": [
182
+ {
183
+ "ID": "12D3KooWGC6TvWhfapngX6wvJHMYvKpDMXPb3ZnCZ6dMoaMtimQ5",
184
+ "Addrs": ["/ip4/127.0.0.1/tcp/4001", "/ip4/127.0.0.1/udp/4001/quic"]
185
+ }
186
+ ]
187
+ }
188
+}
189
+`
190
+
191
+ tmpDir := makeConfig(t, configBadBootstrap)
192
+
193
+ bootstrap, peers := readIpfsConfig(&tmpDir)
194
+ if bootstrap != nil {
195
+ t.Fatal("expected nil bootstrap")
196
+ }
197
+ if len(peers) != 1 {
198
+ t.Fatal("wrong number of peers")
199
+ }
200
+ if len(peers[0].Addrs) != 2 {
201
+ t.Error("wrong number of addrs for first peer")
202
+ }
203
+ os.RemoveAll(tmpDir)
204
+}
205
+
206
+func TestBadPeersIpfsConfig(t *testing.T) {
207
+ const configBadPeers = `
208
+{
209
+ "Bootstrap": [
210
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
211
+ "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
212
+ ],
213
+ "Migration": {
214
+ "DownloadSources": ["IPFS", "HTTP", "127.0.0.1"],
215
+ "Keep": "cache"
216
+ },
217
+ "Peering": "Unreadable-data"
218
+}
219
+`
220
+
221
+ tmpDir := makeConfig(t, configBadPeers)
222
+
223
+ bootstrap, peers := readIpfsConfig(&tmpDir)
224
+ if peers != nil {
225
+ t.Fatal("expected nil peers")
226
+ }
227
+ if len(bootstrap) != 2 {
228
+ t.Fatal("wrong number of bootstrap addresses")
229
+ }
230
+ if bootstrap[0] != "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt" {
231
+ t.Fatal("wrong bootstrap address")
232
+ }
233
+}
234
+
235
+func makeConfig(t *testing.T, configData string) string {
236
+ tmpDir := t.TempDir()
237
+
238
+ cfgFile, err := os.Create(filepath.Join(tmpDir, "config"))
239
+ if err != nil {
240
+ t.Fatal(err)
241
+ }
242
+ if _, err = cfgFile.Write([]byte(configData)); err != nil {
243
+ t.Fatal(err)
244
+ }
245
+ if err = cfgFile.Close(); err != nil {
246
+ t.Fatal(err)
247
+ }
248
+ return tmpDir
249
+}
250
+
251
func skipUnlessEpic(t *testing.T) {
252
if os.Getenv("IPFS_EPIC_TEST") == "" {
253
t.SkipNow()
repo/fsrepo/migrations/migrations.go
+88
@@ -2,15 +2,20 @@ package migrations
2
3
import (
4
"context"
5
+ "encoding/json"
6
+ "errors"
7
"fmt"
8
"io/ioutil"
9
"log"
10
+ "net/url"
11
"os"
12
"os/exec"
13
"path"
14
"runtime"
15
"strings"
16
"sync"
17
+
18
+ config "github.com/ipfs/go-ipfs-config"
19
)
20
21
const (
@@ -107,6 +112,89 @@ func ExeName(name string) string {
112
return name
113
}
114
115
+// ReadMigrationConfig reads the Migration section of the IPFS config, avoiding
116
+// reading anything other than the Migration section. That way, we're free to
117
+// make arbitrary changes to all _other_ sections in migrations.
118
+func ReadMigrationConfig(repoRoot string) (*config.Migration, error) {
119
+ var cfg struct {
120
+ Migration config.Migration
121
+ }
122
+
123
+ cfgPath, err := config.Filename(repoRoot)
124
+ if err != nil {
125
+ return nil, err
126
+ }
127
+
128
+ cfgFile, err := os.Open(cfgPath)
129
+ if err != nil {
130
+ return nil, err
131
+ }
132
+ defer cfgFile.Close()
133
+
134
+ err = json.NewDecoder(cfgFile).Decode(&cfg)
135
+ if err != nil {
136
+ return nil, err
137
+ }
138
+
139
+ switch cfg.Migration.Keep {
140
+ case "":
141
+ cfg.Migration.Keep = config.DefaultMigrationKeep
142
+ case "discard", "cache", "keep":
143
+ default:
144
+ return nil, errors.New("unknown config value, Migrations.Keep must be 'cache', 'pin', or 'discard'")
145
+ }
146
+
147
+ if len(cfg.Migration.DownloadSources) == 0 {
148
+ cfg.Migration.DownloadSources = config.DefaultMigrationDownloadSources
149
+ }
150
+
151
+ return &cfg.Migration, nil
152
+}
153
+
154
+// GetMigrationFetcher creates one or more fetchers according to
155
+// downloadSources,
156
+func GetMigrationFetcher(downloadSources []string, distPath string, newIpfsFetcher func(string) Fetcher) (Fetcher, error) {
157
+ const httpUserAgent = "go-ipfs"
158
+
159
+ var fetchers []Fetcher
160
+ for _, src := range downloadSources {
161
+ src := strings.TrimSpace(src)
162
+ switch src {
163
+ case "HTTPS", "https", "HTTP", "http":
164
+ fetchers = append(fetchers, NewHttpFetcher(distPath, "", httpUserAgent, 0))
165
+ case "IPFS", "ipfs":
166
+ if newIpfsFetcher != nil {
167
+ fetchers = append(fetchers, newIpfsFetcher(distPath))
168
+ }
169
+ default:
170
+ u, err := url.Parse(src)
171
+ if err != nil {
172
+ return nil, fmt.Errorf("bad gateway address: %s", err)
173
+ }
174
+ switch u.Scheme {
175
+ case "":
176
+ u.Scheme = "https"
177
+ case "https", "http":
178
+ default:
179
+ return nil, errors.New("bad gateway address: url scheme must be http or https")
180
+ }
181
+ fetchers = append(fetchers, NewHttpFetcher(distPath, u.String(), httpUserAgent, 0))
182
+ case "":
183
+ // Ignore empty string
184
+ }
185
+ }
186
+
187
+ switch len(fetchers) {
188
+ case 0:
189
+ return nil, errors.New("no sources specified")
190
+ case 1:
191
+ return fetchers[0], nil
192
+ }
193
+
194
+ // Wrap fetchers in a MultiFetcher to try them in order
195
+ return NewMultiFetcher(fetchers...), nil
196
+}
197
+
198
func migrationName(from, to int) string {
199
return fmt.Sprintf("fs-repo-%d-to-%d", from, to)
200
}
repo/fsrepo/migrations/migrations_test.go
+210
-22
@@ -3,20 +3,18 @@ package migrations
3
import (
4
"context"
5
"fmt"
6
- "io/ioutil"
6
+ "io"
7
"log"
8
"os"
9
"path/filepath"
10
"strings"
11
"testing"
12
+
13
+ config "github.com/ipfs/go-ipfs-config"
14
)
15
16
func TestFindMigrations(t *testing.T) {
15
- tmpDir, err := ioutil.TempDir("", "migratetest")
16
- if err != nil {
17
- panic(err)
18
- }
19
- defer os.RemoveAll(tmpDir)
17
+ tmpDir := t.TempDir()
18
19
ctx, cancel := context.WithCancel(context.Background())
20
defer cancel()
@@ -63,11 +61,7 @@ func TestFindMigrations(t *testing.T) {
61
}
62
63
func TestFindMigrationsReverse(t *testing.T) {
66
- tmpDir, err := ioutil.TempDir("", "migratetest")
67
- if err != nil {
68
- panic(err)
69
- }
70
- defer os.RemoveAll(tmpDir)
64
+ tmpDir := t.TempDir()
65
66
ctx, cancel := context.WithCancel(context.Background())
67
defer cancel()
@@ -121,11 +115,7 @@ func TestFetchMigrations(t *testing.T) {
115
defer ts.Close()
116
fetcher := NewHttpFetcher(CurrentIpfsDist, ts.URL, "", 0)
117
124
- tmpDir, err := ioutil.TempDir("", "migratetest")
125
- if err != nil {
126
- panic(err)
127
- }
128
- defer os.RemoveAll(tmpDir)
118
+ tmpDir := t.TempDir()
119
120
needed := []string{"fs-repo-1-to-2", "fs-repo-2-to-3"}
121
buf := new(strings.Builder)
@@ -157,16 +147,12 @@ func TestFetchMigrations(t *testing.T) {
147
}
148
149
func TestRunMigrations(t *testing.T) {
160
- fakeHome, err := ioutil.TempDir("", "testhome")
161
- if err != nil {
162
- panic(err)
163
- }
164
- defer os.RemoveAll(fakeHome)
150
+ fakeHome := t.TempDir()
151
152
os.Setenv("HOME", fakeHome)
153
fakeIpfs := filepath.Join(fakeHome, ".ipfs")
154
169
- err = os.Mkdir(fakeIpfs, os.ModePerm)
155
+ err := os.Mkdir(fakeIpfs, os.ModePerm)
156
if err != nil {
157
panic(err)
158
}
@@ -211,3 +197,205 @@ func createFakeBin(from, to int, tmpDir string) {
197
panic(err)
198
}
199
}
200
+
201
+var testConfig = `
202
+{
203
+ "Bootstrap": [
204
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
205
+ "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
206
+ ],
207
+ "Migration": {
208
+ "DownloadSources": ["IPFS", "HTTP", "127.0.0.1", "https://127.0.1.1"],
209
+ "Keep": "cache"
210
+ },
211
+ "Peering": {
212
+ "Peers": [
213
+ {
214
+ "ID": "12D3KooWGC6TvWhfapngX6wvJHMYvKpDMXPb3ZnCZ6dMoaMtimQ5",
215
+ "Addrs": ["/ip4/127.0.0.1/tcp/4001", "/ip4/127.0.0.1/udp/4001/quic"]
216
+ }
217
+ ]
218
+ }
219
+}
220
+`
221
+
222
+func TestReadMigrationConfigDefaults(t *testing.T) {
223
+ tmpDir := makeConfig(t, "{}")
224
+
225
+ cfg, err := ReadMigrationConfig(tmpDir)
226
+ if err != nil {
227
+ t.Fatal(err)
228
+ }
229
+
230
+ if cfg.Keep != config.DefaultMigrationKeep {
231
+ t.Error("expected default value for Keep")
232
+ }
233
+
234
+ if len(cfg.DownloadSources) != len(config.DefaultMigrationDownloadSources) {
235
+ t.Fatal("expected default number of download sources")
236
+ }
237
+ for i, src := range config.DefaultMigrationDownloadSources {
238
+ if cfg.DownloadSources[i] != src {
239
+ t.Errorf("wrong DownloadSource: %s", cfg.DownloadSources[i])
240
+ }
241
+ }
242
+}
243
+
244
+func TestReadMigrationConfigErrors(t *testing.T) {
245
+ tmpDir := makeConfig(t, `{"Migration": {"Keep": "badvalue"}}`)
246
+
247
+ _, err := ReadMigrationConfig(tmpDir)
248
+ if err == nil {
249
+ t.Fatal("expected error")
250
+ }
251
+ if !strings.HasPrefix(err.Error(), "unknown") {
252
+ t.Fatal("did not get expected error:", err)
253
+ }
254
+
255
+ os.RemoveAll(tmpDir)
256
+ _, err = ReadMigrationConfig(tmpDir)
257
+ if err == nil {
258
+ t.Fatal("expected error")
259
+ }
260
+
261
+ tmpDir = makeConfig(t, `}{`)
262
+ _, err = ReadMigrationConfig(tmpDir)
263
+ if err == nil {
264
+ t.Fatal("expected error")
265
+ }
266
+}
267
+
268
+func TestReadMigrationConfig(t *testing.T) {
269
+ tmpDir := makeConfig(t, testConfig)
270
+
271
+ cfg, err := ReadMigrationConfig(tmpDir)
272
+ if err != nil {
273
+ t.Fatal(err)
274
+ }
275
+
276
+ if len(cfg.DownloadSources) != 4 {
277
+ t.Fatal("wrong number of DownloadSources")
278
+ }
279
+ expect := []string{"IPFS", "HTTP", "127.0.0.1", "https://127.0.1.1"}
280
+ for i := range expect {
281
+ if cfg.DownloadSources[i] != expect[i] {
282
+ t.Errorf("wrong DownloadSource at %d", i)
283
+ }
284
+ }
285
+
286
+ if cfg.Keep != "cache" {
287
+ t.Error("wrong value for Keep")
288
+ }
289
+}
290
+
291
+type mockIpfsFetcher struct{}
292
+
293
+func (m *mockIpfsFetcher) Fetch(ctx context.Context, filePath string) (io.ReadCloser, error) {
294
+ return nil, nil
295
+}
296
+
297
+func (m *mockIpfsFetcher) Close() error {
298
+ return nil
299
+}
300
+
301
+func TestGetMigrationFetcher(t *testing.T) {
302
+ var f Fetcher
303
+ var err error
304
+
305
+ newIpfsFetcher := func(distPath string) Fetcher {
306
+ return &mockIpfsFetcher{}
307
+ }
308
+
309
+ downloadSources := []string{"ftp://bad.gateway.io"}
310
+ _, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
311
+ if err == nil || !strings.HasPrefix(err.Error(), "bad gateway addr") {
312
+ t.Fatal("Expected bad gateway address error, got:", err)
313
+ }
314
+
315
+ downloadSources = []string{"::bad.gateway.io"}
316
+ _, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
317
+ if err == nil || !strings.HasPrefix(err.Error(), "bad gateway addr") {
318
+ t.Fatal("Expected bad gateway address error, got:", err)
319
+ }
320
+
321
+ downloadSources = []string{"http://localhost"}
322
+ f, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
323
+ if err != nil {
324
+ t.Fatal(err)
325
+ }
326
+ if _, ok := f.(*HttpFetcher); !ok {
327
+ t.Fatal("expected HttpFetcher")
328
+ }
329
+
330
+ downloadSources = []string{"ipfs"}
331
+ f, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
332
+ if err != nil {
333
+ t.Fatal(err)
334
+ }
335
+ if _, ok := f.(*mockIpfsFetcher); !ok {
336
+ t.Fatal("expected IpfsFetcher")
337
+ }
338
+
339
+ downloadSources = []string{"http"}
340
+ f, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
341
+ if err != nil {
342
+ t.Fatal(err)
343
+ }
344
+ if _, ok := f.(*HttpFetcher); !ok {
345
+ t.Fatal("expected HttpFetcher")
346
+ }
347
+
348
+ downloadSources = []string{"IPFS", "HTTPS"}
349
+ f, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
350
+ if err != nil {
351
+ t.Fatal(err)
352
+ }
353
+ mf, ok := f.(*MultiFetcher)
354
+ if !ok {
355
+ t.Fatal("expected MultiFetcher")
356
+ }
357
+ if mf.Len() != 2 {
358
+ t.Fatal("expected 2 fetchers in MultiFetcher")
359
+ }
360
+
361
+ downloadSources = []string{"ipfs", "https", "some.domain.io"}
362
+ f, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
363
+ if err != nil {
364
+ t.Fatal(err)
365
+ }
366
+ mf, ok = f.(*MultiFetcher)
367
+ if !ok {
368
+ t.Fatal("expected MultiFetcher")
369
+ }
370
+ if mf.Len() != 3 {
371
+ t.Fatal("expected 3 fetchers in MultiFetcher")
372
+ }
373
+
374
+ downloadSources = nil
375
+ _, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
376
+ if err == nil {
377
+ t.Fatal("expected error when no sources specified")
378
+ }
379
+
380
+ downloadSources = []string{"", ""}
381
+ _, err = GetMigrationFetcher(downloadSources, "", newIpfsFetcher)
382
+ if err == nil {
383
+ t.Fatal("expected error when empty string fetchers specified")
384
+ }
385
+}
386
+
387
+func makeConfig(t *testing.T, configData string) string {
388
+ tmpDir := t.TempDir()
389
+
390
+ cfgFile, err := os.Create(filepath.Join(tmpDir, "config"))
391
+ if err != nil {
392
+ t.Fatal(err)
393
+ }
394
+ if _, err = cfgFile.Write([]byte(configData)); err != nil {
395
+ t.Fatal(err)
396
+ }
397
+ if err = cfgFile.Close(); err != nil {
398
+ t.Fatal(err)
399
+ }
400
+ return tmpDir
401
+}
repo/fsrepo/migrations/unpack_test.go
+4
-12
@@ -33,14 +33,10 @@ func TestUnpackArchive(t *testing.T) {
33
}
34
35
func TestUnpackTgz(t *testing.T) {
36
- tmpDir, err := ioutil.TempDir("", "testunpacktgz")
37
- if err != nil {
38
- panic(err)
39
- }
40
- defer os.RemoveAll(tmpDir)
36
+ tmpDir := t.TempDir()
37
38
badTarGzip := filepath.Join(tmpDir, "bad.tar.gz")
43
- err = ioutil.WriteFile(badTarGzip, []byte("bad-data\n"), 0644)
39
+ err := ioutil.WriteFile(badTarGzip, []byte("bad-data\n"), 0644)
40
if err != nil {
41
panic(err)
42
}
@@ -81,14 +77,10 @@ func TestUnpackTgz(t *testing.T) {
77
}
78
79
func TestUnpackZip(t *testing.T) {
84
- tmpDir, err := ioutil.TempDir("", "testunpackzip")
85
- if err != nil {
86
- panic(err)
87
- }
88
- defer os.RemoveAll(tmpDir)
80
+ tmpDir := t.TempDir()
81
82
badZip := filepath.Join(tmpDir, "bad.zip")
91
- err = ioutil.WriteFile(badZip, []byte("bad-data\n"), 0644)
83
+ err := ioutil.WriteFile(badZip, []byte("bad-data\n"), 0644)
84
if err != nil {
85
panic(err)
86
}