github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/updater/util/file.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package util
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"net/url"
    10  	"os"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"time"
    15  )
    16  
    17  // File uses a safer file API
    18  type File struct {
    19  	name string
    20  	data []byte
    21  	perm os.FileMode
    22  }
    23  
    24  // SafeWriter defines a writer that is safer (atomic)
    25  type SafeWriter interface {
    26  	GetFilename() string
    27  	WriteTo(io.Writer) (int64, error)
    28  }
    29  
    30  // NewFile returns a File
    31  func NewFile(name string, data []byte, perm os.FileMode) File {
    32  	return File{name, data, perm}
    33  }
    34  
    35  // Save file
    36  func (f File) Save(log Log) error {
    37  	return safeWriteToFile(f, f.perm, log)
    38  }
    39  
    40  // GetFilename returns the file name for SafeWriter
    41  func (f File) GetFilename() string {
    42  	return f.name
    43  }
    44  
    45  // WriteTo is for SafeWriter
    46  func (f File) WriteTo(w io.Writer) (int64, error) {
    47  	n, err := w.Write(f.data)
    48  	return int64(n), err
    49  }
    50  
    51  // safeWriteToFile to safely write to a file
    52  func safeWriteToFile(t SafeWriter, mode os.FileMode, log Log) error {
    53  	filename := t.GetFilename()
    54  	if filename == "" {
    55  		return fmt.Errorf("No filename")
    56  	}
    57  	log.Debugf("Writing to %s", filename)
    58  	tempFilename, tempFile, err := openTempFile(filename+"-", "", mode)
    59  	log.Debugf("Temporary file generated: %s", tempFilename)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	_, err = t.WriteTo(tempFile)
    64  	if err != nil {
    65  		log.Errorf("Error writing temporary file %s: %s", tempFilename, err)
    66  		_ = tempFile.Close()
    67  		_ = os.Remove(tempFilename)
    68  		return err
    69  	}
    70  	err = tempFile.Close()
    71  	if err != nil {
    72  		log.Errorf("Error closing temporary file %s: %s", tempFilename, err)
    73  		_ = os.Remove(tempFilename)
    74  		return err
    75  	}
    76  	err = os.Rename(tempFilename, filename)
    77  	if err != nil {
    78  		log.Errorf("Error renaming temporary file %s to %s: %s", tempFilename, filename, err)
    79  		_ = os.Remove(tempFilename)
    80  		return err
    81  	}
    82  	log.Debugf("Wrote to %s", filename)
    83  	return nil
    84  }
    85  
    86  // Close closes a file and ignores the error.
    87  // This satisfies lint checks when using with defer and you don't care if there
    88  // is an error, so instead of:
    89  //
    90  //	defer func() { _ = f.Close() }()
    91  //	defer Close(f)
    92  func Close(f io.Closer) {
    93  	if f == nil {
    94  		return
    95  	}
    96  	_ = f.Close()
    97  }
    98  
    99  // RemoveFileAtPath removes a file at path (and any children) ignoring any error.
   100  // We do nothing if path == "".
   101  // This satisfies lint checks when using with defer and you don't care if there
   102  // is an error, so instead of:
   103  //
   104  //	defer func() { _ = os.Remove(path) }()
   105  //	defer RemoveFileAtPath(path)
   106  func RemoveFileAtPath(path string) {
   107  	if path == "" {
   108  		return
   109  	}
   110  	_ = os.RemoveAll(path)
   111  }
   112  
   113  // openTempFile creates an opened temporary file.
   114  //
   115  //	openTempFile("foo", ".zip", 0755) => "foo.RCG2KUSCGYOO3PCKNWQHBOXBKACOPIKL.zip"
   116  //	openTempFile(path.Join(os.TempDir(), "foo"), "", 0600) => "/tmp/foo.RCG2KUSCGYOO3PCKNWQHBOXBKACOPIKL"
   117  func openTempFile(prefix string, suffix string, mode os.FileMode) (string, *os.File, error) {
   118  	filename, err := RandomID(prefix)
   119  	if err != nil {
   120  		return "", nil, err
   121  	}
   122  	if suffix != "" {
   123  		filename += suffix
   124  	}
   125  	flags := os.O_WRONLY | os.O_CREATE | os.O_EXCL
   126  	if mode == 0 {
   127  		mode = 0600
   128  	}
   129  	file, err := os.OpenFile(filename, flags, mode)
   130  	return filename, file, err
   131  }
   132  
   133  // FileExists returns whether the given file or directory exists or not
   134  func FileExists(path string) (bool, error) {
   135  	_, err := os.Stat(path)
   136  	if err == nil {
   137  		return true, nil
   138  	}
   139  	if os.IsNotExist(err) {
   140  		return false, nil
   141  	}
   142  	return false, err
   143  }
   144  
   145  // MakeParentDirs ensures parent directory exist for path
   146  func MakeParentDirs(path string, mode os.FileMode, log Log) error {
   147  	// 2nd return value here is filename (not an error), which is not needed
   148  	dir, _ := filepath.Split(path)
   149  	if dir == "" {
   150  		return fmt.Errorf("No base directory")
   151  	}
   152  	return MakeDirs(dir, mode, log)
   153  }
   154  
   155  // MakeDirs ensures directory exists for path
   156  func MakeDirs(dir string, mode os.FileMode, log Log) error {
   157  	exists, err := FileExists(dir)
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	if !exists {
   163  		log.Debugf("Creating: %s\n", dir)
   164  		err = os.MkdirAll(dir, mode)
   165  		if err != nil {
   166  			return err
   167  		}
   168  	}
   169  	return nil
   170  }
   171  
   172  // TempPath returns a temporary unique file path.
   173  // If for some reason we can't obtain random data, we still return a valid
   174  // path, which may not be as unique.
   175  // If tempDir is "", then os.TempDir() is used.
   176  func TempPath(tempDir string, prefix string) string {
   177  	if tempDir == "" {
   178  		tempDir = os.TempDir()
   179  	}
   180  	filename, err := RandomID(prefix)
   181  	if err != nil {
   182  		// We had an error getting random bytes, we'll use current nanoseconds
   183  		filename = fmt.Sprintf("%s%d", prefix, time.Now().UnixNano())
   184  	}
   185  	path := filepath.Join(tempDir, filename)
   186  	return path
   187  }
   188  
   189  // WriteTempFile creates a unique temp file with data.
   190  //
   191  // For example:
   192  //
   193  //	WriteTempFile("Test.", byte[]("test data"), 0600)
   194  func WriteTempFile(prefix string, data []byte, mode os.FileMode) (string, error) {
   195  	path := TempPath("", prefix)
   196  	if err := os.WriteFile(path, data, mode); err != nil {
   197  		return "", err
   198  	}
   199  	return path, nil
   200  }
   201  
   202  // MakeTempDir creates a unique temp directory.
   203  //
   204  // For example:
   205  //
   206  //	MakeTempDir("Test.", 0700)
   207  func MakeTempDir(prefix string, mode os.FileMode) (string, error) {
   208  	path := TempPath("", prefix)
   209  	if err := os.MkdirAll(path, mode); err != nil {
   210  		return "", err
   211  	}
   212  	return path, nil
   213  }
   214  
   215  // IsDirReal returns true if directory exists and is a real directory (not a symlink).
   216  // If it returns false, an error will be set explaining why.
   217  func IsDirReal(path string) (bool, error) {
   218  	fileInfo, err := os.Lstat(path)
   219  	if err != nil {
   220  		return false, err
   221  	}
   222  	// Check if symlink
   223  	if fileInfo.Mode()&os.ModeSymlink != 0 {
   224  		return false, fmt.Errorf("Path is a symlink")
   225  	}
   226  	if !fileInfo.Mode().IsDir() {
   227  		return false, fmt.Errorf("Path is not a directory")
   228  	}
   229  	return true, nil
   230  }
   231  
   232  // MoveFile moves a file safely.
   233  // It will create parent directories for destinationPath if they don't exist.
   234  // If the destination already exists and you specify a tmpDir, it will move
   235  // it there, otherwise it will be removed.
   236  func MoveFile(sourcePath string, destinationPath string, tmpDir string, log Log) error {
   237  	if _, statErr := os.Stat(destinationPath); statErr == nil {
   238  		if tmpDir == "" {
   239  			log.Infof("Removing existing destination path: %s", destinationPath)
   240  			if removeErr := os.RemoveAll(destinationPath); removeErr != nil {
   241  				return removeErr
   242  			}
   243  		} else {
   244  			tmpPath := filepath.Join(tmpDir, filepath.Base(destinationPath))
   245  			log.Infof("Moving existing destination %q to %q", destinationPath, tmpPath)
   246  			if tmpMoveErr := os.Rename(destinationPath, tmpPath); tmpMoveErr != nil {
   247  				return tmpMoveErr
   248  			}
   249  		}
   250  	}
   251  
   252  	if err := MakeParentDirs(destinationPath, 0700, log); err != nil {
   253  		return err
   254  	}
   255  
   256  	log.Infof("Moving %s to %s", sourcePath, destinationPath)
   257  	// Rename will copy over an existing destination
   258  	return os.Rename(sourcePath, destinationPath)
   259  }
   260  
   261  // CopyFile copies a file safely.
   262  // It will create parent directories for destinationPath if they don't exist.
   263  // It will overwrite an existing destinationPath.
   264  func CopyFile(sourcePath string, destinationPath string, log Log) error {
   265  	log.Infof("Copying %s to %s", sourcePath, destinationPath)
   266  	in, err := os.Open(sourcePath)
   267  	if err != nil {
   268  		return err
   269  	}
   270  	defer Close(in)
   271  
   272  	if _, statErr := os.Stat(destinationPath); statErr == nil {
   273  		log.Infof("Removing existing destination path: %s", destinationPath)
   274  		if removeErr := os.RemoveAll(destinationPath); removeErr != nil {
   275  			return removeErr
   276  		}
   277  	}
   278  
   279  	if makeDirErr := MakeParentDirs(destinationPath, 0700, log); makeDirErr != nil {
   280  		return makeDirErr
   281  	}
   282  
   283  	out, err := os.Create(destinationPath)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	defer Close(out)
   288  	_, err = io.Copy(out, in)
   289  	closeErr := out.Close()
   290  	if err != nil {
   291  		return err
   292  	}
   293  	return closeErr
   294  }
   295  
   296  // ReadFile returns data for file at path
   297  func ReadFile(path string) ([]byte, error) {
   298  	file, err := os.Open(path)
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  	defer Close(file)
   303  	data, err := io.ReadAll(file)
   304  	if err != nil {
   305  		return nil, err
   306  	}
   307  	return data, nil
   308  }
   309  
   310  func convertPathForWindows(path string) string {
   311  	return "/" + strings.ReplaceAll(path, `\`, `/`)
   312  }
   313  
   314  // URLStringForPath returns an URL as string with file scheme for path.
   315  // For example,
   316  //
   317  //	/usr/local/go/bin => file:///usr/local/go/bin
   318  //	C:\Go\bin => file:///C:/Go/bin
   319  func URLStringForPath(path string) string {
   320  	if runtime.GOOS == "windows" {
   321  		path = convertPathForWindows(path)
   322  	}
   323  	u := &url.URL{Path: path}
   324  	encodedPath := u.String()
   325  	return fmt.Sprintf("%s://%s", fileScheme, encodedPath)
   326  }
   327  
   328  // PathFromURL returns path for file URL scheme
   329  // For example,
   330  //
   331  //	file:///usr/local/go/bin => /usr/local/go/bin
   332  //	file:///C:/Go/bin => C:\Go\bin
   333  func PathFromURL(u *url.URL) string {
   334  	path := u.Path
   335  	if runtime.GOOS == "windows" && u.Scheme == fileScheme {
   336  		// Remove leading slash for Windows
   337  		path = strings.TrimPrefix(path, "/")
   338  		path = filepath.FromSlash(path)
   339  	}
   340  	return path
   341  }
   342  
   343  // Touch a file, updating its modification time
   344  func Touch(path string) error {
   345  	f, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE|os.O_TRUNC, 0600)
   346  	Close(f)
   347  	return err
   348  }
   349  
   350  // FileModTime returns modification time for file.
   351  // If file doesn't exist returns error.
   352  func FileModTime(path string) (time.Time, error) {
   353  	info, err := os.Stat(path)
   354  	if err != nil {
   355  		return time.Time{}, err
   356  	}
   357  	return info.ModTime(), nil
   358  }