Add notifications with tests #87
30
cmd.go
30
cmd.go
|
@ -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 {
|
||||
|
|
2
conn.go
2
conn.go
|
@ -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
|
||||
|
|
|
@ -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
113
notifier.go
Normal 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
175
notifier_test.go
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user