github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/overlord/snapshotstate/backend/backend.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2018-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package backend 21 22 import ( 23 "archive/tar" 24 "archive/zip" 25 "bytes" 26 "context" 27 "crypto" 28 "encoding/json" 29 "errors" 30 "fmt" 31 "io" 32 "io/ioutil" 33 "os" 34 "path" 35 "path/filepath" 36 "regexp" 37 "runtime" 38 "sort" 39 "strconv" 40 "strings" 41 "syscall" 42 "time" 43 44 "github.com/snapcore/snapd/client" 45 "github.com/snapcore/snapd/dirs" 46 "github.com/snapcore/snapd/logger" 47 "github.com/snapcore/snapd/osutil" 48 "github.com/snapcore/snapd/snap" 49 "github.com/snapcore/snapd/snapdenv" 50 "github.com/snapcore/snapd/strutil" 51 ) 52 53 const ( 54 archiveName = "archive.tgz" 55 metadataName = "meta.json" 56 metaHashName = "meta.sha3_384" 57 58 userArchivePrefix = "user/" 59 userArchiveSuffix = ".tgz" 60 ) 61 62 var ( 63 // Stop is used to ask Iter to stop iteration, without it being an error. 64 Stop = errors.New("stop iteration") 65 66 osOpen = os.Open 67 dirNames = (*os.File).Readdirnames 68 backendOpen = Open 69 timeNow = time.Now 70 71 usersForUsernames = usersForUsernamesImpl 72 ) 73 74 // LastSnapshotSetID returns the highest set id number for the snapshots stored 75 // in snapshots directory; set ids are inferred from the filenames. 76 func LastSnapshotSetID() (uint64, error) { 77 dir, err := osOpen(dirs.SnapshotsDir) 78 if err != nil { 79 if osutil.IsDirNotExist(err) { 80 // no snapshots 81 return 0, nil 82 } 83 return 0, fmt.Errorf("cannot open snapshots directory: %v", err) 84 } 85 defer dir.Close() 86 87 var maxSetID uint64 88 89 var readErr error 90 for readErr == nil { 91 var names []string 92 // note os.Readdirnames can return a non-empty names and a non-nil err 93 names, readErr = dirNames(dir, 100) 94 for _, name := range names { 95 if ok, setID := isSnapshotFilename(name); ok { 96 if setID > maxSetID { 97 maxSetID = setID 98 } 99 } 100 } 101 } 102 if readErr != nil && readErr != io.EOF { 103 return 0, readErr 104 } 105 return maxSetID, nil 106 } 107 108 // Iter loops over all snapshots in the snapshots directory, applying the given 109 // function to each. The snapshot will be closed after the function returns. If 110 // the function returns an error, iteration is stopped (and if the error isn't 111 // Stop, it's returned as the error of the iterator). 112 func Iter(ctx context.Context, f func(*Reader) error) error { 113 if err := ctx.Err(); err != nil { 114 return err 115 } 116 117 dir, err := osOpen(dirs.SnapshotsDir) 118 if err != nil { 119 if osutil.IsDirNotExist(err) { 120 // no dir -> no snapshots 121 return nil 122 } 123 return fmt.Errorf("cannot open snapshots directory: %v", err) 124 } 125 defer dir.Close() 126 127 importsInProgress := map[uint64]bool{} 128 var names []string 129 var readErr error 130 for readErr == nil && err == nil { 131 names, readErr = dirNames(dir, 100) 132 // note os.Readdirnames can return a non-empty names and a non-nil err 133 for _, name := range names { 134 if err = ctx.Err(); err != nil { 135 break 136 } 137 138 // filter out non-snapshot directory entries 139 ok, setID := isSnapshotFilename(name) 140 if !ok { 141 continue 142 } 143 // keep track of in-progress in a map as well 144 // to avoid races. E.g.: 145 // 1. The dirNnames() are read 146 // 2. 99_some-snap_1.0_x1.zip is returned 147 // 3. the code checks if 99_importing is there, 148 // it is so 99_some-snap is skipped 149 // 4. other snapshots are examined 150 // 5. in-parallel 99_importing finishes 151 // 7. 99_other-snap_1.0_x1.zip is now examined 152 // 8. code checks if 99_importing is there, but it 153 // is no longer there because import 154 // finished in the meantime. We still 155 // want to not call the callback with 156 // 99_other-snap or the callback would get 157 // an incomplete view about 99_snapshot. 158 if importsInProgress[setID] { 159 continue 160 } 161 if importInProgressFor(setID) { 162 importsInProgress[setID] = true 163 continue 164 } 165 166 filename := filepath.Join(dirs.SnapshotsDir, name) 167 reader, openError := backendOpen(filename, setID) 168 // reader can be non-nil even when openError is not nil (in 169 // which case reader.Broken will have a reason). f can 170 // check and either ignore or return an error when 171 // finding a broken snapshot. 172 if reader != nil { 173 err = f(reader) 174 } else { 175 // TODO: use warnings instead 176 logger.Noticef("Cannot open snapshot %q: %v.", name, openError) 177 } 178 if openError == nil { 179 // if openError was nil the snapshot was opened and needs closing 180 if closeError := reader.Close(); err == nil { 181 err = closeError 182 } 183 } 184 if err != nil { 185 break 186 } 187 } 188 } 189 190 if readErr != nil && readErr != io.EOF { 191 return readErr 192 } 193 194 if err == Stop { 195 err = nil 196 } 197 198 return err 199 } 200 201 // List valid snapshots sets. 202 func List(ctx context.Context, setID uint64, snapNames []string) ([]client.SnapshotSet, error) { 203 setshots := map[uint64][]*client.Snapshot{} 204 err := Iter(ctx, func(reader *Reader) error { 205 if setID == 0 || reader.SetID == setID { 206 if len(snapNames) == 0 || strutil.ListContains(snapNames, reader.Snap) { 207 setshots[reader.SetID] = append(setshots[reader.SetID], &reader.Snapshot) 208 } 209 } 210 return nil 211 }) 212 213 sets := make([]client.SnapshotSet, 0, len(setshots)) 214 for id, shots := range setshots { 215 sort.Sort(bySnap(shots)) 216 sets = append(sets, client.SnapshotSet{ID: id, Snapshots: shots}) 217 } 218 219 sort.Sort(byID(sets)) 220 221 return sets, err 222 } 223 224 // Filename of the given client.Snapshot in this backend. 225 func Filename(snapshot *client.Snapshot) string { 226 // this _needs_ the snap name and version to be valid 227 return filepath.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s_%s_%s.zip", snapshot.SetID, snapshot.Snap, snapshot.Version, snapshot.Revision)) 228 } 229 230 // isSnapshotFilename checks if the given filePath is a snapshot file name, i.e. 231 // if it starts with a numeric set id and ends with .zip extension; 232 // filePath can be just a file name, or a full path. 233 func isSnapshotFilename(filePath string) (ok bool, setID uint64) { 234 fname := filepath.Base(filePath) 235 // XXX: we could use a regexp here to match very precisely all the elements 236 // of the filename following Filename() above, but perhaps it's better no to 237 // go overboard with it in case the format evolves in the future. Only check 238 // if the name starts with a set-id and ends with .zip. 239 // 240 // Filename is "<sid>_<snapName>_version_revision.zip", e.g. "16_snapcraft_4.2_5407.zip" 241 ext := filepath.Ext(fname) 242 if ext != ".zip" { 243 return false, 0 244 } 245 parts := strings.SplitN(fname, "_", 2) 246 if len(parts) != 2 { 247 return false, 0 248 } 249 // invalid: no parts following <sid>_ 250 if parts[1] == ext { 251 return false, 0 252 } 253 id, err := strconv.Atoi(parts[0]) 254 if err != nil { 255 return false, 0 256 } 257 return true, uint64(id) 258 } 259 260 // EstimateSnapshotSize calculates estimated size of the snapshot. 261 func EstimateSnapshotSize(si *snap.Info, usernames []string) (uint64, error) { 262 var total uint64 263 calculateSize := func(path string, finfo os.FileInfo, err error) error { 264 if finfo.Mode().IsRegular() { 265 total += uint64(finfo.Size()) 266 } 267 return err 268 } 269 270 visitDir := func(dir string) error { 271 exists, isDir, err := osutil.DirExists(dir) 272 if err != nil { 273 return err 274 } 275 if !(exists && isDir) { 276 return nil 277 } 278 return filepath.Walk(dir, calculateSize) 279 } 280 281 for _, dir := range []string{si.DataDir(), si.CommonDataDir()} { 282 if err := visitDir(dir); err != nil { 283 return 0, err 284 } 285 } 286 287 users, err := usersForUsernames(usernames) 288 if err != nil { 289 return 0, err 290 } 291 for _, usr := range users { 292 if err := visitDir(si.UserDataDir(usr.HomeDir)); err != nil { 293 return 0, err 294 } 295 if err := visitDir(si.UserCommonDataDir(usr.HomeDir)); err != nil { 296 return 0, err 297 } 298 } 299 300 // XXX: we could use a typical compression factor here 301 return total, nil 302 } 303 304 // Save a snapshot 305 func Save(ctx context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { 306 if err := os.MkdirAll(dirs.SnapshotsDir, 0700); err != nil { 307 return nil, err 308 } 309 310 snapshot := &client.Snapshot{ 311 SetID: id, 312 Snap: si.InstanceName(), 313 SnapID: si.SnapID, 314 Revision: si.Revision, 315 Version: si.Version, 316 Epoch: si.Epoch, 317 Time: timeNow(), 318 SHA3_384: make(map[string]string), 319 Size: 0, 320 Conf: cfg, 321 // Note: Auto is no longer set in the Snapshot. 322 } 323 324 aw, err := osutil.NewAtomicFile(Filename(snapshot), 0600, 0, osutil.NoChown, osutil.NoChown) 325 if err != nil { 326 return nil, err 327 } 328 // if things worked, we'll commit (and Cancel becomes a NOP) 329 defer aw.Cancel() 330 331 w := zip.NewWriter(aw) 332 defer w.Close() // note this does not close the file descriptor (that's done by hand on the atomic writer, above) 333 if err := addDirToZip(ctx, snapshot, w, "root", archiveName, si.DataDir()); err != nil { 334 return nil, err 335 } 336 337 users, err := usersForUsernames(usernames) 338 if err != nil { 339 return nil, err 340 } 341 342 for _, usr := range users { 343 if err := addDirToZip(ctx, snapshot, w, usr.Username, userArchiveName(usr), si.UserDataDir(usr.HomeDir)); err != nil { 344 return nil, err 345 } 346 } 347 348 metaWriter, err := w.Create(metadataName) 349 if err != nil { 350 return nil, err 351 } 352 353 hasher := crypto.SHA3_384.New() 354 enc := json.NewEncoder(io.MultiWriter(metaWriter, hasher)) 355 if err := enc.Encode(snapshot); err != nil { 356 return nil, err 357 } 358 359 hashWriter, err := w.Create(metaHashName) 360 if err != nil { 361 return nil, err 362 } 363 fmt.Fprintf(hashWriter, "%x\n", hasher.Sum(nil)) 364 if err := w.Close(); err != nil { 365 return nil, err 366 } 367 368 if err := ctx.Err(); err != nil { 369 return nil, err 370 } 371 372 if err := aw.Commit(); err != nil { 373 return nil, err 374 } 375 376 return snapshot, nil 377 } 378 379 var isTesting = snapdenv.Testing() 380 381 func addDirToZip(ctx context.Context, snapshot *client.Snapshot, w *zip.Writer, username string, entry, dir string) error { 382 parent, revdir := filepath.Split(dir) 383 exists, isDir, err := osutil.DirExists(parent) 384 if err != nil { 385 return err 386 } 387 if exists && !isDir { 388 logger.Noticef("Not saving directories under %q in snapshot #%d of %q as it is not a directory.", parent, snapshot.SetID, snapshot.Snap) 389 return nil 390 } 391 if !exists { 392 logger.Debugf("Not saving directories under %q in snapshot #%d of %q as it is does not exist.", parent, snapshot.SetID, snapshot.Snap) 393 return nil 394 } 395 tarArgs := []string{ 396 "--create", 397 "--sparse", "--gzip", 398 "--format", "gnu", 399 "--directory", parent, 400 } 401 402 noRev, noCommon := true, true 403 404 exists, isDir, err = osutil.DirExists(dir) 405 if err != nil { 406 return err 407 } 408 switch { 409 case exists && isDir: 410 tarArgs = append(tarArgs, revdir) 411 noRev = false 412 case exists && !isDir: 413 logger.Noticef("Not saving %q in snapshot #%d of %q as it is not a directory.", dir, snapshot.SetID, snapshot.Snap) 414 case !exists: 415 logger.Debugf("Not saving %q in snapshot #%d of %q as it is does not exist.", dir, snapshot.SetID, snapshot.Snap) 416 } 417 418 common := filepath.Join(parent, "common") 419 exists, isDir, err = osutil.DirExists(common) 420 if err != nil { 421 return err 422 } 423 switch { 424 case exists && isDir: 425 tarArgs = append(tarArgs, "common") 426 noCommon = false 427 case exists && !isDir: 428 logger.Noticef("Not saving %q in snapshot #%d of %q as it is not a directory.", common, snapshot.SetID, snapshot.Snap) 429 case !exists: 430 logger.Debugf("Not saving %q in snapshot #%d of %q as it is does not exist.", common, snapshot.SetID, snapshot.Snap) 431 } 432 433 if noCommon && noRev { 434 return nil 435 } 436 437 archiveWriter, err := w.CreateHeader(&zip.FileHeader{Name: entry}) 438 if err != nil { 439 return err 440 } 441 442 var sz osutil.Sizer 443 hasher := crypto.SHA3_384.New() 444 445 cmd := tarAsUser(username, tarArgs...) 446 cmd.Stdout = io.MultiWriter(archiveWriter, hasher, &sz) 447 matchCounter := &strutil.MatchCounter{N: 1} 448 cmd.Stderr = matchCounter 449 if isTesting { 450 matchCounter.N = -1 451 cmd.Stderr = io.MultiWriter(os.Stderr, matchCounter) 452 } 453 if err := osutil.RunWithContext(ctx, cmd); err != nil { 454 matches, count := matchCounter.Matches() 455 if count > 0 { 456 return fmt.Errorf("cannot create archive: %s (and %d more)", matches[0], count-1) 457 } 458 return fmt.Errorf("tar failed: %v", err) 459 } 460 461 snapshot.SHA3_384[entry] = fmt.Sprintf("%x", hasher.Sum(nil)) 462 snapshot.Size += sz.Size() 463 464 return nil 465 } 466 467 var ErrCannotCancel = errors.New("cannot cancel: import already finished") 468 469 // multiError collects multiple errors that affected an operation. 470 type multiError struct { 471 header string 472 errs []error 473 } 474 475 // newMultiError returns a new multiError struct initialized with 476 // the given format string that explains what operation potentially 477 // went wrong. multiError can be nested and will render correctly 478 // in these cases. 479 func newMultiError(header string, errs []error) error { 480 return &multiError{header: header, errs: errs} 481 } 482 483 // Error formats the error string. 484 func (me *multiError) Error() string { 485 return me.nestedError(0) 486 } 487 488 // helper to ensure formating of nested multiErrors works. 489 func (me *multiError) nestedError(level int) string { 490 indent := strings.Repeat(" ", level) 491 buf := bytes.NewBufferString(fmt.Sprintf("%s:\n", me.header)) 492 if level > 8 { 493 return "circular or too deep error nesting (max 8)?!" 494 } 495 for i, err := range me.errs { 496 switch v := err.(type) { 497 case *multiError: 498 fmt.Fprintf(buf, "%s- %v", indent, v.nestedError(level+1)) 499 default: 500 fmt.Fprintf(buf, "%s- %v", indent, err) 501 } 502 if i < len(me.errs)-1 { 503 fmt.Fprintf(buf, "\n") 504 } 505 } 506 return buf.String() 507 } 508 509 var ( 510 importingFnRegexp = regexp.MustCompile("^([0-9]+)_importing$") 511 importingFnGlob = "[0-9]*_importing" 512 importingFnFmt = "%d_importing" 513 importingForIDFmt = "%d_*.zip" 514 ) 515 516 // importInProgressFor return true if the given snapshot id has an import 517 // that is in progress. 518 func importInProgressFor(setID uint64) bool { 519 return newImportTransaction(setID).InProgress() 520 } 521 522 // importTransaction keeps track of the given snapshot ID import and 523 // ensures it can be committed/cancelled in an atomic way. 524 // 525 // Start() must be called before the first data is imported. When the 526 // import is successful Commit() should be called. 527 // 528 // Cancel() will cancel the given import and cleanup. It's always safe 529 // to defer a Cancel() it will just return a "ErrCannotCancel" after 530 // a commit. 531 type importTransaction struct { 532 id uint64 533 lockPath string 534 committed bool 535 } 536 537 // newImportTransaction creates a new importTransaction for the given 538 // snapshot id. 539 func newImportTransaction(setID uint64) *importTransaction { 540 return &importTransaction{ 541 id: setID, 542 lockPath: filepath.Join(dirs.SnapshotsDir, fmt.Sprintf(importingFnFmt, setID)), 543 } 544 } 545 546 // newImportTransactionFromImportFile creates a new importTransaction 547 // for the given import file path. It may return an error if an 548 // invalid file was specified. 549 func newImportTransactionFromImportFile(p string) (*importTransaction, error) { 550 parts := importingFnRegexp.FindStringSubmatch(path.Base(p)) 551 if len(parts) != 2 { 552 return nil, fmt.Errorf("cannot determine snapshot id from %q", p) 553 } 554 setID, err := strconv.ParseUint(parts[1], 10, 64) 555 if err != nil { 556 return nil, err 557 } 558 return newImportTransaction(setID), nil 559 } 560 561 // Start marks the start of a snapshot import 562 func (t *importTransaction) Start() error { 563 return t.lock() 564 } 565 566 // InProgress returns true if there is an import for this transactions 567 // snapshot ID already. 568 func (t *importTransaction) InProgress() bool { 569 return osutil.FileExists(t.lockPath) 570 } 571 572 // Cancel cancels a snapshot import and cleanups any files on disk belonging 573 // to this snapshot ID. 574 func (t *importTransaction) Cancel() error { 575 if t.committed { 576 return ErrCannotCancel 577 } 578 inProgressImports, err := filepath.Glob(filepath.Join(dirs.SnapshotsDir, fmt.Sprintf(importingForIDFmt, t.id))) 579 if err != nil { 580 return err 581 } 582 var errs []error 583 for _, p := range inProgressImports { 584 if err := os.Remove(p); err != nil { 585 errs = append(errs, err) 586 } 587 } 588 if err := t.unlock(); err != nil { 589 errs = append(errs, err) 590 } 591 if len(errs) > 0 { 592 return newMultiError(fmt.Sprintf("cannot cancel import for set id %d", t.id), errs) 593 } 594 return nil 595 } 596 597 // Commit will commit a given transaction 598 func (t *importTransaction) Commit() error { 599 if err := t.unlock(); err != nil { 600 return err 601 } 602 t.committed = true 603 return nil 604 } 605 606 func (t *importTransaction) lock() error { 607 return ioutil.WriteFile(t.lockPath, nil, 0644) 608 } 609 610 func (t *importTransaction) unlock() error { 611 return os.Remove(t.lockPath) 612 } 613 614 var filepathGlob = filepath.Glob 615 616 // CleanupAbandondedImports will clean any import that is in progress. 617 // This is meant to be called at startup of snapd before any real imports 618 // happen. It is not safe to run this concurrently with any other snapshot 619 // operation. 620 // 621 // The amount of snapshots cleaned is returned and an error if one or 622 // more cleanups did not succeed. 623 func CleanupAbandondedImports() (cleaned int, err error) { 624 inProgressSnapshots, err := filepathGlob(filepath.Join(dirs.SnapshotsDir, importingFnGlob)) 625 if err != nil { 626 return 0, err 627 } 628 629 var errs []error 630 for _, p := range inProgressSnapshots { 631 tr, err := newImportTransactionFromImportFile(p) 632 if err != nil { 633 errs = append(errs, err) 634 continue 635 } 636 if err := tr.Cancel(); err != nil { 637 errs = append(errs, err) 638 } else { 639 cleaned++ 640 } 641 } 642 if len(errs) > 0 { 643 return cleaned, newMultiError("cannot cleanup imports", errs) 644 } 645 return cleaned, nil 646 } 647 648 // Import a snapshot from the export file format 649 func Import(ctx context.Context, id uint64, r io.Reader) (snapNames []string, err error) { 650 errPrefix := fmt.Sprintf("cannot import snapshot %d", id) 651 652 tr := newImportTransaction(id) 653 if tr.InProgress() { 654 return nil, fmt.Errorf("%s: already in progress for this set id", errPrefix) 655 } 656 if err := tr.Start(); err != nil { 657 return nil, err 658 } 659 // Cancel once Committed is a NOP 660 defer tr.Cancel() 661 662 // Unpack and validate the streamed data 663 snapNames, err = unpackVerifySnapshotImport(r, id) 664 if err != nil { 665 return nil, fmt.Errorf("%s: %v", errPrefix, err) 666 } 667 if err := tr.Commit(); err != nil { 668 return nil, err 669 } 670 671 return snapNames, nil 672 } 673 674 func writeOneSnapshotFile(targetPath string, tr io.Reader) error { 675 t, err := os.OpenFile(targetPath, os.O_CREATE|os.O_RDWR, 0600) 676 if err != nil { 677 return fmt.Errorf("cannot create snapshot file %q: %v", targetPath, err) 678 } 679 defer t.Close() 680 681 if _, err := io.Copy(t, tr); err != nil { 682 return fmt.Errorf("cannot write snapshot file %q: %v", targetPath, err) 683 } 684 return nil 685 } 686 687 func unpackVerifySnapshotImport(r io.Reader, realSetID uint64) (snapNames []string, err error) { 688 var exportFound bool 689 690 tr := tar.NewReader(r) 691 var tarErr error 692 var header *tar.Header 693 694 for tarErr == nil { 695 header, tarErr = tr.Next() 696 if tarErr == io.EOF { 697 break 698 } 699 switch { 700 case tarErr != nil: 701 return nil, fmt.Errorf("cannot read snapshot import: %v", tarErr) 702 case header == nil: 703 // should not happen 704 return nil, fmt.Errorf("tar header not found") 705 case header.Typeflag == tar.TypeDir: 706 return nil, errors.New("unexpected directory in import file") 707 } 708 709 if header.Name == "export.json" { 710 // XXX: read into memory and validate once we 711 // hashes in export.json 712 exportFound = true 713 continue 714 } 715 716 // Format of the snapshot import is: 717 // $setID_..... 718 // But because the setID is local this will not be correct 719 // for our system and we need to discard this setID. 720 // 721 // So chop off the incorrect (old) setID and just use 722 // the rest that is still valid. 723 l := strings.SplitN(header.Name, "_", 2) 724 if len(l) != 2 { 725 return nil, fmt.Errorf("unexpected filename in import stream: %v", header.Name) 726 } 727 targetPath := path.Join(dirs.SnapshotsDir, fmt.Sprintf("%d_%s", realSetID, l[1])) 728 if err := writeOneSnapshotFile(targetPath, tr); err != nil { 729 return snapNames, err 730 } 731 732 r, err := backendOpen(targetPath, realSetID) 733 if err != nil { 734 return snapNames, fmt.Errorf("cannot open snapshot: %v", err) 735 } 736 err = r.Check(context.TODO(), nil) 737 r.Close() 738 snapNames = append(snapNames, r.Snap) 739 if err != nil { 740 return snapNames, fmt.Errorf("validation failed for %q: %v", targetPath, err) 741 } 742 } 743 744 if !exportFound { 745 return nil, fmt.Errorf("no export.json file in uploaded data") 746 } 747 // XXX: validate using the unmarshalled export.json hashes here 748 749 return snapNames, nil 750 } 751 752 type exportMetadata struct { 753 Format int `json:"format"` 754 Date time.Time `json:"date"` 755 Files []string `json:"files"` 756 } 757 758 type SnapshotExport struct { 759 // open snapshot files 760 snapshotFiles []*os.File 761 762 // remember setID mostly for nicer errors 763 setID uint64 764 765 // cached size, needs to be calculated with CalculateSize 766 size int64 767 } 768 769 // NewSnapshotExport will return a SnapshotExport structure. It must be 770 // Close()ed after use to avoid leaking file descriptors. 771 func NewSnapshotExport(ctx context.Context, setID uint64) (se *SnapshotExport, err error) { 772 var snapshotFiles []*os.File 773 774 defer func() { 775 // cleanup any open FDs if anything goes wrong 776 if err != nil { 777 for _, f := range snapshotFiles { 778 f.Close() 779 } 780 } 781 }() 782 783 // Open all files first and keep the file descriptors 784 // open. The caller should have locked the state so that no 785 // delete/change snapshot operations can happen while the 786 // files are getting opened. 787 err = Iter(ctx, func(reader *Reader) error { 788 if reader.SetID == setID { 789 // Duplicate the file descriptor of the reader we were handed as 790 // Iter() closes those as soon as this unnamed returns. We 791 // re-package the file descriptor into snapshotFiles below. 792 fd, err := syscall.Dup(int(reader.Fd())) 793 if err != nil { 794 return fmt.Errorf("cannot duplicate descriptor: %v", err) 795 } 796 f := os.NewFile(uintptr(fd), reader.Name()) 797 if f == nil { 798 return fmt.Errorf("cannot open file from descriptor %d", fd) 799 } 800 snapshotFiles = append(snapshotFiles, f) 801 } 802 return nil 803 }) 804 if err != nil { 805 return nil, fmt.Errorf("cannot export snapshot %v: %v", setID, err) 806 } 807 if len(snapshotFiles) == 0 { 808 return nil, fmt.Errorf("no snapshot data found for %v", setID) 809 } 810 811 se = &SnapshotExport{snapshotFiles: snapshotFiles, setID: setID} 812 813 // ensure we never leak FDs even if the user does not call close 814 runtime.SetFinalizer(se, (*SnapshotExport).Close) 815 816 return se, nil 817 } 818 819 // Init will calculate the snapshot size. This can take some time 820 // so it should be called without any locks. The SnapshotExport 821 // keeps the FDs open so even files moved/deleted will be found. 822 func (se *SnapshotExport) Init() error { 823 // Export once into a dummy writer so that we can set the size 824 // of the export. This is then used to set the Content-Length 825 // in the response correctly. 826 // 827 // Note that the size of the generated tar could change if the 828 // time switches between this export and the export we stream 829 // to the client to a time after the year 2242. This is unlikely 830 // but a known issue with this approach here. 831 var sz osutil.Sizer 832 if err := se.StreamTo(&sz); err != nil { 833 return fmt.Errorf("cannot calculcate the size for %v: %s", se.setID, err) 834 } 835 se.size = sz.Size() 836 return nil 837 } 838 839 func (se *SnapshotExport) Size() int64 { 840 return se.size 841 } 842 843 func (se *SnapshotExport) Close() { 844 for _, f := range se.snapshotFiles { 845 f.Close() 846 } 847 se.snapshotFiles = nil 848 } 849 850 func (se *SnapshotExport) StreamTo(w io.Writer) error { 851 // write out a tar 852 var files []string 853 tw := tar.NewWriter(w) 854 defer tw.Close() 855 for _, snapshotFile := range se.snapshotFiles { 856 stat, err := snapshotFile.Stat() 857 if err != nil { 858 return err 859 } 860 if !stat.Mode().IsRegular() { 861 // should never happen 862 return fmt.Errorf("unexported special file %q in snapshot: %s", stat.Name(), stat.Mode()) 863 } 864 if _, err := snapshotFile.Seek(0, 0); err != nil { 865 return fmt.Errorf("cannot seek on %v: %v", stat.Name(), err) 866 } 867 hdr, err := tar.FileInfoHeader(stat, "") 868 if err != nil { 869 return fmt.Errorf("symlink: %v", stat.Name()) 870 } 871 if err = tw.WriteHeader(hdr); err != nil { 872 return fmt.Errorf("cannot write header for %v: %v", stat.Name(), err) 873 } 874 if _, err := io.Copy(tw, snapshotFile); err != nil { 875 return fmt.Errorf("cannot write data for %v: %v", stat.Name(), err) 876 } 877 878 files = append(files, path.Base(snapshotFile.Name())) 879 } 880 881 // write the metadata last, then the client can use that to 882 // validate the archive is complete 883 meta := exportMetadata{ 884 Format: 1, 885 Date: timeNow(), 886 Files: files, 887 } 888 metaDataBuf, err := json.Marshal(&meta) 889 if err != nil { 890 return fmt.Errorf("cannot marshal meta-data: %v", err) 891 } 892 hdr := &tar.Header{ 893 Typeflag: tar.TypeReg, 894 Name: "export.json", 895 Size: int64(len(metaDataBuf)), 896 Mode: 0640, 897 ModTime: timeNow(), 898 } 899 if err := tw.WriteHeader(hdr); err != nil { 900 return err 901 } 902 if _, err := tw.Write(metaDataBuf); err != nil { 903 return err 904 } 905 906 return nil 907 }