Add notifications with tests #87

Merged
lunny merged 5 commits from lunny/add_notifier into master 2020-02-19 06:20:26 +00:00
7 changed files with 327 additions and 10 deletions

30
cmd.go
View File

@ -113,13 +113,16 @@ func (cmd commandAppe) Execute(conn *Conn, param string) {
targetPath := conn.buildPath(param)
conn.writeMessage(150, "Data transfer starting")
bytes, err := conn.driver.PutFile(targetPath, conn.dataConn, true)
conn.server.notifiers.BeforePutFile(conn, targetPath)
size, err := conn.driver.PutFile(targetPath, conn.dataConn, true)
conn.server.notifiers.AfterFilePut(conn, targetPath, size, err)
if err == nil {
msg := "OK, received " + strconv.Itoa(int(bytes)) + " bytes"
msg := fmt.Sprintf("OK, received %d bytes", size)
conn.writeMessage(226, msg)
} else {
conn.writeMessage(450, fmt.Sprint("error during transfer: ", err))
}
}
type commandOpts struct{}
@ -235,7 +238,9 @@ func (cmd commandCwd) Execute(conn *Conn, param string) {
return
}
conn.server.notifiers.BeforeChangeCurDir(conn, conn.curDir, path)
err = conn.changeCurDir(path)
conn.server.notifiers.AfterCurDirChanged(conn, conn.curDir, path, err)
if err == nil {
conn.writeMessage(250, "Directory changed to "+path)
} else {
@ -261,7 +266,9 @@ func (cmd commandDele) RequireAuth() bool {
func (cmd commandDele) Execute(conn *Conn, param string) {
path := conn.buildPath(param)
conn.server.notifiers.BeforeDeleteFile(conn, path)
err := conn.driver.DeleteFile(path)
conn.server.notifiers.AfterFileDeleted(conn, path, err)
if err == nil {
conn.writeMessage(250, "File deleted")
} else {
@ -546,7 +553,9 @@ func (cmd commandMkd) RequireAuth() bool {
func (cmd commandMkd) Execute(conn *Conn, param string) {
path := conn.buildPath(param)
conn.server.notifiers.BeforeCreateDir(conn, path)
err := conn.driver.MakeDir(path)
conn.server.notifiers.AfterDirCreated(conn, path, err)
if err == nil {
conn.writeMessage(257, "Directory created")
} else {
@ -622,6 +631,7 @@ func (cmd commandPass) RequireAuth() bool {
func (cmd commandPass) Execute(conn *Conn, param string) {
ok, err := conn.server.Auth.CheckPasswd(conn.reqUser, param)
conn.server.notifiers.AfterUserLogin(conn, conn.reqUser, param, ok, err)
if err != nil {
conn.writeMessage(550, "Checking password error")
return
@ -774,15 +784,18 @@ func (cmd commandRetr) Execute(conn *Conn, param string) {
conn.lastFilePos = 0
conn.appendData = false
}()
bytes, data, err := conn.driver.GetFile(path, conn.lastFilePos)
conn.server.notifiers.BeforeDownloadFile(conn, path)
size, data, err := conn.driver.GetFile(path, conn.lastFilePos)
if err == nil {
defer data.Close()
conn.writeMessage(150, fmt.Sprintf("Data transfer starting %v bytes", bytes))
conn.writeMessage(150, fmt.Sprintf("Data transfer starting %d bytes", size))
err = conn.sendOutofBandDataWriter(data)
conn.server.notifiers.AfterFileDownloaded(conn, path, size, err)
if err != nil {
conn.writeMessage(551, "Error reading file")
}
} else {
conn.server.notifiers.AfterFileDownloaded(conn, path, size, err)
conn.writeMessage(551, "File not available")
}
}
@ -883,7 +896,9 @@ func (cmd commandRmd) RequireAuth() bool {
func (cmd commandRmd) Execute(conn *Conn, param string) {
path := conn.buildPath(param)
conn.server.notifiers.BeforeDeleteDir(conn, path)
err := conn.driver.DeleteDir(path)
conn.server.notifiers.AfterDirDeleted(conn, path, err)
if err == nil {
conn.writeMessage(250, "Directory deleted")
} else {
@ -1104,9 +1119,11 @@ func (cmd commandStor) Execute(conn *Conn, param string) {
conn.appendData = false
}()
bytes, err := conn.driver.PutFile(targetPath, conn.dataConn, conn.appendData)
conn.server.notifiers.BeforePutFile(conn, targetPath)
size, err := conn.driver.PutFile(targetPath, conn.dataConn, conn.appendData)
conn.server.notifiers.AfterFilePut(conn, targetPath, size, err)
if err == nil {
msg := "OK, received " + strconv.Itoa(int(bytes)) + " bytes"
msg := fmt.Sprintf("OK, received %d bytes", size)
conn.writeMessage(226, msg)
} else {
conn.writeMessage(450, fmt.Sprint("error during transfer: ", err))
@ -1214,6 +1231,7 @@ func (cmd commandUser) RequireAuth() bool {
func (cmd commandUser) Execute(conn *Conn, param string) {
conn.reqUser = param
conn.server.notifiers.BeforeLoginUser(conn, conn.reqUser)
if conn.tls || conn.tlsConfig == nil {
conn.writeMessage(331, "User name ok, password required")
} else {

View File

@ -142,6 +142,8 @@ func (conn *Conn) Serve() {
func (conn *Conn) Close() {
conn.conn.Close()
conn.closed = true
conn.reqUser = ""
conn.user = ""
if conn.dataConn != nil {
conn.dataConn.Close()
conn.dataConn = nil

View File

@ -39,7 +39,7 @@ func TestMinioDriver(t *testing.T) {
Logger: new(DiscardLogger),
}
runServer(t, opt, func() {
runServer(t, opt, nil, func() {
// Give server 0.5 seconds to get to the listening state
timeout := time.NewTimer(time.Millisecond * 500)
for {

113
notifier.go Normal file
View File

@ -0,0 +1,113 @@
// Copyright 2020 The goftp Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package server
// Notifier represents a notification operator interface
type Notifier interface {
BeforeLoginUser(conn *Conn, userName string)
BeforePutFile(conn *Conn, dstPath string)
BeforeDeleteFile(conn *Conn, dstPath string)
BeforeChangeCurDir(conn *Conn, oldCurDir, newCurDir string)
BeforeCreateDir(conn *Conn, dstPath string)
BeforeDeleteDir(conn *Conn, dstPath string)
BeforeDownloadFile(conn *Conn, dstPath string)
AfterUserLogin(conn *Conn, userName, password string, passMatched bool, err error)
AfterFilePut(conn *Conn, dstPath string, size int64, err error)
AfterFileDeleted(conn *Conn, dstPath string, err error)
AfterFileDownloaded(conn *Conn, dstPath string, size int64, err error)
AfterCurDirChanged(conn *Conn, oldCurDir, newCurDir string, err error)
AfterDirCreated(conn *Conn, dstPath string, err error)
AfterDirDeleted(conn *Conn, dstPath string, err error)
}
type notifierList []Notifier
var (
_ Notifier = notifierList{}
)
func (notifiers notifierList) BeforeLoginUser(conn *Conn, userName string) {
for _, notifier := range notifiers {
notifier.BeforeLoginUser(conn, userName)
}
}
func (notifiers notifierList) BeforePutFile(conn *Conn, dstPath string) {
for _, notifier := range notifiers {
notifier.BeforePutFile(conn, dstPath)
}
}
func (notifiers notifierList) BeforeDeleteFile(conn *Conn, dstPath string) {
for _, notifier := range notifiers {
notifier.BeforeDeleteFile(conn, dstPath)
}
}
func (notifiers notifierList) BeforeChangeCurDir(conn *Conn, oldCurDir, newCurDir string) {
for _, notifier := range notifiers {
notifier.BeforeChangeCurDir(conn, oldCurDir, newCurDir)
}
}
func (notifiers notifierList) BeforeCreateDir(conn *Conn, dstPath string) {
for _, notifier := range notifiers {
notifier.BeforeCreateDir(conn, dstPath)
}
}
func (notifiers notifierList) BeforeDeleteDir(conn *Conn, dstPath string) {
for _, notifier := range notifiers {
notifier.BeforeDeleteDir(conn, dstPath)
}
}
func (notifiers notifierList) BeforeDownloadFile(conn *Conn, dstPath string) {
for _, notifier := range notifiers {
notifier.BeforeDownloadFile(conn, dstPath)
}
}
func (notifiers notifierList) AfterUserLogin(conn *Conn, userName, password string, passMatched bool, err error) {
for _, notifier := range notifiers {
notifier.AfterUserLogin(conn, userName, password, passMatched, err)
}
}
func (notifiers notifierList) AfterFilePut(conn *Conn, dstPath string, size int64, err error) {
for _, notifier := range notifiers {
notifier.AfterFilePut(conn, dstPath, size, err)
}
}
func (notifiers notifierList) AfterFileDeleted(conn *Conn, dstPath string, err error) {
for _, notifier := range notifiers {
notifier.AfterFileDeleted(conn, dstPath, err)
}
}
func (notifiers notifierList) AfterFileDownloaded(conn *Conn, dstPath string, size int64, err error) {
for _, notifier := range notifiers {
notifier.AfterFileDownloaded(conn, dstPath, size, err)
}
}
func (notifiers notifierList) AfterCurDirChanged(conn *Conn, oldCurDir, newCurDir string, err error) {
for _, notifier := range notifiers {
notifier.AfterCurDirChanged(conn, oldCurDir, newCurDir, err)
}
}
func (notifiers notifierList) AfterDirCreated(conn *Conn, dstPath string, err error) {
for _, notifier := range notifiers {
notifier.AfterDirCreated(conn, dstPath, err)
}
}
func (notifiers notifierList) AfterDirDeleted(conn *Conn, dstPath string, err error) {
for _, notifier := range notifiers {
notifier.AfterDirDeleted(conn, dstPath, err)
}
}

175
notifier_test.go Normal file
View File

@ -0,0 +1,175 @@
// Copyright 2020 The goftp Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package server
import (
"io/ioutil"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/jlaffaye/ftp"
"github.com/stretchr/testify/assert"
)
type mockNotifier struct {
actions []string
lock sync.Mutex
}
func (m *mockNotifier) BeforeLoginUser(conn *Conn, userName string) {
m.lock.Lock()
m.actions = append(m.actions, "BeforeLoginUser")
m.lock.Unlock()
}
func (m *mockNotifier) BeforePutFile(conn *Conn, dstPath string) {
m.lock.Lock()
m.actions = append(m.actions, "BeforePutFile")
m.lock.Unlock()
}
func (m *mockNotifier) BeforeDeleteFile(conn *Conn, dstPath string) {
m.lock.Lock()
m.actions = append(m.actions, "BeforeDeleteFile")
m.lock.Unlock()
}
func (m *mockNotifier) BeforeChangeCurDir(conn *Conn, oldCurDir, newCurDir string) {
m.lock.Lock()
m.actions = append(m.actions, "BeforeChangeCurDir")
m.lock.Unlock()
}
func (m *mockNotifier) BeforeCreateDir(conn *Conn, dstPath string) {
m.lock.Lock()
m.actions = append(m.actions, "BeforeCreateDir")
m.lock.Unlock()
}
func (m *mockNotifier) BeforeDeleteDir(conn *Conn, dstPath string) {
m.lock.Lock()
m.actions = append(m.actions, "BeforeDeleteDir")
m.lock.Unlock()
}
func (m *mockNotifier) BeforeDownloadFile(conn *Conn, dstPath string) {
m.lock.Lock()
m.actions = append(m.actions, "BeforeDownloadFile")
m.lock.Unlock()
}
func (m *mockNotifier) AfterUserLogin(conn *Conn, userName, password string, passMatched bool, err error) {
m.lock.Lock()
m.actions = append(m.actions, "AfterUserLogin")
m.lock.Unlock()
}
func (m *mockNotifier) AfterFilePut(conn *Conn, dstPath string, size int64, err error) {
m.lock.Lock()
m.actions = append(m.actions, "AfterFilePut")
m.lock.Unlock()
}
func (m *mockNotifier) AfterFileDeleted(conn *Conn, dstPath string, err error) {
m.lock.Lock()
m.actions = append(m.actions, "AfterFileDeleted")
m.lock.Unlock()
}
func (m *mockNotifier) AfterCurDirChanged(conn *Conn, oldCurDir, newCurDir string, err error) {
m.lock.Lock()
m.actions = append(m.actions, "AfterCurDirChanged")
m.lock.Unlock()
}
func (m *mockNotifier) AfterDirCreated(conn *Conn, dstPath string, err error) {
m.lock.Lock()
m.actions = append(m.actions, "AfterDirCreated")
m.lock.Unlock()
}
func (m *mockNotifier) AfterDirDeleted(conn *Conn, dstPath string, err error) {
m.lock.Lock()
m.actions = append(m.actions, "AfterDirDeleted")
m.lock.Unlock()
}
func (m *mockNotifier) AfterFileDownloaded(conn *Conn, dstPath string, size int64, err error) {
m.lock.Lock()
m.actions = append(m.actions, "AfterFileDownloaded")
m.lock.Unlock()
}
func assetMockNotifier(t *testing.T, mock *mockNotifier, lastActions []string) {
if len(lastActions) == 0 {
return
}
mock.lock.Lock()
assert.EqualValues(t, lastActions, mock.actions[len(mock.actions)-len(lastActions):])
mock.lock.Unlock()
}
func TestNotification(t *testing.T) {
os.MkdirAll("./testdata", os.ModePerm)
var perm = NewSimplePerm("test", "test")
opt := &ServerOpts{
Name: "test ftpd",
Factory: &FileDriverFactory{
RootPath: "./testdata",
Perm: perm,
},
Port: 2121,
Auth: &SimpleAuth{
Name: "admin",
Password: "admin",
},
Logger: new(DiscardLogger),
}
mock := &mockNotifier{}
runServer(t, opt, notifierList{mock}, func() {
// Give server 0.5 seconds to get to the listening state
timeout := time.NewTimer(time.Millisecond * 500)
for {
f, err := ftp.Connect("localhost:2121")
if err != nil && len(timeout.C) == 0 { // Retry errors
continue
}
assert.NoError(t, err)
assert.NoError(t, f.Login("admin", "admin"))
assetMockNotifier(t, mock, []string{"BeforeLoginUser", "AfterUserLogin"})
assert.Error(t, f.Login("admin", "1111"))
assetMockNotifier(t, mock, []string{"BeforeLoginUser", "AfterUserLogin"})
var content = `test`
assert.NoError(t, f.Stor("server_test.go", strings.NewReader(content)))
assetMockNotifier(t, mock, []string{"BeforePutFile", "AfterFilePut"})
r, err := f.RetrFrom("/server_test.go", 2)
assert.NoError(t, err)
buf, err := ioutil.ReadAll(r)
r.Close()
assert.NoError(t, err)
assert.EqualValues(t, "st", string(buf))
assetMockNotifier(t, mock, []string{"BeforeDownloadFile", "AfterFileDownloaded"})
err = f.Rename("/server_test.go", "/test.go")
assert.NoError(t, err)
err = f.MakeDir("/src")
assetMockNotifier(t, mock, []string{"BeforeCreateDir", "AfterDirCreated"})
err = f.Delete("/test.go")
assetMockNotifier(t, mock, []string{"BeforeDeleteFile", "AfterFileDeleted"})
err = f.ChangeDir("/src")
assetMockNotifier(t, mock, []string{"BeforeChangeCurDir", "AfterCurDirChanged"})
err = f.RemoveDir("/src")
assetMockNotifier(t, mock, []string{"BeforeDeleteDir", "AfterDirDeleted"})
err = f.Quit()
assert.NoError(t, err)
break
}
})
}

View File

@ -75,6 +75,7 @@ type Server struct {
ctx context.Context
cancel context.CancelFunc
feats string
notifiers notifierList
}
// ErrServerClosed is returned by ListenAndServe() or Serve() when a shutdown
@ -157,6 +158,11 @@ func NewServer(opts *ServerOpts) *Server {
return s
}
// RegisterNotifer registers a notifier
func (server *Server) RegisterNotifer(notifier Notifier) {
server.notifiers = append(server.notifiers, notifier)
}
// NewConn constructs a new object that will handle the FTP protocol over
// an active net.TCPConn. The TCP connection should already be open before
// it is handed to this functions. driver is an instance of FTPDriver that

View File

@ -16,8 +16,11 @@ import (
"github.com/stretchr/testify/assert"
)
func runServer(t *testing.T, opt *ServerOpts, execute func()) {
func runServer(t *testing.T, opt *ServerOpts, notifiers notifierList, execute func()) {
s := NewServer(opt)
for _, notifier := range notifiers {
s.RegisterNotifer(notifier)
}
go func() {
err := s.ListenAndServe()
assert.EqualError(t, err, ErrServerClosed.Error())
@ -43,10 +46,10 @@ func TestFileDriver(t *testing.T) {
Name: "admin",
Password: "admin",
},
//Logger: new(DiscardLogger),
Logger: new(DiscardLogger),
}
runServer(t, opt, func() {
runServer(t, opt, nil, func() {
// Give server 0.5 seconds to get to the listening state
timeout := time.NewTimer(time.Millisecond * 500)