Skip to content

Commit

Permalink
cmd/serve: add ftp server - implement rclone#2151
Browse files Browse the repository at this point in the history
  • Loading branch information
sapk committed Apr 21, 2018
1 parent 870c58f commit 517b772
Show file tree
Hide file tree
Showing 5 changed files with 497 additions and 0 deletions.
357 changes: 357 additions & 0 deletions cmd/serve/ftp/ftp.go
@@ -0,0 +1,357 @@
package ftp

import (
"errors"
"fmt"
"io"
"os"
"os/user"
"strconv"
"strings"

ftp "github.com/goftp/server"
"github.com/ncw/rclone/cmd"
"github.com/ncw/rclone/cmd/serve/ftp/ftpflags"
"github.com/ncw/rclone/cmd/serve/ftp/ftpopt"
"github.com/ncw/rclone/fs"
"github.com/ncw/rclone/fs/log"
"github.com/ncw/rclone/vfs"
"github.com/ncw/rclone/vfs/vfsflags"
"github.com/spf13/cobra"
)

func init() {
ftpflags.AddFlags(Command.Flags())
vfsflags.AddFlags(Command.Flags())
}

// Command definition for cobra
var Command = &cobra.Command{
Use: "ftp remote:path",
Short: `Serve remote:path over ftp.`,
Long: `
rclone serve ftp implements a basic ftp server to serve the
remote over FTP protocol. This can be viewed with a ftp client
or you can make a remote of type ftp to read and write it.
` + ftpopt.Help + vfs.Help,
Run: func(command *cobra.Command, args []string) {
cmd.CheckArgs(1, 1, command, args)
f := cmd.NewFsSrc(args)
cmd.Run(false, false, command, func() error {
s := newServer(f, &ftpflags.Opt)
return s.serve()
})
},
}

// server contains everything to run the server
type server struct {
f fs.Fs
srv *ftp.Server
}

// Make a new WebDAV to serve the remote
func newServer(f fs.Fs, opt *ftpopt.Options) *server {
hostport := strings.Split(opt.ListenAddr, ":") //TODO better
port, _ := strconv.Atoi(hostport[1]) //TODO better
ftpopt := &ftp.ServerOpts{
Name: "Rclone FTP Server",
Factory: &FTPDriverFactory{
vfs: vfs.New(f, &vfsflags.Opt),
},
Hostname: hostport[0],
Port: port,
Auth: &FTPAuth{
BasicUser: opt.BasicUser,
BasicPass: opt.BasicPass,
},
Logger: &FTPLogger{},
//TODO implement a maximum of https://godoc.org/github.com/goftp/server#ServerOpts
}
return &server{
f: f,
srv: ftp.NewServer(ftpopt),
}
}

// serve runs the ftp server
func (s *server) serve() error {
fs.Logf(s.f, "Serving FTP on %s", s.srv.Hostname+":"+strconv.Itoa(s.srv.Port))
return s.srv.ListenAndServe()
}

// serve runs the ftp server
func (s *server) close() error {
fs.Logf(s.f, "Stopping FTP on %s", s.srv.Hostname+":"+strconv.Itoa(s.srv.Port))
return s.srv.Shutdown()
}

//FTPLogger ftp logger output formatted message
type FTPLogger struct{}

func (l *FTPLogger) Print(sessionId string, message interface{}) {
fs.Infof(sessionId, "%s", message)
}
func (l *FTPLogger) Printf(sessionId string, format string, v ...interface{}) {
fs.Infof(sessionId, format, v...)
}
func (l *FTPLogger) PrintCommand(sessionId string, command string, params string) {
if command == "PASS" {
fs.Infof(sessionId, "> PASS ****")
} else {
fs.Infof(sessionId, "> %s %s", command, params)
}
}
func (l *FTPLogger) PrintResponse(sessionId string, code int, message string) {
fs.Infof(sessionId, "< %d %s", code, message)
}

//FTPAuth struct to handle ftp auth (temporary simple for POC)
type FTPAuth struct {
BasicUser string
BasicPass string
}

//CheckPasswd handle auth based on configuration
func (a *FTPAuth) CheckPasswd(user, pass string) (bool, error) {
return a.BasicUser == user && a.BasicPass == pass, nil
}

//FTPDriverFactory factory of ftp driver for each session
type FTPDriverFactory struct {
vfs *vfs.VFS
}

//NewDriver start a new session
func (f *FTPDriverFactory) NewDriver() (ftp.Driver, error) {
log.Trace("", "Init driver")("")
return &FTPDriver{
vfs: f.vfs,
}, nil
}

//FTPDriver impletation of ftp server
type FTPDriver struct {
vfs *vfs.VFS
}

//Init a connection
func (d *FTPDriver) Init(*ftp.Conn) {
defer log.Trace("", "Init session")("")
}

//Stat get information on file or folder
func (d *FTPDriver) Stat(path string) (fi ftp.FileInfo, err error) {
defer log.Trace(path, "")("fi=%+v, err = %v", &fi, &err)
n, err := d.vfs.Stat(path)
if err != nil {
return nil, err
}
return &FTPFileInfo{n, n.Mode(), d.vfs.Opt.UID, d.vfs.Opt.GID}, err
}

//ChangeDir move current folder
func (d *FTPDriver) ChangeDir(path string) (err error) {
defer log.Trace(path, "")("err = %v", &err)
n, err := d.vfs.Stat(path)
if err != nil {
return err
}
if !n.IsDir() {
return errors.New("Not a directory")
}
return nil
}

//ListDir list content of a folder
func (d *FTPDriver) ListDir(path string, callback func(ftp.FileInfo) error) (err error) {
defer log.Trace(path, "")("err = %v", &err)
node, err := d.vfs.Stat(path)
if err != nil {
return err
}
if !node.IsDir() {
return errors.New("Not a directory")
}
handle, err := node.Open(os.O_RDONLY)
if err != nil {
return err
}

files, err := handle.Readdir(-1)
if err != nil {
return err
}

for _, file := range files {
err = callback(&FTPFileInfo{file, file.Mode(), d.vfs.Opt.UID, d.vfs.Opt.GID})
if err != nil {
return err
}
}
return nil
}

//DeleteDir delete a folder and his content
func (d *FTPDriver) DeleteDir(path string) (err error) {
defer log.Trace(path, "")("err = %v", &err)
node, err := d.vfs.Stat(path)
if err != nil {
return err
}
if !node.IsDir() {
return errors.New("Not a directory")
}
err = node.Remove()
if err != nil {
return err
}
return nil
}

//DeleteFile delete a file
func (d *FTPDriver) DeleteFile(path string) (err error) {
defer log.Trace(path, "")("err = %v", &err)
node, err := d.vfs.Stat(path)
if err != nil {
return err
}
if !node.IsFile() {
return errors.New("Not a file")
}
err = node.Remove()
if err != nil {
return err
}
return nil
}

//Rename rename a file or folder
func (d *FTPDriver) Rename(oldName, newName string) (err error) {
defer log.Trace(oldName, "newName=%q", newName)("err = %v", &err)
return d.vfs.Rename(oldName, newName)
}

//MakeDir create a folder
func (d *FTPDriver) MakeDir(path string) (err error) {
defer log.Trace(path, "")("err = %v", &err)
dir, leaf, err := d.vfs.StatParent(path)
if err != nil {
return err
}
_, err = dir.Mkdir(leaf)
return err
}

//GetFile download a file
func (d *FTPDriver) GetFile(path string, offset int64) (size int64, fr io.ReadCloser, err error) {
defer log.Trace(path, "offset=%v", offset)("err = %v", &err)
node, err := d.vfs.Stat(path)
if err != nil {
return 0, nil, err
}
handle, err := node.Open(os.O_RDONLY)
if err != nil {
return 0, nil, err
}
handle.Seek(offset, os.SEEK_SET)
if err != nil {
return 0, nil, err
}

return node.Size(), handle, nil
}

//PutFile upload a file
func (d *FTPDriver) PutFile(path string, data io.Reader, appendData bool) (n int64, err error) {
defer log.Trace(path, "append=%v", appendData)("err = %v", &err)
var isExist bool
node, err := d.vfs.Stat(path)
if err == nil {
isExist = true
if node.IsDir() {
return 0, errors.New("A dir has the same name")
}
} else {
if os.IsNotExist(err) {
isExist = false
} else {
return 0, err
}
}

if appendData && !isExist {
appendData = false
}

if !appendData {
if isExist {
err = node.Remove()
if err != nil {
return 0, err
}
}
f, err := d.vfs.OpenFile(path, os.O_RDWR|os.O_CREATE, 0660)
if err != nil {
return 0, err
}
defer f.Close()
bytes, err := io.Copy(f, data)
if err != nil {
return 0, err
}
return bytes, nil
}

of, err := d.vfs.OpenFile(path, os.O_APPEND|os.O_RDWR, 0660)
if err != nil {
return 0, err
}
defer of.Close()

_, err = of.Seek(0, os.SEEK_END)
if err != nil {
return 0, err
}

bytes, err := io.Copy(of, data)
if err != nil {
return 0, err
}

return bytes, nil
}

//FTPFileInfo struct ot hold file infor for ftp server
type FTPFileInfo struct {
os.FileInfo

mode os.FileMode
owner uint32
group uint32
}

//Mode return êrm mode of file.
func (f *FTPFileInfo) Mode() os.FileMode {
return f.mode
}

//Owner return owner of file. Try to find the username if possible
func (f *FTPFileInfo) Owner() string {
str := fmt.Sprint(f.owner)
u, err := user.LookupId(str)
if err != nil {
return str //User not found
}
return u.Username
}

//Group return group of file. Try to find the group name if possible
func (f *FTPFileInfo) Group() string {
str := fmt.Sprint(f.group)
g, err := user.LookupGroupId(str)
if err != nil {
return str //Group not found
}
return g.Name
}

0 comments on commit 517b772

Please sign in to comment.