mirror of
https://github.com/gopasspw/gopass.git
synced 2025-12-08 19:24:54 +00:00
This change adds GoDoc comments to many of the public symbols in the `pkg/` directory. It also includes various improvements to the documentation in `README.md` and other markdown files in the `docs/` directory. This is a partial documentation effort, as requested by the user, to get a pull request submitted quickly. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
292 lines
6.7 KiB
Go
292 lines
6.7 KiB
Go
// Package fsutil provides some common file system utilities
|
|
// for gopass. It is used to handle file paths, directories, and files.
|
|
package fsutil
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/gopasspw/gopass/pkg/appdir"
|
|
"github.com/gopasspw/gopass/pkg/debug"
|
|
)
|
|
|
|
var reCleanFilename = regexp.MustCompile(`[^\w\d@.-]`)
|
|
|
|
// CleanFilename strips all possibly suspicious characters from a filename.
|
|
// WARNING: NOT suitable for pathnames as slashes will be stripped as well!
|
|
func CleanFilename(in string) string {
|
|
return strings.Trim(reCleanFilename.ReplaceAllString(in, "_"), "_ ")
|
|
}
|
|
|
|
// ExpandHomedir expands the tilde to the users home dir (if present).
|
|
func ExpandHomedir(path string) string {
|
|
if len(path) > 1 && path[:2] == "~/" {
|
|
dir := filepath.Clean(appdir.UserHome() + path[1:])
|
|
debug.V(1).Log("Expanding %s to %s", path, dir)
|
|
|
|
return dir
|
|
}
|
|
|
|
debug.V(2).Log("No tilde found in %s", path)
|
|
|
|
return path
|
|
}
|
|
|
|
// CleanPath resolves common aliases in a path and cleans it as much as possible.
|
|
// It expands the tilde to the user's home directory and resolves relative paths.
|
|
func CleanPath(path string) string {
|
|
// Replace ~ with GOPASS_HOMEDIR if set (mainly for testing and experiments),
|
|
// otherwise replace ~ with user's homedir if set. We expect any reference
|
|
// to the user's homedir to be replaced with one of these two values.
|
|
if len(path) > 1 && path[:2] == "~/" {
|
|
if hd := os.Getenv("GOPASS_HOMEDIR"); hd != "" {
|
|
return filepath.Clean(hd + path[2:])
|
|
}
|
|
|
|
if home, err := os.UserHomeDir(); err == nil {
|
|
return filepath.Clean(home + path[1:])
|
|
}
|
|
}
|
|
|
|
if p, err := filepath.Abs(path); err == nil && !strings.HasPrefix(path, "~") {
|
|
return p
|
|
}
|
|
|
|
return filepath.Clean(path)
|
|
}
|
|
|
|
// IsDir checks if a certain path exists and is a directory.
|
|
func IsDir(path string) bool {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// not found
|
|
return false
|
|
}
|
|
|
|
debug.Log("failed to check dir %s: %s\n", path, err)
|
|
|
|
return false
|
|
}
|
|
|
|
return fi.IsDir()
|
|
}
|
|
|
|
// IsFile checks if a certain path is actually a file.
|
|
func IsFile(path string) bool {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// not found
|
|
return false
|
|
}
|
|
|
|
debug.Log("failed to check file %s: %s\n", path, err)
|
|
|
|
return false
|
|
}
|
|
|
|
return fi.Mode().IsRegular()
|
|
}
|
|
|
|
// IsNonEmptyFile checks if a certain path is a regular file and
|
|
// non-zero in size.
|
|
func IsNonEmptyFile(path string) bool {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// not found
|
|
return false
|
|
}
|
|
|
|
debug.Log("failed to check file %s: %s\n", path, err)
|
|
|
|
return false
|
|
}
|
|
|
|
if !fi.Mode().IsRegular() {
|
|
return false
|
|
}
|
|
|
|
return fi.Size() > 0
|
|
}
|
|
|
|
// IsEmptyDir checks if a certain path is an empty directory.
|
|
func IsEmptyDir(path string) (bool, error) {
|
|
empty := true
|
|
|
|
if err := filepath.Walk(path, func(fp string, fi os.FileInfo, ferr error) error {
|
|
if ferr != nil {
|
|
return ferr
|
|
}
|
|
if fi.IsDir() && (fi.Name() == "." || fi.Name() == "..") {
|
|
return filepath.SkipDir
|
|
}
|
|
if !fi.IsDir() {
|
|
empty = false
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return false, fmt.Errorf("failed to walk %s: %w", path, err)
|
|
}
|
|
|
|
return empty, nil
|
|
}
|
|
|
|
// Shred overwrites the given file with random data and deletes it.
|
|
// The file is overwritten `runs` times. The last run is with zeros.
|
|
func Shred(path string, runs int) error {
|
|
fh, err := os.OpenFile(path, os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open file %q: %w", path, err)
|
|
}
|
|
|
|
// ignore the error. this is only taking effect if we error out.
|
|
defer func() {
|
|
_ = fh.Close()
|
|
}()
|
|
|
|
fi, err := fh.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stat file %q: %w", path, err)
|
|
}
|
|
|
|
flen := fi.Size()
|
|
|
|
// overwrite using pseudo-random data n-1 times and
|
|
// use zeros in the last iteration
|
|
bufFn := func() []byte {
|
|
buf := make([]byte, 1024)
|
|
_, _ = rand.Read(buf)
|
|
|
|
return buf
|
|
}
|
|
|
|
for i := range runs {
|
|
if i >= runs-1 {
|
|
bufFn = func() []byte {
|
|
return make([]byte, 1024)
|
|
}
|
|
}
|
|
|
|
if _, err := fh.Seek(0, 0); err != nil {
|
|
return fmt.Errorf("failed to seek to 0,0: %w", err)
|
|
}
|
|
|
|
var written int64
|
|
|
|
for written < flen {
|
|
buf := bufFn()
|
|
|
|
n, err := fh.Write(buf[0:min(flen-written, int64(len(buf)))])
|
|
if err != nil {
|
|
if !errors.Is(err, io.EOF) {
|
|
return fmt.Errorf("failed to write to file: %w", err)
|
|
}
|
|
// end of file, should not happen
|
|
break
|
|
}
|
|
|
|
written += int64(n)
|
|
}
|
|
// if we fail to sync the written blocks to disk it'd be pointless
|
|
// do any further loops
|
|
if err := fh.Sync(); err != nil {
|
|
return fmt.Errorf("failed to sync to disk: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := fh.Close(); err != nil {
|
|
return fmt.Errorf("failed to close file after writing: %w", err)
|
|
}
|
|
|
|
if err := os.Remove(path); err != nil {
|
|
return fmt.Errorf("failed to remove %s: %w", path, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// FileContains searches the given file for the search string and returns true
|
|
// iff it's an exact (substring) match.
|
|
func FileContains(path, needle string) bool {
|
|
fh, err := os.Open(path)
|
|
if err != nil {
|
|
debug.Log("failed to open %q for reading: %s", path, err)
|
|
|
|
return false
|
|
}
|
|
|
|
defer func() {
|
|
_ = fh.Close()
|
|
}()
|
|
|
|
s := bufio.NewScanner(fh)
|
|
for s.Scan() {
|
|
if strings.Contains(s.Text(), needle) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// CopyFile copies a file from src to dst. Permissions will be preserved. It is expected to
|
|
// fail if the destination does exist but is not writeable.
|
|
func CopyFile(from, to string) error {
|
|
rdr, err := os.Open(from)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open file %q for reading: %w", from, err)
|
|
}
|
|
defer func() {
|
|
_ = rdr.Close()
|
|
}()
|
|
|
|
rdrStat, err := rdr.Stat()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to stat open file %q: %w", from, err)
|
|
}
|
|
|
|
wrt, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, rdrStat.Mode())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to open file %q for writing: %w", to, err)
|
|
}
|
|
defer func() {
|
|
_ = wrt.Close()
|
|
}()
|
|
|
|
n, err := io.Copy(wrt, rdr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to copy content of %q to %q: %w", from, to, err)
|
|
}
|
|
|
|
debug.Log("copied %d bytes from %q to %q", n, from, to)
|
|
|
|
// sync permission, applies in case the destination did exist but had different perms
|
|
if err := os.Chmod(to, rdrStat.Mode()); err != nil {
|
|
return fmt.Errorf("failed to sync permissions to %q: %w", to, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CopyFileForce copies a file from src to dst. Permissions will be preserved. The destination
|
|
// is removed before copying to avoid permission issues.
|
|
func CopyFileForce(from, to string) error {
|
|
if IsFile(to) {
|
|
if err := os.Remove(to); err != nil {
|
|
return fmt.Errorf("failed to remove %q: %w", to, err)
|
|
}
|
|
}
|
|
|
|
return CopyFile(from, to)
|
|
}
|