github.com/sqlitebrowser/dio@v0.0.0-20240125125356-b587368e5c6b/cmd/shared.go (about) 1 package cmd 2 3 import ( 4 "bytes" 5 "crypto/sha256" 6 "crypto/tls" 7 "crypto/x509" 8 "encoding/hex" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io/ioutil" 13 "log" 14 "net/http" 15 "net/url" 16 "os" 17 "path/filepath" 18 "runtime" 19 "strings" 20 "time" 21 22 "github.com/mitchellh/go-homedir" 23 rq "github.com/parnurzeal/gorequest" 24 ) 25 26 // Check if the database with the given SHA256 checksum is in local cache. If it's not then download and cache it 27 func checkDBCache(db, shaSum string) (err error) { 28 if _, err = os.Stat(filepath.Join(".dio", db, "db", shaSum)); os.IsNotExist(err) { 29 var body []byte 30 _, body, err = retrieveDatabase(db, pullCmdBranch, pullCmdCommit) 31 if err != nil { 32 return 33 } 34 35 // Verify the SHA256 checksum of the new download 36 s := sha256.Sum256(body) 37 thisSum := hex.EncodeToString(s[:]) 38 if thisSum != shaSum { 39 // The newly downloaded database file doesn't have the expected checksum. Abort. 40 return errors.New(fmt.Sprintf("Aborting: newly downloaded database file should have "+ 41 "checksum '%s', but data with checksum '%s' received\n", shaSum, thisSum)) 42 } 43 44 // Write the database file to disk in the cache directory 45 err = ioutil.WriteFile(filepath.Join(".dio", db, "db", shaSum), body, 0644) 46 } 47 return 48 } 49 50 // Generate a stable SHA256 for a commit. 51 func createCommitID(c commitEntry) string { 52 var b bytes.Buffer 53 b.WriteString(fmt.Sprintf("tree %s\n", c.Tree.ID)) 54 if c.Parent != "" { 55 b.WriteString(fmt.Sprintf("parent %s\n", c.Parent)) 56 } 57 for _, j := range c.OtherParents { 58 b.WriteString(fmt.Sprintf("parent %s\n", j)) 59 } 60 b.WriteString(fmt.Sprintf("author %s <%s> %v\n", c.AuthorName, c.AuthorEmail, 61 c.Timestamp.UTC().Format(time.UnixDate))) 62 if c.CommitterEmail != "" { 63 b.WriteString(fmt.Sprintf("committer %s <%s> %v\n", c.CommitterName, c.CommitterEmail, 64 c.Timestamp.UTC().Format(time.UnixDate))) 65 } 66 b.WriteString("\n" + c.Message) 67 b.WriteByte(0) 68 s := sha256.Sum256(b.Bytes()) 69 return hex.EncodeToString(s[:]) 70 } 71 72 // Generate the SHA256 for a tree. 73 // Tree entry structure is: 74 // * [ entry type ] [ licence sha256] [ file sha256 ] [ file name ] [ last modified (timestamp) ] [ file size (bytes) ] 75 func createDBTreeID(entries []dbTreeEntry) string { 76 var b bytes.Buffer 77 for _, j := range entries { 78 b.WriteString(string(j.EntryType)) 79 b.WriteByte(0) 80 b.WriteString(string(j.LicenceSHA)) 81 b.WriteByte(0) 82 b.WriteString(j.Sha256) 83 b.WriteByte(0) 84 b.WriteString(j.Name) 85 b.WriteByte(0) 86 b.WriteString(j.LastModified.Format(time.RFC3339)) 87 b.WriteByte(0) 88 b.WriteString(fmt.Sprintf("%d\n", j.Size)) 89 } 90 s := sha256.Sum256(b.Bytes()) 91 return hex.EncodeToString(s[:]) 92 } 93 94 // Returns true if a database has been changed on disk since the last commit 95 func dbChanged(db string, meta metaData) (changed bool, err error) { 96 // Retrieve the sha256, file size, and last modified date from the head commit of the active branch 97 head, ok := meta.Branches[meta.ActiveBranch] 98 if !ok { 99 err = errors.New("Aborting: info for the active branch isn't found in the local branch cache") 100 return 101 } 102 c, ok := meta.Commits[head.Commit] 103 if !ok { 104 err = errors.New("Aborting: info for the head commit isn't found in the local commit cache") 105 return 106 } 107 metaSHASum := c.Tree.Entries[0].Sha256 108 metaFileSize := c.Tree.Entries[0].Size 109 metaLastModified := c.Tree.Entries[0].LastModified.Truncate(time.Second).UTC() 110 111 // If the file size or last modified date in the metadata are different from the current file info, then the 112 // local file has probably changed. Well, "probably" for the last modified day, but "definitely" if the file 113 // size is different 114 fi, err := os.Stat(db) 115 if err != nil { 116 if os.IsNotExist(err) { 117 return false, nil 118 } 119 return 120 } 121 fileSize := fi.Size() 122 lastModified := fi.ModTime().Truncate(time.Second).UTC() 123 if metaFileSize != fileSize || !metaLastModified.Equal(lastModified) { 124 changed = true 125 return 126 } 127 128 // * If the file size and last modified date are still the same, we SHA256 checksum and compare the file * 129 130 // TODO: Should we only do this for smaller files (below some TBD threshold)? 131 132 // Read the database from disk, and calculate it's sha256 133 b, err := ioutil.ReadFile(db) 134 if err != nil { 135 return 136 } 137 if int64(len(b)) != fileSize { 138 err = errors.New(numFormat.Sprintf("Aborting: # of bytes read (%d) when reading the database "+ 139 "doesn't match the database file size (%d)", len(b), fileSize)) 140 return 141 } 142 s := sha256.Sum256(b) 143 shaSum := hex.EncodeToString(s[:]) 144 145 // Check if a change has been made 146 if metaSHASum != shaSum { 147 changed = true 148 } 149 return 150 } 151 152 // Retrieves the list of databases available to the user 153 var getDatabases = func(url string, user string) (dbList []dbListEntry, err error) { 154 resp, body, errs := rq.New().TLSClientConfig(&TLSConfig). 155 Get(fmt.Sprintf("%s/%s", url, user)). 156 Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)). 157 EndBytes() 158 if errs != nil { 159 e := fmt.Sprintln("Errors when retrieving the database list:") 160 for _, err := range errs { 161 e += fmt.Sprintf(err.Error()) 162 } 163 err = errors.New(e) 164 return 165 } 166 defer resp.Body.Close() 167 err = json.Unmarshal(body, &dbList) 168 if err != nil { 169 _, errInner := fmt.Fprintf(fOut, "Error retrieving database list: '%v'\n", err.Error()) 170 if errInner != nil { 171 err = fmt.Errorf("%s: %s", err, errInner) 172 return 173 } 174 } 175 return 176 } 177 178 // Generates an initial default (production) configuration file. Before it's useful, the user will need to fill out 179 // their display name + provide a DB4S certificate file 180 func generateConfig(cfgFile string) (err error) { 181 // Create the ".dio" directory in the users home folder, to store the configuration file in 182 var home string 183 home, err = homedir.Dir() 184 if err != nil { 185 return 186 } 187 if _, err = os.Stat(filepath.Join(home, ".dio")); os.IsNotExist(err) { 188 err = os.Mkdir(filepath.Join(home, ".dio"), 0770) 189 if err != nil { 190 return 191 } 192 } 193 194 // Download the Certificate Authority chain file 195 caURL := "https://github.com/sqlitebrowser/dio/raw/master/cert/ca-chain.cert.pem" 196 chainFile := filepath.Join(home, ".dio", "ca-chain.cert.pem") 197 resp, body, errs := rq.New().TLSClientConfig(&tls.Config{InsecureSkipVerify: true}).Get(caURL). 198 Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)). 199 EndBytes() 200 if errs != nil { 201 e := fmt.Sprintln("errors when retrieving the CA chain file:") 202 for _, errInner := range errs { 203 e += fmt.Sprintf(errInner.Error()) 204 } 205 return errors.New(e) 206 } 207 defer resp.Body.Close() 208 err = ioutil.WriteFile(chainFile, body, 0644) 209 if err != nil { 210 return err 211 } 212 213 // Generate the initial config file 214 var f *os.File 215 f, err = os.Create(cfgFile) 216 if err != nil { 217 return 218 } 219 defer f.Close() 220 lineEnd := "\n" 221 if runtime.GOOS == "windows" { 222 lineEnd = "\r\n" 223 } 224 certPath := fmt.Sprintf("%c%s", os.PathSeparator, filepath.Join("path", "to", "your", "certificate", "here")) 225 _, err = fmt.Fprint(f, `[certs]`+lineEnd) 226 _, err = fmt.Fprint(f, fmt.Sprintf(`cachain = '%s'%s`, chainFile, lineEnd)) 227 _, err = fmt.Fprint(f, fmt.Sprintf(`cert = '%s'%s`, certPath, lineEnd)) 228 _, err = fmt.Fprint(f, lineEnd) 229 _, err = fmt.Fprint(f, `[general]`+lineEnd) 230 _, err = fmt.Fprint(f, `cloud = 'https://db4s.dbhub.io'`+lineEnd) 231 _, err = fmt.Fprint(f, lineEnd) 232 _, err = fmt.Fprint(f, `[user]`+lineEnd) 233 _, err = fmt.Fprint(f, `name = 'Your Name'`+lineEnd) 234 return 235 } 236 237 // Returns the name of the default database, if one has been selected. Returns an empty string if not 238 func getDefaultDatabase() (db string, err error) { 239 // Check if the local defaults info exists 240 var z []byte 241 if z, err = ioutil.ReadFile(filepath.Join(".dio", "defaults.json")); err != nil { 242 if os.IsNotExist(err) { 243 return "", nil 244 } 245 return 246 } 247 248 // Read and parse the metadata 249 var y defaultSettings 250 err = json.Unmarshal([]byte(z), &y) 251 if err != nil { 252 return 253 } 254 if y.SelectedDatabase != "" { 255 db = y.SelectedDatabase 256 } 257 return 258 } 259 260 // Returns a map with the list of licences available on the remote server 261 var getLicences = func() (list map[string]licenceEntry, err error) { 262 // Retrieve the database list from the cloud 263 resp, body, errs := rq.New().TLSClientConfig(&TLSConfig).Get(cloud+"/licence/list"). 264 Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)). 265 End() 266 if errs != nil { 267 e := fmt.Sprintln("errors when retrieving the licence list:") 268 for _, err := range errs { 269 e += fmt.Sprintf(err.Error()) 270 } 271 return list, errors.New(e) 272 } 273 defer resp.Body.Close() 274 275 // Convert the JSON response to our licence entry structure 276 err = json.Unmarshal([]byte(body), &list) 277 if err != nil { 278 return list, errors.New(fmt.Sprintf("error retrieving licence list: '%v'\n", err.Error())) 279 } 280 return list, err 281 } 282 283 // getUserAndServer() returns the user name and server from a DBHub.io client certificate 284 func getUserAndServer() (userAcc string, email string, certServer string, err error) { 285 if numCerts := len(TLSConfig.Certificates); numCerts == 0 { 286 err = errors.New("No client certificates installed. Can't proceed.") 287 return 288 } 289 290 // Parse the client certificate 291 // TODO: Add support for multiple certificates 292 cert, err := x509.ParseCertificate(TLSConfig.Certificates[0].Certificate[0]) 293 if err != nil { 294 err = errors.New("Couldn't parse cert") 295 return 296 } 297 298 // Extract the account name, email address, and associated server from the certificate 299 email = cert.Subject.CommonName 300 if email == "" { 301 // The common name field is empty in the client cert. Can't proceed. 302 err = errors.New("Common name is blank in client certificate") 303 return 304 } 305 s := strings.Split(email, "@") 306 if len(s) < 2 { 307 err = errors.New("Missing information in client certificate") 308 return 309 } 310 userAcc = s[0] 311 certServer = s[1] 312 if userAcc == "" || certServer == "" { 313 // Missing details in common name field 314 err = errors.New("Missing information in client certificate") 315 return 316 } 317 return 318 } 319 320 // Loads the local metadata from disk (if present). If not, then grab it from the remote server, storing it locally. 321 // Note - This is subtly different than calling updateMetadata() itself. This function 322 // (loadMetadata()) is for use by commands which can use a local metadata cache all by itself 323 // (eg branch creation), but only if it already exists. For those, it only calls the 324 // remote server when a local metadata cache doesn't exist. 325 func loadMetadata(db string) (meta metaData, err error) { 326 // Check if the local metadata exists. If not, pull it from the remote server 327 if _, err = os.Stat(filepath.Join(".dio", db, "metadata.json")); os.IsNotExist(err) { 328 _, err = updateMetadata(db, true) 329 if err != nil { 330 return 331 } 332 } 333 334 // Read and parse the metadata 335 var md []byte 336 md, err = ioutil.ReadFile(filepath.Join(".dio", db, "metadata.json")) 337 if err != nil { 338 return 339 } 340 err = json.Unmarshal([]byte(md), &meta) 341 342 // If the tag or release maps are missing, create initial empty ones. 343 // This is a safety check, not sure if it's really needed 344 if meta.Tags == nil { 345 meta.Tags = make(map[string]tagEntry) 346 } 347 if meta.Releases == nil { 348 meta.Releases = make(map[string]releaseEntry) 349 } 350 return 351 } 352 353 // Loads the local metadata cache for the requested database, if present. Otherwise, (optionally) retrieve it from 354 // the server. 355 // Note - this is suitable for use by read-only functions (eg: branch/tag list, log) 356 // as it doesn't store or change any metadata on disk 357 var localFetchMetadata = func(db string, getRemote bool) (meta metaData, err error) { 358 md, err := ioutil.ReadFile(filepath.Join(".dio", db, "metadata.json")) 359 if err == nil { 360 err = json.Unmarshal([]byte(md), &meta) 361 return 362 } 363 364 // Can't read local metadata, and we're requested to not grab remote metadata. So, nothing to do but exit 365 if !getRemote { 366 err = errors.New("No local metadata for the database exists") 367 return 368 } 369 370 // Can't read local metadata, but we're ok to grab the remote. So, use that instead 371 meta, _, err = retrieveMetadata(db) 372 return 373 } 374 375 // Merges old and new metadata 376 func mergeMetadata(origMeta metaData, newMeta metaData) (mergedMeta metaData, err error) { 377 mergedMeta.Branches = make(map[string]branchEntry) 378 mergedMeta.Commits = make(map[string]commitEntry) 379 mergedMeta.Tags = make(map[string]tagEntry) 380 mergedMeta.Releases = make(map[string]releaseEntry) 381 if len(origMeta.Commits) > 0 { 382 // Start by check branches which exist locally 383 // TODO: Change sort order to be by alphabetical branch name, as the current unordered approach leads to 384 // inconsistent output across runs 385 for brName, brData := range origMeta.Branches { 386 matchFound := false 387 for newBranch, newData := range newMeta.Branches { 388 if brName == newBranch { 389 // A branch with this name exists on both the local and remote server 390 matchFound = true 391 skipFurtherChecks := false 392 393 // Rewind back to the local root commit, making a list of the local commits IDs we pass through 394 var localList []string 395 localCommit := origMeta.Commits[brData.Commit] 396 localList = append(localList, localCommit.ID) 397 for localCommit.Parent != "" { 398 localCommit = origMeta.Commits[localCommit.Parent] 399 localList = append(localList, localCommit.ID) 400 } 401 localLength := len(localList) - 1 402 403 // Rewind back to the remote root commit, making a list of the remote commit IDs we pass through 404 var remoteList []string 405 remoteCommit := newMeta.Commits[newData.Commit] 406 remoteList = append(remoteList, remoteCommit.ID) 407 for remoteCommit.Parent != "" { 408 remoteCommit = newMeta.Commits[remoteCommit.Parent] 409 remoteList = append(remoteList, remoteCommit.ID) 410 } 411 remoteLength := len(remoteList) - 1 412 413 // Make sure the local and remote commits start out with the same commit ID 414 if localCommit.ID != remoteCommit.ID { 415 // The local and remote branches don't have a common root, so abort 416 err = errors.New(fmt.Sprintf("Local and remote branch %s don't have a common root. "+ 417 "Aborting.", brName)) 418 return 419 } 420 421 // If there are more commits in the local branch than in the remote one, we keep the local branch 422 // as it probably means the user is adding stuff locally (prior to pushing to the server) 423 if localLength > remoteLength { 424 c := origMeta.Commits[brData.Commit] 425 mergedMeta.Commits[c.ID] = origMeta.Commits[c.ID] 426 for c.Parent != "" { 427 c = origMeta.Commits[c.Parent] 428 mergedMeta.Commits[c.ID] = origMeta.Commits[c.ID] 429 } 430 431 // Copy the local branch data 432 mergedMeta.Branches[brName] = brData 433 } 434 435 // We've wound back to the root commit for both the local and remote branch, and the root commit 436 // IDs match. Now we walk forwards through the commits, comparing them. 437 branchesSame := true 438 for i := 0; i <= localLength; i++ { 439 lCommit := localList[localLength-i] 440 if i > remoteLength { 441 branchesSame = false 442 } else { 443 if lCommit != remoteList[remoteLength-i] { 444 // There are conflicting commits in this branch between the local metadata and the 445 // remote. This will probably need to be resolved by user action. 446 branchesSame = false 447 } 448 } 449 } 450 451 // If the local branch commits are in the remote branch already, then we only need to check for 452 // newer commits in the remote branch 453 if branchesSame { 454 if remoteLength > localLength { 455 _, err = fmt.Fprintf(fOut, " * Remote branch '%s' has %d new commit(s)... merged\n", 456 brName, remoteLength-localLength) 457 if err != nil { 458 return 459 } 460 for _, j := range remoteList { 461 mergedMeta.Commits[j] = newMeta.Commits[j] 462 } 463 mergedMeta.Branches[brName] = newMeta.Branches[brName] 464 } else { 465 // The local and remote branches are the same, so copy the local branch commits across to 466 // the merged data structure 467 _, err = fmt.Fprintf(fOut, " * Branch '%s' is unchanged\n", brName) 468 if err != nil { 469 return 470 } 471 for _, j := range localList { 472 mergedMeta.Commits[j] = origMeta.Commits[j] 473 } 474 mergedMeta.Branches[brName] = brData 475 } 476 // No need to do further checks on this branch 477 skipFurtherChecks = true 478 } 479 480 if skipFurtherChecks == false && brData.Commit != newData.Commit { 481 _, err = fmt.Fprintf(fOut, " * Branch '%s' has local changes, not on the server\n", 482 brName) 483 if err != nil { 484 return 485 } 486 487 // Copy across the commits from the local branch 488 localCommit := origMeta.Commits[brData.Commit] 489 mergedMeta.Commits[localCommit.ID] = origMeta.Commits[localCommit.ID] 490 for localCommit.Parent != "" { 491 localCommit = origMeta.Commits[localCommit.Parent] 492 mergedMeta.Commits[localCommit.ID] = origMeta.Commits[localCommit.ID] 493 } 494 495 // Copy across the branch data entry for the local branch 496 mergedMeta.Branches[brName] = brData 497 } 498 if skipFurtherChecks == false && brData.Description != newData.Description { 499 _, err = fmt.Fprintf(fOut, " * Description for branch %s differs between the local "+ 500 "and remote\n"+ 501 " * Local: '%s'\n"+ 502 " * Remote: '%s'\n", brName, brData.Description, newData.Description) 503 if err != nil { 504 return 505 } 506 } 507 } 508 } 509 if !matchFound { 510 // This seems to be a branch that's not on the server, so we keep it as-is 511 _, err = fmt.Fprintf(fOut, " * Branch '%s' is local only, not on the server\n", brName) 512 if err != nil { 513 return 514 } 515 mergedMeta.Branches[brName] = brData 516 517 // Copy across the commits from the local branch 518 localCommit := origMeta.Commits[brData.Commit] 519 mergedMeta.Commits[localCommit.ID] = origMeta.Commits[localCommit.ID] 520 for localCommit.Parent != "" { 521 localCommit = origMeta.Commits[localCommit.Parent] 522 mergedMeta.Commits[localCommit.ID] = origMeta.Commits[localCommit.ID] 523 } 524 525 // Copy across the branch data entry for the local branch 526 mergedMeta.Branches[brName] = brData 527 } 528 } 529 530 // Add new branches 531 for remoteName, remoteData := range newMeta.Branches { 532 if _, ok := origMeta.Branches[remoteName]; ok == false { 533 // Copy their commit data 534 newCommit := newMeta.Commits[remoteData.Commit] 535 mergedMeta.Commits[newCommit.ID] = newMeta.Commits[newCommit.ID] 536 for newCommit.Parent != "" { 537 newCommit = newMeta.Commits[newCommit.Parent] 538 mergedMeta.Commits[newCommit.ID] = newMeta.Commits[newCommit.ID] 539 } 540 541 // Copy their branch data 542 mergedMeta.Branches[remoteName] = remoteData 543 544 _, err = fmt.Fprintf(fOut, " * New remote branch '%s' merged\n", remoteName) 545 if err != nil { 546 return 547 } 548 } 549 } 550 551 // Preserve existing tags 552 for tagName, tagData := range origMeta.Tags { 553 mergedMeta.Tags[tagName] = tagData 554 } 555 556 // Add new tags 557 for tagName, tagData := range newMeta.Tags { 558 // Only add tags which aren't already in the merged metadata structure 559 if _, tagFound := mergedMeta.Tags[tagName]; tagFound == false { 560 // Also make sure its commit is in the commit list. If it's not, then skip adding the tag 561 if _, commitFound := mergedMeta.Commits[tagData.Commit]; commitFound == true { 562 _, err = fmt.Fprintf(fOut, " * New tag '%s' merged\n", tagName) 563 if err != nil { 564 return 565 } 566 mergedMeta.Tags[tagName] = tagData 567 } 568 } 569 } 570 571 // Preserve existing releases 572 for relName, relData := range origMeta.Releases { 573 mergedMeta.Releases[relName] = relData 574 } 575 576 // Add new releases 577 for relName, relData := range newMeta.Releases { 578 // Only add releases which aren't already in the merged metadata structure 579 if _, relFound := mergedMeta.Releases[relName]; relFound == false { 580 // Also make sure its commit is in the commit list. If it's not, then skip adding the release 581 if _, commitFound := mergedMeta.Commits[relData.Commit]; commitFound == true { 582 _, err = fmt.Fprintf(fOut, " * New release '%s' merged\n", relName) 583 if err != nil { 584 return 585 } 586 mergedMeta.Releases[relName] = relData 587 } 588 } 589 } 590 591 // Copy the default branch name from the remote server 592 mergedMeta.DefBranch = newMeta.DefBranch 593 594 // If an active (local) branch has been set, then copy it to the merged metadata. Otherwise use the default 595 // branch as given by the remote server 596 if origMeta.ActiveBranch != "" { 597 mergedMeta.ActiveBranch = origMeta.ActiveBranch 598 } else { 599 mergedMeta.ActiveBranch = newMeta.DefBranch 600 } 601 602 _, err = fmt.Fprintln(fOut) 603 if err != nil { 604 return 605 } 606 } else { 607 // No existing metadata, so just copy across the remote metadata 608 mergedMeta = newMeta 609 610 // Use the remote default branch as the initial active (local) branch 611 mergedMeta.ActiveBranch = newMeta.DefBranch 612 } 613 return 614 } 615 616 // Retrieves a database from DBHub.io 617 func retrieveDatabase(db string, branch string, commit string) (resp rq.Response, body []byte, err error) { 618 dbURL := fmt.Sprintf("%s/%s/%s", cloud, certUser, db) 619 req := rq.New().TLSClientConfig(&TLSConfig).Get(dbURL). 620 Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)) 621 if branch != "" { 622 req.Query(fmt.Sprintf("branch=%s", url.QueryEscape(branch))) 623 } else { 624 req.Query(fmt.Sprintf("commit=%s", url.QueryEscape(commit))) 625 } 626 var errs []error 627 resp, body, errs = req.EndBytes() 628 if errs != nil { 629 log.Print("Errors when downloading database:") 630 for _, err := range errs { 631 log.Print(err.Error()) 632 } 633 err = errors.New("Error when downloading database") 634 return 635 } 636 if resp.StatusCode != http.StatusOK { 637 if resp.StatusCode == http.StatusNotFound { 638 if branch != "" { 639 err = errors.New(fmt.Sprintf("That database & branch '%s' aren't known on DBHub.io", 640 branch)) 641 return 642 } 643 if commit != "" { 644 err = errors.New(fmt.Sprintf("Requested database not found with commit %s.", 645 commit)) 646 return 647 } 648 err = errors.New("Requested database not found") 649 return 650 } 651 err = errors.New(fmt.Sprintf("Download failed with an error: HTTP status %d - '%v'\n", 652 resp.StatusCode, resp.Status)) 653 } 654 return 655 } 656 657 // Retrieves database metadata from DBHub.io 658 var retrieveMetadata = func(db string) (meta metaData, onCloud bool, err error) { 659 // Download the database metadata 660 resp, md, errs := rq.New().TLSClientConfig(&TLSConfig).Get(cloud+"/metadata/get"). 661 Query(fmt.Sprintf("username=%s", url.QueryEscape(certUser))). 662 Query(fmt.Sprintf("folder=%s", "/")). 663 Query(fmt.Sprintf("dbname=%s", url.QueryEscape(db))). 664 Set("User-Agent", fmt.Sprintf("Dio %s", DIO_VERSION)). 665 End() 666 667 if errs != nil { 668 log.Print("Errors when downloading database metadata:") 669 for _, err := range errs { 670 log.Print(err.Error()) 671 } 672 return metaData{}, false, errors.New("Error when downloading database metadata") 673 } 674 if resp.StatusCode == http.StatusNotFound { 675 return metaData{}, false, nil 676 } 677 if resp.StatusCode != http.StatusOK { 678 return metaData{}, false, 679 errors.New(fmt.Sprintf("Metadata download failed with an error: HTTP status %d - '%v'\n", 680 resp.StatusCode, resp.Status)) 681 } 682 err = json.Unmarshal([]byte(md), &meta) 683 if err != nil { 684 return 685 } 686 return meta, true, nil 687 } 688 689 // Returns the name of the default database, if one has been selected. Returns an empty string if not 690 func saveDefaultDatabase(db string) (err error) { 691 // Load the local default info 692 var z []byte 693 var def defaultSettings 694 if z, err = ioutil.ReadFile(filepath.Join(".dio", "defaults.json")); err == nil { 695 err = json.Unmarshal([]byte(z), &def) 696 if err != nil { 697 return 698 } 699 } else { 700 // No local default info, so we use a new blank set instead 701 def = defaultSettings{} 702 } 703 704 // Save the new default database setting to disk 705 def.SelectedDatabase = db 706 var j []byte 707 j, err = json.MarshalIndent(def, "", " ") 708 if err != nil { 709 return 710 } 711 err = ioutil.WriteFile(filepath.Join(".dio", "defaults.json"), j, 0644) 712 return 713 } 714 715 // Saves the metadata to a local cache 716 func saveMetadata(db string, meta metaData) (err error) { 717 // Create the metadata directory if needed 718 if _, err = os.Stat(filepath.Join(".dio", db)); os.IsNotExist(err) { 719 // We create the "db" directory instead, as that'll be needed anyway and MkdirAll() ensures the .dio/<db> 720 // directory will be created on the way through 721 err = os.MkdirAll(filepath.Join(".dio", db, "db"), 0770) 722 if err != nil { 723 return 724 } 725 } 726 727 // Serialise the metadata to JSON 728 var jsonString []byte 729 jsonString, err = json.MarshalIndent(meta, "", " ") 730 if err != nil { 731 return 732 } 733 734 // Write the updated metadata to disk 735 mdFile := filepath.Join(".dio", db, "metadata.json") 736 err = ioutil.WriteFile(mdFile, jsonString, 0644) 737 return err 738 } 739 740 // Saves metadata to the local cache, merging in with any existing metadata 741 func updateMetadata(db string, saveMeta bool) (mergedMeta metaData, err error) { 742 // Check for existing metadata file, loading it if present 743 var md []byte 744 origMeta := metaData{} 745 md, err = ioutil.ReadFile(filepath.Join(".dio", db, "metadata.json")) 746 if err == nil { 747 err = json.Unmarshal([]byte(md), &origMeta) 748 if err != nil { 749 return 750 } 751 } 752 753 // Download the latest database metadata 754 _, err = fmt.Fprintln(fOut, "Updating metadata") 755 if err != nil { 756 return 757 } 758 newMeta, _, err := retrieveMetadata(db) 759 if err != nil { 760 return 761 } 762 763 // If we have existing local metadata, then merge the metadata from DBHub.io with it 764 if len(origMeta.Commits) > 0 { 765 mergedMeta, err = mergeMetadata(origMeta, newMeta) 766 if err != nil { 767 return 768 } 769 } else { 770 // No existing metadata, so just copy across the remote metadata 771 mergedMeta = newMeta 772 773 // Use the remote default branch as the initial active (local) branch 774 mergedMeta.ActiveBranch = newMeta.DefBranch 775 } 776 777 // Serialise the updated metadata to JSON 778 var jsonString []byte 779 jsonString, err = json.MarshalIndent(mergedMeta, "", " ") 780 if err != nil { 781 errMsg := fmt.Sprintf("Error when JSON marshalling the merged metadata: %v\n", err) 782 log.Print(errMsg) 783 return 784 } 785 786 // If requested, write the updated metadata to disk 787 if saveMeta { 788 if _, err = os.Stat(filepath.Join(".dio", db)); os.IsNotExist(err) { 789 err = os.MkdirAll(filepath.Join(".dio", db), 0770) 790 if err != nil { 791 return 792 } 793 } 794 mdFile := filepath.Join(".dio", db, "metadata.json") 795 err = ioutil.WriteFile(mdFile, []byte(jsonString), 0644) 796 } 797 return 798 }