Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ ftpserver
debug
__debug_bin*
*.toml
.opencode/
coverage.txt
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The test suite uses a **reference driver implementation** (`driver_test.go`) wit
## Key Dependencies

- `github.com/spf13/afero`: File system abstraction for driver implementations
- `github.com/fclairamb/go-log`: Logging abstraction supporting multiple frameworks
- `log/slog`: Go standard library structured logging (no external logging dependencies)

## Code Conventions

Expand Down
22 changes: 0 additions & 22 deletions OpenCode.md

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ If you're interested in a fully featured FTP server, you should use [sftpgo](htt
* Clean code: No sleep, no panic, no global sync (only around control/transfer connection per client)
* Uses only the standard library except for:
* [afero](https://github.com/spf13/afero) for generic file systems handling
* [fclairamb/go-log](https://github.com/fclairamb/go-log) for logging through your existing libraries [go-kit/log](https://github.com/go-kit/log), [log15](https://github.com/inconshreveable/log15), [zap](https://github.com/uber-go/zap), [zerolog](https://github.com/rs/zerolog/), [logrus](https://github.com/sirupsen/logrus)
* Uses standard library [log/slog](https://pkg.go.dev/log/slog) for structured logging
* Supported extensions:
* [AUTH](https://tools.ietf.org/html/rfc2228#page-6) - Control session protection
* [AUTH TLS](https://tools.ietf.org/html/rfc4217#section-4.1) - TLS session
Expand Down
5 changes: 2 additions & 3 deletions client_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import (
"errors"
"fmt"
"io"
"log/slog"
"net"
"strings"
"sync"
"time"

log "github.com/fclairamb/go-log"
)

// HASHAlgo is the enumerable that represents the supported HASH algorithms.
Expand Down Expand Up @@ -88,7 +87,7 @@ type clientHandler struct {
clnt string // Identified client
command string // Command received on the connection
ctxRnfr string // Rename from
logger log.Logger // Client handler logging
logger *slog.Logger // Client handler logging
transferWg sync.WaitGroup // wait group for command that open a transfer connection
transfer transferHandler // Transfer connection (passive or active)s
extra any // Additional application-specific data
Expand Down
8 changes: 8 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
coverage:
status:
project:
default:
target: 87%
patch:
default:
target: 70%
18 changes: 8 additions & 10 deletions driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ import (
"crypto/tls"
"errors"
"io"
"log/slog"
"net"
"os"
"strings"
"sync"
"testing"
"time"

log "github.com/fclairamb/go-log"
"github.com/fclairamb/go-log/gokit"
gklog "github.com/go-kit/log"
"github.com/spf13/afero"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -72,14 +70,14 @@ func NewTestServerWithTestDriver(t *testing.T, driver *TestServerDriver) *FtpSer
driver.Init()

// If we are in debug mode, we should log things
var logger log.Logger
var logger *slog.Logger
if driver.Debug {
logger = gokit.NewWrap(gklog.NewLogfmtLogger(gklog.NewSyncWriter(os.Stdout))).With(
"ts", gokit.GKDefaultTimestampUTC,
"caller", gokit.GKDefaultCaller,
)
logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}))
} else {
logger = nil
logger = slog.New(slog.NewTextHandler(io.Discard, nil)) //nolint:sloglint // DiscardHandler requires Go 1.23+
}

s := NewTestServerWithDriverAndLogger(t, driver, logger)
Expand All @@ -88,7 +86,7 @@ func NewTestServerWithTestDriver(t *testing.T, driver *TestServerDriver) *FtpSer
}

// NewTestServerWithTestDriver provides a server instantiated with some settings
func NewTestServerWithDriverAndLogger(t *testing.T, driver MainDriver, logger log.Logger) *FtpServer {
func NewTestServerWithDriverAndLogger(t *testing.T, driver MainDriver, logger *slog.Logger) *FtpServer {
t.Helper()

server := NewFtpServer(driver)
Expand Down
3 changes: 0 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ go 1.24.0
toolchain go1.25.5

require (
github.com/fclairamb/go-log v0.6.0
github.com/go-kit/log v0.2.1
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.11.1
Expand All @@ -15,7 +13,6 @@ require (

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
30 changes: 0 additions & 30 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/drakkan/goftp v0.0.0-20201220151643-27b7174e8caf h1:hb1QxC7CuOP25cKVNL5vVU+22w1m1A2ia76o4kt4n60=
github.com/drakkan/goftp v0.0.0-20201220151643-27b7174e8caf/go.mod h1:K3yqfa64LwJzUpdUWC6b524HO7U7DmBnrJuBjxKSZOQ=
github.com/fclairamb/go-log v0.5.0 h1:Gz9wSamEaA6lta4IU2cjJc2xSq5sV5VYSB5w/SUHhVc=
github.com/fclairamb/go-log v0.5.0/go.mod h1:XoRO1dYezpsGmLLkZE9I+sHqpqY65p8JA+Vqblb7k40=
github.com/fclairamb/go-log v0.6.0 h1:1V7BJ75P2PvanLHRyGBBFjncB6d4AgEmu+BPWKbMkaU=
github.com/fclairamb/go-log v0.6.0/go.mod h1:cyXxOw4aJwO6lrZb8GRELSw+sxO6wwkLJdsjY5xYCWA=
github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
5 changes: 1 addition & 4 deletions handle_misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,7 @@ func (c *clientHandler) handleABOR(param string) error {
c.isTransferAborted = true

if err := c.closeTransfer(); err != nil {
c.logger.Warn(
"Problem aborting transfer for command", param,
"err", err,
)
c.logger.Warn("Problem aborting transfer for command", "command", param, "err", err)
}

if c.debug {
Expand Down
5 changes: 3 additions & 2 deletions no_ports_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package ftpserver

import (
"io"
"log/slog"
"net"
"testing"

lognoop "github.com/fclairamb/go-log/noop"
"github.com/stretchr/testify/require"
)

Expand All @@ -27,7 +28,7 @@ func TestPortRangeFetchNextFailure(t *testing.T) {
handler := &clientHandler{
server: server,
driver: driver,
logger: lognoop.NewNoOpLogger(),
logger: slog.New(slog.NewTextHandler(io.Discard, nil)), //nolint:sloglint // DiscardHandler requires Go 1.23+
}

// Set the mock port mapping
Expand Down
12 changes: 6 additions & 6 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import (
"crypto/tls"
"errors"
"fmt"
"io"
"log/slog"
"net"
"syscall"
"time"

log "github.com/fclairamb/go-log"
lognoop "github.com/fclairamb/go-log/noop"
)

// ErrNotListening is returned when we are performing an action that is only valid while listening
Expand Down Expand Up @@ -127,7 +126,7 @@ var specialAttentionCommands = []string{"ABOR", "STAT", "QUIT"} //nolint:gocheck
// FtpServer is where everything is stored
// We want to keep it as simple as possible
type FtpServer struct {
Logger log.Logger // fclairamb/go-log generic logger
Logger *slog.Logger // Structured logger (log/slog)
settings *Settings // General settings
listener net.Listener // listener used to receive files
clientCounter uint32 // Clients counter
Expand Down Expand Up @@ -296,7 +295,8 @@ func (server *FtpServer) handleAcceptError(err error, tempDelay *time.Duration)
}

server.Logger.Warn(
"accept error", err,
"accept error",
"err", err,
"retry delay", tempDelay)
time.Sleep(*tempDelay)

Expand All @@ -323,7 +323,7 @@ func (server *FtpServer) ListenAndServe() error {
func NewFtpServer(driver MainDriver) *FtpServer {
return &FtpServer{
driver: driver,
Logger: lognoop.NewNoOpLogger(),
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)), //nolint:sloglint // DiscardHandler requires Go 1.23+
}
}

Expand Down
51 changes: 30 additions & 21 deletions server_stop_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package ftpserver

import (
"context"
"log/slog"
"sync"
"testing"
"time"

log "github.com/fclairamb/go-log"
"github.com/stretchr/testify/require"
)

Expand All @@ -22,8 +23,8 @@ func TestServerStopDoesNotLogError(t *testing.T) {
})

// Use a custom logger that tracks error logs
mockLogger := &MockLogger{}
server.Logger = mockLogger
mockHandler := &MockLogHandler{}
server.Logger = slog.New(mockHandler)

// Start listening
err := server.Listen()
Expand Down Expand Up @@ -54,37 +55,45 @@ func TestServerStopDoesNotLogError(t *testing.T) {

// Check that no error was logged for the "use of closed network connection"
// The mock logger should not have received any error logs
req.Empty(mockLogger.ErrorLogs, "Expected no error logs when stopping server, but got: %v", mockLogger.ErrorLogs)
req.Empty(mockHandler.ErrorLogs, "Expected no error logs when stopping server, but got: %v", mockHandler.ErrorLogs)
}

// MockLogger captures log calls to verify behavior
type MockLogger struct {
// MockLogHandler captures log calls to verify behavior
type MockLogHandler struct {
ErrorLogs []string
WarnLogs []string
InfoLogs []string
DebugLogs []string
mu sync.Mutex
}

func (m *MockLogger) Debug(message string, _ ...any) {
m.DebugLogs = append(m.DebugLogs, message)
func (m *MockLogHandler) Enabled(_ context.Context, _ slog.Level) bool {
return true
}

func (m *MockLogger) Info(message string, _ ...any) {
m.InfoLogs = append(m.InfoLogs, message)
//nolint:gocritic // slog.Handler interface requires value receiver
func (m *MockLogHandler) Handle(_ context.Context, record slog.Record) error {
m.mu.Lock()
defer m.mu.Unlock()

switch record.Level {
case slog.LevelDebug:
m.DebugLogs = append(m.DebugLogs, record.Message)
case slog.LevelInfo:
m.InfoLogs = append(m.InfoLogs, record.Message)
case slog.LevelWarn:
m.WarnLogs = append(m.WarnLogs, record.Message)
case slog.LevelError:
m.ErrorLogs = append(m.ErrorLogs, record.Message)
}

return nil
}

func (m *MockLogger) Warn(message string, _ ...any) {
m.WarnLogs = append(m.WarnLogs, message)
}

func (m *MockLogger) Error(message string, _ ...any) {
m.ErrorLogs = append(m.ErrorLogs, message)
}

func (m *MockLogger) Panic(message string, _ ...any) {
panic(message)
func (m *MockLogHandler) WithAttrs(_ []slog.Attr) slog.Handler {
return m
}

func (m *MockLogger) With(_ ...any) log.Logger {
func (m *MockLogHandler) WithGroup(_ string) slog.Handler {
return m
}
Loading
Loading