Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public class LocalResourceController extends BaseLocalResourceController {
protected final List<RMFileOperationHandler> fileHandlers;

private final Map<String, RMLocalProject> projectRegistries = new LinkedHashMap<>();
private final ProjectsMetadataInfo sharedProjectsMetadataInfo;

public LocalResourceController(
@NotNull DBPWorkspace workspace,
Expand All @@ -100,6 +101,7 @@ public LocalResourceController(

this.globalProjectName = DBWorkbench.getPlatform().getApplication().getDefaultProjectName();
this.fileHandlers = RMFileOperationHandlersRegistry.getInstance().getFileHandlers();
this.sharedProjectsMetadataInfo = new ProjectsMetadataInfo(sharedProjectsPath);
}

@NotNull
Expand All @@ -124,6 +126,9 @@ protected RMLocalProject getWebProject(@NotNull String projectId, boolean refres
RMLocalProject project = projectRegistries.get(projectId);
if (project == null || refresh) {
project = createWebProjectImpl(projectId, new SessionContextImpl(null));
if (project.getProjectType() == RMProjectType.SHARED) {
project.setProjectInfo(sharedProjectsMetadataInfo.getProjectInfo(projectId));
}
projectRegistries.put(projectId, project);
}
return project;
Expand Down Expand Up @@ -316,6 +321,7 @@ public RMProject updateProject(@NotNull String projectId, @NotNull RMProjectInfo
throw new DBException("Project '" + projectId + "' is not shared");
}
project.updateProject(projectInfo.getName(), projectInfo.getDescription());
sharedProjectsMetadataInfo.updateProjectInfo(projectId, projectInfo);
return WebRMUtils.createRmProjectFromWebProject(project);
}
}
Expand Down Expand Up @@ -346,6 +352,7 @@ public void deleteProject(@NotNull String projectId) throws DBException {
synchronized (projectRegistries) {
projectRegistries.remove(projectId);
}
sharedProjectsMetadataInfo.updateProjectInfo(projectId, null);
} catch (IOException e) {
throw new DBException("Error deleting project '" + projectId + "'", e);
}
Expand Down Expand Up @@ -1007,6 +1014,9 @@ private RMProject makeProjectFromPath(Path path, Set<RMProjectPermission> permis
.toArray(String[]::new);

RMLocalProject webProject = new RMLocalProject(workspace, new SessionContextImpl(null), path, type);
if (type == RMProjectType.SHARED) {
webProject.setProjectInfo(sharedProjectsMetadataInfo.getProjectInfo(webProject.getId()));
}
return createRmProjectFromWebProject(path, webProject, allProjectPermissions);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2025 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.local;

import com.google.gson.reflect.TypeToken;
import io.cloudbeaver.utils.ServletAppUtils;
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.app.DBPProject;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.jkiss.dbeaver.model.fs.lock.FileLockController;
import org.jkiss.dbeaver.model.impl.app.BaseProjectImpl;
import org.jkiss.dbeaver.model.rm.RMProjectInfo;
import org.jkiss.dbeaver.model.rm.RMProjectType;
import org.jkiss.dbeaver.model.rm.RMUtils;
import org.jkiss.utils.CommonUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Stream;

public class ProjectsMetadataInfo {
private static final Log log = Log.getLog(ProjectsMetadataInfo.class);
private static final String PROJECTS_INFO_FILE_NAME = "projects-info.json";

private final Map<String, RMProjectInfo> projectsInfo = new LinkedHashMap<>();
private final Path projectsPath;
private final FileLockController lockController;


public ProjectsMetadataInfo(@NotNull Path projectsPath) throws DBException {
this.projectsPath = projectsPath;
this.lockController = new FileLockController(ServletAppUtils.getServletApplication().getApplicationInstanceId());
readProjectInfos(projectsPath);
}

public RMProjectInfo getProjectInfo(@NotNull String projectId) {
return projectsInfo.computeIfAbsent(projectId, key -> new RMProjectInfo());
}

public void updateProjectInfo(@NotNull String projectId, @Nullable RMProjectInfo projectInfo) {
if (projectInfo != null) {
projectsInfo.put(projectId, projectInfo);
} else {
projectsInfo.remove(projectId);
}
saveProjectsInfo();
}

private void readProjectInfos(@NotNull Path path) {
if (!Files.exists(path)) {
return;
}
Path projectInfoFile = path.resolve(PROJECTS_INFO_FILE_NAME);
if (!Files.exists(projectInfoFile)) {
loadAndMigrateProjectDataToCommonFile(path);
saveProjectsInfo();
return;
}
readProjectInfosFromFile(projectInfoFile);
}

private void readProjectInfosFromFile(@NotNull Path projectInfoFile) {
try {
log.info("Reading project metadata information");
String content = Files.readString(projectInfoFile, StandardCharsets.UTF_8);
Map<String, RMProjectInfo> loaded = JSONUtils.GSON.fromJson(
content,
TypeToken.getParameterized(Map.class, String.class, RMProjectInfo.class).getType()
);
projectsInfo.clear();
if (loaded != null) {
projectsInfo.putAll(loaded);
}
} catch (IOException e) {
log.error("Error reading existing " + PROJECTS_INFO_FILE_NAME, e);
}
}

private void loadAndMigrateProjectDataToCommonFile(@NotNull Path path) {
log.info("Migrating shared project information to the common place");
Map<String, RMProjectInfo> infos = new LinkedHashMap<>();
try (Stream<Path> stream = Files.list(path)) {
stream.filter(Files::isDirectory).forEach(projectDir -> {
RMProjectInfo info = getProjectInfoFromProjectSettings(projectDir);
String projectId = RMUtils.makeProjectIdFromPath(projectDir, RMProjectType.SHARED);
infos.put(projectId, info);
});
} catch (IOException e) {
log.error("Error listing shared projects path", e);
}
log.info("Migration for project information completed");
projectsInfo.clear();
projectsInfo.putAll(infos);
}

@NotNull
private RMProjectInfo getProjectInfoFromProjectSettings(@NotNull Path projectPath) {
String name = null;
String description = null;

Path settings = projectPath.resolve(DBPProject.METADATA_FOLDER).resolve(BaseProjectImpl.SETTINGS_STORAGE_FILE);
if (Files.exists(settings) && Files.isRegularFile(settings)) {
try {
String json = Files.readString(settings, StandardCharsets.UTF_8);
Map<String, Object> map = JSONUtils.GSON.fromJson(json, JSONUtils.MAP_TYPE_TOKEN);
name = JSONUtils.getString(map, BaseProjectImpl.PROP_PROJECT_NAME);
description = JSONUtils.getString(map, BaseProjectImpl.PROP_PROJECT_DESCRIPTION);
} catch (IOException e) {
log.warn("Failed to read project settings for " + projectPath + ": " + e.getMessage());
} catch (Exception e) {
log.warn("Failed to parse project settings for " + projectPath + ": " + e.getMessage());
}
}
RMProjectInfo info = new RMProjectInfo();
info.setName(CommonUtils.isEmpty(name) ? projectPath.getFileName().toString() : name);
info.setDescription(description);
return info;
}

private void saveProjectsInfo() {
try (var lock = lockController.lock(PROJECTS_INFO_FILE_NAME, "saveProjectsInfo")) {
log.info("Saving project information");
Files.writeString(projectsPath.resolve(PROJECTS_INFO_FILE_NAME), JSONUtils.GSON.toJson(projectsInfo));
} catch (IOException e) {
log.error("Error writing " + PROJECTS_INFO_FILE_NAME, e);
} catch (DBException e) {
log.error("Error locking file " + PROJECTS_INFO_FILE_NAME + ": " + e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@

import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.app.DBPWorkspace;
import org.jkiss.dbeaver.model.auth.SMSessionContext;
import org.jkiss.dbeaver.model.rm.RMProjectInfo;
import org.jkiss.dbeaver.model.rm.RMProjectType;
import org.jkiss.dbeaver.model.rm.RMUtils;
import org.jkiss.dbeaver.registry.project.LocalProjectImpl;
Expand All @@ -29,6 +31,8 @@
public class RMLocalProject extends LocalProjectImpl {
@NotNull
private final RMProjectType projectType;
@Nullable
private RMProjectInfo projectInfo;

public RMLocalProject(
@NotNull DBPWorkspace workspace,
Expand All @@ -51,6 +55,39 @@ public String getId() {
return RMUtils.makeProjectIdFromPath(projectPath, projectType);
}

@Nullable
@Override
public String getDescription() {
if (projectInfo == null) {
return null;
}
return projectInfo.getDescription();
}

@NotNull
@Override
public String getName() {
if (projectInfo == null || projectInfo.getName() == null) {
return projectPath.getFileName().toString();
}
return projectInfo.getName();
}

@NotNull
public RMProjectType getProjectType() {
return projectType;
}

@Override
public void updateProject(@Nullable String newName, @Nullable String description) throws DBException {
this.projectInfo = new RMProjectInfo();
projectInfo.setName(newName);
projectInfo.setDescription(description);
}

public void setProjectInfo(@NotNull RMProjectInfo projectInfo) {
this.projectInfo = projectInfo;
}

public boolean canUpdateProjectName() {
return RMProjectType.SHARED.equals(projectType);
Expand Down
Loading