diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java index 4b0ff25edb..b83cbf6330 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java @@ -27,8 +27,11 @@ import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.registry.BaseProjectImpl; import org.jkiss.dbeaver.registry.DataSourceRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.Pair; import java.nio.file.Path; +import java.util.Collection; public class WebProjectImpl extends BaseProjectImpl { @@ -113,4 +116,24 @@ public class WebProjectImpl extends BaseProjectImpl { protected DBPDataSourceRegistry createDataSourceRegistry() { return new WebDataSourceRegistryProxy(new DataSourceRegistry(this), dataSourceFilter); } + + /** + * Method for Bulk Update of resources properties paths + * + * @param oldToNewPaths collection of OldPath to NewPath pairs + */ + public void moveResourcePropertiesBatch(@NotNull Collection> oldToNewPaths) { + loadMetadata(); + synchronized (metadataSync) { + for (var pathsPair : oldToNewPaths) { + final var oldResourcePath = CommonUtils.normalizeResourcePath(pathsPair.getFirst()); + final var newResourcePath = CommonUtils.normalizeResourcePath(pathsPair.getSecond()); + final var resProps = resourceProperties.remove(oldResourcePath); + if (resProps != null) { + resourceProperties.put(newResourcePath, resProps); + } + } + } + flushMetadata(); + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index 140b5955b8..fe26bfdaf5 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -81,6 +81,11 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements return true; } + @Override + public boolean isMultiuser() { + return true; + } + @Nullable protected Path loadServerConfiguration() throws DBException { String configPath = DEFAULT_CONFIG_FILE_PATH; 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 fb1edec745..b3854a8ad0 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 @@ -22,6 +22,7 @@ import io.cloudbeaver.model.rm.RMUtils; import io.cloudbeaver.service.security.SMUtils; import io.cloudbeaver.service.sql.WebSQLConstants; import io.cloudbeaver.utils.WebAppUtils; +import io.cloudbeaver.utils.file.UniversalFileVisitor; import org.eclipse.core.runtime.IPath; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; @@ -46,6 +47,7 @@ import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IOUtils; +import org.jkiss.utils.Pair; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -472,26 +474,63 @@ public class LocalResourceController implements RMController { @NotNull String oldResourcePath, @NotNull String newResourcePath ) throws DBException { - Path oldTargetPath = getTargetPath(projectId, oldResourcePath); + var normalizedOldResourcePath = CommonUtils.normalizeResourcePath(oldResourcePath); + var normalizedNewResourcePath = CommonUtils.normalizeResourcePath(newResourcePath); + if (log.isDebugEnabled()) { + log.debug("Moving resource from '" + normalizedOldResourcePath + "' to '" + normalizedNewResourcePath + "'"); + } + Path oldTargetPath = getTargetPath(projectId, normalizedOldResourcePath); List rmOldResourcePath = makeResourcePath(projectId, oldTargetPath, false); if (!Files.exists(oldTargetPath)) { throw new DBException("Resource '" + oldTargetPath + "' doesn't exists"); } - Path newTargetPath = getTargetPath(projectId, newResourcePath); + Path newTargetPath = getTargetPath(projectId, normalizedNewResourcePath); validateResourcePath(newTargetPath.toString()); try { Files.move(oldTargetPath, newTargetPath); } catch (IOException e) { - throw new DBException("Error moving resource '" + oldResourcePath + "'", e); + throw new DBException("Error moving resource '" + normalizedOldResourcePath + "'", e); + } + + log.debug("Moving resource properties"); + try { + movePropertiesRecursive(projectId, newTargetPath, normalizedOldResourcePath, normalizedNewResourcePath); + } catch (IOException | DBException e) { + throw new DBException("Unable to move resource properties", e); } - // Move properties - getProjectMetadata(projectId, false).moveResourceProperties(oldResourcePath, newResourcePath); fireRmResourceDeleteEvent(projectId, rmOldResourcePath); - fireRmResourceAddEvent(projectId, newResourcePath); + fireRmResourceAddEvent(projectId, normalizedNewResourcePath); return DEFAULT_CHANGE_ID; } + /** + * Iterates the tree starting at {@code rootResourcePath}. + * Calculates for each file/folder {@code newResourcePropertiesPath} and restores {@code oldResourcePropertiesPath} + * by replacing the first {@code newRootPropertiesPath} with {@code oldRootPropertiesPath} in {@code newResourcePropertiesPath}. + * Gathers the old-new properties paths pairs and updates properties via BaseProjectImpl#moveResourcePropertiesBatch() + */ + private void movePropertiesRecursive( + @NotNull String projectId, + @NotNull Path rootResourcePath, + @NotNull String oldRootPropertiesPath, + @NotNull String newRootPropertiesPath + ) throws IOException, DBException { + var project = getProjectMetadata(projectId, false); + var projectPath = getProjectPath(projectId); + var propertiesPathsList = new ArrayList>(); + Files.walkFileTree(rootResourcePath, (UniversalFileVisitor) (path, attrs) -> { + var newResourcePropertiesPath = CommonUtils.normalizeResourcePath(projectPath.relativize(path.toAbsolutePath()).toString()); + var oldResourcePropertiesPath = newResourcePropertiesPath.replaceFirst(newRootPropertiesPath, oldRootPropertiesPath); + propertiesPathsList.add(new Pair<>(oldResourcePropertiesPath, newResourcePropertiesPath)); + return FileVisitResult.CONTINUE; + }); + if (log.isDebugEnabled()) { + log.debug("Move resources properties:\n" + propertiesPathsList); + } + project.moveResourcePropertiesBatch(propertiesPathsList); + } + @Override public void deleteResource(@NotNull String projectId, @NotNull String resourcePath, boolean recursive) throws DBException { validateResourcePath(resourcePath); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/file/UniversalFileVisitor.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/file/UniversalFileVisitor.java new file mode 100644 index 0000000000..5a9b6dbd7b --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/file/UniversalFileVisitor.java @@ -0,0 +1,60 @@ +/* + * 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.utils.file; + +import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +/** + * A simple file visitor with default behavior to visit all files and folders + * and perform the same operation on them and to re-throw I/O errors. + */ +@FunctionalInterface +public interface UniversalFileVisitor extends FileVisitor { + + /** + * Method that applies to both files and folders starting with {@code dirOrFile} + */ + FileVisitResult dirOrFileOperation(T dirOrFile, BasicFileAttributes attrs) throws IOException; + + @Override + default FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException { + dirOrFileOperation(dir, attrs); + return FileVisitResult.CONTINUE; + } + + @Override + default FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException { + dirOrFileOperation(file, attrs); + return FileVisitResult.CONTINUE; + } + + @Override + default FileVisitResult visitFileFailed(T file, IOException exc) throws IOException { + throw exc; + } + + @Override + default FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException { + if (exc != null) { + throw exc; + } + return FileVisitResult.CONTINUE; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index 562b084221..a830c713f3 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -234,11 +234,6 @@ public class CBApplication extends BaseWebApplication implements WebAuthApplicat return true; } - @Override - public boolean isMultiuser() { - return true; - } - @Override protected void startServer() { CBPlatform.setApplication(this);