| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | // Copyright (c) 2015-2023 MinIO, Inc.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This file is part of MinIO Object Storage stack
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This program is free software: you can redistribute it and/or modify
 | 
					
						
							|  |  |  | // it under the terms of the GNU Affero General Public License as published by
 | 
					
						
							|  |  |  | // the Free Software Foundation, either version 3 of the License, or
 | 
					
						
							|  |  |  | // (at your option) any later version.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // This program is distributed in the hope that it will be useful
 | 
					
						
							|  |  |  | // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
					
						
							|  |  |  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
					
						
							|  |  |  | // GNU Affero General Public License for more details.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // You should have received a copy of the GNU Affero General Public License
 | 
					
						
							|  |  |  | // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | package cmd | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"bytes" | 
					
						
							|  |  |  | 	"context" | 
					
						
							|  |  |  | 	"errors" | 
					
						
							|  |  |  | 	"fmt" | 
					
						
							|  |  |  | 	"io" | 
					
						
							|  |  |  | 	"os" | 
					
						
							| 
									
										
										
										
											2024-04-19 20:23:42 +08:00
										 |  |  | 	"path" | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 	"strings" | 
					
						
							|  |  |  | 	"sync" | 
					
						
							|  |  |  | 	"time" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-20 08:53:08 +08:00
										 |  |  | 	"github.com/minio/madmin-go/v3" | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 	"github.com/minio/minio-go/v7" | 
					
						
							|  |  |  | 	"github.com/minio/minio-go/v7/pkg/credentials" | 
					
						
							|  |  |  | 	"github.com/minio/minio/internal/auth" | 
					
						
							| 
									
										
										
										
											2024-01-29 02:04:17 +08:00
										 |  |  | 	xioutil "github.com/minio/minio/internal/ioutil" | 
					
						
							| 
									
										
										
										
											2024-04-19 20:23:42 +08:00
										 |  |  | 	"github.com/minio/pkg/v2/mimedb" | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 	"github.com/pkg/sftp" | 
					
						
							|  |  |  | 	"golang.org/x/crypto/ssh" | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type sftpDriver struct { | 
					
						
							|  |  |  | 	permissions *ssh.Permissions | 
					
						
							|  |  |  | 	endpoint    string | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //msgp:ignore sftpMetrics
 | 
					
						
							|  |  |  | type sftpMetrics struct{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var globalSftpMetrics sftpMetrics | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func sftpTrace(s *sftp.Request, startTime time.Time, source string, user string, err error) madmin.TraceInfo { | 
					
						
							|  |  |  | 	var errStr string | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		errStr = err.Error() | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return madmin.TraceInfo{ | 
					
						
							|  |  |  | 		TraceType: madmin.TraceFTP, | 
					
						
							|  |  |  | 		Time:      startTime, | 
					
						
							|  |  |  | 		NodeName:  globalLocalNodeName, | 
					
						
							|  |  |  | 		FuncName:  fmt.Sprintf("sftp USER=%s COMMAND=%s PARAM=%s, Source=%s", user, s.Method, s.Filepath, source), | 
					
						
							|  |  |  | 		Duration:  time.Since(startTime), | 
					
						
							|  |  |  | 		Path:      s.Filepath, | 
					
						
							|  |  |  | 		Error:     errStr, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (m *sftpMetrics) log(s *sftp.Request, user string) func(err error) { | 
					
						
							|  |  |  | 	startTime := time.Now() | 
					
						
							|  |  |  | 	source := getSource(2) | 
					
						
							|  |  |  | 	return func(err error) { | 
					
						
							|  |  |  | 		globalTrace.Publish(sftpTrace(s, startTime, source, user, err)) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // NewSFTPDriver initializes sftp.Handlers implementation of following interfaces
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // - sftp.Fileread
 | 
					
						
							|  |  |  | // - sftp.Filewrite
 | 
					
						
							|  |  |  | // - sftp.Filelist
 | 
					
						
							|  |  |  | // - sftp.Filecmd
 | 
					
						
							|  |  |  | func NewSFTPDriver(perms *ssh.Permissions) sftp.Handlers { | 
					
						
							|  |  |  | 	handler := &sftpDriver{endpoint: fmt.Sprintf("127.0.0.1:%s", globalMinioPort), permissions: perms} | 
					
						
							|  |  |  | 	return sftp.Handlers{ | 
					
						
							|  |  |  | 		FileGet:  handler, | 
					
						
							|  |  |  | 		FilePut:  handler, | 
					
						
							|  |  |  | 		FileCmd:  handler, | 
					
						
							|  |  |  | 		FileList: handler, | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *sftpDriver) getMinIOClient() (*minio.Client, error) { | 
					
						
							|  |  |  | 	ui, ok := globalIAMSys.GetUser(context.Background(), f.AccessKey()) | 
					
						
							|  |  |  | 	if !ok && !globalIAMSys.LDAPConfig.Enabled() { | 
					
						
							|  |  |  | 		return nil, errNoSuchUser | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if !ok && globalIAMSys.LDAPConfig.Enabled() { | 
					
						
							| 
									
										
										
										
											2023-12-06 20:31:35 +08:00
										 |  |  | 		sa, _, err := globalIAMSys.getServiceAccount(context.Background(), f.AccessKey()) | 
					
						
							|  |  |  | 		if err != nil && !errors.Is(err, errNoSuchServiceAccount) { | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-12-06 20:31:35 +08:00
										 |  |  | 		var mcreds *credentials.Credentials | 
					
						
							|  |  |  | 		if errors.Is(err, errNoSuchServiceAccount) { | 
					
						
							|  |  |  | 			targetUser, targetGroups, err := globalIAMSys.LDAPConfig.LookupUserDN(f.AccessKey()) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			expiryDur, err := globalIAMSys.LDAPConfig.GetExpiryDuration("") | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			claims := make(map[string]interface{}) | 
					
						
							|  |  |  | 			claims[expClaim] = UTCNow().Add(expiryDur).Unix() | 
					
						
							|  |  |  | 			for k, v := range f.permissions.CriticalOptions { | 
					
						
							|  |  |  | 				claims[k] = v | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			cred, err := auth.GetNewCredentialsWithMetadata(claims, globalActiveCred.SecretKey) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Set the parent of the temporary access key, this is useful
 | 
					
						
							|  |  |  | 			// in obtaining service accounts by this cred.
 | 
					
						
							|  |  |  | 			cred.ParentUser = targetUser | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Set this value to LDAP groups, LDAP user can be part
 | 
					
						
							|  |  |  | 			// of large number of groups
 | 
					
						
							|  |  |  | 			cred.Groups = targetGroups | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Set the newly generated credentials, policyName is empty on purpose
 | 
					
						
							|  |  |  | 			// LDAP policies are applied automatically using their ldapUser, ldapGroups
 | 
					
						
							|  |  |  | 			// mapping.
 | 
					
						
							|  |  |  | 			updatedAt, err := globalIAMSys.SetTempUser(context.Background(), cred.AccessKey, cred, "") | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Call hook for site replication.
 | 
					
						
							| 
									
										
										
										
											2024-04-04 20:04:40 +08:00
										 |  |  | 			replLogIf(context.Background(), globalSiteReplicationSys.IAMChangeHook(context.Background(), madmin.SRIAMItem{ | 
					
						
							| 
									
										
										
										
											2023-12-06 20:31:35 +08:00
										 |  |  | 				Type: madmin.SRIAMItemSTSAcc, | 
					
						
							|  |  |  | 				STSCredential: &madmin.SRSTSCredential{ | 
					
						
							|  |  |  | 					AccessKey:    cred.AccessKey, | 
					
						
							|  |  |  | 					SecretKey:    cred.SecretKey, | 
					
						
							|  |  |  | 					SessionToken: cred.SessionToken, | 
					
						
							|  |  |  | 					ParentUser:   cred.ParentUser, | 
					
						
							|  |  |  | 				}, | 
					
						
							|  |  |  | 				UpdatedAt: updatedAt, | 
					
						
							|  |  |  | 			})) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			mcreds = credentials.NewStaticV4(cred.AccessKey, cred.SecretKey, cred.SessionToken) | 
					
						
							|  |  |  | 		} else { | 
					
						
							|  |  |  | 			mcreds = credentials.NewStaticV4(sa.Credentials.AccessKey, sa.Credentials.SecretKey, "") | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return minio.New(f.endpoint, &minio.Options{ | 
					
						
							| 
									
										
										
										
											2023-12-06 20:31:35 +08:00
										 |  |  | 			Creds:     mcreds, | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 			Secure:    globalIsTLS, | 
					
						
							| 
									
										
										
										
											2023-06-30 23:04:13 +08:00
										 |  |  | 			Transport: globalRemoteFTPClientTransport, | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 		}) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// ok == true - at this point
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if ui.Credentials.IsTemp() { | 
					
						
							|  |  |  | 		// Temporary credentials are not allowed.
 | 
					
						
							|  |  |  | 		return nil, errAuthentication | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return minio.New(f.endpoint, &minio.Options{ | 
					
						
							|  |  |  | 		Creds:     credentials.NewStaticV4(ui.Credentials.AccessKey, ui.Credentials.SecretKey, ""), | 
					
						
							|  |  |  | 		Secure:    globalIsTLS, | 
					
						
							| 
									
										
										
										
											2023-06-30 23:04:13 +08:00
										 |  |  | 		Transport: globalRemoteFTPClientTransport, | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 	}) | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *sftpDriver) AccessKey() string { | 
					
						
							| 
									
										
										
										
											2023-06-29 04:15:21 +08:00
										 |  |  | 	if _, ok := f.permissions.CriticalOptions["accessKey"]; !ok { | 
					
						
							|  |  |  | 		return f.permissions.CriticalOptions[ldapUserN] | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 	return f.permissions.CriticalOptions["accessKey"] | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *sftpDriver) Fileread(r *sftp.Request) (ra io.ReaderAt, err error) { | 
					
						
							|  |  |  | 	stopFn := globalSftpMetrics.log(r, f.AccessKey()) | 
					
						
							|  |  |  | 	defer stopFn(err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	flags := r.Pflags() | 
					
						
							|  |  |  | 	if !flags.Read { | 
					
						
							|  |  |  | 		// sanity check
 | 
					
						
							|  |  |  | 		return nil, os.ErrInvalid | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	bucket, object := path2BucketObject(r.Filepath) | 
					
						
							|  |  |  | 	if bucket == "" { | 
					
						
							|  |  |  | 		return nil, errors.New("bucket name cannot be empty") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	clnt, err := f.getMinIOClient() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	obj, err := clnt.GetObject(context.Background(), bucket, object, minio.GetObjectOptions{}) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	_, err = obj.Stat() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return obj, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-15 01:31:00 +08:00
										 |  |  | // TransferError will catch network errors during transfer.
 | 
					
						
							|  |  |  | // When TransferError() is called Close() will also
 | 
					
						
							|  |  |  | // be called, so we do not need to Wait() here.
 | 
					
						
							|  |  |  | func (w *writerAt) TransferError(err error) { | 
					
						
							|  |  |  | 	_ = w.w.CloseWithError(err) | 
					
						
							|  |  |  | 	_ = w.r.CloseWithError(err) | 
					
						
							|  |  |  | 	w.err = err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-11 16:14:41 +08:00
										 |  |  | func (w *writerAt) Close() (err error) { | 
					
						
							| 
									
										
										
										
											2023-11-15 01:31:00 +08:00
										 |  |  | 	switch { | 
					
						
							|  |  |  | 	case len(w.buffer) > 0: | 
					
						
							|  |  |  | 		err = errors.New("some file segments were not flushed from the queue") | 
					
						
							|  |  |  | 		_ = w.w.CloseWithError(err) | 
					
						
							|  |  |  | 	case w.err != nil: | 
					
						
							|  |  |  | 		// No need to close here since both pipes were
 | 
					
						
							|  |  |  | 		// closing inside TransferError()
 | 
					
						
							|  |  |  | 		err = w.err | 
					
						
							|  |  |  | 	default: | 
					
						
							| 
									
										
										
										
											2023-11-11 16:14:41 +08:00
										 |  |  | 		err = w.w.Close() | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-11-15 01:31:00 +08:00
										 |  |  | 	for i := range w.buffer { | 
					
						
							|  |  |  | 		delete(w.buffer, i) | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 	w.wg.Wait() | 
					
						
							|  |  |  | 	return err | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-11 16:14:41 +08:00
										 |  |  | type writerAt struct { | 
					
						
							|  |  |  | 	w      *io.PipeWriter | 
					
						
							| 
									
										
										
										
											2023-11-15 01:31:00 +08:00
										 |  |  | 	r      *io.PipeReader | 
					
						
							| 
									
										
										
										
											2023-11-11 16:14:41 +08:00
										 |  |  | 	wg     *sync.WaitGroup | 
					
						
							|  |  |  | 	buffer map[int64][]byte | 
					
						
							| 
									
										
										
										
											2023-11-15 01:31:00 +08:00
										 |  |  | 	err    error | 
					
						
							| 
									
										
										
										
											2023-11-11 16:14:41 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	nextOffset int64 | 
					
						
							|  |  |  | 	m          sync.Mutex | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | func (w *writerAt) WriteAt(b []byte, offset int64) (n int, err error) { | 
					
						
							| 
									
										
										
										
											2023-11-11 16:14:41 +08:00
										 |  |  | 	w.m.Lock() | 
					
						
							|  |  |  | 	defer w.m.Unlock() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	if w.nextOffset == offset { | 
					
						
							|  |  |  | 		n, err = w.w.Write(b) | 
					
						
							|  |  |  | 		w.nextOffset += int64(n) | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		w.buffer[offset] = make([]byte, len(b)) | 
					
						
							|  |  |  | 		copy(w.buffer[offset], b) | 
					
						
							|  |  |  | 		n = len(b) | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | again: | 
					
						
							|  |  |  | 	nextOut, ok := w.buffer[w.nextOffset] | 
					
						
							|  |  |  | 	if ok { | 
					
						
							|  |  |  | 		n, err = w.w.Write(nextOut) | 
					
						
							|  |  |  | 		delete(w.buffer, w.nextOffset) | 
					
						
							|  |  |  | 		w.nextOffset += int64(n) | 
					
						
							|  |  |  | 		if n != len(nextOut) { | 
					
						
							|  |  |  | 			return 0, fmt.Errorf("expected write size %d but wrote %d bytes", len(nextOut), n) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			return 0, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		goto again | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return len(b), nil | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *sftpDriver) Filewrite(r *sftp.Request) (w io.WriterAt, err error) { | 
					
						
							|  |  |  | 	stopFn := globalSftpMetrics.log(r, f.AccessKey()) | 
					
						
							|  |  |  | 	defer stopFn(err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	flags := r.Pflags() | 
					
						
							|  |  |  | 	if !flags.Write { | 
					
						
							|  |  |  | 		// sanity check
 | 
					
						
							|  |  |  | 		return nil, os.ErrInvalid | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	bucket, object := path2BucketObject(r.Filepath) | 
					
						
							|  |  |  | 	if bucket == "" { | 
					
						
							|  |  |  | 		return nil, errors.New("bucket name cannot be empty") | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	clnt, err := f.getMinIOClient() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-11-22 13:36:32 +08:00
										 |  |  | 	ok, err := clnt.BucketExists(r.Context(), bucket) | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	if !ok { | 
					
						
							|  |  |  | 		return nil, os.ErrNotExist | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	pr, pw := io.Pipe() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-11-11 16:14:41 +08:00
										 |  |  | 	wa := &writerAt{ | 
					
						
							|  |  |  | 		buffer: make(map[int64][]byte), | 
					
						
							|  |  |  | 		w:      pw, | 
					
						
							| 
									
										
										
										
											2023-11-15 01:31:00 +08:00
										 |  |  | 		r:      pr, | 
					
						
							| 
									
										
										
										
											2023-11-11 16:14:41 +08:00
										 |  |  | 		wg:     &sync.WaitGroup{}, | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 	wa.wg.Add(1) | 
					
						
							|  |  |  | 	go func() { | 
					
						
							| 
									
										
										
										
											2024-04-19 20:23:42 +08:00
										 |  |  | 		_, err := clnt.PutObject(r.Context(), bucket, object, pr, -1, minio.PutObjectOptions{ | 
					
						
							|  |  |  | 			ContentType:          mimedb.TypeByExtension(path.Ext(object)), | 
					
						
							|  |  |  | 			DisableContentSha256: true, | 
					
						
							|  |  |  | 		}) | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 		pr.CloseWithError(err) | 
					
						
							|  |  |  | 		wa.wg.Done() | 
					
						
							|  |  |  | 	}() | 
					
						
							|  |  |  | 	return wa, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *sftpDriver) Filecmd(r *sftp.Request) (err error) { | 
					
						
							|  |  |  | 	stopFn := globalSftpMetrics.log(r, f.AccessKey()) | 
					
						
							|  |  |  | 	defer stopFn(err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	clnt, err := f.getMinIOClient() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch r.Method { | 
					
						
							|  |  |  | 	case "Setstat", "Rename", "Link", "Symlink": | 
					
						
							|  |  |  | 		return NotImplemented{} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	case "Rmdir": | 
					
						
							|  |  |  | 		bucket, prefix := path2BucketObject(r.Filepath) | 
					
						
							|  |  |  | 		if bucket == "" { | 
					
						
							|  |  |  | 			return errors.New("deleting all buckets not allowed") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		cctx, cancel := context.WithCancel(context.Background()) | 
					
						
							|  |  |  | 		defer cancel() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-29 04:15:21 +08:00
										 |  |  | 		if prefix == "" { | 
					
						
							|  |  |  | 			// if all objects are not deleted yet this call may fail.
 | 
					
						
							|  |  |  | 			return clnt.RemoveBucket(cctx, bucket) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 		objectsCh := make(chan minio.ObjectInfo) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Send object names that are needed to be removed to objectsCh
 | 
					
						
							|  |  |  | 		go func() { | 
					
						
							| 
									
										
										
										
											2024-01-29 02:04:17 +08:00
										 |  |  | 			defer xioutil.SafeClose(objectsCh) | 
					
						
							| 
									
										
										
										
											2023-06-29 04:15:21 +08:00
										 |  |  | 			opts := minio.ListObjectsOptions{ | 
					
						
							|  |  |  | 				Prefix:    prefix, | 
					
						
							|  |  |  | 				Recursive: true, | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 			for object := range clnt.ListObjects(cctx, bucket, opts) { | 
					
						
							|  |  |  | 				if object.Err != nil { | 
					
						
							|  |  |  | 					return | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				objectsCh <- object | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		}() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Call RemoveObjects API
 | 
					
						
							|  |  |  | 		for err := range clnt.RemoveObjects(context.Background(), bucket, objectsCh, minio.RemoveObjectsOptions{}) { | 
					
						
							|  |  |  | 			if err.Err != nil { | 
					
						
							|  |  |  | 				return err.Err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-11-15 01:26:46 +08:00
										 |  |  | 		return err | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	case "Remove": | 
					
						
							|  |  |  | 		bucket, object := path2BucketObject(r.Filepath) | 
					
						
							|  |  |  | 		if bucket == "" { | 
					
						
							|  |  |  | 			return errors.New("bucket name cannot be empty") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return clnt.RemoveObject(context.Background(), bucket, object, minio.RemoveObjectOptions{}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	case "Mkdir": | 
					
						
							|  |  |  | 		bucket, prefix := path2BucketObject(r.Filepath) | 
					
						
							|  |  |  | 		if bucket == "" { | 
					
						
							|  |  |  | 			return errors.New("bucket name cannot be empty") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-29 04:15:21 +08:00
										 |  |  | 		if prefix == "" { | 
					
						
							|  |  |  | 			return clnt.MakeBucket(context.Background(), bucket, minio.MakeBucketOptions{Region: globalSite.Region}) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 		dirPath := buildMinioDir(prefix) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		_, err = clnt.PutObject(context.Background(), bucket, dirPath, bytes.NewReader([]byte("")), 0, | 
					
						
							| 
									
										
										
										
											2024-04-19 20:23:42 +08:00
										 |  |  | 			minio.PutObjectOptions{DisableContentSha256: true}, | 
					
						
							| 
									
										
										
										
											2023-04-15 22:34:02 +08:00
										 |  |  | 		) | 
					
						
							|  |  |  | 		return err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return NotImplemented{} | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | type listerAt []os.FileInfo | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Modeled after strings.Reader's ReadAt() implementation
 | 
					
						
							|  |  |  | func (f listerAt) ListAt(ls []os.FileInfo, offset int64) (int, error) { | 
					
						
							|  |  |  | 	var n int | 
					
						
							|  |  |  | 	if offset >= int64(len(f)) { | 
					
						
							|  |  |  | 		return 0, io.EOF | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	n = copy(ls, f[offset:]) | 
					
						
							|  |  |  | 	if n < len(ls) { | 
					
						
							|  |  |  | 		return n, io.EOF | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	return n, nil | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | func (f *sftpDriver) Filelist(r *sftp.Request) (la sftp.ListerAt, err error) { | 
					
						
							|  |  |  | 	stopFn := globalSftpMetrics.log(r, f.AccessKey()) | 
					
						
							|  |  |  | 	defer stopFn(err) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	clnt, err := f.getMinIOClient() | 
					
						
							|  |  |  | 	if err != nil { | 
					
						
							|  |  |  | 		return nil, err | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	switch r.Method { | 
					
						
							|  |  |  | 	case "List": | 
					
						
							|  |  |  | 		var files []os.FileInfo | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		bucket, prefix := path2BucketObject(r.Filepath) | 
					
						
							|  |  |  | 		if bucket == "" { | 
					
						
							|  |  |  | 			buckets, err := clnt.ListBuckets(r.Context()) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			for _, bucket := range buckets { | 
					
						
							|  |  |  | 				files = append(files, &minioFileInfo{ | 
					
						
							|  |  |  | 					p:     bucket.Name, | 
					
						
							|  |  |  | 					info:  minio.ObjectInfo{Key: bucket.Name, LastModified: bucket.CreationDate}, | 
					
						
							|  |  |  | 					isDir: true, | 
					
						
							|  |  |  | 				}) | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return listerAt(files), nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		prefix = retainSlash(prefix) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		for object := range clnt.ListObjects(r.Context(), bucket, minio.ListObjectsOptions{ | 
					
						
							|  |  |  | 			Prefix:    prefix, | 
					
						
							|  |  |  | 			Recursive: false, | 
					
						
							|  |  |  | 		}) { | 
					
						
							|  |  |  | 			if object.Err != nil { | 
					
						
							|  |  |  | 				return nil, object.Err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if object.Key == prefix { | 
					
						
							|  |  |  | 				continue | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			isDir := strings.HasSuffix(object.Key, SlashSeparator) | 
					
						
							|  |  |  | 			files = append(files, &minioFileInfo{ | 
					
						
							|  |  |  | 				p:     pathClean(strings.TrimPrefix(object.Key, prefix)), | 
					
						
							|  |  |  | 				info:  object, | 
					
						
							|  |  |  | 				isDir: isDir, | 
					
						
							|  |  |  | 			}) | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return listerAt(files), nil | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	case "Stat": | 
					
						
							|  |  |  | 		if r.Filepath == SlashSeparator { | 
					
						
							|  |  |  | 			return listerAt{&minioFileInfo{ | 
					
						
							|  |  |  | 				p:     r.Filepath, | 
					
						
							|  |  |  | 				isDir: true, | 
					
						
							|  |  |  | 			}}, nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		bucket, object := path2BucketObject(r.Filepath) | 
					
						
							|  |  |  | 		if bucket == "" { | 
					
						
							|  |  |  | 			return nil, errors.New("bucket name cannot be empty") | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if object == "" { | 
					
						
							|  |  |  | 			ok, err := clnt.BucketExists(context.Background(), bucket) | 
					
						
							|  |  |  | 			if err != nil { | 
					
						
							|  |  |  | 				return nil, err | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if !ok { | 
					
						
							|  |  |  | 				return nil, os.ErrNotExist | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return listerAt{&minioFileInfo{ | 
					
						
							|  |  |  | 				p:     pathClean(bucket), | 
					
						
							|  |  |  | 				info:  minio.ObjectInfo{Key: bucket}, | 
					
						
							|  |  |  | 				isDir: true, | 
					
						
							|  |  |  | 			}}, nil | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		objInfo, err := clnt.StatObject(context.Background(), bucket, object, minio.StatObjectOptions{}) | 
					
						
							|  |  |  | 		if err != nil { | 
					
						
							|  |  |  | 			if minio.ToErrorResponse(err).Code == "NoSuchKey" { | 
					
						
							|  |  |  | 				// dummy return to satisfy LIST (stat -> list) behavior.
 | 
					
						
							|  |  |  | 				return listerAt{&minioFileInfo{ | 
					
						
							|  |  |  | 					p:     pathClean(object), | 
					
						
							|  |  |  | 					info:  minio.ObjectInfo{Key: object}, | 
					
						
							|  |  |  | 					isDir: true, | 
					
						
							|  |  |  | 				}}, nil | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			return nil, err | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		isDir := strings.HasSuffix(objInfo.Key, SlashSeparator) | 
					
						
							|  |  |  | 		return listerAt{&minioFileInfo{ | 
					
						
							|  |  |  | 			p:     pathClean(object), | 
					
						
							|  |  |  | 			info:  objInfo, | 
					
						
							|  |  |  | 			isDir: isDir, | 
					
						
							|  |  |  | 		}}, nil | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return nil, NotImplemented{} | 
					
						
							|  |  |  | } |