mirror of
https://github.com/gopasspw/gopass.git
synced 2026-09-30 11:16:53 +02:00
249 lines
6.3 KiB
Go
249 lines
6.3 KiB
Go
// Package fs implement a password-store compatible on disk storage layout
|
|
// with unencrypted paths.
|
|
package fs
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/blang/semver/v4"
|
|
"github.com/gopasspw/gopass/internal/store"
|
|
"github.com/gopasspw/gopass/pkg/debug"
|
|
"github.com/gopasspw/gopass/pkg/fsutil"
|
|
)
|
|
|
|
// Store is a fs based store.
|
|
type Store struct {
|
|
path string
|
|
}
|
|
|
|
// New creates a new store.
|
|
func New(dir string) *Store {
|
|
if d, err := filepath.EvalSymlinks(dir); err == nil {
|
|
dir = d
|
|
}
|
|
|
|
return &Store{
|
|
path: fsutil.ExpandHomedir(dir),
|
|
}
|
|
}
|
|
|
|
// Get retrieves the named content.
|
|
func (s *Store) Get(ctx context.Context, name string) ([]byte, error) {
|
|
if runtime.GOOS == "windows" {
|
|
name = filepath.FromSlash(name)
|
|
}
|
|
|
|
path := filepath.Join(s.path, filepath.Clean(name))
|
|
debug.V(3).Log("Reading %s from %s", name, path)
|
|
|
|
return os.ReadFile(path)
|
|
}
|
|
|
|
// Set writes the given content.
|
|
func (s *Store) Set(ctx context.Context, name string, value []byte) error {
|
|
if runtime.GOOS == "windows" {
|
|
name = filepath.FromSlash(name)
|
|
}
|
|
|
|
filename := filepath.Join(s.path, filepath.Clean(name))
|
|
filedir := filepath.Dir(filename)
|
|
|
|
if !fsutil.IsDir(filedir) {
|
|
if err := os.MkdirAll(filedir, 0o700); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
debug.V(3).Log("Writing %s to %q", name, filename)
|
|
|
|
// if we ever try to write a secret that is identical (in ciphertext) to the secret in store,
|
|
// we might want to act differently
|
|
// (for instance, by not adding/committing/pushing the secret in git,
|
|
// or by panicking in the case of password generation)
|
|
oldvalue, err := os.ReadFile(filename)
|
|
if err == nil && bytes.Equal(oldvalue, value) {
|
|
return store.ErrMeaninglessWrite
|
|
}
|
|
|
|
return os.WriteFile(filename, value, 0o644)
|
|
}
|
|
|
|
// Move moves the named entity to the new location.
|
|
func (s *Store) Move(ctx context.Context, from, to string, del bool) error {
|
|
if runtime.GOOS == "windows" {
|
|
from = filepath.FromSlash(from)
|
|
to = filepath.FromSlash(to)
|
|
}
|
|
|
|
fromFn := filepath.Join(s.path, filepath.Clean(from))
|
|
toFn := filepath.Join(s.path, filepath.Clean(to))
|
|
toDir := filepath.Dir(toFn)
|
|
|
|
if !fsutil.IsDir(toDir) {
|
|
if err := os.MkdirAll(toDir, 0o700); err != nil {
|
|
return fmt.Errorf("failed to create directory %q: %w", toDir, err)
|
|
}
|
|
}
|
|
debug.V(3).Log("Copying %q (%q) to %q (%q)", from, fromFn, to, toFn)
|
|
|
|
if del {
|
|
if err := os.Rename(fromFn, toFn); err != nil {
|
|
return fmt.Errorf("failed to copy %q to %q: %w", from, to, err)
|
|
}
|
|
|
|
return s.removeEmptyParentDirectories(fromFn)
|
|
}
|
|
|
|
return fsutil.CopyFile(fromFn, toFn)
|
|
}
|
|
|
|
// Delete removes the named entity.
|
|
func (s *Store) Delete(ctx context.Context, name string) error {
|
|
if runtime.GOOS == "windows" {
|
|
name = filepath.FromSlash(name)
|
|
}
|
|
path := filepath.Join(s.path, filepath.Clean(name))
|
|
debug.V(3).Log("Deleting %s from %s", name, path)
|
|
|
|
if err := os.Remove(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.removeEmptyParentDirectories(path)
|
|
}
|
|
|
|
// Deletes all empty parent directories up to the store root.
|
|
func (s *Store) removeEmptyParentDirectories(path string) error {
|
|
if runtime.GOOS == "windows" {
|
|
path = filepath.FromSlash(path)
|
|
}
|
|
parent := filepath.Dir(path)
|
|
|
|
if relpath, err := filepath.Rel(s.path, parent); err != nil {
|
|
return err
|
|
} else if strings.HasPrefix(relpath, ".") {
|
|
return nil
|
|
}
|
|
|
|
debug.V(1).Log("removing empty parent dir: %q", parent)
|
|
err := os.Remove(parent)
|
|
switch {
|
|
case err == nil:
|
|
return s.removeEmptyParentDirectories(parent)
|
|
case notEmptyErr(err):
|
|
// ignore when directory is non-empty.
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Exists checks if the named entity exists.
|
|
func (s *Store) Exists(ctx context.Context, name string) bool {
|
|
if runtime.GOOS == "windows" {
|
|
name = filepath.FromSlash(name)
|
|
}
|
|
path := filepath.Join(s.path, filepath.Clean(name))
|
|
found := fsutil.IsFile(path)
|
|
debug.V(2).Log("Checking if '%s' exists at %s: %t", name, path, found)
|
|
|
|
return found
|
|
}
|
|
|
|
// List returns a list of all entities
|
|
// e.g. foo, far/bar baz/.bang
|
|
// directory separator are normalized using `/`.
|
|
func (s *Store) List(ctx context.Context, prefix string) ([]string, error) {
|
|
prefix = strings.TrimPrefix(prefix, "/")
|
|
debug.V(2).Log("Listing %s/%s", s.path, prefix)
|
|
|
|
files := make([]string, 0, 100)
|
|
if err := walkSymlinks(s.path, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
relPath := strings.TrimPrefix(path, s.path+string(filepath.Separator)) + string(filepath.Separator)
|
|
if runtime.GOOS == "windows" {
|
|
relPath = filepath.ToSlash(relPath)
|
|
}
|
|
if info.IsDir() && strings.HasPrefix(info.Name(), ".") && path != s.path && !strings.HasPrefix(prefix, relPath) && filepath.Base(path) != filepath.Base(prefix) {
|
|
debug.V(3).Log("skipping dot dir (relPath: %s, prefix: %s)", relPath, prefix)
|
|
|
|
return filepath.SkipDir
|
|
}
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
if path == s.path {
|
|
return nil
|
|
}
|
|
name := strings.TrimPrefix(path, s.path+string(filepath.Separator))
|
|
if runtime.GOOS == "windows" {
|
|
name = filepath.ToSlash(name)
|
|
}
|
|
if !strings.HasPrefix(name, prefix) {
|
|
return nil
|
|
}
|
|
|
|
files = append(files, name)
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
sort.Strings(files)
|
|
|
|
return files, nil
|
|
}
|
|
|
|
// IsDir returns true if the named entity is a directory.
|
|
func (s *Store) IsDir(ctx context.Context, name string) bool {
|
|
if runtime.GOOS == "windows" {
|
|
name = filepath.FromSlash(name)
|
|
}
|
|
path := filepath.Join(s.path, filepath.Clean(name))
|
|
isDir := fsutil.IsDir(path)
|
|
debug.V(2).Log("%s at %s is a directory? %t", name, path, isDir)
|
|
|
|
return isDir
|
|
}
|
|
|
|
// Prune removes a named directory.
|
|
func (s *Store) Prune(ctx context.Context, prefix string) error {
|
|
path := filepath.Join(s.path, filepath.Clean(prefix))
|
|
debug.Log("Purning %s from %s", prefix, path)
|
|
|
|
if err := os.RemoveAll(path); err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.removeEmptyParentDirectories(path)
|
|
}
|
|
|
|
// Name returns the name of this backend.
|
|
func (s *Store) Name() string {
|
|
return "fs"
|
|
}
|
|
|
|
// Version returns the version of this backend.
|
|
func (s *Store) Version(context.Context) semver.Version {
|
|
return debug.ModuleVersion("github.com/gopasspw/gopass/internal/backend/storage/fs")
|
|
}
|
|
|
|
// String implements fmt.Stringer.
|
|
func (s *Store) String() string {
|
|
return fmt.Sprintf("fs(%s,path:%s)", s.Version(context.TODO()).String(), s.path)
|
|
}
|
|
|
|
// Path returns the path to this storage.
|
|
func (s *Store) Path() string {
|
|
return s.path
|
|
}
|