github.com/git-amp/amp-sdk-go@v0.7.5/stdlib/utils/files.go (about)

     1  package utils
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"path"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/git-amp/amp-sdk-go/stdlib/bufs"
    14  	"github.com/git-amp/amp-sdk-go/stdlib/errors"
    15  )
    16  
    17  var (
    18  	// DefaultDirPerms expresses the default mode of dir creation.
    19  	DefaultDirPerms = os.FileMode(0755)
    20  )
    21  
    22  // EnsureDirAndMaxPerms ensures that the given path exists, that it's a directory,
    23  // and that it has permissions that are no more permissive than the given ones.
    24  //
    25  // - If the path does not exist, it is created
    26  // - If the path exists, but is not a directory, an error is returned
    27  // - If the path exists, and is a directory, but has the wrong perms, it is chmod'ed
    28  func EnsureDirAndMaxPerms(path string, perms os.FileMode) error {
    29  	stat, err := os.Stat(path)
    30  	if err != nil && !os.IsNotExist(err) {
    31  		// Regular error
    32  		return err
    33  	} else if os.IsNotExist(err) {
    34  		// Dir doesn't exist, create it with desired perms
    35  		return os.MkdirAll(path, perms)
    36  	} else if !stat.IsDir() {
    37  		// Path exists, but it's a file, so don't clobber
    38  		return errors.Errorf("%v already exists and is not a directory", path)
    39  	} else if stat.Mode() != perms {
    40  		// Dir exists, but wrong perms, so chmod
    41  		return os.Chmod(path, (stat.Mode() & perms))
    42  	}
    43  	return nil
    44  }
    45  
    46  // WriteFileWithMaxPerms writes `data` to `path` and ensures that
    47  // the file has permissions that are no more permissive than the given ones.
    48  func WriteFileWithMaxPerms(path string, data []byte, perms os.FileMode) error {
    49  	f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perms)
    50  	if err != nil {
    51  		return err
    52  	}
    53  	defer f.Close()
    54  	err = EnsureFileMaxPerms(f, perms)
    55  	if err != nil {
    56  		return err
    57  	}
    58  	_, err = f.Write(data)
    59  	return err
    60  }
    61  
    62  // CopyFileWithMaxPerms copies the file at `srcPath` to `dstPath`
    63  // and ensures that it has permissions that are no more permissive than the given ones.
    64  func CopyFileWithMaxPerms(srcPath, dstPath string, perms os.FileMode) error {
    65  	src, err := os.Open(srcPath)
    66  	if err != nil {
    67  		return errors.Wrap(err, "could not open source file")
    68  	}
    69  	defer src.Close()
    70  
    71  	dst, err := os.OpenFile(dstPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perms)
    72  	if err != nil {
    73  		return errors.Wrap(err, "could not open destination file")
    74  	}
    75  	defer dst.Close()
    76  
    77  	err = EnsureFileMaxPerms(dst, perms)
    78  	if err != nil {
    79  		return errors.Wrap(err, "could not set file permissions")
    80  	}
    81  
    82  	_, err = io.Copy(dst, src)
    83  	return errors.Wrap(err, "could not copy file contents")
    84  }
    85  
    86  // EnsureFileMaxPerms ensures that the given file has permissions
    87  // that are no more permissive than the given ones.
    88  func EnsureFileMaxPerms(file *os.File, perms os.FileMode) error {
    89  	stat, err := file.Stat()
    90  	if err != nil {
    91  		return err
    92  	}
    93  	if stat.Mode() == perms {
    94  		return nil
    95  	}
    96  	return file.Chmod(stat.Mode() & perms)
    97  }
    98  
    99  // EnsureFilepathMaxPerms ensures that the file at the given filepath
   100  // has permissions that are no more permissive than the given ones.
   101  func EnsureFilepathMaxPerms(filepath string, perms os.FileMode) error {
   102  	dst, err := os.OpenFile(filepath, os.O_RDWR, perms)
   103  	if err != nil {
   104  		return err
   105  	}
   106  	defer dst.Close()
   107  
   108  	return EnsureFileMaxPerms(dst, perms)
   109  }
   110  
   111  type FileSize uint64
   112  
   113  var fsregex = regexp.MustCompile(`(\d+\.?\d*)(tb|gb|mb|kb|b)?`)
   114  
   115  const (
   116  	KB FileSize = 1024
   117  	MB FileSize = 1000000
   118  	GB FileSize = 1000 * MB
   119  	TB FileSize = 1000 * GB
   120  )
   121  
   122  func ParseFileSize(s string) (FileSize, error) {
   123  	var fs FileSize
   124  	err := fs.UnmarshalText([]byte(s))
   125  	return fs, err
   126  }
   127  
   128  func (s FileSize) MarshalText() ([]byte, error) {
   129  	if s > TB {
   130  		return []byte(fmt.Sprintf("%.2ftb", float64(s)/float64(TB))), nil
   131  	} else if s > GB {
   132  		return []byte(fmt.Sprintf("%.2fgb", float64(s)/float64(GB))), nil
   133  	} else if s > MB {
   134  		return []byte(fmt.Sprintf("%.2fmb", float64(s)/float64(MB))), nil
   135  	} else if s > KB {
   136  		return []byte(fmt.Sprintf("%.2fkb", float64(s)/float64(KB))), nil
   137  	}
   138  	return []byte(fmt.Sprintf("%db", s)), nil
   139  }
   140  
   141  func (s *FileSize) UnmarshalText(bs []byte) error {
   142  	matches := fsregex.FindAllStringSubmatch(strings.ToLower(string(bs)), -1)
   143  	if len(matches) != 1 {
   144  		return errors.Errorf(`bad filesize: "%v"`, string(bs))
   145  	} else if len(matches[0]) != 3 {
   146  		return errors.Errorf(`bad filesize: "%v"`, string(bs))
   147  	}
   148  	var (
   149  		num  = matches[0][1]
   150  		unit = matches[0][2]
   151  	)
   152  	bytes, err := strconv.ParseFloat(num, 64)
   153  	if err != nil {
   154  		return err
   155  	}
   156  
   157  	switch unit {
   158  	case "", "b":
   159  	case "kb":
   160  		bytes *= float64(KB)
   161  	case "mb":
   162  		bytes *= float64(MB)
   163  	case "gb":
   164  		bytes *= float64(GB)
   165  	case "tb":
   166  		bytes *= float64(TB)
   167  	default:
   168  		return errors.Errorf(`bad filesize unit: "%v"`, unit)
   169  	}
   170  	*s = FileSize(bytes)
   171  	return nil
   172  }
   173  
   174  func (s FileSize) String() string {
   175  	str, _ := s.MarshalText()
   176  	return string(str)
   177  }
   178  
   179  // ExpandAndCheckPath parses/expands the given path and then verifies it's existence or non-existence,
   180  // depending on autoCreate and returning the the expanded path.
   181  //
   182  // If autoCreate == true, an error is returned if the dir didn't exist and failed to be created.
   183  //
   184  // If autoCreate == false, an error is returned if the dir doesn't exist.
   185  func ExpandAndCheckPath(
   186  	pathname string,
   187  	autoCreate bool,
   188  ) (string, error) {
   189  
   190  	var err error
   191  	if err != nil {
   192  		err = errors.Errorf("error expanding '%s'", pathname)
   193  	} else {
   194  		_, err = os.Stat(pathname)
   195  		if err != nil && os.IsNotExist(err) {
   196  			if autoCreate {
   197  				err = os.MkdirAll(pathname, DefaultDirPerms)
   198  			} else {
   199  				err = errors.Errorf("path '%s' does not exist", pathname)
   200  			}
   201  		}
   202  	}
   203  
   204  	if err != nil {
   205  		return "", err
   206  	}
   207  
   208  	return pathname, nil
   209  }
   210  
   211  // CreateNewDir creates the specified dir (and returns an error if the dir already exists)
   212  //
   213  // If dirPath is absolute then basePath is ignored.
   214  // Returns the effective pathname.
   215  func CreateNewDir(basePath, dirPath string) (string, error) {
   216  	var pathname string
   217  
   218  	if path.IsAbs(dirPath) {
   219  		pathname = dirPath
   220  	} else {
   221  		pathname = path.Join(basePath, dirPath)
   222  	}
   223  
   224  	if _, err := os.Stat(pathname); !os.IsNotExist(err) {
   225  		return "", errors.Errorf("for safety, the path '%s' must not already exist", pathname)
   226  	}
   227  
   228  	if err := os.MkdirAll(pathname, DefaultDirPerms); err != nil {
   229  		return "", err
   230  	}
   231  
   232  	return pathname, nil
   233  }
   234  
   235  // GetExePath returns the pathname of the dir containing the host exe
   236  func GetExePath() (string, error) {
   237  	hostExe, err := os.Executable()
   238  	if err != nil {
   239  		return "", err
   240  	}
   241  	hostDir := path.Dir(hostExe)
   242  	return hostDir, nil
   243  }
   244  
   245  var remapCharset = map[rune]rune{
   246  	' ':  '-',
   247  	'.':  '-',
   248  	'?':  '-',
   249  	'\\': '+',
   250  	'/':  '+',
   251  	'&':  '+',
   252  }
   253  
   254  // MakeFSFriendly makes a given string safe to use for a file system.
   255  // If suffix is given, the hex encoding of those bytes are appended after a space.
   256  func MakeFSFriendly(name string, suffix []byte) string {
   257  
   258  	var b strings.Builder
   259  	for _, r := range name {
   260  		if replace, ok := remapCharset[r]; ok {
   261  			if replace != 0 {
   262  				b.WriteRune(replace)
   263  			}
   264  		} else {
   265  			b.WriteRune(r)
   266  		}
   267  	}
   268  
   269  	if len(suffix) > 0 {
   270  		b.WriteString(" ")
   271  		b.WriteString(bufs.Base32Encoding.EncodeToString(suffix))
   272  	}
   273  
   274  	friendlyName := b.String()
   275  	return friendlyName
   276  }
   277  
   278  // CreateTemp creates a temporary file with the given file mode flags using the given name pattern.
   279  // The name pattern should contain a '*' to where a random alphanumeric should go.
   280  func CreateTemp(dir, pattern string, fileFlags int) (ofile *os.File, err error) {
   281  	if dir == "" {
   282  		dir = os.TempDir()
   283  	}
   284  
   285  	var prefix, suffix string
   286  	if pos := strings.LastIndexByte(pattern, '*'); pos != -1 {
   287  		prefix, suffix = pattern[:pos], pattern[pos+1:]
   288  	} else {
   289  		prefix = pattern
   290  	}
   291  	prefix = path.Join(dir, prefix)
   292  
   293  	for {
   294  		msec := time.Now().UnixMicro()
   295  		name := prefix + strconv.FormatInt(int64(msec), 32) + suffix
   296  		ofile, err = os.OpenFile(name, os.O_CREATE|fileFlags, 0600)
   297  		if os.IsExist(err) {
   298  			continue
   299  		}
   300  		break
   301  	}
   302  	return
   303  }