feat: sftp server support (#7643)
* feat: sftp server support * fix(sftp-server): try fix build failed * fix: sftp download lack
This commit is contained in:
@ -5,13 +5,15 @@ import (
|
||||
"errors"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/fs"
|
||||
"github.com/spf13/afero"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AferoAdapter struct {
|
||||
ctx context.Context
|
||||
ctx context.Context
|
||||
nextFileSize int64
|
||||
}
|
||||
|
||||
func NewAferoAdapter(ctx context.Context) *AferoAdapter {
|
||||
@ -78,14 +80,36 @@ func (a *AferoAdapter) ReadDir(name string) ([]os.FileInfo, error) {
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) GetHandle(name string, flags int, offset int64) (ftpserver.FileTransfer, error) {
|
||||
fileSize := a.nextFileSize
|
||||
a.nextFileSize = 0
|
||||
if offset != 0 {
|
||||
return nil, errors.New("offset")
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
if (flags & os.O_APPEND) > 0 {
|
||||
return nil, errors.New("append")
|
||||
if (flags & os.O_SYNC) != 0 {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
if (flags & os.O_WRONLY) > 0 {
|
||||
return OpenUpload(a.ctx, name)
|
||||
if (flags & os.O_APPEND) != 0 {
|
||||
return nil, errs.NotSupport
|
||||
}
|
||||
_, err := fs.Get(a.ctx, name, &fs.GetArgs{})
|
||||
exists := err == nil
|
||||
if (flags&os.O_CREATE) == 0 && !exists {
|
||||
return nil, errs.ObjectNotFound
|
||||
}
|
||||
if (flags&os.O_EXCL) != 0 && exists {
|
||||
return nil, errors.New("file already exists")
|
||||
}
|
||||
if (flags & os.O_WRONLY) != 0 {
|
||||
trunc := (flags & os.O_TRUNC) != 0
|
||||
if fileSize > 0 {
|
||||
return OpenUploadWithLength(a.ctx, name, trunc, fileSize)
|
||||
} else {
|
||||
return OpenUpload(a.ctx, name, trunc)
|
||||
}
|
||||
}
|
||||
return OpenDownload(a.ctx, name)
|
||||
}
|
||||
|
||||
func (a *AferoAdapter) SetNextFileSize(size int64) {
|
||||
a.nextFileSize = size
|
||||
}
|
||||
|
11
server/ftp/const.go
Normal file
11
server/ftp/const.go
Normal file
@ -0,0 +1,11 @@
|
||||
package ftp
|
||||
|
||||
// From leffss/sftpd
|
||||
const (
|
||||
SSH_FXF_READ = 0x00000001
|
||||
SSH_FXF_WRITE = 0x00000002
|
||||
SSH_FXF_APPEND = 0x00000004
|
||||
SSH_FXF_CREAT = 0x00000008
|
||||
SSH_FXF_TRUNC = 0x00000010
|
||||
SSH_FXF_EXCL = 0x00000020
|
||||
)
|
@ -1,6 +1,7 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"github.com/alist-org/alist/v3/internal/conf"
|
||||
@ -23,29 +24,38 @@ type FileUploadProxy struct {
|
||||
buffer *os.File
|
||||
path string
|
||||
ctx context.Context
|
||||
trunc bool
|
||||
}
|
||||
|
||||
func OpenUpload(ctx context.Context, path string) (*FileUploadProxy, error) {
|
||||
func uploadAuth(ctx context.Context, path string) error {
|
||||
user := ctx.Value("user").(*model.User)
|
||||
path, err := user.JoinPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
meta, err := op.GetNearestMeta(stdpath.Dir(path))
|
||||
if err != nil {
|
||||
if !errors.Is(errors.Cause(err), errs.MetaNotFound) {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !(common.CanAccess(user, meta, path, ctx.Value("meta_pass").(string)) &&
|
||||
((user.CanFTPManage() && user.CanWrite()) || common.CanWrite(meta, stdpath.Dir(path)))) {
|
||||
return nil, errs.PermissionDenied
|
||||
return errs.PermissionDenied
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func OpenUpload(ctx context.Context, path string, trunc bool) (*FileUploadProxy, error) {
|
||||
err := uploadAuth(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmpFile, err := os.CreateTemp(conf.Conf.TempDir, "file-*")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &FileUploadProxy{buffer: tmpFile, path: path, ctx: ctx}, nil
|
||||
return &FileUploadProxy{buffer: tmpFile, path: path, ctx: ctx, trunc: trunc}, nil
|
||||
}
|
||||
|
||||
func (f *FileUploadProxy) Read(p []byte) (n int, err error) {
|
||||
@ -77,6 +87,9 @@ func (f *FileUploadProxy) Close() error {
|
||||
if _, err := f.buffer.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.trunc {
|
||||
_ = fs.Remove(f.ctx, f.path)
|
||||
}
|
||||
s := &stream.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: name,
|
||||
@ -84,10 +97,113 @@ func (f *FileUploadProxy) Close() error {
|
||||
Modified: time.Now(),
|
||||
},
|
||||
Mimetype: contentType,
|
||||
WebPutAsTask: false,
|
||||
WebPutAsTask: true,
|
||||
}
|
||||
s.SetTmpFile(f.buffer)
|
||||
s.Closers.Add(f.buffer)
|
||||
_, err = fs.PutAsTask(f.ctx, dir, s)
|
||||
return err
|
||||
}
|
||||
|
||||
type FileUploadWithLengthProxy struct {
|
||||
ftpserver.FileTransfer
|
||||
ctx context.Context
|
||||
path string
|
||||
length int64
|
||||
first512Bytes [512]byte
|
||||
pFirst int
|
||||
pipeWriter io.WriteCloser
|
||||
errChan chan error
|
||||
}
|
||||
|
||||
func OpenUploadWithLength(ctx context.Context, path string, trunc bool, length int64) (*FileUploadWithLengthProxy, error) {
|
||||
err := uploadAuth(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if trunc {
|
||||
_ = fs.Remove(ctx, path)
|
||||
}
|
||||
return &FileUploadWithLengthProxy{ctx: ctx, path: path, length: length}, nil
|
||||
}
|
||||
|
||||
func (f *FileUploadWithLengthProxy) Read(p []byte) (n int, err error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (f *FileUploadWithLengthProxy) Write(p []byte) (n int, err error) {
|
||||
if f.pipeWriter != nil {
|
||||
select {
|
||||
case e := <-f.errChan:
|
||||
return 0, e
|
||||
default:
|
||||
return f.pipeWriter.Write(p)
|
||||
}
|
||||
} else if len(p) < 512-f.pFirst {
|
||||
copy(f.first512Bytes[f.pFirst:], p)
|
||||
f.pFirst += len(p)
|
||||
return len(p), nil
|
||||
} else {
|
||||
copy(f.first512Bytes[f.pFirst:], p[:512-f.pFirst])
|
||||
contentType := http.DetectContentType(f.first512Bytes[:])
|
||||
dir, name := stdpath.Split(f.path)
|
||||
reader, writer := io.Pipe()
|
||||
f.errChan = make(chan error, 1)
|
||||
s := &stream.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: name,
|
||||
Size: f.length,
|
||||
Modified: time.Now(),
|
||||
},
|
||||
Mimetype: contentType,
|
||||
WebPutAsTask: false,
|
||||
Reader: reader,
|
||||
}
|
||||
go func() {
|
||||
e := fs.PutDirectly(f.ctx, dir, s, true)
|
||||
f.errChan <- e
|
||||
close(f.errChan)
|
||||
}()
|
||||
f.pipeWriter = writer
|
||||
n, err = writer.Write(f.first512Bytes[:])
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
n1, err := writer.Write(p[512-f.pFirst:])
|
||||
if err != nil {
|
||||
return n1 + 512 - f.pFirst, err
|
||||
}
|
||||
f.pFirst = 512
|
||||
return len(p), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileUploadWithLengthProxy) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, errs.NotSupport
|
||||
}
|
||||
|
||||
func (f *FileUploadWithLengthProxy) Close() error {
|
||||
if f.pipeWriter != nil {
|
||||
err := f.pipeWriter.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = <-f.errChan
|
||||
return err
|
||||
} else {
|
||||
data := f.first512Bytes[:f.pFirst]
|
||||
contentType := http.DetectContentType(data)
|
||||
dir, name := stdpath.Split(f.path)
|
||||
s := &stream.FileStream{
|
||||
Obj: &model.Object{
|
||||
Name: name,
|
||||
Size: int64(f.pFirst),
|
||||
Modified: time.Now(),
|
||||
},
|
||||
Mimetype: contentType,
|
||||
WebPutAsTask: false,
|
||||
Reader: bytes.NewReader(data),
|
||||
}
|
||||
return fs.PutDirectly(f.ctx, dir, s, true)
|
||||
}
|
||||
}
|
||||
|
122
server/ftp/sftp.go
Normal file
122
server/ftp/sftp.go
Normal file
@ -0,0 +1,122 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"github.com/KirCute/sftpd-alist"
|
||||
"github.com/alist-org/alist/v3/internal/errs"
|
||||
"github.com/alist-org/alist/v3/internal/model"
|
||||
"github.com/alist-org/alist/v3/pkg/utils"
|
||||
"os"
|
||||
)
|
||||
|
||||
type SftpDriverAdapter struct {
|
||||
FtpDriver *AferoAdapter
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) OpenFile(_ string, _ uint32, _ *sftpd.Attr) (sftpd.File, error) {
|
||||
// See also GetHandle
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) OpenDir(_ string) (sftpd.Dir, error) {
|
||||
// See also GetHandle
|
||||
return nil, errs.NotImplement
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Remove(name string) error {
|
||||
return s.FtpDriver.Remove(name)
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Rename(old, new string, _ uint32) error {
|
||||
return s.FtpDriver.Rename(old, new)
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Mkdir(name string, attr *sftpd.Attr) error {
|
||||
return s.FtpDriver.Mkdir(name, attr.Mode)
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Rmdir(name string) error {
|
||||
return s.Remove(name)
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) Stat(name string, _ bool) (*sftpd.Attr, error) {
|
||||
stat, err := s.FtpDriver.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fileInfoToSftpAttr(stat), nil
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) SetStat(_ string, _ *sftpd.Attr) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) ReadLink(_ string) (string, error) {
|
||||
return "", errs.NotSupport
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) CreateLink(_, _ string, _ uint32) error {
|
||||
return errs.NotSupport
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) RealPath(path string) (string, error) {
|
||||
return utils.FixAndCleanPath(path), nil
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) GetHandle(name string, flags uint32, _ *sftpd.Attr, offset uint64) (sftpd.FileTransfer, error) {
|
||||
return s.FtpDriver.GetHandle(name, sftpFlagToOpenMode(flags), int64(offset))
|
||||
}
|
||||
|
||||
func (s *SftpDriverAdapter) ReadDir(name string) ([]sftpd.NamedAttr, error) {
|
||||
dir, err := s.FtpDriver.ReadDir(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]sftpd.NamedAttr, len(dir))
|
||||
for i, d := range dir {
|
||||
ret[i] = *fileInfoToSftpNamedAttr(d)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// From leffss/sftpd
|
||||
func sftpFlagToOpenMode(flags uint32) int {
|
||||
mode := 0
|
||||
if (flags & SSH_FXF_READ) != 0 {
|
||||
mode |= os.O_RDONLY
|
||||
}
|
||||
if (flags & SSH_FXF_WRITE) != 0 {
|
||||
mode |= os.O_WRONLY
|
||||
}
|
||||
if (flags & SSH_FXF_APPEND) != 0 {
|
||||
mode |= os.O_APPEND
|
||||
}
|
||||
if (flags & SSH_FXF_CREAT) != 0 {
|
||||
mode |= os.O_CREATE
|
||||
}
|
||||
if (flags & SSH_FXF_TRUNC) != 0 {
|
||||
mode |= os.O_TRUNC
|
||||
}
|
||||
if (flags & SSH_FXF_EXCL) != 0 {
|
||||
mode |= os.O_EXCL
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
func fileInfoToSftpAttr(stat os.FileInfo) *sftpd.Attr {
|
||||
ret := &sftpd.Attr{}
|
||||
ret.Flags |= sftpd.ATTR_SIZE
|
||||
ret.Size = uint64(stat.Size())
|
||||
ret.Flags |= sftpd.ATTR_MODE
|
||||
ret.Mode = stat.Mode()
|
||||
ret.Flags |= sftpd.ATTR_TIME
|
||||
ret.ATime = stat.Sys().(model.Obj).CreateTime()
|
||||
ret.MTime = stat.ModTime()
|
||||
return ret
|
||||
}
|
||||
|
||||
func fileInfoToSftpNamedAttr(stat os.FileInfo) *sftpd.NamedAttr {
|
||||
return &sftpd.NamedAttr{
|
||||
Name: stat.Name(),
|
||||
Attr: *fileInfoToSftpAttr(stat),
|
||||
}
|
||||
}
|
21
server/ftp/site.go
Normal file
21
server/ftp/site.go
Normal file
@ -0,0 +1,21 @@
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
ftpserver "github.com/KirCute/ftpserverlib-pasvportmap"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func HandleSIZE(param string, client ftpserver.ClientDriver) (int, string) {
|
||||
fs, ok := client.(*AferoAdapter)
|
||||
if !ok {
|
||||
return ftpserver.StatusNotLoggedIn, "Unexpected exception (driver is nil)"
|
||||
}
|
||||
size, err := strconv.ParseInt(param, 10, 64)
|
||||
if err != nil {
|
||||
return ftpserver.StatusSyntaxErrorParameters, fmt.Sprintf(
|
||||
"Couldn't parse file size, given: %s, err: %v", param, err)
|
||||
}
|
||||
fs.SetNextFileSize(size)
|
||||
return ftpserver.StatusOK, "Accepted next file size"
|
||||
}
|
Reference in New Issue
Block a user