dep/txn_writer.go

300 строки
8.8 KiB
Go
Исходник Обычный вид История

// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package dep
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/sdboyer/gps"
)
// SafeWriter transactionalizes writes of manifest, lock, and vendor dir, both
// individually and in any combination, into a pseudo-atomic action with
// transactional rollback.
//
// It is not impervious to errors (writing to disk is hard), but it should
// guard against non-arcane failure conditions.
type SafeWriter struct {
2017-03-09 22:28:42 +03:00
Payload *SafeWriterPayload
}
2017-03-09 22:28:42 +03:00
// SafeWriterPayload represents the actions SafeWriter will execute when SafeWriter.Write is called.
type SafeWriterPayload struct {
Manifest *Manifest
Lock *Lock
LockDiff *LockDiff
ForceWriteVendor bool
}
func (payload *SafeWriterPayload) HasLock() bool {
return payload.Lock != nil
}
func (payload *SafeWriterPayload) HasManifest() bool {
return payload.Manifest != nil
}
func (payload *SafeWriterPayload) HasVendor() bool {
// TODO(carolynvs) this can be calculated based on if we are writing the lock
// init -> switch to newlock
// ensure checks existence, why not move that into the prep?
return payload.ForceWriteVendor
}
// LockDiff is the set of differences between an existing lock file and an updated lock file.
// TODO(carolynvs) this should be moved to gps
type LockDiff struct {
Add []gps.LockedProject
Remove []gps.LockedProject
Modify []LockedProjectDiff
}
// LockedProjectDiff contains the before and after snapshot of a project reference.
// TODO(carolynvs) this should be moved to gps
type LockedProjectDiff struct {
Current gps.LockedProject // Current represents the project reference as defined in the existing lock file.
Updated gps.LockedProject // Updated represents the desired project reference.
}
// Prepare to write a set of config yaml, lock and vendor tree.
//
2017-03-09 22:28:42 +03:00
// - If manifest is provided, it will be written to the standard manifest file
// name beneath root.
// - If lock is provided it will be written to the standard
// lock file name in the root dir, but vendor will NOT be written
2017-03-09 22:28:42 +03:00
// - If lock and newLock are both provided and are equivalent, then neither lock
// nor vendor will be written
2017-03-09 22:28:42 +03:00
// - If lock and newLock are both provided and are not equivalent,
// the newLock will be written to the same location as above, and a vendor
// tree will be written to the vendor directory
// - If newLock is provided and lock is not, it will write both a lock
// and the vendor directory in the same way
// - If the forceVendor param is true, then vendor/ will be unconditionally
// written out based on newLock if present, else lock, else error.
func (sw *SafeWriter) Prepare(manifest *Manifest, lock *Lock, newLock gps.Lock, forceVendor bool) {
sw.Payload = &SafeWriterPayload{
Manifest: manifest,
ForceWriteVendor: forceVendor,
}
2017-03-09 22:28:42 +03:00
if newLock != nil {
rlf := LockFromInterface(newLock)
if lock == nil {
sw.Payload.Lock = rlf
sw.Payload.ForceWriteVendor = true
} else {
2017-03-09 22:28:42 +03:00
if !locksAreEquivalent(rlf, lock) {
sw.Payload.Lock = rlf
sw.Payload.ForceWriteVendor = true
}
}
2017-03-09 22:28:42 +03:00
} else if lock != nil {
sw.Payload.Lock = lock
}
2017-03-09 22:28:42 +03:00
}
2017-03-09 22:28:42 +03:00
func (payload SafeWriterPayload) validate(root string, sm gps.SourceManager) error {
if root == "" {
return errors.New("root path must be non-empty")
}
2017-03-09 22:28:42 +03:00
if is, err := IsDir(root); !is {
if err != nil {
return err
}
2017-03-09 22:28:42 +03:00
return fmt.Errorf("root path %q does not exist", root)
}
2017-03-09 22:28:42 +03:00
if payload.HasVendor() && sm == nil {
2017-01-25 00:48:14 +03:00
return errors.New("must provide a SourceManager if writing out a vendor dir")
}
2017-03-09 22:28:42 +03:00
if payload.HasVendor() && payload.Lock == nil {
return errors.New("must provide a lock in order to write out vendor")
}
2017-03-09 22:28:42 +03:00
return nil
}
// Write saves some combination of config yaml, lock, and a vendor tree.
// root is the absolute path of root dir in which to write.
// sm is only required if vendor is being written.
//
// It first writes to a temp dir, then moves them in place if and only if all the write
// operations succeeded. It also does its best to roll back if any moves fail.
// This mostly guarantees that dep cannot exit with a partial write that would
// leave an undefined state on disk.
func (sw *SafeWriter) Write(root string, sm gps.SourceManager) error {
if sw.Payload == nil {
return errors.New("Cannot call SafeWriter.Write before SafeWriter.Prepare")
}
err := sw.Payload.validate(root, sm)
if err != nil {
return err
}
if !sw.Payload.HasManifest() && !sw.Payload.HasLock() && !sw.Payload.HasVendor() {
// nothing to do
return nil
}
mpath := filepath.Join(root, ManifestName)
lpath := filepath.Join(root, LockName)
vpath := filepath.Join(root, "vendor")
td, err := ioutil.TempDir(os.TempDir(), "dep")
if err != nil {
return errors.Wrap(err, "error while creating temp dir for writing manifest/lock/vendor")
}
defer os.RemoveAll(td)
2017-03-09 22:28:42 +03:00
if sw.Payload.HasManifest() {
if err := writeFile(filepath.Join(td, ManifestName), sw.Payload.Manifest); err != nil {
return errors.Wrap(err, "failed to write manifest file to temp dir")
}
}
2017-03-09 22:28:42 +03:00
if sw.Payload.HasLock() {
if err := writeFile(filepath.Join(td, LockName), sw.Payload.Lock); err != nil {
return errors.Wrap(err, "failed to write lock file to temp dir")
}
}
2017-03-09 22:28:42 +03:00
if sw.Payload.HasVendor() {
err = gps.WriteDepTree(filepath.Join(td, "vendor"), sw.Payload.Lock, sm, true)
if err != nil {
return errors.Wrap(err, "error while writing out vendor tree")
}
}
// Move the existing files and dirs to the temp dir while we put the new
2017-01-25 10:29:04 +03:00
// ones in, to provide insurance against errors for as long as possible.
type pathpair struct {
from, to string
}
var restore []pathpair
var failerr error
var vendorbak string
2017-03-09 22:28:42 +03:00
if sw.Payload.HasManifest() {
if _, err := os.Stat(mpath); err == nil {
2017-01-25 10:29:04 +03:00
// Move out the old one.
tmploc := filepath.Join(td, ManifestName+".orig")
failerr = renameWithFallback(mpath, tmploc)
if failerr != nil {
goto fail
}
restore = append(restore, pathpair{from: tmploc, to: mpath})
}
2017-01-25 10:29:04 +03:00
// Move in the new one.
failerr = renameWithFallback(filepath.Join(td, ManifestName), mpath)
if failerr != nil {
goto fail
}
}
2017-03-09 22:28:42 +03:00
if sw.Payload.HasLock() {
if _, err := os.Stat(lpath); err == nil {
2017-01-25 10:29:04 +03:00
// Move out the old one.
tmploc := filepath.Join(td, LockName+".orig")
failerr = renameWithFallback(lpath, tmploc)
if failerr != nil {
goto fail
}
restore = append(restore, pathpair{from: tmploc, to: lpath})
}
2017-01-25 10:29:04 +03:00
// Move in the new one.
failerr = renameWithFallback(filepath.Join(td, LockName), lpath)
if failerr != nil {
goto fail
}
}
2017-03-09 22:28:42 +03:00
if sw.Payload.HasVendor() {
if _, err := os.Stat(vpath); err == nil {
2017-01-25 10:29:04 +03:00
// Move out the old vendor dir. just do it into an adjacent dir, to
// try to mitigate the possibility of a pointless cross-filesystem
2017-01-25 10:29:04 +03:00
// move with a temp directory.
vendorbak = vpath + ".orig"
if _, err := os.Stat(vendorbak); err == nil {
// If the adjacent dir already exists, bite the bullet and move
2017-01-25 10:29:04 +03:00
// to a proper tempdir.
vendorbak = filepath.Join(td, "vendor.orig")
}
failerr = renameWithFallback(vpath, vendorbak)
if failerr != nil {
goto fail
}
restore = append(restore, pathpair{from: vendorbak, to: vpath})
}
2017-01-25 10:29:04 +03:00
// Move in the new one.
failerr = renameWithFallback(filepath.Join(td, "vendor"), vpath)
if failerr != nil {
goto fail
}
}
// Renames all went smoothly. The deferred os.RemoveAll will get the temp
// dir, but if we wrote vendor, we have to clean that up directly
2017-03-09 22:28:42 +03:00
if sw.Payload.HasVendor() {
// Nothing we can really do about an error at this point, so ignore it
os.RemoveAll(vendorbak)
}
return nil
fail:
2017-01-25 10:29:04 +03:00
// If we failed at any point, move all the things back into place, then bail.
for _, pair := range restore {
2017-01-25 10:29:04 +03:00
// Nothing we can do on err here, as we're already in recovery mode.
renameWithFallback(pair.from, pair.to)
}
return failerr
}
2017-03-09 22:28:42 +03:00
func (sw *SafeWriter) PrintPreparedActions() error {
if sw.Payload.HasManifest() {
fmt.Println("Would have written the following manifest.json:")
m, err := sw.Payload.Manifest.MarshalJSON()
if err != nil {
return errors.Wrap(err, "ensure DryRun cannot read manifest")
}
fmt.Println(string(m))
}
if sw.Payload.HasLock() {
fmt.Println("Would have written the following lock.json:")
m, err := sw.Payload.Lock.MarshalJSON()
if err != nil {
return errors.Wrap(err, "ensure DryRun cannot read lock")
}
fmt.Println(string(m))
}
if sw.Payload.HasVendor() {
fmt.Println("Would have written the following projects to the vendor directory:")
for _, project := range sw.Payload.Lock.Projects() {
prj := project.Ident()
rev := GetRevisionFromVersion(project.Version())
if prj.Source == "" {
fmt.Printf("%s@%s\n", prj.ProjectRoot, rev)
} else {
fmt.Printf("%s -> %s@%s\n", prj.ProjectRoot, prj.Source, rev)
}
}
}
return nil
}