forked from rclone/rclone
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cmd/serve: add ftp server - implement rclone#2151
- Loading branch information
Showing
5 changed files
with
497 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.