add httpfs
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
3170991aa8
commit
7ab30099dd
45 changed files with 3008 additions and 344 deletions
|
@ -271,6 +271,10 @@ Each user can be mapped to another SFTP server account or a subfolder of it. Mor
|
|||
|
||||
Data at-rest encryption is supported via the [cryptfs backend](./docs/dare.md).
|
||||
|
||||
### HTTP/S backend
|
||||
|
||||
HTTP/S backend allows you to write your own custom storage backend by implementing a REST API. More information can be found [here](./docs/httpfs.md).
|
||||
|
||||
### Other Storage backends
|
||||
|
||||
Adding new storage backends is quite easy:
|
||||
|
|
|
@ -136,6 +136,8 @@ func newActionNotification(
|
|||
}
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
endpoint = fsConfig.SFTPConfig.Endpoint
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
endpoint = fsConfig.HTTPConfig.Endpoint
|
||||
}
|
||||
|
||||
if err == ErrQuotaExceeded {
|
||||
|
|
|
@ -49,6 +49,11 @@ func TestNewActionNotification(t *testing.T) {
|
|||
Endpoint: "sftpendpoint",
|
||||
},
|
||||
}
|
||||
user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: "httpendpoint",
|
||||
},
|
||||
}
|
||||
sessionID := xid.New().String()
|
||||
a := newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSFTP, "", sessionID,
|
||||
123, 0, errors.New("fake error"))
|
||||
|
@ -71,6 +76,12 @@ func TestNewActionNotification(t *testing.T) {
|
|||
assert.Equal(t, 0, len(a.Endpoint))
|
||||
assert.Equal(t, 3, a.Status)
|
||||
|
||||
user.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSSH, "", sessionID,
|
||||
123, 0, nil)
|
||||
assert.Equal(t, "httpendpoint", a.Endpoint)
|
||||
assert.Equal(t, 1, a.Status)
|
||||
|
||||
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
|
||||
a = newActionNotification(user, operationDownload, "path", "vpath", "target", "", "", ProtocolSCP, "", sessionID,
|
||||
123, 0, nil)
|
||||
|
|
|
@ -110,7 +110,6 @@ var (
|
|||
ErrGenericFailure = errors.New("failure")
|
||||
ErrQuotaExceeded = errors.New("denying write due to space limit")
|
||||
ErrReadQuotaExceeded = errors.New("denying read due to quota limit")
|
||||
ErrSkipPermissionsCheck = errors.New("permission check skipped")
|
||||
ErrConnectionDenied = errors.New("you are not allowed to connect")
|
||||
ErrNoBinding = errors.New("no binding configured")
|
||||
ErrCrtRevoked = errors.New("your certificate has been revoked")
|
||||
|
|
|
@ -752,7 +752,7 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
|
|||
initialSize = info.Size()
|
||||
err = fs.Truncate(fsPath, size)
|
||||
}
|
||||
if err == nil && vfs.IsLocalOrSFTPFs(fs) {
|
||||
if err == nil && vfs.HasTruncateSupport(fs) {
|
||||
sizeDiff := initialSize - size
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(virtualPath))
|
||||
if err == nil {
|
||||
|
@ -768,22 +768,13 @@ func (c *BaseConnection) truncateFile(fs vfs.Fs, fsPath, virtualPath string, siz
|
|||
}
|
||||
|
||||
func (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs, sourcePath, targetPath string) error {
|
||||
err := fsSrc.Walk(sourcePath, func(walkedPath string, info os.FileInfo, err error) error {
|
||||
return fsSrc.Walk(sourcePath, func(walkedPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return c.GetFsError(fsSrc, err)
|
||||
}
|
||||
dstPath := strings.Replace(walkedPath, sourcePath, targetPath, 1)
|
||||
virtualSrcPath := fsSrc.GetRelativePath(walkedPath)
|
||||
virtualDstPath := fsDst.GetRelativePath(dstPath)
|
||||
// walk scans the directory tree in order, checking the parent directory permissions we are sure that all contents
|
||||
// inside the parent path was checked. If the current dir has no subdirs with defined permissions inside it
|
||||
// and it has all the possible permissions we can stop scanning
|
||||
if !c.User.HasPermissionsInside(path.Dir(virtualSrcPath)) && !c.User.HasPermissionsInside(path.Dir(virtualDstPath)) {
|
||||
if c.User.HasPermsRenameAll(path.Dir(virtualSrcPath)) &&
|
||||
c.User.HasPermsRenameAll(path.Dir(virtualDstPath)) {
|
||||
return ErrSkipPermissionsCheck
|
||||
}
|
||||
}
|
||||
if !c.isRenamePermitted(fsSrc, fsDst, walkedPath, dstPath, virtualSrcPath, virtualDstPath, info) {
|
||||
c.Log(logger.LevelInfo, "rename %#v -> %#v is not allowed, virtual destination path: %#v",
|
||||
walkedPath, dstPath, virtualDstPath)
|
||||
|
@ -791,10 +782,6 @@ func (c *BaseConnection) checkRecursiveRenameDirPermissions(fsSrc, fsDst vfs.Fs,
|
|||
}
|
||||
return nil
|
||||
})
|
||||
if err == ErrSkipPermissionsCheck {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *BaseConnection) hasRenamePerms(virtualSourcePath, virtualTargetPath string, fi os.FileInfo) bool {
|
||||
|
|
|
@ -153,6 +153,8 @@ func (u *User) getRootFs(connectionID string) (fs vfs.Fs, err error) {
|
|||
}
|
||||
forbiddenSelfUsers = append(forbiddenSelfUsers, u.Username)
|
||||
return vfs.NewSFTPFs(connectionID, "", u.GetHomeDir(), forbiddenSelfUsers, u.FsConfig.SFTPConfig)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
return vfs.NewHTTPFs(connectionID, u.GetHomeDir(), "", u.FsConfig.HTTPConfig)
|
||||
default:
|
||||
return vfs.NewOsFs(connectionID, u.GetHomeDir(), ""), nil
|
||||
}
|
||||
|
@ -1366,6 +1368,8 @@ func (u *User) GetStorageDescrition() string {
|
|||
return fmt.Sprintf("Encrypted: %v", u.GetHomeDir())
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
return fmt.Sprintf("SFTP: %v", u.FsConfig.SFTPConfig.Endpoint)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
return fmt.Sprintf("HTTP: %v", u.FsConfig.HTTPConfig.Endpoint)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
@ -1595,6 +1599,8 @@ func (u *User) replaceFsConfigPlaceholders(fsConfig vfs.Filesystem) vfs.Filesyst
|
|||
case sdk.SFTPFilesystemProvider:
|
||||
fsConfig.SFTPConfig.Username = u.replacePlaceholder(fsConfig.SFTPConfig.Username)
|
||||
fsConfig.SFTPConfig.Prefix = u.replacePlaceholder(fsConfig.SFTPConfig.Prefix)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
fsConfig.HTTPConfig.Username = u.replacePlaceholder(fsConfig.HTTPConfig.Username)
|
||||
}
|
||||
return fsConfig
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ SFTPGo provides an official Docker image, it is available on both [Docker Hub](h
|
|||
|
||||
## Supported tags and respective Dockerfile links
|
||||
|
||||
- [v2.3.0, v2.3, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile)
|
||||
- [v2.3.0-alpine, v2.3-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile.alpine)
|
||||
- [v2.3.0-slim, v2.3-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile)
|
||||
- [v2.3.0-alpine-slim, v2.3-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile.alpine)
|
||||
- [v2.3.0-distroless-slim, v2.3-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.3.0/Dockerfile.distroless)
|
||||
- [v2.3.1, v2.3, v2, latest](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile)
|
||||
- [v2.3.1-alpine, v2.3-alpine, v2-alpine, alpine](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile.alpine)
|
||||
- [v2.3.1-slim, v2.3-slim, v2-slim, slim](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile)
|
||||
- [v2.3.1-alpine-slim, v2.3-alpine-slim, v2-alpine-slim, alpine-slim](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile.alpine)
|
||||
- [v2.3.1-distroless-slim, v2.3-distroless-slim, v2-distroless-slim, distroless-slim](https://github.com/drakkan/sftpgo/blob/v2.3.1/Dockerfile.distroless)
|
||||
- [edge](../Dockerfile)
|
||||
- [edge-alpine](../Dockerfile.alpine)
|
||||
- [edge-slim](../Dockerfile)
|
||||
|
|
|
@ -19,7 +19,7 @@ The following settings are inherited from the primary group:
|
|||
|
||||
The following settings are inherited from the primary and secondary groups:
|
||||
|
||||
- virtual folders, file patterns, permissions: they are added to the user configuration if the user does not already have a setting for the configured path. The `/` path is ignored for secondary groups. The `%username%` placeholder is replaced with the username within the virtual path, the defined "prefix", for any vfs, and the "username" for the SFTP filesystem config
|
||||
- virtual folders, file patterns, permissions: they are added to the user configuration if the user does not already have a setting for the configured path. The `/` path is ignored for secondary groups. The `%username%` placeholder is replaced with the username within the virtual path, the defined "prefix", for any vfs, and the "username" for the SFTP and HTTP filesystem config
|
||||
- per-source bandwidth limits
|
||||
- per-source data transfer limits
|
||||
- allowed/denied IPs
|
||||
|
|
16
docs/httpfs.md
Normal file
16
docs/httpfs.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# HTTP/S storage backend
|
||||
|
||||
SFTPGo can use custom storage backend implementations compliant with the REST API documented [here](./../openapi/httpfs.yaml).
|
||||
|
||||
The only required parameter is the HTTP/S endpoint that SFTPGo must use to make API calls.
|
||||
If you define `http://127.0.0.1:9999/api/v1` as endpoint, SFTPGo will add the API path, for example for the `stat` API it will invoke `http://127.0.0.1:9999/api/v1/stat/{name}`.
|
||||
|
||||
You can set a `username` and/or a `password` to instruct SFTPGo to use the basic authentication, or you can set an API key to instruct SFTPGo to add it to each API call in the `X-API-KEY` HTTP header.
|
||||
|
||||
Here is a mapping between HTTP response codes and protocol errors:
|
||||
|
||||
- `401`, `403` mean permission denied error
|
||||
- `404`, means not found error
|
||||
- `501`, means not supported error
|
||||
- `200`, `201`, mean no error
|
||||
- any other response code means a generic error
|
|
@ -1,6 +1,6 @@
|
|||
# SFTPGo repositories
|
||||
|
||||
These repositories are available through Oregon State University's free mirror service. Special thanks to Lance Albertson, Director of the Oregon State University Open Source Lab, who helped me with the initial setup.
|
||||
These repositories are available through Oregon State University's free mirroring service. Special thanks to Lance Albertson, Director of the Oregon State University Open Source Lab, who helped me with the initial setup.
|
||||
|
||||
## APT repo
|
||||
|
||||
|
|
|
@ -230,6 +230,8 @@ xr5cb9VBRBtB9aOKVfuRhpatAfS2Pzm2Htae9lFn7slGPUmu2hkjDw==
|
|||
testDLFileName = "test_download_ftp.dat"
|
||||
tlsClient1Username = "client1"
|
||||
tlsClient2Username = "client2"
|
||||
httpFsPort = 23456
|
||||
defaultHTTPFsUsername = "httpfs_ftp_user"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -414,6 +416,7 @@ func TestMain(m *testing.M) {
|
|||
|
||||
waitTCPListening(ftpdConf.Bindings[0].GetAddress())
|
||||
waitNoConnections()
|
||||
startHTTPFs()
|
||||
|
||||
exitCode := m.Run()
|
||||
os.Remove(logFilePath)
|
||||
|
@ -595,6 +598,50 @@ func TestBasicFTPHandling(t *testing.T) {
|
|||
50*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestHTTPFs(t *testing.T) {
|
||||
u := getTestUserWithHTTPFs()
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
client, err := getFTPClient(user, true, nil)
|
||||
if assert.NoError(t, err) {
|
||||
err = checkBasicFTP(client)
|
||||
assert.NoError(t, err)
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(65535)
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||
assert.NoError(t, err)
|
||||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0)
|
||||
assert.NoError(t, err)
|
||||
// test a download resume
|
||||
data := []byte("test data")
|
||||
err = os.WriteFile(testFilePath, data, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0)
|
||||
assert.NoError(t, err)
|
||||
err = ftpDownloadFile(testFileName, localDownloadPath, int64(len(data)-5), client, 5)
|
||||
assert.NoError(t, err)
|
||||
readed, err := os.ReadFile(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("data"), readed, "readed data mismatch: %q", string(readed))
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
err = client.Quit()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool { return len(common.Connections.GetStats()) == 0 }, 1*time.Second, 50*time.Millisecond)
|
||||
assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond,
|
||||
50*time.Millisecond)
|
||||
}
|
||||
|
||||
func TestListDirWithWildcards(t *testing.T) {
|
||||
localUser, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -3422,6 +3469,18 @@ func getTestSFTPUser() dataprovider.User {
|
|||
return u
|
||||
}
|
||||
|
||||
func getTestUserWithHTTPFs() dataprovider.User {
|
||||
u := getTestUser()
|
||||
u.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
|
||||
Username: defaultHTTPFsUsername,
|
||||
},
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func getExtAuthScriptContent(user dataprovider.User, nonJSONResponse bool, username string) []byte {
|
||||
extAuthContent := []byte("#!/bin/sh\n\n")
|
||||
extAuthContent = append(extAuthContent, []byte(fmt.Sprintf("if test \"$SFTPGO_AUTHD_USERNAME\" = \"%v\"; then\n", user.Username))...)
|
||||
|
@ -3511,3 +3570,13 @@ func generateTOTPPasscode(secret string, algo otp.Algorithm) (string, error) {
|
|||
Algorithm: algo,
|
||||
})
|
||||
}
|
||||
|
||||
func startHTTPFs() {
|
||||
go func() {
|
||||
if err := httpdtest.StartTestHTTPFs(httpFsPort); err != nil {
|
||||
logger.ErrorToConsole("could not start HTTPfs test server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
waitTCPListening(fmt.Sprintf(":%d", httpFsPort))
|
||||
}
|
||||
|
|
|
@ -462,7 +462,7 @@ func (c *Connection) handleFTPUploadToExistingFile(fs vfs.Fs, flags int, resolve
|
|||
file.Seek(initialSize, io.SeekStart) //nolint:errcheck // for sftp seek simply set the offset
|
||||
}
|
||||
} else {
|
||||
if vfs.IsLocalOrSFTPFs(fs) {
|
||||
if vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
|
|
72
go.mod
72
go.mod
|
@ -8,16 +8,16 @@ require (
|
|||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.4
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.9
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.4
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.14
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.5
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.10
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.9
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.6
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.11
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.5
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.5
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.6
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.15
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.6
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.11
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.10
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.7
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.12
|
||||
github.com/coreos/go-oidc/v3 v3.2.0
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e
|
||||
|
@ -31,7 +31,7 @@ require (
|
|||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/grandcat/zeroconf v1.0.0
|
||||
github.com/hashicorp/go-hclog v1.2.0
|
||||
github.com/hashicorp/go-hclog v1.2.1
|
||||
github.com/hashicorp/go-plugin v1.4.4
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1
|
||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||
|
@ -50,13 +50,13 @@ require (
|
|||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/cors v1.8.2
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/rs/zerolog v1.26.2-0.20220505171737-a4ec5e4cdd4b
|
||||
github.com/sftpgo/sdk v0.1.1
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d
|
||||
github.com/shirou/gopsutil/v3 v3.22.5
|
||||
github.com/spf13/afero v1.8.2
|
||||
github.com/spf13/cobra v1.4.0
|
||||
github.com/spf13/viper v1.12.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
github.com/stretchr/testify v1.7.2
|
||||
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62
|
||||
github.com/unrolled/secure v1.10.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
|
@ -66,11 +66,11 @@ require (
|
|||
go.uber.org/automaxprocs v1.5.1
|
||||
gocloud.dev v0.25.0
|
||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93
|
||||
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306
|
||||
google.golang.org/api v0.82.0
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
|
||||
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
google.golang.org/api v0.83.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
)
|
||||
|
||||
|
@ -79,17 +79,17 @@ require (
|
|||
cloud.google.com/go/compute v1.6.1 // indirect
|
||||
cloud.google.com/go/iam v0.3.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.7 // indirect
|
||||
github.com/aws/smithy-go v1.11.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.8 // indirect
|
||||
github.com/aws/smithy-go v1.11.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||
|
@ -132,7 +132,7 @@ require (
|
|||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
|
@ -149,12 +149,12 @@ require (
|
|||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
|
||||
golang.org/x/tools v0.1.11 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac // indirect
|
||||
google.golang.org/grpc v1.47.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
|
@ -166,5 +166,5 @@ require (
|
|||
replace (
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220527053356-5e1caf8ed0e1
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20220603083515-6ce0d6be4d73
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20220609171611-ca8008b74f1f
|
||||
)
|
||||
|
|
142
go.sum
142
go.sum
|
@ -136,64 +136,67 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
|
|||
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.4 h1:swQTEQUyJF/UkEA94/Ga55miiKFoXmm/Zd67XHgmjSg=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.4/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 h1:SdK4Ppk5IzLs64ZMvr6MrSficMtjY2oS0WOORXTlxwU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.5 h1:Ah9h1TZD9E2S1LzHpViBO3Jz9FPL5+rmflmb8hXirtI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.5/go.mod h1:Wh7MEsmEApyL5hrWzpDkba4gwAPc5/piwLVLFnCxp48=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD2wJ9kCRTczA83gYbBmjSwZp3umc6zF4EeM=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.2 h1:LFOGNUQxc/8BlhA4FD+JdYjJKQK6tsz9Xiuh+GUTKAQ=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.2/go.mod h1:u/38zebMi809w7YFnqY/07Tw/FSs6DGhPD95Xiig7XQ=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.9 h1:TK5yNEnFDQ9iaO04gJS/3Y+eW8BioQiCUafW75/Wc3Q=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.9/go.mod h1:rv/l/TbZo67kp99v/3Kb0qV6Fm1KEtKyruEV2GvVfgs=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.10 h1:0HSMRNGlR0/WlGbeKC9DbBphBwRIK5H4cKUbgqNTKcA=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.10/go.mod h1:XL4DzwzWdwXBzKdwMdpLkMIaGEQCYRQyzA4UnJaUnNk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.4 h1:xggwS+qxCukXRVXJBJWQJGyUsvuxGC8+J1kKzv2cxuw=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.4/go.mod h1:7g+GGSp7xtR823o1jedxKmqRZGqLdoHQfI4eFasKKxs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.5 h1:WNNCUTWA0vyMy5t8LfS4iB7QshsW0DsHS/VdhyCGZWM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.5/go.mod h1:DOcdLlkqUiNGyXnjWgspC3eIAdXhj8q0pO1LiSvrTI4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnqSqz8O48LBYDSC+k6brng09jcMOk=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5 h1:YPxclBeE07HsLQE8vtjC8T2emcTjM9nzqsnDi2fv5UM=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5/go.mod h1:WAPnuhG5IQ/i6DETFl5NmX3kKqCzw7aau9NHAGcm4QE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.6 h1:+NZzDh/RpcQTpo9xMFUgkseIam6PC+YJbdhbQp1NOXI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.6/go.mod h1:ClLMcuQA/wcHPmOIfNzNI4Y1Q0oDbmEkbYhMFOzHDh8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.14 h1:qpJmFbypCfwPok5PGTSnQy1NKbv4Hn8xGsee9l4xOPE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.14/go.mod h1:IOYB+xOZik8YgdTlnDSwbvKmCkikA3nVue8/Qnfzs0c=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.15 h1:WrTFSORSXKw+ZNV5CJnQjHgACSsteMyq2Oy9psCxtl4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.15/go.mod h1:t/cWdEpu8thFU8Gv3SQnDiRq+g5heJPcHtrCbpUZR4E=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 h1:gsqHplNh1DaQunEKZISK56wlpbCg0yKxNVvGWCFuF1k=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11/go.mod h1:tmUB6jakq5DFNcXsXOA/ZQ7/C8VnSKYkx58OI7Fh79g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12 h1:Zt7DDk5V7SyQULUUwIKzsROtVzp/kVvcz15uQx/Tkow=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.12/go.mod h1:Afj/U8svX6sJ77Q+FPWMzabJ9QjbwP32YlopgKALUpg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.3/go.mod h1:ssOhaLpRlh88H3UmEcsBoVKq309quMvm3Ds8e9d4eJM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 h1:PLFj+M2PgIDHG//hw3T0O0KLI4itVtAjtxrZx4AHPLg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5/go.mod h1:fV1AaS2gFc1tM0RCb015FJ0pvWVUfJZANzjwoO4YakM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6 h1:eeXdGVtXEe+2Jc49+/vAzna3FAQnUD4AagAw8tzbmfc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.6/go.mod h1:FwpAKI+FBPIELJIdmQzlLtRe8LQSOreMcM2wBsPMvvc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.10/go.mod h1:8DcYQcz0+ZJaSxANlHIsbbi6S+zMwjwdDqwW3r9AzaE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 h1:j0VqrjtgsY1Bx27tD0ysay36/K4kFMWRp9K3ieO9nLU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12/go.mod h1:00c7+ALdPh4YeEUPXJzyU0Yy01nPGOq2+9rUaz05z9g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.2 h1:1fs9WkbFcMawQjxEI0B5L0SqvBhJZebxWM6Z3x/qHWY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.2/go.mod h1:0jDVeWUFPbI3sOfsXXAsIdiawXcn7VBLx/IlFVTRP64=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1 h1:T4pFel53bkHjL2mMo+4DKE6r6AuoZnM0fg7k1/ratr4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.13 h1:L/l0WbIpIadRO7i44jZh1/XeXpNDX0sokFppb4ZnXUI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.13/go.mod h1:hiM/y1XPp3DoEPhoVEYc/CZcS58dP6RKJRDFp99wdX0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.3 h1:m1vDVDoNK4tZAoWtcetHopEdIeUlrNNpdLZ7cwZke6s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.3/go.mod h1:annFthsb7FiHQd5X9wKDNst9OJvVFY0l0LjQ8zQniJA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.1/go.mod h1:GeUru+8VzrTXV/83XyMJ80KpH8xO89VPoUileyNQ+tc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2 h1:T/ywkX1ed+TsZVQccu/8rRJGxKZF/t0Ivgrb4MHTSeo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.2/go.mod h1:RnloUnyZ4KN9JStGY1LuQ7Wzqh7V0f8FinmRdHYtuaA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.3/go.mod h1:Seb8KNmD6kVTjwRjVEgOT5hPin6sq+v4C2ycJQDwuH8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.6 h1:9mvDAsMiN+07wcfGM+hJ1J3dOKZ2YOpDiPZ6ufRJcgw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.6/go.mod h1:Eus+Z2iBIEfhOvhSdMTcscNOMy6n3X9/BJV0Zgax98w=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.7 h1:DYUAx8lWAhIzFiD284oq6RUPKppKk3cyqv/hyUkbWuA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.7/go.mod h1:6tcs0yjwAW2Z9Yb3Z4X/2tm3u9jNox1dvXxVXTd73Zw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.3/go.mod h1:wlY6SVjuwvh3TVRpTqdy4I1JpBFLX4UGeKZdWntaocw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 h1:gRW1ZisKc93EWEORNJRvy/ZydF3o6xLSveJHdi1Oa0U=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5/go.mod h1:ZbkttHXaVn3bBo/wpJbQGiiIWR90eTBUVBrEHUEQlho=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.6 h1:0ZxYAZ1cn7Swi/US55VKciCE6RhRHIwCKIWaMLdT6pg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.6/go.mod h1:DxAPjquoEHf3rUHh1b9+47RAaXB8/7cB6jkzCt/GOEI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.3/go.mod h1:Bm/v2IaN6rZ+Op7zX+bOUMdL4fsrYZiD0dsjLhNKwZc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.5 h1:DyPYkrH4R2zn+Pdu6hM3VTuPsQYAE6x2WB24X85Sgw0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.5/go.mod h1:XtL92YWo0Yq80iN3AgYRERJqohg4TozrqRlxYhHGJ7g=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.6 h1:SSrqxZVhrO371eg/C8Fnj6kduzltKHj/mJl2swkTBGc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.6/go.mod h1:TzDyqDka0783D93yVirkcysbibVRxjX5HFJEWms4kKA=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.16.3/go.mod h1:QuiHPBqlOFCi4LqdSskYYAWpQlx3PKmohy+rE2F+o5g=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.5 h1:CvRAsgxd1BN5l961+xXfS0mEhhyJTMxqdoWpZQIJZt4=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.5/go.mod h1:tnwCNkQvihXdRZ8Fyita7EJ0IeY46DcJWhgcWaquT+o=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.6 h1:QHv9AYaolQo8Tj+PIfIizQ/aD/EHrb7eOlNpeuEKyEU=
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.6/go.mod h1:M8WgODCojJa7pJRL7vx2bS4NO+NjcRtlvGDr9ls/MAI=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.3/go.mod h1:g1qvDuRsJY+XghsV6zg00Z4KJ7DtFFCx8fJD2a491Ak=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.10 h1:GWdLZK0r1AK5sKb8rhB9bEXqXCK8WNuyv4TBAD6ZviQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.10/go.mod h1:+O7qJxF8nLorAhuIVhYTHse6okjHJJm4EwhhzvpnkT0=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.11 h1:Wt0512f6GfLiMd6a+NuOCC9r3/trmzHMTB697CBDUwg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.26.11/go.mod h1:VMTprbiZWqW44viXgPSQhWdeZ8JTAeJwhO7OXpC/Rsg=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.4/go.mod h1:PJc8s+lxyU8rrre0/4a0pn2wgwiDvOEzoOjcJUBr67o=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.9 h1:a7+ZYQbKAziY5a7H8Ggwp/6HM9UKT6h9al+QHY+P6jI=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.9/go.mod h1:Jt1lSw1fYlQ60lqrZ9ViN2LMGizbWTWbkStm4rbuYuE=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.10 h1:quGsZJn6aaTtmplz+AwPSukYWuD6LFJiQJZmD8M+YPk=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.10/go.mod h1:pgtQihVJw8OxQCkC4BmJOuVWT52mBTaj8LcsF5Kr9iA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.17.4/go.mod h1:kElt+uCcXxcqFyc+bQqZPFD9DME/eC6oHBXvFzQ9Bcw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.18.3/go.mod h1:skmQo0UPvsjsuYYSYMVmrPc1HWCbHUJyrCEp+ZaLzqM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.24.1/go.mod h1:NR/xoKjdbRJ+qx0pMR4mI+N/H1I1ynHwXnO6FowXJc0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.3/go.mod h1:7UQ/e69kU7LDPtY40OyoHYgRmgfGM4mgsLYtcObdveU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.7 h1:suAGD+RyiHWPPihZzY+jw4mCZlOFWgmdjb2AeTenz7c=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.7/go.mod h1:TFVe6Rr2joVLsYQ1ABACXgOC6lXip/qpX2x5jWg/A9w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.8 h1:GNIdO14AHW5CgnzMml3Tg5Fy/+NqPQvnh1HsC1zpcPo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.8/go.mod h1:UqRD9bBt15P0ofRyDZX6CfsIqPpzeHOhZKWzgSuAzpo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.3/go.mod h1:bfBj0iVmsUyUg4weDB4NxktD9rDGeKSVWnjTnwbx9b8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.6 h1:aYToU0/iazkMY67/BYLt3r6/LT/mUtarLAF5mGof1Kg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.6/go.mod h1:rP1rEOKAGZoXp4iGDxSXFvODAtXpm34Egf0lL0eshaQ=
|
||||
github.com/aws/smithy-go v1.11.2 h1:eG/N+CcUMAvsdffgMvjMKwfyDzIkjM6pfxMJ8Mzc6mE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.7 h1:HLzjwQM9975FQWSF3uENDGHT1gFQm/q3QXu2BYIcI08=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.7/go.mod h1:lVxTdiiSHY3jb1aeg+BBFtDzZGSUCv6qaNOyEGCJ1AY=
|
||||
github.com/aws/smithy-go v1.11.2/go.mod h1:3xHYmszWVx2c0kIwQeEVf9uSm4fYZt67FBJnwub1bgM=
|
||||
github.com/aws/smithy-go v1.11.3 h1:DQixirEFM9IaKxX1olZ3ke3nvxRS2xMDteKIDWxozW8=
|
||||
github.com/aws/smithy-go v1.11.3/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
|
@ -226,8 +229,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
|
|||
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.11 h1:gddwKS4W+zxfZdA0/dEMMjiruiQCCrG2iRbk0c1T13Y=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.11/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.12 h1:yGneJ5OvdtAky2nD5BKTKmIqrcUlbrEIJ1ILHirnn3o=
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.12/go.mod h1:xZ2VHjUEb/cySv0scXBx7YsBnHtLHkR1+w/w73b5i3M=
|
||||
github.com/coreos/go-oidc/v3 v3.2.0 h1:2eR2MGR7thBXSQ2YbODlF0fcmgtliLCfr9iX6RW11fc=
|
||||
github.com/coreos/go-oidc/v3 v3.2.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
|
@ -256,8 +259,8 @@ github.com/drakkan/crypto v0.0.0-20220527053356-5e1caf8ed0e1 h1:2sXHktgJwUnVAHbt
|
|||
github.com/drakkan/crypto v0.0.0-20220527053356-5e1caf8ed0e1/go.mod h1:SiM6ypd8Xu1xldObYtbDztuUU7xUzMnUULfphXFZmro=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||
github.com/drakkan/net v0.0.0-20220603083515-6ce0d6be4d73 h1:/+VzNg0b5DfRn5mMwgKfRI9fQv+Ak66i5rG7OmGaP30=
|
||||
github.com/drakkan/net v0.0.0-20220603083515-6ce0d6be4d73/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
github.com/drakkan/net v0.0.0-20220609171611-ca8008b74f1f h1:DyQRIok4cgDwQUJd1E+KN53ANLdirNPqmx8ewqFN77U=
|
||||
github.com/drakkan/net v0.0.0-20220609171611-ca8008b74f1f/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
|
@ -270,7 +273,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
|
|||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fclairamb/ftpserverlib v0.18.1-0.20220515214847-f96d31ec626e h1:D7/to1KmKRTTRQyExulywEVYKhB+/WOW3gqiKimrbXg=
|
||||
|
@ -448,8 +450,8 @@ github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
|
|||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
|
||||
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
|
||||
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
|
||||
github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ=
|
||||
|
@ -584,7 +586,6 @@ github.com/lufia/plan9stats v0.0.0-20220517141722-cf486979b281/go.mod h1:lc+czkg
|
|||
github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo=
|
||||
github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
|
@ -593,8 +594,6 @@ github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/
|
|||
github.com/mattn/go-ieproxy v0.0.3/go.mod h1:6ZpRmhBaYuBX1U2za+9rC9iCGLsSp2tftelZne7CPko=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
|
@ -638,8 +637,8 @@ github.com/otiai10/mint v1.3.3 h1:7JgpsBaN0uMkyju4tbYHu0mnM55hNKVYLsXmwr15NQI=
|
|||
github.com/otiai10/mint v1.3.3/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1 h1:8e3L2cCQzLFi2CR4g7vGFuFxX7Jl1kKX8gW+iV0GUKU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw=
|
||||
github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI=
|
||||
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
|
||||
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
||||
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
|
||||
|
@ -696,14 +695,14 @@ github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
|||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/rs/zerolog v1.26.2-0.20220505171737-a4ec5e4cdd4b h1:wKjeedusHurN46dp/9kF0JLBh3YO54lu5juBX1oqJWE=
|
||||
github.com/rs/zerolog v1.26.2-0.20220505171737-a4ec5e4cdd4b/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/sftpgo/sdk v0.1.1 h1:3vGGmRWLr+1vp+Z7OJG2LHt/u9MjTs3odZZtUcbfAsQ=
|
||||
github.com/sftpgo/sdk v0.1.1/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d h1:gpshxOhLsGFbCy4ke96X8FVMy4xvXZQChSF7dikqxp4=
|
||||
github.com/sftpgo/sdk v0.1.2-0.20220611083241-b653555f7f4d/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
|
||||
github.com/shirou/gopsutil/v3 v3.22.5 h1:atX36I/IXgFiB81687vSiBI5zrMsxcIBkP9cQMJQoJA=
|
||||
github.com/shirou/gopsutil/v3 v3.22.5/go.mod h1:so9G9VzeHt/hsd0YwqprnjHnfARAUktauykSbr+y2gA=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
|
@ -735,8 +734,9 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
|||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62 h1:b2nJXyPCa9HY7giGM+kYcnQ71m14JnGdQabMPmyt++8=
|
||||
github.com/studio-b12/gowebdav v0.0.0-20220128162035-c7b1ff8a5e62/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
|
||||
github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs=
|
||||
|
@ -835,8 +835,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -858,8 +858,9 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ
|
|||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401 h1:zwrSfklXn0gxyLRX/aR+q6cgHbV/ItVyzbPlbA+dkAw=
|
||||
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb h1:8tDJ3aechhddbdPAxpycgXHJRMLpk/Ab+aa4OgdN5/g=
|
||||
golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -871,8 +872,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4=
|
||||
golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8=
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -890,7 +891,6 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -956,8 +956,9 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s=
|
||||
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
@ -976,8 +977,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb
|
|||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
|
||||
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
|
@ -1040,8 +1041,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
|||
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20=
|
||||
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
|
||||
golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY=
|
||||
golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
@ -1049,8 +1050,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df h1:5Pf6pFKu98ODmgnpvkJ3kFUOQGGLIzLIkbzUHp47618=
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0=
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
|
@ -1096,8 +1098,8 @@ google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRR
|
|||
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
|
||||
google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw=
|
||||
google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg=
|
||||
google.golang.org/api v0.82.0 h1:h6EGeZuzhoKSS7BUznzkW+2wHZ+4Ubd6rsVvvh3dRkw=
|
||||
google.golang.org/api v0.82.0/go.mod h1:Ld58BeTlL9DIYr2M2ajvoSqmGLei0BMn+kVBmkam1os=
|
||||
google.golang.org/api v0.83.0 h1:pMvST+6v+46Gabac4zlJlalxZjCeRcepwg2EdBU+nCc=
|
||||
google.golang.org/api v0.83.0/go.mod h1:CNywQoj/AfhTw26ZWAa6LwOv+6WFxHmeLPZq2uncLZk=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
@ -1199,9 +1201,9 @@ google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX
|
|||
google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
|
||||
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8 h1:qRu95HZ148xXw+XeZ3dvqe85PxH4X8+jIo0iRPKcEnM=
|
||||
google.golang.org/genproto v0.0.0-20220602131408-e326c6e8e9c8/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac h1:ByeiW1F67iV9o8ipGskA+HWzSkMbRJuKLlwCdPxzn7A=
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
|
|
@ -68,12 +68,15 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
|
|||
currentSFTPPassword := folder.FsConfig.SFTPConfig.Password
|
||||
currentSFTPKey := folder.FsConfig.SFTPConfig.PrivateKey
|
||||
currentSFTPKeyPassphrase := folder.FsConfig.SFTPConfig.KeyPassphrase
|
||||
currentHTTPPassword := folder.FsConfig.HTTPConfig.Password
|
||||
currentHTTPAPIKey := folder.FsConfig.HTTPConfig.APIKey
|
||||
|
||||
folder.FsConfig.S3Config = vfs.S3FsConfig{}
|
||||
folder.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{}
|
||||
folder.FsConfig.GCSConfig = vfs.GCSFsConfig{}
|
||||
folder.FsConfig.CryptConfig = vfs.CryptFsConfig{}
|
||||
folder.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
|
||||
folder.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
|
||||
err = render.DecodeJSON(r.Body, &folder)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
|
@ -83,7 +86,8 @@ func updateFolder(w http.ResponseWriter, r *http.Request) {
|
|||
folder.Name = name
|
||||
folder.FsConfig.SetEmptySecretsIfNil()
|
||||
updateEncryptedSecrets(&folder.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl, currentGCSCredentials,
|
||||
currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase)
|
||||
currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase, currentHTTPPassword,
|
||||
currentHTTPAPIKey)
|
||||
err = dataprovider.UpdateFolder(&folder, users, groups, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
|
|
|
@ -73,12 +73,15 @@ func updateGroup(w http.ResponseWriter, r *http.Request) {
|
|||
currentSFTPPassword := group.UserSettings.FsConfig.SFTPConfig.Password
|
||||
currentSFTPKey := group.UserSettings.FsConfig.SFTPConfig.PrivateKey
|
||||
currentSFTPKeyPassphrase := group.UserSettings.FsConfig.SFTPConfig.KeyPassphrase
|
||||
currentHTTPPassword := group.UserSettings.FsConfig.HTTPConfig.Password
|
||||
currentHTTPAPIKey := group.UserSettings.FsConfig.HTTPConfig.APIKey
|
||||
|
||||
group.UserSettings.FsConfig.S3Config = vfs.S3FsConfig{}
|
||||
group.UserSettings.FsConfig.AzBlobConfig = vfs.AzBlobFsConfig{}
|
||||
group.UserSettings.FsConfig.GCSConfig = vfs.GCSFsConfig{}
|
||||
group.UserSettings.FsConfig.CryptConfig = vfs.CryptFsConfig{}
|
||||
group.UserSettings.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
|
||||
group.UserSettings.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
|
||||
err = render.DecodeJSON(r.Body, &group)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
|
@ -88,7 +91,8 @@ func updateGroup(w http.ResponseWriter, r *http.Request) {
|
|||
group.Name = name
|
||||
group.UserSettings.FsConfig.SetEmptySecretsIfNil()
|
||||
updateEncryptedSecrets(&group.UserSettings.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
|
||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase)
|
||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
|
||||
currentHTTPPassword, currentHTTPAPIKey)
|
||||
err = dataprovider.UpdateGroup(&group, users, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
|
|
|
@ -135,6 +135,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
currentSFTPPassword := user.FsConfig.SFTPConfig.Password
|
||||
currentSFTPKey := user.FsConfig.SFTPConfig.PrivateKey
|
||||
currentSFTPKeyPassphrase := user.FsConfig.SFTPConfig.KeyPassphrase
|
||||
currentHTTPPassword := user.FsConfig.HTTPConfig.Password
|
||||
currentHTTPAPIKey := user.FsConfig.HTTPConfig.APIKey
|
||||
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.FsConfig.S3Config = vfs.S3FsConfig{}
|
||||
|
@ -142,6 +144,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
user.FsConfig.GCSConfig = vfs.GCSFsConfig{}
|
||||
user.FsConfig.CryptConfig = vfs.CryptFsConfig{}
|
||||
user.FsConfig.SFTPConfig = vfs.SFTPFsConfig{}
|
||||
user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{}
|
||||
user.Filters.TOTPConfig = dataprovider.UserTOTPConfig{}
|
||||
user.Filters.RecoveryCodes = nil
|
||||
user.VirtualFolders = nil
|
||||
|
@ -160,7 +163,8 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
user.Permissions = currentPermissions
|
||||
}
|
||||
updateEncryptedSecrets(&user.FsConfig, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
|
||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase)
|
||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
|
||||
currentHTTPPassword, currentHTTPAPIKey)
|
||||
err = dataprovider.UpdateUser(&user, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
|
@ -232,7 +236,8 @@ func disconnectUser(username string) {
|
|||
}
|
||||
|
||||
func updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentS3AccessSecret, currentAzAccountKey, currentAzSASUrl,
|
||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey *kms.Secret, currentSFTPKeyPassphrase *kms.Secret) {
|
||||
currentGCSCredentials, currentCryptoPassphrase, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase,
|
||||
currentHTTPPassword, currentHTTPAPIKey *kms.Secret) {
|
||||
// we use the new access secret if plain or empty, otherwise the old value
|
||||
switch fsConfig.Provider {
|
||||
case sdk.S3FilesystemProvider:
|
||||
|
@ -257,6 +262,15 @@ func updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentS3AccessSecret, cur
|
|||
fsConfig.CryptConfig.Passphrase = currentCryptoPassphrase
|
||||
}
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
updateSFTPFsEncryptedSecrets(fsConfig, currentSFTPPassword, currentSFTPKey, currentSFTPKeyPassphrase)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
updateHTTPFsEncryptedSecrets(fsConfig, currentHTTPPassword, currentHTTPAPIKey)
|
||||
}
|
||||
}
|
||||
|
||||
func updateSFTPFsEncryptedSecrets(fsConfig *vfs.Filesystem, currentSFTPPassword, currentSFTPKey,
|
||||
currentSFTPKeyPassphrase *kms.Secret,
|
||||
) {
|
||||
if fsConfig.SFTPConfig.Password.IsNotPlainAndNotEmpty() {
|
||||
fsConfig.SFTPConfig.Password = currentSFTPPassword
|
||||
}
|
||||
|
@ -267,4 +281,12 @@ func updateEncryptedSecrets(fsConfig *vfs.Filesystem, currentS3AccessSecret, cur
|
|||
fsConfig.SFTPConfig.KeyPassphrase = currentSFTPKeyPassphrase
|
||||
}
|
||||
}
|
||||
|
||||
func updateHTTPFsEncryptedSecrets(fsConfig *vfs.Filesystem, currentHTTPPassword, currentHTTPAPIKey *kms.Secret) {
|
||||
if fsConfig.HTTPConfig.Password.IsNotPlainAndNotEmpty() {
|
||||
fsConfig.HTTPConfig.Password = currentHTTPPassword
|
||||
}
|
||||
if fsConfig.HTTPConfig.APIKey.IsNotPlainAndNotEmpty() {
|
||||
fsConfig.HTTPConfig.APIKey = currentHTTPAPIKey
|
||||
}
|
||||
}
|
||||
|
|
|
@ -196,7 +196,7 @@ func (c *Connection) handleUploadFile(fs vfs.Fs, resolvedPath, filePath, request
|
|||
initialSize := int64(0)
|
||||
truncatedSize := int64(0) // bytes truncated and not included in quota
|
||||
if !isNewFile {
|
||||
if vfs.IsLocalOrSFTPFs(fs) {
|
||||
if vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
|
|
|
@ -2409,6 +2409,26 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
|
|||
if assert.NoError(t, err) {
|
||||
assert.Contains(t, string(resp), "invalid buffer_size")
|
||||
}
|
||||
|
||||
u = getTestUser()
|
||||
u.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
u.FsConfig.HTTPConfig.Endpoint = "http://foo\x7f.com/"
|
||||
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Contains(t, string(resp), "invalid endpoint")
|
||||
}
|
||||
u.FsConfig.HTTPConfig.Endpoint = "http://127.0.0.1:9999/api/v1"
|
||||
u.FsConfig.HTTPConfig.Password = kms.NewSecret(sdkkms.SecretStatusSecretBox, "", "", "")
|
||||
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Contains(t, string(resp), "invalid encrypted password")
|
||||
}
|
||||
u.FsConfig.HTTPConfig.Password = nil
|
||||
u.FsConfig.HTTPConfig.APIKey = kms.NewSecret(sdkkms.SecretStatusRedacted, redactedSecret, "", "")
|
||||
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Contains(t, string(resp), "cannot save a user with a redacted secret")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserRedactedPassword(t *testing.T) {
|
||||
|
@ -3247,6 +3267,74 @@ func TestUserS3Config(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPFsConfig(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
user.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: "http://127.0.0.1/httpfs",
|
||||
Username: defaultUsername,
|
||||
},
|
||||
Password: kms.NewPlainSecret(defaultPassword),
|
||||
APIKey: kms.NewPlainSecret(defaultTokenAuthUser),
|
||||
}
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
initialPwdPayload := user.FsConfig.HTTPConfig.Password.GetPayload()
|
||||
initialAPIKeyPayload := user.FsConfig.HTTPConfig.APIKey.GetPayload()
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.NotEmpty(t, initialPwdPayload)
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())
|
||||
assert.NotEmpty(t, initialAPIKeyPayload)
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())
|
||||
user.FsConfig.HTTPConfig.Password.SetStatus(sdkkms.SecretStatusSecretBox)
|
||||
user.FsConfig.HTTPConfig.Password.SetAdditionalData(util.GenerateUniqueID())
|
||||
user.FsConfig.HTTPConfig.Password.SetKey(util.GenerateUniqueID())
|
||||
user.FsConfig.HTTPConfig.APIKey.SetStatus(sdkkms.SecretStatusSecretBox)
|
||||
user.FsConfig.HTTPConfig.APIKey.SetAdditionalData(util.GenerateUniqueID())
|
||||
user.FsConfig.HTTPConfig.APIKey.SetKey(util.GenerateUniqueID())
|
||||
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.Equal(t, initialPwdPayload, user.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())
|
||||
assert.Equal(t, initialAPIKeyPayload, user.FsConfig.HTTPConfig.APIKey.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
// also test AddUser
|
||||
u := getTestUser()
|
||||
u.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: "http://127.0.0.1/httpfs",
|
||||
Username: defaultUsername,
|
||||
},
|
||||
Password: kms.NewPlainSecret(defaultPassword),
|
||||
APIKey: kms.NewPlainSecret(defaultTokenAuthUser),
|
||||
}
|
||||
user, _, err = httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.NotEmpty(t, user.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, user.FsConfig.HTTPConfig.APIKey.GetStatus())
|
||||
assert.NotEmpty(t, user.FsConfig.HTTPConfig.APIKey.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.HTTPConfig.APIKey.GetKey())
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserAzureBlobConfig(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -3494,7 +3582,7 @@ func TestUserSFTPFs(t *testing.T) {
|
|||
|
||||
func TestUserHiddenFields(t *testing.T) {
|
||||
// sensitive data must be hidden but not deleted from the dataprovider
|
||||
usernames := []string{"user1", "user2", "user3", "user4", "user5"}
|
||||
usernames := []string{"user1", "user2", "user3", "user4", "user5", "user6"}
|
||||
u1 := getTestUser()
|
||||
u1.Username = usernames[0]
|
||||
u1.FsConfig.Provider = sdk.S3FilesystemProvider
|
||||
|
@ -3542,9 +3630,23 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
user5, _, err := httpdtest.AddUser(u5, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
u6 := getTestUser()
|
||||
u6.Username = usernames[5]
|
||||
u6.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
u6.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: "http://127.0.0.1/api/v1",
|
||||
Username: defaultUsername,
|
||||
},
|
||||
Password: kms.NewPlainSecret(defaultPassword),
|
||||
APIKey: kms.NewPlainSecret(defaultTokenAuthUser),
|
||||
}
|
||||
user6, _, err := httpdtest.AddUser(u6, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
users, _, err := httpdtest.GetUsers(0, 0, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(users), 5)
|
||||
assert.GreaterOrEqual(t, len(users), 6)
|
||||
for _, username := range usernames {
|
||||
user, _, err := httpdtest.GetUserByUsername(username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -3595,6 +3697,14 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
assert.NotEmpty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetPayload())
|
||||
assert.Equal(t, "/prefix", user5.FsConfig.SFTPConfig.Prefix)
|
||||
|
||||
user6, _, err = httpdtest.GetUserByUsername(user6.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, user6.Password)
|
||||
assert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.APIKey.GetStatus())
|
||||
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.APIKey.GetPayload())
|
||||
|
||||
// finally check that we have all the data inside the data provider
|
||||
user1, err = dataprovider.UserExists(user1.Username)
|
||||
assert.NoError(t, err)
|
||||
|
@ -3676,6 +3786,20 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
assert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetKey())
|
||||
assert.Empty(t, user5.FsConfig.SFTPConfig.PrivateKey.GetAdditionalData())
|
||||
|
||||
user6, err = dataprovider.UserExists(user6.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, user6.Password)
|
||||
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.NotEmpty(t, user6.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
err = user6.FsConfig.HTTPConfig.Password.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sdkkms.SecretStatusPlain, user6.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.Equal(t, u6.FsConfig.HTTPConfig.Password.GetPayload(), user6.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
assert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Empty(t, user6.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
|
||||
// update the GCS user and check that the credentials are preserved
|
||||
user2.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
|
||||
user2.FsConfig.GCSConfig.ACL = "private"
|
||||
|
@ -3700,6 +3824,8 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user5, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveUser(user6, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSecretObject(t *testing.T) {
|
||||
|
@ -16951,6 +17077,125 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebUserHTTPFsMock(t *testing.T) {
|
||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
|
||||
assert.NoError(t, err)
|
||||
user := getTestUser()
|
||||
userAsJSON := getUserAsJSON(t, user)
|
||||
req, err := http.NewRequest(http.MethodPost, userPath, bytes.NewBuffer(userAsJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
err = render.DecodeJSON(rr.Body, &user)
|
||||
assert.NoError(t, err)
|
||||
user.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
user.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: "https://127.0.0.1:9999/api/v1",
|
||||
Username: defaultUsername,
|
||||
SkipTLSVerify: true,
|
||||
},
|
||||
Password: kms.NewPlainSecret(defaultPassword),
|
||||
APIKey: kms.NewPlainSecret(defaultTokenAuthPass),
|
||||
}
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
form.Set("password", redactedSecret)
|
||||
form.Set("home_dir", user.HomeDir)
|
||||
form.Set("uid", "0")
|
||||
form.Set("gid", strconv.FormatInt(int64(user.GID), 10))
|
||||
form.Set("max_sessions", strconv.FormatInt(int64(user.MaxSessions), 10))
|
||||
form.Set("quota_size", strconv.FormatInt(user.QuotaSize, 10))
|
||||
form.Set("quota_files", strconv.FormatInt(int64(user.QuotaFiles), 10))
|
||||
form.Set("upload_bandwidth", "0")
|
||||
form.Set("download_bandwidth", "0")
|
||||
form.Set("upload_data_transfer", "0")
|
||||
form.Set("download_data_transfer", "0")
|
||||
form.Set("total_data_transfer", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("status", strconv.Itoa(user.Status))
|
||||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("allowed_ip", "")
|
||||
form.Set("denied_ip", "")
|
||||
form.Set("fs_provider", "6")
|
||||
form.Set("http_endpoint", user.FsConfig.HTTPConfig.Endpoint)
|
||||
form.Set("http_username", user.FsConfig.HTTPConfig.Username)
|
||||
form.Set("http_password", user.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
form.Set("http_api_key", user.FsConfig.HTTPConfig.APIKey.GetPayload())
|
||||
form.Set("http_skip_tls_verify", "checked")
|
||||
form.Set("pattern_path0", "/dir1")
|
||||
form.Set("patterns0", "*.jpg,*.png")
|
||||
form.Set("pattern_type0", "allowed")
|
||||
form.Set("pattern_path1", "/dir2")
|
||||
form.Set("patterns1", "*.zip")
|
||||
form.Set("pattern_type1", "denied")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
// check the updated user
|
||||
req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
var updateUser dataprovider.User
|
||||
err = render.DecodeJSON(rr.Body, &updateUser)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1577836800000), updateUser.ExpirationDate)
|
||||
assert.Equal(t, 2, len(updateUser.Filters.FilePatterns))
|
||||
assert.Equal(t, user.FsConfig.HTTPConfig.Endpoint, updateUser.FsConfig.HTTPConfig.Endpoint)
|
||||
assert.Equal(t, user.FsConfig.HTTPConfig.Username, updateUser.FsConfig.HTTPConfig.Username)
|
||||
assert.Equal(t, user.FsConfig.HTTPConfig.SkipTLSVerify, updateUser.FsConfig.HTTPConfig.SkipTLSVerify)
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.NotEmpty(t, updateUser.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
assert.Empty(t, updateUser.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Empty(t, updateUser.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, updateUser.FsConfig.HTTPConfig.APIKey.GetStatus())
|
||||
assert.NotEmpty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetPayload())
|
||||
assert.Empty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetKey())
|
||||
assert.Empty(t, updateUser.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
|
||||
// now check that a redacted password is not saved
|
||||
form.Set("http_password", " "+redactedSecret+" ")
|
||||
form.Set("http_api_key", redactedSecret)
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
req, _ = http.NewRequest(http.MethodGet, path.Join(userPath, user.Username), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
var lastUpdatedUser dataprovider.User
|
||||
err = render.DecodeJSON(rr.Body, &lastUpdatedUser)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.Equal(t, updateUser.FsConfig.HTTPConfig.Password.GetPayload(), lastUpdatedUser.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetStatus())
|
||||
assert.Equal(t, updateUser.FsConfig.HTTPConfig.APIKey.GetPayload(), lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetPayload())
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetKey())
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
|
||||
|
||||
req, err = http.NewRequest(http.MethodDelete, path.Join(userPath, user.Username), nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
}
|
||||
|
||||
func TestWebUserAzureBlobMock(t *testing.T) {
|
||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
|
@ -17623,6 +17868,101 @@ func TestAddWebFoldersMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
}
|
||||
|
||||
func TestHTTPFsWebFolderMock(t *testing.T) {
|
||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
apiToken, err := getJWTAPITokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
|
||||
assert.NoError(t, err)
|
||||
mappedPath := filepath.Clean(os.TempDir())
|
||||
folderName := filepath.Base(mappedPath)
|
||||
httpfsConfig := vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: "https://127.0.0.1:9998/api/v1",
|
||||
Username: folderName,
|
||||
SkipTLSVerify: true,
|
||||
},
|
||||
Password: kms.NewPlainSecret(defaultPassword),
|
||||
APIKey: kms.NewPlainSecret(defaultTokenAuthPass),
|
||||
}
|
||||
form := make(url.Values)
|
||||
form.Set("mapped_path", mappedPath)
|
||||
form.Set("name", folderName)
|
||||
form.Set("fs_provider", "6")
|
||||
form.Set("http_endpoint", httpfsConfig.Endpoint)
|
||||
form.Set("http_username", "%name%")
|
||||
form.Set("http_password", httpfsConfig.Password.GetPayload())
|
||||
form.Set("http_api_key", httpfsConfig.APIKey.GetPayload())
|
||||
form.Set("http_skip_tls_verify", "checked")
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
b, contentType, err := getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, webFolderPath, &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
// check
|
||||
var folder vfs.BaseVirtualFolder
|
||||
req, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
err = render.DecodeJSON(rr.Body, &folder)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, mappedPath, folder.MappedPath)
|
||||
assert.Equal(t, folderName, folder.Name)
|
||||
assert.Equal(t, sdk.HTTPFilesystemProvider, folder.FsConfig.Provider)
|
||||
assert.Equal(t, httpfsConfig.Endpoint, folder.FsConfig.HTTPConfig.Endpoint)
|
||||
assert.Equal(t, httpfsConfig.Username, folder.FsConfig.HTTPConfig.Username)
|
||||
assert.Equal(t, httpfsConfig.SkipTLSVerify, folder.FsConfig.HTTPConfig.SkipTLSVerify)
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.NotEmpty(t, folder.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
assert.Empty(t, folder.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Empty(t, folder.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, folder.FsConfig.HTTPConfig.APIKey.GetStatus())
|
||||
assert.NotEmpty(t, folder.FsConfig.HTTPConfig.APIKey.GetPayload())
|
||||
assert.Empty(t, folder.FsConfig.HTTPConfig.APIKey.GetKey())
|
||||
assert.Empty(t, folder.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
|
||||
// update
|
||||
form.Set("http_password", redactedSecret)
|
||||
form.Set("http_api_key", redactedSecret)
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), &b)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
// check
|
||||
var updateFolder vfs.BaseVirtualFolder
|
||||
req, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
err = render.DecodeJSON(rr.Body, &updateFolder)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, mappedPath, updateFolder.MappedPath)
|
||||
assert.Equal(t, folderName, updateFolder.Name)
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, updateFolder.FsConfig.HTTPConfig.Password.GetStatus())
|
||||
assert.Equal(t, folder.FsConfig.HTTPConfig.Password.GetPayload(), updateFolder.FsConfig.HTTPConfig.Password.GetPayload())
|
||||
assert.Empty(t, updateFolder.FsConfig.HTTPConfig.Password.GetKey())
|
||||
assert.Empty(t, updateFolder.FsConfig.HTTPConfig.Password.GetAdditionalData())
|
||||
assert.Equal(t, sdkkms.SecretStatusSecretBox, updateFolder.FsConfig.HTTPConfig.APIKey.GetStatus())
|
||||
assert.Equal(t, folder.FsConfig.HTTPConfig.APIKey.GetPayload(), updateFolder.FsConfig.HTTPConfig.APIKey.GetPayload())
|
||||
assert.Empty(t, updateFolder.FsConfig.HTTPConfig.APIKey.GetKey())
|
||||
assert.Empty(t, updateFolder.FsConfig.HTTPConfig.APIKey.GetAdditionalData())
|
||||
|
||||
// cleanup
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
}
|
||||
|
||||
func TestS3WebFolderMock(t *testing.T) {
|
||||
webToken, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -393,8 +393,8 @@ func loadAdminTemplates(templatesPath string) {
|
|||
fsBaseTpl := template.New("fsBaseTemplate").Funcs(template.FuncMap{
|
||||
"ListFSProviders": func() []sdk.FilesystemProvider {
|
||||
return []sdk.FilesystemProvider{sdk.LocalFilesystemProvider, sdk.CryptedFilesystemProvider,
|
||||
sdk.S3FilesystemProvider, sdk.GCSFilesystemProvider,
|
||||
sdk.AzureBlobFilesystemProvider, sdk.SFTPFilesystemProvider,
|
||||
sdk.S3FilesystemProvider, sdk.GCSFilesystemProvider, sdk.AzureBlobFilesystemProvider,
|
||||
sdk.SFTPFilesystemProvider, sdk.HTTPFilesystemProvider,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
@ -1116,8 +1116,8 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
|||
if util.Contains(hooks, "check_password_disabled") {
|
||||
filters.Hooks.CheckPasswordDisabled = true
|
||||
}
|
||||
filters.DisableFsChecks = len(r.Form.Get("disable_fs_checks")) > 0
|
||||
filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||
filters.DisableFsChecks = r.Form.Get("disable_fs_checks") != ""
|
||||
filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
|
||||
filters.StartDirectory = r.Form.Get("start_directory")
|
||||
filters.MaxUploadFileSize = maxFileSize
|
||||
filters.ExternalAuthCacheTime, err = strconv.ParseInt(r.Form.Get("external_auth_cache_time"), 10, 64)
|
||||
|
@ -1223,7 +1223,7 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
|
|||
fingerprintsFormValue := r.Form.Get("sftp_fingerprints")
|
||||
config.Fingerprints = getSliceFromDelimitedValues(fingerprintsFormValue, "\n")
|
||||
config.Prefix = r.Form.Get("sftp_prefix")
|
||||
config.DisableCouncurrentReads = len(r.Form.Get("sftp_disable_concurrent_reads")) > 0
|
||||
config.DisableCouncurrentReads = r.Form.Get("sftp_disable_concurrent_reads") != ""
|
||||
config.BufferSize, err = strconv.ParseInt(r.Form.Get("sftp_buffer_size"), 10, 64)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid SFTP buffer size: %w", err)
|
||||
|
@ -1231,6 +1231,16 @@ func getSFTPConfig(r *http.Request) (vfs.SFTPFsConfig, error) {
|
|||
return config, nil
|
||||
}
|
||||
|
||||
func getHTTPFsConfig(r *http.Request) vfs.HTTPFsConfig {
|
||||
config := vfs.HTTPFsConfig{}
|
||||
config.Endpoint = r.Form.Get("http_endpoint")
|
||||
config.Username = r.Form.Get("http_username")
|
||||
config.SkipTLSVerify = r.Form.Get("http_skip_tls_verify") != ""
|
||||
config.Password = getSecretFromFormField(r, "http_password")
|
||||
config.APIKey = getSecretFromFormField(r, "http_api_key")
|
||||
return config
|
||||
}
|
||||
|
||||
func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
|
||||
var err error
|
||||
config := vfs.AzBlobFsConfig{}
|
||||
|
@ -1241,7 +1251,7 @@ func getAzureConfig(r *http.Request) (vfs.AzBlobFsConfig, error) {
|
|||
config.Endpoint = r.Form.Get("az_endpoint")
|
||||
config.KeyPrefix = r.Form.Get("az_key_prefix")
|
||||
config.AccessTier = r.Form.Get("az_access_tier")
|
||||
config.UseEmulator = len(r.Form.Get("az_use_emulator")) > 0
|
||||
config.UseEmulator = r.Form.Get("az_use_emulator") != ""
|
||||
config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("az_upload_part_size"), 10, 64)
|
||||
if err != nil {
|
||||
return config, fmt.Errorf("invalid azure upload part size: %w", err)
|
||||
|
@ -1291,6 +1301,8 @@ func getFsConfigFromPostFields(r *http.Request) (vfs.Filesystem, error) {
|
|||
return fs, err
|
||||
}
|
||||
fs.SFTPConfig = config
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
fs.HTTPConfig = getHTTPFsConfig(r)
|
||||
}
|
||||
return fs, nil
|
||||
}
|
||||
|
@ -1311,7 +1323,7 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
|
|||
admin.Email = r.Form.Get("email")
|
||||
admin.Status = status
|
||||
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||
admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
|
||||
admin.AdditionalInfo = r.Form.Get("additional_info")
|
||||
admin.Description = r.Form.Get("description")
|
||||
return admin, nil
|
||||
|
@ -1342,6 +1354,8 @@ func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVi
|
|||
folder.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(folder.FsConfig.AzBlobConfig, replacements)
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
folder.FsConfig.SFTPConfig = getSFTPFsFromTemplate(folder.FsConfig.SFTPConfig, replacements)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
folder.FsConfig.HTTPConfig = getHTTPFsFromTemplate(folder.FsConfig.HTTPConfig, replacements)
|
||||
}
|
||||
|
||||
return folder
|
||||
|
@ -1392,6 +1406,11 @@ func getSFTPFsFromTemplate(fsConfig vfs.SFTPFsConfig, replacements map[string]st
|
|||
return fsConfig
|
||||
}
|
||||
|
||||
func getHTTPFsFromTemplate(fsConfig vfs.HTTPFsConfig, replacements map[string]string) vfs.HTTPFsConfig {
|
||||
fsConfig.Username = replacePlaceholders(fsConfig.Username, replacements)
|
||||
return fsConfig
|
||||
}
|
||||
|
||||
func getUserFromTemplate(user dataprovider.User, template userTemplateFields) dataprovider.User {
|
||||
user.Username = template.Username
|
||||
user.Password = template.Password
|
||||
|
@ -1425,6 +1444,8 @@ func getUserFromTemplate(user dataprovider.User, template userTemplateFields) da
|
|||
user.FsConfig.AzBlobConfig = getAzBlobFsFromTemplate(user.FsConfig.AzBlobConfig, replacements)
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
user.FsConfig.SFTPConfig = getSFTPFsFromTemplate(user.FsConfig.SFTPConfig, replacements)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
user.FsConfig.HTTPConfig = getHTTPFsFromTemplate(user.FsConfig.HTTPConfig, replacements)
|
||||
}
|
||||
|
||||
return user
|
||||
|
@ -1699,7 +1720,7 @@ func (s *httpdServer) handleWebAdminProfilePost(w http.ResponseWriter, r *http.R
|
|||
s.renderProfilePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
admin.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||
admin.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
|
||||
admin.Email = r.Form.Get("email")
|
||||
admin.Description = r.Form.Get("description")
|
||||
err = dataprovider.UpdateAdmin(&admin, dataprovider.ActionExecutorSelf, ipAddr)
|
||||
|
@ -2203,7 +2224,8 @@ func (s *httpdServer) handleWebUpdateUserPost(w http.ResponseWriter, r *http.Req
|
|||
}
|
||||
updateEncryptedSecrets(&updatedUser.FsConfig, user.FsConfig.S3Config.AccessSecret, user.FsConfig.AzBlobConfig.AccountKey,
|
||||
user.FsConfig.AzBlobConfig.SASURL, user.FsConfig.GCSConfig.Credentials, user.FsConfig.CryptConfig.Passphrase,
|
||||
user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey, user.FsConfig.SFTPConfig.KeyPassphrase)
|
||||
user.FsConfig.SFTPConfig.Password, user.FsConfig.SFTPConfig.PrivateKey, user.FsConfig.SFTPConfig.KeyPassphrase,
|
||||
user.FsConfig.HTTPConfig.Password, user.FsConfig.HTTPConfig.APIKey)
|
||||
|
||||
updatedUser = getUserFromTemplate(updatedUser, userTemplateFields{
|
||||
Username: updatedUser.Username,
|
||||
|
@ -2337,7 +2359,8 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
|
|||
updatedFolder.FsConfig.SetEmptySecretsIfNil()
|
||||
updateEncryptedSecrets(&updatedFolder.FsConfig, folder.FsConfig.S3Config.AccessSecret, folder.FsConfig.AzBlobConfig.AccountKey,
|
||||
folder.FsConfig.AzBlobConfig.SASURL, folder.FsConfig.GCSConfig.Credentials, folder.FsConfig.CryptConfig.Passphrase,
|
||||
folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey, folder.FsConfig.SFTPConfig.KeyPassphrase)
|
||||
folder.FsConfig.SFTPConfig.Password, folder.FsConfig.SFTPConfig.PrivateKey, folder.FsConfig.SFTPConfig.KeyPassphrase,
|
||||
folder.FsConfig.HTTPConfig.Password, folder.FsConfig.HTTPConfig.APIKey)
|
||||
|
||||
updatedFolder = getFolderFromTemplate(updatedFolder, updatedFolder.Name)
|
||||
|
||||
|
@ -2502,7 +2525,8 @@ func (s *httpdServer) handleWebUpdateGroupPost(w http.ResponseWriter, r *http.Re
|
|||
group.UserSettings.FsConfig.AzBlobConfig.AccountKey, group.UserSettings.FsConfig.AzBlobConfig.SASURL,
|
||||
group.UserSettings.FsConfig.GCSConfig.Credentials, group.UserSettings.FsConfig.CryptConfig.Passphrase,
|
||||
group.UserSettings.FsConfig.SFTPConfig.Password, group.UserSettings.FsConfig.SFTPConfig.PrivateKey,
|
||||
group.UserSettings.FsConfig.SFTPConfig.KeyPassphrase)
|
||||
group.UserSettings.FsConfig.SFTPConfig.KeyPassphrase, group.UserSettings.FsConfig.HTTPConfig.Password,
|
||||
group.UserSettings.FsConfig.HTTPConfig.APIKey)
|
||||
|
||||
err = dataprovider.UpdateGroup(&updatedGroup, group.Users, claims.Username, ipAddr)
|
||||
if err != nil {
|
||||
|
|
|
@ -1160,7 +1160,7 @@ func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.
|
|||
user.PublicKeys = r.Form["public_keys"]
|
||||
}
|
||||
if userMerged.CanChangeAPIKeyAuth() {
|
||||
user.Filters.AllowAPIKeyAuth = len(r.Form.Get("allow_api_key_auth")) > 0
|
||||
user.Filters.AllowAPIKeyAuth = r.Form.Get("allow_api_key_auth") != ""
|
||||
}
|
||||
if userMerged.CanChangeInfo() {
|
||||
user.Email = r.Form.Get("email")
|
||||
|
|
|
@ -1426,7 +1426,10 @@ func compareFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
|
|||
if err := checkEncryptedSecret(expected.CryptConfig.Passphrase, actual.CryptConfig.Passphrase); err != nil {
|
||||
return err
|
||||
}
|
||||
return compareSFTPFsConfig(expected, actual)
|
||||
if err := compareSFTPFsConfig(expected, actual); err != nil {
|
||||
return err
|
||||
}
|
||||
return compareHTTPFsConfig(expected, actual)
|
||||
}
|
||||
|
||||
func compareS3Config(expected *vfs.Filesystem, actual *vfs.Filesystem) error { //nolint:gocyclo
|
||||
|
@ -1502,6 +1505,25 @@ func compareGCSConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func compareHTTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
|
||||
if expected.HTTPConfig.Endpoint != actual.HTTPConfig.Endpoint {
|
||||
return errors.New("HTTPFs endpoint mismatch")
|
||||
}
|
||||
if expected.HTTPConfig.Username != actual.HTTPConfig.Username {
|
||||
return errors.New("HTTPFs username mismatch")
|
||||
}
|
||||
if expected.HTTPConfig.SkipTLSVerify != actual.HTTPConfig.SkipTLSVerify {
|
||||
return errors.New("HTTPFs skip_tls_verify mismatch")
|
||||
}
|
||||
if err := checkEncryptedSecret(expected.HTTPConfig.Password, actual.HTTPConfig.Password); err != nil {
|
||||
return fmt.Errorf("HTTPFs password mismatch: %v", err)
|
||||
}
|
||||
if err := checkEncryptedSecret(expected.HTTPConfig.APIKey, actual.HTTPConfig.APIKey); err != nil {
|
||||
return fmt.Errorf("HTTPFs API key mismatch: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareSFTPFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) error {
|
||||
if expected.SFTPConfig.Endpoint != actual.SFTPConfig.Endpoint {
|
||||
return errors.New("SFTPFs endpoint mismatch")
|
||||
|
|
495
httpdtest/httpfsimpl.go
Normal file
495
httpdtest/httpfsimpl.go
Normal file
|
@ -0,0 +1,495 @@
|
|||
package httpdtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
statPath = "/api/v1/stat"
|
||||
openPath = "/api/v1/open"
|
||||
createPath = "/api/v1/create"
|
||||
renamePath = "/api/v1/rename"
|
||||
removePath = "/api/v1/remove"
|
||||
mkdirPath = "/api/v1/mkdir"
|
||||
chmodPath = "/api/v1/chmod"
|
||||
chtimesPath = "/api/v1/chtimes"
|
||||
truncatePath = "/api/v1/truncate"
|
||||
readdirPath = "/api/v1/readdir"
|
||||
dirsizePath = "/api/v1/dirsize"
|
||||
mimetypePath = "/api/v1/mimetype"
|
||||
statvfsPath = "/api/v1/statvfs"
|
||||
)
|
||||
|
||||
// StartTestHTTPFs starts a test HTTP service that implements httpfs
|
||||
func StartTestHTTPFs(port int) error {
|
||||
fs := httpFsImpl{
|
||||
port: port,
|
||||
}
|
||||
return fs.Run()
|
||||
}
|
||||
|
||||
type httpFsImpl struct {
|
||||
router *chi.Mux
|
||||
basePath string
|
||||
port int
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
|
||||
var errorString string
|
||||
if err != nil {
|
||||
errorString = err.Error()
|
||||
}
|
||||
resp := apiResponse{
|
||||
Error: errorString,
|
||||
Message: message,
|
||||
}
|
||||
ctx := context.WithValue(r.Context(), render.StatusCtxKey, code)
|
||||
render.JSON(w, r.WithContext(ctx), resp)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) getUsername(r *http.Request) (string, error) {
|
||||
username, _, ok := r.BasicAuth()
|
||||
if !ok || username == "" {
|
||||
return "", os.ErrPermission
|
||||
}
|
||||
rootPath := filepath.Join(fs.basePath, username)
|
||||
_, err := os.Stat(rootPath)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
err = os.MkdirAll(rootPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return username, err
|
||||
}
|
||||
}
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) getRespStatus(err error) int {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
return http.StatusForbidden
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) stat(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
info, err := os.Stat(fsPath)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
render.JSON(w, r, getStatFromInfo(info))
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) open(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
var offset int64
|
||||
if r.URL.Query().Has("offset") {
|
||||
offset, err = strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
f, err := os.Open(fsPath)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if offset > 0 {
|
||||
_, err = f.Seek(offset, io.SeekStart)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
ctype := mime.TypeByExtension(filepath.Ext(name))
|
||||
if ctype != "" {
|
||||
ctype = "application/octet-stream"
|
||||
}
|
||||
w.Header().Set("Content-Type", ctype)
|
||||
_, err = io.Copy(w, f)
|
||||
if err != nil {
|
||||
panic(http.ErrAbortHandler)
|
||||
}
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) create(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
|
||||
if r.URL.Query().Has("flags") {
|
||||
openFlags, err := strconv.ParseInt(r.URL.Query().Get("flags"), 10, 32)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
if openFlags > 0 {
|
||||
flags = int(openFlags)
|
||||
}
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
f, err := os.OpenFile(fsPath, flags, 0666)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, r.Body)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
fs.sendAPIResponse(w, r, nil, "upload OK", http.StatusOK)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) rename(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
target := r.URL.Query().Get("target")
|
||||
if target == "" {
|
||||
fs.sendAPIResponse(w, r, nil, "target path cannot be empty", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
sourcePath := filepath.Join(fs.basePath, username, name)
|
||||
targetPath := filepath.Join(fs.basePath, username, target)
|
||||
err = os.Rename(sourcePath, targetPath)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
fs.sendAPIResponse(w, r, nil, "rename OK", http.StatusOK)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) remove(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
err = os.Remove(fsPath)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
fs.sendAPIResponse(w, r, nil, "remove OK", http.StatusOK)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) mkdir(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
err = os.Mkdir(fsPath, os.ModePerm)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
fs.sendAPIResponse(w, r, nil, "mkdir OK", http.StatusOK)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) chmod(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
mode, err := strconv.ParseUint(r.URL.Query().Get("mode"), 10, 32)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
err = os.Chmod(fsPath, os.FileMode(mode))
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
fs.sendAPIResponse(w, r, nil, "chmod OK", http.StatusOK)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) chtimes(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
atime, err := time.Parse(time.RFC3339, r.URL.Query().Get("access_time"))
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
mtime, err := time.Parse(time.RFC3339, r.URL.Query().Get("modification_time"))
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
err = os.Chtimes(fsPath, atime, mtime)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
fs.sendAPIResponse(w, r, nil, "chtimes OK", http.StatusOK)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) truncate(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
size, err := strconv.ParseInt(r.URL.Query().Get("size"), 10, 64)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
err = os.Truncate(fsPath, size)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
fs.sendAPIResponse(w, r, nil, "chmod OK", http.StatusOK)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) readdir(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
f, err := os.Open(fsPath)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
list, err := f.Readdir(-1)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
result := make([]map[string]any, 0, len(list))
|
||||
for _, fi := range list {
|
||||
result = append(result, getStatFromInfo(fi))
|
||||
}
|
||||
render.JSON(w, r, result)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) dirsize(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
info, err := os.Stat(fsPath)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
numFiles := 0
|
||||
size := int64(0)
|
||||
if info.IsDir() {
|
||||
err = filepath.Walk(fsPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info != nil && info.Mode().IsRegular() {
|
||||
size += info.Size()
|
||||
numFiles++
|
||||
}
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
render.JSON(w, r, map[string]any{
|
||||
"files": numFiles,
|
||||
"size": size,
|
||||
})
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) mimetype(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
f, err := os.OpenFile(fsPath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
var buf [512]byte
|
||||
n, err := io.ReadFull(f, buf[:])
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
ctype := http.DetectContentType(buf[:n])
|
||||
render.JSON(w, r, map[string]any{
|
||||
"mime": ctype,
|
||||
})
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) statvfs(w http.ResponseWriter, r *http.Request) {
|
||||
username, err := fs.getUsername(r)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name := getNameURLParam(r)
|
||||
fsPath := filepath.Join(fs.basePath, username, name)
|
||||
usage, err := disk.Usage(fsPath)
|
||||
if err != nil {
|
||||
fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
|
||||
return
|
||||
}
|
||||
// we assume block size = 4096
|
||||
bsize := uint64(4096)
|
||||
blocks := usage.Total / bsize
|
||||
bfree := usage.Free / bsize
|
||||
files := usage.InodesTotal
|
||||
ffree := usage.InodesFree
|
||||
if files == 0 {
|
||||
// these assumptions are wrong but still better than returning 0
|
||||
files = blocks / 4
|
||||
ffree = bfree / 4
|
||||
}
|
||||
render.JSON(w, r, map[string]any{
|
||||
"bsize": bsize,
|
||||
"frsize": bsize,
|
||||
"blocks": blocks,
|
||||
"bfree": bfree,
|
||||
"bavail": bfree,
|
||||
"files": files,
|
||||
"ffree": ffree,
|
||||
"favail": ffree,
|
||||
"namemax": 255,
|
||||
})
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) configureRouter() {
|
||||
fs.router = chi.NewRouter()
|
||||
fs.router.Use(middleware.Recoverer)
|
||||
|
||||
fs.router.Get(statPath+"/{name}", fs.stat)
|
||||
fs.router.Get(openPath+"/{name}", fs.open)
|
||||
fs.router.Post(createPath+"/{name}", fs.create)
|
||||
fs.router.Patch(renamePath+"/{name}", fs.rename)
|
||||
fs.router.Delete(removePath+"/{name}", fs.remove)
|
||||
fs.router.Post(mkdirPath+"/{name}", fs.mkdir)
|
||||
fs.router.Patch(chmodPath+"/{name}", fs.chmod)
|
||||
fs.router.Patch(chtimesPath+"/{name}", fs.chtimes)
|
||||
fs.router.Patch(truncatePath+"/{name}", fs.truncate)
|
||||
fs.router.Get(readdirPath+"/{name}", fs.readdir)
|
||||
fs.router.Get(dirsizePath+"/{name}", fs.dirsize)
|
||||
fs.router.Get(mimetypePath+"/{name}", fs.mimetype)
|
||||
fs.router.Get(statvfsPath+"/{name}", fs.statvfs)
|
||||
}
|
||||
|
||||
func (fs *httpFsImpl) Run() error {
|
||||
fs.basePath = filepath.Join(os.TempDir(), "httpfs")
|
||||
if err := os.RemoveAll(fs.basePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(fs.basePath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
fs.configureRouter()
|
||||
|
||||
httpServer := http.Server{
|
||||
Addr: fmt.Sprintf(":%d", fs.port),
|
||||
Handler: fs.router,
|
||||
ReadTimeout: 60 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxHeaderBytes: 1 << 16, // 64KB
|
||||
}
|
||||
|
||||
return httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
func getStatFromInfo(info os.FileInfo) map[string]any {
|
||||
return map[string]any{
|
||||
"name": info.Name(),
|
||||
"size": info.Size(),
|
||||
"mode": info.Mode(),
|
||||
"last_modified": info.ModTime(),
|
||||
}
|
||||
}
|
||||
|
||||
func getNameURLParam(r *http.Request) string {
|
||||
v := chi.URLParam(r, "name")
|
||||
unescaped, err := url.PathUnescape(v)
|
||||
if err != nil {
|
||||
return util.CleanPath(v)
|
||||
}
|
||||
return util.CleanPath(unescaped)
|
||||
}
|
601
openapi/httpfs.yaml
Normal file
601
openapi/httpfs.yaml
Normal file
|
@ -0,0 +1,601 @@
|
|||
openapi: 3.0.3
|
||||
tags:
|
||||
- name: fs
|
||||
info:
|
||||
title: SFTPGo HTTPFs
|
||||
description: 'SFTPGo HTTP Filesystem API'
|
||||
version: 0.1.0
|
||||
servers:
|
||||
- url: /v1
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
- BasicAuth: []
|
||||
paths:
|
||||
/stat/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
tags:
|
||||
- fs
|
||||
summary: Describes the named object
|
||||
operationId: stat
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/FileInfo'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/open/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: offset
|
||||
in: query
|
||||
description: 'offset, in bytes, from the start. If not specified 0 must be assumed'
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
get:
|
||||
tags:
|
||||
- fs
|
||||
summary: Opens the named file for reading
|
||||
operationId: open
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/create/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: flags
|
||||
in: query
|
||||
description: 'flags to use for opening the file, if omitted O_RDWR|O_CREATE|O_TRUNC must be assumed. Supported flags: https://pkg.go.dev/os#pkg-constants'
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
post:
|
||||
tags:
|
||||
- fs
|
||||
summary: Creates or opens the named file for writing
|
||||
operationId: create
|
||||
requestBody:
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
required: true
|
||||
responses:
|
||||
201:
|
||||
$ref: '#/components/responses/OKResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/rename/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: target
|
||||
in: query
|
||||
description: target name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
patch:
|
||||
tags:
|
||||
- fs
|
||||
summary: Renames (moves) source to target
|
||||
operationId: rename
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/OKResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/remove/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
delete:
|
||||
tags:
|
||||
- fs
|
||||
summary: Removes the named file or (empty) directory.
|
||||
operationId: delete
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/OKResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/mkdir/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
post:
|
||||
tags:
|
||||
- fs
|
||||
summary: Creates a new directory with the specified name
|
||||
operationId: mkdir
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/OKResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/chmod/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: mode
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
patch:
|
||||
tags:
|
||||
- fs
|
||||
summary: Changes the mode of the named file
|
||||
operationId: chmod
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/OKResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/chtimes/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: access_time
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- name: modification_time
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
patch:
|
||||
tags:
|
||||
- fs
|
||||
summary: Changes the access and modification time of the named file
|
||||
operationId: chtimes
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/OKResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/truncate/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: size
|
||||
in: query
|
||||
required: true
|
||||
description: 'new file size in bytes'
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
patch:
|
||||
tags:
|
||||
- fs
|
||||
summary: Changes the size of the named file
|
||||
operationId: truncate
|
||||
responses:
|
||||
200:
|
||||
$ref: '#/components/responses/OKResponse'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/readdir/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
tags:
|
||||
- fs
|
||||
summary: Reads the named directory and returns the contents
|
||||
operationId: readdir
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/FileInfo'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/dirsize/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
tags:
|
||||
- fs
|
||||
summary: Returns the number of files and the size for the named directory including any sub-directory
|
||||
operationId: dirsize
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
files:
|
||||
type: integer
|
||||
description: 'Total number of files'
|
||||
size:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 'Total size of files'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/mimetype/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
tags:
|
||||
- fs
|
||||
summary: Returns the mime type for the named file
|
||||
operationId: mimetype
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
mime:
|
||||
type: string
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/statvfs/{name}:
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
description: object name
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
get:
|
||||
tags:
|
||||
- fs
|
||||
summary: Returns the VFS stats for the specified path
|
||||
operationId: statvfs
|
||||
responses:
|
||||
200:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/StatVFS'
|
||||
401:
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
403:
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
404:
|
||||
$ref: '#/components/responses/NotFound'
|
||||
500:
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
501:
|
||||
$ref: '#/components/responses/NotImplemented'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
components:
|
||||
responses:
|
||||
OKResponse:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
BadRequest:
|
||||
description: Bad Request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
Unauthorized:
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
Forbidden:
|
||||
description: Forbidden
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
NotFound:
|
||||
description: Not Found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
NotImplemented:
|
||||
description: Not Implemented
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
Conflict:
|
||||
description: Conflict
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
RequestEntityTooLarge:
|
||||
description: Request Entity Too Large, max allowed size exceeded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
InternalServerError:
|
||||
description: Internal Server Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
DefaultResponse:
|
||||
description: Unexpected Error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
schemas:
|
||||
ApiResponse:
|
||||
type: object
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
description: 'message, can be empty'
|
||||
error:
|
||||
type: string
|
||||
description: error description if any
|
||||
FileInfo:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: base name of the file
|
||||
size:
|
||||
type: integer
|
||||
format: int64
|
||||
description: length in bytes for regular files; system-dependent for others
|
||||
mode:
|
||||
type: integer
|
||||
description: |
|
||||
File mode and permission bits. More details here: https://golang.org/pkg/io/fs/#FileMode.
|
||||
Let's see some examples:
|
||||
- for a directory mode&2147483648 != 0
|
||||
- for a symlink mode&134217728 != 0
|
||||
- for a regular file mode&2401763328 == 0
|
||||
last_modified:
|
||||
type: string
|
||||
format: date-time
|
||||
StatVFS:
|
||||
type: object
|
||||
properties:
|
||||
bsize:
|
||||
type: integer
|
||||
description: file system block size
|
||||
frsize:
|
||||
type: integer
|
||||
description: fundamental fs block size
|
||||
blocks:
|
||||
type: integer
|
||||
description: number of blocks
|
||||
bfree:
|
||||
type: integer
|
||||
description: free blocks in file system
|
||||
bavail:
|
||||
type: integer
|
||||
description: free blocks for non-root
|
||||
files:
|
||||
type: integer
|
||||
description: total file inodes
|
||||
ffree:
|
||||
type: integer
|
||||
description: free file inodes
|
||||
favail:
|
||||
type: integer
|
||||
description: free file inodes for non-root
|
||||
fsid:
|
||||
type: integer
|
||||
description: file system id
|
||||
flag:
|
||||
type: integer
|
||||
description: bit mask of f_flag values
|
||||
namemax:
|
||||
type: integer
|
||||
description: maximum filename length
|
||||
securitySchemes:
|
||||
BasicAuth:
|
||||
type: http
|
||||
scheme: basic
|
||||
ApiKeyAuth:
|
||||
type: apiKey
|
||||
in: header
|
||||
name: X-API-KEY
|
|
@ -26,7 +26,7 @@ info:
|
|||
SFTPGo supports groups to simplify the administration of multiple accounts by letting you assign settings once to a group, instead of multiple times to each individual user.
|
||||
The SFTPGo WebClient allows end users to change their credentials, browse and manage their files in the browser and setup two-factor authentication which works with Authy, Google Authenticator and other compatible apps.
|
||||
From the WebClient each authorized user can also create HTTP/S links to externally share files and folders securely, by setting limits to the number of downloads/uploads, protecting the share with a password, limiting access by source IP address, setting an automatic expiration date.
|
||||
version: 2.3.0-dev
|
||||
version: 2.3.1-dev
|
||||
contact:
|
||||
name: API support
|
||||
url: 'https://github.com/drakkan/sftpgo'
|
||||
|
@ -4473,6 +4473,7 @@ components:
|
|||
- 3
|
||||
- 4
|
||||
- 5
|
||||
- 6
|
||||
description: |
|
||||
Filesystem providers:
|
||||
* `0` - Local filesystem
|
||||
|
@ -4481,6 +4482,7 @@ components:
|
|||
* `3` - Azure Blob Storage
|
||||
* `4` - Local filesystem encrypted
|
||||
* `5` - SFTP
|
||||
* `6` - HTTP filesystem
|
||||
LoginMethods:
|
||||
type: string
|
||||
enum:
|
||||
|
@ -5054,6 +5056,20 @@ components:
|
|||
maximum: 16
|
||||
example: 2
|
||||
description: The size of the buffer (in MB) to use for transfers. By enabling buffering, the reads and writes, from/to the remote SFTP server, are split in multiple concurrent requests and this allows data to be transferred at a faster rate, over high latency networks, by overlapping round-trip times. With buffering enabled, resuming uploads is not supported and a file cannot be opened for both reading and writing at the same time. 0 means disabled.
|
||||
HTTPFsConfig:
|
||||
type: object
|
||||
properties:
|
||||
endpoint:
|
||||
type: string
|
||||
description: 'HTTP/S endpoint URL. SFTPGo will use this URL as base, for example for the `stat` API, SFTPGo will add `/stat/{name}`'
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
$ref: '#/components/schemas/Secret'
|
||||
api_key:
|
||||
$ref: '#/components/schemas/Secret'
|
||||
skip_tls_verify:
|
||||
type: boolean
|
||||
FilesystemConfig:
|
||||
type: object
|
||||
properties:
|
||||
|
@ -5069,6 +5085,8 @@ components:
|
|||
$ref: '#/components/schemas/CryptFsConfig'
|
||||
sftpconfig:
|
||||
$ref: '#/components/schemas/SFTPFsConfig'
|
||||
httpconfig:
|
||||
$ref: '#/components/schemas/HTTPFsConfig'
|
||||
description: Storage filesystem details
|
||||
BaseVirtualFolder:
|
||||
type: object
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
sftpgo (2.3.1-1ppa1) bionic; urgency=medium
|
||||
|
||||
* New upstream release
|
||||
|
||||
-- Nicola Murino <nicola.murino@gmail.com> Fri, 10 Jun 2022 19:48:21 +0200
|
||||
|
||||
sftpgo (2.3.0-1ppa1) bionic; urgency=medium
|
||||
|
||||
* New upstream release
|
||||
|
|
|
@ -229,15 +229,20 @@ func (s *Service) advertiseServices(advertiseService, advertiseCredentials bool)
|
|||
}
|
||||
|
||||
func (s *Service) getPortableDirToServe() string {
|
||||
var dirToServe string
|
||||
if s.PortableUser.FsConfig.Provider == sdk.S3FilesystemProvider {
|
||||
dirToServe = s.PortableUser.FsConfig.S3Config.KeyPrefix
|
||||
} else if s.PortableUser.FsConfig.Provider == sdk.GCSFilesystemProvider {
|
||||
dirToServe = s.PortableUser.FsConfig.GCSConfig.KeyPrefix
|
||||
} else {
|
||||
dirToServe = s.PortableUser.HomeDir
|
||||
switch s.PortableUser.FsConfig.Provider {
|
||||
case sdk.S3FilesystemProvider:
|
||||
return s.PortableUser.FsConfig.S3Config.KeyPrefix
|
||||
case sdk.GCSFilesystemProvider:
|
||||
return s.PortableUser.FsConfig.GCSConfig.KeyPrefix
|
||||
case sdk.AzureBlobFilesystemProvider:
|
||||
return s.PortableUser.FsConfig.AzBlobConfig.KeyPrefix
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
return s.PortableUser.FsConfig.SFTPConfig.Prefix
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
return "/"
|
||||
default:
|
||||
return s.PortableUser.HomeDir
|
||||
}
|
||||
return dirToServe
|
||||
}
|
||||
|
||||
// configures the portable user and return the printable password if any
|
||||
|
@ -266,43 +271,36 @@ func (s *Service) configurePortableSecrets() {
|
|||
switch s.PortableUser.FsConfig.Provider {
|
||||
case sdk.S3FilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.S3Config.AccessSecret.GetPayload()
|
||||
s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(payload)
|
||||
}
|
||||
s.PortableUser.FsConfig.S3Config.AccessSecret = getSecretFromString(payload)
|
||||
case sdk.GCSFilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.GCSConfig.Credentials.GetPayload()
|
||||
s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(payload)
|
||||
}
|
||||
s.PortableUser.FsConfig.GCSConfig.Credentials = getSecretFromString(payload)
|
||||
case sdk.AzureBlobFilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.AzBlobConfig.AccountKey.GetPayload()
|
||||
s.PortableUser.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(payload)
|
||||
}
|
||||
s.PortableUser.FsConfig.AzBlobConfig.AccountKey = getSecretFromString(payload)
|
||||
payload = s.PortableUser.FsConfig.AzBlobConfig.SASURL.GetPayload()
|
||||
s.PortableUser.FsConfig.AzBlobConfig.SASURL = kms.NewEmptySecret()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.AzBlobConfig.SASURL = kms.NewPlainSecret(payload)
|
||||
}
|
||||
s.PortableUser.FsConfig.AzBlobConfig.SASURL = getSecretFromString(payload)
|
||||
case sdk.CryptedFilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.CryptConfig.Passphrase.GetPayload()
|
||||
s.PortableUser.FsConfig.CryptConfig.Passphrase = kms.NewEmptySecret()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(payload)
|
||||
}
|
||||
s.PortableUser.FsConfig.CryptConfig.Passphrase = getSecretFromString(payload)
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.SFTPConfig.Password.GetPayload()
|
||||
s.PortableUser.FsConfig.SFTPConfig.Password = kms.NewEmptySecret()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.SFTPConfig.Password = kms.NewPlainSecret(payload)
|
||||
}
|
||||
s.PortableUser.FsConfig.SFTPConfig.Password = getSecretFromString(payload)
|
||||
payload = s.PortableUser.FsConfig.SFTPConfig.PrivateKey.GetPayload()
|
||||
s.PortableUser.FsConfig.SFTPConfig.PrivateKey = kms.NewEmptySecret()
|
||||
s.PortableUser.FsConfig.SFTPConfig.PrivateKey = getSecretFromString(payload)
|
||||
payload = s.PortableUser.FsConfig.SFTPConfig.KeyPassphrase.GetPayload()
|
||||
s.PortableUser.FsConfig.SFTPConfig.KeyPassphrase = getSecretFromString(payload)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.HTTPConfig.Password.GetPayload()
|
||||
s.PortableUser.FsConfig.HTTPConfig.Password = getSecretFromString(payload)
|
||||
payload = s.PortableUser.FsConfig.HTTPConfig.APIKey.GetPayload()
|
||||
s.PortableUser.FsConfig.HTTPConfig.APIKey = getSecretFromString(payload)
|
||||
}
|
||||
}
|
||||
|
||||
func getSecretFromString(payload string) *kms.Secret {
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.SFTPConfig.PrivateKey = kms.NewPlainSecret(payload)
|
||||
}
|
||||
return kms.NewPlainSecret(payload)
|
||||
}
|
||||
return kms.NewEmptySecret()
|
||||
}
|
||||
|
|
|
@ -443,7 +443,7 @@ func (c *Connection) handleSFTPUploadToExistingFile(fs vfs.Fs, pflags sftp.FileO
|
|||
}
|
||||
initialSize = fileSize
|
||||
} else {
|
||||
if vfs.IsLocalOrSFTPFs(fs) && isTruncate {
|
||||
if isTruncate && vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
|
|
294
sftpd/httpfs_test.go
Normal file
294
sftpd/httpfs_test.go
Normal file
|
@ -0,0 +1,294 @@
|
|||
package sftpd_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sftpgo/sdk"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/httpdtest"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
)
|
||||
|
||||
const (
|
||||
httpFsPort = 12345
|
||||
defaultHTTPFsUsername = "httpfs_user"
|
||||
)
|
||||
|
||||
func TestBasicHTTPFsHandling(t *testing.T) {
|
||||
usePubKey := true
|
||||
u := getTestUserWithHTTPFs(usePubKey)
|
||||
u.QuotaSize = 6553600
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(65535)
|
||||
expectedQuotaSize := user.UsedQuotaSize + testFileSize*2
|
||||
expectedQuotaFiles := user.UsedQuotaFiles + 2
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join("/missing_dir", testFileName), testFileSize, client)
|
||||
assert.Error(t, err)
|
||||
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
info, err := client.Stat(testFileName)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, testFileSize, info.Size())
|
||||
}
|
||||
contents, err := client.ReadDir("/")
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, contents, 1) {
|
||||
assert.Equal(t, testFileName, contents[0].Name())
|
||||
}
|
||||
dirName := "test dirname"
|
||||
err = client.Mkdir(dirName)
|
||||
assert.NoError(t, err)
|
||||
contents, err = client.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, contents, 2)
|
||||
contents, err = client.ReadDir(dirName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, contents, 0)
|
||||
err = sftpUploadFile(testFilePath, path.Join(dirName, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
contents, err = client.ReadDir(dirName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, contents, 1)
|
||||
dirRenamed := dirName + "_renamed"
|
||||
err = client.Rename(dirName, dirRenamed)
|
||||
assert.NoError(t, err)
|
||||
info, err = client.Stat(dirRenamed)
|
||||
if assert.NoError(t, err) {
|
||||
assert.True(t, info.IsDir())
|
||||
}
|
||||
// mode 0666 and 0444 works on Windows too
|
||||
newPerm := os.FileMode(0444)
|
||||
err = client.Chmod(testFileName, newPerm)
|
||||
assert.NoError(t, err)
|
||||
info, err = client.Stat(testFileName)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newPerm, info.Mode().Perm())
|
||||
newPerm = os.FileMode(0666)
|
||||
err = client.Chmod(testFileName, newPerm)
|
||||
assert.NoError(t, err)
|
||||
info, err = client.Stat(testFileName)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newPerm, info.Mode().Perm())
|
||||
// chtimes
|
||||
acmodTime := time.Now().Add(-36 * time.Hour)
|
||||
err = client.Chtimes(testFileName, acmodTime, acmodTime)
|
||||
assert.NoError(t, err)
|
||||
info, err = client.Stat(testFileName)
|
||||
if assert.NoError(t, err) {
|
||||
diff := math.Abs(info.ModTime().Sub(acmodTime).Seconds())
|
||||
assert.LessOrEqual(t, diff, float64(1))
|
||||
}
|
||||
_, err = client.StatVFS("/")
|
||||
assert.NoError(t, err)
|
||||
|
||||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
|
||||
// execute a quota scan
|
||||
_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool {
|
||||
scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
|
||||
if err == nil {
|
||||
return len(scans) == 0
|
||||
}
|
||||
return false
|
||||
}, 1*time.Second, 50*time.Millisecond)
|
||||
|
||||
err = client.Remove(testFileName)
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Lstat(testFileName)
|
||||
assert.Error(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
|
||||
// truncate
|
||||
err = client.Truncate(path.Join(dirRenamed, testFileName), 100)
|
||||
assert.NoError(t, err)
|
||||
info, err = client.Stat(path.Join(dirRenamed, testFileName))
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, int64(100), info.Size())
|
||||
}
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), user.UsedQuotaSize)
|
||||
// update quota
|
||||
_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
|
||||
assert.NoError(t, err)
|
||||
assert.Eventually(t, func() bool {
|
||||
scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
|
||||
if err == nil {
|
||||
return len(scans) == 0
|
||||
}
|
||||
return false
|
||||
}, 1*time.Second, 50*time.Millisecond)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(100), user.UsedQuotaSize)
|
||||
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPFsVirtualFolder(t *testing.T) {
|
||||
usePubKey := false
|
||||
u := getTestUser(usePubKey)
|
||||
folderName := "httpfsfolder"
|
||||
vdirPath := "/vdir/http fs"
|
||||
u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
|
||||
BaseVirtualFolder: vfs.BaseVirtualFolder{
|
||||
Name: folderName,
|
||||
FsConfig: vfs.Filesystem{
|
||||
Provider: sdk.HTTPFilesystemProvider,
|
||||
HTTPConfig: vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
|
||||
Username: defaultHTTPFsUsername,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
VirtualPath: vdirPath,
|
||||
})
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClient(user, usePubKey)
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
|
||||
testFilePath := filepath.Join(homeBasePath, testFileName)
|
||||
testFileSize := int64(65535)
|
||||
err = createTestFile(testFilePath, testFileSize)
|
||||
assert.NoError(t, err)
|
||||
err = sftpUploadFile(testFilePath, path.Join(vdirPath, testFileName), testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Stat(path.Join(vdirPath, testFileName))
|
||||
assert.NoError(t, err)
|
||||
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
|
||||
err = sftpDownloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(testFilePath)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(localDownloadPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestHTTPFsWalk(t *testing.T) {
|
||||
user := getTestUserWithHTTPFs(false)
|
||||
httpFs, err := user.GetFilesystem("")
|
||||
require.NoError(t, err)
|
||||
basePath := filepath.Join(os.TempDir(), "httpfs", user.FsConfig.HTTPConfig.Username)
|
||||
err = os.RemoveAll(basePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
var walkedPaths []string
|
||||
err = httpFs.Walk("/", func(walkedPath string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
walkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, walkedPaths, 1)
|
||||
require.Contains(t, walkedPaths, "/")
|
||||
// now add some files/folders
|
||||
for i := 0; i < 10; i++ {
|
||||
err = os.WriteFile(filepath.Join(basePath, fmt.Sprintf("file%d", i)), nil, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.Mkdir(filepath.Join(basePath, fmt.Sprintf("dir%d", i)), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
for j := 0; j < 5; j++ {
|
||||
err = os.WriteFile(filepath.Join(basePath, fmt.Sprintf("dir%d", i), fmt.Sprintf("subfile%d", j)), nil, os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
}
|
||||
walkedPaths = nil
|
||||
err = httpFs.Walk("/", func(walkedPath string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
walkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, walkedPaths, 71)
|
||||
require.Contains(t, walkedPaths, "/")
|
||||
for i := 0; i < 10; i++ {
|
||||
require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("file%d", i)))
|
||||
require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("dir%d", i)))
|
||||
for j := 0; j < 5; j++ {
|
||||
require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("dir%d", i), fmt.Sprintf("subfile%d", j)))
|
||||
}
|
||||
}
|
||||
|
||||
err = os.RemoveAll(basePath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func getTestUserWithHTTPFs(usePubKey bool) dataprovider.User {
|
||||
u := getTestUser(usePubKey)
|
||||
u.FsConfig.Provider = sdk.HTTPFilesystemProvider
|
||||
u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
|
||||
Username: defaultHTTPFsUsername,
|
||||
},
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func startHTTPFs() {
|
||||
go func() {
|
||||
if err := httpdtest.StartTestHTTPFs(httpFsPort); err != nil {
|
||||
logger.ErrorToConsole("could not start HTTPfs test server: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
waitTCPListening(fmt.Sprintf(":%d", httpFsPort))
|
||||
}
|
|
@ -243,7 +243,7 @@ func (c *scpCommand) handleUploadFile(fs vfs.Fs, resolvedPath, filePath string,
|
|||
initialSize := int64(0)
|
||||
truncatedSize := int64(0) // bytes truncated and not included in quota
|
||||
if !isNewFile {
|
||||
if vfs.IsLocalOrSFTPFs(fs) {
|
||||
if vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.connection.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
|
|
|
@ -331,6 +331,7 @@ func TestMain(m *testing.M) {
|
|||
|
||||
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
|
||||
getHostKeysFingerprints(sftpdConf.HostKeys)
|
||||
startHTTPFs()
|
||||
|
||||
exitCode := m.Run()
|
||||
os.Remove(logFilePath)
|
||||
|
|
|
@ -593,37 +593,19 @@ func (c *sshCommand) checkRecursiveCopyPermissions(fsSrc vfs.Fs, fsDst vfs.Fs, f
|
|||
if !c.connection.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(sshDestPath)) {
|
||||
return common.ErrPermissionDenied
|
||||
}
|
||||
dstPerms := []string{
|
||||
dataprovider.PermCreateDirs,
|
||||
dataprovider.PermCreateSymlinks,
|
||||
dataprovider.PermUpload,
|
||||
}
|
||||
|
||||
err := fsSrc.Walk(fsSourcePath, func(walkedPath string, info os.FileInfo, err error) error {
|
||||
return fsSrc.Walk(fsSourcePath, func(walkedPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return c.connection.GetFsError(fsSrc, err)
|
||||
}
|
||||
fsDstSubPath := strings.Replace(walkedPath, fsSourcePath, fsDestPath, 1)
|
||||
sshSrcSubPath := fsSrc.GetRelativePath(walkedPath)
|
||||
sshDstSubPath := fsDst.GetRelativePath(fsDstSubPath)
|
||||
// If the current dir has no subdirs with defined permissions inside it
|
||||
// and it has all the possible permissions we can stop scanning
|
||||
if !c.connection.User.HasPermissionsInside(path.Dir(sshSrcSubPath)) &&
|
||||
!c.connection.User.HasPermissionsInside(path.Dir(sshDstSubPath)) {
|
||||
if c.connection.User.HasPerm(dataprovider.PermListItems, path.Dir(sshSrcSubPath)) &&
|
||||
c.connection.User.HasPerms(dstPerms, path.Dir(sshDstSubPath)) {
|
||||
return common.ErrSkipPermissionsCheck
|
||||
}
|
||||
}
|
||||
if !c.hasCopyPermissions(sshSrcSubPath, sshDstSubPath, info) {
|
||||
return common.ErrPermissionDenied
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err == common.ErrSkipPermissionsCheck {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *sshCommand) checkCopyPermissions(fsSrc vfs.Fs, fsDst vfs.Fs, fsSourcePath, fsDestPath, sshSourcePath, sshDestPath string, info os.FileInfo) error {
|
||||
|
|
|
@ -465,6 +465,44 @@
|
|||
<label for="idDisableConcurrentReads" class="form-check-label">Disable concurrent reads</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row fsconfig fsconfig-httpfs">
|
||||
<label for="idHTTPEndpoint" class="col-sm-2 col-form-label">Endpoint</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idHTTPEndpoint" name="http_endpoint" placeholder=""
|
||||
value="{{.HTTPConfig.Endpoint}}" maxlength="255">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row fsconfig fsconfig-httpfs">
|
||||
<label for="idHTTPUsername" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idHTTPUsername" name="http_username" placeholder=""
|
||||
value="{{.HTTPConfig.Username}}" maxlength="255">
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<label for="idHTTPPassword" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="password" class="form-control" id="idHTTPPassword" name="http_password" placeholder=""
|
||||
value="{{if .HTTPConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.Password.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row fsconfig fsconfig-httpfs">
|
||||
<label for="idHTTPAPIKey" class="col-sm-2 col-form-label">API Key</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="idHTTPAPIKey" name="http_api_key" placeholder=""
|
||||
value="{{if .HTTPConfig.APIKey.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.HTTPConfig.APIKey.GetPayload}}{{end}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group fsconfig fsconfig-httpfs">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="idHTTPSkipTLSVerify"
|
||||
name="http_skip_tls_verify" {{if .HTTPConfig.SkipTLSVerify}}checked{{end}}>
|
||||
<label for="idHTTPSkipTLSVerify" class="form-check-label">Skip TLS verify</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -4,24 +4,23 @@ go 1.18
|
|||
|
||||
require (
|
||||
github.com/hashicorp/go-plugin v1.4.4
|
||||
github.com/sftpgo/sdk v0.1.0
|
||||
github.com/sftpgo/sdk v0.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/hashicorp/go-hclog v1.2.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.2.1 // indirect
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
|
||||
google.golang.org/grpc v1.46.2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac // indirect
|
||||
google.golang.org/grpc v1.47.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
@ -20,7 +20,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
|
@ -51,19 +50,16 @@ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
|
|||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
|
||||
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
|
||||
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
|
||||
github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
|
@ -75,13 +71,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/sftpgo/sdk v0.1.0 h1:t94VfxsmNbCYLYRDr3x/UTwSbFFtL9DJ171zkQ3MchQ=
|
||||
github.com/sftpgo/sdk v0.1.0/go.mod h1:Bhgac6kiwIziILXLzH4wepT8lQXyhF83poDXqZorN6Q=
|
||||
github.com/sftpgo/sdk v0.1.1 h1:3vGGmRWLr+1vp+Z7OJG2LHt/u9MjTs3odZZtUcbfAsQ=
|
||||
github.com/sftpgo/sdk v0.1.1/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
@ -98,8 +94,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -108,9 +104,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -121,8 +115,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -142,16 +137,16 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
|
|||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac h1:ByeiW1F67iV9o8ipGskA+HWzSkMbRJuKLlwCdPxzn7A=
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
|
||||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
|
||||
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
|
|
@ -4,23 +4,22 @@ go 1.18
|
|||
|
||||
require (
|
||||
github.com/hashicorp/go-plugin v1.4.4
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4
|
||||
github.com/sftpgo/sdk v0.1.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/hashicorp/go-hclog v1.2.0 // indirect
|
||||
github.com/hashicorp/go-hclog v1.2.1 // indirect
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 // indirect
|
||||
google.golang.org/grpc v1.46.2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac // indirect
|
||||
google.golang.org/grpc v1.47.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
@ -20,7 +20,6 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
|
@ -50,19 +49,16 @@ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
|||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
|
||||
github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
|
||||
github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw=
|
||||
github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ=
|
||||
github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s=
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I=
|
||||
github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
|
@ -74,13 +70,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4 h1:zpu89DMnl3d5Bu3YlvQuu3/KsjkhERgvqgqz+Lnn4CY=
|
||||
github.com/sftpgo/sdk v0.1.1-0.20220323191209-5d4ff81576b4/go.mod h1:m5J7DH8unhD5RUsREFRiidP8zgBjup0+iQaxQnYHJOM=
|
||||
github.com/sftpgo/sdk v0.1.1 h1:3vGGmRWLr+1vp+Z7OJG2LHt/u9MjTs3odZZtUcbfAsQ=
|
||||
github.com/sftpgo/sdk v0.1.1/go.mod h1:JdxJrGnk6RKhRMTqwH5fFfaMiZuGi5qR1HxQaSDsswo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
@ -97,8 +93,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
|||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
@ -107,9 +103,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -120,8 +114,9 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
@ -142,16 +137,16 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA
|
|||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58 h1:a221mAAEAzq4Lz6ZWRkcS8ptb2mxoxYSt4N68aRyQHM=
|
||||
google.golang.org/genproto v0.0.0-20220527130721-00d5c0f3be58/go.mod h1:yKyY4AMRwFiC8yMMNaMi+RkCnjZJt9LoWuvhXjMs+To=
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac h1:ByeiW1F67iV9o8ipGskA+HWzSkMbRJuKLlwCdPxzn7A=
|
||||
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
|
||||
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
|
||||
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
|
|
@ -2,7 +2,7 @@ package version
|
|||
|
||||
import "strings"
|
||||
|
||||
const version = "2.3.0-dev"
|
||||
const version = "2.3.1-dev"
|
||||
|
||||
var (
|
||||
commit = ""
|
||||
|
|
|
@ -15,6 +15,7 @@ type Filesystem struct {
|
|||
AzBlobConfig AzBlobFsConfig `json:"azblobconfig,omitempty"`
|
||||
CryptConfig CryptFsConfig `json:"cryptconfig,omitempty"`
|
||||
SFTPConfig SFTPFsConfig `json:"sftpconfig,omitempty"`
|
||||
HTTPConfig HTTPFsConfig `json:"httpconfig,omitempty"`
|
||||
}
|
||||
|
||||
// SetEmptySecrets sets the secrets to empty
|
||||
|
@ -27,6 +28,8 @@ func (f *Filesystem) SetEmptySecrets() {
|
|||
f.SFTPConfig.Password = kms.NewEmptySecret()
|
||||
f.SFTPConfig.PrivateKey = kms.NewEmptySecret()
|
||||
f.SFTPConfig.KeyPassphrase = kms.NewEmptySecret()
|
||||
f.HTTPConfig.Password = kms.NewEmptySecret()
|
||||
f.HTTPConfig.APIKey = kms.NewEmptySecret()
|
||||
}
|
||||
|
||||
// SetEmptySecretsIfNil sets the secrets to empty if nil
|
||||
|
@ -55,6 +58,12 @@ func (f *Filesystem) SetEmptySecretsIfNil() {
|
|||
if f.SFTPConfig.KeyPassphrase == nil {
|
||||
f.SFTPConfig.KeyPassphrase = kms.NewEmptySecret()
|
||||
}
|
||||
if f.HTTPConfig.Password == nil {
|
||||
f.HTTPConfig.Password = kms.NewEmptySecret()
|
||||
}
|
||||
if f.HTTPConfig.APIKey == nil {
|
||||
f.HTTPConfig.APIKey = kms.NewEmptySecret()
|
||||
}
|
||||
}
|
||||
|
||||
// SetNilSecretsIfEmpty set the secrets to nil if empty.
|
||||
|
@ -77,6 +86,7 @@ func (f *Filesystem) SetNilSecretsIfEmpty() {
|
|||
f.CryptConfig.Passphrase = nil
|
||||
}
|
||||
f.SFTPConfig.setNilSecretsIfEmpty()
|
||||
f.HTTPConfig.setNilSecretsIfEmpty()
|
||||
}
|
||||
|
||||
// IsEqual returns true if the fs is equal to other
|
||||
|
@ -95,6 +105,8 @@ func (f *Filesystem) IsEqual(other *Filesystem) bool {
|
|||
return f.CryptConfig.isEqual(&other.CryptConfig)
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
return f.SFTPConfig.isEqual(&other.SFTPConfig)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
return f.HTTPConfig.isEqual(&other.HTTPConfig)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
|
@ -112,6 +124,7 @@ func (f *Filesystem) Validate(additionalData string) error {
|
|||
f.AzBlobConfig = AzBlobFsConfig{}
|
||||
f.CryptConfig = CryptFsConfig{}
|
||||
f.SFTPConfig = SFTPFsConfig{}
|
||||
f.HTTPConfig = HTTPFsConfig{}
|
||||
return nil
|
||||
case sdk.GCSFilesystemProvider:
|
||||
if err := f.GCSConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
|
||||
|
@ -121,6 +134,7 @@ func (f *Filesystem) Validate(additionalData string) error {
|
|||
f.AzBlobConfig = AzBlobFsConfig{}
|
||||
f.CryptConfig = CryptFsConfig{}
|
||||
f.SFTPConfig = SFTPFsConfig{}
|
||||
f.HTTPConfig = HTTPFsConfig{}
|
||||
return nil
|
||||
case sdk.AzureBlobFilesystemProvider:
|
||||
if err := f.AzBlobConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
|
||||
|
@ -130,6 +144,7 @@ func (f *Filesystem) Validate(additionalData string) error {
|
|||
f.GCSConfig = GCSFsConfig{}
|
||||
f.CryptConfig = CryptFsConfig{}
|
||||
f.SFTPConfig = SFTPFsConfig{}
|
||||
f.HTTPConfig = HTTPFsConfig{}
|
||||
return nil
|
||||
case sdk.CryptedFilesystemProvider:
|
||||
if err := f.CryptConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
|
||||
|
@ -139,6 +154,7 @@ func (f *Filesystem) Validate(additionalData string) error {
|
|||
f.GCSConfig = GCSFsConfig{}
|
||||
f.AzBlobConfig = AzBlobFsConfig{}
|
||||
f.SFTPConfig = SFTPFsConfig{}
|
||||
f.HTTPConfig = HTTPFsConfig{}
|
||||
return nil
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
if err := f.SFTPConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
|
||||
|
@ -148,6 +164,17 @@ func (f *Filesystem) Validate(additionalData string) error {
|
|||
f.GCSConfig = GCSFsConfig{}
|
||||
f.AzBlobConfig = AzBlobFsConfig{}
|
||||
f.CryptConfig = CryptFsConfig{}
|
||||
f.HTTPConfig = HTTPFsConfig{}
|
||||
return nil
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
if err := f.HTTPConfig.ValidateAndEncryptCredentials(additionalData); err != nil {
|
||||
return err
|
||||
}
|
||||
f.S3Config = S3FsConfig{}
|
||||
f.GCSConfig = GCSFsConfig{}
|
||||
f.AzBlobConfig = AzBlobFsConfig{}
|
||||
f.CryptConfig = CryptFsConfig{}
|
||||
f.SFTPConfig = SFTPFsConfig{}
|
||||
return nil
|
||||
default:
|
||||
f.Provider = sdk.LocalFilesystemProvider
|
||||
|
@ -156,6 +183,7 @@ func (f *Filesystem) Validate(additionalData string) error {
|
|||
f.AzBlobConfig = AzBlobFsConfig{}
|
||||
f.CryptConfig = CryptFsConfig{}
|
||||
f.SFTPConfig = SFTPFsConfig{}
|
||||
f.HTTPConfig = HTTPFsConfig{}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -165,24 +193,16 @@ func (f *Filesystem) HasRedactedSecret() bool {
|
|||
// TODO move vfs specific code into each *FsConfig struct
|
||||
switch f.Provider {
|
||||
case sdk.S3FilesystemProvider:
|
||||
if f.S3Config.AccessSecret.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
return f.S3Config.AccessSecret.IsRedacted()
|
||||
case sdk.GCSFilesystemProvider:
|
||||
if f.GCSConfig.Credentials.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
return f.GCSConfig.Credentials.IsRedacted()
|
||||
case sdk.AzureBlobFilesystemProvider:
|
||||
if f.AzBlobConfig.AccountKey.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
if f.AzBlobConfig.SASURL.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
return f.AzBlobConfig.SASURL.IsRedacted()
|
||||
case sdk.CryptedFilesystemProvider:
|
||||
if f.CryptConfig.Passphrase.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
return f.CryptConfig.Passphrase.IsRedacted()
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
if f.SFTPConfig.Password.IsRedacted() {
|
||||
return true
|
||||
|
@ -190,9 +210,12 @@ func (f *Filesystem) HasRedactedSecret() bool {
|
|||
if f.SFTPConfig.PrivateKey.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
if f.SFTPConfig.KeyPassphrase.IsRedacted() {
|
||||
return f.SFTPConfig.KeyPassphrase.IsRedacted()
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
if f.HTTPConfig.Password.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
return f.HTTPConfig.APIKey.IsRedacted()
|
||||
}
|
||||
|
||||
return false
|
||||
|
@ -211,6 +234,8 @@ func (f *Filesystem) HideConfidentialData() {
|
|||
f.CryptConfig.HideConfidentialData()
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
f.SFTPConfig.HideConfidentialData()
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
f.HTTPConfig.HideConfidentialData()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -280,6 +305,15 @@ func (f *Filesystem) GetACopy() Filesystem {
|
|||
PrivateKey: f.SFTPConfig.PrivateKey.Clone(),
|
||||
KeyPassphrase: f.SFTPConfig.KeyPassphrase.Clone(),
|
||||
},
|
||||
HTTPConfig: HTTPFsConfig{
|
||||
BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
|
||||
Endpoint: f.HTTPConfig.Endpoint,
|
||||
Username: f.HTTPConfig.Username,
|
||||
SkipTLSVerify: f.HTTPConfig.SkipTLSVerify,
|
||||
},
|
||||
Password: f.HTTPConfig.Password.Clone(),
|
||||
APIKey: f.HTTPConfig.APIKey.Clone(),
|
||||
},
|
||||
}
|
||||
if len(f.SFTPConfig.Fingerprints) > 0 {
|
||||
fs.SFTPConfig.Fingerprints = make([]string, len(f.SFTPConfig.Fingerprints))
|
||||
|
|
|
@ -94,17 +94,19 @@ func (v *BaseVirtualFolder) GetQuotaSummary() string {
|
|||
func (v *BaseVirtualFolder) GetStorageDescrition() string {
|
||||
switch v.FsConfig.Provider {
|
||||
case sdk.LocalFilesystemProvider:
|
||||
return fmt.Sprintf("Local: %v", v.MappedPath)
|
||||
return fmt.Sprintf("Local: %s", v.MappedPath)
|
||||
case sdk.S3FilesystemProvider:
|
||||
return fmt.Sprintf("S3: %v", v.FsConfig.S3Config.Bucket)
|
||||
return fmt.Sprintf("S3: %s", v.FsConfig.S3Config.Bucket)
|
||||
case sdk.GCSFilesystemProvider:
|
||||
return fmt.Sprintf("GCS: %v", v.FsConfig.GCSConfig.Bucket)
|
||||
return fmt.Sprintf("GCS: %s", v.FsConfig.GCSConfig.Bucket)
|
||||
case sdk.AzureBlobFilesystemProvider:
|
||||
return fmt.Sprintf("AzBlob: %v", v.FsConfig.AzBlobConfig.Container)
|
||||
return fmt.Sprintf("AzBlob: %s", v.FsConfig.AzBlobConfig.Container)
|
||||
case sdk.CryptedFilesystemProvider:
|
||||
return fmt.Sprintf("Encrypted: %v", v.MappedPath)
|
||||
return fmt.Sprintf("Encrypted: %s", v.MappedPath)
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
return fmt.Sprintf("SFTP: %v", v.FsConfig.SFTPConfig.Endpoint)
|
||||
return fmt.Sprintf("SFTP: %s", v.FsConfig.SFTPConfig.Endpoint)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
return fmt.Sprintf("HTTP: %s", v.FsConfig.HTTPConfig.Endpoint)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
@ -128,6 +130,8 @@ func (v *BaseVirtualFolder) hideConfidentialData() {
|
|||
v.FsConfig.CryptConfig.HideConfidentialData()
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
v.FsConfig.SFTPConfig.HideConfidentialData()
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
v.FsConfig.HTTPConfig.HideConfidentialData()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -141,38 +145,7 @@ func (v *BaseVirtualFolder) PrepareForRendering() {
|
|||
|
||||
// HasRedactedSecret returns true if the folder has a redacted secret
|
||||
func (v *BaseVirtualFolder) HasRedactedSecret() bool {
|
||||
switch v.FsConfig.Provider {
|
||||
case sdk.S3FilesystemProvider:
|
||||
if v.FsConfig.S3Config.AccessSecret.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
case sdk.GCSFilesystemProvider:
|
||||
if v.FsConfig.GCSConfig.Credentials.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
case sdk.AzureBlobFilesystemProvider:
|
||||
if v.FsConfig.AzBlobConfig.AccountKey.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
if v.FsConfig.AzBlobConfig.SASURL.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
case sdk.CryptedFilesystemProvider:
|
||||
if v.FsConfig.CryptConfig.Passphrase.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
if v.FsConfig.SFTPConfig.Password.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
if v.FsConfig.SFTPConfig.PrivateKey.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
if v.FsConfig.SFTPConfig.KeyPassphrase.IsRedacted() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return v.FsConfig.HasRedactedSecret()
|
||||
}
|
||||
|
||||
// VirtualFolder defines a mapping between an SFTPGo exposed virtual path and a
|
||||
|
@ -203,6 +176,8 @@ func (v *VirtualFolder) GetFilesystem(connectionID string, forbiddenSelfUsers []
|
|||
return NewCryptFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.CryptConfig)
|
||||
case sdk.SFTPFilesystemProvider:
|
||||
return NewSFTPFs(connectionID, v.VirtualPath, v.MappedPath, forbiddenSelfUsers, v.FsConfig.SFTPConfig)
|
||||
case sdk.HTTPFilesystemProvider:
|
||||
return NewHTTPFs(connectionID, v.MappedPath, v.VirtualPath, v.FsConfig.HTTPConfig)
|
||||
default:
|
||||
return NewOsFs(connectionID, v.MappedPath, v.VirtualPath), nil
|
||||
}
|
||||
|
|
712
vfs/httpfs.go
Normal file
712
vfs/httpfs.go
Normal file
|
@ -0,0 +1,712 @@
|
|||
package vfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/eikenb/pipeat"
|
||||
"github.com/pkg/sftp"
|
||||
"github.com/sftpgo/sdk"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/kms"
|
||||
"github.com/drakkan/sftpgo/v2/logger"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
// httpFsName is the name for the HTTP Fs implementation
|
||||
httpFsName = "httpfs"
|
||||
)
|
||||
|
||||
// HTTPFsConfig defines the configuration for HTTP based filesystem
|
||||
type HTTPFsConfig struct {
|
||||
sdk.BaseHTTPFsConfig
|
||||
Password *kms.Secret `json:"password,omitempty"`
|
||||
APIKey *kms.Secret `json:"api_key,omitempty"`
|
||||
}
|
||||
|
||||
// HideConfidentialData hides confidential data
|
||||
func (c *HTTPFsConfig) HideConfidentialData() {
|
||||
if c.Password != nil {
|
||||
c.Password.Hide()
|
||||
}
|
||||
if c.APIKey != nil {
|
||||
c.APIKey.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPFsConfig) setNilSecretsIfEmpty() {
|
||||
if c.Password != nil && c.Password.IsEmpty() {
|
||||
c.Password = nil
|
||||
}
|
||||
if c.APIKey != nil && c.APIKey.IsEmpty() {
|
||||
c.APIKey = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPFsConfig) setEmptyCredentialsIfNil() {
|
||||
if c.Password == nil {
|
||||
c.Password = kms.NewEmptySecret()
|
||||
}
|
||||
if c.APIKey == nil {
|
||||
c.APIKey = kms.NewEmptySecret()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HTTPFsConfig) isEqual(other *HTTPFsConfig) bool {
|
||||
if c.Endpoint != other.Endpoint {
|
||||
return false
|
||||
}
|
||||
if c.Username != other.Username {
|
||||
return false
|
||||
}
|
||||
if c.SkipTLSVerify != other.SkipTLSVerify {
|
||||
return false
|
||||
}
|
||||
c.setEmptyCredentialsIfNil()
|
||||
other.setEmptyCredentialsIfNil()
|
||||
if !c.Password.IsEqual(other.Password) {
|
||||
return false
|
||||
}
|
||||
return c.APIKey.IsEqual(other.APIKey)
|
||||
}
|
||||
|
||||
// validate returns an error if the configuration is not valid
|
||||
func (c *HTTPFsConfig) validate() error {
|
||||
c.setEmptyCredentialsIfNil()
|
||||
if c.Endpoint == "" {
|
||||
return errors.New("httpfs: endpoint cannot be empty")
|
||||
}
|
||||
c.Endpoint = strings.TrimRight(c.Endpoint, "/")
|
||||
_, err := url.Parse(c.Endpoint)
|
||||
if err != nil {
|
||||
return fmt.Errorf("httpfs: invalid endpoint: %w", err)
|
||||
}
|
||||
if c.Password.IsEncrypted() && !c.Password.IsValid() {
|
||||
return errors.New("httpfs: invalid encrypted password")
|
||||
}
|
||||
if !c.Password.IsEmpty() && !c.Password.IsValidInput() {
|
||||
return errors.New("httpfs: invalid password")
|
||||
}
|
||||
if c.APIKey.IsEncrypted() && !c.APIKey.IsValid() {
|
||||
return errors.New("httpfs: invalid encrypted API key")
|
||||
}
|
||||
if !c.APIKey.IsEmpty() && !c.APIKey.IsValidInput() {
|
||||
return errors.New("httpfs: invalid API key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text
|
||||
func (c *HTTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not validate HTTP fs config: %v", err))
|
||||
}
|
||||
if c.Password.IsPlain() {
|
||||
c.Password.SetAdditionalData(additionalData)
|
||||
if err := c.Password.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs password: %v", err))
|
||||
}
|
||||
}
|
||||
if c.APIKey.IsPlain() {
|
||||
c.APIKey.SetAdditionalData(additionalData)
|
||||
if err := c.APIKey.Encrypt(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not encrypt HTTP fs API key: %v", err))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HTTPFs is a Fs implementation for the SFTPGo HTTP filesystem backend
|
||||
type HTTPFs struct {
|
||||
connectionID string
|
||||
localTempDir string
|
||||
// if not empty this fs is mouted as virtual folder in the specified path
|
||||
mountPath string
|
||||
config *HTTPFsConfig
|
||||
client *http.Client
|
||||
ctxTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewHTTPFs returns an HTTPFs object that allows to interact with SFTPGo HTTP filesystem backends
|
||||
func NewHTTPFs(connectionID, localTempDir, mountPath string, config HTTPFsConfig) (Fs, error) {
|
||||
if localTempDir == "" {
|
||||
if tempPath != "" {
|
||||
localTempDir = tempPath
|
||||
} else {
|
||||
localTempDir = filepath.Clean(os.TempDir())
|
||||
}
|
||||
}
|
||||
if err := config.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !config.Password.IsEmpty() {
|
||||
if err := config.Password.TryDecrypt(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !config.APIKey.IsEmpty() {
|
||||
if err := config.APIKey.TryDecrypt(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
fs := &HTTPFs{
|
||||
connectionID: connectionID,
|
||||
localTempDir: localTempDir,
|
||||
mountPath: mountPath,
|
||||
config: &config,
|
||||
ctxTimeout: 30 * time.Second,
|
||||
}
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.MaxResponseHeaderBytes = 1 << 16
|
||||
transport.WriteBufferSize = 1 << 16
|
||||
transport.ReadBufferSize = 1 << 16
|
||||
if config.SkipTLSVerify {
|
||||
if transport.TLSClientConfig != nil {
|
||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
||||
} else {
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
InsecureSkipVerify: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
fs.client = &http.Client{
|
||||
Transport: transport,
|
||||
}
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
// Name returns the name for the Fs implementation
|
||||
func (fs *HTTPFs) Name() string {
|
||||
return fmt.Sprintf("%v %#v", httpFsName, fs.config.Endpoint)
|
||||
}
|
||||
|
||||
// ConnectionID returns the connection ID associated to this Fs implementation
|
||||
func (fs *HTTPFs) ConnectionID() string {
|
||||
return fs.connectionID
|
||||
}
|
||||
|
||||
// Stat returns a FileInfo describing the named file
|
||||
func (fs *HTTPFs) Stat(name string) (os.FileInfo, error) {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "stat", name, "", "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response statResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.getFileInfo(), nil
|
||||
}
|
||||
|
||||
// Lstat returns a FileInfo describing the named file
|
||||
func (fs *HTTPFs) Lstat(name string) (os.FileInfo, error) {
|
||||
return fs.Stat(name)
|
||||
}
|
||||
|
||||
// Open opens the named file for reading
|
||||
func (fs *HTTPFs) Open(name string, offset int64) (File, *pipeat.PipeReaderAt, func(), error) {
|
||||
r, w, err := pipeat.PipeInDir(fs.localTempDir)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ctx, cancelFn := context.WithCancel(context.Background())
|
||||
|
||||
var queryString string
|
||||
if offset > 0 {
|
||||
queryString = fmt.Sprintf("?offset=%d", offset)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "open", name, queryString, "", nil)
|
||||
if err != nil {
|
||||
fsLog(fs, logger.LevelError, "download error, path %q, err: %v", name, err)
|
||||
w.CloseWithError(err) //nolint:errcheck
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
n, err := io.Copy(w, resp.Body)
|
||||
w.CloseWithError(err) //nolint:errcheck
|
||||
fsLog(fs, logger.LevelDebug, "download completed, path %q size: %v, err: %+v", name, n, err)
|
||||
}()
|
||||
|
||||
return nil, r, cancelFn, nil
|
||||
}
|
||||
|
||||
// Create creates or opens the named file for writing
|
||||
func (fs *HTTPFs) Create(name string, flag int) (File, *PipeWriter, func(), error) {
|
||||
r, w, err := pipeat.PipeInDir(fs.localTempDir)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
p := NewPipeWriter(w)
|
||||
ctx, cancelFn := context.WithCancel(context.Background())
|
||||
|
||||
var queryString string
|
||||
if flag > 0 {
|
||||
queryString = fmt.Sprintf("?flags=%d", flag)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer cancelFn()
|
||||
|
||||
contentType := mime.TypeByExtension(path.Ext(name))
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodPost, "create", name, queryString, contentType,
|
||||
&wrapReader{reader: r})
|
||||
if err != nil {
|
||||
fsLog(fs, logger.LevelError, "upload error, path %q, err: %v", name, err)
|
||||
r.CloseWithError(err) //nolint:errcheck
|
||||
p.Done(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
r.CloseWithError(err) //nolint:errcheck
|
||||
p.Done(err)
|
||||
fsLog(fs, logger.LevelDebug, "upload completed, path: %q, readed bytes: %d", name, r.GetReadedBytes())
|
||||
}()
|
||||
|
||||
return nil, p, cancelFn, nil
|
||||
}
|
||||
|
||||
// Rename renames (moves) source to target.
|
||||
func (fs *HTTPFs) Rename(source, target string) error {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
queryString := fmt.Sprintf("?target=%s", url.QueryEscape(target))
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "rename", source, queryString, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove removes the named file or (empty) directory.
|
||||
func (fs *HTTPFs) Remove(name string, isDir bool) error {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodDelete, "remove", name, "", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mkdir creates a new directory with the specified name and default permissions
|
||||
func (fs *HTTPFs) Mkdir(name string) error {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodPost, "mkdir", name, "", "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Symlink creates source as a symbolic link to target.
|
||||
func (*HTTPFs) Symlink(source, target string) error {
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Readlink returns the destination of the named symbolic link
|
||||
func (*HTTPFs) Readlink(name string) (string, error) {
|
||||
return "", ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chown changes the numeric uid and gid of the named file.
|
||||
func (fs *HTTPFs) Chown(name string, uid int, gid int) error {
|
||||
/*ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
queryString := fmt.Sprintf("?uid=%d&gid=%d", uid, gid)
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chown", name, queryString, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil*/
|
||||
return ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// Chmod changes the mode of the named file to mode.
|
||||
func (fs *HTTPFs) Chmod(name string, mode os.FileMode) error {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
queryString := fmt.Sprintf("?mode=%d", mode)
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chmod", name, queryString, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Chtimes changes the access and modification times of the named file.
|
||||
func (fs *HTTPFs) Chtimes(name string, atime, mtime time.Time, isUploading bool) error {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
queryString := fmt.Sprintf("?access_time=%s&modification_time=%s", atime.UTC().Format(time.RFC3339),
|
||||
mtime.UTC().Format(time.RFC3339))
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "chtimes", name, queryString, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Truncate changes the size of the named file.
|
||||
// Truncate by path is not supported, while truncating an opened
|
||||
// file is handled inside base transfer
|
||||
func (fs *HTTPFs) Truncate(name string, size int64) error {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
queryString := fmt.Sprintf("?size=%d", size)
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodPatch, "truncate", name, queryString, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadDir reads the directory named by dirname and returns
|
||||
// a list of directory entries.
|
||||
func (fs *HTTPFs) ReadDir(dirname string) ([]os.FileInfo, error) {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "readdir", dirname, "", "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response []statResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]os.FileInfo, 0, len(response))
|
||||
for _, stat := range response {
|
||||
result = append(result, stat.getFileInfo())
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// IsUploadResumeSupported returns true if resuming uploads is supported.
|
||||
func (*HTTPFs) IsUploadResumeSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsAtomicUploadSupported returns true if atomic upload is supported.
|
||||
func (*HTTPFs) IsAtomicUploadSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsNotExist returns a boolean indicating whether the error is known to
|
||||
// report that a file or directory does not exist
|
||||
func (*HTTPFs) IsNotExist(err error) bool {
|
||||
return errors.Is(err, fs.ErrNotExist)
|
||||
}
|
||||
|
||||
// IsPermission returns a boolean indicating whether the error is known to
|
||||
// report that permission is denied.
|
||||
func (*HTTPFs) IsPermission(err error) bool {
|
||||
return errors.Is(err, fs.ErrPermission)
|
||||
}
|
||||
|
||||
// IsNotSupported returns true if the error indicate an unsupported operation
|
||||
func (*HTTPFs) IsNotSupported(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return err == ErrVfsUnsupported
|
||||
}
|
||||
|
||||
// CheckRootPath creates the specified local root directory if it does not exists
|
||||
func (fs *HTTPFs) CheckRootPath(username string, uid int, gid int) bool {
|
||||
// we need a local directory for temporary files
|
||||
osFs := NewOsFs(fs.ConnectionID(), fs.localTempDir, "")
|
||||
return osFs.CheckRootPath(username, uid, gid)
|
||||
}
|
||||
|
||||
// ScanRootDirContents returns the number of files and their size
|
||||
func (fs *HTTPFs) ScanRootDirContents() (int, int64, error) {
|
||||
return fs.GetDirSize("/")
|
||||
}
|
||||
|
||||
// CheckMetadata checks the metadata consistency
|
||||
func (*HTTPFs) CheckMetadata() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetDirSize returns the number of files and the size for a folder
|
||||
// including any subfolders
|
||||
func (fs *HTTPFs) GetDirSize(dirname string) (int, int64, error) {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "dirsize", dirname, "", "", nil)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response dirSizeResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return response.Files, response.Size, nil
|
||||
}
|
||||
|
||||
// GetAtomicUploadPath returns the path to use for an atomic upload.
|
||||
func (*HTTPFs) GetAtomicUploadPath(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetRelativePath returns the path for a file relative to the user's home dir.
|
||||
// This is the path as seen by SFTPGo users
|
||||
func (fs *HTTPFs) GetRelativePath(name string) string {
|
||||
rel := path.Clean(name)
|
||||
if rel == "." {
|
||||
rel = ""
|
||||
}
|
||||
if !path.IsAbs(rel) {
|
||||
rel = "/" + rel
|
||||
}
|
||||
if fs.mountPath != "" {
|
||||
rel = path.Join(fs.mountPath, rel)
|
||||
}
|
||||
return rel
|
||||
}
|
||||
|
||||
// Walk walks the file tree rooted at root, calling walkFn for each file or
|
||||
// directory in the tree, including root. The result are unordered
|
||||
func (fs *HTTPFs) Walk(root string, walkFn filepath.WalkFunc) error {
|
||||
info, err := fs.Lstat(root)
|
||||
if err != nil {
|
||||
return walkFn(root, nil, err)
|
||||
}
|
||||
return fs.walk(root, info, walkFn)
|
||||
}
|
||||
|
||||
// Join joins any number of path elements into a single path
|
||||
func (*HTTPFs) Join(elem ...string) string {
|
||||
return strings.TrimPrefix(path.Join(elem...), "/")
|
||||
}
|
||||
|
||||
// HasVirtualFolders returns true if folders are emulated
|
||||
func (*HTTPFs) HasVirtualFolders() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ResolvePath returns the matching filesystem path for the specified virtual path
|
||||
func (fs *HTTPFs) ResolvePath(virtualPath string) (string, error) {
|
||||
if fs.mountPath != "" {
|
||||
virtualPath = strings.TrimPrefix(virtualPath, fs.mountPath)
|
||||
}
|
||||
if !path.IsAbs(virtualPath) {
|
||||
virtualPath = path.Clean("/" + virtualPath)
|
||||
}
|
||||
return virtualPath, nil
|
||||
}
|
||||
|
||||
// GetMimeType returns the content type
|
||||
func (fs *HTTPFs) GetMimeType(name string) (string, error) {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "stat", name, "", "", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response mimeTypeResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return response.Mime, nil
|
||||
}
|
||||
|
||||
// Close closes the fs
|
||||
func (fs *HTTPFs) Close() error {
|
||||
fs.client.CloseIdleConnections()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAvailableDiskSize returns the available size for the specified path
|
||||
func (fs *HTTPFs) GetAvailableDiskSize(dirName string) (*sftp.StatVFS, error) {
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(fs.ctxTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
resp, err := fs.sendHTTPRequest(ctx, http.MethodGet, "statvfs", dirName, "", "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var response statVFSResponse
|
||||
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.toSFTPStatVFS(), nil
|
||||
}
|
||||
|
||||
func (fs *HTTPFs) sendHTTPRequest(ctx context.Context, method, base, name, queryString, contentType string,
|
||||
body io.Reader,
|
||||
) (*http.Response, error) {
|
||||
url := fmt.Sprintf("%s/%s/%s%s", fs.config.Endpoint, base, url.PathEscape(name), queryString)
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
if fs.config.APIKey.GetPayload() != "" {
|
||||
req.Header.Set("X-API-KEY", fs.config.APIKey.GetPayload())
|
||||
}
|
||||
if fs.config.Username != "" || fs.config.Password.GetPayload() != "" {
|
||||
req.SetBasicAuth(fs.config.Username, fs.config.Password.GetPayload())
|
||||
}
|
||||
resp, err := fs.client.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to send HTTP request to URL %v: %w", url, err)
|
||||
}
|
||||
if err = getErrorFromResponseCode(resp.StatusCode); err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// walk recursively descends path, calling walkFn.
|
||||
func (fs *HTTPFs) walk(filePath string, info fs.FileInfo, walkFn filepath.WalkFunc) error {
|
||||
if !info.IsDir() {
|
||||
return walkFn(filePath, info, nil)
|
||||
}
|
||||
files, err := fs.ReadDir(filePath)
|
||||
err1 := walkFn(filePath, info, err)
|
||||
if err != nil || err1 != nil {
|
||||
return err1
|
||||
}
|
||||
for _, fi := range files {
|
||||
objName := path.Join(filePath, fi.Name())
|
||||
err = fs.walk(objName, fi, walkFn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getErrorFromResponseCode(code int) error {
|
||||
switch code {
|
||||
case 401, 403:
|
||||
return os.ErrPermission
|
||||
case 404:
|
||||
return os.ErrNotExist
|
||||
case 501:
|
||||
return ErrVfsUnsupported
|
||||
case 200, 201:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected response code: %v", code)
|
||||
}
|
||||
}
|
||||
|
||||
type wrapReader struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
func (r *wrapReader) Read(p []byte) (n int, err error) {
|
||||
return r.reader.Read(p)
|
||||
}
|
||||
|
||||
type statResponse struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Mode uint32 `json:"mode"`
|
||||
LastModified time.Time `json:"last_modified"`
|
||||
}
|
||||
|
||||
func (s *statResponse) getFileInfo() os.FileInfo {
|
||||
info := NewFileInfo(s.Name, false, s.Size, s.LastModified, false)
|
||||
info.SetMode(fs.FileMode(s.Mode))
|
||||
return info
|
||||
}
|
||||
|
||||
type dirSizeResponse struct {
|
||||
Files int `json:"files"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type mimeTypeResponse struct {
|
||||
Mime string `json:"mime"`
|
||||
}
|
||||
|
||||
type statVFSResponse struct {
|
||||
ID uint32 `json:"-"`
|
||||
Bsize uint64 `json:"bsize"`
|
||||
Frsize uint64 `json:"frsize"`
|
||||
Blocks uint64 `json:"blocks"`
|
||||
Bfree uint64 `json:"bfree"`
|
||||
Bavail uint64 `json:"bavail"`
|
||||
Files uint64 `json:"files"`
|
||||
Ffree uint64 `json:"ffree"`
|
||||
Favail uint64 `json:"favail"`
|
||||
Fsid uint64 `json:"fsid"`
|
||||
Flag uint64 `json:"flag"`
|
||||
Namemax uint64 `json:"namemax"`
|
||||
}
|
||||
|
||||
func (s *statVFSResponse) toSFTPStatVFS() *sftp.StatVFS {
|
||||
return &sftp.StatVFS{
|
||||
Bsize: s.Bsize,
|
||||
Frsize: s.Frsize,
|
||||
Blocks: s.Blocks,
|
||||
Bfree: s.Bfree,
|
||||
Bavail: s.Bavail,
|
||||
Files: s.Files,
|
||||
Ffree: s.Ffree,
|
||||
Favail: s.Ffree,
|
||||
Flag: s.Flag,
|
||||
Namemax: s.Namemax,
|
||||
}
|
||||
}
|
|
@ -288,7 +288,7 @@ func (*OsFs) Join(elem ...string) string {
|
|||
// ResolvePath returns the matching filesystem path for the specified sftp path
|
||||
func (fs *OsFs) ResolvePath(virtualPath string) (string, error) {
|
||||
if !filepath.IsAbs(fs.rootDir) {
|
||||
return "", fmt.Errorf("invalid root path: %v", fs.rootDir)
|
||||
return "", fmt.Errorf("invalid root path %q", fs.rootDir)
|
||||
}
|
||||
if fs.mountPath != "" {
|
||||
virtualPath = strings.TrimPrefix(virtualPath, fs.mountPath)
|
||||
|
|
|
@ -168,7 +168,7 @@ func (c *SFTPFsConfig) validateCredentials() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ValidateAndEncryptCredentials encrypts password and/or private key if they are in plain text
|
||||
// ValidateAndEncryptCredentials validates the config and encrypts credentials if they are in plain text
|
||||
func (c *SFTPFsConfig) ValidateAndEncryptCredentials(additionalData string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return util.NewValidationError(fmt.Sprintf("could not validate SFTP fs config: %v", err))
|
||||
|
|
10
vfs/vfs.go
10
vfs/vfs.go
|
@ -669,6 +669,11 @@ func IsSFTPFs(fs Fs) bool {
|
|||
return strings.HasPrefix(fs.Name(), sftpFsName)
|
||||
}
|
||||
|
||||
// IsHTTPFs returns true if fs is an HTTP filesystem
|
||||
func IsHTTPFs(fs Fs) bool {
|
||||
return strings.HasPrefix(fs.Name(), httpFsName)
|
||||
}
|
||||
|
||||
// IsBufferedSFTPFs returns true if this is a buffered SFTP filesystem
|
||||
func IsBufferedSFTPFs(fs Fs) bool {
|
||||
if !IsSFTPFs(fs) {
|
||||
|
@ -693,6 +698,11 @@ func IsLocalOrSFTPFs(fs Fs) bool {
|
|||
return IsLocalOsFs(fs) || IsSFTPFs(fs)
|
||||
}
|
||||
|
||||
// HasTruncateSupport returns true if the fs supports truncate files
|
||||
func HasTruncateSupport(fs Fs) bool {
|
||||
return IsLocalOsFs(fs) || IsSFTPFs(fs) || IsHTTPFs(fs)
|
||||
}
|
||||
|
||||
// HasOpenRWSupport returns true if the fs can open a file
|
||||
// for reading and writing at the same time
|
||||
func HasOpenRWSupport(fs Fs) bool {
|
||||
|
|
|
@ -254,7 +254,7 @@ func (c *Connection) handleUploadToExistingFile(fs vfs.Fs, resolvedPath, filePat
|
|||
}
|
||||
initialSize := int64(0)
|
||||
truncatedSize := int64(0) // bytes truncated and not included in quota
|
||||
if vfs.IsLocalOrSFTPFs(fs) {
|
||||
if vfs.HasTruncateSupport(fs) {
|
||||
vfolder, err := c.User.GetVirtualFolderForPath(path.Dir(requestPath))
|
||||
if err == nil {
|
||||
dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, 0, -fileSize, false) //nolint:errcheck
|
||||
|
|
Loading…
Reference in a new issue