Skip to content

Commit

Permalink
Renaming a namespace appears to leave incorrect URLs.
Browse files Browse the repository at this point in the history
server: move files to new namespace in background job
webui: show confirmation dialog, instead of loading new namespace
  • Loading branch information
amvanbaren committed Mar 21, 2023
1 parent 2563617 commit 8f1b167
Show file tree
Hide file tree
Showing 20 changed files with 535 additions and 171 deletions.
2 changes: 1 addition & 1 deletion server/src/main/java/org/eclipse/openvsx/AdminAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ public ResponseEntity<ResultJson> changeNamespace(@RequestBody ChangeNamespaceJs
try {
admins.checkAdminUser();
admins.changeNamespace(json);
return ResponseEntity.ok(ResultJson.success("Changed namespace " + json.oldNamespace + " to " + json.newNamespace));
return ResponseEntity.ok(ResultJson.success("Scheduled namespace change from '" + json.oldNamespace + "' to '" + json.newNamespace + "'.\nIt can take 15 minutes to a couple hours for the change to become visible."));
} catch (ErrorResultException exc) {
return exc.toResponseEntity();
}
Expand Down
66 changes: 22 additions & 44 deletions server/src/main/java/org/eclipse/openvsx/AdminService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,7 @@
********************************************************************************/
package org.eclipse.openvsx;

import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;
import javax.transaction.Transactional;

import com.google.common.base.Strings;

import org.eclipse.openvsx.cache.CacheService;
import org.eclipse.openvsx.eclipse.EclipseService;
import org.eclipse.openvsx.entities.*;
Expand All @@ -31,10 +21,20 @@
import org.eclipse.openvsx.util.TimeUtil;
import org.eclipse.openvsx.util.UrlUtil;
import org.eclipse.openvsx.util.VersionService;
import org.jobrunr.scheduling.JobRequestScheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.persistence.EntityManager;
import javax.transaction.Transactional;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.stream.Collectors;

import static org.eclipse.openvsx.entities.FileResource.*;

@Component
Expand Down Expand Up @@ -70,6 +70,9 @@ public class AdminService {
@Autowired
CacheService cache;

@Autowired
JobRequestScheduler scheduler;

@Transactional(rollbackOn = ErrorResultException.class)
public ResultJson deleteExtension(String namespaceName, String extensionName, UserData admin)
throws ErrorResultException {
Expand Down Expand Up @@ -183,7 +186,11 @@ public ResultJson editNamespaceMember(String namespaceName, String userName, Str

@Transactional(rollbackOn = ErrorResultException.class)
public ResultJson createNamespace(NamespaceJson json) {
validateNamespace(json.name);
var namespaceIssue = validator.validateNamespace(json.name);
if (namespaceIssue.isPresent()) {
throw new ErrorResultException(namespaceIssue.get().toString());
}

var namespace = repositories.findNamespace(json.name);
if (namespace != null) {
throw new ErrorResultException("Namespace already exists: " + namespace.getName());
Expand All @@ -196,10 +203,10 @@ public ResultJson createNamespace(NamespaceJson json) {

@Transactional
public void changeNamespace(ChangeNamespaceJson json) {
if(Strings.isNullOrEmpty(json.oldNamespace)) {
if (Strings.isNullOrEmpty(json.oldNamespace)) {
throw new ErrorResultException("Old namespace must have a value");
}
if(Strings.isNullOrEmpty(json.newNamespace)) {
if (Strings.isNullOrEmpty(json.newNamespace)) {
throw new ErrorResultException("New namespace must have a value");
}

Expand All @@ -209,40 +216,11 @@ public void changeNamespace(ChangeNamespaceJson json) {
}

var newNamespace = repositories.findNamespace(json.newNamespace);
if(newNamespace != null && !json.mergeIfNewNamespaceAlreadyExists) {
if (newNamespace != null && !json.mergeIfNewNamespaceAlreadyExists) {
throw new ErrorResultException("New namespace already exists: " + json.newNamespace);
}
if(newNamespace == null) {
validateNamespace(json.newNamespace);
newNamespace = new Namespace();
newNamespace.setName(json.newNamespace);
entityManager.persist(newNamespace);
}

var extensions = repositories.findExtensions(oldNamespace);
for(var extension : extensions) {
cache.evictExtensionJsons(extension);
cache.evictLatestExtensionVersion(extension);
extension.setNamespace(newNamespace);
}

var memberships = repositories.findMemberships(oldNamespace);
for(var membership : memberships) {
membership.setNamespace(newNamespace);
}

if(json.removeOldNamespace) {
entityManager.remove(oldNamespace);
}

search.updateSearchEntries(extensions.toList());
}

private void validateNamespace(String namespace) {
var namespaceIssue = validator.validateNamespace(namespace);
if (namespaceIssue.isPresent()) {
throw new ErrorResultException(namespaceIssue.get().toString());
}
scheduler.enqueue(new ChangeNamespaceJobRequest(json));
}

public UserPublishInfoJson getUserPublishInfo(String provider, String loginName) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/** ******************************************************************************
* Copyright (c) 2023 Precies. Software Ltd and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
* ****************************************************************************** */
package org.eclipse.openvsx;

import org.eclipse.openvsx.json.ChangeNamespaceJson;
import org.jobrunr.jobs.lambdas.JobRequest;
import org.jobrunr.jobs.lambdas.JobRequestHandler;

import java.util.Objects;

public class ChangeNamespaceJobRequest implements JobRequest {

private ChangeNamespaceJson data;

public ChangeNamespaceJobRequest() {}

public ChangeNamespaceJobRequest(ChangeNamespaceJson data) {
this.data = data;
}

@Override
public Class<? extends JobRequestHandler> getJobRequestHandler() {
return ChangeNamespaceJobRequestHandler.class;
}

public ChangeNamespaceJson getData() {
return data;
}

public void setData(ChangeNamespaceJson data) {
this.data = data;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ChangeNamespaceJobRequest that = (ChangeNamespaceJobRequest) o;
return Objects.equals(data, that.data);
}

@Override
public int hashCode() {
return Objects.hash(data);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/** ******************************************************************************
* Copyright (c) 2023 Precies. Software Ltd and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* SPDX-License-Identifier: EPL-2.0
* ****************************************************************************** */
package org.eclipse.openvsx;

import org.eclipse.openvsx.entities.Extension;
import org.eclipse.openvsx.entities.ExtensionVersion;
import org.eclipse.openvsx.entities.FileResource;
import org.eclipse.openvsx.entities.Namespace;
import org.eclipse.openvsx.repositories.RepositoryService;
import org.eclipse.openvsx.storage.StorageUtilService;
import org.eclipse.openvsx.util.ErrorResultException;
import org.jobrunr.jobs.lambdas.JobRequestHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.util.Pair;
import org.springframework.data.util.Streamable;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.stream.Collectors;

@Component
public class ChangeNamespaceJobRequestHandler implements JobRequestHandler<ChangeNamespaceJobRequest> {

private static final Logger LOGGER = LoggerFactory.getLogger(ChangeNamespaceJobRequestHandler.class);

private static final Map<String, Object> LOCKS;

static {
var MAX_SIZE = 100;
LOCKS = Collections.synchronizedMap(new LinkedHashMap<>(MAX_SIZE) {
protected boolean removeEldestEntry(Map.Entry eldest){
return size() > MAX_SIZE;
}
});
}

@Autowired
ExtensionValidator validator;

@Autowired
RepositoryService repositories;

@Autowired
StorageUtilService storageUtil;

@Autowired
ChangeNamespaceService service;

@Override
public void run(ChangeNamespaceJobRequest jobRequest) throws Exception {
var oldNamespace = jobRequest.getData().oldNamespace;
synchronized (LOCKS.computeIfAbsent(oldNamespace, key -> new Object())) {
execute(jobRequest);
}
}

private void execute(ChangeNamespaceJobRequest jobRequest) {
var json = jobRequest.getData();
LOGGER.info(">> Change namespace from {} to {}", json.oldNamespace, json.newNamespace);
var oldNamespace = repositories.findNamespace(json.oldNamespace);
if(oldNamespace == null) {
return;
}

var oldResources = repositories.findFileResources(oldNamespace);
var newNamespaceOptional = Optional.ofNullable(repositories.findNamespace(json.newNamespace));
var createNewNamespace = newNamespaceOptional.isEmpty();
var newNamespace = newNamespaceOptional.orElseGet(() -> {
validateNamespace(json.newNamespace);
var namespace = new Namespace();
namespace.setName(json.newNamespace);
return namespace;
});

var copyResources = oldResources.stream()
.findFirst()
.map(storageUtil::shouldStoreExternally)
.orElse(false);

List<Pair<FileResource, FileResource>> pairs = null;
List<FileResource> updatedResources;
if(copyResources) {
pairs = copyResources(oldResources, newNamespace);
storageUtil.copyFiles(pairs);
updatedResources = pairs.stream()
.filter(pair -> pair.getFirst().getType().equals(FileResource.DOWNLOAD))
.map(pair -> {
var oldResource = pair.getFirst();
var newResource = pair.getSecond();
oldResource.setName(newResource.getName());
return oldResource;
})
.collect(Collectors.toList());
} else {
updatedResources = oldResources
.filter(resource -> resource.getType().equals(FileResource.DOWNLOAD))
.map(resource -> {
resource.setName(newResourceName(newNamespace, resource));
return resource;
})
.toList();
}

service.changeNamespaceInDatabase(newNamespace, oldNamespace, updatedResources, createNewNamespace, json.removeOldNamespace);
if(copyResources) {
// remove the old resources from external storage
pairs.stream()
.map(Pair::getFirst)
.forEach(storageUtil::removeFile);
}
LOGGER.info("<< Changed namespace from {} to {}", json.oldNamespace, json.newNamespace);
}

private void validateNamespace(String namespace) {
var namespaceIssue = validator.validateNamespace(namespace);
if (namespaceIssue.isPresent()) {
throw new ErrorResultException(namespaceIssue.get().toString());
}
}

private List<Pair<FileResource, FileResource>> copyResources(Streamable<FileResource> resources, Namespace newNamespace) {
var extVersions = resources.stream()
.map(FileResource::getExtension)
.collect(Collectors.toMap(ExtensionVersion::getId, ev -> ev, (ev1, ev2) -> ev1));

var extensions = extVersions.values().stream()
.map(ExtensionVersion::getExtension)
.collect(Collectors.groupingBy(Extension::getId))
.entrySet().stream()
.map(entry -> {
var extension = entry.getValue().get(0);
var newExtension = new Extension();
newExtension.setId(extension.getId());
newExtension.setName(extension.getName());
newExtension.setNamespace(newNamespace);
return new AbstractMap.SimpleEntry<>(entry.getKey(), newExtension);
})
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

for(var key : extVersions.keySet()) {
var extVersion = extVersions.get(key);
var newExtVersion = new ExtensionVersion();
newExtVersion.setId(extVersion.getId());
newExtVersion.setExtension(extensions.get(extVersion.getExtension().getId()));
newExtVersion.setVersion(extVersion.getVersion());
newExtVersion.setTargetPlatform(extVersion.getTargetPlatform());
extVersions.put(key, newExtVersion);
}

return resources.stream()
.map(resource -> {
var newExtVersion = extVersions.get(resource.getExtension().getId());
var newResource = new FileResource();
newResource.setId(resource.getId());
newResource.setExtension(newExtVersion);
newResource.setType(resource.getType());
newResource.setStorageType(resource.getStorageType());
var newResourceName = resource.getType().equals(FileResource.DOWNLOAD)
? newResourceName(newNamespace, resource)
: resource.getName();

newResource.setName(newResourceName);
return Pair.of(resource, newResource);
})
.collect(Collectors.toList());
}

private String newResourceName(Namespace newNamespace, FileResource resource) {
var extVersion = resource.getExtension();
var extension = extVersion.getExtension();

var newExtension = new Extension();
newExtension.setNamespace(newNamespace);
newExtension.setName(extension.getName());

var newExtVersion = new ExtensionVersion();
newExtVersion.setVersion(extVersion.getVersion());
newExtVersion.setTargetPlatform(extVersion.getTargetPlatform());
newExtVersion.setExtension(newExtension);
try(var processor = new ExtensionProcessor(null)) {
var newResourceName = processor.getBinaryName(newExtVersion);
LOGGER.info("newResourceName: {}", newResourceName);
return newResourceName;
}
}
}
Loading

0 comments on commit 8f1b167

Please sign in to comment.