WebClient: allow bulk move or copy actions

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2023-11-06 19:10:35 +01:00
parent 9e9d6a5585
commit d5a9bec3da
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
3 changed files with 211 additions and 80 deletions

View file

@ -1133,22 +1133,24 @@ func (c *BaseConnection) checkFolderRename(fsSrc, fsDst vfs.Fs, fsSourcePath, fs
if util.IsDirOverlapped(virtualSourcePath, virtualTargetPath, true, "/") { if util.IsDirOverlapped(virtualSourcePath, virtualTargetPath, true, "/") {
c.Log(logger.LevelDebug, "renaming the folder %q->%q is not supported: nested folders", c.Log(logger.LevelDebug, "renaming the folder %q->%q is not supported: nested folders",
virtualSourcePath, virtualTargetPath) virtualSourcePath, virtualTargetPath)
return c.GetOpUnsupportedError() return fmt.Errorf("nested rename %q => %q is not supported: %w",
virtualSourcePath, virtualTargetPath, c.GetOpUnsupportedError())
} }
if util.IsDirOverlapped(fsSourcePath, fsTargetPath, true, c.User.FsConfig.GetPathSeparator()) { if util.IsDirOverlapped(fsSourcePath, fsTargetPath, true, c.User.FsConfig.GetPathSeparator()) {
c.Log(logger.LevelDebug, "renaming the folder %q->%q is not supported: nested fs folders", c.Log(logger.LevelDebug, "renaming the folder %q->%q is not supported: nested fs folders",
fsSourcePath, fsTargetPath) fsSourcePath, fsTargetPath)
return c.GetOpUnsupportedError() return fmt.Errorf("nested fs rename %q => %q is not supported: %w",
fsSourcePath, fsTargetPath, c.GetOpUnsupportedError())
} }
if c.User.HasVirtualFoldersInside(virtualSourcePath) { if c.User.HasVirtualFoldersInside(virtualSourcePath) {
c.Log(logger.LevelDebug, "renaming the folder %q is not supported: it has virtual folders inside it", c.Log(logger.LevelDebug, "renaming the folder %q is not supported: it has virtual folders inside it",
virtualSourcePath) virtualSourcePath)
return c.GetOpUnsupportedError() return fmt.Errorf("folder %q has virtual folders inside it: %w", virtualSourcePath, c.GetOpUnsupportedError())
} }
if c.User.HasVirtualFoldersInside(virtualTargetPath) { if c.User.HasVirtualFoldersInside(virtualTargetPath) {
c.Log(logger.LevelDebug, "renaming the folder %q is not supported, the target %q has virtual folders inside it", c.Log(logger.LevelDebug, "renaming the folder %q is not supported, the target %q has virtual folders inside it",
virtualSourcePath, virtualTargetPath) virtualSourcePath, virtualTargetPath)
return c.GetOpUnsupportedError() return fmt.Errorf("folder %q has virtual folders inside it: %w", virtualTargetPath, c.GetOpUnsupportedError())
} }
if err := c.checkRecursiveRenameDirPermissions(fsSrc, fsDst, fsSourcePath, fsTargetPath, if err := c.checkRecursiveRenameDirPermissions(fsSrc, fsDst, fsSourcePath, fsTargetPath,
virtualSourcePath, virtualTargetPath, fi); err != nil { virtualSourcePath, virtualTargetPath, fi); err != nil {

View file

@ -147,7 +147,7 @@ func renameUserFsEntry(w http.ResponseWriter, r *http.Request) {
} }
} else { } else {
if err := connection.Rename(oldName, newName); err != nil { if err := connection.Rename(oldName, newName); err != nil {
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to rename %q -> %q", oldName, newName), sendAPIResponse(w, r, err, fmt.Sprintf("Unable to rename %q => %q", oldName, newName),
getMappedStatusCode(err)) getMappedStatusCode(err))
return return
} }
@ -178,7 +178,7 @@ func copyUserFsEntry(w http.ResponseWriter, r *http.Request) {
} }
err = connection.Copy(source, target) err = connection.Copy(source, target)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to copy %q -> %q", source, target), sendAPIResponse(w, r, err, fmt.Sprintf("Unable to copy %q => %q", source, target),
getMappedStatusCode(err)) getMappedStatusCode(err))
return return
} }

View file

@ -74,6 +74,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</a> </a>
</div> </div>
{{- end}} {{- end}}
{{- if not .ShareUploadBaseURL}}
{{- if or .CanRename .CanAddFiles}}
<div class="menu-item px-3">
<a href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="move_or_copy_selected">
Move or copy
</a>
</div>
{{- end}}
{{- if .CanShare}} {{- if .CanShare}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="share_selected"> <a href="#" class="menu-link px-3 fs-6" data-kt-filemanager-table-select="share_selected">
@ -81,6 +89,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
</a> </a>
</div> </div>
{{- end}} {{- end}}
{{- end}}
{{- if .CanDelete}} {{- if .CanDelete}}
<div class="menu-item px-3"> <div class="menu-item px-3">
<a href="#" class="menu-link px-3 text-danger fs-6" data-kt-filemanager-table-select="delete_selected"> <a href="#" class="menu-link px-3 text-danger fs-6" data-kt-filemanager-table-select="delete_selected">
@ -311,7 +320,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let errTxtEl = $('#errorModalTxt'); let errTxtEl = $('#errorModalTxt');
let dirName = replaceSlash($("#dirsbrowser_new_folder_input").val()); let dirName = replaceSlash($("#dirsbrowser_new_folder_input").val());
let submitButton = document.querySelector('#dirsbrowser_add_folder'); let submitButton = document.querySelector('#dirsbrowser_add_folder');
let canceButton = document.querySelector('#dirsbrowser_cancel_folder'); let cancelButton = document.querySelector('#dirsbrowser_cancel_folder');
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
if (!dirName){ if (!dirName){
errTxtEl.text("Folder name is required"); errTxtEl.text("Folder name is required");
@ -321,7 +330,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let path = '{{.DirsURL}}?path='+ curDir + encodeURIComponent("/"+dirName); let path = '{{.DirsURL}}?path='+ curDir + encodeURIComponent("/"+dirName);
submitButton.setAttribute('data-kt-indicator', 'on'); submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true; submitButton.disabled = true;
canceButton.disabled = true; cancelButton.disabled = true;
axios.post(path, null, { axios.post(path, null, {
timeout: 15000, timeout: 15000,
@ -336,7 +345,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
submitButton.removeAttribute('data-kt-indicator'); submitButton.removeAttribute('data-kt-indicator');
submitButton.disabled = false; submitButton.disabled = false;
canceButton.disabled = false; cancelButton.disabled = false;
$('#dirsbrowser_new_folder').addClass("d-none"); $('#dirsbrowser_new_folder').addClass("d-none");
}).catch(function (error) { }).catch(function (error) {
let errorMessage = "Unable to create the new folder"; let errorMessage = "Unable to create the new folder";
@ -352,7 +361,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
submitButton.removeAttribute('data-kt-indicator'); submitButton.removeAttribute('data-kt-indicator');
submitButton.disabled = false; submitButton.disabled = false;
canceButton.disabled = false; cancelButton.disabled = false;
}); });
}); });
} }
@ -808,6 +817,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}); });
} }
const moveOrCopyButton = document.querySelector('[data-kt-filemanager-table-select="move_or_copy_selected"]');
if (moveOrCopyButton){
moveOrCopyButton.addEventListener('click', function(e){
$('#errorMsg').addClass("d-none");
$('#move_copy_name_container').addClass("d-none");
$('#move_copy_source').val("");
$('#modal_move_or_copy').modal('show');
KTDatatablesFoldersExplorer.init('{{.DirsURL}}?dirtree=1&path={{.CurrentDir}}', '{{.CurrentDir}}');
});
}
const shareButton = document.querySelector('[data-kt-filemanager-table-select="share_selected"]'); const shareButton = document.querySelector('[data-kt-filemanager-table-select="share_selected"]');
if (shareButton){ if (shareButton){
shareButton.addEventListener('click', function(e){ shareButton.addEventListener('click', function(e){
@ -851,11 +871,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (selectedRowsIdx.length == 0){ if (selectedRowsIdx.length == 0){
return; return;
} }
keepAlive();
let keepAliveTimer = setInterval(keepAlive, 300000);
$('#loading_message').text(""); $('#loading_message').text("");
KTApp.showPageLoading(); KTApp.showPageLoading();
function deleteSelected() { function deleteSelected() {
if (index >= selectedRowsIdx.length || hasError){ if (index >= selectedRowsIdx.length || hasError){
clearInterval(keepAliveTimer);
KTApp.hidePageLoading(); KTApp.hidePageLoading();
if (!hasError){ if (!hasError){
location.reload(); location.reload();
@ -864,6 +887,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
let meta = dt.row(selectedRowsIdx[index]).data()['meta']; let meta = dt.row(selectedRowsIdx[index]).data()['meta'];
let attrs = getDeleteReqAttrs(meta); let attrs = getDeleteReqAttrs(meta);
let deleteTxt = "";
if (selectedRowsIdx.length > 1){
let name = getNameFromMeta(meta);
deleteTxt = `Delete ${index+1}/${selectedRowsIdx.length}: ${name}`;
}
$('#loading_message').text(deleteTxt);
axios.delete(attrs.path,{ axios.delete(attrs.path,{
timeout: attrs.reqTimeout, timeout: attrs.reqTimeout,
headers: { headers: {
@ -956,15 +985,15 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
function moveOrCopyItem(meta) { function moveOrCopyItem(meta) {
$('#errorMsg').addClass("d-none"); $('#errorMsg').addClass("d-none");
let decodedMeta = UnicodeDecodeB64(meta); let decodedMeta = UnicodeDecodeB64(meta);
$('#move_copy_name_container').removeClass("d-none");
$('#move_copy_source').val(meta); $('#move_copy_source').val(meta);
$('#move_copy_name').val(getNameFromMeta(decodedMeta)); $('#move_copy_name').val(getNameFromMeta(decodedMeta));
$('#modal_move_or_copy').modal('show'); $('#modal_move_or_copy').modal('show');
KTDatatablesFoldersExplorer.init('{{.DirsURL}}?dirtree=1&path={{.CurrentDir}}', '{{.CurrentDir}}'); KTDatatablesFoldersExplorer.init('{{.DirsURL}}?dirtree=1&path={{.CurrentDir}}', '{{.CurrentDir}}');
} }
function doCopy() { function getMoveOtCopyItems() {
let meta = UnicodeDecodeB64($('#move_copy_source').val()); let items = [];
let targetName = $("#move_copy_name").val();
let targetDir = $("#move_copy_folder").val(); let targetDir = $("#move_copy_folder").val();
if (targetDir != "/") { if (targetDir != "/") {
targetDir = targetDir.endsWith('/') ? targetDir.slice(0, -1) : targetDir; targetDir = targetDir.endsWith('/') ? targetDir.slice(0, -1) : targetDir;
@ -974,12 +1003,76 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} else { } else {
targetDir = encodeURIComponent(targetDir); targetDir = encodeURIComponent(targetDir);
} }
let path = '{{.FileActionsURL}}/copy';
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+getNameFromMeta(meta))+'&target='+targetDir+encodeURIComponent("/"+targetName); if ($('#move_copy_name_container').hasClass("d-none")){
let dt = $('#file_manager_list').DataTable();
dt.rows({ selected: true, search: 'applied' }).every(function (rowIdx, tableLoop, rowLoop){
let row = dt.row(rowIdx);
let sourceName = getNameFromMeta(row.data()['meta']);
items.push({
targetDir: targetDir,
sourceName: sourceName,
targetName: sourceName
});
});
} else {
let meta = UnicodeDecodeB64($('#move_copy_source').val());
let sourceName = getNameFromMeta(meta);
items.push({
targetDir: targetDir,
sourceName: sourceName,
targetName: $("#move_copy_name").val()
});
}
return items;
}
function doCopy() {
let items = getMoveOtCopyItems();
if (items.length == 0){
return;
}
keepAlive();
let keepAliveTimer = setInterval(keepAlive, 300000);
let hasError = false;
let index = 0;
let errDivEl = $('#errorMsg');
let errTxtEl = $('#errorTxt');
errDivEl.addClass("d-none");
$('#loading_message').text(""); $('#loading_message').text("");
KTApp.showPageLoading(); KTApp.showPageLoading();
function copyItem() {
if (index >= items.length || hasError){
clearInterval(keepAliveTimer);
KTApp.hidePageLoading();
if (!hasError){
location.reload();
}
return;
}
let item = items[index];
let sourcePath = decodeURIComponent('{{.CurrentDir}}');
if (!sourcePath.endsWith('/')){
sourcePath+="/";
}
sourcePath+=item.sourceName;
let targetPath = decodeURIComponent(item.targetDir);
if (!targetPath.endsWith('/')){
targetPath+="/";
}
targetPath+=item.targetName;
if (items.length > 1){
let msgTxt = `${sourcePath} => ${targetPath}`;
msgTxt = `Copy ${index+1}/${items.length}: ${msgTxt}`;
$('#loading_message').text(msgTxt);
}
let path = '{{.FileActionsURL}}/copy';
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+item.sourceName)+'&target='+item.targetDir+encodeURIComponent("/"+item.targetName);
axios.post(path, null, { axios.post(path, null, {
timeout: 180000, timeout: 180000,
headers: { headers: {
@ -989,11 +1082,11 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
return status == 200; return status == 200;
} }
}).then(function (response) { }).then(function (response) {
location.reload(); index++;
copyItem();
}).catch(function (error) { }).catch(function (error) {
KTApp.hidePageLoading(); index++;
let errDivEl = $('#errorMsg'); hasError = true;
let errTxtEl = $('#errorTxt');
let errorMessage = "Error copying item"; let errorMessage = "Error copying item";
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { if (error.response.data.message) {
@ -1005,27 +1098,59 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
errTxtEl.text(errorMessage); errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
copyItem();
}); });
} }
copyItem();
}
function doMove() { function doMove() {
let meta = UnicodeDecodeB64($('#move_copy_source').val()); let items = getMoveOtCopyItems();
let targetName = $("#move_copy_name").val(); if (items.length == 0){
let targetDir = $("#move_copy_folder").val(); return;
if (targetDir != "/") {
targetDir = targetDir.endsWith('/') ? targetDir.slice(0, -1) : targetDir;
} }
if (targetDir.trim() == ""){ keepAlive();
targetDir = "{{.CurrentDir}}"; let keepAliveTimer = setInterval(keepAlive, 300000);
} else { let hasError = false;
targetDir = encodeURIComponent(targetDir); let index = 0;
} let errDivEl = $('#errorMsg');
let path = '{{.FileActionsURL}}/move'; let errTxtEl = $('#errorTxt');
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+getNameFromMeta(meta))+'&target='+targetDir+encodeURIComponent("/"+targetName); errDivEl.addClass("d-none");
$('#loading_message').text(""); $('#loading_message').text("");
KTApp.showPageLoading(); KTApp.showPageLoading();
function moveItem() {
if (index >= items.length || hasError){
clearInterval(keepAliveTimer);
KTApp.hidePageLoading();
if (!hasError){
location.reload();
}
return;
}
let item = items[index];
let sourcePath = decodeURIComponent('{{.CurrentDir}}');
if (!sourcePath.endsWith('/')){
sourcePath+="/";
}
sourcePath+=item.sourceName;
let targetPath = decodeURIComponent(item.targetDir);
if (!targetPath.endsWith('/')){
targetPath+="/";
}
targetPath+=item.targetName;
if (items.length > 1){
let msgTxt = `${sourcePath} => ${targetPath}`;
msgTxt = `Move ${index+1}/${items.length}: ${msgTxt}`;
$('#loading_message').text(msgTxt);
}
let path = '{{.FileActionsURL}}/move';
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+item.sourceName)+'&target='+item.targetDir+encodeURIComponent("/"+item.targetName);
axios.post(path, null, { axios.post(path, null, {
timeout: 180000, timeout: 180000,
headers: { headers: {
@ -1035,12 +1160,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
return status == 200; return status == 200;
} }
}).then(function (response) { }).then(function (response) {
location.reload(); index++;
moveItem();
}).catch(function (error) { }).catch(function (error) {
KTApp.hidePageLoading(); index++;
let errDivEl = $('#errorMsg'); hasError = true;
let errTxtEl = $('#errorTxt'); let errorMessage = "Error moving item";
let errorMessage = "Error copying item";
if (error && error.response) { if (error && error.response) {
if (error.response.data.message) { if (error.response.data.message) {
errorMessage = error.response.data.message; errorMessage = error.response.data.message;
@ -1051,9 +1176,13 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
errTxtEl.text(errorMessage); errTxtEl.text(errorMessage);
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
moveItem();
}); });
} }
moveItem();
}
function getDeleteReqAttrs(meta) { function getDeleteReqAttrs(meta) {
let path; let path;
let reqTimeout = 15000; let reqTimeout = 15000;
@ -1213,7 +1342,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let errTxtEl = $('#errorTxt'); let errTxtEl = $('#errorTxt');
let dirName = replaceSlash($("#file_manager_new_folder_input").val()); let dirName = replaceSlash($("#file_manager_new_folder_input").val());
let submitButton = document.querySelector('#file_manager_add_folder'); let submitButton = document.querySelector('#file_manager_add_folder');
let canceButton = document.querySelector('#file_manager_cancel_folder'); let cancelButton = document.querySelector('#file_manager_cancel_folder');
errDivEl.addClass("d-none"); errDivEl.addClass("d-none");
if (!dirName){ if (!dirName){
errTxtEl.text("Folder name is required"); errTxtEl.text("Folder name is required");
@ -1224,7 +1353,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let path = '{{.DirsURL}}?path={{.CurrentDir}}' + encodeURIComponent("/"+dirName); let path = '{{.DirsURL}}?path={{.CurrentDir}}' + encodeURIComponent("/"+dirName);
submitButton.setAttribute('data-kt-indicator', 'on'); submitButton.setAttribute('data-kt-indicator', 'on');
submitButton.disabled = true; submitButton.disabled = true;
canceButton.disabled = true; cancelButton.disabled = true;
axios.post(path, null, { axios.post(path, null, {
timeout: 15000, timeout: 15000,
@ -1250,7 +1379,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
errDivEl.removeClass("d-none"); errDivEl.removeClass("d-none");
submitButton.removeAttribute('data-kt-indicator'); submitButton.removeAttribute('data-kt-indicator');
submitButton.disabled = false; submitButton.disabled = false;
canceButton.disabled = false; cancelButton.disabled = false;
}); });
} }
//{{- end}} //{{- end}}
@ -1576,7 +1705,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<div class="modal-content"> <div class="modal-content">
<div class="modal-header border-0"> <div class="modal-header border-0">
<h3 class="modal-title"> <h3 class="modal-title">
Choose target folder and name Choose target folder
</h3> </h3>
<div class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close"> <div class="btn btn-icon btn-sm btn-active-color-primary" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i> <i class="ki-duotone ki-cross fs-1"><span class="path1"></span><span class="path2"></span></i>
@ -1660,7 +1789,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
<label for="move_copy_folder">Target folder</label> <label for="move_copy_folder">Target folder</label>
</div> </div>
<div class="form-floating"> <div class="form-floating" id="move_copy_name_container">
<input type="text" class="form-control form-control-solid" id="move_copy_name"/> <input type="text" class="form-control form-control-solid" id="move_copy_name"/>
<label for="move_copy_name">Destination name</label> <label for="move_copy_name">Destination name</label>
</div> </div>