mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
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:
co-authored by
naumov
EvgeniaBzzz
parent
a3bdfb68fe
commit
4f0fa25d83
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+14
@@ -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);
|
||||
}
|
||||
}
|
||||
+29
-8
@@ -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/*"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+111
@@ -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',
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user