github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/config/migrate/migrate.go (about) 1 // Package migrate defines migrations for qri configuration files 2 package migrate 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "strings" 12 "sync" 13 "time" 14 15 logging "github.com/ipfs/go-log" 16 "github.com/mitchellh/go-homedir" 17 "github.com/qri-io/ioes" 18 "github.com/qri-io/qfs/qipfs" 19 "github.com/qri-io/qri/config" 20 qerr "github.com/qri-io/qri/errors" 21 "github.com/qri-io/qri/repo" 22 "github.com/qri-io/qri/repo/buildrepo" 23 ) 24 25 var ( 26 log = logging.Logger("migrate") 27 // ErrNeedMigration indicates a migration is required 28 ErrNeedMigration = fmt.Errorf("migration required") 29 // ErrMigrationSucceeded indicates a migration completed executing 30 ErrMigrationSucceeded = errors.New("migration succeeded") 31 ) 32 33 // RunMigrations executes migrations. if a migration is required, the shouldRun 34 // func is called, and exits without migrating if shouldRun returns false. 35 // if errorOnSuccess is true, a completed migration will return 36 // ErrMigrationSucceeded instead of nil 37 func RunMigrations(streams ioes.IOStreams, cfg *config.Config, shouldRun func() bool, errorOnSuccess bool) (err error) { 38 if cfg.Revision != config.CurrentConfigRevision { 39 if !shouldRun() { 40 return qerr.New(ErrNeedMigration, `your repo requires migration before it can run`) 41 } 42 43 streams.PrintErr("migrating configuration...\n") 44 if cfg.Revision == 0 { 45 if err := ZeroToOne(cfg); err != nil { 46 return err 47 } 48 } 49 if cfg.Revision == 1 { 50 if err := OneToTwo(cfg); err != nil { 51 return err 52 } 53 } 54 if cfg.Revision == 2 { 55 if err := TwoToThree(cfg); err != nil { 56 return err 57 } 58 } 59 if cfg.Revision == 3 { 60 if err := ThreeToFour(cfg); err != nil { 61 return err 62 } 63 } 64 streams.PrintErr("done!\n") 65 66 if errorOnSuccess { 67 return ErrMigrationSucceeded 68 } 69 } 70 return nil 71 } 72 73 // ZeroToOne migrates a configuration from Revision Zero (no revision number) to Revision 1 74 func ZeroToOne(cfg *config.Config) error { 75 if cfg.P2P != nil { 76 removes := map[string]bool{ 77 "/ip4/130.211.198.23/tcp/4001/ipfs/QmNX9nSos8sRFvqGTwdEme6LQ8R1eJ8EuFgW32F9jjp2Pb": true, // mojo 78 "/ip4/35.193.162.149/tcp/4001/ipfs/QmTZxETL4YCCzB1yFx4GT1te68henVHD1XPQMkHZ1N22mm": true, // epa 79 "/ip4/35.226.92.45/tcp/4001/ipfs/QmP6sbnHXANXgQ7JeCCeCKdJrgpvUd8s75YNfzdkHf6Mpi": true, // 538 80 "/ip4/35.192.140.245/tcp/4001/ipfs/QmUUVNiTz2K9zQSH9PxerKWXmN1p3DBo3oJXurvYziFzqh": true, // EDGI 81 } 82 83 adds := config.DefaultP2P().QriBootstrapAddrs 84 85 for i, addr := range cfg.P2P.QriBootstrapAddrs { 86 // remove any old, invalid addresses 87 if removes[addr] { 88 cfg.P2P.QriBootstrapAddrs = delIdx(i, cfg.P2P.QriBootstrapAddrs) 89 } 90 // remove address from list of additions if already configured 91 for j, add := range adds { 92 if addr == add { 93 // adds = append(adds[:j], adds[j+1:]...) 94 adds = delIdx(j, adds) 95 } 96 } 97 } 98 99 cfg.P2P.QriBootstrapAddrs = append(cfg.P2P.QriBootstrapAddrs, adds...) 100 } 101 102 cfg.Revision = 1 103 104 if err := safeWriteConfig(cfg); err != nil { 105 rollbackConfigWrite(cfg) 106 return err 107 } 108 109 return nil 110 } 111 112 // OneToTwo migrates a configuration from Revision 1 to Revision 2 113 func OneToTwo(cfg *config.Config) error { 114 log.Debug("migrating from version one to two") 115 qriPath := filepath.Dir(cfg.Path()) 116 newIPFSPath := filepath.Join(qriPath, "ipfs") 117 oldIPFSPath := configVersionOneIPFSPath() 118 119 if err := qipfs.InternalizeIPFSRepo(oldIPFSPath, newIPFSPath); err != nil { 120 return err 121 } 122 123 if err := oneToTwoConfig(cfg); err != nil { 124 return err 125 } 126 cfg.Revision = 2 127 if err := cfg.Validate(); err != nil { 128 return qerr.New(err, "config is invalid") 129 } 130 if err := safeWriteConfig(cfg); err != nil { 131 rollbackConfigWrite(cfg) 132 return err 133 } 134 135 if err := maybeRemoveIPFSRepo(cfg, oldIPFSPath); err != nil { 136 log.Debug(err) 137 fmt.Printf("error removing IPFS repo at %q:\n\t%s", oldIPFSPath, err) 138 fmt.Printf(`qri has successfully internalized this IPFS repo, and no longer 139 needs the folder at %q. you may want to remove it 140 `, oldIPFSPath) 141 } 142 143 return nil 144 } 145 146 func oneToTwoConfig(cfg *config.Config) error { 147 if cfg.API != nil { 148 apiCfg := cfg.API 149 defaultAPICfg := config.DefaultAPI() 150 if apiCfg.Address == "" { 151 apiCfg.Address = defaultAPICfg.Address 152 } 153 // TODO(b5): need a strategy for setting config now that this field is removed 154 // in config revision 4 155 // if apiCfg.WebsocketAddress == "" { 156 // apiCfg.WebsocketAddress = defaultAPICfg.WebsocketAddress 157 // } 158 } else { 159 return qerr.New(fmt.Errorf("invalid config"), "config does not contain API configuration") 160 } 161 162 // TODO(b5): need a strategy for setting config now that this field is removed 163 // in config revision 4 164 // if cfg.RPC != nil { 165 // defaultRPCCfg := config.DefaultRPC() 166 // if cfg.RPC.Address == "" { 167 // cfg.RPC.Address = defaultRPCCfg.Address 168 // } 169 // } else { 170 // return qerr.New(fmt.Errorf("invalid config"), "config does not contain RPC configuration") 171 // } 172 173 cfg.Filesystems = config.DefaultFilesystems() 174 175 return nil 176 } 177 178 // TwoToThree migrates a configuration from Revision 2 to Revision 3 179 func TwoToThree(cfg *config.Config) error { 180 log.Debugf("migrating from revision 2 to 3") 181 if cfg.P2P != nil { 182 removes := map[string]bool{ 183 "/ip4/35.239.80.82/tcp/4001/ipfs/QmdpGkbqDYRPCcwLYnEm8oYGz2G9aUZn9WwPjqvqw3XUAc": true, // red 184 "/ip4/35.225.152.38/tcp/4001/ipfs/QmTRqTLbKndFC2rp6VzpyApxHCLrFV35setF1DQZaRWPVf": true, // orange 185 "/ip4/35.202.155.225/tcp/4001/ipfs/QmegNYmwHUQFc3v3eemsYUVf3WiSg4RcMrh3hovA5LncJ2": true, // yellow 186 "/ip4/35.238.10.180/tcp/4001/ipfs/QmessbA6uGLJ7HTwbUJ2niE49WbdPfzi27tdYXdAaGRB4G": true, // green 187 "/ip4/35.238.105.35/tcp/4001/ipfs/Qmc353gHY5Wx5iHKHPYj3QDqHP4hVA1MpoSsT6hwSyVx3r": true, // blue 188 "/ip4/35.239.138.186/tcp/4001/ipfs/QmT9YHJF2YkysLqWhhiVTL5526VFtavic3bVueF9rCsjVi": true, // indigo 189 "/ip4/35.226.44.58/tcp/4001/ipfs/QmQS2ryqZrjJtPKDy9VTkdPwdUSpTi1TdpGUaqAVwfxcNh": true, // violet 190 } 191 192 adds := map[string]bool{} 193 for _, addr := range config.DefaultP2P().QriBootstrapAddrs { 194 adds[addr] = true 195 } 196 197 res := []string{} 198 199 for _, addr := range cfg.P2P.QriBootstrapAddrs { 200 if removes[addr] || adds[addr] { 201 continue 202 } 203 res = append(res, addr) 204 } 205 206 res = append(res, config.DefaultP2P().QriBootstrapAddrs...) 207 208 cfg.P2P.QriBootstrapAddrs = res 209 } 210 211 cfg.Revision = 3 212 213 if err := safeWriteConfig(cfg); err != nil { 214 rollbackConfigWrite(cfg) 215 return err 216 } 217 218 return nil 219 } 220 221 // ThreeToFour migrates a configuration from Revision 3 to Revision 4 222 func ThreeToFour(cfg *config.Config) error { 223 log.Debugf("migrating from revision 3 to 4") 224 ipfsRepoPath, err := maybeRelativizeIPFSPath(cfg) 225 if err != nil { 226 return err 227 } 228 229 // migrate any existing IPFS repo 230 if _, err := os.Stat(ipfsRepoPath); !os.IsNotExist(err) { 231 ctx := context.Background() 232 if err := qipfs.Migrate(ctx, ipfsRepoPath); err != nil { 233 return err 234 } 235 } 236 237 if cfg.API != nil { 238 cfg.API.Webui = true 239 } 240 cfg.Automation = config.DefaultAutomation() 241 cfg.Revision = 4 242 243 if err := safeWriteConfig(cfg); err != nil { 244 rollbackConfigWrite(cfg) 245 return err 246 } 247 248 return nil 249 } 250 251 func rollbackConfigWrite(cfg *config.Config) { 252 cfgPath := cfg.Path() 253 if len(cfgPath) == 0 { 254 return 255 } 256 tmpCfgPath := getTmpConfigFilepath(cfgPath) 257 if _, err := os.Stat(tmpCfgPath); !os.IsNotExist(err) { 258 os.Remove(tmpCfgPath) 259 } 260 } 261 262 func safeWriteConfig(cfg *config.Config) error { 263 cfgPath := cfg.Path() 264 if len(cfgPath) == 0 { 265 return qerr.New(fmt.Errorf("invalid path"), "could not determine config path") 266 } 267 tmpCfgPath := getTmpConfigFilepath(cfgPath) 268 if err := cfg.WriteToFile(tmpCfgPath); err != nil { 269 return qerr.New(err, fmt.Sprintf("could not write config to path %s", tmpCfgPath)) 270 } 271 if err := os.Rename(tmpCfgPath, cfgPath); err != nil { 272 return qerr.New(err, fmt.Sprintf("could not write config to path %s", cfgPath)) 273 } 274 275 return nil 276 } 277 278 func getTmpConfigFilepath(cfgPath string) string { 279 cfgDir := filepath.Dir(cfgPath) 280 tmpCfgPath := filepath.Join(cfgDir, "config_updated.yaml") 281 return tmpCfgPath 282 } 283 284 func delIdx(i int, sl []string) []string { 285 if i < len(sl)-1 { 286 return append(sl[:i], sl[i+1:]...) 287 } 288 289 return sl[:i] 290 } 291 292 // In qri v0.9.8 & earlier, the IPFS path location was determined by the 293 // IPFS_PATH env var, and falling back to $HOME/.ipfs. 294 func configVersionOneIPFSPath() string { 295 path := os.Getenv("IPFS_PATH") 296 if path != "" { 297 return path 298 } 299 home, err := homedir.Dir() 300 if err != nil { 301 panic(fmt.Sprintf("Failed to find the home directory: %s", err.Error())) 302 } 303 return filepath.Join(home, ".ipfs") 304 } 305 306 func confirm(w io.Writer, r io.Reader, message string) bool { 307 input := prompt(w, r, fmt.Sprintf("%s [Y/n]: ", message)) 308 if input == "" { 309 return true 310 } 311 312 return (input == "y" || input == "yes") 313 } 314 315 func prompt(w io.Writer, r io.Reader, msg string) string { 316 var input string 317 fmt.Fprintf(w, msg) 318 fmt.Fscanln(r, &input) 319 return strings.TrimSpace(strings.ToLower(input)) 320 } 321 322 func maybeRelativizeIPFSPath(cfg *config.Config) (string, error) { 323 cfgBasePath := filepath.Dir(cfg.Path()) 324 for i, fsCfg := range cfg.Filesystems { 325 if fsCfg.Type == "ipfs" { 326 if ipath, ok := fsCfg.Config["path"]; ok { 327 if path, ok := ipath.(string); ok { 328 if filepath.IsAbs(path) { 329 if rel, err := filepath.Rel(cfgBasePath, path); err == nil { 330 cfg.Filesystems[i].Config["path"] = rel 331 return path, nil 332 } 333 } 334 return filepath.Join(cfgBasePath, path), nil 335 } 336 } 337 return "", fmt.Errorf("IPFS path is not specified") 338 } 339 } 340 return "", fmt.Errorf("no IPFS filesystem is specified") 341 } 342 343 func maybeRemoveIPFSRepo(cfg *config.Config, oldPath string) error { 344 fmt.Println("\nChecking if existing IPFS directory contains non-qri data...") 345 repoPath := filepath.Dir(cfg.Path()) 346 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 347 348 r, err := buildrepo.New(ctx, repoPath, cfg) 349 if err != nil { 350 cancel() 351 return err 352 } 353 354 defer gracefulShutdown(cancel, r) 355 356 // Note: this is intentionally using the new post-migration IPFS repo to judge 357 // pin presence, because we can't operate on the old one 358 fs := r.Filesystem().Filesystem(qipfs.FilestoreType) 359 if fs == nil { 360 return nil 361 } 362 363 logbookPaths, err := r.Logbook().AllReferencedDatasetPaths(ctx) 364 if err != nil { 365 return err 366 } 367 368 paths := map[string]struct{}{ 369 // add common paths auto-added on IPFS init we can safely ignore 370 "/ipld/QmQPeNsJPyVWPFDVHb77w8G42Fvo15z4bG2X8D2GhfbSXc": {}, 371 "/ipld/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn": {}, 372 "/ipld/QmS4ustL54uo8FzR9455qaxZwuMiUhyvMcX9Ba8nUH4uVv": {}, 373 } 374 375 for p := range logbookPaths { 376 // switch "/ipfs/" prefixes for "/ipld/" 377 p = strings.Replace(p, "/ipfs", "/ipld", 1) 378 paths[p] = struct{}{} 379 } 380 381 unknownPinCh, err := fs.(*qipfs.Filestore).PinsetDifference(ctx, paths) 382 if err != nil { 383 return err 384 } 385 386 log.Debugf("checking pins...%#v\n", paths) 387 388 unknown := []string{} 389 for path := range unknownPinCh { 390 log.Debugf("checking if unknown pin is a dataset: %s\n", path) 391 path = strings.Replace(path, "/ipld", "/ipfs", 1) 392 // check if the pinned path is a valid qri dataset, looking for "dataset.json" 393 // this check allows us to ignore qri data logbook doesn't know about 394 if f, err := fs.Get(ctx, fmt.Sprintf("%s/dataset.json", path)); err == nil { 395 f.Close() 396 } else { 397 unknown = append(unknown, path) 398 } 399 } 400 401 if len(unknown) > 0 { 402 fmt.Printf(` 403 Qri left your original IPFS repo in place because it contains pinned data that 404 Qri isn't managing. Qri has created an internal copy of your IPFS repo, and no 405 longer requires the repo at %q 406 `, oldPath) 407 if len(unknown) < 10 { 408 fmt.Printf("unknown pins:\n\t%s\n\n", strings.Join(unknown, "\n\t")) 409 } else { 410 fmt.Printf("\nfound %d unknown pins\n\n", len(unknown)) 411 } 412 } else { 413 if err := os.RemoveAll(oldPath); err != nil { 414 return err 415 } 416 fmt.Printf("moved IPFS repo from %q into qri repo\n", oldPath) 417 } 418 419 log.Info("successfully migrated repo, shutting down") 420 421 return nil 422 } 423 424 func gracefulShutdown(cancel context.CancelFunc, r repo.Repo) { 425 var wg sync.WaitGroup 426 go func() { 427 <-r.Done() 428 wg.Done() 429 }() 430 431 wg.Add(1) 432 cancel() 433 wg.Wait() 434 }