mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-25 09:00:27 +00:00
WebClient: do not silently overwrite files/directories
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
e35e07acdb
commit
3121c35437
20 changed files with 564 additions and 90 deletions
|
@ -352,7 +352,6 @@ The configuration file contains the following sections:
|
|||
- `content_security_policy`, string. Allows to set the `Content-Security-Policy` header value. Default: blank.
|
||||
- `permissions_policy`, string. Allows to set the `Permissions-Policy` header value. Default: blank.
|
||||
- `cross_origin_opener_policy`, string. Allows to set the `Cross-Origin-Opener-Policy` header value. Default: blank.
|
||||
- `expect_ct_header`, string. Allows to set the `Expect-CT` header value. Default: blank.
|
||||
- `branding`, struct. Defines the supported customizations to suit your brand. It contains the `web_admin` and `web_client` structs that define customizations for the WebAdmin and the WebClient UIs. Each customization struct contains the following fields:
|
||||
- `name`, string. Defines the UI name
|
||||
- `short_name`, string. Defines the short name to show next to the logo image and on the login page
|
||||
|
|
6
go.mod
6
go.mod
|
@ -48,7 +48,7 @@ require (
|
|||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pquerna/otp v1.4.0
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/cors v1.10.1
|
||||
github.com/rs/xid v1.5.0
|
||||
|
@ -61,13 +61,13 @@ require (
|
|||
github.com/stretchr/testify v1.8.4
|
||||
github.com/studio-b12/gowebdav v0.9.0
|
||||
github.com/subosito/gotenv v1.6.0
|
||||
github.com/unrolled/secure v1.13.0
|
||||
github.com/unrolled/secure v1.14.0
|
||||
github.com/wagslane/go-password-validator v0.3.0
|
||||
github.com/wneessen/go-mail v0.4.1-0.20230815095916-0189acf1e45f
|
||||
github.com/yl2chen/cidranger v1.0.3-0.20210928021809-d1cb2c52f37a
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
gocloud.dev v0.35.0
|
||||
gocloud.dev v0.36.0
|
||||
golang.org/x/crypto v0.17.0
|
||||
golang.org/x/net v0.19.0
|
||||
golang.org/x/oauth2 v0.15.0
|
||||
|
|
12
go.sum
12
go.sum
|
@ -327,8 +327,8 @@ github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
|||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
|
||||
github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
|
@ -397,8 +397,8 @@ github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5I
|
|||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
|
||||
github.com/unrolled/secure v1.13.0 h1:sdr3Phw2+f8Px8HE5sd1EHdj1aV3yUwed/uZXChLFsk=
|
||||
github.com/unrolled/secure v1.13.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/unrolled/secure v1.14.0 h1:u9vJTU/pR4Bny0ntLUMxdfLtmIRGvQf2sEFuA0TG9AE=
|
||||
github.com/unrolled/secure v1.14.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSVP061Ry2PX0/ON6I=
|
||||
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
|
||||
github.com/wneessen/go-mail v0.4.1-0.20230815095916-0189acf1e45f h1:IYzF42VUzA6es43UO0q8rdB1+d7fge5ALPOVKN192jA=
|
||||
|
@ -429,8 +429,8 @@ go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
|||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
gocloud.dev v0.35.0 h1:x/Gtt5OJdT4j+ir1AXAIXb7bBnFawXAAaJptCUGk3HU=
|
||||
gocloud.dev v0.35.0/go.mod h1:wbyF+BhfdtLWyUtVEWRW13hFLb1vXnV2ovEhYGQe3ck=
|
||||
gocloud.dev v0.36.0 h1:q5zoXux4xkOZP473e1EZbG8Gq9f0vlg1VNH5Du/ybus=
|
||||
gocloud.dev v0.36.0/go.mod h1:bLxah6JQVKBaIxzsr5BQLYB4IYdWHkMZdzCXlo6F0gg=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b h1:kLiC65FbiHWFAOu+lxwNPujcsl8VYyTYYEZnsOO1WK4=
|
||||
golang.org/x/exp v0.0.0-20231226003508-02704c960a9b/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
|
|
|
@ -1164,7 +1164,7 @@ func (c *BaseConnection) isRenamePermitted(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
|
|||
virtualTargetPath string, fi os.FileInfo,
|
||||
) bool {
|
||||
if !c.IsSameResource(virtualSourcePath, virtualTargetPath) {
|
||||
c.Log(logger.LevelInfo, "rename %#q->%q is not allowed: the paths must be on the same resource",
|
||||
c.Log(logger.LevelInfo, "rename %q->%q is not allowed: the paths must be on the same resource",
|
||||
virtualSourcePath, virtualTargetPath)
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -147,7 +147,6 @@ var (
|
|||
ContentSecurityPolicy: "",
|
||||
PermissionsPolicy: "",
|
||||
CrossOriginOpenerPolicy: "",
|
||||
ExpectCTHeader: "",
|
||||
},
|
||||
Branding: httpd.Branding{},
|
||||
}
|
||||
|
@ -1542,12 +1541,6 @@ func getHTTPDSecurityConfFromEnv(idx int) (httpd.SecurityConf, bool) { //nolint:
|
|||
isSet = true
|
||||
}
|
||||
|
||||
expectCTHeader, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__SECURITY__EXPECT_CT_HEADER", idx))
|
||||
if ok {
|
||||
result.ExpectCTHeader = expectCTHeader
|
||||
isSet = true
|
||||
}
|
||||
|
||||
return result, isSet
|
||||
}
|
||||
|
||||
|
|
|
@ -1237,7 +1237,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY", "script-src $NONCE")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY", "fullscreen=(), geolocation=()")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY", "same-origin")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER", `max-age=86400, enforce, report-uri="https://foo.example/report"`)
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH", "path1")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH", "favicon.ico")
|
||||
|
@ -1303,7 +1302,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CONTENT_SECURITY_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BRANDING__WEB_ADMIN__FAVICON_PATH")
|
||||
|
@ -1414,7 +1412,6 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, "script-src $NONCE", bindings[2].Security.ContentSecurityPolicy)
|
||||
require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy)
|
||||
require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy)
|
||||
require.Equal(t, `max-age=86400, enforce, report-uri="https://foo.example/report"`, bindings[2].Security.ExpectCTHeader)
|
||||
require.Equal(t, "favicon.ico", bindings[2].Branding.WebAdmin.FaviconPath)
|
||||
require.Equal(t, "logo.png", bindings[2].Branding.WebClient.LogoPath)
|
||||
require.Equal(t, "login_image.png", bindings[2].Branding.WebAdmin.LoginImagePath)
|
||||
|
|
|
@ -201,7 +201,7 @@ func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share, r)
|
||||
name, err := getBrowsableSharedPath(share.Paths[0], r)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
|
@ -232,7 +232,7 @@ func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http
|
|||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share, r)
|
||||
name, err := getBrowsableSharedPath(share.Paths[0], r)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
|
@ -552,7 +552,10 @@ func validateBrowsableShare(share dataprovider.Share, connection *Connection) er
|
|||
basePath := share.Paths[0]
|
||||
info, err := connection.Stat(basePath, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to check the share directory: %w", err)
|
||||
return util.NewI18nError(
|
||||
fmt.Errorf("unable to check the share directory: %w", err),
|
||||
util.I18nErrorShareInvalidPath,
|
||||
)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return util.NewI18nError(
|
||||
|
@ -563,12 +566,12 @@ func validateBrowsableShare(share dataprovider.Share, connection *Connection) er
|
|||
return nil
|
||||
}
|
||||
|
||||
func getBrowsableSharedPath(share dataprovider.Share, r *http.Request) (string, error) {
|
||||
name := util.CleanPath(path.Join(share.Paths[0], r.URL.Query().Get("path")))
|
||||
if share.Paths[0] == "/" {
|
||||
func getBrowsableSharedPath(shareBasePath string, r *http.Request) (string, error) {
|
||||
name := util.CleanPath(path.Join(shareBasePath, r.URL.Query().Get("path")))
|
||||
if shareBasePath == "/" {
|
||||
return name, nil
|
||||
}
|
||||
if name != share.Paths[0] && !strings.HasPrefix(name, share.Paths[0]+"/") {
|
||||
if name != shareBasePath && !strings.HasPrefix(name, shareBasePath+"/") {
|
||||
return "", util.NewI18nError(
|
||||
util.NewValidationError(fmt.Sprintf("Invalid path %q", r.URL.Query().Get("path"))),
|
||||
util.I18nErrorPathInvalid,
|
||||
|
|
|
@ -112,11 +112,11 @@ func getRespStatus(err error) int {
|
|||
func getMappedStatusCode(err error) int {
|
||||
var statusCode int
|
||||
switch {
|
||||
case errors.Is(err, os.ErrPermission):
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
statusCode = http.StatusForbidden
|
||||
case errors.Is(err, common.ErrReadQuotaExceeded):
|
||||
statusCode = http.StatusForbidden
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
statusCode = http.StatusNotFound
|
||||
case errors.Is(err, common.ErrQuotaExceeded):
|
||||
statusCode = http.StatusRequestEntityTooLarge
|
||||
|
|
|
@ -178,6 +178,7 @@ const (
|
|||
webClientResetPwdPathDefault = "/web/client/reset-password"
|
||||
webClientViewPDFPathDefault = "/web/client/viewpdf"
|
||||
webClientGetPDFPathDefault = "/web/client/getpdf"
|
||||
webClientExistPathDefault = "/web/client/exist"
|
||||
webStaticFilesPathDefault = "/static"
|
||||
webOpenAPIPathDefault = "/openapi"
|
||||
// MaxRestoreSize defines the max size for the loaddata input file
|
||||
|
@ -278,6 +279,7 @@ var (
|
|||
webClientResetPwdPath string
|
||||
webClientViewPDFPath string
|
||||
webClientGetPDFPath string
|
||||
webClientExistPath string
|
||||
webStaticFilesPath string
|
||||
webOpenAPIPath string
|
||||
// max upload size for http clients, 1GB by default
|
||||
|
@ -341,9 +343,7 @@ type SecurityConf struct {
|
|||
PermissionsPolicy string `json:"permissions_policy" mapstructure:"permissions_policy"`
|
||||
// CrossOriginOpenerPolicy allows to set the `Cross-Origin-Opener-Policy` header value. Default is "".
|
||||
CrossOriginOpenerPolicy string `json:"cross_origin_opener_policy" mapstructure:"cross_origin_opener_policy"`
|
||||
// ExpectCTHeader allows to set the Expect-CT header value. Default is "".
|
||||
ExpectCTHeader string `json:"expect_ct_header" mapstructure:"expect_ct_header"`
|
||||
proxyHeaders []string
|
||||
proxyHeaders []string
|
||||
}
|
||||
|
||||
func (s *SecurityConf) updateProxyHeaders() {
|
||||
|
@ -1110,6 +1110,7 @@ func updateWebClientURLs(baseURL string) {
|
|||
webClientResetPwdPath = path.Join(baseURL, webClientResetPwdPathDefault)
|
||||
webClientViewPDFPath = path.Join(baseURL, webClientViewPDFPathDefault)
|
||||
webClientGetPDFPath = path.Join(baseURL, webClientGetPDFPathDefault)
|
||||
webClientExistPath = path.Join(baseURL, webClientExistPathDefault)
|
||||
}
|
||||
|
||||
func updateWebAdminURLs(baseURL string) {
|
||||
|
|
|
@ -193,6 +193,7 @@ const (
|
|||
webClientResetPwdPath = "/web/client/reset-password"
|
||||
webClientViewPDFPath = "/web/client/viewpdf"
|
||||
webClientGetPDFPath = "/web/client/getpdf"
|
||||
webClientExistPath = "/web/client/exist"
|
||||
httpBaseURL = "http://127.0.0.1:8081"
|
||||
defaultRemoteAddr = "127.0.0.1:1234"
|
||||
sftpServerAddr = "127.0.0.1:8022"
|
||||
|
@ -13893,6 +13894,12 @@ func TestShareMaxSessions(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nError429Message)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, webClientPubSharesPath+"/"+objectID+"/browse/exist", nil)
|
||||
assert.NoError(t, err)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "invalid share scope")
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, sharesPath+"/"+objectID+"/files?path=afile", nil)
|
||||
assert.NoError(t, err)
|
||||
rr = executeRequest(req)
|
||||
|
@ -13958,6 +13965,27 @@ func TestShareMaxSessions(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusTooManyRequests, rr)
|
||||
assert.Contains(t, rr.Body.String(), "too many open sessions")
|
||||
|
||||
share = dataprovider.Share{
|
||||
Name: "test share max sessions read/write",
|
||||
Scope: dataprovider.ShareScopeReadWrite,
|
||||
Paths: []string{"/"},
|
||||
}
|
||||
asJSON, err = json.Marshal(share)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
objectID = rr.Header().Get("X-Object-ID")
|
||||
assert.NotEmpty(t, objectID)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, webClientPubSharesPath+"/"+objectID+"/browse/exist", nil)
|
||||
assert.NoError(t, err)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusTooManyRequests, rr)
|
||||
assert.Contains(t, rr.Body.String(), "too many open sessions")
|
||||
|
||||
common.Connections.Remove(connection.GetID())
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -14088,6 +14116,21 @@ func TestShareReadWrite(t *testing.T) {
|
|||
objectID := rr.Header().Get("X-Object-ID")
|
||||
assert.NotEmpty(t, objectID)
|
||||
|
||||
filesToCheck := make(map[string]any)
|
||||
filesToCheck["files"] = []string{testFileName}
|
||||
asJSON, err = json.Marshal(filesToCheck)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "/browse/exist?path=%2F"), bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
var fileList []any
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &fileList)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fileList, 0)
|
||||
|
||||
content := []byte("shared rw content")
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, testFileName), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
|
@ -14096,6 +14139,16 @@ func TestShareReadWrite(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
assert.FileExists(t, filepath.Join(user.GetHomeDir(), user.Filters.StartDirectory, testFileName))
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "/browse/exist?path=%2F"), bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
fileList = nil
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &fileList)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fileList, 1)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, path.Join(webClientPubSharesPath, objectID, "browse?path=%2F"), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
|
@ -14721,6 +14774,50 @@ func TestBrowseShares(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), util.I18nErrorShareBrowsePaths)
|
||||
|
||||
share = dataprovider.Share{
|
||||
Name: "test share rw",
|
||||
Scope: dataprovider.ShareScopeReadWrite,
|
||||
Paths: []string{"/missingdir"},
|
||||
MaxTokens: 0,
|
||||
}
|
||||
asJSON, err = json.Marshal(share)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
objectID = rr.Header().Get("X-Object-ID")
|
||||
assert.NotEmpty(t, objectID)
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "/browse/exist?path=%2F"), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "unable to check the share directory")
|
||||
|
||||
share = dataprovider.Share{
|
||||
Name: "test share rw",
|
||||
Scope: dataprovider.ShareScopeReadWrite,
|
||||
Paths: []string{shareDir},
|
||||
MaxTokens: 0,
|
||||
}
|
||||
asJSON, err = json.Marshal(share)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
objectID = rr.Header().Get("X-Object-ID")
|
||||
assert.NotEmpty(t, objectID)
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "/browse/exist?path=%2F.."), nil)
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid path")
|
||||
// share the root path
|
||||
share = dataprovider.Share{
|
||||
Name: "test share root",
|
||||
|
@ -15336,6 +15433,120 @@ func TestUserAPIKey(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebClientExistenceCheck(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
||||
webToken, err := getJWTWebClientTokenFromTestServer(defaultUsername, defaultPassword)
|
||||
assert.NoError(t, err)
|
||||
csrfToken, err := getCSRFToken(httpBaseURL + webLoginPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, webClientExistPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr) // no CSRF header
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, webClientExistPath, bytes.NewBuffer([]byte(`[]`)))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
|
||||
filesToCheck := make(map[string]any)
|
||||
filesToCheck["files"] = nil
|
||||
asJSON, err := json.Marshal(filesToCheck)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webClientExistPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "files to be checked are mandatory")
|
||||
|
||||
testFileName := "file.dat"
|
||||
testDirName := "adirname"
|
||||
filesToCheck["files"] = []string{testFileName}
|
||||
asJSON, err = json.Marshal(filesToCheck)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webClientExistPath+"?path=%2Fmissingdir", bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, webClientExistPath+"?path=%2F", bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
var fileList []any
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &fileList)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fileList, 0)
|
||||
|
||||
err = createTestFile(filepath.Join(user.GetHomeDir(), testFileName), 100)
|
||||
assert.NoError(t, err)
|
||||
err = os.Mkdir(filepath.Join(user.GetHomeDir(), testDirName), 0755)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, webClientExistPath+"?path=%2F", bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
fileList = nil
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &fileList)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fileList, 1)
|
||||
|
||||
filesToCheck["files"] = []string{testFileName, testDirName}
|
||||
asJSON, err = json.Marshal(filesToCheck)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webClientExistPath+"?path=%2F", bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
fileList = nil
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &fileList)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fileList, 2)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, webClientExistPath+"?path=%2F"+testDirName, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
fileList = nil
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &fileList)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fileList, 0)
|
||||
|
||||
user.Filters.DeniedProtocols = []string{common.ProtocolHTTP}
|
||||
_, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webClientExistPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
setCSRFHeaderForReq(req, csrfToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWebClientViewPDF(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -2858,35 +2858,35 @@ func TestBrowsableSharePaths(t *testing.T) {
|
|||
}
|
||||
req, err := http.NewRequest(http.MethodGet, "/share", nil)
|
||||
require.NoError(t, err)
|
||||
name, err := getBrowsableSharedPath(share, req)
|
||||
name, err := getBrowsableSharedPath(share.Paths[0], req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/", name)
|
||||
req, err = http.NewRequest(http.MethodGet, "/share?path=abc", nil)
|
||||
require.NoError(t, err)
|
||||
name, err = getBrowsableSharedPath(share, req)
|
||||
name, err = getBrowsableSharedPath(share.Paths[0], req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/abc", name)
|
||||
|
||||
share.Paths = []string{"/a/b/c"}
|
||||
req, err = http.NewRequest(http.MethodGet, "/share?path=abc", nil)
|
||||
require.NoError(t, err)
|
||||
name, err = getBrowsableSharedPath(share, req)
|
||||
name, err = getBrowsableSharedPath(share.Paths[0], req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/a/b/c/abc", name)
|
||||
req, err = http.NewRequest(http.MethodGet, "/share?path=%2Fabc/d", nil)
|
||||
require.NoError(t, err)
|
||||
name, err = getBrowsableSharedPath(share, req)
|
||||
name, err = getBrowsableSharedPath(share.Paths[0], req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/a/b/c/abc/d", name)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, "/share?path=%2Fabc%2F..%2F..", nil)
|
||||
require.NoError(t, err)
|
||||
_, err = getBrowsableSharedPath(share, req)
|
||||
_, err = getBrowsableSharedPath(share.Paths[0], req)
|
||||
assert.Error(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, "/share?path=%2Fabc%2F..", nil)
|
||||
require.NoError(t, err)
|
||||
name, err = getBrowsableSharedPath(share, req)
|
||||
name, err = getBrowsableSharedPath(share.Paths[0], req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "/a/b/c", name)
|
||||
|
||||
|
|
|
@ -1233,7 +1233,6 @@ func (s *httpdServer) initializeRouter() {
|
|||
ContentSecurityPolicy: s.binding.Security.ContentSecurityPolicy,
|
||||
PermissionsPolicy: s.binding.Security.PermissionsPolicy,
|
||||
CrossOriginOpenerPolicy: s.binding.Security.CrossOriginOpenerPolicy,
|
||||
ExpectCTHeader: s.binding.Security.ExpectCTHeader,
|
||||
})
|
||||
secureMiddleware.SetBadHostHandler(http.HandlerFunc(s.badHostHandler))
|
||||
s.router.Use(secureMiddleware.Handler)
|
||||
|
@ -1541,6 +1540,7 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
s.router.Get(webClientPubSharesPath+"/{id}", s.downloadFromShare)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}/partial", s.handleClientSharePartialDownload)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/browse", s.handleShareGetFiles)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}/browse/exist", s.handleClientShareCheckExist)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/download", s.handleClientSharedFile)
|
||||
s.router.Get(webClientPubSharesPath+"/{id}/upload", s.handleClientUploadToShare)
|
||||
s.router.With(compressor.Handler).Get(webClientPubSharesPath+"/{id}/dirs", s.handleShareGetDirContents)
|
||||
|
@ -1563,6 +1563,8 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
router.With(s.checkAuthRequirements, s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Post(webClientFilePath, uploadUserFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Post(webClientExistPath, s.handleClientCheckExist)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientEditFilePath, s.handleClientEditFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Delete(webClientFilesPath, deleteUserFile)
|
||||
|
@ -1578,7 +1580,7 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
Post(webClientFileActionsPath+"/copy", copyUserFsEntry)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).
|
||||
Post(webClientDownloadZipPath, s.handleWebClientDownloadZip)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientPingPath, s.handleClientPing)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientPingPath, handlePingRequest)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientProfilePath,
|
||||
s.handleClientGetProfile)
|
||||
router.With(s.checkAuthRequirements).Post(webClientProfilePath, s.handleWebClientProfilePost)
|
||||
|
|
|
@ -20,6 +20,7 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/unrolled/secure"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/util"
|
||||
|
@ -148,3 +149,8 @@ func getI18NErrorString(err error, fallback string) string {
|
|||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func handlePingRequest(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
render.PlainText(w, r, "PONG")
|
||||
}
|
||||
|
|
|
@ -131,6 +131,7 @@ type filesPage struct {
|
|||
CurrentDir string
|
||||
DirsURL string
|
||||
FileActionsURL string
|
||||
CheckExistURL string
|
||||
DownloadURL string
|
||||
ViewPDFURL string
|
||||
FileURL string
|
||||
|
@ -802,6 +803,7 @@ func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Reque
|
|||
DirsURL: path.Join(baseSharePath, "dirs"),
|
||||
FileURL: "",
|
||||
FileActionsURL: "",
|
||||
CheckExistURL: path.Join(baseSharePath, "browse", "exist"),
|
||||
CanAddFiles: share.Scope == dataprovider.ShareScopeReadWrite,
|
||||
CanCreateDirs: false,
|
||||
CanRename: false,
|
||||
|
@ -843,6 +845,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
|
|||
DirsURL: webClientDirsPath,
|
||||
FileURL: webClientFilePath,
|
||||
FileActionsURL: webClientFileActionsPath,
|
||||
CheckExistURL: webClientExistPath,
|
||||
CanAddFiles: user.CanAddFilesFromWeb(dirName),
|
||||
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
|
||||
CanRename: user.CanRenameFromWeb(dirName, dirName),
|
||||
|
@ -955,7 +958,7 @@ func (s *httpdServer) handleClientSharePartialDownload(w http.ResponseWriter, r
|
|||
s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "")
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share, r)
|
||||
name, err := getBrowsableSharedPath(share.Paths[0], r)
|
||||
if err != nil {
|
||||
s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "")
|
||||
return
|
||||
|
@ -999,7 +1002,7 @@ func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.R
|
|||
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share, r)
|
||||
name, err := getBrowsableSharedPath(share.Paths[0], r)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), getRespStatus(err))
|
||||
return
|
||||
|
@ -1064,7 +1067,7 @@ func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request
|
|||
s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "")
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share, r)
|
||||
name, err := getBrowsableSharedPath(share.Paths[0], r)
|
||||
if err != nil {
|
||||
s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "")
|
||||
return
|
||||
|
@ -1131,7 +1134,7 @@ func (s *httpdServer) handleShareGetPDF(w http.ResponseWriter, r *http.Request)
|
|||
s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "")
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share, r)
|
||||
name, err := getBrowsableSharedPath(share.Paths[0], r)
|
||||
if err != nil {
|
||||
s.renderClientMessagePage(w, r, util.I18nShareAccessErrorTitle, getRespStatus(err), err, "")
|
||||
return
|
||||
|
@ -1919,9 +1922,82 @@ func (s *httpdServer) handleClientSharedFile(w http.ResponseWriter, r *http.Requ
|
|||
s.renderShareDownloadPage(w, r, path.Join(webClientPubSharesPath, share.ShareID)+query)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientPing(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientCheckExist(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
render.PlainText(w, r, "PONG")
|
||||
connection, err := getUserConnection(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
|
||||
|
||||
doCheckExist(w, r, connection, name)
|
||||
}
|
||||
|
||||
func (s *httpdServer) handleClientShareCheckExist(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
validScopes := []dataprovider.ShareScope{dataprovider.ShareScopeReadWrite}
|
||||
share, connection, err := s.checkPublicShare(w, r, validScopes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := validateBrowsableShare(share, connection); err != nil {
|
||||
sendAPIResponse(w, r, err, "", getRespStatus(err))
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share.Paths[0], r)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err = common.Connections.Add(connection); err != nil {
|
||||
sendAPIResponse(w, r, err, "Unable to add connection", http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
doCheckExist(w, r, connection, name)
|
||||
}
|
||||
|
||||
type filesToCheck struct {
|
||||
Files []string `json:"files"`
|
||||
}
|
||||
|
||||
func doCheckExist(w http.ResponseWriter, r *http.Request, connection *Connection, name string) {
|
||||
var filesList filesToCheck
|
||||
err := render.DecodeJSON(r.Body, &filesList)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if len(filesList.Files) == 0 {
|
||||
sendAPIResponse(w, r, errors.New("files to be checked are mandatory"), "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
contents, err := connection.ListDir(name)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err))
|
||||
return
|
||||
}
|
||||
existing := make([]map[string]any, 0)
|
||||
for _, info := range contents {
|
||||
if util.Contains(filesList.Files, info.Name()) {
|
||||
res := make(map[string]any)
|
||||
res["name"] = info.Name()
|
||||
if info.IsDir() {
|
||||
res["type"] = "1"
|
||||
res["size"] = ""
|
||||
} else {
|
||||
res["type"] = "2"
|
||||
res["size"] = info.Size()
|
||||
}
|
||||
existing = append(existing, res)
|
||||
}
|
||||
}
|
||||
render.JSON(w, r, existing)
|
||||
}
|
||||
|
||||
func checkShareRedirectURL(next, base string) (bool, string) {
|
||||
|
|
|
@ -131,6 +131,7 @@ const (
|
|||
I18nErrorNoPermissions = "general.no_permissions"
|
||||
I18nErrorShareBrowsePaths = "share.browsable_multiple_paths"
|
||||
I18nErrorShareBrowseNoDir = "share.browsable_non_dir"
|
||||
I18nErrorShareInvalidPath = "share.invalid_path"
|
||||
I18nErrorPathInvalid = "general.path_invalid"
|
||||
I18nErrorQuotaRead = "general.err_quota_read"
|
||||
I18nErrorEditDir = "general.error_edit_dir"
|
||||
|
|
|
@ -309,8 +309,7 @@
|
|||
"content_type_nosniff": false,
|
||||
"content_security_policy": "",
|
||||
"permissions_policy": "",
|
||||
"cross_origin_opener_policy": "",
|
||||
"expect_ct_header": ""
|
||||
"cross_origin_opener_policy": ""
|
||||
},
|
||||
"branding": {
|
||||
"web_admin": {
|
||||
|
|
|
@ -171,6 +171,7 @@
|
|||
"err_429": "Too many concurrent requests",
|
||||
"err_generic": "Unable to access the requested resource",
|
||||
"err_validation": "Invalid filesystem configuration",
|
||||
"err_exists": "The destination already exists",
|
||||
"dir_list": {
|
||||
"err_generic": "Failed to get directory listing",
|
||||
"err_403": "$t(fs.dir_list.err_generic). $t(fs.err_403)",
|
||||
|
@ -204,20 +205,23 @@
|
|||
"msg": "Copy",
|
||||
"err_generic": "Error copying files/directories",
|
||||
"err_403": "$t(fs.copy.err_generic). $t(fs.err_403)",
|
||||
"err_429": "$t(fs.copy.err_generic). $t(fs.err_429)"
|
||||
"err_429": "$t(fs.copy.err_generic). $t(fs.err_429)",
|
||||
"err_exists": "$t(fs.copy.err_generic). $t(fs.err_exists)"
|
||||
},
|
||||
"move": {
|
||||
"msg": "Move",
|
||||
"err_generic": "Error moving files/directories",
|
||||
"err_403": "$t(fs.move.err_generic). $t(fs.err_403)",
|
||||
"err_429": "$t(fs.move.err_generic). $t(fs.err_429)"
|
||||
"err_429": "$t(fs.move.err_generic). $t(fs.err_429)",
|
||||
"err_exists": "$t(fs.move.err_generic). $t(fs.err_exists)"
|
||||
},
|
||||
"rename": {
|
||||
"title": "Rename \"{{- name}}\"",
|
||||
"new_name": "New name",
|
||||
"err_generic": "Unable to rename \"{{- name}}\"",
|
||||
"err_403": "$t(fs.rename.err_generic). $t(fs.err_403)",
|
||||
"err_429": "$t(fs.rename.err_generic). $t(fs.err_429)"
|
||||
"err_429": "$t(fs.rename.err_generic). $t(fs.err_429)",
|
||||
"err_exists": "$t(fs.rename.err_generic). $t(fs.err_exists)"
|
||||
},
|
||||
"upload": {
|
||||
"text": "Upload Files",
|
||||
|
@ -226,7 +230,9 @@
|
|||
"message_empty": "This directory is empty. $t(fs.upload.message)",
|
||||
"err_generic": "Error uploading files",
|
||||
"err_403": "$t(fs.upload.err_generic). $t(fs.err_403)",
|
||||
"err_429": "$t(fs.upload.err_generic). $t(fs.err_429)"
|
||||
"err_429": "$t(fs.upload.err_generic). $t(fs.err_429)",
|
||||
"err_dir_overwrite": "$t(fs.upload.err_generic). There are directories with the same name as the files: {{- val}}",
|
||||
"overwrite_text": "File conflict detected. Do you want to overwrite the following files?"
|
||||
},
|
||||
"quota_usage": {
|
||||
"title": "Quota usage",
|
||||
|
@ -334,7 +340,8 @@
|
|||
"link_uncompressed_title": "Uncompressed file",
|
||||
"link_uncompressed_desc": "If the share consists of a single file, it can also be downloaded uncompressed",
|
||||
"upload_desc": "You can upload one or more files to the shared directory",
|
||||
"expired_desc": "This share is no longer accessible because it has expired"
|
||||
"expired_desc": "This share is no longer accessible because it has expired",
|
||||
"invalid_path": "The shared directory is missing or not accessible"
|
||||
},
|
||||
"select2": {
|
||||
"no_results": "No results found",
|
||||
|
|
|
@ -171,6 +171,7 @@
|
|||
"err_429": "Troppe richieste contemporanee",
|
||||
"err_generic": "Impossibile accedere alla risorsa richiesta",
|
||||
"err_validation": "Configurazione del filesystem non valida",
|
||||
"err_exists": "La destinazione esiste già",
|
||||
"dir_list": {
|
||||
"err_generic": "Impossibile ottenere l'elenco della directory",
|
||||
"err_403": "$t(fs.dir_list.err_generic). $t(fs.err_403)",
|
||||
|
@ -204,20 +205,23 @@
|
|||
"msg": "Copia",
|
||||
"err_generic": "Errore copia file/cartelle",
|
||||
"err_403": "$t(fs.copy.err_generic). $t(fs.err_403)",
|
||||
"err_429": "$t(fs.copy.err_generic). $t(fs.err_429)"
|
||||
"err_429": "$t(fs.copy.err_generic). $t(fs.err_429)",
|
||||
"err_exists": "$t(fs.copy.err_generic). $t(fs.err_exists)"
|
||||
},
|
||||
"move": {
|
||||
"msg": "Sposta",
|
||||
"err_generic": "Errore nello spostamento di file/directory",
|
||||
"err_403": "$t(fs.move.err_generic). $t(fs.err_403)",
|
||||
"err_429": "$t(fs.move.err_generic). $t(fs.err_429)"
|
||||
"err_429": "$t(fs.move.err_generic). $t(fs.err_429)",
|
||||
"err_exists": "$t(fs.move.err_generic). $t(fs.err_exists)"
|
||||
},
|
||||
"rename": {
|
||||
"title": "Rinomina \"{{- name}}\"",
|
||||
"new_name": "Nuovo nome",
|
||||
"err_generic": "Impossibile rinominare \"{{- name}}\"",
|
||||
"err_403": "$t(fs.rename.err_generic): $t(fs.err_403)",
|
||||
"err_429": "$t(fs.rename.err_generic): $t(fs.err_429)"
|
||||
"err_429": "$t(fs.rename.err_generic): $t(fs.err_429)",
|
||||
"err_exists": "$t(fs.rename.err_generic). $t(fs.err_exists)"
|
||||
},
|
||||
"upload": {
|
||||
"text": "Carica file",
|
||||
|
@ -226,7 +230,9 @@
|
|||
"message_empty": "Questa cartella è vuota. $t(fs.upload.message)",
|
||||
"err_generic": "Errore caricamento file",
|
||||
"err_403": "$t(fs.upload.err_generic). $t(fs.err_403)",
|
||||
"err_429": "$t(fs.upload.err_generic). $t(fs.err_429)"
|
||||
"err_429": "$t(fs.upload.err_generic). $t(fs.err_429)",
|
||||
"err_dir_overwrite": "$t(fs.upload.err_generic). Ci sono cartelle con lo stesso nome dei file: {{- val}}",
|
||||
"overwrite_text": "Rilevato conflitto di file. Vuoi sovrascrivere i seguenti file?"
|
||||
},
|
||||
"quota_usage": {
|
||||
"title": "Utilizzo quota",
|
||||
|
@ -334,7 +340,8 @@
|
|||
"link_uncompressed_title": "File non compresso",
|
||||
"link_uncompressed_desc": "Se la condivisione è costituita da un unico file è possibile scaricarlo anche non compresso",
|
||||
"upload_desc": "È possibile caricare uno o più file nella directory condivisa",
|
||||
"expired_desc": "Questa condivisione non è più accessibile perché è scaduta"
|
||||
"expired_desc": "Questa condivisione non è più accessibile perché è scaduta",
|
||||
"invalid_path": "La directory condivisa manca o non è accessibile"
|
||||
},
|
||||
"select2": {
|
||||
"no_results": "Nessun risultato trovato",
|
||||
|
|
|
@ -64,7 +64,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
{{- block "additionalnavitems" .}}{{- end}}
|
||||
{{- if ne .CurrentURL .EditURL }}
|
||||
<div class="d-flex align-items-center ms-2 ms-lg-3">
|
||||
<a href="#" class="btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px" data-kt-menu-trigger="{default:'click', lg: 'hover'}" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
|
||||
<a href="#" class="btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px" data-kt-menu-trigger="click" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
|
||||
<i class="ki-duotone ki-night-day theme-light-show fs-2">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
|
@ -130,7 +130,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
</div>
|
||||
{{- end}}
|
||||
<div class="d-flex align-items-center ms-2 ms-lg-3">
|
||||
<div class="btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px" data-kt-menu-trigger="{default:'click', lg: 'hover'}" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
|
||||
<div class="btn btn-icon btn-active-light-primary w-35px h-35px w-md-40px h-md-40px" data-kt-menu-trigger="click" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
|
||||
<i class="ki-duotone ki-user fs-2">
|
||||
<i class="path1"></i>
|
||||
<i class="path2"></i>
|
||||
|
@ -285,6 +285,8 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
<span id="modal_alert_text" class="fs-6 text-gray-900 fw-semibold"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modal_alert_items" class="d-flex flex-column mt-5 d-none">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-0 justify-content-center">
|
||||
<button id="modal_alert_cancel" type="button" class="btn btn-secondary m-2"></button>
|
||||
|
@ -355,6 +357,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
let modalEl = $('#modal_alert');
|
||||
let okBtn = $("#modal_alert_ok");
|
||||
let cancelBtn = $("#modal_alert_cancel");
|
||||
let itemsList = $('#modal_alert_items');
|
||||
|
||||
modalEl.off('hide.bs.modal');
|
||||
modalEl.on('hide.bs.modal', hideFn);
|
||||
|
@ -376,6 +379,15 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
}
|
||||
|
||||
$("#modal_alert_text").text(params.text);
|
||||
itemsList.empty();
|
||||
itemsList.addClass("d-none");
|
||||
if (params.items && params.items.length > 0){
|
||||
itemsList.removeClass("d-none");
|
||||
$.each(params.items, function(key, item) {
|
||||
itemText = escapeHTML(item);
|
||||
itemsList.append(`<li class="d-flex align-items-center py-2 fw-bold fs-6 text-gray-800"><span class="bullet bullet-dot me-5"></span>${itemText}</li>`);
|
||||
});
|
||||
}
|
||||
|
||||
switch (params.icon){
|
||||
case "warning":
|
||||
|
|
|
@ -1168,6 +1168,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
}
|
||||
|
||||
if ($('#move_copy_name_container').hasClass("d-none")){
|
||||
// bulk action
|
||||
let dt = $('#file_manager_list').DataTable();
|
||||
dt.rows({ selected: true, search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){
|
||||
let row = dt.row(rowIdx);
|
||||
|
@ -1283,7 +1284,27 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
});
|
||||
}
|
||||
|
||||
copyItem();
|
||||
let filesArray = [];
|
||||
for (let i = 0; i < items.length; i++){
|
||||
filesArray.push({
|
||||
name: items[i].targetName
|
||||
});
|
||||
}
|
||||
|
||||
CheckExist.fire({
|
||||
operation: "copy",
|
||||
files: filesArray,
|
||||
path: items[0].targetDir
|
||||
}).then((result)=>{
|
||||
if (result.error) {
|
||||
hasError = true;
|
||||
showToast("fs.copy.err_generic");
|
||||
} else if (result.data.length > 0){
|
||||
hasError = true;
|
||||
showToast("fs.copy.err_exists");
|
||||
}
|
||||
copyItem();
|
||||
});
|
||||
}
|
||||
|
||||
function doMove() {
|
||||
|
@ -1371,7 +1392,27 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
});
|
||||
}
|
||||
|
||||
moveItem();
|
||||
let filesArray = [];
|
||||
for (let i = 0; i < items.length; i++){
|
||||
filesArray.push({
|
||||
name: items[i].targetName
|
||||
});
|
||||
}
|
||||
|
||||
CheckExist.fire({
|
||||
operation: "move",
|
||||
files: filesArray,
|
||||
path: items[0].targetDir
|
||||
}).then((result)=>{
|
||||
if (result.error) {
|
||||
hasError = true;
|
||||
showToast("fs.move.err_generic");
|
||||
} else if (result.data.length > 0){
|
||||
hasError = true;
|
||||
showToast("fs.move.err_exists");
|
||||
}
|
||||
moveItem();
|
||||
});
|
||||
}
|
||||
|
||||
function getDeleteReqAttrs(meta) {
|
||||
|
@ -1476,34 +1517,59 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
showToast("fs.invalid_name");
|
||||
return;
|
||||
}
|
||||
let path = '{{.FileActionsURL}}/move';
|
||||
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+oldName)+'&target={{.CurrentDir}}'+encodeURIComponent("/"+newName);
|
||||
axios.post(path, null, {
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
validateStatus: function (status) {
|
||||
return status == 200;
|
||||
}
|
||||
}).then(function (response) {
|
||||
location.reload();
|
||||
}).catch(function (error) {
|
||||
let errorMessage;
|
||||
if (error && error.response) {
|
||||
switch (error.response.status) {
|
||||
case 403:
|
||||
errorMessage = "fs.rename.err_403";
|
||||
break;
|
||||
case 429:
|
||||
errorMessage = "fs.rename.err_429";
|
||||
break;
|
||||
|
||||
$('#loading_message').text("");
|
||||
KTApp.showPageLoading();
|
||||
|
||||
function executeRename() {
|
||||
let path = '{{.FileActionsURL}}/move';
|
||||
path += '?path={{.CurrentDir}}' + encodeURIComponent("/" + oldName) + '&target={{.CurrentDir}}' + encodeURIComponent("/" + newName);
|
||||
axios.post(path, null, {
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
validateStatus: function (status) {
|
||||
return status == 200;
|
||||
}
|
||||
}).then(function (response) {
|
||||
location.reload();
|
||||
}).catch(function (error) {
|
||||
KTApp.hidePageLoading();
|
||||
let errorMessage;
|
||||
if (error && error.response) {
|
||||
switch (error.response.status) {
|
||||
case 403:
|
||||
errorMessage = "fs.rename.err_403";
|
||||
break;
|
||||
case 429:
|
||||
errorMessage = "fs.rename.err_429";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!errorMessage) {
|
||||
errorMessage = "fs.rename.err_generic";
|
||||
}
|
||||
showToast(errorMessage, { name: oldName });
|
||||
});
|
||||
}
|
||||
|
||||
CheckExist.fire({
|
||||
operation: "move",
|
||||
files: [{name: newName}],
|
||||
path: '{{.CurrentDir}}'
|
||||
}).then((result)=>{
|
||||
if (result.error) {
|
||||
KTApp.hidePageLoading();
|
||||
showToast("fs.rename.err_generic", { name: oldName });
|
||||
return;
|
||||
}
|
||||
if (!errorMessage){
|
||||
errorMessage = "fs.rename.err_generic";
|
||||
if (result.data.length > 0){
|
||||
KTApp.hidePageLoading();
|
||||
showToast("fs.rename.err_exists", { name: oldName });
|
||||
return;
|
||||
}
|
||||
showToast(errorMessage, {name: oldName});
|
||||
executeRename();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1686,9 +1752,103 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
});
|
||||
}
|
||||
|
||||
uploadFile();
|
||||
CheckExist.fire({
|
||||
operation: "upload",
|
||||
files: files,
|
||||
path: "{{.CurrentDir}}"
|
||||
}).then((result)=> {
|
||||
if (result.error) {
|
||||
has_errors = true;
|
||||
setI18NData($('#errorTxt'), "fs.upload.err_generic");
|
||||
$('#errorMsg').removeClass("d-none");
|
||||
uploadFile();
|
||||
return;
|
||||
}
|
||||
let existingFiles = [];
|
||||
let existingDirs = [];
|
||||
$.each(result.data, function (key, item) {
|
||||
if (item.type === "1") {
|
||||
existingDirs.push(item.name);
|
||||
} else {
|
||||
existingFiles.push(item.name);
|
||||
}
|
||||
});
|
||||
if (existingDirs.length > 0) {
|
||||
has_errors = true;
|
||||
setI18NData($('#errorTxt'), "fs.upload.err_dir_overwrite", {val: existingDirs.join(", ")});
|
||||
$('#errorMsg').removeClass("d-none");
|
||||
uploadFile();
|
||||
return;
|
||||
}
|
||||
if (existingFiles.length > 0) {
|
||||
KTApp.hidePageLoading();
|
||||
ModalAlert.fire({
|
||||
text: $.t('fs.upload.overwrite_text'),
|
||||
items: existingFiles,
|
||||
icon: "warning",
|
||||
confirmButtonText: $.t('general.confirm'),
|
||||
cancelButtonText: $.t('general.cancel'),
|
||||
customClass: {
|
||||
confirmButton: "btn btn-danger",
|
||||
cancelButton: 'btn btn-secondary'
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed){
|
||||
KTApp.showPageLoading();
|
||||
} else {
|
||||
has_errors = true;
|
||||
}
|
||||
uploadFile();
|
||||
});
|
||||
return;
|
||||
}
|
||||
uploadFile();
|
||||
});
|
||||
}
|
||||
|
||||
var CheckExist = function () {
|
||||
var promiseResolve;
|
||||
|
||||
function doCheck(operation, files, target) {
|
||||
let filesArray = [];
|
||||
if (files && files.length > 0){
|
||||
for (let i = 0; i < files.length; i++){
|
||||
filesArray.push(files[i].name);
|
||||
}
|
||||
}
|
||||
let path = '{{.CheckExistURL}}?op='+encodeURIComponent(operation)+"&path="+target;
|
||||
axios.post(path, {
|
||||
files: filesArray
|
||||
}, {
|
||||
headers: {
|
||||
timeout: 15000,
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
validateStatus: function (status) {
|
||||
return status == 200;
|
||||
}
|
||||
}).then(function(response){
|
||||
promiseResolve({
|
||||
error: false,
|
||||
data: response.data
|
||||
});
|
||||
}).catch(function(error){
|
||||
promiseResolve({
|
||||
error: true
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
fire: function (params) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
promiseResolve = resolve;
|
||||
doCheck(params.operation, params.files, params.path);
|
||||
});
|
||||
}
|
||||
}
|
||||
}();
|
||||
|
||||
function openMediaPlayer(name, url){
|
||||
$("#video_title").text(name);
|
||||
$("#video_player").attr("src", url);
|
||||
|
@ -1830,7 +1990,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
{{- define "additionalnavitems"}}
|
||||
{{- if .QuotaUsage.HasQuotaInfo}}
|
||||
<div class="d-flex align-items-center ms-2 ms-lg-3">
|
||||
<div class="btn btn-icon btn-active-light-primary position-relative w-35px h-35px w-md-40px h-md-40px" data-kt-menu-trigger="{default:'click', lg: 'hover'}" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
|
||||
<div class="btn btn-icon btn-active-light-primary position-relative w-35px h-35px w-md-40px h-md-40px" data-kt-menu-trigger="click" data-kt-menu-attach="parent" data-kt-menu-placement="bottom-end">
|
||||
<i class="ki-duotone {{if .QuotaUsage.IsQuotaLow}}ki-information-5 text-warning{{else}}ki-information-2{{end}} fs-2">
|
||||
<span class="path1"></span>
|
||||
<span class="path2"></span>
|
||||
|
|
Loading…
Reference in a new issue