WebClient: add copy action

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2022-12-30 19:30:16 +01:00
parent fe9904a54d
commit 15ad31da54
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
13 changed files with 416 additions and 172 deletions

6
go.mod
View file

@ -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
View file

@ -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=

View file

@ -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 {

View file

@ -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)

View file

@ -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) {

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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),

View file

@ -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':

View file

@ -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

View file

@ -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">&times;</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)+'&#8230;';
}
@ -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>&nbsp;<a class="${cssClass}" href="${row['url']}" title="${title}">${shortened}</a>`;
}
var icon = getIconForFile(data);
let icon = getIconForFile(data);
return `<i class="${icon}"></i>&nbsp;<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}}