aboutsummaryrefslogtreecommitdiffstats
path: root/internal/wfs
diff options
context:
space:
mode:
Diffstat (limited to 'internal/wfs')
-rw-r--r--internal/wfs/osfs/osfs.go2
-rw-r--r--internal/wfs/sftpfs/sftpfs.go21
-rw-r--r--internal/wfs/wfs.go84
3 files changed, 86 insertions, 21 deletions
diff --git a/internal/wfs/osfs/osfs.go b/internal/wfs/osfs/osfs.go
index bf6951c..c879f85 100644
--- a/internal/wfs/osfs/osfs.go
+++ b/internal/wfs/osfs/osfs.go
@@ -1,3 +1,4 @@
+// Package osfs implements [wfs.FS] backed by the local filesystem.
package osfs
import (
@@ -15,6 +16,7 @@ var (
_ fs.StatFS = FS{}
)
+// An FS is a [wfs.FS] backed by the local filesystem.
type FS struct{}
func (FS) Open(name string) (fs.File, error) {
diff --git a/internal/wfs/sftpfs/sftpfs.go b/internal/wfs/sftpfs/sftpfs.go
index d7f0cdc..2ec9635 100644
--- a/internal/wfs/sftpfs/sftpfs.go
+++ b/internal/wfs/sftpfs/sftpfs.go
@@ -1,3 +1,4 @@
+// Package sftpfs implements [wfs.FS] using [github.com/pkg/sftp].
package sftpfs
import (
@@ -24,10 +25,11 @@ var (
_ wfs.FS = (*FS)(nil)
_ wfs.ReadLinkFS = (*FS)(nil)
_ fs.StatFS = (*FS)(nil)
- _ fs.StatFS = (*FS)(nil)
_ fs.ReadDirFS = (*FS)(nil)
)
+// An FS holds an SFTP connection and wraps its operations into the
+// [wfs.FS] interface.
type FS struct {
User, Host string
conn *sftp.Client
@@ -46,6 +48,14 @@ var sshAgent = sync.OnceValue(func() agent.ExtendedAgent {
return agent.NewClient(conn)
})
+// sshKeys returns the available ssh public keys. If an ssh agent can be
+// contacted with $SSH_AUTH_SOCK, sshKeys uses the keys from the agent if
+// possible. Otherwise sshKeys loads keys from ~/.ssh. If there are any password
+// protected keys, sshKeys may prompt the user for the password (although it
+// will do so at most once).
+//
+// If a password-protected key is loaded from ~/.ssh, it will be added to the
+// ssh agent if possible.
func sshKeys() ([]ssh.Signer, error) {
sshAgent := sshAgent()
if sshAgent != nil {
@@ -121,6 +131,7 @@ func appendToKnownHosts(hostname string, key ssh.PublicKey) error {
return f.Close()
}
+// Dial establishes a new SFTP connection to the given host.
func Dial(target string) (*FS, error) {
knownHostChecker, err := knownhosts.New(path.Join(os.Getenv("HOME"), ".ssh/known_hosts"))
if err != nil {
@@ -152,6 +163,9 @@ func Dial(target string) (*FS, error) {
if !errors.As(err, &keyErr) || len(keyErr.Want) > 0 {
return err
}
+ // scp prompts the user if the host is not found in
+ // known_hosts, but when is that ever useful? We'll just
+ // add it to known_hosts without bothering the user.
appendToKnownHosts(hostname, key)
return nil
},
@@ -172,6 +186,7 @@ func Dial(target string) (*FS, error) {
}, nil
}
+// Close closes the underlying SFTP connection.
func (f *FS) Close() error {
sftpErr := f.conn.Close()
if err := f.sshConn.Close(); err != nil {
@@ -181,9 +196,13 @@ func (f *FS) Close() error {
}
func (f *FS) err(op, path string, err error) error {
+ // github.com/pkg/sftp's errors are pretty terrible.
+ // We'll wrap them to be more similar to the amazing package os errors.
return fmt.Errorf("%s %q: %w", op, f.User+"@"+f.Host+":"+path, err)
}
+// wfs.FS implementation:
+
func (f *FS) Open(name string) (fs.File, error) {
file, err := f.conn.Open(name)
if err != nil {
diff --git a/internal/wfs/wfs.go b/internal/wfs/wfs.go
index bcdca00..4168b58 100644
--- a/internal/wfs/wfs.go
+++ b/internal/wfs/wfs.go
@@ -1,11 +1,14 @@
+// Package wfs implements a "writable file system" in the spirit of [fs.FS].
package wfs
import (
+ "errors"
"io"
"io/fs"
"path"
)
+// ReadLinkFS is backported from the latest go master.
type ReadLinkFS interface {
fs.FS
@@ -13,6 +16,9 @@ type ReadLinkFS interface {
Lstat(string) (fs.FileInfo, error)
}
+// ReadLink returns the destination of the named symbolic link.
+//
+// If fsys does not implement [ReadLinkFS], then ReadLink returns an error.
func ReadLink(fsys fs.FS, name string) (string, error) {
sym, ok := fsys.(ReadLinkFS)
if !ok {
@@ -21,6 +27,12 @@ func ReadLink(fsys fs.FS, name string) (string, error) {
return sym.ReadLink(name)
}
+// Lstat returns an [fs.FileInfo] describing the named file.
+// If the file is a symbolic link, the returned [fs.FileInfo] describes the
+// symbolic link. Lstat makes no attempt to follow the link.
+//
+// If fsys does not implement [ReadLinkFS], then Lstat is identical
+// to [fs.Stat].
func Lstat(fsys fs.FS, name string) (fs.FileInfo, error) {
sym, ok := fsys.(ReadLinkFS)
if !ok {
@@ -29,6 +41,7 @@ func Lstat(fsys fs.FS, name string) (fs.FileInfo, error) {
return sym.Lstat(name)
}
+// An FS provides access to a writable hierarchical file system.
type FS interface {
fs.FS
@@ -39,12 +52,16 @@ type FS interface {
Chmod(string, fs.FileMode) error
}
+// A MkdirModeFS is a file system with a mkdir method that accepts a file mode.
type MkdirModeFS interface {
FS
MkdirMode(string, fs.FileMode) error
}
+// MkdirMode creates a directory with the given file permission. If fsys
+// implements [MkdirModeFS], MkdirMode calls fsys.MkdirMode. Otherwise,
+// MkdirMode calls Mkdir and then Chmod to set the mode.
func MkdirMode(fsys FS, name string, mode fs.FileMode) error {
if fsys, ok := fsys.(MkdirModeFS); ok {
return fsys.MkdirMode(name, mode)
@@ -56,31 +73,58 @@ func MkdirMode(fsys FS, name string, mode fs.FileMode) error {
}
func removeDir(fsys FS, dir string) error {
- entries, err := fs.ReadDir(fsys, dir)
- if err != nil {
- return err
- }
- for _, f := range entries {
- name := path.Join(dir, f.Name())
- if f.IsDir() {
- err = removeDir(fsys, name)
- } else {
- err = fsys.Remove(name)
- }
- if err != nil {
- return err
+ entries, readErr := fs.ReadDir(fsys, dir)
+ var err error
+ for _, d := range entries {
+ if err1 := removeAll(fsys, path.Join(dir, d.Name()), d); err == nil {
+ err = err1
}
}
- return fsys.Remove(dir)
+ if err == nil {
+ err = readErr
+ }
+ err1 := fsys.Remove(dir)
+ if err1 == nil || errors.Is(err, fs.ErrNotExist) {
+ return nil
+ }
+ if err == nil {
+ err = err1
+ }
+ return err
}
-func RemoveAll(fsys FS, path string) error {
- stat, err := Lstat(fsys, path)
- if err != nil {
+func removeAll(fsys FS, path string, d fs.DirEntry) error {
+ err := fsys.Remove(path)
+ if err == nil || errors.Is(err, fs.ErrNotExist) {
+ return nil
+ }
+ if !d.IsDir() {
return err
}
- if stat.IsDir() {
- return removeDir(fsys, path)
+ return removeDir(fsys, path)
+}
+
+// RemoveAll removes path and any children it contains. It removes everything it
+// can but returns the first error it encounters. If the path does not exist,
+// RemoveAll returns nil (no error).
+func RemoveAll(fsys FS, path string) error {
+ // Simple case: if Remove works, we're done.
+ err := fsys.Remove(path)
+ if err == nil || errors.Is(err, fs.ErrNotExist) {
+ return nil
+ }
+
+ // Otherwise, is this a directory we need to recurse into?
+ dir, serr := Lstat(fsys, path)
+ if serr != nil {
+ if errors.Is(serr, fs.ErrNotExist) {
+ return nil
+ }
+ return serr
+ }
+ if !dir.IsDir() {
+ // Not a directory; return the error from Remove.
+ return err
}
- return fsys.Remove(path)
+ return removeDir(fsys, path)
}