فهرست منبع

FTP: add support for AVBL command

Nicola Murino 4 سال پیش
والد
کامیت
2a95d031ea
12فایلهای تغییر یافته به همراه153 افزوده شده و 6 حذف شده
  1. 2 2
      common/common.go
  2. 69 1
      ftpd/ftpd_test.go
  3. 37 2
      ftpd/handler.go
  4. 4 0
      ftpd/internal_test.go
  5. 1 0
      go.mod
  6. 5 0
      go.sum
  7. 5 0
      vfs/azblobfs.go
  8. 5 0
      vfs/gcsfs.go
  9. 10 0
      vfs/osfs.go
  10. 5 0
      vfs/s3fs.go
  11. 5 0
      vfs/sftpfs.go
  12. 5 1
      vfs/vfs.go

+ 2 - 2
common/common.go

@@ -87,8 +87,8 @@ var (
 	ErrGenericFailure       = errors.New("failure")
 	ErrQuotaExceeded        = errors.New("denying write due to space limit")
 	ErrSkipPermissionsCheck = errors.New("permission check skipped")
-	ErrConnectionDenied     = errors.New("You are not allowed to connect")
-	ErrNoBinding            = errors.New("No binding configured")
+	ErrConnectionDenied     = errors.New("you are not allowed to connect")
+	ErrNoBinding            = errors.New("no binding configured")
 	errNoTransfer           = errors.New("requested transfer not found")
 	errTransferMismatch     = errors.New("transfer mismatch")
 )

+ 69 - 1
ftpd/ftpd_test.go

@@ -1395,7 +1395,7 @@ func TestUploadOverwriteVfolder(t *testing.T) {
 	assert.NoError(t, err)
 }
 
-func TestAllocate(t *testing.T) {
+func TestAllocateAvailable(t *testing.T) {
 	u := getTestUser()
 	mappedPath := filepath.Join(os.TempDir(), "vdir")
 	u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
@@ -1415,6 +1415,16 @@ func TestAllocate(t *testing.T) {
 		assert.NoError(t, err)
 		assert.Equal(t, ftp.StatusCommandOK, code)
 		assert.Equal(t, "Done !", response)
+
+		code, response, err = client.SendCustomCommand("AVBL /vdir")
+		assert.NoError(t, err)
+		assert.Equal(t, ftp.StatusFile, code)
+		assert.Equal(t, "110", response)
+
+		code, _, err = client.SendCustomCommand("AVBL")
+		assert.NoError(t, err)
+		assert.Equal(t, ftp.StatusFile, code)
+
 		err = client.Quit()
 		assert.NoError(t, err)
 	}
@@ -1442,6 +1452,12 @@ func TestAllocate(t *testing.T) {
 
 		err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
 		assert.NoError(t, err)
+
+		code, response, err = client.SendCustomCommand("AVBL")
+		assert.NoError(t, err)
+		assert.Equal(t, ftp.StatusFile, code)
+		assert.Equal(t, "1", response)
+
 		// we still have space in vdir
 		code, response, err = client.SendCustomCommand("allo 50")
 		assert.NoError(t, err)
@@ -1475,10 +1491,38 @@ func TestAllocate(t *testing.T) {
 		assert.Equal(t, ftp.StatusFileUnavailable, code)
 		assert.Contains(t, response, common.ErrQuotaExceeded.Error())
 
+		code, response, err = client.SendCustomCommand("AVBL")
+		assert.NoError(t, err)
+		assert.Equal(t, ftp.StatusFile, code)
+		assert.Equal(t, "100", response)
+
 		err = client.Quit()
 		assert.NoError(t, err)
 	}
 
+	user.QuotaSize = 50
+	user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
+	assert.NoError(t, err)
+	client, err = getFTPClient(user, false)
+	if assert.NoError(t, err) {
+		code, response, err := client.SendCustomCommand("AVBL")
+		assert.NoError(t, err)
+		assert.Equal(t, ftp.StatusFile, code)
+		assert.Equal(t, "0", response)
+	}
+
+	user.QuotaSize = 1000
+	user.Filters.MaxUploadFileSize = 1
+	user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
+	assert.NoError(t, err)
+	client, err = getFTPClient(user, false)
+	if assert.NoError(t, err) {
+		code, response, err := client.SendCustomCommand("AVBL")
+		assert.NoError(t, err)
+		assert.Equal(t, ftp.StatusFile, code)
+		assert.Equal(t, "1", response)
+	}
+
 	_, err = httpd.RemoveUser(user, http.StatusOK)
 	assert.NoError(t, err)
 	_, err = httpd.RemoveFolder(vfs.BaseVirtualFolder{MappedPath: mappedPath}, http.StatusOK)
@@ -1489,6 +1533,30 @@ func TestAllocate(t *testing.T) {
 	assert.NoError(t, err)
 }
 
+func TestAvailableUnsupportedFs(t *testing.T) {
+	u := getTestUser()
+	localUser, _, err := httpd.AddUser(u, http.StatusOK)
+	assert.NoError(t, err)
+	sftpUser, _, err := httpd.AddUser(getTestSFTPUser(), http.StatusOK)
+	assert.NoError(t, err)
+	client, err := getFTPClient(sftpUser, false)
+	if assert.NoError(t, err) {
+		code, response, err := client.SendCustomCommand("AVBL")
+		assert.NoError(t, err)
+		assert.Equal(t, ftp.StatusFileUnavailable, code)
+		assert.Contains(t, response, "unable to get available size for this storage backend")
+
+		err = client.Quit()
+		assert.NoError(t, err)
+	}
+	_, err = httpd.RemoveUser(sftpUser, http.StatusOK)
+	assert.NoError(t, err)
+	_, err = httpd.RemoveUser(localUser, http.StatusOK)
+	assert.NoError(t, err)
+	err = os.RemoveAll(localUser.GetHomeDir())
+	assert.NoError(t, err)
+}
+
 func TestChtimes(t *testing.T) {
 	u := getTestUser()
 	localUser, _, err := httpd.AddUser(u, http.StatusOK)

+ 37 - 2
ftpd/handler.go

@@ -49,7 +49,7 @@ func (c *Connection) Disconnect() error {
 	return c.clientContext.Close()
 }
 
-// GetCommand returns an empty string
+// GetCommand returns the last received FTP command
 func (c *Connection) GetCommand() string {
 	return c.clientContext.GetLastCommand()
 }
@@ -209,7 +209,42 @@ func (c *Connection) Chtimes(name string, atime time.Time, mtime time.Time) erro
 	return c.SetStat(p, name, &attrs)
 }
 
-// AllocateSpace implements ClientDriverExtensionAllocate
+// GetAvailableSpace implements ClientDriverExtensionAvailableSpace interface
+func (c *Connection) GetAvailableSpace(dirName string) (int64, error) {
+	c.UpdateLastActivity()
+
+	quotaResult := c.HasSpace(false, path.Join(dirName, "fakefile.txt"))
+
+	if !quotaResult.HasSpace {
+		return 0, nil
+	}
+
+	if quotaResult.AllowedSize == 0 {
+		// no quota restrictions
+		if c.User.Filters.MaxUploadFileSize > 0 {
+			return c.User.Filters.MaxUploadFileSize, nil
+		}
+
+		p, err := c.Fs.ResolvePath(dirName)
+		if err != nil {
+			return 0, c.GetFsError(err)
+		}
+
+		return c.Fs.GetAvailableDiskSize(p)
+	}
+
+	// the available space is the minimum between MaxUploadFileSize, if setted,
+	// and quota allowed size
+	if c.User.Filters.MaxUploadFileSize > 0 {
+		if c.User.Filters.MaxUploadFileSize < quotaResult.AllowedSize {
+			return c.User.Filters.MaxUploadFileSize, nil
+		}
+	}
+
+	return quotaResult.AllowedSize, nil
+}
+
+// AllocateSpace implements ClientDriverExtensionAllocate interface
 func (c *Connection) AllocateSpace(size int) error {
 	c.UpdateLastActivity()
 	// check the max allowed file size first

+ 4 - 0
ftpd/internal_test.go

@@ -342,6 +342,10 @@ func TestResolvePathErrors(t *testing.T) {
 	if assert.Error(t, err) {
 		assert.EqualError(t, err, common.ErrGenericFailure.Error())
 	}
+	_, err = connection.GetAvailableSpace("")
+	if assert.Error(t, err) {
+		assert.EqualError(t, err, common.ErrGenericFailure.Error())
+	}
 }
 
 func TestUploadFileStatError(t *testing.T) {

+ 1 - 0
go.mod

@@ -34,6 +34,7 @@ require (
 	github.com/rs/xid v1.2.1
 	github.com/rs/zerolog v1.20.0
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/shirou/gopsutil/v3 v3.20.11
 	github.com/spf13/afero v1.5.1
 	github.com/spf13/cast v1.3.1 // indirect
 	github.com/spf13/cobra v1.1.1

+ 5 - 0
go.sum

@@ -85,6 +85,7 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
 github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g=
 github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
 github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@@ -208,6 +209,7 @@ github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
 github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
 github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
 github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@@ -559,6 +561,8 @@ github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIH
 github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
+github.com/shirou/gopsutil/v3 v3.20.11 h1:NeVf1K0cgxsWz+N3671ojRptdgzvp7BXL3KV21R0JnA=
+github.com/shirou/gopsutil/v3 v3.20.11/go.mod h1:igHnfak0qnw1biGeI2qKQvu0ZkwvEkUcCLlYhZzdr/4=
 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@@ -742,6 +746,7 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201024232916-9f70ab9862d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

+ 5 - 0
vfs/azblobfs.go

@@ -701,6 +701,11 @@ func (*AzureBlobFs) Close() error {
 	return nil
 }
 
+// GetAvailableDiskSize return the available size for the specified path
+func (*AzureBlobFs) GetAvailableDiskSize(dirName string) (int64, error) {
+	return 0, errStorageSizeUnavailable
+}
+
 func (fs *AzureBlobFs) isEqual(key string, virtualName string) bool {
 	if key == virtualName {
 		return true

+ 5 - 0
vfs/gcsfs.go

@@ -754,3 +754,8 @@ func (fs *GCSFs) GetMimeType(name string) (string, error) {
 func (fs *GCSFs) Close() error {
 	return nil
 }
+
+// GetAvailableDiskSize return the available size for the specified path
+func (*GCSFs) GetAvailableDiskSize(dirName string) (int64, error) {
+	return 0, errStorageSizeUnavailable
+}

+ 10 - 0
vfs/osfs.go

@@ -12,6 +12,7 @@ import (
 
 	"github.com/eikenb/pipeat"
 	"github.com/rs/xid"
+	"github.com/shirou/gopsutil/v3/disk"
 
 	"github.com/drakkan/sftpgo/logger"
 	"github.com/drakkan/sftpgo/utils"
@@ -477,3 +478,12 @@ func (fs *OsFs) GetMimeType(name string) (string, error) {
 func (*OsFs) Close() error {
 	return nil
 }
+
+// GetAvailableDiskSize return the available size for the specified path
+func (*OsFs) GetAvailableDiskSize(dirName string) (int64, error) {
+	usage, err := disk.Usage(dirName)
+	if err != nil {
+		return 0, err
+	}
+	return int64(usage.Free), nil
+}

+ 5 - 0
vfs/s3fs.go

@@ -703,3 +703,8 @@ func (fs *S3Fs) GetMimeType(name string) (string, error) {
 func (*S3Fs) Close() error {
 	return nil
 }
+
+// GetAvailableDiskSize return the available size for the specified path
+func (*S3Fs) GetAvailableDiskSize(dirName string) (int64, error) {
+	return 0, errStorageSizeUnavailable
+}

+ 5 - 0
vfs/sftpfs.go

@@ -494,6 +494,11 @@ func (fs *SFTPFs) Close() error {
 	return sshErr
 }
 
+// GetAvailableDiskSize return the available size for the specified path
+func (*SFTPFs) GetAvailableDiskSize(dirName string) (int64, error) {
+	return 0, errStorageSizeUnavailable
+}
+
 func (fs *SFTPFs) checkConnection() error {
 	err := fs.closed()
 	if err == nil {

+ 5 - 1
vfs/vfs.go

@@ -22,7 +22,10 @@ import (
 
 const dirMimeType = "inode/directory"
 
-var validAzAccessTier = []string{"", "Archive", "Hot", "Cool"}
+var (
+	validAzAccessTier         = []string{"", "Archive", "Hot", "Cool"}
+	errStorageSizeUnavailable = errors.New("unable to get available size for this storage backend")
+)
 
 // Fs defines the interface for filesystem backends
 type Fs interface {
@@ -57,6 +60,7 @@ type Fs interface {
 	Join(elem ...string) string
 	HasVirtualFolders() bool
 	GetMimeType(name string) (string, error)
+	GetAvailableDiskSize(dirName string) (int64, error)
 	Close() error
 }