The sftpfs package provides bidirectional SFTP support for the absfs ecosystem:
- Client Mode: Access remote SFTP servers as an
absfs.FileSystem - Server Mode: Serve any
absfs.FileSystemover SFTP protocol
- Bidirectional: Both client and server implementations
- Secure file operations: All operations encrypted over SSH
- Multiple authentication methods: Password and SSH key authentication
- Standard interface: Client implements
absfs.Filerfor seamless integration - Full file operations: Read, write, seek, truncate, and more
- Directory operations: Create, remove, and list directories
- Server mode: Expose any absfs filesystem via SFTP
go get github.com/absfs/sftpfspackage main
import (
"log"
"os"
"github.com/absfs/sftpfs"
)
func main() {
// Connect using password
fs, err := sftpfs.Dial("example.com:22", "username", "password")
if err != nil {
log.Fatal(err)
}
defer fs.Close()
// Use like any other filesystem
f, _ := fs.OpenFile("/remote/path/file.txt", os.O_RDONLY, 0)
defer f.Close()
// Read, write, etc.
}package main
import (
"log"
"os"
"github.com/absfs/sftpfs"
)
func main() {
// Read private key
key, err := os.ReadFile("/home/user/.ssh/id_rsa")
if err != nil {
log.Fatal(err)
}
// Connect using SSH key
fs, err := sftpfs.DialWithKey("example.com:22", "username", key)
if err != nil {
log.Fatal(err)
}
defer fs.Close()
// Use filesystem operations
fs.Mkdir("/remote/newdir", 0755)
}package main
import (
"log"
"time"
"github.com/absfs/sftpfs"
)
func main() {
config := &sftpfs.Config{
Host: "example.com:22",
User: "username",
Password: "password",
Timeout: 60 * time.Second,
}
fs, err := sftpfs.New(config)
if err != nil {
log.Fatal(err)
}
defer fs.Close()
// Use filesystem
}The server mode allows you to expose any absfs.FileSystem over SFTP protocol. This is useful for creating custom file servers, testing, or bridging different storage backends.
package main
import (
"crypto/rand"
"crypto/rsa"
"log"
"net"
"github.com/absfs/memfs"
"github.com/absfs/sftpfs"
"golang.org/x/crypto/ssh"
)
func main() {
// Create a filesystem to serve (could be any absfs.FileSystem)
fs, err := memfs.NewFS()
if err != nil {
log.Fatal(err)
}
// Generate a host key (in production, load from file)
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.Fatal(err)
}
signer, err := ssh.NewSignerFromKey(privateKey)
if err != nil {
log.Fatal(err)
}
// Create SFTP server with password authentication
server := sftpfs.NewServer(fs, &sftpfs.ServerConfig{
HostKeys: []ssh.Signer{signer},
PasswordCallback: sftpfs.SimplePasswordAuth("admin", "secret"),
})
// Listen and serve
listener, err := net.Listen("tcp", ":2222")
if err != nil {
log.Fatal(err)
}
log.Println("SFTP server listening on :2222")
log.Fatal(server.Serve(listener))
}users := map[string]string{
"alice": "password1",
"bob": "password2",
"carol": "password3",
}
server := sftpfs.NewServer(fs, &sftpfs.ServerConfig{
HostKeys: []ssh.Signer{signer},
PasswordCallback: sftpfs.MultiUserPasswordAuth(users),
})server := sftpfs.NewServer(fs, &sftpfs.ServerConfig{
HostKeys: []ssh.Signer{hostKey},
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
// Verify the public key against your authorized keys
authorizedKey, _, _, _, err := ssh.ParseAuthorizedKey(authorizedKeysData)
if err != nil {
return nil, err
}
if ssh.KeysEqual(key, authorizedKey) {
return nil, nil // Authentication successful
}
return nil, fmt.Errorf("unknown public key")
},
})// Serve local files
osfs, _ := osfs.NewFS()
server := sftpfs.NewServer(osfs, config)
// Serve in-memory files
memfs, _ := memfs.NewFS()
server := sftpfs.NewServer(memfs, config)
// Serve composed filesystems
union := unionfs.New(baseFS, overlayFS)
server := sftpfs.NewServer(union, config)Unit tests use mock interfaces and do not require an SFTP server:
go test -v ./...Integration tests require a running SFTP server. The project includes Docker Compose configuration for easy setup:
# Start the SFTP server
docker-compose up -d
# Run integration tests
go test -v -tags=integration ./...
# Stop the server
docker-compose downThe Docker setup uses atmoz/sftp with the following credentials:
- Host:
localhost:2222 - Username:
testuser - Password:
testpass
Run benchmarks to measure performance:
go test -bench=. -benchmem ./...Check test coverage:
go test -cover ./...The current implementation uses ssh.InsecureIgnoreHostKey() which skips host key verification. For production use, you should implement proper host key verification to prevent man-in-the-middle attacks.
Example of implementing host key verification:
// For production, implement proper host key callback
hostKeyCallback, err := knownhosts.New("/home/user/.ssh/known_hosts")
if err != nil {
log.Fatal(err)
}
// Use hostKeyCallback in your SSH configuration| Method | Description |
|---|---|
New(config *Config) |
Create a new SFTP filesystem with configuration |
Dial(host, user, password string) |
Quick connect with password auth |
DialWithKey(host, user string, privateKey []byte) |
Quick connect with key auth |
Close() |
Close the SFTP connection |
OpenFile(name string, flag int, perm os.FileMode) |
Open or create a file |
Mkdir(name string, perm os.FileMode) |
Create a directory |
Remove(name string) |
Remove a file or empty directory |
Rename(oldpath, newpath string) |
Rename a file |
Stat(name string) |
Get file information |
Chmod(name string, mode os.FileMode) |
Change file mode |
Chtimes(name string, atime, mtime time.Time) |
Change file times |
Chown(name string, uid, gid int) |
Change file ownership |
| Method | Description |
|---|---|
Name() |
Return the file name |
Read(b []byte) |
Read bytes from file |
ReadAt(b []byte, off int64) |
Read at specific offset |
Write(b []byte) |
Write bytes to file |
WriteAt(b []byte, off int64) |
Write at specific offset |
WriteString(s string) |
Write string to file |
Seek(offset int64, whence int) |
Seek within file |
Close() |
Close the file |
Stat() |
Get file information |
Sync() |
Sync file (no-op for SFTP) |
Truncate(size int64) |
Truncate file to size |
Readdir(n int) |
Read directory entries |
Readdirnames(n int) |
Read directory entry names |
| Method | Description |
|---|---|
NewServer(fs absfs.FileSystem, config *ServerConfig) |
Create a new SFTP server |
Serve(listener net.Listener) |
Accept connections and serve SFTP |
ServeConn(conn net.Conn) |
Handle a single connection |
SSHConfig() |
Get the underlying SSH server config |
| Field | Type | Description |
|---|---|---|
HostKeys |
[]ssh.Signer |
SSH host keys (at least one required) |
PasswordCallback |
func(ssh.ConnMetadata, []byte) (*ssh.Permissions, error) |
Password authentication handler |
PublicKeyCallback |
func(ssh.ConnMetadata, ssh.PublicKey) (*ssh.Permissions, error) |
Public key authentication handler |
NoClientAuth |
bool |
Allow connections without authentication (testing only) |
MaxAuthTries |
int |
Maximum authentication attempts (default: 6) |
ServerVersion |
string |
SSH server version string |
| Function | Description |
|---|---|
SimplePasswordAuth(user, pass string) |
Create single-user password callback |
MultiUserPasswordAuth(users map[string]string) |
Create multi-user password callback |
NewServerHandler(fs absfs.FileSystem) |
Create low-level SFTP handlers |
Check out the absfs repo for more information about the abstract filesystem interface and features like filesystem composition.
This project is governed by the MIT License. See LICENSE