github.com/gogf/selfupdate@v0.0.0-20231215043001-5c48c528462f/apply.go (about)

     1  package selfupdate
     2  
     3  import (
     4  	"bytes"
     5  	"crypto"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  
    13  	"github.com/gogf/selfupdate/internal/osext"
    14  )
    15  
    16  // Apply performs an update of the current executable or opts.TargetFile, with
    17  // the contents of the given io.Reader. When the update fails, it is unlikely
    18  // that old executable is corrupted, but still, applications need to check the
    19  // returned error with RollbackError() and notify the user of the bad news and
    20  // ask them to recover manually.
    21  func Apply(update io.Reader, opts Options) error {
    22  	err := PrepareAndCheckBinary(update, opts)
    23  	if err != nil {
    24  		return err
    25  	}
    26  	return CommitBinary(opts)
    27  }
    28  
    29  // PrepareAndCheckBinary reads the new binary content from io.Reader and performs the following actions:
    30  //  1. If configured, applies the contents of the update io.Reader as a binary patch.
    31  //  2. If configured, computes the checksum of the executable and verifies it matches.
    32  //  3. If configured, verifies the signature with a public key.
    33  //  4. Creates a new file, /path/to/.target.new with the TargetMode with the contents of the updated file
    34  func PrepareAndCheckBinary(update io.Reader, opts Options) error {
    35  	// get target path
    36  	targetPath, err := opts.getPath()
    37  	if err != nil {
    38  		return err
    39  	}
    40  
    41  	var newBytes []byte
    42  	if opts.Patcher != nil {
    43  		if newBytes, err = opts.applyPatch(update, targetPath); err != nil {
    44  			return err
    45  		}
    46  	} else {
    47  		// no patch to apply, go on through
    48  		if newBytes, err = ioutil.ReadAll(update); err != nil {
    49  			return err
    50  		}
    51  	}
    52  
    53  	// verify checksum if requested
    54  	if opts.Checksum != nil {
    55  		if err = opts.verifyChecksum(newBytes); err != nil {
    56  			return err
    57  		}
    58  	}
    59  
    60  	if opts.Verifier != nil {
    61  		if err = opts.Verifier.Verify(newBytes); err != nil {
    62  			return err
    63  		}
    64  	}
    65  
    66  	// get the directory the executable exists in
    67  	updateDir := filepath.Dir(targetPath)
    68  	filename := filepath.Base(targetPath)
    69  
    70  	// Copy the contents of newbinary to a new executable file
    71  	newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
    72  	fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, opts.getMode())
    73  	if err != nil {
    74  		return err
    75  	}
    76  	defer fp.Close()
    77  
    78  	_, err = io.Copy(fp, bytes.NewReader(newBytes))
    79  	if err != nil {
    80  		return err
    81  	}
    82  
    83  	// if we don't call fp.Close(), windows won't let us move the new executable
    84  	// because the file will still be "in use"
    85  	fp.Close()
    86  	return nil
    87  }
    88  
    89  // CommitBinary moves the new executable to the location of the current executable or opts.TargetPath
    90  // if specified. It performs the following operations:
    91  //  1. Renames /path/to/target to /path/to/.target.old
    92  //  2. Renames /path/to/.target.new to /path/to/target
    93  //  3. If the final rename is successful, deletes /path/to/.target.old, returns no error. On Windows,
    94  //     the removal of /path/to/target.old always fails, so instead Apply hides the old file instead.
    95  //  4. If the final rename fails, attempts to roll back by renaming /path/to/.target.old
    96  //     back to /path/to/target.
    97  //
    98  // If the roll back operation fails, the file system is left in an inconsistent state where there is
    99  // no new executable file and the old executable file could not be be moved to its original location.
   100  // In this case you should notify the user of the bad news and ask them to recover manually. Applications
   101  // can determine whether the rollback failed by calling RollbackError, see the documentation on that function
   102  // for additional detail.
   103  func CommitBinary(opts Options) error {
   104  	// get the directory the file exists in
   105  	targetPath, err := opts.getPath()
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	updateDir := filepath.Dir(targetPath)
   111  	filename := filepath.Base(targetPath)
   112  	newPath := filepath.Join(updateDir, fmt.Sprintf(".%s.new", filename))
   113  
   114  	// this is where we'll move the executable to so that we can swap in the updated replacement
   115  	oldPath := opts.OldSavePath
   116  	removeOld := opts.OldSavePath == ""
   117  	if removeOld {
   118  		oldPath = filepath.Join(updateDir, fmt.Sprintf(".%s.old", filename))
   119  	}
   120  
   121  	// delete any existing old exec file - this is necessary on Windows for two reasons:
   122  	// 1. after a successful update, Windows can't remove the .old file because the process is still running
   123  	// 2. windows rename operations fail if the destination file already exists
   124  	_ = os.Remove(oldPath)
   125  
   126  	// move the existing executable to a new file in the same directory
   127  	err = os.Rename(targetPath, oldPath)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	// move the new exectuable in to become the new program
   133  	err = os.Rename(newPath, targetPath)
   134  
   135  	if err != nil {
   136  		// move unsuccessful
   137  		//
   138  		// The filesystem is now in a bad state. We have successfully
   139  		// moved the existing binary to a new location, but we couldn't move the new
   140  		// binary to take its place. That means there is no file where the current executable binary
   141  		// used to be!
   142  		// Try to rollback by restoring the old binary to its original path.
   143  		rerr := os.Rename(oldPath, targetPath)
   144  		if rerr != nil {
   145  			return &rollbackErr{err, rerr}
   146  		}
   147  
   148  		return err
   149  	}
   150  
   151  	// move successful, remove the old binary if needed
   152  	if removeOld {
   153  		errRemove := os.Remove(oldPath)
   154  
   155  		// windows has trouble with removing old binaries, so hide it instead
   156  		if errRemove != nil {
   157  			_ = hideFile(oldPath)
   158  		}
   159  	}
   160  
   161  	return nil
   162  }
   163  
   164  // RollbackError takes an error value returned by Apply and returns the error, if any,
   165  // that occurred when attempting to roll back from a failed update. Applications should
   166  // always call this function on any non-nil errors returned by Apply.
   167  //
   168  // If no rollback was needed or if the rollback was successful, RollbackError returns nil,
   169  // otherwise it returns the error encountered when trying to roll back.
   170  func RollbackError(err error) error {
   171  	if err == nil {
   172  		return nil
   173  	}
   174  	if rerr, ok := err.(*rollbackErr); ok {
   175  		return rerr.rollbackErr
   176  	}
   177  	return nil
   178  }
   179  
   180  type rollbackErr struct {
   181  	error             // original error
   182  	rollbackErr error // error encountered while rolling back
   183  }
   184  
   185  type Options struct {
   186  	// TargetPath defines the path to the file to update.
   187  	// The emptry string means 'the executable file of the running program'.
   188  	TargetPath string
   189  
   190  	// Create TargetPath replacement with this file mode. If zero, defaults to 0755.
   191  	TargetMode os.FileMode
   192  
   193  	// Checksum of the new binary to verify against. If nil, no checksum or signature verification is done.
   194  	Checksum []byte
   195  
   196  	// Verifier for signature verification. If nil, no signature verification is done.
   197  	Verifier *Verifier
   198  
   199  	// Use this hash function to generate the checksum. If not set, SHA256 is used.
   200  	Hash crypto.Hash
   201  
   202  	// If nil, treat the update as a complete replacement for the contents of the file at TargetPath.
   203  	// If non-nil, treat the update contents as a patch and use this object to apply the patch.
   204  	Patcher Patcher
   205  
   206  	// Store the old executable file at this path after a successful update.
   207  	// The empty string means the old executable file will be removed after the update.
   208  	OldSavePath string
   209  }
   210  
   211  // CheckPermissions determines whether the process has the correct permissions to
   212  // perform the requested update. If the update can proceed, it returns nil, otherwise
   213  // it returns the error that would occur if an update were attempted.
   214  func (o *Options) CheckPermissions() error {
   215  	// get the directory the file exists in
   216  	path, err := o.getPath()
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	fileDir := filepath.Dir(path)
   222  	fileName := filepath.Base(path)
   223  
   224  	// attempt to open a file in the file's directory
   225  	newPath := filepath.Join(fileDir, fmt.Sprintf(".%s.check-perm", fileName))
   226  	fp, err := os.OpenFile(newPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, o.getMode())
   227  	if err != nil {
   228  		return err
   229  	}
   230  	fp.Close()
   231  
   232  	_ = os.Remove(newPath)
   233  	return nil
   234  }
   235  
   236  func (o *Options) getPath() (string, error) {
   237  	if o.TargetPath == "" {
   238  		return osext.Executable()
   239  	} else {
   240  		return o.TargetPath, nil
   241  	}
   242  }
   243  
   244  func (o *Options) getMode() os.FileMode {
   245  	if o.TargetMode == 0 {
   246  		return 0755
   247  	}
   248  	return o.TargetMode
   249  }
   250  
   251  func (o *Options) getHash() crypto.Hash {
   252  	if o.Hash == 0 {
   253  		o.Hash = crypto.SHA256
   254  	}
   255  	return o.Hash
   256  }
   257  
   258  func (o *Options) applyPatch(patch io.Reader, targetPath string) ([]byte, error) {
   259  	// open the file to patch
   260  	old, err := os.Open(targetPath)
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  	defer old.Close()
   265  
   266  	// apply the patch
   267  	var applied bytes.Buffer
   268  	if err = o.Patcher.Patch(old, &applied, patch); err != nil {
   269  		return nil, err
   270  	}
   271  
   272  	return applied.Bytes(), nil
   273  }
   274  
   275  func (o *Options) verifyChecksum(updated []byte) error {
   276  	checksum, err := checksumFor(o.getHash(), updated)
   277  	if err != nil {
   278  		return err
   279  	}
   280  
   281  	if !bytes.Equal(o.Checksum, checksum) {
   282  		return fmt.Errorf("Updated file has wrong checksum. Expected: %x, got: %x", o.Checksum, checksum)
   283  	}
   284  	return nil
   285  }
   286  
   287  func checksumFor(h crypto.Hash, payload []byte) ([]byte, error) {
   288  	if !h.Available() {
   289  		return nil, errors.New("requested hash function not available")
   290  	}
   291  	hash := h.New()
   292  	hash.Write(payload) // guaranteed not to error
   293  	return hash.Sum([]byte{}), nil
   294  }