diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index 2469bde4a7..3ed724a284 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -12,6 +12,7 @@ Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport, org.jkiss.dbeaver.model;visibility:=reexport, org.jkiss.dbeaver.model.sm;visibility:=reexport, org.jkiss.dbeaver.model.event;visibility:=reexport, + org.jkiss.dbeaver.model.nio;visibility:=reexport, org.jkiss.dbeaver.registry;visibility:=reexport, org.jkiss.dbeaver.net.ssh, org.jkiss.bundle.graphql.java;visibility:=reexport, @@ -29,6 +30,7 @@ Export-Package: io.cloudbeaver, io.cloudbeaver.model.rm, io.cloudbeaver.model.rm.local, io.cloudbeaver.model.rm.lock, + io.cloudbeaver.model.rm.fs.nio, io.cloudbeaver.model.session, io.cloudbeaver.model.user, io.cloudbeaver.registry, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java index ddeb6b94af..072304983a 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java @@ -208,8 +208,8 @@ public void removeSavedFilter(String filterName) { @Nullable @Override - public DBWNetworkProfile getNetworkProfile(String name) { - return dataSourceRegistry.getNetworkProfile(name); + public DBWNetworkProfile getNetworkProfile(String source, String name) { + return dataSourceRegistry.getNetworkProfile(source, name); } @NotNull diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMByteArrayChannel.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMByteArrayChannel.java new file mode 100644 index 0000000000..d0f6913f7f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMByteArrayChannel.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package io.cloudbeaver.model.rm.fs.nio; + +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.nio.ByteArrayChannel; + +import java.io.IOException; +import java.nio.file.OpenOption; +import java.util.Set; + +// copy of jdk.nio.zipfs.ByteArrayChannel +public class RMByteArrayChannel extends ByteArrayChannel { + + private final RMPath rmPath; + + public RMByteArrayChannel(byte[] buf, RMPath rmPath, Set options) { + super(buf, options); + this.rmPath = rmPath; + } + + @Override + protected void createNewFile() throws IOException { + try { + rmPath.getFileSystem().getRmController().createResource( + rmPath.getRmProjectId(), rmPath.getResourcePath(), false + ); + } catch (DBException e) { + throw new IOException("Failed to create new file: " + e.getMessage(), e); + } + } + + @Override + protected void writeToFile() throws IOException { + try { + rmPath.getFileSystem().getRmController().setResourceContents( + rmPath.getRmProjectId(), rmPath.getResourcePath(), buf, true + ); + } catch (DBException e) { + throw new IOException("Failed to write data to the file: " + e.getMessage(), e); + } + } + + @Override + protected void deleteFile() throws IOException { + try { + rmPath.getFileSystem().getRmController().deleteResource( + rmPath.getRmProjectId(), rmPath.getResourcePath(), true + ); + } catch (DBException e) { + throw new IOException("Failed to delete file: " + e.getMessage(), e); + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystem.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystem.java new file mode 100644 index 0000000000..0abc646d75 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystem.java @@ -0,0 +1,149 @@ +/* + * 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.rm.fs.nio; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.nio.NIOFileSystem; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.dbeaver.model.rm.RMProjectPermission; +import org.jkiss.utils.ArrayUtils; +import org.jkiss.utils.CommonUtils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileStore; +import java.nio.file.Path; +import java.nio.file.spi.FileSystemProvider; +import java.util.List; + +public class RMNIOFileSystem extends NIOFileSystem { + private static final Log log = Log.getLog(RMNIOFileSystem.class); + @Nullable + // null for root 'rm://' path + private final String rmProjectId; + @NotNull + private final RMController rmController; + @NotNull + private final RMNIOFileSystemProvider rmNioFileSystemProvider; + @Nullable + private RMProject rmProject; + + public RMNIOFileSystem( + @Nullable String rmProjectId, + @NotNull RMController rmController, + @NotNull RMNIOFileSystemProvider rmNioFileSystemProvider + ) { + this.rmProjectId = rmProjectId; + this.rmController = rmController; + this.rmNioFileSystemProvider = rmNioFileSystemProvider; + } + + @Override + public FileSystemProvider provider() { + return rmNioFileSystemProvider; + } + + @Override + public void close() throws IOException { + + } + + @Override + public boolean isOpen() { + return true; + } + + @Override + public boolean isReadOnly() { + try { + return getRmProject().hasProjectPermission(RMProjectPermission.RESOURCE_EDIT.getPermissionId()); + } catch (IOException e) { + return true; + } + } + + @Override + public Iterable getRootDirectories() { + return List.of(new RMPath(this)); + } + + @Override + public Iterable getFileStores() { + try { + return List.of(new RMNIOProjectFileStore(getRmProject())); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + return List.of(); + } + + @NotNull + @Override + public Path getPath(@NotNull String first, @NotNull String... more) { + if (CommonUtils.isEmpty(first)) { + throw new IllegalArgumentException("Empty path"); + } + StringBuilder uriBuilder = new StringBuilder(); + uriBuilder.append(provider().getScheme()).append("://"); + if (rmProjectId != null) { + uriBuilder.append(rmProjectId).append(getSeparator()); + + } + uriBuilder.append(first); + if (!ArrayUtils.isEmpty(more)) { + uriBuilder + .append(getSeparator()) + .append(String.join(getSeparator(), more)); + } + return provider().getPath(URI.create(uriBuilder.toString())); + } + + @NotNull + public RMController getRmController() { + return rmController; + } + + @Nullable + public String getRmProjectId() { + return rmProjectId; + } + + @NotNull + public synchronized RMProject getRmProject() throws IOException { + if (rmProject != null) { + return rmProject; + } + if (rmProjectId == null) { + throw new IOException("Project id not specified"); + } + try { + RMProject project = rmController.getProject(rmProjectId, false, false); + if (project == null) { + throw new FileNotFoundException("Project not exist: " + rmProjectId); + } + rmProject = project; + return rmProject; + } catch (DBException e) { + throw new IOException("Failed to get project:" + e.getMessage(), e); + } + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystemProvider.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystemProvider.java new file mode 100644 index 0000000000..e87da418fa --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOFileSystemProvider.java @@ -0,0 +1,293 @@ +/* + * 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.rm.fs.nio; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.nio.NIOFileSystemProvider; +import org.jkiss.dbeaver.model.nio.NIOUtils; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMResource; +import org.jkiss.dbeaver.model.rm.RMUtils; +import org.jkiss.utils.CommonUtils; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.FileAttributeView; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public class RMNIOFileSystemProvider extends NIOFileSystemProvider { + + @NotNull + private final RMController rmController; + + public RMNIOFileSystemProvider(@NotNull RMController rmController) { + this.rmController = rmController; + } + + @Override + public String getScheme() { + return "rm"; + } + + @Override + public FileSystem newFileSystem(URI uri, Map env) throws IOException { + throw new FileAlreadyExistsException("RM file system already exist"); + } + + @Override + public FileSystem getFileSystem(URI uri) { + validateUri(uri); + String projectId = uri.getAuthority(); + if (CommonUtils.isEmpty(projectId)) { + projectId = null; + } + try { + return new RMNIOFileSystem(projectId, rmController, this); + } catch (Exception e) { + throw new FileSystemNotFoundException("RM file system not found: " + e.getMessage()); + } + } + + @Override + public Path getPath(URI uri) { + validateUri(uri); + String projectId = uri.getAuthority(); + if (CommonUtils.isEmpty(projectId)) { + projectId = null; + } + RMNIOFileSystem rmNioFileSystem = new RMNIOFileSystem(projectId, rmController, this); + String resourcePath = uri.getPath(); + if (CommonUtils.isNotEmpty(resourcePath) && projectId == null) { + throw new IllegalArgumentException("Project is not specified in URI"); + } + if (CommonUtils.isEmpty(resourcePath)) { + return new RMPath(rmNioFileSystem); + } else { + return new RMPath(rmNioFileSystem, resourcePath); + } + } + + @Override + public RMByteArrayChannel newByteChannel(Path path, Set options, FileAttribute... attrs) throws IOException { + RMPath rmPath = (RMPath) path; + if (rmPath.isProjectPath()) { + throw new IllegalArgumentException("Cannot open channel for the project"); + } + + try { + if (Files.exists(path)) { + byte[] data = rmController.getResourceContents(rmPath.getRmProjectId(), rmPath.getResourcePath()); + return new RMByteArrayChannel(data, rmPath, options); + } else { + return new RMByteArrayChannel(new byte[0], rmPath, options); + } + } catch (DBException e) { + throw new IOException("Failed to read resource: " + e.getMessage(), e); + } + } + + @Override + public OutputStream newOutputStream(Path path, OpenOption... options) throws IOException { + return new RMOutputStream((RMPath) path); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) throws IOException { + RMPath rmDir = (RMPath) dir; + String rmDirPath = rmDir.getResourcePath(); + String separator = rmDir.getFileSystem().getSeparator(); + + return new DirectoryStream<>() { + @Override + public Iterator iterator() { + var rmController = rmDir.getFileSystem().getRmController(); + try { + if (rmDir.isRmRootPath()) { + return Arrays.stream(rmController.listAccessibleProjects()) + .map(rmProject -> (Path) new RMPath( + new RMNIOFileSystem( + rmProject.getId(), + rmController, + RMNIOFileSystemProvider.this + ) + ) + ).iterator(); + } else { + var resources = rmController.listResources( + rmDir.getRmProjectId(), + rmDirPath, + null, + false, + false, + false + ); + return Arrays.stream(resources) + .map(rmResource -> (Path) new RMPath(rmDir.getFileSystem(), + NIOUtils.resolve(separator, rmDirPath, rmResource.getName()) + )) + .iterator(); + } + } catch (DBException e) { + throw new RuntimeException("Failed to read resources from rm path: " + e.getMessage(), e); + } + } + + @Override + public void close() throws IOException { + + } + }; + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + RMPath rmDir = (RMPath) dir; + try { + if (rmDir.isProjectPath()) { + rmController.createProject(RMUtils.getProjectName(rmDir.getRmProjectId()), null); + } else { + rmController.createResource(rmDir.getRmProjectId(), rmDir.getResourcePath(), true); + } + } catch (DBException e) { + throw new IOException("Failed to create directory: " + e.getMessage(), e); + } + } + + @Override + public void delete(Path path) throws IOException { + RMPath rmPath = (RMPath) path; + if (rmPath.isRmRootPath()) { + throw new IOException("Can not delete root rm directory"); + } + try { + String rmProjectId = rmPath.getRmProjectId(); + var rmController = rmPath.getFileSystem().getRmController(); + if (rmPath.isProjectPath()) { + rmController.deleteProject(rmProjectId); + } else { + rmController.deleteResource(rmProjectId, rmPath.getResourcePath(), true); + } + } catch (DBException e) { + throw new IOException("Failed to delete rm resource: " + e.getMessage(), e); + } + } + + @Override + public void copy(Path source, Path target, CopyOption... options) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void move(Path source, Path target, CopyOption... options) throws IOException { + RMPath rmSource = (RMPath) source; + RMPath rmTarget = (RMPath) target; + try { + rmController.moveResource(rmSource.getRmProjectId(), rmSource.getResourcePath(), rmTarget.getResourcePath()); + } catch (DBException e) { + throw new IOException("Failed to move rm resource: " + e.getMessage(), e); + } + } + + @Override + public boolean isSameFile(Path path, Path path2) throws IOException { + return path.toString().equals(path2.toString()); + } + + @Override + public boolean isHidden(Path path) throws IOException { + //rm does not support hidden files + return false; + } + + @Override + public FileStore getFileStore(Path path) throws IOException { + RMPath rmPath = (RMPath) path; + if (rmPath.isProjectPath()) { + return new RMNIOProjectFileStore(rmPath.getFileSystem().getRmProject()); + } else { + try { + RMResource rmResource = rmController.getResource(rmPath.getRmProjectId(), rmPath.getResourcePath()); + if (rmResource == null) { + throw new FileNotFoundException(); + } + return new RMNIOResourceFileStore(rmResource); + } catch (DBException e) { + throw new IOException("Failed to "); + } + } + } + + @Override + public void checkAccess(Path path, AccessMode... modes) throws IOException { + RMPath rmPath = (RMPath) path; + try { + if (rmPath.isProjectPath()) { + if (rmController.getProject(rmPath.getRmProjectId(), false, false) == null) { + // we should throw error, otherwise Files.exists return true + throw new FileNotFoundException("Project not found: " + rmPath.getRmProjectId()); + } + } else if (rmController.getResource(rmPath.getRmProjectId(), rmPath.getResourcePath()) == null) { + // some here + throw new FileNotFoundException("Resource not found: " + rmPath.getResourcePath()); + } + } catch (Exception e) { + throw new IOException("Failed to check path access: " + e.getMessage(), e); + } + } + + @Override + public V getFileAttributeView(Path path, Class type, LinkOption... options) { + return null; + } + + @Override + public A readAttributes(Path path, Class type, LinkOption... options) throws IOException { + if (type == BasicFileAttributes.class) { + RMPath rmPath = (RMPath) path; + try { + if (rmPath.isRmRootPath()) { + return type.cast(new RMRootBasicAttribute()); + } + if (rmPath.isProjectPath()) { + boolean projectNotExist = rmController.getProject(rmPath.getRmProjectId(), false, false) == null; + if (projectNotExist) { + throw new FileNotFoundException(); + } + return type.cast(new RMRootBasicAttribute()); + } else { + RMResource rmResource = rmController.getResource(rmPath.getRmProjectId(), rmPath.getResourcePath()); + if (rmResource == null) { + throw new FileNotFoundException(); + } + return type.cast(new RMResourceBasicAttribute(rmResource)); + } + } catch (DBException e) { + throw new IOException("Failed to read resource attribute: " + e.getMessage(), e); + } + } + return null; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOProjectFileStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOProjectFileStore.java new file mode 100644 index 0000000000..da1bd715f8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOProjectFileStore.java @@ -0,0 +1,50 @@ +/* + * 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.rm.fs.nio; + +import org.jkiss.dbeaver.model.nio.NIOFileStore; +import org.jkiss.dbeaver.model.rm.RMProject; + +import java.io.IOException; + +public class RMNIOProjectFileStore extends NIOFileStore { + private final RMProject rmProject; + + public RMNIOProjectFileStore(RMProject rmProject) { + this.rmProject = rmProject; + } + + @Override + public String name() { + return rmProject.getName(); + } + + @Override + public String type() { + return "rmfs"; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public long getTotalSpace() throws IOException { + return 0; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOResourceFileStore.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOResourceFileStore.java new file mode 100644 index 0000000000..a4bc1d4c5c --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMNIOResourceFileStore.java @@ -0,0 +1,50 @@ +/* + * 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.rm.fs.nio; + +import org.jkiss.dbeaver.model.nio.NIOFileStore; +import org.jkiss.dbeaver.model.rm.RMResource; + +import java.io.IOException; + +public class RMNIOResourceFileStore extends NIOFileStore { + private final RMResource rmResource; + + public RMNIOResourceFileStore(RMResource rmResource) { + this.rmResource = rmResource; + } + + @Override + public String name() { + return rmResource.getName(); + } + + @Override + public String type() { + return "rmfs"; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public long getTotalSpace() throws IOException { + return rmResource.getLength(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMOutputStream.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMOutputStream.java new file mode 100644 index 0000000000..09fee299e9 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMOutputStream.java @@ -0,0 +1,46 @@ +/* + * 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.rm.fs.nio; + +import org.jkiss.dbeaver.DBException; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; + +public class RMOutputStream extends ByteArrayOutputStream { + private final RMPath rmPath; + + public RMOutputStream(RMPath rmPath) { + this.rmPath = rmPath; + } + + @Override + public void close() throws IOException { + try { + rmPath.getFileSystem().getRmController().setResourceContents( + rmPath.getRmProjectId(), + rmPath.getResourcePath(), + Arrays.copyOfRange(buf, 0, count), + true + ); + } catch (DBException e) { + throw new IOException("Failed to write data to the resource: " + e.getMessage(), e); + } + super.close(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMPath.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMPath.java new file mode 100644 index 0000000000..a1e9a49c37 --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMPath.java @@ -0,0 +1,205 @@ +/* + * 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.rm.fs.nio; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.nio.NIOPath; +import org.jkiss.utils.CommonUtils; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.util.Arrays; + +public class RMPath extends NIOPath { + @NotNull + private final RMNIOFileSystem rmNioFileSystem; + @Nullable + private final String rmProjectId; + + public RMPath( + @NotNull RMNIOFileSystem rmNioFileSystem + ) { + super(null, rmNioFileSystem); + this.rmNioFileSystem = rmNioFileSystem; + this.rmProjectId = rmNioFileSystem.getRmProjectId(); + } + + public RMPath( + @NotNull RMNIOFileSystem rmNioFileSystem, + @NotNull String path + ) { + super(path, rmNioFileSystem); + this.rmNioFileSystem = rmNioFileSystem; + this.rmProjectId = rmNioFileSystem.getRmProjectId(); + } + + @Override + public RMNIOFileSystem getFileSystem() { + return rmNioFileSystem; + } + + @Override + public Path getRoot() { + if (isProjectPath()) { + return null; + } + return new RMPath(rmNioFileSystem); + } + + @Override + public Path getFileName() { + return this; + } + + @Override + public Path getParent() { + // project is root and have no parent + if (isProjectPath()) { + return null; + } + + String[] parts = pathParts(); + if (parts.length == 1) { + return new RMPath(rmNioFileSystem); // project is parent + } + //return parent folder + String[] parentParts = Arrays.copyOfRange(parts, 0, parts.length - 1); + return new RMPath(rmNioFileSystem, String.join(getFileSystem().getSeparator(), parentParts)); + } + + @Override + public int getNameCount() { + return pathParts().length; + } + + @Override + public boolean startsWith(@NotNull Path other) { + if (!(other instanceof RMPath)) { + return false; + } + return toString().startsWith(other.toString()); + } + + @Override + public Path normalize() { + return this; + } + + @Override + public Path resolve(Path other) { + RMPath rmOther = (RMPath) other; + if (!rmOther.rmProjectId.equals(rmProjectId)) { + throw new IllegalArgumentException("Cannot resolve path from other project"); + } + return resolve(rmOther.getResourcePath()); + } + + @Override + public Path resolve(String other) { + if (CommonUtils.isEmpty(other)) { + return this; + } + return new RMPath( + rmNioFileSystem, + resolveString(other) + ); + } + + @Override + public URI toUri() { + var fileSystem = getFileSystem(); + var uriBuilder = new StringBuilder(fileSystem.provider().getScheme()) + .append("://") + .append(rmProjectId); + + String rmResourcePath = getResourcePath(); + if (rmResourcePath != null) { + uriBuilder.append(fileSystem.getSeparator()) + .append(rmResourcePath); + } + + return URI.create(uriBuilder.toString()); + } + + @Override + public Path toAbsolutePath() { + if (isAbsolute()) { + return this; + } + return new RMPath(rmNioFileSystem, "/" + path); + } + + @Override + public Path toRealPath(@NotNull LinkOption... options) throws IOException { + return toAbsolutePath(); + } + + @Override + public Path relativize(@NotNull Path other) { + var relativeUri = toUri().resolve(other.toUri()); + return new RMPath(rmNioFileSystem, relativeUri.getPath()); + } + + @Override + public Path getName(int index) { + String[] parts = pathParts(); + if (index < 0 || index > parts.length) { + throw new IllegalArgumentException("Invalid index value: " + index); + } + return new RMPath(rmNioFileSystem, parts[index]); + } + + @Override + public Path subpath(int beginIndex, int endIndex) { + String[] parts = pathParts(); + if (beginIndex < 0 || beginIndex > parts.length) { + throw new IllegalArgumentException("Invalid begin index value: " + beginIndex); + } + + if (endIndex < 0 || endIndex > parts.length || endIndex < beginIndex) { + throw new IllegalArgumentException("Invalid end index value: " + endIndex); + } + + String[] subParts = Arrays.copyOfRange(parts, beginIndex, endIndex); + + return new RMPath(rmNioFileSystem, String.join(getFileSystem().getSeparator(), subParts)); + } + + @Nullable + public String getResourcePath() { + return path; + } + + public boolean isRmRootPath() { + return rmProjectId == null && CommonUtils.isEmpty(path); + } + public boolean isProjectPath() { + return rmProjectId != null && CommonUtils.isEmpty(path); + } + + @NotNull + public String getRmProjectId() throws DBException { + if (rmProjectId == null) { + throw new DBException("Project id not specified"); + } + return rmProjectId; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMResourceBasicAttribute.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMResourceBasicAttribute.java new file mode 100644 index 0000000000..2387e3e07e --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMResourceBasicAttribute.java @@ -0,0 +1,48 @@ +/* + * 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.rm.fs.nio; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.nio.NIOFileBasicAttribute; +import org.jkiss.dbeaver.model.rm.RMResource; + +import java.nio.file.attribute.FileTime; + +public class RMResourceBasicAttribute extends NIOFileBasicAttribute { + + @NotNull + private final RMResource rmResource; + + public RMResourceBasicAttribute(@NotNull RMResource rmResource) { + this.rmResource = rmResource; + } + + @Override + public FileTime lastModifiedTime() { + return FileTime.fromMillis(rmResource.getLastModified()); + } + + @Override + public boolean isDirectory() { + return rmResource.isFolder(); + } + + @Override + public long size() { + return rmResource.getLength(); + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMRootBasicAttribute.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMRootBasicAttribute.java new file mode 100644 index 0000000000..5ce279ae3f --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/fs/nio/RMRootBasicAttribute.java @@ -0,0 +1,38 @@ +/* + * 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.rm.fs.nio; + +import org.jkiss.dbeaver.model.nio.NIOFileBasicAttribute; + +import java.nio.file.attribute.FileTime; + +public class RMRootBasicAttribute extends NIOFileBasicAttribute { + @Override + public FileTime lastModifiedTime() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDirectory() { + return true; + } + + @Override + public long size() { + return 0; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index dacdc3ea4d..34d11920e8 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -730,6 +730,12 @@ public RMResource[] getResourcePath(@NotNull String projectId, @NotNull String r return makeResourcePath(projectId, getTargetPath(projectId, resourcePath), false).toArray(RMResource[]::new); } + @Nullable + @Override + public RMResource getResource(@NotNull String projectId, @NotNull String resourcePath) throws DBException { + return makeResourceFromPath(projectId, getTargetPath(projectId, resourcePath), null, false, false, false); + } + @NotNull @Override public byte[] getResourceContents(@NotNull String projectId, @NotNull String resourcePath) throws DBException { diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java index ca2bc074e5..3fd68da3c2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/service/sql/WebSQLResultsInfo.java @@ -26,7 +26,9 @@ import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.DBSTypedObject; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Web query results info. @@ -82,6 +84,18 @@ public DBDRowIdentifier getDefaultRowIdentifier() { return null; } + @NotNull + public Set getRowIdentifiers() { + Set rowIdentifiers = new HashSet<>(); + for (DBDAttributeBinding column : attributes) { + DBDRowIdentifier rowIdentifier = column.getRowIdentifier(); + if (rowIdentifier != null) { + rowIdentifiers.add(rowIdentifier); + } + } + return rowIdentifiers; + } + public DBSAttributeBase getAttribute(String attributeName) { DBPDataSource dataSource = dataContainer.getDataSource(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java index fd3c72e29d..a7451a897b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jobs/SessionStateJob.java @@ -23,7 +23,7 @@ public class SessionStateJob extends PeriodicSystemJob { private static final Log log = Log.getLog(SessionStateJob.class); - private static final int PERIOD_MS = 30_000; // once per 60 seconds + private static final int PERIOD_MS = 30_000; // once per 30 seconds public SessionStateJob(@NotNull CBPlatform platform) { super("Session state sender", platform, PERIOD_MS); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index 4ea954fae9..28b3b20261 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -55,6 +55,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; /** * Web SQL processor. @@ -315,56 +316,67 @@ public WebSQLExecuteInfo updateResultsDataBatch( @Nullable List addedRows, @Nullable WebDataFormat dataFormat) throws DBException { - Map resultBatches = new LinkedHashMap<>(); - + List newResultSetRows = new ArrayList<>(); KeyDataReceiver keyReceiver = new KeyDataReceiver(contextInfo.getResults(resultsId)); - - DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( - monitor, contextInfo, resultsId, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, keyReceiver); - WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + Set rowIdentifierList = new HashSet<>(); + // several row identifiers could be if we update result set table with join + // we can't add or delete rows from result set table with join + if (!CommonUtils.isEmpty(deletedRows) || !CommonUtils.isEmpty(addedRows)) { + rowIdentifierList.add(resultsInfo.getDefaultRowIdentifier()); + } else if (!CommonUtils.isEmpty(updatedRows)) { + rowIdentifierList = resultsInfo.getRowIdentifiers(); + } + long totalUpdateCount = 0; WebSQLExecuteInfo result = new WebSQLExecuteInfo(); List queryResults = new ArrayList<>(); + for (var rowIdentifier : rowIdentifierList) { + Map resultBatches = new LinkedHashMap<>(); + DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( + monitor, resultsInfo, rowIdentifier, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, keyReceiver); + + + DBCExecutionContext executionContext = getExecutionContext(dataManipulator); + try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { + DBCTransactionManager txnManager = DBUtils.getTransactionManager(executionContext); + boolean revertToAutoCommit = false; + if (txnManager != null && txnManager.isSupportsTransactions() && txnManager.isAutoCommit()) { + txnManager.setAutoCommit(monitor, false); + revertToAutoCommit = true; + } + try { + Map options = Collections.emptyMap(); + for (Map.Entry rb : resultBatches.entrySet()) { + DBSDataManipulator.ExecuteBatch batch = rb.getKey(); + Object[] rowValues = rb.getValue(); + keyReceiver.setRow(rowValues); + DBCStatistics statistics = batch.execute(session, options); + + // Patch result rows (adapt to web format) + for (int i = 0; i < rowValues.length; i++) { + rowValues[i] = WebSQLUtils.makeWebCellValue(webSession, resultsInfo.getAttributeByPosition(i), rowValues[i], dataFormat); + } - DBCExecutionContext executionContext = getExecutionContext(dataManipulator); - try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { - DBCTransactionManager txnManager = DBUtils.getTransactionManager(executionContext); - boolean revertToAutoCommit = false; - if (txnManager != null && txnManager.isSupportsTransactions() && txnManager.isAutoCommit()) { - txnManager.setAutoCommit(monitor, false); - revertToAutoCommit = true; - } - try { - Map options = Collections.emptyMap(); - for (Map.Entry rb : resultBatches.entrySet()) { - DBSDataManipulator.ExecuteBatch batch = rb.getKey(); - Object[] rowValues = rb.getValue(); - keyReceiver.setRow(rowValues); - DBCStatistics statistics = batch.execute(session, options); - - // Patch result rows (adapt to web format) - for (int i = 0; i < rowValues.length; i++) { - rowValues[i] = WebSQLUtils.makeWebCellValue(webSession, resultsInfo.getAttributeByPosition(i), rowValues[i], dataFormat); + totalUpdateCount += statistics.getRowsUpdated(); + result.setDuration(result.getDuration() + statistics.getExecuteTime()); + newResultSetRows.add(rowValues); } - totalUpdateCount += statistics.getRowsUpdated(); - result.setDuration(result.getDuration() + statistics.getExecuteTime()); - } - - if (txnManager != null && txnManager.isSupportsTransactions()) { - txnManager.commit(session); - } - } catch (Exception e) { - if (txnManager != null && txnManager.isSupportsTransactions()) { - txnManager.rollback(session, null); - } - throw new DBCException("Error persisting data changes", e); - } finally { - if (revertToAutoCommit) { - txnManager.setAutoCommit(monitor, true); + if (txnManager != null && txnManager.isSupportsTransactions()) { + txnManager.commit(session); + } + } catch (Exception e) { + if (txnManager != null && txnManager.isSupportsTransactions()) { + txnManager.rollback(session, null); + } + throw new DBCException("Error persisting data changes", e); + } finally { + if (revertToAutoCommit) { + txnManager.setAutoCommit(monitor, true); + } } } } @@ -376,7 +388,7 @@ public WebSQLExecuteInfo updateResultsDataBatch( WebSQLQueryResults updateResults = new WebSQLQueryResults(webSession, dataFormat); updateResults.setUpdateRowCount(totalUpdateCount); updateResults.setResultSet(updatedResultSet); - updatedResultSet.setRows(resultBatches.values().toArray(new Object[0][])); + updatedResultSet.setRows(newResultSetRows.toArray(new Object[0][])); queryResults.add(updateResults); @@ -396,26 +408,42 @@ public String generateResultsDataUpdateScript( { Map resultBatches = new LinkedHashMap<>(); - DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( - monitor, contextInfo, resultsId, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, null); - List actions = new ArrayList<>(); - - DBCExecutionContext executionContext = getExecutionContext(dataManipulator); - try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { - Map options = Collections.emptyMap(); - for (DBSDataManipulator.ExecuteBatch batch : resultBatches.keySet()) { - batch.generatePersistActions(session, actions, options); + WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + Set rowIdentifierList = new HashSet<>(); + // several row identifiers could be if we update result set table with join + // we can't add or delete rows from result set table with join + if (!CommonUtils.isEmpty(deletedRows) || !CommonUtils.isEmpty(addedRows)) { + rowIdentifierList.add(resultsInfo.getDefaultRowIdentifier()); + } else if (!CommonUtils.isEmpty(updatedRows)) { + rowIdentifierList = resultsInfo.getRowIdentifiers(); + } + StringBuilder sqlBuilder = new StringBuilder(); + for (var rowIdentifier : rowIdentifierList) { + DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( + monitor, resultsInfo, rowIdentifier, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, null); + + List actions = new ArrayList<>(); + + DBCExecutionContext executionContext = getExecutionContext(dataManipulator); + try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { + Map options = Collections.emptyMap(); + for (DBSDataManipulator.ExecuteBatch batch : resultBatches.keySet()) { + batch.generatePersistActions(session, actions, options); + } } - } - return SQLUtils.generateScript(executionContext.getDataSource(), actions.toArray(new DBEPersistAction[0]), false); + sqlBuilder.append( + SQLUtils.generateScript(executionContext.getDataSource(), actions.toArray(new DBEPersistAction[0]), false) + ); + } + return sqlBuilder.toString(); } private DBSDataManipulator generateUpdateResultsDataBatch( @NotNull DBRProgressMonitor monitor, - @NotNull WebSQLContextInfo contextInfo, - @NotNull String resultsId, + @NotNull WebSQLResultsInfo resultsInfo, + @NotNull DBDRowIdentifier rowIdentifier, @Nullable List updatedRows, @Nullable List deletedRows, @Nullable List addedRows, @@ -424,10 +452,7 @@ private DBSDataManipulator generateUpdateResultsDataBatch( @Nullable DBDDataReceiver keyReceiver) throws DBException { - WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); - DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); - checkRowIdentifier(resultsInfo, rowIdentifier); DBSEntity dataContainer = rowIdentifier.getEntity(); checkDataEditAllowed(dataContainer); DBSDataManipulator dataManipulator = (DBSDataManipulator) dataContainer; @@ -448,7 +473,9 @@ private DBSDataManipulator generateUpdateResultsDataBatch( if (!CommonUtils.isEmpty(updatedRows)) { for (WebSQLResultsRow row : updatedRows) { - Map updateValues = row.getUpdateValues(); + Map updateValues = row.getUpdateValues().entrySet().stream() + .filter(x -> CommonUtils.equalObjects(allAttributes[CommonUtils.toInt(x.getKey())].getRowIdentifier(), rowIdentifier)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); if (CommonUtils.isEmpty(row.getData()) || CommonUtils.isEmpty(updateValues)) { continue; } diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java new file mode 100644 index 0000000000..7aeb0c9f6d --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/model/rm/RMNIOTest.java @@ -0,0 +1,177 @@ +/* + * 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.rm; + +import io.cloudbeaver.model.rm.fs.nio.RMNIOFileSystem; +import io.cloudbeaver.model.rm.fs.nio.RMNIOFileSystemProvider; +import io.cloudbeaver.model.rm.fs.nio.RMPath; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.server.CBConstants; +import io.cloudbeaver.test.platform.CEServerTestSuite; +import io.cloudbeaver.utils.WebTestUtils; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.auth.SMAuthStatus; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.rm.RMController; +import org.jkiss.dbeaver.model.rm.RMProject; +import org.jkiss.utils.SecurityUtils; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.net.CookieManager; +import java.net.URI; +import java.net.http.HttpClient; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class RMNIOTest { + + private static WebSession webSession; + private static RMProject testProject; + private static RMNIOFileSystemProvider rmFsProvider; + + @BeforeClass + public static void init() throws Exception { + var cookieManager = new CookieManager(); + var client = HttpClient.newBuilder() + .cookieHandler(cookieManager) + .version(HttpClient.Version.HTTP_2) + .build(); + Map authInfo = WebTestUtils.authenticateUser( + client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL); + Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus")); + + String sessionId = cookieManager.getCookieStore().getCookies() + .stream() + .filter(cookie -> cookie.getName().equals(CBConstants.CB_SESSION_COOKIE_NAME)) + .findFirst() + .get() + .getValue(); + webSession = (WebSession) CEServerTestSuite.getTestApp().getSessionManager().getSession(sessionId); + Assert.assertNotNull(webSession); + var projectName = "NIO_Test" + SecurityUtils.generateUniqueId(); + testProject = webSession.getRmController().createProject(projectName, null); + rmFsProvider = new RMNIOFileSystemProvider(webSession.getRmController()); + } + + @AfterClass + public static void destroy() throws Exception { + if (webSession != null && testProject != null) { + webSession.getUserContext().getRmController().deleteProject(testProject.getId()); + } + } + + @Test + public void projectPathTest() throws DBException { + var projectUri = getProjectUri(); + RMPath path = (RMPath) rmFsProvider.getPath(projectUri); + Assert.assertEquals(path.getRmProjectId(), testProject.getId()); + Assert.assertTrue(path.isProjectPath()); + Assert.assertTrue(path.isAbsolute()); + Assert.assertNull(path.getParent()); + Assert.assertNull(path.getRoot()); + Assert.assertTrue(Files.isDirectory(path)); + Assert.assertTrue(Files.exists(path)); + } + + @Test + public void testNotExistProject() { + RMPath notExistPath = new RMPath(new RMNIOFileSystem("s_not_exist", webSession.getRmController(), rmFsProvider)); + Assert.assertFalse(Files.exists(notExistPath)); + } + + @Test + public void testNioProjectOperations() throws IOException, DBException { + var randomName = SecurityUtils.generateUniqueId(); + String randomProject = "s_random_project_" + randomName; + + RMPath newProjectPath = new RMPath(new RMNIOFileSystem(randomProject, webSession.getRmController(), rmFsProvider)); + Assert.assertFalse(Files.exists(newProjectPath)); + + //create project via nio + Files.createDirectory(newProjectPath); + Assert.assertTrue(Files.exists(newProjectPath)); + Assert.assertNotNull(webSession.getRmController().getProject(randomProject, false, false)); + + //delete project via nio + Files.delete(newProjectPath); + Assert.assertFalse(Files.exists(newProjectPath)); + Assert.assertNull(webSession.getRmController().getProject(randomProject, false, false)); + } + + @Test + public void testResolve() { + RMPath rootPath = (RMPath) rmFsProvider.getPath(getProjectUri()); + String scriptName = "child.sql"; + Path scriptPath = rootPath.resolve(scriptName); + Assert.assertEquals(getProjectUri() + "/" + "child.sql", scriptPath.toString()); + } + + @Test + public void testListFiles() throws DBException, IOException { + RMPath rootPath = (RMPath) rmFsProvider.getPath(getProjectUri()); + RMController rm = webSession.getRmController(); + String file1 = "folder" + SecurityUtils.generateUniqueId(); + String file2 = "script" + SecurityUtils.generateUniqueId() + ".sql"; + rm.createResource(testProject.getId(), file1, true); + rm.createResource(testProject.getId(), file2, false); + Set filesFromNio = + Files.list(rootPath) + .map(path -> ((RMPath) path).getResourcePath()) + .collect(Collectors.toSet()); + Assert.assertTrue(filesFromNio.contains(file1)); + Assert.assertTrue(filesFromNio.contains(file2)); + } + + @Test + public void testWriteResource() throws IOException, DBException { + RMPath rootPath = (RMPath) rmFsProvider.getPath(getProjectUri()); + RMController rm = webSession.getRmController(); + String script = "test_script_" + SecurityUtils.generateUniqueId() + ".sql"; + RMPath scriptPath = (RMPath) rootPath.resolve(script); + + //create file + Files.createFile(scriptPath); + Assert.assertTrue(Files.exists(scriptPath)); + Assert.assertNotNull(rm.getResource(testProject.getId(), script)); + + //set content rm://s_test_project/test_script.sql + + String sql = "select " + SecurityUtils.getRandom().nextInt(1000); + Files.writeString(scriptPath, sql); + String dataFromNio = Files.readString(scriptPath); + String dataFromRM = new String(rm.getResourceContents(testProject.getId(), script)); + Assert.assertEquals(sql, dataFromNio); + Assert.assertEquals(sql, dataFromRM); + + //delete + Files.delete(scriptPath); + Assert.assertFalse(Files.exists(scriptPath)); + Assert.assertNull(rm.getResource(testProject.getId(), script)); + } + + + private URI getProjectUri() { + return URI.create("rm://" + testProject.getId()); + } +} diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java index cea79cfe56..9bd50a7fd8 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/CEServerTestSuite.java @@ -17,6 +17,7 @@ package io.cloudbeaver.test.platform; +import io.cloudbeaver.model.rm.RMNIOTest; import io.cloudbeaver.model.rm.lock.RMLockTest; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBApplicationCE; @@ -36,7 +37,8 @@ ConnectionsTest.class, AuthenticationTest.class, ResourceManagerTest.class, - RMLockTest.class + RMLockTest.class, + RMNIOTest.class } ) public class CEServerTestSuite { diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index c7db9c9117..4f39ea584b 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -108,7 +108,7 @@ export class UserInfoResource extends CachedDataResource boolean; + geLayoutSize?: (property: ObjectPropertyInfo) => ILayoutSizeProps; onFocus?: (name: string) => void; } @@ -49,6 +50,7 @@ export const ObjectPropertyInfoForm = observer(function hideEmptyPlaceholder, canShowPassword, isSaved, + geLayoutSize, onFocus, ...rest }) { @@ -77,7 +79,7 @@ export const ObjectPropertyInfoForm = observer(function return ( ) => void; } -type ControlType = 'checkbox' | 'combobox' | 'link' | 'input' | 'textarea' | 'file'; - -function getControlTypeFor(property: ObjectPropertyInfo): ControlType { - const dataType = property.dataType?.toLowerCase(); - - if (dataType === 'boolean') { - return 'checkbox'; - } else if (property.validValues && property.validValues.length > 0) { - return 'combobox'; - } else if (property.features.includes('href')) { - return 'link'; - } else if (dataType === 'string' && property.length === 'MULTILINE') { - return 'textarea'; - } else if (property.features.includes('file')) { - return 'file'; - } - - return 'input'; -} - function getValue(value: any, controlType: ControlType) { const checkbox = controlType === 'checkbox'; @@ -87,15 +68,15 @@ export const RenderField = observer(function RenderField({ }) { const translate = useTranslate(); - const controltype = getControlTypeFor(property); + const controlType = getPropertyControlType(property); const password = property.features.includes('password'); - const value = getValue(property.value, controltype); - const defaultValue = getValue(property.defaultValue, controltype); + const value = getValue(property.value, controlType); + const defaultValue = getValue(property.defaultValue, controlType); const passwordSaved = showRememberTip && ((password && !!property.value) || saved); const description = passwordSaved ? translate('ui_processing_saved') : undefined; - if (controltype === 'file' && state) { + if (controlType === 'file' && state) { return ( (function RenderField({ ); } - if (controltype === 'link') { + if (controlType === 'link') { return ( @@ -133,7 +114,7 @@ export const RenderField = observer(function RenderField({ ); } - if (controltype === 'checkbox') { + if (controlType === 'checkbox') { if (state !== undefined) { return ( (function RenderField({ ); } - if (controltype === 'combobox') { + if (controlType === 'combobox') { if (state !== undefined) { return ( (function RenderField({ ); } - if (controltype === 'textarea') { + if (controlType === 'textarea') { if (state !== undefined) { return (