blob: ebf084eeae7b9ad41422a966c4961bcd6b65255e [file] [log] [blame]
package fileutil
import (
"bufio"
"crypto/sha1"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
// EnsureDirExists checks whether the given path to a directory exits and creates it
// if necessary. Returns the absolute path that corresponds to the input path
// and an error indicating a problem.
func EnsureDirExists(dirPath string) (string, error) {
absPath, err := filepath.Abs(dirPath)
if err != nil {
return "", err
}
return absPath, os.MkdirAll(absPath, 0700)
}
// EnsureDirPathExists checks whether the directories of the given file path
// exist and creates them if necessary. Returns an error if there was a problem
// creating the path.
func EnsureDirPathExists(dirPath string) error {
absPath, err := filepath.Abs(dirPath)
if err != nil {
return err
}
dirs, _ := filepath.Split(absPath)
if err := os.MkdirAll(dirs, 0700); err != nil {
return err
}
return nil
}
// Must checks whether err in the provided pair (s, err) is nil. If so it
// returns s otherwise it cause the program to stop with the error message.
func Must(s string, err error) string {
if err != nil {
sklog.Fatal(err)
}
return s
}
// MustOpen opens the file of the given name, returning an *os.File on success, or causing the program to stop with the error message.
func MustOpen(name string) *os.File {
f, err := os.Open(name)
if err != nil {
sklog.Fatal(err)
}
return f
}
// MustReaddir returns a slice of os.FileInfo for every file in the given dir. This is equivalent to calling dir.Readdir(-1), except this call will stop the program on an error
func MustReaddir(dir *os.File) []os.FileInfo {
fi, err := dir.Readdir(-1)
if err != nil {
sklog.Fatal(err)
}
return fi
}
// FileExists returns true if the given path exists and false otherwise.
// If there is an error it will return false and log the error message.
func FileExists(path string) bool {
if _, err := os.Stat(path); os.IsNotExist(err) {
return false
} else if err != nil {
sklog.Error(err)
return false
}
return true
}
// TwoLevelRadixPath expands a path (defined by one or more path segments) by
// adding two additional directories based on the filename in the last segment.
// i.e. TwoLevelRadixPath("/some", "path", "to", "abcdefgh.txt") will
// return "/some/path/to/ab/cd/abcdefgh.txt".
// If the filename does not have at least four characters before a period the
// return values is equivalent to filepath.Join(pathSegments...).
func TwoLevelRadixPath(pathSegments ...string) string {
lastSeg := pathSegments[len(pathSegments)-1]
dirName, fileName := filepath.Split(lastSeg)
dotIdx := strings.Index(fileName, ".")
if ((dotIdx < 4) && (dotIdx >= 0)) || ((dotIdx == -1) && (len(fileName) < 4)) {
return filepath.Join(pathSegments...)
}
return filepath.Join(filepath.Join(pathSegments[:len(pathSegments)-1]...), filepath.Join(dirName, fileName[0:2], fileName[2:4], fileName))
}
// ReadAndSha1File opens a file, reads the contents, hashes them using the sha1
// hashing algorithm and returns the contents and hash. If the file doesn't exist, the err will be
// nil and both contents and hash will be empty string.
func ReadAndSha1File(path string) (contents, hash string, err error) {
if stat, err := os.Stat(path); err != nil {
if os.IsNotExist(err) {
return "", "", nil
}
return "", "", fmt.Errorf("Problem getting information for file %s: %s", path, err)
} else if stat.IsDir() {
return "", "", fmt.Errorf("Cannot open a directory: %s", path)
}
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return "", "", nil
}
return "", "", fmt.Errorf("Problem opening file %s: %s", path, err)
}
defer util.Close(f)
b, err := io.ReadAll(f)
if err != nil {
return "", "", fmt.Errorf("Problem reading file %s: %s", path, err)
}
return string(b), fmt.Sprintf("%x", sha1.Sum(b)), nil
}
// CountLines opens the given path and counts the number of lines in the file.
// Returns -1 with a non-nil error if an error is encountered.
func CountLines(path string) (int, error) {
file, err := os.Open(path)
if err != nil {
return -1, err
}
defer util.Close(file)
numLines := 0
// Using ReadLine instead of Scanner and ReadString because it can handle
// lines longer than 65536 characters.
r := bufio.NewReader(file)
for {
var isPrefix bool
for {
_, isPrefix, err = r.ReadLine()
// If we've reached the end of the line, stop reading.
if !isPrefix {
break
}
// If we're just at the EOF, break
if err != nil {
break
}
}
if err == io.EOF {
break
} else if err != nil {
return -1, err
}
numLines++
}
if err != io.EOF {
return -1, err
}
return numLines, nil
}
// ReadAllFilesRecursive recursively reads all files in the given dir and
// returns a map of filename to contents.
func ReadAllFilesRecursive(dir string, excludeDirs []string) (map[string][]byte, error) {
contents := map[string][]byte{}
if err := filepath.Walk(dir, func(fp string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("Failed to walk filesystem: %s", err)
}
if info.IsDir() {
base := filepath.Base(fp)
if util.In(base, excludeDirs) {
return filepath.SkipDir
}
return nil
}
b, err := os.ReadFile(fp)
if err != nil {
return fmt.Errorf("Failed to read file: %s", err)
}
relpath := strings.TrimPrefix(strings.TrimPrefix(fp, dir), string(filepath.Separator))
contents[relpath] = b
return nil
}); err != nil {
return nil, fmt.Errorf("Failed to walk filesystem: %s", err)
}
return contents, nil
}