You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							417 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							417 lines
						
					
					
						
							12 KiB
						
					
					
				
								// sftp_filer_refactored.go
							 | 
						|
								package sftpd
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"crypto/md5"
							 | 
						|
									"encoding/json"
							 | 
						|
									"fmt"
							 | 
						|
									"io"
							 | 
						|
									"net/http"
							 | 
						|
									"os"
							 | 
						|
									"path"
							 | 
						|
									"strings"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/pkg/sftp"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/glog"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/pb"
							 | 
						|
									filer_pb "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
							 | 
						|
									weed_server "github.com/seaweedfs/seaweedfs/weed/server"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/sftpd/user"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/util"
							 | 
						|
									"google.golang.org/grpc"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								const (
							 | 
						|
									defaultTimeout   = 30 * time.Second
							 | 
						|
									defaultListLimit = 1000
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// ==================== Filer RPC Helpers ====================
							 | 
						|
								
							 | 
						|
								// callWithClient wraps a gRPC client call with timeout and client creation.
							 | 
						|
								func (fs *SftpServer) callWithClient(streaming bool, fn func(ctx context.Context, client filer_pb.SeaweedFilerClient) error) error {
							 | 
						|
									return fs.withTimeoutContext(func(ctx context.Context) error {
							 | 
						|
										return fs.WithFilerClient(streaming, func(client filer_pb.SeaweedFilerClient) error {
							 | 
						|
											return fn(ctx, client)
							 | 
						|
										})
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// getEntry retrieves a single directory entry by path.
							 | 
						|
								func (fs *SftpServer) getEntry(p string) (*filer_pb.Entry, error) {
							 | 
						|
									dir, name := util.FullPath(p).DirAndName()
							 | 
						|
									var entry *filer_pb.Entry
							 | 
						|
									err := fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										r, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{Directory: dir, Name: name})
							 | 
						|
										if err != nil {
							 | 
						|
											if isNotExistError(err) {
							 | 
						|
												return os.ErrNotExist
							 | 
						|
											}
							 | 
						|
											return err
							 | 
						|
										}
							 | 
						|
										if r.Entry == nil {
							 | 
						|
											return fmt.Errorf("%s not found in %s", name, dir)
							 | 
						|
										}
							 | 
						|
										entry = r.Entry
							 | 
						|
										return nil
							 | 
						|
									})
							 | 
						|
									if err != nil {
							 | 
						|
										if isNotExistError(err) {
							 | 
						|
											return nil, os.ErrNotExist
							 | 
						|
										}
							 | 
						|
										return nil, fmt.Errorf("lookup %s: %w", p, err)
							 | 
						|
									}
							 | 
						|
									return entry, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func isNotExistError(err error) bool {
							 | 
						|
									return strings.Contains(err.Error(), "not found") ||
							 | 
						|
										strings.Contains(err.Error(), "no entry is found") ||
							 | 
						|
										strings.Contains(err.Error(), "file does not exist") ||
							 | 
						|
										err == os.ErrNotExist
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// updateEntry sends an UpdateEntryRequest for the given entry.
							 | 
						|
								func (fs *SftpServer) updateEntry(dir string, entry *filer_pb.Entry) error {
							 | 
						|
									return fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										_, err := client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{Directory: dir, Entry: entry})
							 | 
						|
										return err
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ==================== FilerClient Interface ====================
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) AdjustedUrl(location *filer_pb.Location) string { return location.Url }
							 | 
						|
								func (fs *SftpServer) GetDataCenter() string                          { return fs.dataCenter }
							 | 
						|
								func (fs *SftpServer) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
							 | 
						|
									addr := fs.filerAddr.ToGrpcAddress()
							 | 
						|
									return pb.WithGrpcClient(streamingMode, util.RandomInt32(), func(conn *grpc.ClientConn) error {
							 | 
						|
										return fn(filer_pb.NewSeaweedFilerClient(conn))
							 | 
						|
									}, addr, false, fs.grpcDialOption)
							 | 
						|
								}
							 | 
						|
								func (fs *SftpServer) withTimeoutContext(fn func(ctx context.Context) error) error {
							 | 
						|
									ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
							 | 
						|
									defer cancel()
							 | 
						|
									return fn(ctx)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ==================== Command Dispatcher ====================
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) dispatchCmd(r *sftp.Request) error {
							 | 
						|
									glog.V(0).Infof("Dispatch: %s %s", r.Method, r.Filepath)
							 | 
						|
									switch r.Method {
							 | 
						|
									case "Remove":
							 | 
						|
										return fs.removeEntry(r)
							 | 
						|
									case "Rename":
							 | 
						|
										return fs.renameEntry(r)
							 | 
						|
									case "Mkdir":
							 | 
						|
										return fs.makeDir(r)
							 | 
						|
									case "Rmdir":
							 | 
						|
										return fs.removeDir(r)
							 | 
						|
									case "Setstat":
							 | 
						|
										return fs.setFileStat(r)
							 | 
						|
									default:
							 | 
						|
										return fmt.Errorf("unsupported: %s", r.Method)
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ==================== File Operations ====================
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) readFile(r *sftp.Request) (io.ReaderAt, error) {
							 | 
						|
									if err := fs.checkFilePermission(r.Filepath, "read"); err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
									entry, err := fs.getEntry(r.Filepath)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
									return NewSeaweedFileReaderAt(fs, entry), nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) newFileWriter(r *sftp.Request) (io.WriterAt, error) {
							 | 
						|
									dir, _ := util.FullPath(r.Filepath).DirAndName()
							 | 
						|
									if err := fs.checkFilePermission(dir, "write"); err != nil {
							 | 
						|
										glog.Errorf("Permission denied for %s", dir)
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
									// Create a temporary file to buffer writes
							 | 
						|
									tmpFile, err := os.CreateTemp("", "sftp-upload-*")
							 | 
						|
									if err != nil {
							 | 
						|
										return nil, fmt.Errorf("failed to create temp file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return &SeaweedSftpFileWriter{
							 | 
						|
										fs:          *fs,
							 | 
						|
										req:         r,
							 | 
						|
										tmpFile:     tmpFile,
							 | 
						|
										permissions: 0644,
							 | 
						|
										uid:         fs.user.Uid,
							 | 
						|
										gid:         fs.user.Gid,
							 | 
						|
										offset:      0,
							 | 
						|
									}, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) removeEntry(r *sftp.Request) error {
							 | 
						|
									return fs.deleteEntry(r.Filepath, false)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) renameEntry(r *sftp.Request) error {
							 | 
						|
									if err := fs.checkFilePermission(r.Filepath, "rename"); err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									oldDir, oldName := util.FullPath(r.Filepath).DirAndName()
							 | 
						|
									newDir, newName := util.FullPath(r.Target).DirAndName()
							 | 
						|
									return fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										_, err := client.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{
							 | 
						|
											OldDirectory: oldDir, OldName: oldName,
							 | 
						|
											NewDirectory: newDir, NewName: newName,
							 | 
						|
										})
							 | 
						|
										return err
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) setFileStat(r *sftp.Request) error {
							 | 
						|
									if err := fs.checkFilePermission(r.Filepath, "write"); err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									entry, err := fs.getEntry(r.Filepath)
							 | 
						|
									if err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									dir, _ := util.FullPath(r.Filepath).DirAndName()
							 | 
						|
									// apply attrs
							 | 
						|
									if r.AttrFlags().Permissions {
							 | 
						|
										entry.Attributes.FileMode = uint32(r.Attributes().FileMode())
							 | 
						|
									}
							 | 
						|
									if r.AttrFlags().UidGid {
							 | 
						|
										entry.Attributes.Uid = uint32(r.Attributes().UID)
							 | 
						|
										entry.Attributes.Gid = uint32(r.Attributes().GID)
							 | 
						|
									}
							 | 
						|
									if r.AttrFlags().Acmodtime {
							 | 
						|
										entry.Attributes.Mtime = int64(r.Attributes().Mtime)
							 | 
						|
									}
							 | 
						|
									if r.AttrFlags().Size {
							 | 
						|
										entry.Attributes.FileSize = uint64(r.Attributes().Size)
							 | 
						|
									}
							 | 
						|
									return fs.updateEntry(dir, entry)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ==================== Directory Operations ====================
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) listDir(r *sftp.Request) (sftp.ListerAt, error) {
							 | 
						|
									if err := fs.checkFilePermission(r.Filepath, "list"); err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
									if r.Method == "Stat" || r.Method == "Lstat" {
							 | 
						|
										entry, err := fs.getEntry(r.Filepath)
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, err
							 | 
						|
										}
							 | 
						|
										fi := &EnhancedFileInfo{FileInfo: FileInfoFromEntry(entry), uid: entry.Attributes.Uid, gid: entry.Attributes.Gid}
							 | 
						|
										return listerat([]os.FileInfo{fi}), nil
							 | 
						|
									}
							 | 
						|
									return fs.listAllPages(r.Filepath)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) listAllPages(dirPath string) (sftp.ListerAt, error) {
							 | 
						|
									var all []os.FileInfo
							 | 
						|
									last := ""
							 | 
						|
									for {
							 | 
						|
										page, err := fs.fetchDirectoryPage(dirPath, last)
							 | 
						|
										if err != nil {
							 | 
						|
											return nil, err
							 | 
						|
										}
							 | 
						|
										all = append(all, page...)
							 | 
						|
										if len(page) < defaultListLimit {
							 | 
						|
											break
							 | 
						|
										}
							 | 
						|
										last = page[len(page)-1].Name()
							 | 
						|
									}
							 | 
						|
									return listerat(all), nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) fetchDirectoryPage(dirPath, start string) ([]os.FileInfo, error) {
							 | 
						|
									var list []os.FileInfo
							 | 
						|
									err := fs.callWithClient(true, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										stream, err := client.ListEntries(ctx, &filer_pb.ListEntriesRequest{Directory: dirPath, StartFromFileName: start, Limit: defaultListLimit})
							 | 
						|
										if err != nil {
							 | 
						|
											return err
							 | 
						|
										}
							 | 
						|
										for {
							 | 
						|
											r, err := stream.Recv()
							 | 
						|
											if err == io.EOF {
							 | 
						|
												break
							 | 
						|
											}
							 | 
						|
											if err != nil || r.Entry == nil {
							 | 
						|
												continue
							 | 
						|
											}
							 | 
						|
											p := path.Join(dirPath, r.Entry.Name)
							 | 
						|
											if err := fs.checkFilePermission(p, "list"); err != nil {
							 | 
						|
												continue
							 | 
						|
											}
							 | 
						|
											list = append(list, &EnhancedFileInfo{FileInfo: FileInfoFromEntry(r.Entry), uid: r.Entry.Attributes.Uid, gid: r.Entry.Attributes.Gid})
							 | 
						|
										}
							 | 
						|
										return nil
							 | 
						|
									})
							 | 
						|
									return list, err
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// makeDir creates a new directory with proper permissions.
							 | 
						|
								func (fs *SftpServer) makeDir(r *sftp.Request) error {
							 | 
						|
									if fs.user == nil {
							 | 
						|
										return fmt.Errorf("cannot create directory: no user info")
							 | 
						|
									}
							 | 
						|
									dir, name := util.FullPath(r.Filepath).DirAndName()
							 | 
						|
									if err := fs.checkFilePermission(r.Filepath, "mkdir"); err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									// default mode and ownership
							 | 
						|
									err := filer_pb.Mkdir(context.Background(), fs, string(dir), name, func(entry *filer_pb.Entry) {
							 | 
						|
										mode := uint32(0755 | os.ModeDir)
							 | 
						|
										if strings.HasPrefix(r.Filepath, fs.user.HomeDir) {
							 | 
						|
											mode = uint32(0700 | os.ModeDir)
							 | 
						|
										}
							 | 
						|
										entry.Attributes.FileMode = mode
							 | 
						|
										entry.Attributes.Uid = fs.user.Uid
							 | 
						|
										entry.Attributes.Gid = fs.user.Gid
							 | 
						|
										now := time.Now().Unix()
							 | 
						|
										entry.Attributes.Crtime = now
							 | 
						|
										entry.Attributes.Mtime = now
							 | 
						|
										if entry.Extended == nil {
							 | 
						|
											entry.Extended = make(map[string][]byte)
							 | 
						|
										}
							 | 
						|
										entry.Extended["creator"] = []byte(fs.user.Username)
							 | 
						|
									})
							 | 
						|
									return err
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// removeDir deletes a directory.
							 | 
						|
								func (fs *SftpServer) removeDir(r *sftp.Request) error {
							 | 
						|
									return fs.deleteEntry(r.Filepath, false)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) putFile(filepath string, reader io.Reader, user *user.User) error {
							 | 
						|
									dir, filename := util.FullPath(filepath).DirAndName()
							 | 
						|
									uploadUrl := fmt.Sprintf("http://%s%s", fs.filerAddr, filepath)
							 | 
						|
								
							 | 
						|
									// Compute MD5 while uploading
							 | 
						|
									hash := md5.New()
							 | 
						|
									body := io.TeeReader(reader, hash)
							 | 
						|
								
							 | 
						|
									// We can skip ContentLength if unknown (chunked transfer encoding)
							 | 
						|
									req, err := http.NewRequest(http.MethodPut, uploadUrl, body)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("create request: %w", err)
							 | 
						|
									}
							 | 
						|
									req.Header.Set("Content-Type", "application/octet-stream")
							 | 
						|
								
							 | 
						|
									resp, err := http.DefaultClient.Do(req)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("upload to filer: %w", err)
							 | 
						|
									}
							 | 
						|
									defer resp.Body.Close()
							 | 
						|
								
							 | 
						|
									respBody, err := io.ReadAll(resp.Body)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("read response: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
							 | 
						|
										return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(respBody))
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var result weed_server.FilerPostResult
							 | 
						|
									if err := json.Unmarshal(respBody, &result); err != nil {
							 | 
						|
										return fmt.Errorf("parse response: %w", err)
							 | 
						|
									}
							 | 
						|
									if result.Error != "" {
							 | 
						|
										return fmt.Errorf("filer error: %s", result.Error)
							 | 
						|
									}
							 | 
						|
									// Update file ownership using the same pattern as other functions
							 | 
						|
									if user != nil {
							 | 
						|
										err := fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
							 | 
						|
											// Look up the file to get its current entry
							 | 
						|
											lookupResp, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{
							 | 
						|
												Directory: dir,
							 | 
						|
												Name:      filename,
							 | 
						|
											})
							 | 
						|
											if err != nil {
							 | 
						|
												return fmt.Errorf("lookup file for attribute update: %w", err)
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											if lookupResp.Entry == nil {
							 | 
						|
												return fmt.Errorf("file not found after upload: %s/%s", dir, filename)
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Update the entry with new uid/gid
							 | 
						|
											entry := lookupResp.Entry
							 | 
						|
											entry.Attributes.Uid = user.Uid
							 | 
						|
											entry.Attributes.Gid = user.Gid
							 | 
						|
								
							 | 
						|
											// Update the entry in the filer
							 | 
						|
											_, err = client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{
							 | 
						|
												Directory: dir,
							 | 
						|
												Entry:     entry,
							 | 
						|
											})
							 | 
						|
											return err
							 | 
						|
										})
							 | 
						|
								
							 | 
						|
										if err != nil {
							 | 
						|
											// Log the error but don't fail the whole operation
							 | 
						|
											glog.Errorf("Failed to update file ownership for %s: %v", filepath, err)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ==================== Common Arguments Helpers ====================
							 | 
						|
								
							 | 
						|
								func FileInfoFromEntry(e *filer_pb.Entry) FileInfo {
							 | 
						|
									return FileInfo{name: e.Name, size: int64(e.Attributes.FileSize), mode: os.FileMode(e.Attributes.FileMode), modTime: time.Unix(e.Attributes.Mtime, 0), isDir: e.IsDirectory}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) deleteEntry(p string, recursive bool) error {
							 | 
						|
									if err := fs.checkFilePermission(p, "delete"); err != nil {
							 | 
						|
										return err
							 | 
						|
									}
							 | 
						|
									dir, name := util.FullPath(p).DirAndName()
							 | 
						|
									return fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
							 | 
						|
										r, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{Directory: dir, Name: name, IsDeleteData: true, IsRecursive: recursive})
							 | 
						|
										if err != nil {
							 | 
						|
											return err
							 | 
						|
										}
							 | 
						|
										if r.Error != "" {
							 | 
						|
											return fmt.Errorf("%s", r.Error)
							 | 
						|
										}
							 | 
						|
										return nil
							 | 
						|
									})
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ==================== Custom Types ====================
							 | 
						|
								
							 | 
						|
								type EnhancedFileInfo struct {
							 | 
						|
									FileInfo
							 | 
						|
									uid uint32
							 | 
						|
									gid uint32
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// FileStat represents file statistics in a platform-independent way
							 | 
						|
								type FileStat struct {
							 | 
						|
									Uid uint32
							 | 
						|
									Gid uint32
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fi *EnhancedFileInfo) Sys() interface{} {
							 | 
						|
									return &FileStat{Uid: fi.uid, Gid: fi.gid}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fi *EnhancedFileInfo) Owner() (uid, gid int) {
							 | 
						|
									return int(fi.uid), int(fi.gid)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (fs *SftpServer) checkFilePermission(filepath string, permissions string) error {
							 | 
						|
									return fs.CheckFilePermission(filepath, permissions)
							 | 
						|
								}
							 |