mirror of
https://github.com/scm-manager/scm-manager.git
synced 2026-02-27 17:00:50 +01:00
Add functionality to modify repository storage locations
The repository location resolver gets a new function that allows to change the location of a repository. Pushed-by: Rene Pfeuffer<rene.pfeuffer@cloudogu.com> Co-authored-by: René Pfeuffer<rene.pfeuffer@cloudogu.com> Committed-by: René Pfeuffer<rene.pfeuffer@cloudogu.com>
This commit is contained in:
2
gradle/changelog/modify_repository_location.yaml
Normal file
2
gradle/changelog/modify_repository_location.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: Internal API to modify repository storage locations
|
||||
@@ -94,6 +94,8 @@ ext {
|
||||
guava: 'com.google.guava:guava:32.0.1-jre',
|
||||
commonsLang: 'commons-lang:commons-lang:2.6',
|
||||
commonsCompress: 'org.apache.commons:commons-compress:1.23.0',
|
||||
commonsIo: 'commons-io:commons-io:2.13.0',
|
||||
commonsLang3: 'org.apache.commons:commons-lang3:3.13.0',
|
||||
|
||||
// security
|
||||
shiroCore: "org.apache.shiro:shiro-core:${shiroVersion}",
|
||||
|
||||
@@ -21,13 +21,15 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
@@ -49,6 +51,13 @@ public class InitialRepositoryLocationResolver {
|
||||
|
||||
private static final CharMatcher ID_MATCHER = CharMatcher.anyOf("/\\.");
|
||||
|
||||
private final Set<RepositoryLocationOverride> repositoryLocationOverrides;
|
||||
|
||||
@Inject
|
||||
public InitialRepositoryLocationResolver(Set<RepositoryLocationOverride> repositoryLocationOverrides) {
|
||||
this.repositoryLocationOverrides = repositoryLocationOverrides;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the initial path to repository.
|
||||
*
|
||||
@@ -63,4 +72,11 @@ public class InitialRepositoryLocationResolver {
|
||||
return Paths.get(DEFAULT_REPOSITORY_PATH, repositoryId);
|
||||
}
|
||||
|
||||
public Path getPath(Repository repository) {
|
||||
Path path = getPath(repository.getId());
|
||||
for (RepositoryLocationOverride o : repositoryLocationOverrides) {
|
||||
path = o.overrideLocation(repository, path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.plugin.ExtensionPoint;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@ExtensionPoint
|
||||
public interface RepositoryLocationOverride {
|
||||
Path overrideLocation(Repository repository, Path defaultPath);
|
||||
}
|
||||
@@ -24,6 +24,10 @@
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import sonia.scm.ContextEntry;
|
||||
import sonia.scm.ExceptionWithContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public abstract class RepositoryLocationResolver {
|
||||
@@ -62,6 +66,28 @@ public abstract class RepositoryLocationResolver {
|
||||
*/
|
||||
void setLocation(String repositoryId, T location);
|
||||
|
||||
/**
|
||||
* Modifies the location for an existing repository.
|
||||
* @param repositoryId The id of the new repository.
|
||||
* @throws IllegalArgumentException if the new location equals the current location.
|
||||
* @throws UnsupportedOperationException if the backing persistence layer does not support modification.
|
||||
* @throws RepositoryStorageException if any occurs during the move.
|
||||
*/
|
||||
default void modifyLocation(String repositoryId, T location) throws RepositoryStorageException {
|
||||
throw new UnsupportedOperationException("location modification not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the location for an existing repository without removing the original location.
|
||||
* @param repositoryId The id of the repository.
|
||||
* @throws IllegalArgumentException if the new location equals the current location.
|
||||
* @throws UnsupportedOperationException if the backing persistence layer does not support modification.
|
||||
* @throws RepositoryStorageException if any occurs during the move.
|
||||
*/
|
||||
default void modifyLocationAndKeepOld(String repositoryId, T location) throws RepositoryStorageException {
|
||||
throw new UnsupportedOperationException("location modification not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates all repository locations known to this resolver instance and calls the consumer giving the repository id
|
||||
* and its location for each repository.
|
||||
@@ -70,9 +96,27 @@ public abstract class RepositoryLocationResolver {
|
||||
void forAllLocations(BiConsumer<String, T> consumer);
|
||||
}
|
||||
|
||||
public class LocationNotFoundException extends IllegalStateException {
|
||||
public static class LocationNotFoundException extends IllegalStateException {
|
||||
public LocationNotFoundException(String repositoryId) {
|
||||
super("location for repository " + repositoryId + " does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
public static class RepositoryStorageException extends RuntimeException {
|
||||
public RepositoryStorageException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public RepositoryStorageException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public String getRootMessage() {
|
||||
if (getCause() == null) {
|
||||
return this.getMessage();
|
||||
} else {
|
||||
return getCause().getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.update;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
@@ -31,5 +31,11 @@ import sonia.scm.repository.Repository;
|
||||
* {@link sonia.scm.repository.RepositoryLocationResolver}.
|
||||
*/
|
||||
public interface UpdateStepRepositoryMetadataAccess<T> {
|
||||
/**
|
||||
* Reads the repository from the given location.
|
||||
* @param location the location to read from
|
||||
* @return the repository
|
||||
* @throws sonia.scm.repository.InternalRepositoryException if the repository could not be read
|
||||
*/
|
||||
Repository read(T location);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.repository;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
@@ -32,19 +32,22 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static java.util.Collections.singleton;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith({MockitoExtension.class})
|
||||
class InitialRepositoryLocationResolverTest {
|
||||
|
||||
private InitialRepositoryLocationResolver resolver = new InitialRepositoryLocationResolver();
|
||||
private final InitialRepositoryLocationResolver resolver = new InitialRepositoryLocationResolver(emptySet());
|
||||
|
||||
@Test
|
||||
void shouldComputeInitialPath() {
|
||||
Path path = resolver.getPath("42");
|
||||
|
||||
assertThat(path).isRelative();
|
||||
assertThat(path.toString()).isEqualTo("repositories" + File.separator + "42");
|
||||
assertThat(path)
|
||||
.isRelative()
|
||||
.hasToString("repositories" + File.separator + "42");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,4 +69,16 @@ class InitialRepositoryLocationResolverTest {
|
||||
void shouldThrowIllegalArgumentExceptionIfIdIsDot() {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> resolver.getPath("."));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseOverrideForRepository() {
|
||||
InitialRepositoryLocationResolver resolver = new InitialRepositoryLocationResolver(
|
||||
singleton((repository, defaultPath) -> defaultPath.resolve(repository.getId()))
|
||||
);
|
||||
Path path = resolver.getPath(new Repository("42", "git", "space", "X"));
|
||||
|
||||
assertThat(path)
|
||||
.isRelative()
|
||||
.hasToString("repositories" + File.separator + "42" + File.separator + "42");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libraries.commonsIo
|
||||
implementation libraries.commonsLang3
|
||||
|
||||
api platform(project(':'))
|
||||
|
||||
api project(':scm-core')
|
||||
|
||||
@@ -54,12 +54,13 @@ public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repository read(Path path) {
|
||||
LOG.trace("read repository metadata from {}", path);
|
||||
return compute(() -> {
|
||||
try {
|
||||
return (Repository) jaxbContext.createUnmarshaller().unmarshal(resolveDataPath(path).toFile());
|
||||
} catch (JAXBException ex) {
|
||||
} catch (JAXBException | IllegalArgumentException ex) {
|
||||
throw new InternalRepositoryException(
|
||||
ContextEntry.ContextBuilder.entity(Path.class, path.toString()).build(), "failed read repository metadata", ex
|
||||
);
|
||||
@@ -67,6 +68,9 @@ public class MetadataStore implements UpdateStepRepositoryMetadataAccess<Path> {
|
||||
}).withLockedFileForRead(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the repository metadata to the given path.
|
||||
*/
|
||||
void write(Path path, Repository repository) {
|
||||
LOG.trace("write repository metadata of {} to {}", repository, path);
|
||||
try {
|
||||
|
||||
@@ -24,23 +24,27 @@
|
||||
|
||||
package sonia.scm.repository.xml;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.event.EventListenerSupport;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.BasicRepositoryLocationResolver;
|
||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.store.StoreConstants;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||
|
||||
/**
|
||||
* A Location Resolver for File based Repository Storage.
|
||||
* <p>
|
||||
@@ -69,6 +73,8 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
private long creationTime;
|
||||
private long lastModified;
|
||||
|
||||
private EventListenerSupport<MaintenanceCallback> maintenanceCallbacks = EventListenerSupport.create(MaintenanceCallback.class);
|
||||
|
||||
@Inject
|
||||
public PathBasedRepositoryLocationResolver(SCMContextProvider contextProvider, InitialRepositoryLocationResolver initialRepositoryLocationResolver, FileSystem fileSystem) {
|
||||
this(contextProvider, initialRepositoryLocationResolver, fileSystem, Clock.systemUTC());
|
||||
@@ -90,50 +96,92 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected <T> RepositoryLocationResolverInstance<T> create(Class<T> type) {
|
||||
return new RepositoryLocationResolverInstance<T>() {
|
||||
@Override
|
||||
public T getLocation(String repositoryId) {
|
||||
if (pathById.containsKey(repositoryId)) {
|
||||
return (T) contextProvider.resolve(pathById.get(repositoryId));
|
||||
} else {
|
||||
throw new LocationNotFoundException(repositoryId);
|
||||
if (type.isAssignableFrom(Path.class)) {
|
||||
return (RepositoryLocationResolverInstance<T>) new RepositoryLocationResolverInstance<Path>() {
|
||||
@Override
|
||||
public Path getLocation(String repositoryId) {
|
||||
if (pathById.containsKey(repositoryId)) {
|
||||
return contextProvider.resolve(pathById.get(repositoryId));
|
||||
} else {
|
||||
throw new LocationNotFoundException(repositoryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T createLocation(String repositoryId) {
|
||||
if (pathById.containsKey(repositoryId)) {
|
||||
throw new IllegalStateException("location for repository " + repositoryId + " already exists");
|
||||
} else {
|
||||
return (T) create(repositoryId);
|
||||
@Override
|
||||
public Path createLocation(String repositoryId) {
|
||||
if (pathById.containsKey(repositoryId)) {
|
||||
throw new IllegalStateException("location for repository " + repositoryId + " already exists");
|
||||
} else {
|
||||
return create(repositoryId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setLocation(String repositoryId, T location) {
|
||||
if (pathById.containsKey(repositoryId)) {
|
||||
throw new IllegalStateException("location for repository " + repositoryId + " already exists");
|
||||
} else {
|
||||
PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, ((Path) location).toAbsolutePath());
|
||||
@Override
|
||||
public void setLocation(String repositoryId, Path location) {
|
||||
if (pathById.containsKey(repositoryId)) {
|
||||
throw new IllegalStateException("location for repository " + repositoryId + " already exists");
|
||||
} else {
|
||||
PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, location.toAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forAllLocations(BiConsumer<String, T> consumer) {
|
||||
pathById.forEach((id, path) -> consumer.accept(id, (T) contextProvider.resolve(path)));
|
||||
}
|
||||
};
|
||||
@Override
|
||||
public void modifyLocation(String repositoryId, Path newPath) throws RepositoryStorageException {
|
||||
modifyLocation(repositoryId, newPath, oldPath -> FileUtils.moveDirectory(contextProvider.resolve(oldPath).toFile(), newPath.toFile()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modifyLocationAndKeepOld(String repositoryId, Path newPath) throws RepositoryStorageException {
|
||||
modifyLocation(repositoryId, newPath, oldPath -> FileUtils.copyDirectory(contextProvider.resolve(oldPath).toFile(), newPath.toFile()));
|
||||
}
|
||||
|
||||
private void modifyLocation(String repositoryId, Path newPath, Modifier modifier) throws RepositoryStorageException {
|
||||
maintenanceCallbacks.fire().downForMaintenance(new DownForMaintenanceContext(repositoryId));
|
||||
Path oldPath = pathById.get(repositoryId);
|
||||
pathById.remove(repositoryId);
|
||||
try {
|
||||
modifier.modify(contextProvider.resolve(oldPath));
|
||||
} catch (Exception e) {
|
||||
PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, oldPath);
|
||||
maintenanceCallbacks.fire().upAfterMaintenance(new UpAfterMaintenanceContext(repositoryId, oldPath));
|
||||
throw new RepositoryStorageException("could not create repository at new path " + newPath, e);
|
||||
}
|
||||
PathBasedRepositoryLocationResolver.this.setLocation(repositoryId, newPath);
|
||||
maintenanceCallbacks.fire().upAfterMaintenance(new UpAfterMaintenanceContext(repositoryId, newPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forAllLocations(BiConsumer<String, Path> consumer) {
|
||||
pathById.forEach((id, path) -> consumer.accept(id, contextProvider.resolve(path)));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
throw new IllegalArgumentException("type not supported: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
Path create(Repository repository) {
|
||||
Path path = initialRepositoryLocationResolver.getPath(repository);
|
||||
return create(repository.getId(), path);
|
||||
}
|
||||
|
||||
Path create(String repositoryId) {
|
||||
Path path = initialRepositoryLocationResolver.getPath(repositoryId);
|
||||
return create(repositoryId, path);
|
||||
}
|
||||
|
||||
private Path create(String repositoryId, Path path) {
|
||||
if (Files.exists(path)) {
|
||||
throw new RepositoryStorageException("path " + path + " for repository " + repositoryId + " already exists");
|
||||
}
|
||||
setLocation(repositoryId, path);
|
||||
Path resolvedPath = contextProvider.resolve(path);
|
||||
try {
|
||||
fileSystem.create(resolvedPath.toFile());
|
||||
} catch (Exception e) {
|
||||
throw new InternalRepositoryException(entity("Repository", repositoryId), "could not create directory for new repository", e);
|
||||
throw new RepositoryStorageException("could not create directory " + path + " for new repository " + repositoryId, e);
|
||||
}
|
||||
return resolvedPath;
|
||||
}
|
||||
@@ -195,4 +243,40 @@ public class PathBasedRepositoryLocationResolver extends BasicRepositoryLocation
|
||||
public void refresh() {
|
||||
this.read();
|
||||
}
|
||||
|
||||
void registerMaintenanceCallback(MaintenanceCallback maintenanceCallback) {
|
||||
maintenanceCallbacks.addListener(maintenanceCallback);
|
||||
}
|
||||
|
||||
public interface MaintenanceCallback {
|
||||
default void downForMaintenance(DownForMaintenanceContext context) {}
|
||||
|
||||
default void upAfterMaintenance(UpAfterMaintenanceContext context) {}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
public static class DownForMaintenanceContext {
|
||||
private final String repositoryId;
|
||||
|
||||
DownForMaintenanceContext(String repositoryId) {
|
||||
this.repositoryId = repositoryId;
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@EqualsAndHashCode
|
||||
public static class UpAfterMaintenanceContext {
|
||||
private final String repositoryId;
|
||||
private final Path location;
|
||||
|
||||
UpAfterMaintenanceContext(String repositoryId, Path location) {
|
||||
this.repositoryId = repositoryId;
|
||||
this.location = location;
|
||||
}
|
||||
}
|
||||
|
||||
private interface Modifier {
|
||||
void modify(Path oldPath) throws IOException;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ package sonia.scm.repository.xml;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.inject.Singleton;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.InternalRepositoryException;
|
||||
import sonia.scm.repository.NamespaceAndName;
|
||||
@@ -35,6 +36,8 @@ import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.DownForMaintenanceContext;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.UpAfterMaintenanceContext;
|
||||
import sonia.scm.store.StoreReadOnlyException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@@ -55,6 +58,7 @@ import java.util.stream.Collectors;
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
@Singleton
|
||||
@Slf4j
|
||||
public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
|
||||
private final MetadataStore metadataStore = new MetadataStore();
|
||||
@@ -77,15 +81,35 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
this.byNamespaceAndName = new TreeMap<>();
|
||||
|
||||
init();
|
||||
|
||||
this.repositoryLocationResolver.registerMaintenanceCallback(new PathBasedRepositoryLocationResolver.MaintenanceCallback() {
|
||||
@Override
|
||||
public void downForMaintenance(DownForMaintenanceContext context) {
|
||||
Repository repository = byId.get(context.getRepositoryId());
|
||||
byNamespaceAndName.remove(repository.getNamespaceAndName());
|
||||
byId.remove(context.getRepositoryId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upAfterMaintenance(UpAfterMaintenanceContext context) {
|
||||
Repository repository = metadataStore.read(context.getLocation());
|
||||
byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
|
||||
byId.put(context.getRepositoryId(), repository);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void init() {
|
||||
withWriteLockedMaps(() -> {
|
||||
RepositoryLocationResolver.RepositoryLocationResolverInstance<Path> pathRepositoryLocationResolverInstance = repositoryLocationResolver.create(Path.class);
|
||||
pathRepositoryLocationResolverInstance.forAllLocations((repositoryId, repositoryPath) -> {
|
||||
Repository repository = metadataStore.read(repositoryPath);
|
||||
byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
|
||||
byId.put(repositoryId, repository);
|
||||
try {
|
||||
Repository repository = metadataStore.read(repositoryPath);
|
||||
byNamespaceAndName.put(repository.getNamespaceAndName(), repository);
|
||||
byId.put(repositoryId, repository);
|
||||
} catch (InternalRepositoryException e) {
|
||||
log.error("could not read repository metadata from {}", repositoryPath, e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -97,7 +121,7 @@ public class XmlRepositoryDAO implements RepositoryDAO {
|
||||
|
||||
@Override
|
||||
public synchronized void add(Repository repository) {
|
||||
add(repository, repositoryLocationResolver.create(repository.getId()));
|
||||
add(repository, repositoryLocationResolver.create(repository));
|
||||
}
|
||||
|
||||
public synchronized void add(Repository repository, Object location) {
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
@@ -38,17 +39,25 @@ import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.io.DefaultFileSystem;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryLocationResolver.RepositoryLocationResolverInstance;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.DownForMaintenanceContext;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.UpAfterMaintenanceContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.time.Clock;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -90,6 +99,16 @@ class PathBasedRepositoryLocationResolverTest {
|
||||
assertThat(path).isDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfDirectoryExists() throws IOException {
|
||||
Files.createDirectories(basePath.resolve("newId"));
|
||||
|
||||
RepositoryLocationResolverInstance<Path> resolverInstance = resolver.forClass(Path.class);
|
||||
|
||||
assertThatThrownBy(() -> resolverInstance.createLocation("newId"))
|
||||
.isInstanceOf(RepositoryLocationResolver.RepositoryStorageException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPersistInitialDirectory() {
|
||||
resolver.forClass(Path.class).createLocation("newId");
|
||||
@@ -133,11 +152,15 @@ class PathBasedRepositoryLocationResolverTest {
|
||||
|
||||
private PathBasedRepositoryLocationResolver resolverWithExistingData;
|
||||
|
||||
@Spy
|
||||
private PathBasedRepositoryLocationResolver.MaintenanceCallback maintenanceCallback;
|
||||
|
||||
@BeforeEach
|
||||
void createExistingDatabase() {
|
||||
resolver.forClass(Path.class).createLocation("existingId_1");
|
||||
resolver.forClass(Path.class).createLocation("existingId_2");
|
||||
resolverWithExistingData = createResolver();
|
||||
resolverWithExistingData.registerMaintenanceCallback(maintenanceCallback);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -176,6 +199,51 @@ class PathBasedRepositoryLocationResolverTest {
|
||||
|
||||
assertThat(path).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldModifyLocation() throws IOException {
|
||||
Path oldPath = resolverWithExistingData.create(Path.class).getLocation("existingId_1");
|
||||
Path newPath = basePath.resolve("modified_location");
|
||||
|
||||
resolverWithExistingData.create(Path.class).modifyLocation("existingId_1", newPath);
|
||||
|
||||
assertThat(newPath).exists();
|
||||
assertThat(oldPath).doesNotExist();
|
||||
assertThat(resolverWithExistingData.create(Path.class).getLocation("existingId_1")).isEqualTo(newPath);
|
||||
verify(maintenanceCallback).downForMaintenance(new DownForMaintenanceContext("existingId_1"));
|
||||
verify(maintenanceCallback).upAfterMaintenance(new UpAfterMaintenanceContext("existingId_1", newPath));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldModifyLocationAndKeepOld() throws IOException {
|
||||
Path oldPath = resolverWithExistingData.create(Path.class).getLocation("existingId_1");
|
||||
Path newPath = basePath.resolve("modified_location");
|
||||
|
||||
resolverWithExistingData.create(Path.class).modifyLocationAndKeepOld("existingId_1", newPath);
|
||||
|
||||
assertThat(newPath).exists();
|
||||
assertThat(oldPath).exists();
|
||||
assertThat(resolverWithExistingData.create(Path.class).getLocation("existingId_1")).isEqualTo(newPath);
|
||||
verify(maintenanceCallback).downForMaintenance(new DownForMaintenanceContext("existingId_1"));
|
||||
verify(maintenanceCallback).upAfterMaintenance(new UpAfterMaintenanceContext("existingId_1", newPath));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleErrorOnModifyLocation() throws IOException {
|
||||
Path oldPath = resolverWithExistingData.create(Path.class).getLocation("existingId_1");
|
||||
Path newPath = basePath.resolve("thou").resolve("shall").resolve("not").resolve("move").resolve("here");
|
||||
Files.createDirectories(newPath);
|
||||
Files.setPosixFilePermissions(newPath, Set.of(PosixFilePermission.OWNER_READ));
|
||||
|
||||
assertThatThrownBy(() -> resolverWithExistingData.create(Path.class).modifyLocationAndKeepOld("existingId_1", newPath))
|
||||
.isInstanceOf(RepositoryLocationResolver.RepositoryStorageException.class);
|
||||
|
||||
assertThat(newPath).exists();
|
||||
assertThat(oldPath).exists();
|
||||
assertThat(resolverWithExistingData.create(Path.class).getLocation("existingId_1")).isEqualTo(oldPath);
|
||||
verify(maintenanceCallback).downForMaintenance(new DownForMaintenanceContext("existingId_1"));
|
||||
verify(maintenanceCallback).upAfterMaintenance(new UpAfterMaintenanceContext("existingId_1", oldPath));
|
||||
}
|
||||
}
|
||||
|
||||
private String getXmlFileContent() {
|
||||
|
||||
@@ -43,6 +43,7 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -75,7 +76,7 @@ class XmlRepositoryDAOSynchronizationTest {
|
||||
fileSystem = new DefaultFileSystem();
|
||||
|
||||
resolver = new PathBasedRepositoryLocationResolver(
|
||||
provider, new InitialRepositoryLocationResolver(), fileSystem
|
||||
provider, new InitialRepositoryLocationResolver(emptySet()), fileSystem
|
||||
);
|
||||
|
||||
repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck);
|
||||
|
||||
@@ -32,8 +32,9 @@ import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
@@ -44,6 +45,8 @@ import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryPermission;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.DownForMaintenanceContext;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver.UpAfterMaintenanceContext;
|
||||
import sonia.scm.store.StoreReadOnlyException;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -59,7 +62,9 @@ import static java.util.Collections.singletonList;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.doNothing;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -104,12 +109,13 @@ class XmlRepositoryDAOTest {
|
||||
}
|
||||
}
|
||||
);
|
||||
when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation));
|
||||
when(locationResolver.create(any(Repository.class))).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation.getArgument(0, Repository.class).getId()));
|
||||
when(locationResolver.create(anyString())).thenAnswer(invocation -> createMockedRepoPath(basePath, invocation.getArgument(0, String.class)));
|
||||
when(locationResolver.remove(anyString())).thenAnswer(invocation -> basePath.resolve(invocation.getArgument(0).toString()));
|
||||
}
|
||||
|
||||
private Path createMockedRepoPath(@TempDir Path basePath, InvocationOnMock invocation) {
|
||||
Path resolvedPath = basePath.resolve(invocation.getArgument(0).toString());
|
||||
private static Path createMockedRepoPath(Path basePath, String repositoryId) {
|
||||
Path resolvedPath = basePath.resolve(repositoryId);
|
||||
try {
|
||||
Files.createDirectories(resolvedPath);
|
||||
} catch (IOException e) {
|
||||
@@ -378,6 +384,8 @@ class XmlRepositoryDAOTest {
|
||||
class WithExistingRepositories {
|
||||
|
||||
private Path repositoryPath;
|
||||
@Captor
|
||||
private ArgumentCaptor<PathBasedRepositoryLocationResolver.MaintenanceCallback> callbackArgumentCaptor;
|
||||
|
||||
@BeforeEach
|
||||
void createMetadataFileForRepository(@TempDir Path basePath) throws IOException {
|
||||
@@ -415,6 +423,26 @@ class XmlRepositoryDAOTest {
|
||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleMaintenanceEvents() {
|
||||
doNothing().when(locationResolver).registerMaintenanceCallback(callbackArgumentCaptor.capture());
|
||||
mockExistingPath();
|
||||
|
||||
XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck);
|
||||
|
||||
callbackArgumentCaptor.getValue().downForMaintenance(new DownForMaintenanceContext("existing"));
|
||||
|
||||
assertThat(dao.contains("existing")).isFalse();
|
||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isFalse();
|
||||
assertThat(dao.getAll()).isEmpty();
|
||||
|
||||
callbackArgumentCaptor.getValue().upAfterMaintenance(new UpAfterMaintenanceContext("existing", repositoryPath));
|
||||
|
||||
assertThat(dao.contains("existing")).isTrue();
|
||||
assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue();
|
||||
assertThat(dao.getAll()).hasSize(1);
|
||||
}
|
||||
|
||||
private void mockExistingPath() {
|
||||
triggeredOnForAllLocations = consumer -> consumer.accept("existing", repositoryPath);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
@@ -69,7 +70,7 @@ class JAXBPropertyFileAccessTest {
|
||||
lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile());
|
||||
lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString()));
|
||||
|
||||
locationResolver = new PathBasedRepositoryLocationResolver(contextProvider, new InitialRepositoryLocationResolver(), new DefaultFileSystem());
|
||||
locationResolver = new PathBasedRepositoryLocationResolver(contextProvider, new InitialRepositoryLocationResolver(emptySet()), new DefaultFileSystem());
|
||||
|
||||
fileAccess = new JAXBPropertyFileAccess(contextProvider, locationResolver);
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ import org.junit.Before;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import sonia.scm.io.DefaultFileSystem;
|
||||
import sonia.scm.repository.InitialRepositoryLocationResolver;
|
||||
import sonia.scm.repository.RepositoryDAO;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.util.MockUtil;
|
||||
@@ -50,8 +48,8 @@ import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -76,7 +74,7 @@ public class AbstractTestBase
|
||||
UUID.randomUUID().toString());
|
||||
assertTrue(tempDirectory.mkdirs());
|
||||
contextProvider = MockUtil.getSCMContextProvider(tempDirectory);
|
||||
InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver();
|
||||
InitialRepositoryLocationResolver initialRepoLocationResolver = new InitialRepositoryLocationResolver(emptySet());
|
||||
repositoryLocationResolver = new TempDirRepositoryLocationResolver(tempDirectory);
|
||||
postSetUp();
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm;
|
||||
|
||||
import org.junit.After;
|
||||
@@ -36,6 +36,7 @@ import sonia.scm.util.MockUtil;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -49,7 +50,7 @@ public abstract class ManagerTestBase<T extends ModelObject>
|
||||
|
||||
@Rule
|
||||
public TemporaryFolder tempFolder = new TemporaryFolder();
|
||||
|
||||
|
||||
protected SCMContextProvider contextProvider;
|
||||
protected RepositoryLocationResolver locationResolver;
|
||||
|
||||
@@ -63,18 +64,18 @@ public abstract class ManagerTestBase<T extends ModelObject>
|
||||
temp = tempFolder.newFolder();
|
||||
}
|
||||
contextProvider = MockUtil.getSCMContextProvider(temp);
|
||||
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver();
|
||||
InitialRepositoryLocationResolver initialRepositoryLocationResolver = new InitialRepositoryLocationResolver(emptySet());
|
||||
RepositoryDAO repoDao = mock(RepositoryDAO.class);
|
||||
locationResolver = new TempDirRepositoryLocationResolver(temp);
|
||||
manager = createManager();
|
||||
manager.init(contextProvider);
|
||||
}
|
||||
|
||||
|
||||
@After
|
||||
public void tearDown() throws IOException {
|
||||
manager.close();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
|
||||
@@ -667,6 +667,11 @@ export type RepositoryDeleteButton = RenderableExtensionPointDefinition<
|
||||
{ repository: Repository }
|
||||
>;
|
||||
|
||||
export type RepositoryDangerZone = RenderableExtensionPointDefinition<
|
||||
"repository.dangerZone",
|
||||
{ repository: Repository }
|
||||
>;
|
||||
|
||||
export type RepositoryInformationTableBottom = RenderableExtensionPointDefinition<
|
||||
"repository.information.table.bottom",
|
||||
{ repository: Repository }
|
||||
|
||||
@@ -57,6 +57,14 @@ const RepositoryDangerZone: FC<Props> = ({ repository }) => {
|
||||
if (repository?._links?.unarchive) {
|
||||
dangerZone.push(<UnarchiveRepo repository={repository} />);
|
||||
}
|
||||
dangerZone.push(
|
||||
<ExtensionPoint<extensionPoints.RepositoryDangerZone>
|
||||
name="repository.dangerZone"
|
||||
props={{ repository }}
|
||||
renderAll={true}
|
||||
/>
|
||||
);
|
||||
|
||||
if (dangerZone.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.api;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.MDC;
|
||||
import sonia.scm.api.v2.resources.ErrorDto;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.web.VndMediaType;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
import javax.ws.rs.ext.Provider;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
@Slf4j
|
||||
@Provider
|
||||
public class RepositoryStorageExceptionMapper implements ExceptionMapper<RepositoryLocationResolver.RepositoryStorageException> {
|
||||
@Override
|
||||
public Response toResponse(RepositoryLocationResolver.RepositoryStorageException exception) {
|
||||
log.error("exception in repository storage", exception);
|
||||
ErrorDto error = new ErrorDto();
|
||||
error.setTransactionId(MDC.get("transaction_id"));
|
||||
error.setMessage("could not store repository: " + exception.getMessage());
|
||||
error.setErrorCode("E4TrutUSv1");
|
||||
ErrorDto.ConstraintViolationDto violation = new ErrorDto.ConstraintViolationDto();
|
||||
violation.setPath("storage location");
|
||||
violation.setMessage(exception.getRootMessage());
|
||||
error.setViolations(asList(violation));
|
||||
return Response.status(Response.Status.BAD_REQUEST)
|
||||
.entity(error)
|
||||
.type(VndMediaType.ERROR_TYPE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ import sonia.scm.repository.DefaultRepositoryExportingCheck;
|
||||
import sonia.scm.repository.EventDrivenRepositoryArchiveCheck;
|
||||
import sonia.scm.repository.RepositoryArchivedCheck;
|
||||
import sonia.scm.repository.RepositoryExportingCheck;
|
||||
import sonia.scm.repository.RepositoryLocationOverride;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.xml.MetadataStore;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
@@ -133,6 +134,7 @@ public class BootstrapModule extends AbstractModule {
|
||||
// bind metrics
|
||||
bind(MeterRegistry.class).toProvider(MeterRegistryProvider.class).asEagerSingleton();
|
||||
Multibinder.newSetBinder(binder(), MonitoringSystem.class);
|
||||
Multibinder.newSetBinder(binder(), RepositoryLocationOverride.class);
|
||||
|
||||
// bind cache
|
||||
bind(CacheManager.class, GuavaCacheManager.class);
|
||||
|
||||
@@ -483,6 +483,10 @@
|
||||
"5FSV2kreE1": {
|
||||
"summary": "'svn verify' fehlgeschlagen",
|
||||
"description": "Die Prüfung 'svn verify' ist für das Repository fehlgeschlagen."
|
||||
},
|
||||
"E4TrutUSv1": {
|
||||
"summary": "Speicherung des Repositories fehlgeschlagen",
|
||||
"description": "Beim Speichern des Repositories ist ein Fehler aufgetreten. Weitere Hinweise finden sich im Server Log."
|
||||
}
|
||||
},
|
||||
"namespaceStrategies": {
|
||||
|
||||
@@ -424,6 +424,10 @@
|
||||
"3ITWbABz91": {
|
||||
"displayName": "API keys disabled",
|
||||
"description": "The usage of API keys has been disabled in the global configuration. To use API keys, this has to be enabled again."
|
||||
},
|
||||
"E4TrutUSv1": {
|
||||
"displayName": "Failed to store the repository",
|
||||
"description": "An error occurred while storing the repository. Further information can be found in the server log."
|
||||
}
|
||||
},
|
||||
"healthChecksFailures": {
|
||||
|
||||
Reference in New Issue
Block a user