WebClient: add copy action
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
fe9904a54d
commit
15ad31da54
13 changed files with 416 additions and 172 deletions
6
go.mod
6
go.mod
|
@ -15,7 +15,7 @@ require (
|
|||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.46
|
||||
github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.26
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.11
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.17.0
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.17.7
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.19
|
||||
github.com/coreos/go-oidc/v3 v3.4.0
|
||||
|
@ -120,7 +120,7 @@ require (
|
|||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
|
@ -131,7 +131,7 @@ require (
|
|||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
|
|
10
go.sum
10
go.sum
|
@ -275,8 +275,8 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.27.2/go.mod h1:u+566cosFI+d+motIz3USX
|
|||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 h1:W8pLcSn6Uy0eXgDBUUl8M8Kxv7JCoP68ZKTD04OXLEA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6/go.mod h1:L2l2/q76teehcW7YEsgsDjqdsDTERJeX3nOMIFlgGUE=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.14/go.mod h1:xakbH8KMsQQKqzX87uyyzTHshc/0/Df8bsTneTS5pFU=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.11 h1:77V7vnw/NC4DORHVgA97+Ky2p1ri0+ZVYXh6ordUZU0=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.16.11/go.mod h1:jAeo/PdIJZuDSwsvxJS94G4d6h8tStj7WXVuKwLHWU8=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.17.0 h1:6W6BLZcXytRJsVvc2gGwxKE4wbMSlWqdxZivBP/E+ys=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.17.0/go.mod h1:jAeo/PdIJZuDSwsvxJS94G4d6h8tStj7WXVuKwLHWU8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sns v1.17.10/go.mod h1:uITsRNVMeCB3MkWpXxXw0eDz8pW4TYLzj+eyQtbhSxM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.19.1/go.mod h1:A94o564Gj+Yn+7QO1eLFeI7UVv3riy/YBFOfICVqFvU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.27.6/go.mod h1:fiFzQgj4xNOg4/wqmAiPvzgDMXPD+cUEplX/CYn+0j0=
|
||||
|
@ -1067,8 +1067,9 @@ github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e
|
|||
github.com/klauspost/compress v1.15.13 h1:NFn1Wr8cfnenSJSA46lLq4wHCcBzKTSjnBIexDMMOV0=
|
||||
github.com/klauspost/compress v1.15.13/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM=
|
||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.2 h1:xPMwiykqNK9VK0NYC3+jTMYv9I6Vl3YdjZgPZKG3zO0=
|
||||
github.com/klauspost/cpuid/v2 v2.2.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/kolo/xmlrpc v0.0.0-20201022064351-38db28db192b/go.mod h1:pcaDhQK0/NJZEvtCO0qQPPropqV0sJOJ6YW7X+9kRwM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
@ -1154,8 +1155,9 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME
|
|||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
|
|
|
@ -530,7 +530,15 @@ func (c *BaseConnection) RemoveAll(virtualPath string) error {
|
|||
return c.RemoveFile(fs, fsPath, virtualPath, fi)
|
||||
}
|
||||
|
||||
func (c *BaseConnection) checkCopyFolder(srcInfo, dstInfo os.FileInfo, virtualSource, virtualTarget string) error {
|
||||
func (c *BaseConnection) checkCopy(srcInfo, dstInfo os.FileInfo, virtualSource, virtualTarget string) error {
|
||||
_, fsSourcePath, err := c.GetFsAndResolvedPath(virtualSource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, fsTargetPath, err := c.GetFsAndResolvedPath(virtualTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if srcInfo.IsDir() {
|
||||
if dstInfo != nil && !dstInfo.IsDir() {
|
||||
return fmt.Errorf("cannot overwrite file %q with dir %q: %w", virtualTarget, virtualSource, c.GetOpUnsupportedError())
|
||||
|
@ -538,14 +546,6 @@ func (c *BaseConnection) checkCopyFolder(srcInfo, dstInfo os.FileInfo, virtualSo
|
|||
if util.IsDirOverlapped(virtualSource, virtualTarget, true, "/") {
|
||||
return fmt.Errorf("nested copy %q => %q is not supported: %w", virtualSource, virtualTarget, c.GetOpUnsupportedError())
|
||||
}
|
||||
_, fsSourcePath, err := c.GetFsAndResolvedPath(virtualSource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, fsTargetPath, err := c.GetFsAndResolvedPath(virtualTarget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if util.IsDirOverlapped(fsSourcePath, fsTargetPath, true, c.User.FsConfig.GetPathSeparator()) {
|
||||
c.Log(logger.LevelWarn, "nested fs copy %q => %q not allowed", fsSourcePath, fsTargetPath)
|
||||
return fmt.Errorf("nested fs copy is not supported: %w", c.GetOpUnsupportedError())
|
||||
|
@ -555,6 +555,9 @@ func (c *BaseConnection) checkCopyFolder(srcInfo, dstInfo os.FileInfo, virtualSo
|
|||
if dstInfo != nil && dstInfo.IsDir() {
|
||||
return fmt.Errorf("cannot overwrite file %q with dir %q: %w", virtualSource, virtualTarget, c.GetOpUnsupportedError())
|
||||
}
|
||||
if fsSourcePath == fsTargetPath {
|
||||
return fmt.Errorf("the copy source and target cannot be the same: %w", c.GetOpUnsupportedError())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -605,7 +608,7 @@ func (c *BaseConnection) doRecursiveCopy(virtualSourcePath, virtualTargetPath st
|
|||
if err != nil && !c.IsNotExistError(err) {
|
||||
return err
|
||||
}
|
||||
if err := c.checkCopyFolder(info, targetInfo, sourcePath, targetPath); err != nil {
|
||||
if err := c.checkCopy(info, targetInfo, sourcePath, targetPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.doRecursiveCopy(sourcePath, targetPath, info, true); err != nil {
|
||||
|
@ -657,7 +660,7 @@ func (c *BaseConnection) Copy(virtualSourcePath, virtualTargetPath string) error
|
|||
if dstInfo != nil && dstInfo.IsDir() {
|
||||
createTargetDir = false
|
||||
}
|
||||
if err := c.checkCopyFolder(srcInfo, dstInfo, virtualSourcePath, destPath); err != nil {
|
||||
if err := c.checkCopy(srcInfo, dstInfo, virtualSourcePath, destPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.CheckParentDirs(path.Dir(destPath)); err != nil {
|
||||
|
|
|
@ -590,16 +590,16 @@ func TestErrorResolvePath(t *testing.T) {
|
|||
conn := NewBaseConnection("", ProtocolSFTP, "", "", u)
|
||||
err := conn.doRecursiveRemoveDirEntry("/vpath", nil)
|
||||
assert.Error(t, err)
|
||||
err = conn.checkCopyFolder(vfs.NewFileInfo("name", true, 0, time.Unix(0, 0), false), nil, "/source", "/target")
|
||||
err = conn.checkCopy(vfs.NewFileInfo("name", true, 0, time.Unix(0, 0), false), nil, "/source", "/target")
|
||||
assert.Error(t, err)
|
||||
sourceFile := filepath.Join(os.TempDir(), "f", "source")
|
||||
err = os.MkdirAll(filepath.Dir(sourceFile), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(sourceFile, []byte(""), 0666)
|
||||
assert.NoError(t, err)
|
||||
err = conn.checkCopyFolder(vfs.NewFileInfo("name", true, 0, time.Unix(0, 0), false), nil, "/f/source", "/target")
|
||||
err = conn.checkCopy(vfs.NewFileInfo("name", true, 0, time.Unix(0, 0), false), nil, "/f/source", "/target")
|
||||
assert.Error(t, err)
|
||||
err = conn.checkCopyFolder(vfs.NewFileInfo("", false, 0, time.Unix(0, 0), false), vfs.NewFileInfo("", true, 0, time.Unix(0, 0), false), "", "")
|
||||
err = conn.checkCopy(vfs.NewFileInfo("source", false, 0, time.Unix(0, 0), false), vfs.NewFileInfo("target", true, 0, time.Unix(0, 0), false), "/f/source", "/f/target")
|
||||
assert.Error(t, err)
|
||||
err = os.RemoveAll(filepath.Dir(sourceFile))
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -6920,6 +6920,9 @@ func TestCopyAndRemoveSSHCommands(t *testing.T) {
|
|||
testFileNameCopy := testFileName + "_copy"
|
||||
out, err := runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", testFileName, testFileNameCopy), user)
|
||||
assert.NoError(t, err, string(out))
|
||||
// the resolved destination path match the source path
|
||||
out, err = runSSHCommand(fmt.Sprintf("sftpgo-copy %s %s", testFileName, path.Dir(testFileName)), user)
|
||||
assert.Error(t, err, string(out))
|
||||
|
||||
info, err := client.Stat(testFileNameCopy)
|
||||
if assert.NoError(t, err) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/rs/xid"
|
||||
|
@ -105,11 +106,6 @@ func createUserDir(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, nil, fmt.Sprintf("Directory %#v created", name), http.StatusCreated)
|
||||
}
|
||||
|
||||
func renameUserDir(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renameItem(w, r)
|
||||
}
|
||||
|
||||
func deleteUserDir(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
connection, err := getUserConnection(w, r)
|
||||
|
@ -127,6 +123,56 @@ func deleteUserDir(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, nil, fmt.Sprintf("Directory %q deleted", name), http.StatusOK)
|
||||
}
|
||||
|
||||
func renameUserFsEntry(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
connection, err := getUserConnection(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
oldName := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
|
||||
newName := connection.User.GetCleanedPath(r.URL.Query().Get("target"))
|
||||
err = connection.Rename(oldName, newName)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to rename %q -> %q", oldName, newName),
|
||||
getMappedStatusCode(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, nil, fmt.Sprintf("%q renamed to %q", oldName, newName), http.StatusOK)
|
||||
}
|
||||
|
||||
func copyUserFsEntry(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
connection, err := getUserConnection(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
source := r.URL.Query().Get("path")
|
||||
target := r.URL.Query().Get("target")
|
||||
copyFromSource := strings.HasSuffix(source, "/")
|
||||
copyInTarget := strings.HasSuffix(target, "/")
|
||||
source = connection.User.GetCleanedPath(source)
|
||||
target = connection.User.GetCleanedPath(target)
|
||||
if copyFromSource {
|
||||
source += "/"
|
||||
}
|
||||
if copyInTarget {
|
||||
target += "/"
|
||||
}
|
||||
err = connection.Copy(source, target)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to copy %q -> %q", source, target),
|
||||
getMappedStatusCode(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, nil, fmt.Sprintf("%q copied to %q", source, target), http.StatusOK)
|
||||
}
|
||||
|
||||
func getUserFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
connection, err := getUserConnection(w, r)
|
||||
|
@ -330,11 +376,6 @@ func doUploadFiles(w http.ResponseWriter, r *http.Request, connection *Connectio
|
|||
return uploaded
|
||||
}
|
||||
|
||||
func renameUserFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renameItem(w, r)
|
||||
}
|
||||
|
||||
func deleteUserFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
connection, err := getUserConnection(w, r)
|
||||
|
@ -359,13 +400,13 @@ func deleteUserFile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if fi.IsDir() && fi.Mode()&os.ModeSymlink == 0 {
|
||||
connection.Log(logger.LevelDebug, "cannot remove %#v is not a file/symlink", p)
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable delete %#v, it is not a file/symlink", name), http.StatusBadRequest)
|
||||
connection.Log(logger.LevelDebug, "cannot remove %q is not a file/symlink", p)
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable delete %q, it is not a file/symlink", name), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = connection.RemoveFile(fs, p, name, fi)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to delete file %#v", name), getMappedStatusCode(err))
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to delete file %q", name), getMappedStatusCode(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, nil, fmt.Sprintf("File %#v deleted", name), http.StatusOK)
|
||||
|
@ -520,21 +561,3 @@ func setModificationTimeFromHeader(r *http.Request, c *Connection, filePath stri
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameItem(w http.ResponseWriter, r *http.Request) {
|
||||
connection, err := getUserConnection(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
oldName := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
|
||||
newName := connection.User.GetCleanedPath(r.URL.Query().Get("target"))
|
||||
err = connection.Rename(oldName, newName)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to rename %#v -> %#v", oldName, newName),
|
||||
getMappedStatusCode(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, nil, fmt.Sprintf("%#v renamed to %#v", oldName, newName), http.StatusOK)
|
||||
}
|
||||
|
|
|
@ -66,6 +66,7 @@ const (
|
|||
userPwdPath = "/api/v2/user/changepwd"
|
||||
userDirsPath = "/api/v2/user/dirs"
|
||||
userFilesPath = "/api/v2/user/files"
|
||||
userFileActionsPath = "/api/v2/user/file-actions"
|
||||
userStreamZipPath = "/api/v2/user/streamzip"
|
||||
userUploadFilePath = "/api/v2/user/files/upload"
|
||||
userFilesDirsMetadataPath = "/api/v2/user/files/metadata"
|
||||
|
@ -148,6 +149,7 @@ const (
|
|||
webClientTwoFactorRecoveryPathDefault = "/web/client/twofactor-recovery"
|
||||
webClientFilesPathDefault = "/web/client/files"
|
||||
webClientFilePathDefault = "/web/client/file"
|
||||
webClientFileActionsPathDefault = "/web/client/file-actions"
|
||||
webClientSharesPathDefault = "/web/client/shares"
|
||||
webClientSharePathDefault = "/web/client/share"
|
||||
webClientEditFilePathDefault = "/web/client/editfile"
|
||||
|
@ -239,6 +241,7 @@ var (
|
|||
webClientTwoFactorRecoveryPath string
|
||||
webClientFilesPath string
|
||||
webClientFilePath string
|
||||
webClientFileActionsPath string
|
||||
webClientSharesPath string
|
||||
webClientSharePath string
|
||||
webClientEditFilePath string
|
||||
|
@ -963,6 +966,7 @@ func updateWebClientURLs(baseURL string) {
|
|||
webClientTwoFactorRecoveryPath = path.Join(baseURL, webClientTwoFactorRecoveryPathDefault)
|
||||
webClientFilesPath = path.Join(baseURL, webClientFilesPathDefault)
|
||||
webClientFilePath = path.Join(baseURL, webClientFilePathDefault)
|
||||
webClientFileActionsPath = path.Join(baseURL, webClientFileActionsPathDefault)
|
||||
webClientSharesPath = path.Join(baseURL, webClientSharesPathDefault)
|
||||
webClientPubSharesPath = path.Join(baseURL, webClientPubSharesPathDefault)
|
||||
webClientSharePath = path.Join(baseURL, webClientSharePathDefault)
|
||||
|
|
|
@ -101,6 +101,7 @@ const (
|
|||
userPwdPath = "/api/v2/user/changepwd"
|
||||
userDirsPath = "/api/v2/user/dirs"
|
||||
userFilesPath = "/api/v2/user/files"
|
||||
userFileActionsPath = "/api/v2/user/file-actions"
|
||||
userStreamZipPath = "/api/v2/user/streamzip"
|
||||
userUploadFilePath = "/api/v2/user/files/upload"
|
||||
userFilesDirsMetadataPath = "/api/v2/user/files/metadata"
|
||||
|
@ -14109,7 +14110,13 @@ func TestWebDirsAPI(t *testing.T) {
|
|||
assert.Len(t, contents, 0)
|
||||
|
||||
// rename a missing folder
|
||||
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path="+testDir+"&target="+testDir+"new", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
// copy a missing folder
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/copy?path="+testDir+"%2F&target="+testDir+"new%2F", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -14139,13 +14146,20 @@ func TestWebDirsAPI(t *testing.T) {
|
|||
assert.Equal(t, testDir, contents[0]["name"])
|
||||
}
|
||||
// rename a dir with the same source and target name
|
||||
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir, nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path="+testDir+"&target="+testDir, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "operation unsupported")
|
||||
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target=%2F"+testDir+"%2F", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path="+testDir+"&target=%2F"+testDir+"%2F", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "operation unsupported")
|
||||
// copy a dir with the same source and target name
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/copy?path="+testDir+"&target="+testDir, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -14163,8 +14177,14 @@ func TestWebDirsAPI(t *testing.T) {
|
|||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
// copy the dir
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/copy?path="+testDir+"&target="+testDir+"copy", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
// rename the dir
|
||||
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path="+testDir+"&target="+testDir+"new", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -14175,6 +14195,11 @@ func TestWebDirsAPI(t *testing.T) {
|
|||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
req, err = http.NewRequest(http.MethodDelete, userDirsPath+"?path="+testDir+"copy", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
// the root dir cannot be created
|
||||
req, err = http.NewRequest(http.MethodPost, userDirsPath, nil)
|
||||
assert.NoError(t, err)
|
||||
|
@ -14204,7 +14229,13 @@ func TestWebDirsAPI(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path="+testDir+"&target="+testDir+"new", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path="+testDir+"&target="+testDir+"new", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/copy?path="+testDir+"&target="+testDir+"new", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -14474,20 +14505,26 @@ func TestWebFilesAPI(t *testing.T) {
|
|||
err = json.NewDecoder(rr.Body).Decode(&contents)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, contents, 2)
|
||||
// copy a file
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/copy?path=file1.txt&target=%2Ftdir%2Ffile_copy.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
// rename a file
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
// rename a missing file
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
// rename a file with target name equal to source name
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=file1.txt&target=file1.txt", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path=file1.txt&target=file1.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -14557,7 +14594,7 @@ func TestWebFilesAPI(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path=file1.txt&target=%2Ftdir%2Ffile3.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -14622,7 +14659,7 @@ func TestStartDirectory(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path=testdir&target=testdir1", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path=testdir&target=testdir1", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -15129,7 +15166,7 @@ func TestWebAPIWritePermission(t *testing.T) {
|
|||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesPath+"?path=a&target=b", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path=a&target=b", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
setBearerForReq(req, webAPIToken)
|
||||
|
@ -15164,7 +15201,7 @@ func TestWebAPIWritePermission(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPatch, userDirsPath+"?path=dir&target=dir1", nil)
|
||||
req, err = http.NewRequest(http.MethodPost, userFileActionsPath+"/move?path=dir&target=dir1", nil)
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
setBearerForReq(req, webAPIToken)
|
||||
|
|
|
@ -1339,16 +1339,20 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
|
||||
Post(userDirsPath, createUserDir)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
|
||||
Patch(userDirsPath, renameUserDir)
|
||||
Patch(userDirsPath, renameUserFsEntry)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
|
||||
Delete(userDirsPath, deleteUserDir)
|
||||
router.With(s.checkAuthRequirements).Get(userFilesPath, getUserFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
|
||||
Post(userFilesPath, uploadUserFiles)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
|
||||
Patch(userFilesPath, renameUserFile)
|
||||
Patch(userFilesPath, renameUserFsEntry)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
|
||||
Delete(userFilesPath, deleteUserFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
|
||||
Post(userFileActionsPath+"/move", renameUserFsEntry)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled)).
|
||||
Post(userFileActionsPath+"/copy", copyUserFsEntry)
|
||||
router.With(s.checkAuthRequirements).Post(userStreamZipPath, getUserFilesAsZipStream)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientSharesDisabled)).
|
||||
Get(userSharesPath, getShares)
|
||||
|
@ -1460,18 +1464,18 @@ func (s *httpdServer) setupWebClientRoutes() {
|
|||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Post(webClientFilePath, uploadUserFile)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientEditFilePath, s.handleClientEditFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Patch(webClientFilesPath, renameUserFile)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Delete(webClientFilesPath, deleteUserFile)
|
||||
router.With(s.checkAuthRequirements, compressor.Handler, s.refreshCookie).
|
||||
Get(webClientDirsPath, s.handleClientGetDirContents)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Post(webClientDirsPath, createUserDir)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Patch(webClientDirsPath, renameUserDir)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Delete(webClientDirsPath, deleteUserDir)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Post(webClientFileActionsPath+"/move", renameUserFsEntry)
|
||||
router.With(s.checkAuthRequirements, s.checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Post(webClientFileActionsPath+"/copy", copyUserFsEntry)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).
|
||||
Get(webClientDownloadZipPath, s.handleWebClientDownloadZip)
|
||||
router.With(s.checkAuthRequirements, s.refreshCookie).Get(webClientProfilePath,
|
||||
|
|
|
@ -141,6 +141,7 @@ type filesPage struct {
|
|||
baseClientPage
|
||||
CurrentDir string
|
||||
DirsURL string
|
||||
FileActionsURL string
|
||||
DownloadURL string
|
||||
ViewPDFURL string
|
||||
FileURL string
|
||||
|
@ -573,6 +574,7 @@ func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, di
|
|||
ViewPDFURL: webClientViewPDFPath,
|
||||
DirsURL: webClientDirsPath,
|
||||
FileURL: webClientFilePath,
|
||||
FileActionsURL: webClientFileActionsPath,
|
||||
CanAddFiles: user.CanAddFilesFromWeb(dirName),
|
||||
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
|
||||
CanRename: user.CanRenameFromWeb(dirName, dirName),
|
||||
|
|
|
@ -128,9 +128,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -294,9 +292,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -3963,6 +3959,76 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/file-actions/copy:
|
||||
parameters:
|
||||
- in: query
|
||||
name: path
|
||||
description: Path to the file/folder to copy. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir"
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: query
|
||||
name: target
|
||||
description: New name. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir"
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
post:
|
||||
tags:
|
||||
- user APIs
|
||||
summary: 'Copy a file or a directory'
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/file-actions/move:
|
||||
parameters:
|
||||
- in: query
|
||||
name: path
|
||||
description: Path to the file/folder to rename. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir"
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: query
|
||||
name: target
|
||||
description: New name. It must be URL encoded, for example the path "my dir/àdir" must be sent as "my%20dir%2F%C3%A0dir"
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
post:
|
||||
tags:
|
||||
- user APIs
|
||||
summary: 'Move (rename) a file or a directory'
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/dirs:
|
||||
get:
|
||||
tags:
|
||||
|
@ -4020,9 +4086,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -4036,7 +4100,8 @@ paths:
|
|||
patch:
|
||||
tags:
|
||||
- user APIs
|
||||
summary: Rename a directory
|
||||
deprecated: true
|
||||
summary: 'Rename a directory. Deprecated, use "file-actions/move"'
|
||||
description: Rename a directory for the logged in user. The rename is allowed for empty directory or for non empty local directories, with no virtual folders inside
|
||||
operationId: rename_user_dir
|
||||
parameters:
|
||||
|
@ -4058,9 +4123,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -4075,7 +4138,7 @@ paths:
|
|||
tags:
|
||||
- user APIs
|
||||
summary: Delete a directory
|
||||
description: Delete a directory for the logged in user. Only empty directories can be deleted
|
||||
description: Delete a directory and any children it contains for the logged in user
|
||||
operationId: delete_user_dir
|
||||
parameters:
|
||||
- in: query
|
||||
|
@ -4090,9 +4153,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -4186,9 +4247,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -4204,8 +4263,9 @@ paths:
|
|||
patch:
|
||||
tags:
|
||||
- user APIs
|
||||
deprecated: true
|
||||
summary: Rename a file
|
||||
description: Rename a file for the logged in user
|
||||
description: 'Rename a file for the logged in user. Deprecated, use "file-actions/move"'
|
||||
operationId: rename_user_file
|
||||
parameters:
|
||||
- in: query
|
||||
|
@ -4226,9 +4286,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -4258,9 +4316,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -4325,9 +4381,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
@ -4370,9 +4424,7 @@ paths:
|
|||
content:
|
||||
application/json; charset=utf-8:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
NFPM_VERSION=2.22.2
|
||||
NFPM_VERSION=2.23.0
|
||||
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
||||
if [ -z ${SFTPGO_VERSION} ]
|
||||
then
|
||||
|
|
|
@ -126,6 +126,45 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="copyModal" tabindex="-1" role="dialog" aria-labelledby="copyModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="copyModalLabel">
|
||||
Copy the selected item
|
||||
</h5>
|
||||
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="copy_form" action="" method="POST">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="copy_old_name" class="col-form-label">Source</label>
|
||||
<input type="text" class="form-control" id="copy_old_name" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="copy_new_dir" class="col-form-label">New base dir</label>
|
||||
<input type="text" class="form-control" id="copy_new_dir" required aria-describedby="copyNewDirHelpBlock">
|
||||
<small id="copyNewDirHelpBlock" class="form-text text-muted">
|
||||
Setting a directory other than the current one will copy the item there. This directory will be created if it doesn't exist
|
||||
</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="copy_new_name" class="col-form-label">Target</label>
|
||||
<input type="text" class="form-control" id="copy_new_name" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" type="button" data-dismiss="modal">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="renameModal" tabindex="-1" role="dialog" aria-labelledby="renameModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
|
@ -447,7 +486,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
return escapeHTML(d);
|
||||
}
|
||||
|
||||
var shortened = d.substr(0, cutoff-1);
|
||||
let shortened = d.substr(0, cutoff-1);
|
||||
return escapeHTML(shortened)+'…';
|
||||
}
|
||||
|
||||
|
@ -463,7 +502,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
}
|
||||
|
||||
function getIconForFile(filename) {
|
||||
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
let extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
switch (extension) {
|
||||
case "doc":
|
||||
case "docx":
|
||||
|
@ -573,13 +612,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
}
|
||||
|
||||
function deleteAction() {
|
||||
var table = $('#dataTable').DataTable();
|
||||
let table = $('#dataTable').DataTable();
|
||||
table.button('delete:name').enable(false);
|
||||
|
||||
var selectedItems = table.column(0).checkboxes.selected()
|
||||
var has_errors = false;
|
||||
var index = 0;
|
||||
var success = 0;
|
||||
let selectedItems = table.column(0).checkboxes.selected()
|
||||
let has_errors = false;
|
||||
let index = 0;
|
||||
let success = 0;
|
||||
spinnerDone = false;
|
||||
|
||||
$('#deleteModal').modal('hide');
|
||||
|
@ -596,14 +635,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
}
|
||||
return;
|
||||
}
|
||||
var selected = selectedItems[index];
|
||||
var itemType = getTypeFromMeta(selected);
|
||||
var itemName = getNameFromMeta(selected);
|
||||
var path;
|
||||
var reqTimeout = 15000;
|
||||
let selected = selectedItems[index];
|
||||
let itemType = getTypeFromMeta(selected);
|
||||
let itemName = getNameFromMeta(selected);
|
||||
let path;
|
||||
let reqTimeout = 15000;
|
||||
if (itemType == "1"){
|
||||
path = '{{.DirsURL}}';
|
||||
reqTimeout = 90000
|
||||
reqTimeout = 120000
|
||||
} else {
|
||||
path = '{{.FilesURL}}';
|
||||
}
|
||||
|
@ -623,12 +662,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
error: function ($xhr, textStatus, errorThrown) {
|
||||
index++;
|
||||
has_errors = true;
|
||||
var txt = "Unable to delete the selected item/s";
|
||||
let txt = "Unable to delete the selected item/s";
|
||||
if (success > 0){
|
||||
txt = "Not all the selected items have been deleted, please reload the page";
|
||||
}
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message) {
|
||||
txt = json.message;
|
||||
|
@ -750,8 +789,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
$("#create_dir_form").submit(function (event) {
|
||||
event.preventDefault();
|
||||
$('#createDirModal').modal('hide');
|
||||
var dirName = replaceSlash($("#directory_name").val());
|
||||
var path = '{{.DirsURL}}?path={{.CurrentDir}}' + encodeURIComponent("/"+dirName);
|
||||
let dirName = replaceSlash($("#directory_name").val());
|
||||
let path = '{{.DirsURL}}?path={{.CurrentDir}}' + encodeURIComponent("/"+dirName);
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
|
@ -762,9 +801,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
location.reload();
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
var txt = "Unable to create the requested directory";
|
||||
let txt = "Unable to create the requested directory";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message) {
|
||||
txt = json.message;
|
||||
|
@ -778,7 +817,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
$('#errorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 5000);
|
||||
}, 8000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -811,12 +850,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
async function saveFile() {
|
||||
//console.log("save file, index: "+index);
|
||||
var errorMessage = "Error uploading files";
|
||||
let errorMessage = "Error uploading files";
|
||||
let response;
|
||||
try {
|
||||
var f = files[index].file;
|
||||
var uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+f.name);
|
||||
var lastModified;
|
||||
let f = files[index].file;
|
||||
let uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+f.name);
|
||||
let lastModified;
|
||||
try {
|
||||
lastModified = f.lastModified;
|
||||
} catch (e) {
|
||||
|
@ -874,13 +913,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
|
||||
$("#rename_form").submit(function (event){
|
||||
event.preventDefault();
|
||||
var table = $('#dataTable').DataTable();
|
||||
let table = $('#dataTable').DataTable();
|
||||
table.button('rename:name').enable(false);
|
||||
var selected = table.column(0).checkboxes.selected()[0];
|
||||
var itemType = getTypeFromMeta(selected);
|
||||
var itemName = getNameFromMeta(selected);
|
||||
var targetName = replaceSlash($("#rename_new_name").val());
|
||||
var targetDir = $("#rename_new_dir").val();
|
||||
let selected = table.column(0).checkboxes.selected()[0];
|
||||
let itemName = getNameFromMeta(selected);
|
||||
let targetName = replaceSlash($("#rename_new_name").val());
|
||||
let targetDir = $("#rename_new_dir").val();
|
||||
if (targetDir != "/") {
|
||||
targetDir = targetDir.endsWith('/') ? targetDir.slice(0, -1) : targetDir;
|
||||
}
|
||||
|
@ -889,17 +927,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
} else {
|
||||
targetDir = encodeURIComponent(targetDir);
|
||||
}
|
||||
var path;
|
||||
if (itemType == "1"){
|
||||
path = '{{.DirsURL}}';
|
||||
} else {
|
||||
path = '{{.FilesURL}}';
|
||||
}
|
||||
let path = '{{.FileActionsURL}}/move';
|
||||
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName)+'&target='+targetDir+encodeURIComponent("/"+targetName);
|
||||
$('#renameModal').modal('hide');
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'PATCH',
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' },
|
||||
timeout: 15000,
|
||||
|
@ -907,9 +940,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
location.reload();
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
var txt = "Error renaming item";
|
||||
let txt = "Error renaming item";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message) {
|
||||
txt = json.message;
|
||||
|
@ -924,12 +957,72 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 8000);
|
||||
var selectedItems = table.column(0).checkboxes.selected().length;
|
||||
let selectedItems = table.column(0).checkboxes.selected().length;
|
||||
table.button('rename:name').enable(selectedItems == 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#copy_form").submit(function (event){
|
||||
event.preventDefault();
|
||||
let table = $('#dataTable').DataTable();
|
||||
table.button('copy:name').enable(false);
|
||||
let selected = table.column(0).checkboxes.selected()[0];
|
||||
let itemName = getNameFromMeta(selected);
|
||||
let targetName = $("#copy_new_name").val();
|
||||
let targetDir = $("#copy_new_dir").val();
|
||||
if (targetDir != "/") {
|
||||
targetDir = targetDir.endsWith('/') ? targetDir.slice(0, -1) : targetDir;
|
||||
}
|
||||
if (targetDir.trim() == ""){
|
||||
targetDir = "{{.CurrentDir}}";
|
||||
} else {
|
||||
targetDir = encodeURIComponent(targetDir);
|
||||
}
|
||||
let path = '{{.FileActionsURL}}/copy';
|
||||
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName)+'&target='+targetDir+encodeURIComponent("/"+targetName);
|
||||
|
||||
spinnerDone = false;
|
||||
$('#copyModal').modal('hide');
|
||||
$('#spinnerModal').modal('show');
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' },
|
||||
timeout: 120000,
|
||||
success: function (result) {
|
||||
$('#spinnerModal').modal('hide');
|
||||
spinnerDone = true;
|
||||
location.reload();
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
let txt = "Error copying item";
|
||||
if ($xhr) {
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message) {
|
||||
txt = json.message;
|
||||
}
|
||||
if (json.error) {
|
||||
txt += ": " + json.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 10000);
|
||||
$('#spinnerModal').modal('hide');
|
||||
spinnerDone = true;
|
||||
let selectedItems = table.column(0).checkboxes.selected().length;
|
||||
table.button('copy:name').enable(selectedItems == 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$.fn.dataTable.ext.buttons.refresh = {
|
||||
text: '<i class="fas fa-sync-alt"></i>',
|
||||
name: 'refresh',
|
||||
|
@ -944,15 +1037,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
name: 'download',
|
||||
titleAttr: "Download Zip",
|
||||
action: function (e, dt, node, config) {
|
||||
var filesArray = [];
|
||||
var selected = dt.column(0).checkboxes.selected();
|
||||
let filesArray = [];
|
||||
let selected = dt.column(0).checkboxes.selected();
|
||||
for (i = 0; i < selected.length; i++) {
|
||||
filesArray.push(getNameFromMeta(selected[i]));
|
||||
}
|
||||
var files = encodeURIComponent(JSON.stringify(filesArray));
|
||||
var downloadURL = '{{.DownloadURL}}';
|
||||
var currentDir = '{{.CurrentDir}}';
|
||||
var ts = new Date().getTime().toString();
|
||||
let files = encodeURIComponent(JSON.stringify(filesArray));
|
||||
let downloadURL = '{{.DownloadURL}}';
|
||||
let currentDir = '{{.CurrentDir}}';
|
||||
let ts = new Date().getTime().toString();
|
||||
window.open(`${downloadURL}?path=${currentDir}&files=${files}&_=${ts}`);
|
||||
},
|
||||
enabled: false
|
||||
|
@ -985,8 +1078,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
name: 'rename',
|
||||
titleAttr: "Rename",
|
||||
action: function (e, dt, node, config) {
|
||||
var selected = table.column(0).checkboxes.selected()[0];
|
||||
var itemName = getNameFromMeta(selected);
|
||||
let selected = table.column(0).checkboxes.selected()[0];
|
||||
let itemName = getNameFromMeta(selected);
|
||||
$("#rename_old_name").val(itemName);
|
||||
$("#rename_new_dir").val(decodeURIComponent("{{.CurrentDir}}".replace(/\+/g, '%20')));
|
||||
$("#rename_new_name").val("");
|
||||
|
@ -995,6 +1088,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
enabled: false
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.copy = {
|
||||
text: '<i class="fas fa-copy"></i>',
|
||||
name: 'copy',
|
||||
titleAttr: "Copy",
|
||||
action: function (e, dt, node, config) {
|
||||
let selected = table.column(0).checkboxes.selected()[0];
|
||||
let itemName = getNameFromMeta(selected);
|
||||
$("#copy_old_name").val(itemName);
|
||||
$("#copy_new_dir").val(decodeURIComponent("{{.CurrentDir}}".replace(/\+/g, '%20')));
|
||||
$("#copy_new_name").val("");
|
||||
$('#copyModal').modal('show');
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
|
||||
$.fn.dataTable.ext.buttons.delete = {
|
||||
text: '<i class="fas fa-trash"></i>',
|
||||
name: 'delete',
|
||||
|
@ -1010,29 +1118,29 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
name: 'share',
|
||||
titleAttr: "Share",
|
||||
action: function (e, dt, node, config) {
|
||||
var filesArray = [];
|
||||
var selected = dt.column(0).checkboxes.selected();
|
||||
let filesArray = [];
|
||||
let selected = dt.column(0).checkboxes.selected();
|
||||
for (i = 0; i < selected.length; i++) {
|
||||
filesArray.push(getNameFromMeta(selected[i]));
|
||||
}
|
||||
var files = encodeURIComponent(JSON.stringify(filesArray));
|
||||
var shareURL = '{{.ShareURL}}';
|
||||
var currentDir = '{{.CurrentDir}}';
|
||||
var ts = new Date().getTime().toString();
|
||||
let files = encodeURIComponent(JSON.stringify(filesArray));
|
||||
let shareURL = '{{.ShareURL}}';
|
||||
let currentDir = '{{.CurrentDir}}';
|
||||
let ts = new Date().getTime().toString();
|
||||
window.open(`${shareURL}?path=${currentDir}&files=${files}&_=${ts}`,'_blank');
|
||||
},
|
||||
enabled: false
|
||||
};
|
||||
|
||||
var table = $('#dataTable').DataTable({
|
||||
let table = $('#dataTable').DataTable({
|
||||
"ajax": {
|
||||
"url": "{{.DirsURL}}?path={{.CurrentDir}}",
|
||||
"dataSrc": "",
|
||||
"error": function ($xhr, textStatus, errorThrown) {
|
||||
$(".dataTables_processing").hide();
|
||||
var txt = "Failed to get directory listing";
|
||||
let txt = "Failed to get directory listing";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
let json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message){
|
||||
txt += ": " + json.message;
|
||||
|
@ -1070,9 +1178,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
"data": "name",
|
||||
"render": function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
var title = "";
|
||||
var cssClass = "";
|
||||
var shortened = shortenData(data, 70);
|
||||
let title = "";
|
||||
let cssClass = "";
|
||||
let shortened = shortenData(data, 70);
|
||||
data = escapeHTML(data);
|
||||
if (shortened != data){
|
||||
title = data;
|
||||
|
@ -1085,7 +1193,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
if (row["size"] == "") {
|
||||
return `<i class="fas fa-external-link-alt"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
||||
}
|
||||
var icon = getIconForFile(data);
|
||||
let icon = getIconForFile(data);
|
||||
return `<i class="${icon}"></i> <a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
|
||||
}
|
||||
return data;
|
||||
|
@ -1096,8 +1204,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
{ "data": "edit_url",
|
||||
"render": function (data, type, row) {
|
||||
if (type === 'display') {
|
||||
var filename = escapeHTML(row["name"]);
|
||||
var extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
let filename = escapeHTML(row["name"]);
|
||||
let extension = filename.slice((filename.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
if (data){
|
||||
if (extension == "csv" || extension == "bat" || CodeMirror.findModeByExtension(extension) != null){
|
||||
{{if .CanAddFiles}}
|
||||
|
@ -1117,7 +1225,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
case "bmp":
|
||||
case "svg":
|
||||
case "ico":
|
||||
var title = escapeHTMLForceSafe(row["name"])
|
||||
let title = escapeHTMLForceSafe(row["name"])
|
||||
return `<a href="${row['url']}" data-lightbox="image-gallery" data-title="${title}"><i class="fas fa-eye"></i></a>`;
|
||||
case "mp4":
|
||||
case "mov":
|
||||
|
@ -1132,7 +1240,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
return `<a href="#" onclick="openVideoPlayer('${name}}', '${row['url']}', 'video/ogg');"><i class="fas fa-eye"></i></a>`;
|
||||
case "pdf":
|
||||
if (PDFObject.supportsPDFs){
|
||||
var view_url = row['url'];
|
||||
let view_url = row['url'];
|
||||
view_url = view_url.replace('{{.FilesURL}}','{{.ViewPDFURL}}');
|
||||
return `<a href="${view_url}" target="_blank"><i class="fas fa-eye"></i></a>`;
|
||||
}
|
||||
|
@ -1147,7 +1255,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
{{if .HasIntegrations}}
|
||||
if (type === 'display') {
|
||||
if (data){
|
||||
var name = b64EncodeUnicode(escapeHTML(row["name"]));
|
||||
let name = b64EncodeUnicode(escapeHTML(row["name"]));
|
||||
return `<a href="#" onclick="openExternalURL('${data}', '${row["ext_link"]}', '${name}');"><i class="fas fa-external-link-alt"></i></a>`;
|
||||
}
|
||||
}
|
||||
|
@ -1163,8 +1271,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
"targets": [0],
|
||||
"checkboxes": {
|
||||
"selectCallback": function (nodes, selected) {
|
||||
var selectedItems = table.column(0).checkboxes.selected().length;
|
||||
var selectedText = "";
|
||||
let selectedItems = table.column(0).checkboxes.selected().length;
|
||||
let selectedText = "";
|
||||
if (selectedItems == 1) {
|
||||
selectedText = "1 item selected";
|
||||
} else if (selectedItems > 1) {
|
||||
|
@ -1176,6 +1284,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
{{if .CanRename}}
|
||||
table.button('rename:name').enable(selectedItems == 1);
|
||||
{{end}}
|
||||
{{if .CanAddFiles}}
|
||||
table.button('copy:name').enable(selectedItems == 1);
|
||||
{{end}}
|
||||
{{if .CanDelete}}
|
||||
table.button('delete:name').enable(selectedItems > 0);
|
||||
{{end}}
|
||||
|
@ -1223,6 +1334,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
{{if .CanDelete}}
|
||||
table.button().add(0, 'delete');
|
||||
{{end}}
|
||||
{{if .CanAddFiles}}
|
||||
table.button().add(0, 'copy');
|
||||
{{end}}
|
||||
{{if .CanRename}}
|
||||
table.button().add(0, 'rename');
|
||||
{{end}}
|
||||
|
|
Loading…
Reference in a new issue