diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index 810c538edd..9f16c555c9 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -27,6 +27,7 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.websocket, io.cloudbeaver.model, io.cloudbeaver.model.app, + io.cloudbeaver.model.fs, io.cloudbeaver.model.rm, io.cloudbeaver.model.rm.local, io.cloudbeaver.model.rm.lock, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java new file mode 100644 index 0000000000..9a8a4f240d --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/fs/FSUtils.java @@ -0,0 +1,27 @@ +/* + * 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.model.fs; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; + +public class FSUtils { + @NotNull + public static String makeUniqueFsId(@NotNull DBFVirtualFileSystem fileSystem) { + return fileSystem.getType() + ":" + fileSystem.getId(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java index d076318f3e..b1cd5fa4fb 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebAppUtils.java @@ -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 static String getGlobalProjectId() { 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; + } + } diff --git a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls index d38686e06e..4bc24544d4 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls @@ -78,7 +78,7 @@ type NavigatorNodeInfo { # Associated object. Return value depends on the node type - connectionId for connection node, resource path for resource node, etc. # null - if node currently not support this property - objectId: String + objectId: String @since(version: "23.2.4") # Supported features: item, container, leaf # canDelete, canRename diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java index 58f68f9f4a..280b01d21b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceServletBase.java @@ -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 @@ protected final void service(HttpServletRequest request, HttpServletResponse res protected abstract void processServiceRequest(WebSession session, HttpServletRequest request, HttpServletResponse response) throws DBException, IOException; + protected Map getVariables(HttpServletRequest request) { + return gson.fromJson(request.getParameter(REQUEST_PARAM_VARIABLES), MAP_STRING_OBJECT_TYPE); + } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java index 3aa7f4e777..b53a5c39c8 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java @@ -20,6 +20,7 @@ import io.cloudbeaver.WebProjectImpl; import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.model.WebPropertyInfo; +import io.cloudbeaver.model.fs.FSUtils; import io.cloudbeaver.model.rm.DBNResourceManagerProject; import io.cloudbeaver.model.rm.DBNResourceManagerResource; import io.cloudbeaver.model.session.WebSession; @@ -32,6 +33,7 @@ import org.jkiss.dbeaver.model.meta.Association; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.*; +import org.jkiss.dbeaver.model.navigator.fs.DBNFileSystem; import org.jkiss.dbeaver.model.navigator.fs.DBNPathBase; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.rm.RMProjectPermission; @@ -283,6 +285,8 @@ public WebDatabaseObjectInfo getObject() { public String getObjectId() { if (node instanceof DBNPathBase dbnPath) { return dbnPath.getPath().toUri().toString(); + } else if (node instanceof DBNFileSystem dbnFs) { + return FSUtils.makeUniqueFsId(dbnFs.getFileSystem()); } return null; } diff --git a/server/bundles/io.cloudbeaver.service.fs/plugin.xml b/server/bundles/io.cloudbeaver.service.fs/plugin.xml index 09ad0fdeca..e3cbaf3f82 100644 --- a/server/bundles/io.cloudbeaver.service.fs/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.fs/plugin.xml @@ -8,7 +8,4 @@ class="io.cloudbeaver.service.fs.WebServiceBindingFS"> - - - diff --git a/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls b/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls index 558d088c43..50ce1ef548 100644 --- a/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls +++ b/server/bundles/io.cloudbeaver.service.fs/schema/service.fs.graphqls @@ -5,8 +5,15 @@ type FSFile @since(version: "23.2.2") { metaData: Object! } +type FSFileSystem @since(version: "23.2.4") { + id: ID! + requiredAuth: String +} + extend type Query @since(version: "23.2.2") { - fsListFileSystems(projectId: ID!): [String!]! + fsListFileSystems(projectId: ID!): [FSFileSystem!]! + + fsFileSystem(projectId: ID!, fileSystemId: ID!): FSFileSystem! @since(version: "23.2.4") fsFile(projectId: ID!, fileURI: String!): FSFile! @@ -21,14 +28,14 @@ extend type Mutation @since(version: "23.2.2") { fsCreateFolder(projectId: ID!, folderURI:String!): FSFile! - fsDeleteFile(projectId: ID!, fileURI:String!): Boolean! + fsDelete(projectId: ID!, fileURI:String!): Boolean! - fsMoveFile(projectId: ID!, fromURI: String!, toURI: String!): FSFile! + fsMove(projectId: ID!, fromURI: String!, toURI: String!): FSFile! fsWriteFileStringContent( projectId: ID!, fileURI:String!, data: String!, forceOverwrite: Boolean! - ): Boolean! + ): FSFile! } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java index 461c088b0e..e4d37b4072 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/DBWServiceFS.java @@ -20,6 +20,7 @@ import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWService; import io.cloudbeaver.service.fs.model.FSFile; +import io.cloudbeaver.service.fs.model.FSFileSystem; import org.jkiss.code.NotNull; import java.net.URI; @@ -29,9 +30,17 @@ */ public interface DBWServiceFS extends DBWService { @NotNull - String[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) + FSFileSystem[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException; + + @NotNull + FSFileSystem getFileSystem( + @NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String fileSystemId + ) throws DBWebException; + @NotNull FSFile getFile( @NotNull WebSession webSession, @@ -53,7 +62,7 @@ String readFileContent( @NotNull URI fileURI ) throws DBWebException; - boolean writeFileContent( + FSFile writeFileContent( @NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileURI, diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java index e492bf0ced..4f7e5e6552 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/WebServiceBindingFS.java @@ -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 @@ /** * Web service implementation */ -public class WebServiceBindingFS extends WebServiceBindingBase { +public class WebServiceBindingFS extends WebServiceBindingBase implements DBWServiceBindingServlet { private static final String SCHEMA_FILE_NAME = "schema/service.fs.graphqls"; @@ -40,44 +45,58 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { model.getQueryType() .dataFetcher("fsListFileSystems", env -> getService(env).getAvailableFileSystems(getWebSession(env), env.getArgument("projectId"))) + .dataFetcher("fsFileSystem", + env -> getService(env).getFileSystem( + getWebSession(env), + env.getArgument("projectId"), + env.getArgument("fileSystemId") + ) + ) .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", + .dataFetcher("fsDelete", env -> getService(env).deleteFile(getWebSession(env), env.getArgument("projectId"), - URI.create(env.getArgument("fileURI"))) + URI.create(env.getArgument("fileURI")) + ) ) - .dataFetcher("fsMoveFile", + .dataFetcher("fsMove", 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 +109,13 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { ) ; } + + @Override + public void addServlets(CBApplication application, DBWServletContext servletContext) throws DBException { + servletContext.addServlet( + "fileSystems", + new WebFSServlet(application, getServiceImpl()), + application.getServicesURI() + "fs-data/*" + ); + } } diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java index 2c0a7596d5..e527149b2f 100644 --- a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/impl/WebServiceFS.java @@ -17,14 +17,17 @@ package io.cloudbeaver.service.fs.impl; import io.cloudbeaver.DBWebException; +import io.cloudbeaver.model.fs.FSUtils; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.fs.DBWServiceFS; import io.cloudbeaver.service.fs.model.FSFile; +import io.cloudbeaver.service.fs.model.FSFileSystem; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; -import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; +import org.jkiss.dbeaver.registry.fs.FileSystemProviderRegistry; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -35,19 +38,47 @@ public class WebServiceFS implements DBWServiceFS { @NotNull @Override - public String[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) + public FSFileSystem[] getAvailableFileSystems(@NotNull WebSession webSession, @NotNull String projectId) throws DBWebException { try { + var fsRegistry = FileSystemProviderRegistry.getInstance(); return webSession.getFileSystemManager(projectId) .getVirtualFileSystems() .stream() - .map(DBFVirtualFileSystem::getType) - .toArray(String[]::new); + .map(fs -> new FSFileSystem( + FSUtils.makeUniqueFsId(fs), + fsRegistry.getProvider(fs.getProviderId()).getRequiredAuth() + ) + ) + .toArray(FSFileSystem[]::new); } catch (Exception e) { throw new DBWebException("Failed to load file systems: " + e.getMessage(), e); } } + @NotNull + @Override + public FSFileSystem getFileSystem( + @NotNull WebSession webSession, + @NotNull String projectId, + @NotNull String fileSystemId + ) throws DBWebException { + try { + var fsRegistry = FileSystemProviderRegistry.getInstance(); + return webSession.getFileSystemManager(projectId) + .getVirtualFileSystems() + .stream() + .filter(fs -> FSUtils.makeUniqueFsId(fs).equals(fileSystemId)) + .findFirst() + .map(fs -> new FSFileSystem( + FSUtils.makeUniqueFsId(fs), + fsRegistry.getProvider(fs.getProviderId()).getRequiredAuth() + )).orElseThrow(() -> new DBWebException("File system not found")); + } catch (Exception e) { + throw new DBWebException("Failed to get file system: " + e.getMessage(), e); + } + } + @NotNull @Override public FSFile getFile(@NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileUri) @@ -80,15 +111,17 @@ public FSFile[] getFiles(@NotNull WebSession webSession, @NotNull String project public String readFileContent(@NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileUri) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).getPathFromURI(webSession.getProgressMonitor(), fileUri); - return Files.readString(filePath); + Path filePath = webSession.getFileSystemManager(projectId) + .getPathFromURI(webSession.getProgressMonitor(), fileUri); + var data = Files.readAllBytes(filePath); + return new String(data, StandardCharsets.UTF_8); } catch (Exception e) { throw new DBWebException("Failed to read file content: " + e.getMessage(), e); } } @Override - public boolean writeFileContent( + public FSFile writeFileContent( @NotNull WebSession webSession, @NotNull String projectId, @NotNull URI fileURI, @@ -97,12 +130,13 @@ public boolean writeFileContent( ) throws DBWebException { try { - Path filePath = webSession.getFileSystemManager(projectId).getPathFromURI(webSession.getProgressMonitor(), fileURI); + Path filePath = webSession.getFileSystemManager(projectId) + .getPathFromURI(webSession.getProgressMonitor(), fileURI); if (!forceOverwrite && Files.exists(filePath)) { throw new DBException("Cannot overwrite exist file"); } Files.writeString(filePath, data); - return true; + return new FSFile(filePath); } catch (Exception e) { throw new DBWebException("Failed to write file content: " + e.getMessage(), e); } diff --git a/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java new file mode 100644 index 0000000000..3684946f3e --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.fs/src/io/cloudbeaver/service/fs/model/WebFSServlet.java @@ -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 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); + } +} diff --git a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java index 1909e3126b..17b86299e4 100644 --- a/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java +++ b/server/bundles/io.cloudbeaver.service.rm/src/io/cloudbeaver/service/rm/WebServiceBindingRM.java @@ -118,7 +118,7 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("subjectIds"), env.getArgument("permissions") )) - .dataFetcher("rmDeleteProjectsPermissions", env -> getService(env).addProjectsPermissions( + .dataFetcher("rmDeleteProjectsPermissions", env -> getService(env).deleteProjectsPermissions( getWebSession(env), env.getArgument("projectIds"), env.getArgument("subjectIds"), diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql index 2896c125c2..febb3482c3 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_create.sql @@ -308,7 +308,7 @@ CREATE TABLE {table_prefix}CB_USER_SECRETS SECRET_LABEL VARCHAR(128), SECRET_DESCRIPTION VARCHAR(1024), - ENCODING_TYPE VARCHAR(32) NOT NULL DEFAULT 'PLAINTEXT', + ENCODING_TYPE VARCHAR(32) DEFAULT 'PLAINTEXT' NOT NULL, UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (USER_ID, SECRET_ID), diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_10.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_10.sql index 7fdc12e8d0..8d1d44cf82 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_10.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_10.sql @@ -7,7 +7,7 @@ CREATE TABLE {table_prefix}CB_USER_SECRETS SECRET_LABEL VARCHAR(128), SECRET_DESCRIPTION VARCHAR(1024), - UPDATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (USER_ID, SECRET_ID), FOREIGN KEY (USER_ID) REFERENCES {table_prefix}CB_USER (USER_ID) ON DELETE CASCADE diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_13.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_13.sql index 843ab47a31..200cc664ea 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_13.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_13.sql @@ -1,2 +1,2 @@ ALTER TABLE {table_prefix}CB_USER_SECRETS - ADD COLUMN ENCODING_TYPE VARCHAR(32) NOT NULL DEFAULT 'PLAINTEXT'; \ No newline at end of file + ADD COLUMN ENCODING_TYPE VARCHAR(32) DEFAULT 'PLAINTEXT' NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_2.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_2.sql index ee14af9683..fbffddcab3 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_2.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_2.sql @@ -55,7 +55,7 @@ CREATE TABLE IF NOT EXISTS {table_prefix}CB_WORKSPACE( FOREIGN KEY(INSTANCE_ID) REFERENCES {table_prefix}CB_INSTANCE(INSTANCE_ID) ); -ALTER TABLE {table_prefix}CB_USER_CREDENTIALS ADD UPDATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; +ALTER TABLE {table_prefix}CB_USER_CREDENTIALS ADD UPDATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; ALTER TABLE {table_prefix}CB_SESSION ALTER COLUMN LAST_ACCESS_REMOTE_ADDRESS VARCHAR(128) NULL; ALTER TABLE {table_prefix}CB_SESSION ALTER COLUMN LAST_ACCESS_USER_AGENT VARCHAR(255) NULL; diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_5.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_5.sql index c15cbf971c..b7ccc68097 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_5.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_5.sql @@ -5,7 +5,7 @@ CREATE TABLE {table_prefix}CB_AUTH_TOKEN USER_ID VARCHAR(128), EXPIRATION_TIME TIMESTAMP NOT NULL, - CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (TOKEN_ID), FOREIGN KEY (SESSION_ID) REFERENCES {table_prefix}CB_SESSION (SESSION_ID) ON DELETE CASCADE, diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql index 410597bc0e..64cb9ca652 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_7.sql @@ -8,7 +8,7 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT SESSION_TYPE VARCHAR(64) NOT NULL, APP_SESSION_STATE TEXT NOT NULL, - CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (AUTH_ID), FOREIGN KEY (SESSION_ID) REFERENCES {table_prefix}CB_SESSION (SESSION_ID) ON DELETE CASCADE @@ -21,7 +21,7 @@ CREATE TABLE {table_prefix}CB_AUTH_ATTEMPT_INFO AUTH_PROVIDER_CONFIGURATION_ID VARCHAR(128), AUTH_STATE TEXT NOT NULL, - CREATE_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CREATE_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (AUTH_ID, AUTH_PROVIDER_ID), FOREIGN KEY (AUTH_ID) REFERENCES {table_prefix}CB_AUTH_ATTEMPT (AUTH_ID) ON DELETE CASCADE diff --git a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_9.sql b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_9.sql index ca028aabbc..a6c7e3e712 100644 --- a/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_9.sql +++ b/server/bundles/io.cloudbeaver.service.security/db/cb_schema_update_9.sql @@ -2,4 +2,4 @@ ALTER TABLE {table_prefix}CB_AUTH_TOKEN ADD REFRESH_TOKEN_ID VARCHAR(128); ALTER TABLE {table_prefix}CB_AUTH_TOKEN - ADD REFRESH_TOKEN_EXPIRATION_TIME TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; \ No newline at end of file + ADD REFRESH_TOKEN_EXPIRATION_TIME TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL; \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 592892804f..4b68f9d8ae 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -375,10 +375,8 @@ public SMUser[] findUsers(@NotNull SMUserFilter filter) // Read users try (PreparedStatement dbStat = dbCon.prepareStatement( database.normalizeTableNames("SELECT USER_ID,IS_ACTIVE,DEFAULT_AUTH_ROLE FROM {table_prefix}CB_USER" - + buildUsersFilter(filter) + "\nORDER BY USER_ID LIMIT ? OFFSET ?"))) { + + buildUsersFilter(filter) + "\nORDER BY USER_ID " + getOffsetLimitPart(filter)))) { int parameterIndex = setUsersFilterValues(dbStat, filter, 1); - dbStat.setInt(parameterIndex++, filter.getPage().getLimit()); - dbStat.setInt(parameterIndex++, filter.getPage().getOffset()); try (ResultSet dbResult = dbStat.executeQuery()) { while (dbResult.next()) { @@ -424,6 +422,10 @@ public SMUser[] findUsers(@NotNull SMUserFilter filter) } } + private String getOffsetLimitPart(@NotNull SMUserFilter filter) { + return database.getDialect().getOffsetLimitQueryPart(filter.getPage().getOffset(), filter.getPage().getLimit()); + } + private String buildUsersFilter(SMUserFilter filter) { StringBuilder where = new StringBuilder(); List whereParts = new ArrayList<>(); @@ -2758,9 +2760,10 @@ public void clearOldAuthAttemptInfo() throws DBException { "WHERE EXISTS " + "(SELECT 1 FROM {table_prefix}CB_AUTH_ATTEMPT AA " + "LEFT JOIN {table_prefix}CB_AUTH_TOKEN CAT ON AA.SESSION_ID = CAT.SESSION_ID " + - "WHERE (CAT.REFRESH_TOKEN_EXPIRATION_TIME < NOW() OR CAT.EXPIRATION_TIME IS NULL) " + + "WHERE (CAT.REFRESH_TOKEN_EXPIRATION_TIME < ? OR CAT.EXPIRATION_TIME IS NULL) " + "AND AA.AUTH_ID=AAI.AUTH_ID AND AUTH_STATUS='" + SMAuthStatus.EXPIRED + "') " + "AND CREATE_TIME { - await this.graphQLService.sdk.setUserAuthRole({ userId, authRole }); + await this.performUpdate(userId, undefined, async () => { + await this.graphQLService.sdk.setUserAuthRole({ userId, authRole }); + + const user = this.get(userId); + + if (user) { + user.authRole = authRole; + } + }); if (!skipUpdate) { this.markOutdated(userId); diff --git a/webapp/packages/core-blocks/src/Containers/ColoredContainer.m.css b/webapp/packages/core-blocks/src/Containers/ColoredContainer.m.css index 2d38b9b425..4c30c0cb88 100644 --- a/webapp/packages/core-blocks/src/Containers/ColoredContainer.m.css +++ b/webapp/packages/core-blocks/src/Containers/ColoredContainer.m.css @@ -1,3 +1,6 @@ -.coloredContainer { +.secondary { composes: theme-background-secondary theme-text-on-secondary from global; } +.surface { + composes: theme-background-surface theme-text-on-surface from global; +} diff --git a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx index fe7cce0814..ec572b0904 100644 --- a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx +++ b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx @@ -15,13 +15,14 @@ import { filterContainerFakeProps, getContainerProps } from './filterContainerFa import type { IContainerProps } from './IContainerProps'; import elementsSizeStyles from './shared/ElementsSize.m.css'; -export const ColoredContainer = forwardRef>(function ColoredContainer( - { className, ...rest }, - ref, -) { +interface Props extends IContainerProps, React.HTMLAttributes { + surface?: boolean; +} + +export const ColoredContainer = forwardRef(function ColoredContainer({ className, surface, ...rest }, ref) { const styles = useS(coloredContainerStyles, containerStyles, elementsSizeStyles); const divProps = filterContainerFakeProps(rest); const containerProps = getContainerProps(rest); - return
; + return
; }); diff --git a/webapp/packages/core-blocks/src/Snackbars/SnackbarMarkups/SnackbarStatus.tsx b/webapp/packages/core-blocks/src/Snackbars/SnackbarMarkups/SnackbarStatus.tsx index a6893b2768..5a8280c6d2 100644 --- a/webapp/packages/core-blocks/src/Snackbars/SnackbarMarkups/SnackbarStatus.tsx +++ b/webapp/packages/core-blocks/src/Snackbars/SnackbarMarkups/SnackbarStatus.tsx @@ -22,7 +22,7 @@ export const SnackbarStatus: React.FC = function SnackbarSt const styles = useS(style); return status === ENotificationType.Loading ? (
- +
) : ( diff --git a/webapp/packages/core-browser/src/selectFiles.ts b/webapp/packages/core-browser/src/selectFiles.ts index 3c15074e7e..9021a69b2b 100644 --- a/webapp/packages/core-browser/src/selectFiles.ts +++ b/webapp/packages/core-browser/src/selectFiles.ts @@ -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; diff --git a/webapp/packages/core-events/src/INotification.ts b/webapp/packages/core-events/src/INotification.ts index cee5197b84..0bbda71bb3 100644 --- a/webapp/packages/core-events/src/INotification.ts +++ b/webapp/packages/core-events/src/INotification.ts @@ -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 { diff --git a/webapp/packages/core-events/src/ProcessNotificationController.ts b/webapp/packages/core-events/src/ProcessNotificationController.ts index f7052007de..3324bb4dfa 100644 --- a/webapp/packages/core-events/src/ProcessNotificationController.ts +++ b/webapp/packages/core-events/src/ProcessNotificationController.ts @@ -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; + } } diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 14498d138d..7332c011f9 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -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,8 @@ 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'], + ['ui_filter', 'Filter'], ['root_permission_denied', "You don't have permissions"], ['root_permission_no_permission', "You don't have permission for this action"], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index 2c8a057faf..c310a4e35d 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -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,8 @@ 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'], + ['ui_filter', 'Filter'], ['root_permission_denied', 'Non hai i permessi'], ['app_root_session_expire_warning_title', 'La sessione sta per scadere'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index a61d30fd4f..f7030e15e3 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -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,8 @@ export default [ ['ui_upload_file', 'Загрузить файл'], ['ui_upload_files', 'Загрузить файлы'], ['ui_upload_files_duplicate_error', 'Файлы с такими именами уже существуют'], + ['ui_upload_file_fail', 'Не удалось загрузить файл'], + ['ui_filter', 'Фильтр'], ['root_permission_denied', 'Отказано в доступе'], ['root_permission_no_permission', 'У вас нет разрешения на это действие'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index dcd8b9e5a2..7c0a1619fa 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -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,8 @@ 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'], + ['ui_filter', 'Filter'], ['root_permission_denied', '您没有权限'], ['root_permission_no_permission', '您没有权限执行此操作'], diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts index daf26f1667..fa14691ce9 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts @@ -322,13 +322,13 @@ export class NavNodeManagerService extends Bootstrap { let icon: string | undefined; let canOpen = false; - if (NodeManagerUtils.isDatabaseObject(nodeId)) { - const node = this.getNode(nodeId); - - if (node) { - name = node.name; - icon = node.icon; + const node = this.getNode(nodeId); + if (node) { + name = node.name; + icon = node.icon; + projectId ||= node.projectId; + if (NodeManagerUtils.isDatabaseObject(nodeId)) { if (node.folder) { const parent = this.getParent(node); folderId = nodeId; diff --git a/webapp/packages/core-projects/src/isResourceOfType.ts b/webapp/packages/core-projects/src/isResourceOfType.ts index 2e9f076aba..3f8a9bbfc2 100644 --- a/webapp/packages/core-projects/src/isResourceOfType.ts +++ b/webapp/packages/core-projects/src/isResourceOfType.ts @@ -8,5 +8,5 @@ import type { ProjectInfoResourceType } from './ProjectInfoResource'; export function isResourceOfType(resourceType: ProjectInfoResourceType, name: string): boolean { - return resourceType.fileExtensions.some(type => name.endsWith(`.${type}`)); + return resourceType.fileExtensions.some(type => name.toLowerCase().endsWith(`.${type.toLowerCase()}`)); } diff --git a/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts b/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts index c80271ed8c..10b0a7a1fe 100644 --- a/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts +++ b/webapp/packages/core-sdk/src/Extensions/uploadBlobResultSetExtension.ts @@ -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; } -export function uploadBlobResultSetExtension(client: CustomGraphQLClient): IUploadDriverLibraryExtension { +export function uploadBlobResultSetExtension(client: CustomGraphQLClient): IUploadBlobResultSetExtension { return { uploadBlobResultSet(fileId: string, data: Blob, onUploadProgress?: (event: UploadProgressEvent) => void): Promise { // api/resultset/blob diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBarItem.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBarItem.tsx index f3b2b7c472..fc869bb06e 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBarItem.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBarItem.tsx @@ -35,29 +35,23 @@ export const MenuBarItem = observer( const title = translate(rest.title); return (