CB-4123 servlet for upload download fs files (#2078)

* CB-4123 servlet for upload download fs files

* CB-4123 use variables in fs servlet

* CB-4123 use parameter nodepath in get

* CB-4123 upload file fix

* CB-4123 use project id and uri

* CB-4124 add upload/download context actions

* CB-4124 move fs extension to EE

* CB-4124 export NavigationNodeControl styles

* CB-4123 validate project permissions

* CB-4123 code style fix

* CB-4124 keep loader close to the title in notification

* CB-4123 prettify error

* CB-4113 resolve conflicts

---------

Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: EvgeniaBzzz <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
Ainur
2023-10-30 20:10:53 +03:00
committed by GitHub
co-authored by naumov EvgeniaBzzz
parent a3bdfb68fe
commit 4f0fa25d83
17 changed files with 217 additions and 13 deletions
@@ -16,9 +16,12 @@
*/
package io.cloudbeaver.utils;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebProjectImpl;
import io.cloudbeaver.auth.NoAuthCredentialsProvider;
import io.cloudbeaver.model.app.WebApplication;
import io.cloudbeaver.model.app.WebAuthApplication;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
@@ -209,4 +212,12 @@ public class WebAppUtils {
return RMProjectType.GLOBAL.getPrefix() + "_" + globalConfigurationName;
}
public static WebProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException {
WebProjectImpl project = webSession.getProjectById(projectId);
if (project == null) {
throw new DBWebException("Project '" + projectId + "' not found");
}
return project;
}
}
@@ -1,20 +1,31 @@
package io.cloudbeaver.service;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.server.CBPlatform;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Map;
public abstract class WebServiceServletBase extends HttpServlet {
private static final Log log = Log.getLog(WebServiceServletBase.class);
private static final Type MAP_STRING_OBJECT_TYPE = JSONUtils.MAP_TYPE_TOKEN;
private static final String REQUEST_PARAM_VARIABLES = "variables";
private static final Gson gson = new GsonBuilder()
.serializeNulls()
.setPrettyPrinting()
.create();
private final CBApplication application;
@@ -43,4 +54,7 @@ public abstract class WebServiceServletBase extends HttpServlet {
protected abstract void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException;
protected Map<String, Object> getVariables(HttpServletRequest request) {
return gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE);
}
}
@@ -17,9 +17,14 @@
package io.cloudbeaver.service.fs;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.service.DBWBindingContext;
import io.cloudbeaver.service.DBWServiceBindingServlet;
import io.cloudbeaver.service.DBWServletContext;
import io.cloudbeaver.service.WebServiceBindingBase;
import io.cloudbeaver.service.fs.impl.WebServiceFS;
import io.cloudbeaver.service.fs.model.WebFSServlet;
import org.jkiss.dbeaver.DBException;
import org.jkiss.utils.CommonUtils;
import java.net.URI;
@@ -27,7 +32,7 @@ import java.net.URI;
/**
* Web service implementation
*/
public class WebServiceBindingFS extends WebServiceBindingBase<DBWServiceFS> {
public class WebServiceBindingFS extends WebServiceBindingBase<DBWServiceFS> implements DBWServiceBindingServlet<CBApplication> {
private static final String SCHEMA_FILE_NAME = "schema/service.fs.graphqls";
@@ -43,41 +48,48 @@ public class WebServiceBindingFS extends WebServiceBindingBase<DBWServiceFS> {
.dataFetcher("fsFile",
env -> getService(env).getFile(getWebSession(env),
env.getArgument("projectId"),
URI.create(env.getArgument("fileURI")))
URI.create(env.getArgument("fileURI"))
)
)
.dataFetcher("fsListFiles",
env -> getService(env).getFiles(getWebSession(env),
env.getArgument("projectId"),
URI.create(env.getArgument("folderURI")))
URI.create(env.getArgument("folderURI"))
)
)
.dataFetcher("fsReadFileContentAsString",
env -> getService(env).readFileContent(getWebSession(env),
env.getArgument("projectId"),
URI.create(env.getArgument("fileURI")))
URI.create(env.getArgument("fileURI"))
)
)
;
model.getMutationType()
.dataFetcher("fsCreateFile",
env -> getService(env).createFile(getWebSession(env),
env.getArgument("projectId"),
URI.create(env.getArgument("fileURI")))
URI.create(env.getArgument("fileURI"))
)
)
.dataFetcher("fsCreateFolder",
env -> getService(env).createFolder(getWebSession(env),
env.getArgument("projectId"),
URI.create(env.getArgument("folderURI")))
URI.create(env.getArgument("folderURI"))
)
)
.dataFetcher("fsDeleteFile",
env -> getService(env).deleteFile(getWebSession(env),
env.getArgument("projectId"),
URI.create(env.getArgument("fileURI")))
URI.create(env.getArgument("fileURI"))
)
)
.dataFetcher("fsMoveFile",
env -> getService(env).moveFile(
getWebSession(env),
env.getArgument("projectId"),
URI.create(env.getArgument("fromURI")),
URI.create(env.getArgument("toURI")))
URI.create(env.getArgument("toURI"))
)
)
.dataFetcher("fsWriteFileStringContent",
env -> getService(env).writeFileContent(
@@ -90,4 +102,13 @@ public class WebServiceBindingFS extends WebServiceBindingBase<DBWServiceFS> {
)
;
}
@Override
public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException {
servletContext.addServlet(
"fileSystems",
new WebFSServlet(application, getServiceImpl()),
application.getServicesURI() + "fs-data/*"
);
}
}
@@ -0,0 +1,111 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cloudbeaver.service.fs.model;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.service.WebServiceServletBase;
import io.cloudbeaver.service.fs.DBWServiceFS;
import org.eclipse.jetty.server.Request;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.jkiss.utils.CommonUtils;
import org.jkiss.utils.IOUtils;
import javax.servlet.MultipartConfigElement;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
@MultipartConfig()
public class WebFSServlet extends WebServiceServletBase {
private static final String PARAM_PROJECT_ID = "projectId";
private final DBWServiceFS fs;
public WebFSServlet(CBApplication application, DBWServiceFS fs) {
super(application);
this.fs = fs;
}
@Override
protected void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException {
if (!session.isAuthorizedInSecurityManager()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Anonymous access restricted.");
return;
}
if (request.getMethod().equals("POST")) {
doPost(session, request, response);
} else {
doGet(session, request, response);
}
}
private void doGet(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException {
String projectId = request.getParameter(PARAM_PROJECT_ID);
Path path = getPath(session, projectId, request.getParameter("fileURI"));
session.addInfoMessage("Download data ...");
response.setHeader("Content-Type", "application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=\"" + path.getFileName() + "\"");
response.setHeader("Content-Length", String.valueOf(Files.size(path)));
try (InputStream is = Files.newInputStream(path)) {
IOUtils.copyStream(is, response.getOutputStream());
}
}
private void doPost(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException {
// we need to set this attribute to get parts
request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, new MultipartConfigElement(""));
Map<String, Object> variables = getVariables(request);
String projectId = JSONUtils.getString(variables, PARAM_PROJECT_ID);
String uri = JSONUtils.getString(variables, "toURI");
Path path = getPath(session, projectId, uri);
try {
for (Part part : request.getParts()) {
String fileName = part.getSubmittedFileName();
if (CommonUtils.isEmpty(fileName)) {
continue;
}
try (InputStream is = part.getInputStream()) {
Files.copy(is, path.resolve(fileName));
}
}
} catch (Exception e) {
throw new DBWebException("File Upload Failed: Unable to Save File to the File System", e);
}
}
@NotNull
private Path getPath(WebSession session, String projectId, String uri) throws DBException {
if (CommonUtils.isEmpty(projectId)) {
throw new DBWebException("Project ID is not found");
}
if (CommonUtils.isEmpty(uri)) {
throw new DBWebException("URI is not found");
}
return session.getFileSystemManager(projectId).getPathFromString(session.getProgressMonitor(), uri);
}
}
@@ -22,7 +22,7 @@ export const SnackbarStatus: React.FC<SnackbarStatusProps> = function SnackbarSt
const styles = useS(style);
return status === ENotificationType.Loading ? (
<div data-testid="loader-container" className={s(styles, { loaderContainer: true }, className)}>
<Loader className={styles.loader} fullSize hideMessage />
<Loader className={styles.loader} hideMessage />
</div>
) : (
<NotificationMark className={s(styles, { notificationMark: true }, className)} type={status} />
@@ -6,10 +6,15 @@
* you may not use this file except in compliance with the License.
*/
export function selectFiles(callback: (files: FileList | null) => any): void {
export function selectFiles(callback: (files: FileList | null) => any, multiple?: boolean): void {
let removed = false;
const input = document.createElement('input');
input.type = 'file';
if (multiple) {
input.multiple = true;
}
input.onchange = () => {
callback(input.files);
removed = true;
@@ -14,6 +14,7 @@ export interface IProcessNotificationState {
init: (title: string, message?: string) => void;
resolve: (title: string, message?: string) => void;
reject: (error: Error, title?: string, message?: string) => void;
setMessage: (message: string | null) => void;
}
export enum ENotificationType {
@@ -28,7 +28,7 @@ export class ProcessNotificationController implements IProcessNotificationState
error: observable,
title: observable,
status: observable,
message: observable,
message: observable.ref,
});
}
@@ -52,4 +52,8 @@ export class ProcessNotificationController implements IProcessNotificationState
this.message = message || errorDetails?.message || error.message;
this.error = error;
}
setMessage(message: string | null) {
this.message = message;
}
}
@@ -92,6 +92,7 @@ export default [
['ui_close_all_to_the_left', 'Close all to the Left'],
['ui_or', 'Or'],
['ui_download', 'Download'],
['ui_download_file', 'Download file'],
['ui_upload', 'Upload'],
['ui_import', 'Import'],
['ui_view', 'View'],
@@ -103,6 +104,7 @@ export default [
['ui_upload_file', 'Upload file'],
['ui_upload_files', 'Upload files'],
['ui_upload_files_duplicate_error', 'Files with the same name already exist'],
['ui_upload_file_fail', 'Failed to upload file'],
['root_permission_denied', "You don't have permissions"],
['root_permission_no_permission', "You don't have permission for this action"],
@@ -76,6 +76,7 @@ export default [
['ui_close_all_to_the_left', 'Close all to the Left'],
['ui_or', 'Or'],
['ui_download', 'Download'],
['ui_download_file', 'Download file'],
['ui_upload', 'Upload'],
['ui_import', 'Import'],
['ui_view', 'View'],
@@ -87,6 +88,7 @@ export default [
['ui_upload_file', 'Upload file'],
['ui_upload_files', 'Upload files'],
['ui_upload_files_duplicate_error', 'Files with the same name already exist'],
['ui_upload_file_fail', 'Failed to upload file'],
['root_permission_denied', 'Non hai i permessi'],
['app_root_session_expire_warning_title', 'La sessione sta per scadere'],
@@ -88,6 +88,7 @@ export default [
['ui_close_all_to_the_left', 'Закрыть все слева'],
['ui_or', 'Или'],
['ui_download', 'Cкачать'],
['ui_download_file', 'Скачать файл'],
['ui_upload', 'Загрузить'],
['ui_import', 'Импортировать'],
['ui_view', 'Смотреть'],
@@ -99,6 +100,7 @@ export default [
['ui_upload_file', 'Загрузить файл'],
['ui_upload_files', 'Загрузить файлы'],
['ui_upload_files_duplicate_error', 'Файлы с такими именами уже существуют'],
['ui_upload_file_fail', 'Не удалось загрузить файл'],
['root_permission_denied', 'Отказано в доступе'],
['root_permission_no_permission', 'У вас нет разрешения на это действие'],
@@ -89,6 +89,7 @@ export default [
['ui_close_all_to_the_left', 'Close all to the Left'],
['ui_or', 'Or'],
['ui_download', 'Download'],
['ui_download_file', 'Download file'],
['ui_upload', 'Upload'],
['ui_import', 'Import'],
['ui_view', 'View'],
@@ -100,6 +101,7 @@ export default [
['ui_upload_file', 'Upload file'],
['ui_upload_files', 'Upload files'],
['ui_upload_files_duplicate_error', 'Files with the same name already exist'],
['ui_upload_file_fail', 'Failed to upload file'],
['root_permission_denied', '您没有权限'],
['root_permission_no_permission', '您没有权限执行此操作'],
@@ -9,11 +9,11 @@ import { GlobalConstants } from '@cloudbeaver/core-utils';
import type { CustomGraphQLClient, UploadProgressEvent } from '../CustomGraphQLClient';
export interface IUploadDriverLibraryExtension {
export interface IUploadBlobResultSetExtension {
uploadBlobResultSet: (fileId: string, data: Blob, onUploadProgress?: (event: UploadProgressEvent) => void) => Promise<void>;
}
export function uploadBlobResultSetExtension(client: CustomGraphQLClient): IUploadDriverLibraryExtension {
export function uploadBlobResultSetExtension(client: CustomGraphQLClient): IUploadBlobResultSetExtension {
return {
uploadBlobResultSet(fileId: string, data: Blob, onUploadProgress?: (event: UploadProgressEvent) => void): Promise<void> {
// api/resultset/blob
@@ -0,0 +1,13 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createAction } from '../createAction';
export const ACTION_DOWNLOAD = createAction('download', {
label: 'ui_download',
icon: '/icons/export.svg',
});
@@ -0,0 +1,13 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2023 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createAction } from '../createAction';
export const ACTION_UPLOAD = createAction('upload', {
label: 'ui_upload',
icon: '/icons/import.svg',
});
+2
View File
@@ -16,6 +16,8 @@ export * from './Action/Actions/ACTION_SETTINGS';
export * from './Action/Actions/ACTION_UNDO';
export * from './Action/Actions/ACTION_ZOOM_IN';
export * from './Action/Actions/ACTION_ZOOM_OUT';
export * from './Action/Actions/ACTION_DOWNLOAD';
export * from './Action/Actions/ACTION_UPLOAD';
export * from './Action/KeyBinding/Bindings/KEY_BINDING_OPEN_IN_TAB';
export * from './Action/KeyBinding/Bindings/KEY_BINDING_REDO';
export * from './Action/KeyBinding/Bindings/KEY_BINDING_UNDO';
@@ -32,6 +32,7 @@ export { default as ElementsTreeToolsStyles } from './NavigationTree/ElementsTre
export { default as ElementsTreeFilterStyles } from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeFilter.m.css';
export { default as NavigationNodeNestedStyles } from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.m.css';
export { default as NavigationNodeControlRendererStyles } from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeControlRenderer.m.css';
export { default as NavigationNodeControlStyles } from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.m.css';
export * from './NavigationTree/NavigationTreeLoader';