diff --git a/docs/de/user/repo/settings.md b/docs/de/user/repo/settings.md index 13d6b5ea73..81724c6ada 100644 --- a/docs/de/user/repo/settings.md +++ b/docs/de/user/repo/settings.md @@ -18,6 +18,7 @@ Strategie `benutzerdefiniert` ausgewählt ist, kann zusätzlich zum Repository N Ein archiviertes Repository kann nicht mehr verändert werden. In dem Bereich "Repository exportieren" kann das Repository in unterschiedlichen Formaten exportiert werden. +Während des laufenden Exports kann auf das Repository nur lesend zugriffen werden. Das Ausgabeformat des Repository kann über die angebotenen Optionen verändert werden: * `Standard`: Werden keine Optionen ausgewählt, wird das Repository im Standard Format exportiert. Git und Mercurial werden dabei als `Tar Archiv` exportiert und Subversion nutzt das `Dump` Format. diff --git a/docs/en/user/repo/settings.md b/docs/en/user/repo/settings.md index ded1bc664c..cc2a8504bd 100644 --- a/docs/en/user/repo/settings.md +++ b/docs/en/user/repo/settings.md @@ -16,6 +16,7 @@ strategy in the global SCM-Manager config is set to `custom` you may even rename repository is marked as archived, it can no longer be modified. In the "Export repository" section the repository can be exported in different formats. +During the export the repository cannot be modified! The output format of the repository can be changed via the offered options: * `Standard`: If no options are selected, the repository will be exported in the standard format. Git and Mercurial are exported as `Tar archive` and Subversion uses the `Dump` format. diff --git a/gradle/changelog/repository_export_lock.yaml b/gradle/changelog/repository_export_lock.yaml new file mode 100644 index 0000000000..32e6a1bd0c --- /dev/null +++ b/gradle/changelog/repository_export_lock.yaml @@ -0,0 +1,2 @@ +- type: added + description: Lock repository to "read-only" access during export ([#1519](https://github.com/scm-manager/scm-manager/pull/1519)) diff --git a/scm-core/src/main/java/sonia/scm/repository/DefaultRepositoryExportingCheck.java b/scm-core/src/main/java/sonia/scm/repository/DefaultRepositoryExportingCheck.java new file mode 100644 index 0000000000..5f01a12f4e --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/DefaultRepositoryExportingCheck.java @@ -0,0 +1,70 @@ +/* + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +/** + * Default implementation of {@link RepositoryExportingCheck}. This tracks the exporting status of repositories. + */ +public final class DefaultRepositoryExportingCheck implements RepositoryExportingCheck { + + private static final Logger LOG = LoggerFactory.getLogger(DefaultRepositoryExportingCheck.class); + private static final Map EXPORTING_REPOSITORIES = Collections.synchronizedMap(new HashMap<>()); + + public static boolean isRepositoryExporting(String repositoryId) { + return getLockCount(repositoryId).get() > 0; + } + + @Override + public boolean isExporting(String repositoryId) { + return isRepositoryExporting(repositoryId); + } + + @Override + public T withExportingLock(Repository repository, Supplier callback) { + try { + getLockCount(repository.getId()).incrementAndGet(); + return callback.get(); + } finally { + int lockCount = getLockCount(repository.getId()).decrementAndGet(); + if (lockCount <= 0) { + LOG.warn("Got negative export lock count {} for repository {}", lockCount, repository); + EXPORTING_REPOSITORIES.remove(repository.getId()); + } + } + } + + private static AtomicInteger getLockCount(String repositoryId) { + return EXPORTING_REPOSITORIES.computeIfAbsent(repositoryId, r -> new AtomicInteger(0)); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheck.java b/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheck.java index e961fe82cc..a016802bb9 100644 --- a/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheck.java +++ b/scm-core/src/main/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheck.java @@ -25,15 +25,11 @@ package sonia.scm.repository; import com.github.legman.Subscribe; -import sonia.scm.EagerSingleton; -import sonia.scm.plugin.Extension; import java.util.Collection; import java.util.Collections; import java.util.HashSet; -@Extension -@EagerSingleton /** * Default implementation of {@link RepositoryArchivedCheck}. This tracks the archive status of repositories by using * {@link RepositoryModificationEvent}s. The initial set of archived repositories is read by diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index d829d7f4fd..fce742f4fa 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -188,7 +188,9 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per return name; } - public String getNamespace() { return namespace; } + public String getNamespace() { + return namespace; + } @XmlTransient public NamespaceAndName getNamespaceAndName() { @@ -267,7 +269,9 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per this.lastModified = lastModified; } - public void setNamespace(String namespace) { this.namespace = namespace; } + public void setNamespace(String namespace) { + this.namespace = namespace; + } public void setName(String name) { this.name = name; diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedCheck.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedCheck.java index 2955bdcbb5..b45e343e17 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedCheck.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedCheck.java @@ -27,7 +27,7 @@ package sonia.scm.repository; /** * Implementations of this class can be used to check whether a repository is archived. * - * @since 1.12.0 + * @since 2.12.0 */ public interface RepositoryArchivedCheck { diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryArchivedException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedException.java similarity index 92% rename from scm-core/src/main/java/sonia/scm/repository/api/RepositoryArchivedException.java rename to scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedException.java index 4343a5702e..cd463d3030 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryArchivedException.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryArchivedException.java @@ -22,10 +22,9 @@ * SOFTWARE. */ -package sonia.scm.repository.api; +package sonia.scm.repository; import sonia.scm.ExceptionWithContext; -import sonia.scm.repository.Repository; import static java.lang.String.format; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -34,7 +33,7 @@ public class RepositoryArchivedException extends ExceptionWithContext { public static final String CODE = "3hSIlptme1"; - protected RepositoryArchivedException(Repository repository) { + public RepositoryArchivedException(Repository repository) { super(entity(repository).build(), format("Repository %s is marked as archived and must not be modified", repository)); } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryExportingCheck.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryExportingCheck.java new file mode 100644 index 0000000000..6fd87797de --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryExportingCheck.java @@ -0,0 +1,62 @@ +/* + * 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 java.util.function.Supplier; + +/** + * Implementations of this class can be used to check whether a repository is currently being exported. + * + * @since 2.14.0 + */ +public interface RepositoryExportingCheck { + + /** + * Checks whether the repository with the given id is currently (that is, at this moment) being exported or not. + * @param repositoryId The id of the repository to check. + * @return true when the repository with the given id is currently being exported, false + * otherwise. + */ + boolean isExporting(String repositoryId); + + /** + * Checks whether the given repository is currently (that is, at this moment) being exported or not. This checks the + * status on behalf of the id of the repository, not by the exporting flag provided by the repository itself. + * @param repository The repository to check. + * @return true when the given repository is currently being exported, false otherwise. + */ + default boolean isExporting(Repository repository) { + return isExporting(repository.getId()); + } + + /** + * Asserts that the given repository is marked as being exported during the execution of the given callback. + * @param repository The repository that will be marked as being exported. + * @param callback This callback will be executed. + * @param The return type of the callback. + * @return The result of the callback. + */ + T withExportingLock(Repository repository, Supplier callback); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryExportingException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryExportingException.java new file mode 100644 index 0000000000..d9dd389b24 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryExportingException.java @@ -0,0 +1,44 @@ +/* + * 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.ExceptionWithContext; + +import static java.lang.String.format; +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +public class RepositoryExportingException extends ExceptionWithContext { + + public static final String CODE = "1mSNlpe1V1"; + + public RepositoryExportingException(Repository repository) { + super(entity(repository).build(), format("Repository %s is currently being exported and must not be modified", repository)); + } + + @Override + public String getCode() { + return CODE; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuard.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuard.java index 7b17dc30d9..89a7c3b232 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuard.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryPermissionGuard.java @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.function.BooleanSupplier; +import static sonia.scm.repository.DefaultRepositoryExportingCheck.isRepositoryExporting; import static sonia.scm.repository.EventDrivenRepositoryArchiveCheck.isRepositoryArchived; /** @@ -64,11 +65,14 @@ public class RepositoryPermissionGuard implements PermissionGuard { if (isRepositoryArchived(id)) { throw new AuthorizationException("repository is archived"); } + if (isRepositoryExporting(id)) { + throw new AuthorizationException("repository is exporting"); + } } @Override public boolean isPermitted(Subject subject, String id, BooleanSupplier delegate) { - return !isRepositoryArchived(id) && delegate.getAsBoolean(); + return !isRepositoryArchived(id) && !isRepositoryExporting(id) && delegate.getAsBoolean(); } } } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryReadOnlyChecker.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryReadOnlyChecker.java new file mode 100644 index 0000000000..62324f6736 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryReadOnlyChecker.java @@ -0,0 +1,86 @@ +/* + * 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 javax.inject.Inject; + +/** + * Checks, whether a repository has to be considered read only. Currently, this includes {@link RepositoryArchivedCheck} + * and {@link RepositoryExportingCheck}. + * + * @since 2.14.0 + */ +public final class RepositoryReadOnlyChecker { + + private final RepositoryArchivedCheck archivedCheck; + private final RepositoryExportingCheck exportingCheck; + + @Inject + public RepositoryReadOnlyChecker(RepositoryArchivedCheck archivedCheck, RepositoryExportingCheck exportingCheck) { + this.archivedCheck = archivedCheck; + this.exportingCheck = exportingCheck; + } + + /** + * Checks if the repository is read only. + * @param repository The repository to check. + * @return true if any check locks the repository to read only access. + */ + public boolean isReadOnly(Repository repository) { + return isReadOnly(repository.getId()); + } + + /** + * Checks if the repository for the given id is read only. + * @param repositoryId The id of the given repository to check. + * @return true if any check locks the repository to read only access. + */ + public boolean isReadOnly(String repositoryId) { + return archivedCheck.isArchived(repositoryId) || exportingCheck.isExporting(repositoryId); + } + + /** + * Checks if the repository may be modified. + * + * @throws RepositoryArchivedException if the repository is archived + * @throws RepositoryExportingException if the repository is currently being exported + */ + public static void checkReadOnly(Repository repository) { + if (isArchived(repository)) { + throw new RepositoryArchivedException(repository); + } + if (isExporting(repository)) { + throw new RepositoryExportingException(repository); + } + } + + private static boolean isExporting(Repository repository) { + return DefaultRepositoryExportingCheck.isRepositoryExporting(repository.getId()); + } + + private static boolean isArchived(Repository repository) { + return repository.isArchived() || EventDrivenRepositoryArchiveCheck.isRepositoryArchived(repository.getId()); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java index ad6209c229..fa3997e32c 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BundleCommandBuilder.java @@ -30,7 +30,9 @@ import com.google.common.io.ByteSink; import com.google.common.io.Files; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryExportingCheck; import sonia.scm.repository.spi.BundleCommand; import sonia.scm.repository.spi.BundleCommandRequest; @@ -63,12 +65,13 @@ public final class BundleCommandBuilder { /** * Constructs a new {@link BundleCommandBuilder}. - * - * @param bundleCommand bundle command implementation + * @param bundleCommand bundle command implementation + * @param repositoryExportingCheck * @param repository repository */ - BundleCommandBuilder(BundleCommand bundleCommand, Repository repository) { + BundleCommandBuilder(BundleCommand bundleCommand, RepositoryExportingCheck repositoryExportingCheck, Repository repository) { this.bundleCommand = bundleCommand; + this.repositoryExportingCheck = repositoryExportingCheck; this.repository = repository; } @@ -79,9 +82,8 @@ public final class BundleCommandBuilder { * * @param outputFile output file * @return bundle response - * @throws IOException */ - public BundleResponse bundle(File outputFile) throws IOException { + public BundleResponse bundle(File outputFile) { checkArgument((outputFile != null) && !outputFile.exists(), "file is null or exists already"); @@ -91,7 +93,7 @@ public final class BundleCommandBuilder { logger.info("create bundle at {} for repository {}", outputFile, repository.getId()); - return bundleCommand.bundle(request); + return bundleWithExportingLock(request); } /** @@ -99,16 +101,14 @@ public final class BundleCommandBuilder { * * @param outputStream output stream * @return bundle response - * @throws IOException */ - public BundleResponse bundle(OutputStream outputStream) - throws IOException { + public BundleResponse bundle(OutputStream outputStream) { checkNotNull(outputStream, "output stream is required"); logger.info("bundle {} to output stream", repository); - return bundleCommand.bundle( - new BundleCommandRequest(asByteSink(outputStream))); + BundleCommandRequest request = new BundleCommandRequest(asByteSink(outputStream)); + return bundleWithExportingLock(request); } /** @@ -116,14 +116,23 @@ public final class BundleCommandBuilder { * * @param sink byte sink * @return bundle response - * @throws IOException */ - public BundleResponse bundle(ByteSink sink) - throws IOException { + public BundleResponse bundle(ByteSink sink) { checkNotNull(sink, "byte sink is required"); logger.info("bundle {} to byte sink", sink); - return bundleCommand.bundle(new BundleCommandRequest(sink)); + BundleCommandRequest request = new BundleCommandRequest(sink); + return bundleWithExportingLock(request); + } + + private BundleResponse bundleWithExportingLock(BundleCommandRequest request) { + return repositoryExportingCheck.withExportingLock(repository, () -> { + try { + return bundleCommand.bundle(request); + } catch (IOException e) { + throw new InternalRepositoryException(repository, "Exception during bundle; does not necessarily indicate a problem with the repository", e); + } + }); } /** @@ -162,4 +171,6 @@ public final class BundleCommandBuilder { * repository */ private final Repository repository; + + private final RepositoryExportingCheck repositoryExportingCheck; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index 43cddee2a9..4773deb325 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -31,7 +31,9 @@ import sonia.scm.repository.Changeset; import sonia.scm.repository.Feature; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryExportingCheck; import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.repository.work.WorkdirProvider; import sonia.scm.security.Authentications; @@ -93,15 +95,18 @@ public final class RepositoryService implements Closeable { @Nullable private final EMail eMail; + private final RepositoryExportingCheck repositoryExportingCheck; /** * Constructs a new {@link RepositoryService}. This constructor should only * be called from the {@link RepositoryServiceFactory}. - * @param cacheManager cache manager - * @param provider implementation for {@link RepositoryServiceProvider} - * @param repository the repository - * @param workdirProvider provider for workdirs - * @param eMail utility to compute email addresses if missing + * + * @param cacheManager cache manager + * @param provider implementation for {@link RepositoryServiceProvider} + * @param repository the repository + * @param workdirProvider provider for workdirs + * @param eMail utility to compute email addresses if missing + * @param repositoryExportingCheck */ RepositoryService(CacheManager cacheManager, RepositoryServiceProvider provider, @@ -109,7 +114,7 @@ public final class RepositoryService implements Closeable { PreProcessorUtil preProcessorUtil, @SuppressWarnings({"rawtypes", "java:S3740"}) Set protocolProviders, WorkdirProvider workdirProvider, - @Nullable EMail eMail) { + @Nullable EMail eMail, RepositoryExportingCheck repositoryExportingCheck) { this.cacheManager = cacheManager; this.provider = provider; this.repository = repository; @@ -117,6 +122,7 @@ public final class RepositoryService implements Closeable { this.protocolProviders = protocolProviders; this.workdirProvider = workdirProvider; this.eMail = eMail; + this.repositoryExportingCheck = repositoryExportingCheck; } /** @@ -182,7 +188,7 @@ public final class RepositoryService implements Closeable { * by the implementation of the repository service provider. */ public BranchCommandBuilder getBranchCommand() { - verifyNotArchived(); + RepositoryReadOnlyChecker.checkReadOnly(getRepository()); RepositoryPermissions.push(getRepository()).check(); LOG.debug("create branch command for repository {}", repository.getNamespaceAndName()); @@ -217,7 +223,7 @@ public final class RepositoryService implements Closeable { LOG.debug("create bundle command for repository {}", repository.getNamespaceAndName()); - return new BundleCommandBuilder(provider.getBundleCommand(), repository); + return new BundleCommandBuilder(provider.getBundleCommand(), repositoryExportingCheck, repository); } /** @@ -305,7 +311,7 @@ public final class RepositoryService implements Closeable { */ public ModificationsCommandBuilder getModificationsCommand() { LOG.debug("create modifications command for repository {}", repository); - return new ModificationsCommandBuilder(provider.getModificationsCommand(),repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil); + return new ModificationsCommandBuilder(provider.getModificationsCommand(), repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil); } /** @@ -333,7 +339,7 @@ public final class RepositoryService implements Closeable { * @since 1.31 */ public PullCommandBuilder getPullCommand() { - verifyNotArchived(); + RepositoryReadOnlyChecker.checkReadOnly(getRepository()); LOG.debug("create pull command for repository {}", repository.getNamespaceAndName()); @@ -383,12 +389,11 @@ public final class RepositoryService implements Closeable { * The tag command allows the management of repository tags. * * @return instance of {@link TagCommandBuilder} - * * @throws CommandNotSupportedException if the command is not supported * by the implementation of the repository service provider. */ public TagCommandBuilder getTagCommand() { - verifyNotArchived(); + RepositoryReadOnlyChecker.checkReadOnly(getRepository()); return new TagCommandBuilder(provider.getTagCommand()); } @@ -418,7 +423,7 @@ public final class RepositoryService implements Closeable { * @since 2.0.0 */ public MergeCommandBuilder getMergeCommand() { - verifyNotArchived(); + RepositoryReadOnlyChecker.checkReadOnly(getRepository()); LOG.debug("create merge command for repository {}", repository.getNamespaceAndName()); @@ -440,7 +445,7 @@ public final class RepositoryService implements Closeable { * @since 2.0.0 */ public ModifyCommandBuilder getModifyCommand() { - verifyNotArchived(); + RepositoryReadOnlyChecker.checkReadOnly(getRepository()); LOG.debug("create modify command for repository {}", repository.getNamespaceAndName()); @@ -489,12 +494,6 @@ public final class RepositoryService implements Closeable { .filter(protocol -> !Authentications.isAuthenticatedSubjectAnonymous() || protocol.isAnonymousEnabled()); } - private void verifyNotArchived() { - if (getRepository().isArchived()) { - throw new RepositoryArchivedException(getRepository()); - } - } - @SuppressWarnings({"rawtypes", "java:S3740"}) private ScmProtocol createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) { return protocolProvider.get(repository); @@ -507,6 +506,6 @@ public final class RepositoryService implements Closeable { // no idea how to fix this, without cast .map(p -> (T) p) .findFirst() - .orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(),getRepository().getType()))); + .orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(), getRepository().getType()))); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index 2c13cb63be..3d511c1870 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -43,12 +43,14 @@ import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; import sonia.scm.repository.ClearRepositoryCacheEvent; +import sonia.scm.repository.DefaultRepositoryExportingCheck; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.PostReceiveRepositoryHookEvent; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryCacheKeyPredicate; import sonia.scm.repository.RepositoryEvent; +import sonia.scm.repository.RepositoryExportingCheck; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.spi.RepositoryServiceProvider; @@ -123,6 +125,7 @@ public final class RepositoryServiceFactory { @SuppressWarnings({"rawtypes", "java:S3740"}) private final Set protocolProviders; private final WorkdirProvider workdirProvider; + private final RepositoryExportingCheck repositoryExportingCheck; @Nullable private final EMail eMail; @@ -141,7 +144,7 @@ public final class RepositoryServiceFactory { * @param protocolProviders providers for repository protocols * @param workdirProvider provider for working directories * - * @deprecated use {@link RepositoryServiceFactory#RepositoryServiceFactory(CacheManager, RepositoryManager, Set, PreProcessorUtil, Set, WorkdirProvider, EMail)} instead + * @deprecated use {@link RepositoryServiceFactory#RepositoryServiceFactory(CacheManager, RepositoryManager, Set, PreProcessorUtil, Set, WorkdirProvider, EMail, RepositoryExportingCheck)} instead * @since 1.21 */ @Deprecated @@ -152,7 +155,8 @@ public final class RepositoryServiceFactory { WorkdirProvider workdirProvider) { this( cacheManager, repositoryManager, resolvers, - preProcessorUtil, protocolProviders, workdirProvider, null, ScmEventBus.getInstance() + preProcessorUtil, protocolProviders, workdirProvider, null, ScmEventBus.getInstance(), + new DefaultRepositoryExportingCheck() ); } @@ -174,11 +178,12 @@ public final class RepositoryServiceFactory { public RepositoryServiceFactory(CacheManager cacheManager, RepositoryManager repositoryManager, Set resolvers, PreProcessorUtil preProcessorUtil, @SuppressWarnings({"rawtypes", "java:S3740"}) Set protocolProviders, - WorkdirProvider workdirProvider, EMail eMail) { + WorkdirProvider workdirProvider, EMail eMail, + RepositoryExportingCheck repositoryExportingCheck) { this( cacheManager, repositoryManager, resolvers, preProcessorUtil, protocolProviders, workdirProvider, - eMail, ScmEventBus.getInstance() + eMail, ScmEventBus.getInstance(), repositoryExportingCheck ); } @@ -187,7 +192,8 @@ public final class RepositoryServiceFactory { RepositoryServiceFactory(CacheManager cacheManager, RepositoryManager repositoryManager, Set resolvers, PreProcessorUtil preProcessorUtil, @SuppressWarnings({"rawtypes", "java:S3740"}) Set protocolProviders, - WorkdirProvider workdirProvider, @Nullable EMail eMail, ScmEventBus eventBus) { + WorkdirProvider workdirProvider, @Nullable EMail eMail, ScmEventBus eventBus, + RepositoryExportingCheck repositoryExportingCheck) { this.cacheManager = cacheManager; this.repositoryManager = repositoryManager; this.resolvers = resolvers; @@ -195,6 +201,7 @@ public final class RepositoryServiceFactory { this.protocolProviders = protocolProviders; this.workdirProvider = workdirProvider; this.eMail = eMail; + this.repositoryExportingCheck = repositoryExportingCheck; eventBus.register(new CacheClearHook(cacheManager)); } @@ -284,7 +291,7 @@ public final class RepositoryServiceFactory { } service = new RepositoryService(cacheManager, provider, repository, - preProcessorUtil, protocolProviders, workdirProvider, eMail); + preProcessorUtil, protocolProviders, workdirProvider, eMail, repositoryExportingCheck); break; } diff --git a/scm-core/src/test/java/sonia/scm/repository/DefaultRepositoryExportingCheckTest.java b/scm-core/src/test/java/sonia/scm/repository/DefaultRepositoryExportingCheckTest.java new file mode 100644 index 0000000000..fc3ea0c428 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/DefaultRepositoryExportingCheckTest.java @@ -0,0 +1,67 @@ +/* + * 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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class DefaultRepositoryExportingCheckTest { + + private static final Repository EXPORTING_REPOSITORY = new Repository("exporting_hog", "git", "hitchhiker", "hog"); + + private final DefaultRepositoryExportingCheck check = new DefaultRepositoryExportingCheck(); + + @Test + void shouldBeReadOnlyIfBeingExported() { + check.withExportingLock(EXPORTING_REPOSITORY, () -> { + boolean readOnly = check.isExporting(EXPORTING_REPOSITORY); + assertThat(readOnly).isTrue(); + return null; + }); + } + + @Test + void shouldBeReadOnlyIfBeingExportedMultipleTimes() { + check.withExportingLock(EXPORTING_REPOSITORY, () -> { + check.withExportingLock(EXPORTING_REPOSITORY, () -> { + boolean readOnly = check.isExporting(EXPORTING_REPOSITORY); + assertThat(readOnly).isTrue(); + return null; + }); + boolean readOnly = check.isExporting(EXPORTING_REPOSITORY); + assertThat(readOnly).isTrue(); + return null; + }); + } + + @Test + void shouldNotBeReadOnlyIfNotBeingExported() { + boolean readOnly = check.isExporting(EXPORTING_REPOSITORY); + assertThat(readOnly).isFalse(); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckTest.java b/scm-core/src/test/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckTest.java index 4a518f95e3..c589a4231e 100644 --- a/scm-core/src/test/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/EventDrivenRepositoryArchiveCheckTest.java @@ -38,9 +38,10 @@ import static sonia.scm.repository.EventDrivenRepositoryArchiveCheck.setAsArchiv class EventDrivenRepositoryArchiveCheckTest { private static final Repository NORMAL_REPOSITORY = new Repository("hog", "git", "hitchhiker", "hog"); - private static final Repository ARCHIVED_REPOSITORY = new Repository("hog", "git", "hitchhiker", "hog"); + private static final Repository ARCHIVED_REPOSITORY = new Repository("archived_hog", "git", "hitchhiker", "hog"); static { ARCHIVED_REPOSITORY.setArchived(true); + EventDrivenRepositoryArchiveCheck.setAsArchived(ARCHIVED_REPOSITORY.getId()); } EventDrivenRepositoryArchiveCheck check = new EventDrivenRepositoryArchiveCheck(); @@ -53,7 +54,7 @@ class EventDrivenRepositoryArchiveCheckTest { @Test void shouldBeArchivedAfterFlagHasBeenSet() { check.updateListener(new RepositoryModificationEvent(HandlerEventType.MODIFY, ARCHIVED_REPOSITORY, NORMAL_REPOSITORY)); - assertThat(check.isArchived("hog")).isTrue(); + assertThat(check.isArchived("archived_hog")).isTrue(); } @Test @@ -70,6 +71,18 @@ class EventDrivenRepositoryArchiveCheckTest { new EventDrivenRepositoryArchiveCheckInitializer(repositoryDAO).init(null); - assertThat(check.isArchived("hog")).isTrue(); + assertThat(check.isArchived("archived_hog")).isTrue(); + } + + @Test + void shouldBeReadOnly() { + boolean readOnly = check.isArchived(ARCHIVED_REPOSITORY); + assertThat(readOnly).isTrue(); + } + + @Test + void shouldNotBeReadOnly() { + boolean readOnly = check.isArchived(NORMAL_REPOSITORY); + assertThat(readOnly).isFalse(); } } diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionGuardTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionGuardTest.java index 6d2d3215f6..3e28b18d0e 100644 --- a/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionGuardTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryPermissionGuardTest.java @@ -33,12 +33,16 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.lang.reflect.Method; import java.util.function.BooleanSupplier; -import static java.util.Arrays.asList; +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.mockito.Mockito.doThrow; @@ -58,7 +62,7 @@ class RepositoryPermissionGuardTest { @BeforeAll static void setReadOnlyVerbs() { - RepositoryPermissionGuard.setReadOnlyVerbs(asList("read")); + RepositoryPermissionGuard.setReadOnlyVerbs(singletonList("read")); } @Nested @@ -142,5 +146,47 @@ class RepositoryPermissionGuardTest { verify(checkDelegate).run(); } } + + @Nested + @ExtendWith(WrapInExportCheck.class) + class WithExportingRepository { + + @Test + void shouldInterceptPermissionCheck() { + assertThat(readInterceptor.isPermitted(subject, "1", permittedDelegate)).isFalse(); + + verify(permittedDelegate, never()).getAsBoolean(); + } + + @Test + void shouldInterceptCheckRequest() { + assertThrows(AuthorizationException.class, () -> readInterceptor.check(subject, "1", checkDelegate)); + } + + @Test + void shouldThrowConcretePermissionExceptionOverArchiveException() { + doThrow(new AuthorizationException()).when(checkDelegate).run(); + + assertThrows(AuthorizationException.class, () -> readInterceptor.check(subject, "1", checkDelegate)); + + verify(checkDelegate).run(); + } + } + } + + private static class WrapInExportCheck implements InvocationInterceptor { + + public void interceptTestMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) { + new DefaultRepositoryExportingCheck().withExportingLock(new Repository("1", "git", "space", "X"), () -> { + try { + invocation.proceed(); + return null; + } catch (Throwable t) { + throw new RuntimeException(t); + } + }); + } } } diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryReadOnlyCheckerTest.java b/scm-core/src/test/java/sonia/scm/repository/RepositoryReadOnlyCheckerTest.java new file mode 100644 index 0000000000..5a147af76f --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/RepositoryReadOnlyCheckerTest.java @@ -0,0 +1,79 @@ +/* + * 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 org.junit.jupiter.api.Test; + +import java.util.function.Supplier; + +import static org.assertj.core.api.Assertions.assertThat; + +class RepositoryReadOnlyCheckerTest { + + private final Repository repository = new Repository("1", "git","hitchhiker", "HeartOfGold"); + + private boolean archived = false; + private boolean exporting = false; + + private final RepositoryArchivedCheck archivedCheck = repositoryId -> archived; + private final RepositoryExportingCheck exportingCheck = new RepositoryExportingCheck() { + @Override + public boolean isExporting(String repositoryId) { + return exporting; + } + + @Override + public T withExportingLock(Repository repository, Supplier callback) { + return null; + } + }; + + private final RepositoryReadOnlyChecker checker = new RepositoryReadOnlyChecker(archivedCheck, exportingCheck); + + @Test + void shouldReturnFalseIfAllChecksFalse() { + boolean readOnly = checker.isReadOnly(repository); + + assertThat(readOnly).isFalse(); + } + + @Test + void shouldReturnTrueIfArchivedIsTrue() { + archived = true; + + boolean readOnly = checker.isReadOnly(repository); + + assertThat(readOnly).isTrue(); + } + + @Test + void shouldReturnTrueIfExportingIsTrue() { + exporting = true; + + boolean readOnly = checker.isReadOnly(repository); + + assertThat(readOnly).isTrue(); + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java index d77d0d072b..2b1e2831bf 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceFactoryTest.java @@ -39,6 +39,7 @@ import sonia.scm.NotFoundException; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; +import sonia.scm.repository.DefaultRepositoryExportingCheck; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.PreProcessorUtil; import sonia.scm.repository.Repository; @@ -94,7 +95,8 @@ class RepositoryServiceFactoryTest { return new RepositoryServiceFactory( cacheManager, repositoryManager, builder.build(), preProcessorUtil, ImmutableSet.of(), workdirProvider, - new EMail(new ScmConfiguration()), eventBus + new EMail(new ScmConfiguration()), eventBus, + new DefaultRepositoryExportingCheck() ); } diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java index fcde81f786..daee946fed 100644 --- a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java +++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java @@ -34,7 +34,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.DefaultRepositoryExportingCheck; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryArchivedException; +import sonia.scm.repository.RepositoryExportingException; import sonia.scm.repository.spi.HttpScmProtocol; import sonia.scm.repository.spi.RepositoryServiceProvider; import sonia.scm.user.EMail; @@ -76,7 +79,7 @@ class RepositoryServiceTest { @Test void shouldReturnMatchingProtocolsFromProvider() { when(subject.getPrincipal()).thenReturn("Hitchhiker"); - RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail); + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null); Stream supportedProtocols = repositoryService.getSupportedProtocols(); assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1); @@ -85,7 +88,7 @@ class RepositoryServiceTest { @Test void shouldFilterOutNonAnonymousEnabledProtocolsForAnonymousUser() { when(subject.getPrincipal()).thenReturn(SCMContext.USER_ANONYMOUS); - RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Stream.of(new DummyScmProtocolProvider(), new DummyScmProtocolProvider(false)).collect(Collectors.toSet()), null, eMail); + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Stream.of(new DummyScmProtocolProvider(), new DummyScmProtocolProvider(false)).collect(Collectors.toSet()), null, eMail, null); Stream supportedProtocols = repositoryService.getSupportedProtocols(); assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1); @@ -94,7 +97,7 @@ class RepositoryServiceTest { @Test void shouldFindKnownProtocol() { when(subject.getPrincipal()).thenReturn("Hitchhiker"); - RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail); + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null); HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); @@ -104,7 +107,7 @@ class RepositoryServiceTest { @Test void shouldFailForUnknownProtocol() { when(subject.getPrincipal()).thenReturn("Hitchhiker"); - RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail); + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null); assertThrows(IllegalArgumentException.class, () -> repositoryService.getProtocol(UnknownScmProtocol.class)); } @@ -112,14 +115,29 @@ class RepositoryServiceTest { @Test void shouldFailForArchivedRepository() { repository.setArchived(true); - RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail); + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null); - assertThrows(RepositoryArchivedException.class, () -> repositoryService.getModifyCommand()); - assertThrows(RepositoryArchivedException.class, () -> repositoryService.getBranchCommand()); - assertThrows(RepositoryArchivedException.class, () -> repositoryService.getPullCommand()); - assertThrows(RepositoryArchivedException.class, () -> repositoryService.getTagCommand()); - assertThrows(RepositoryArchivedException.class, () -> repositoryService.getMergeCommand()); - assertThrows(RepositoryArchivedException.class, () -> repositoryService.getModifyCommand()); + assertThrows(RepositoryArchivedException.class, repositoryService::getModifyCommand); + assertThrows(RepositoryArchivedException.class, repositoryService::getBranchCommand); + assertThrows(RepositoryArchivedException.class, repositoryService::getPullCommand); + assertThrows(RepositoryArchivedException.class, repositoryService::getTagCommand); + assertThrows(RepositoryArchivedException.class, repositoryService::getMergeCommand); + assertThrows(RepositoryArchivedException.class, repositoryService::getModifyCommand); + } + + @Test + void shouldFailForExportingRepository() { + new DefaultRepositoryExportingCheck().withExportingLock(repository, () -> { + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider()), null, eMail, null); + + assertThrows(RepositoryExportingException.class, repositoryService::getModifyCommand); + assertThrows(RepositoryExportingException.class, repositoryService::getBranchCommand); + assertThrows(RepositoryExportingException.class, repositoryService::getPullCommand); + assertThrows(RepositoryExportingException.class, repositoryService::getTagCommand); + assertThrows(RepositoryExportingException.class, repositoryService::getMergeCommand); + assertThrows(RepositoryExportingException.class, repositoryService::getModifyCommand); + return null; + }); } private static class DummyHttpProtocol extends HttpScmProtocol { @@ -141,7 +159,7 @@ class RepositoryServiceTest { } } - private static class DummyScmProtocolProvider implements ScmProtocolProvider { + private static class DummyScmProtocolProvider implements ScmProtocolProvider { private final boolean anonymousEnabled; diff --git a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java index d99de42a91..2608f119ed 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java +++ b/scm-dao-xml/src/main/java/sonia/scm/repository/xml/XmlRepositoryDAO.java @@ -33,6 +33,7 @@ import sonia.scm.repository.InternalRepositoryException; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryExportingCheck; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.store.StoreReadOnlyException; @@ -54,14 +55,16 @@ public class XmlRepositoryDAO implements RepositoryDAO { private final PathBasedRepositoryLocationResolver repositoryLocationResolver; private final FileSystem fileSystem; + private final RepositoryExportingCheck repositoryExportingCheck; private final Map byId; private final Map byNamespaceAndName; @Inject - public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem) { + public XmlRepositoryDAO(PathBasedRepositoryLocationResolver repositoryLocationResolver, FileSystem fileSystem, RepositoryExportingCheck repositoryExportingCheck) { this.repositoryLocationResolver = repositoryLocationResolver; this.fileSystem = fileSystem; + this.repositoryExportingCheck = repositoryExportingCheck; this.byId = new ConcurrentHashMap<>(); this.byNamespaceAndName = new ConcurrentHashMap<>(); @@ -140,7 +143,7 @@ public class XmlRepositoryDAO implements RepositoryDAO { @Override public void modify(Repository repository) { Repository clone = repository.clone(); - if (clone.isArchived() && byId.get(clone.getId()).isArchived()) { + if (mustNotModifyRepository(clone)) { throw new StoreReadOnlyException(repository); } @@ -160,9 +163,14 @@ public class XmlRepositoryDAO implements RepositoryDAO { metadataStore.write(repositoryPath, clone); } + private boolean mustNotModifyRepository(Repository clone) { + return clone.isArchived() && byId.get(clone.getId()).isArchived() + || repositoryExportingCheck.isExporting(clone); + } + @Override public void delete(Repository repository) { - if (repository.isArchived()) { + if (repository.isArchived() || repositoryExportingCheck.isExporting(repository)) { throw new StoreReadOnlyException(repository); } Path path; diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java index 9d9520b75e..563203dde5 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBasedStoreFactory.java @@ -29,8 +29,8 @@ package sonia.scm.store; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; -import sonia.scm.repository.RepositoryArchivedCheck; import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.util.IOUtil; import java.io.File; @@ -52,13 +52,13 @@ public abstract class FileBasedStoreFactory { private final SCMContextProvider contextProvider; private final RepositoryLocationResolver repositoryLocationResolver; private final Store store; - private final RepositoryArchivedCheck archivedCheck; + private final RepositoryReadOnlyChecker readOnlyChecker; - protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store, RepositoryArchivedCheck archivedCheck) { + protected FileBasedStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, Store store, RepositoryReadOnlyChecker readOnlyChecker) { this.contextProvider = contextProvider; this.repositoryLocationResolver = repositoryLocationResolver; this.store = store; - this.archivedCheck = archivedCheck; + this.readOnlyChecker = readOnlyChecker; } protected File getStoreLocation(StoreParameters storeParameters) { @@ -83,12 +83,13 @@ public abstract class FileBasedStoreFactory { } protected boolean mustBeReadOnly(StoreParameters storeParameters) { - return storeParameters.getRepositoryId() != null && archivedCheck.isArchived(storeParameters.getRepositoryId()); + return storeParameters.getRepositoryId() != null && readOnlyChecker.isReadOnly(storeParameters.getRepositoryId()); } /** * Get the store directory of a specific repository - * @param store the type of the store + * + * @param store the type of the store * @param repositoryId the id of the repossitory * @return the store directory of a specific repository */ @@ -98,6 +99,7 @@ public abstract class FileBasedStoreFactory { /** * Get the global store directory + * * @param store the type of the store * @return the global store directory */ diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java index 45982a2c16..70d4e33943 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/FileBlobStoreFactory.java @@ -28,11 +28,9 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; -import sonia.scm.repository.RepositoryArchivedCheck; import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.security.KeyGenerator; import sonia.scm.util.IOUtil; @@ -46,11 +44,6 @@ import java.io.File; @Singleton public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobStoreFactory { - /** - * the logger for FileBlobStoreFactory - */ - private static final Logger LOG = LoggerFactory.getLogger(FileBlobStoreFactory.class); - private final KeyGenerator keyGenerator; /** @@ -60,8 +53,8 @@ public class FileBlobStoreFactory extends FileBasedStoreFactory implements BlobS * @param keyGenerator key generator */ @Inject - public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) { - super(contextProvider, repositoryLocationResolver, Store.BLOB, archivedCheck); + public FileBlobStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryReadOnlyChecker readOnlyChecker) { + super(contextProvider, repositoryLocationResolver, Store.BLOB, readOnlyChecker); this.keyGenerator = keyGenerator; } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java index 0727acac19..1b623ab1c7 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationEntryStoreFactory.java @@ -29,8 +29,8 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; import sonia.scm.SCMContextProvider; -import sonia.scm.repository.RepositoryArchivedCheck; import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.security.KeyGenerator; //~--- JDK imports ------------------------------------------------------------ @@ -46,8 +46,8 @@ public class JAXBConfigurationEntryStoreFactory extends FileBasedStoreFactory private KeyGenerator keyGenerator; @Inject - public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) { - super(contextProvider, repositoryLocationResolver, Store.CONFIG, archivedCheck); + public JAXBConfigurationEntryStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryReadOnlyChecker readOnlyChecker) { + super(contextProvider, repositoryLocationResolver, Store.CONFIG, readOnlyChecker); this.keyGenerator = keyGenerator; } diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java index 5f4a17c014..cbbb901fd7 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStoreFactory.java @@ -27,8 +27,8 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; import sonia.scm.SCMContextProvider; -import sonia.scm.repository.RepositoryArchivedCheck; import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryReadOnlyChecker; /** * JAXB implementation of {@link ConfigurationStoreFactory}. @@ -44,8 +44,8 @@ public class JAXBConfigurationStoreFactory extends FileBasedStoreFactory impleme * @param repositoryLocationResolver Resolver to get the repository Directory */ @Inject - public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, RepositoryArchivedCheck archivedCheck) { - super(contextProvider, repositoryLocationResolver, Store.CONFIG, archivedCheck); + public JAXBConfigurationStoreFactory(SCMContextProvider contextProvider, RepositoryLocationResolver repositoryLocationResolver, RepositoryReadOnlyChecker readOnlyChecker) { + super(contextProvider, repositoryLocationResolver, Store.CONFIG, readOnlyChecker); } @Override diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java index d0ec860221..d15a04d3d3 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBDataStoreFactory.java @@ -29,8 +29,8 @@ package sonia.scm.store; import com.google.inject.Inject; import com.google.inject.Singleton; import sonia.scm.SCMContextProvider; -import sonia.scm.repository.RepositoryArchivedCheck; import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.security.KeyGenerator; import sonia.scm.util.IOUtil; @@ -47,8 +47,8 @@ public class JAXBDataStoreFactory extends FileBasedStoreFactory private final KeyGenerator keyGenerator; @Inject - public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryArchivedCheck archivedCheck) { - super(contextProvider, repositoryLocationResolver, Store.DATA, archivedCheck); + public JAXBDataStoreFactory(SCMContextProvider contextProvider , RepositoryLocationResolver repositoryLocationResolver, KeyGenerator keyGenerator, RepositoryReadOnlyChecker readOnlyChecker) { + super(contextProvider, repositoryLocationResolver, Store.DATA, readOnlyChecker); this.keyGenerator = keyGenerator; } diff --git a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOSynchronizationTest.java b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOSynchronizationTest.java index 91db706a76..bddee516d9 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOSynchronizationTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/repository/xml/XmlRepositoryDAOSynchronizationTest.java @@ -36,6 +36,7 @@ import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; import sonia.scm.repository.InitialRepositoryLocationResolver; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryExportingCheck; import java.nio.file.Path; import java.util.concurrent.ExecutorService; @@ -54,6 +55,8 @@ class XmlRepositoryDAOSynchronizationTest { @Mock private SCMContextProvider provider; + @Mock + private RepositoryExportingCheck repositoryExportingCheck; private FileSystem fileSystem; private PathBasedRepositoryLocationResolver resolver; @@ -75,7 +78,7 @@ class XmlRepositoryDAOSynchronizationTest { provider, new InitialRepositoryLocationResolver(), fileSystem ); - repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem); + repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck); } @Test @@ -88,7 +91,7 @@ class XmlRepositoryDAOSynchronizationTest { } private void assertCreated() { - XmlRepositoryDAO assertionDao = new XmlRepositoryDAO(resolver, fileSystem); + XmlRepositoryDAO assertionDao = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck); assertThat(assertionDao.getAll()).hasSize(CREATION_COUNT); } @@ -97,7 +100,7 @@ class XmlRepositoryDAOSynchronizationTest { void shouldCreateALotOfRepositoriesInParallel() throws InterruptedException { ExecutorService executors = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - final XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem); + final XmlRepositoryDAO repositoryDAO = new XmlRepositoryDAO(resolver, fileSystem, repositoryExportingCheck); for (int i=0; i> triggeredOnForAllLocations = none -> {}; + @Mock + private RepositoryExportingCheck repositoryExportingCheck; - private FileSystem fileSystem = new DefaultFileSystem(); + private final FileSystem fileSystem = new DefaultFileSystem(); private XmlRepositoryDAO dao; @@ -120,7 +123,7 @@ class XmlRepositoryDAOTest { @BeforeEach void createDAO() { - dao = new XmlRepositoryDAO(locationResolver, fileSystem); + dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck); } @Test @@ -245,6 +248,15 @@ class XmlRepositoryDAOTest { assertThrows(StoreReadOnlyException.class, () -> dao.modify(heartOfGold)); } + @Test + void shouldNotModifyExportingRepository() { + when(repositoryExportingCheck.isExporting(REPOSITORY)).thenReturn(true); + dao.add(REPOSITORY); + + Repository heartOfGold = createRepository("42"); + assertThrows(StoreReadOnlyException.class, () -> dao.modify(heartOfGold)); + } + @Test void shouldRemoveRepository() { dao.add(REPOSITORY); @@ -268,6 +280,15 @@ class XmlRepositoryDAOTest { assertThrows(StoreReadOnlyException.class, () -> dao.delete(REPOSITORY)); } + @Test + void shouldNotRemoveExportingRepository() { + when(repositoryExportingCheck.isExporting(REPOSITORY)).thenReturn(true); + dao.add(REPOSITORY); + assertThat(dao.contains("42")).isTrue(); + + assertThrows(StoreReadOnlyException.class, () -> dao.delete(REPOSITORY)); + } + @Test void shouldRenameTheRepository() { dao.add(REPOSITORY); @@ -317,8 +338,9 @@ class XmlRepositoryDAOTest { dao.add(REPOSITORY); String content = getXmlFileContent(REPOSITORY.getId()); - assertThat(content).containsSubsequence("trillian", "read", "write"); - assertThat(content).containsSubsequence("vogons", "delete"); + assertThat(content) + .containsSubsequence("trillian", "read", "write") + .containsSubsequence("vogons", "delete"); } @Test @@ -372,7 +394,7 @@ class XmlRepositoryDAOTest { mockExistingPath(); // when - XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck); // then assertThat(dao.contains(new NamespaceAndName("space", "existing"))).isTrue(); @@ -383,7 +405,7 @@ class XmlRepositoryDAOTest { // given mockExistingPath(); - XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem); + XmlRepositoryDAO dao = new XmlRepositoryDAO(locationResolver, fileSystem, repositoryExportingCheck); // when dao.refresh(); diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java index 12d78d4c7c..b4333845f9 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/FileBlobStoreTest.java @@ -30,7 +30,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import sonia.scm.AbstractTestBase; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryArchivedCheck; +import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.repository.RepositoryTestData; import sonia.scm.security.UUIDKeyGenerator; @@ -51,8 +51,8 @@ import static org.mockito.Mockito.when; class FileBlobStoreTest extends AbstractTestBase { - private Repository repository = RepositoryTestData.createHeartOfGold(); - private RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class); + private final Repository repository = RepositoryTestData.createHeartOfGold(); + private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class); private BlobStore store; @BeforeEach @@ -191,7 +191,7 @@ class FileBlobStoreTest extends AbstractTestBase @BeforeEach void setRepositoryArchived() { store.create("1"); // store for test must not be empty - when(archivedCheck.isArchived(repository.getId())).thenReturn(true); + when(readOnlyChecker.isReadOnly(repository.getId())).thenReturn(true); createBlobStore(); } @@ -227,6 +227,6 @@ class FileBlobStoreTest extends AbstractTestBase protected BlobStoreFactory createBlobStoreFactory() { - return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), archivedCheck); + return new FileBlobStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), readOnlyChecker); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java index 5e130d76ad..9c417d0114 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBConfigurationStoreTest.java @@ -26,7 +26,7 @@ package sonia.scm.store; import org.junit.Test; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryArchivedCheck; +import sonia.scm.repository.RepositoryReadOnlyChecker; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -41,17 +41,16 @@ import static org.mockito.Mockito.when; */ public class JAXBConfigurationStoreTest extends StoreTestBase { - private final RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class); + private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class); @Override protected ConfigurationStoreFactory createStoreFactory() { - return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, archivedCheck); + return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, readOnlyChecker); } @Test - @SuppressWarnings("unchecked") public void shouldStoreAndLoadInRepository() { Repository repository = new Repository("id", "git", "ns", "n"); @@ -70,17 +69,17 @@ public class JAXBConfigurationStoreTest extends StoreTestBase { @Test - @SuppressWarnings("unchecked") public void shouldNotWriteArchivedRepository() { Repository repository = new Repository("id", "git", "ns", "n"); - when(archivedCheck.isArchived("id")).thenReturn(true); + when(readOnlyChecker.isReadOnly("id")).thenReturn(true); ConfigurationStore store = createStoreFactory() .withType(StoreObject.class) .withName("test") .forRepository(repository) .build(); - assertThrows(RuntimeException.class, () -> store.set(new StoreObject("value"))); + StoreObject storeObject = new StoreObject("value"); + assertThrows(RuntimeException.class, () -> store.set(storeObject)); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java index c32542a02f..f762502b82 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBDataStoreTest.java @@ -28,7 +28,7 @@ package sonia.scm.store; import org.junit.Test; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryArchivedCheck; +import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.security.UUIDKeyGenerator; import static org.junit.Assert.assertEquals; @@ -42,12 +42,12 @@ import static org.mockito.Mockito.when; */ public class JAXBDataStoreTest extends DataStoreTestBase { - private final RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class); + private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class); @Override protected DataStoreFactory createDataStoreFactory() { - return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), archivedCheck); + return new JAXBDataStoreFactory(contextProvider, repositoryLocationResolver, new UUIDKeyGenerator(), readOnlyChecker); } @Override @@ -80,7 +80,7 @@ public class JAXBDataStoreTest extends DataStoreTestBase { @Test(expected = StoreReadOnlyException.class) public void shouldNotStoreForReadOnlyRepository() { - when(archivedCheck.isArchived(repository.getId())).thenReturn(true); + when(readOnlyChecker.isReadOnly(repository.getId())).thenReturn(true); getDataStore(StoreObject.class, repository).put("abc", new StoreObject("abc_value")); } } diff --git a/scm-dao-xml/src/test/java/sonia/scm/update/xml/XmlV1PropertyDAOTest.java b/scm-dao-xml/src/test/java/sonia/scm/update/xml/XmlV1PropertyDAOTest.java index 68cfe7ada3..0dbfed6288 100644 --- a/scm-dao-xml/src/test/java/sonia/scm/update/xml/XmlV1PropertyDAOTest.java +++ b/scm-dao-xml/src/test/java/sonia/scm/update/xml/XmlV1PropertyDAOTest.java @@ -29,7 +29,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import sonia.scm.SCMContextProvider; import sonia.scm.Stage; -import sonia.scm.repository.RepositoryArchivedCheck; +import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.security.KeyGenerator; import sonia.scm.store.JAXBConfigurationEntryStoreFactory; import sonia.scm.update.RepositoryV1PropertyReader; @@ -111,8 +111,8 @@ class XmlV1PropertyDAOTest { Files.createDirectories(configPath); Path propFile = configPath.resolve("repository-properties-v1.xml"); Files.write(propFile, PROPERTIES.getBytes()); - RepositoryArchivedCheck archivedCheck = mock(RepositoryArchivedCheck.class); - XmlV1PropertyDAO dao = new XmlV1PropertyDAO(new JAXBConfigurationEntryStoreFactory(new SimpleContextProvider(temp), null, new SimpleKeyGenerator(), archivedCheck)); + RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class); + XmlV1PropertyDAO dao = new XmlV1PropertyDAO(new JAXBConfigurationEntryStoreFactory(new SimpleContextProvider(temp), null, new SimpleKeyGenerator(), readOnlyChecker)); dao.getProperties(new RepositoryV1PropertyReader()) .forEachEntry((key, prop) -> { diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index b812b332ec..7437e6b518 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -48475,7 +48475,7 @@ exports[`Storyshots RepositoryEntry Archived 1`] = ` repository.archived @@ -49023,6 +49023,314 @@ exports[`Storyshots RepositoryEntry Default 1`] = ` `; +exports[`Storyshots RepositoryEntry Exporting 1`] = ` + +`; + +exports[`Storyshots RepositoryEntry MultiRepositoryTags 1`] = ` + +`; + exports[`Storyshots RepositoryEntry Quick Link EP 1`] = `
{story()}) @@ -101,4 +103,14 @@ storiesOf("RepositoryEntry", module) const binder = new Binder("title"); bindAvatar(binder, Git); return withBinder(binder, archivedRepository); + }) + .add("Exporting", () => { + const binder = new Binder("title"); + bindAvatar(binder, Git); + return withBinder(binder, exportingRepository); + }) + .add("MultiRepositoryTags", () => { + const binder = new Binder("title"); + bindAvatar(binder, Git); + return withBinder(binder, archivedExportingRepository); }); diff --git a/scm-ui/ui-components/src/repos/RepositoryEntry.tsx b/scm-ui/ui-components/src/repos/RepositoryEntry.tsx index 80b5f12c3c..b0661e23b6 100644 --- a/scm-ui/ui-components/src/repos/RepositoryEntry.tsx +++ b/scm-ui/ui-components/src/repos/RepositoryEntry.tsx @@ -39,7 +39,7 @@ type Props = WithTranslation & { baseDate?: DateProp; }; -const ArchiveTag = styled.span` +const RepositoryTag = styled.span` margin-left: 0.2rem; background-color: #9a9a9a; padding: 0.25rem; @@ -145,13 +145,19 @@ class RepositoryEntry extends React.Component { createTitle = () => { const { repository, t } = this.props; - const archivedFlag = repository.archived && ( - {t("repository.archived")} - ); + const repositoryFlags = []; + if (repository.archived) { + repositoryFlags.push({t("repository.archived")}); + } + + if (repository.exporting) { + repositoryFlags.push({t("repository.exporting")}); + } + return ( <> - {repository.name} {archivedFlag} + {repository.name} {repositoryFlags.map(flag => flag)} ); }; diff --git a/scm-ui/ui-types/src/Repositories.ts b/scm-ui/ui-types/src/Repositories.ts index 697226492d..53a8e3478c 100644 --- a/scm-ui/ui-types/src/Repositories.ts +++ b/scm-ui/ui-types/src/Repositories.ts @@ -33,6 +33,7 @@ export type Repository = { creationDate?: string; lastModified?: string; archived?: boolean; + exporting?: boolean; _links: Links; }; diff --git a/scm-ui/ui-webapp/public/locales/de/repos.json b/scm-ui/ui-webapp/public/locales/de/repos.json index a5f60986f7..99c9e22f48 100644 --- a/scm-ui/ui-webapp/public/locales/de/repos.json +++ b/scm-ui/ui-webapp/public/locales/de/repos.json @@ -7,7 +7,8 @@ "description": "Beschreibung", "creationDate": "Erstellt", "lastModified": "Zuletzt bearbeitet", - "archived": "archiviert" + "archived": "archiviert", + "exporting": "Wird exportiert" }, "validation": { "namespace-invalid": "Der Namespace des Repository ist ungültig", @@ -252,6 +253,7 @@ }, "export": { "subtitle": "Repository exportieren", + "notification": "Achtung: Während eines laufenden Exports kann auf das Repository nur lesend zugegriffen werden.", "compressed": { "label": "Komprimieren", "helpText": "Export Datei vor dem Download komprimieren. Reduziert die Downloadgröße." @@ -399,6 +401,9 @@ "archive": { "tooltip": "Nur lesender Zugriff möglich. Das Archiv kann nicht verändert werden." }, + "exporting": { + "tooltip": "Nur lesender Zugriff möglich. Das Repository wird derzeit exportiert." + }, "diff": { "jumpToSource": "Zur Quelldatei springen", "jumpToTarget": "Zur vorherigen Version der Datei springen", diff --git a/scm-ui/ui-webapp/public/locales/en/repos.json b/scm-ui/ui-webapp/public/locales/en/repos.json index 1566237066..a6b68dec05 100644 --- a/scm-ui/ui-webapp/public/locales/en/repos.json +++ b/scm-ui/ui-webapp/public/locales/en/repos.json @@ -7,7 +7,8 @@ "description": "Description", "creationDate": "Creation Date", "lastModified": "Last Modified", - "archived": "archived" + "archived": "archived", + "exporting": "exporting" }, "validation": { "namespace-invalid": "The repository namespace is invalid", @@ -252,6 +253,7 @@ }, "export": { "subtitle": "Repository Export", + "notification": "Attention: During the export the repository cannot be modified.", "compressed": { "label": "Compress", "helpText": "Compress the export dump size to reduce the download size." @@ -399,6 +401,9 @@ "archive": { "tooltip": "Read only. The archive cannot be changed." }, + "exporting": { + "tooltip": "Read only. The repository is currently being exported." + }, "diff": { "changes": { "add": "added", diff --git a/scm-ui/ui-webapp/src/repos/containers/ExportRepository.tsx b/scm-ui/ui-webapp/src/repos/containers/ExportRepository.tsx index c2e897b505..d3de37f35d 100644 --- a/scm-ui/ui-webapp/src/repos/containers/ExportRepository.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/ExportRepository.tsx @@ -56,6 +56,9 @@ const ExportRepository: FC = ({ repository }) => { <>
+ + {t("export.notification")} + <> void; }; -const ArchiveTag = styled.span` +const RepositoryTag = styled.span` margin-left: 0.2rem; background-color: #9a9a9a; padding: 0.4rem; @@ -153,7 +153,7 @@ class RepositoryRoot extends React.Component { const extensionProps = { repository, url, - indexLinks, + indexLinks }; const redirectUrlFactory = binder.getExtension("repository.redirect", this.props); @@ -164,16 +164,16 @@ class RepositoryRoot extends React.Component { redirectedUrl = url + "/info"; } - const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = (changeset) => (file) => { + const fileControlFactoryFactory: (changeset: Changeset) => FileControlFactory = changeset => file => { const baseUrl = `${url}/code/sources`; const sourceLink = file.newPath && { url: `${baseUrl}/${changeset.id}/${file.newPath}/`, - label: t("diff.jumpToSource"), + label: t("diff.jumpToSource") }; const targetLink = file.oldPath && changeset._embedded?.parents?.length === 1 && { url: `${baseUrl}/${changeset._embedded.parents[0].id}/${file.oldPath}`, - label: t("diff.jumpToTarget"), + label: t("diff.jumpToTarget") }; const links = []; @@ -199,11 +199,22 @@ class RepositoryRoot extends React.Component { return links ? links.map(({ url, label }) => ) : null; }; - const archivedFlag = repository.archived && ( - - {t("repository.archived")} - - ); + const repositoryFlags = []; + if (repository.archived) { + repositoryFlags.push( + + {t("repository.archived")} + + ); + } + + if (repository.exporting) { + repositoryFlags.push( + + {t("repository.exporting")} + + ); + } const titleComponent = ( <> @@ -222,7 +233,7 @@ class RepositoryRoot extends React.Component { afterTitle={ <> - {archivedFlag} + {repositoryFlags.map(flag => flag)} } > @@ -360,7 +371,7 @@ const mapStateToProps = (state: any, ownProps: Props) => { loading, error, repoLink, - indexLinks, + indexLinks }; }; @@ -368,7 +379,7 @@ const mapDispatchToProps = (dispatch: any) => { return { fetchRepoByName: (link: string, namespace: string, name: string) => { dispatch(fetchRepoByName(link, namespace, name)); - }, + } }; }; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index e4580c2c7a..cc5ca973ea 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -58,6 +58,7 @@ public class RepositoryDto extends HalRepresentation implements CreateRepository @NotEmpty private String type; private boolean archived; + private boolean exporting; RepositoryDto(Links links, Embedded embedded) { super(links, embedded); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java index 2ffec2cd60..577b9d3a55 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryExportResource.java @@ -161,12 +161,7 @@ public class RepositoryExportResource { @PathParam("name") String name ) { Repository repository = getVerifiedRepository(namespace, name); - StreamingOutput output = os -> fullScmRepositoryExporter.export(repository, os); - - return Response - .ok(output, "application/x-gzip") - .header("content-disposition", createContentDispositionHeaderValue(repository, "tar.gz")) - .build(); + return exportFullRepository(repository); } private Repository getVerifiedRepository(String namespace, String name) { @@ -186,6 +181,15 @@ public class RepositoryExportResource { return repository; } + private Response exportFullRepository(Repository repository) { + StreamingOutput output = os -> fullScmRepositoryExporter.export(repository, os); + + return Response + .ok(output, "application/x-gzip") + .header("content-disposition", createContentDispositionHeaderValue(repository, "tar.gz")) + .build(); + } + private Response exportRepository(Repository repository, boolean compressed) { StreamingOutput output; String fileExtension; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index 8f9a617ae1..8a275e60e7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -27,9 +27,12 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; import org.mapstruct.ObjectFactory; import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.DefaultRepositoryExportingCheck; import sonia.scm.repository.Feature; import sonia.scm.repository.HealthCheckFailure; import sonia.scm.repository.NamespaceStrategy; @@ -70,6 +73,11 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper { + exportInLock(repository, outputStream); + return null; + }); + } + + private void exportInLock(Repository repository, OutputStream outputStream) { try ( RepositoryService service = serviceFactory.create(repository); BufferedOutputStream bos = new BufferedOutputStream(outputStream); GzipCompressorOutputStream gzos = new GzipCompressorOutputStream(bos); - TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos); + TarArchiveOutputStream taos = new TarArchiveOutputStream(gzos) ) { writeEnvironmentData(taos); writeMetadata(repository, taos); diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java index 83c95fa4ff..cfea4befff 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java @@ -36,8 +36,10 @@ import sonia.scm.io.FileSystem; import sonia.scm.lifecycle.DefaultRestarter; import sonia.scm.lifecycle.Restarter; import sonia.scm.plugin.PluginLoader; +import sonia.scm.repository.DefaultRepositoryExportingCheck; import sonia.scm.repository.EventDrivenRepositoryArchiveCheck; import sonia.scm.repository.RepositoryArchivedCheck; +import sonia.scm.repository.RepositoryExportingCheck; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.repository.xml.MetadataStore; import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; @@ -100,6 +102,7 @@ public class BootstrapModule extends AbstractModule { // bind core bind(RepositoryArchivedCheck.class, EventDrivenRepositoryArchiveCheck.class); + bind(RepositoryExportingCheck.class, DefaultRepositoryExportingCheck.class); bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class); bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class); bind(DataStoreFactory.class, JAXBDataStoreFactory.class); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 776c000f7d..eca8c5107d 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -39,7 +39,6 @@ import sonia.scm.NoChangesMadeException; import sonia.scm.NotFoundException; import sonia.scm.SCMContextProvider; import sonia.scm.Type; -import sonia.scm.config.ScmConfiguration; import sonia.scm.event.ScmEventBus; import sonia.scm.security.AuthorizationChangedEvent; import sonia.scm.security.KeyGenerator; @@ -79,7 +78,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { private static final String THREAD_NAME = "Hook-%s"; private static final Logger logger = LoggerFactory.getLogger(DefaultRepositoryManager.class); - private final ScmConfiguration configuration; private final ExecutorService executorService; private final Map handlerMap; private final KeyGenerator keyGenerator; @@ -89,11 +87,9 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { private final ManagerDaoAdapter managerDaoAdapter; @Inject - public DefaultRepositoryManager(ScmConfiguration configuration, - SCMContextProvider contextProvider, KeyGenerator keyGenerator, + public DefaultRepositoryManager(SCMContextProvider contextProvider, KeyGenerator keyGenerator, RepositoryDAO repositoryDAO, Set handlerSet, Provider namespaceStrategyProvider) { - this.configuration = configuration; this.keyGenerator = keyGenerator; this.repositoryDAO = repositoryDAO; this.namespaceStrategyProvider = namespaceStrategyProvider; diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index ee588f05f2..8faf0f1ae5 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -326,6 +326,10 @@ "4hSNNTBiu1": { "displayName": "Falscher Repository Typ", "description": "Der gegebene Typ entspricht nicht dem Typen des Repositories." + }, + "1mSNlpe1V1": { + "displayName": "Repository wird exportiert", + "description": "Das Repository wird momentan exportiert und darf nicht modifiziert werden." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 5eee2e48a2..79d912d1a8 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -326,6 +326,10 @@ "4hSNNTBiu1": { "displayName": "Wrong repository type", "description": "The given type does not match the type of the repository." + }, + "1mSNlpe1V1": { + "displayName": "Repository is being exported", + "description": "The repository is being exported and therefore must not be modified." } }, "namespaceStrategies": { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index a69581e7fa..4d104cb828 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -88,6 +88,7 @@ import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.function.Supplier; import static java.util.Collections.singletonList; import static java.util.stream.Stream.of; @@ -104,23 +105,24 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyMap; -import static org.mockito.ArgumentMatchers.anyObject; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.RETURNS_SELF; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.MockitoAnnotations.initMocks; +import static org.mockito.MockitoAnnotations.openMocks; @SubjectAware( username = "trillian", password = "secret", configuration = "classpath:sonia/scm/repository/shiro.ini" ) +@SuppressWarnings("UnstableApiUsage") public class RepositoryRootResourceTest extends RepositoryTestBase { private static final String REALM = "AdminRealm"; @@ -160,6 +162,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + private Repository repositoryMarkedAsExported; @InjectMocks private RepositoryToRepositoryDtoMapperImpl repositoryToDtoMapper; @@ -168,7 +171,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Before public void prepareEnvironment() { - initMocks(this); + openMocks(this); super.repositoryToDtoMapper = repositoryToDtoMapper; super.dtoToRepositoryMapper = dtoToRepositoryMapper; super.manager = repositoryManager; @@ -316,7 +319,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { dispatcher.invoke(request, response); assertEquals(SC_NO_CONTENT, response.getStatus()); - verify(repositoryManager).modify(anyObject()); + verify(repositoryManager).modify(any()); } @Test @@ -336,7 +339,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertEquals(SC_CONFLICT, response.getStatus()); assertThat(response.getContentAsString()).contains("space/repo"); - verify(repositoryManager, never()).modify(anyObject()); + verify(repositoryManager, never()).modify(any()); } @Test @@ -355,7 +358,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { dispatcher.invoke(request, response); assertEquals(SC_BAD_REQUEST, response.getStatus()); - verify(repositoryManager, never()).modify(anyObject()); + verify(repositoryManager, never()).modify(any()); } @Test @@ -368,7 +371,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { dispatcher.invoke(request, response); assertEquals(SC_NO_CONTENT, response.getStatus()); - verify(repositoryManager).delete(anyObject()); + verify(repositoryManager).delete(any()); } @Test @@ -826,7 +829,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { /** * This method is a slightly adapted copy of Lin Zaho's gist at https://gist.github.com/lin-zhao/9985191 */ - private MockHttpRequest multipartRequest(MockHttpRequest request, Map files, RepositoryDto repository) throws IOException { + private void multipartRequest(MockHttpRequest request, Map files, RepositoryDto repository) throws IOException { String boundary = UUID.randomUUID().toString(); request.contentType("multipart/form-data; boundary=" + boundary); @@ -864,6 +867,5 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { formWriter.flush(); } request.setInputStream(new ByteArrayInputStream(buffer.toByteArray())); - return request; } } diff --git a/scm-webapp/src/test/java/sonia/scm/importexport/FullScmRepositoryExporterTest.java b/scm-webapp/src/test/java/sonia/scm/importexport/FullScmRepositoryExporterTest.java index 5f4d12bbeb..33cd4ed0b5 100644 --- a/scm-webapp/src/test/java/sonia/scm/importexport/FullScmRepositoryExporterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/importexport/FullScmRepositoryExporterTest.java @@ -33,6 +33,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryExportingCheck; import sonia.scm.repository.RepositoryTestData; import sonia.scm.repository.api.BundleCommandBuilder; import sonia.scm.repository.api.RepositoryService; @@ -47,6 +48,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -70,6 +72,8 @@ class FullScmRepositoryExporterTest { private TarArchiveRepositoryStoreExporter storeExporter; @Mock private WorkdirProvider workdirProvider; + @Mock + private RepositoryExportingCheck repositoryExportingCheck; @InjectMocks private FullScmRepositoryExporter exporter; @@ -81,6 +85,7 @@ class FullScmRepositoryExporterTest { when(serviceFactory.create(REPOSITORY)).thenReturn(repositoryService); when(environmentGenerator.generate()).thenReturn(new byte[0]); when(metadataGenerator.generate(REPOSITORY)).thenReturn(new byte[0]); + when(repositoryExportingCheck.withExportingLock(any(), any())).thenAnswer(invocation -> invocation.getArgument(1, Supplier.class).get()); } @Test @@ -96,6 +101,7 @@ class FullScmRepositoryExporterTest { verify(environmentGenerator, times(1)).generate(); verify(metadataGenerator, times(1)).generate(REPOSITORY); verify(bundleCommandBuilder, times(1)).bundle(any(OutputStream.class)); + verify(repositoryExportingCheck).withExportingLock(eq(REPOSITORY), any()); workDirsCreated.forEach(wd -> assertThat(wd).doesNotExist()); } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index de4f551f3a..a15bfe4ae9 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -108,8 +108,7 @@ public class DefaultRepositoryManagerPerfTest { Set handlerSet = ImmutableSet.of(repositoryHandler); NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class); repositoryManager = new DefaultRepositoryManager( - configuration, - contextProvider, + contextProvider, keyGenerator, repositoryDAO, handlerSet, diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index d747a454be..509d5f5d1e 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -65,7 +65,6 @@ import java.util.Map; import java.util.Set; import java.util.Stack; -import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasProperty; @@ -109,7 +108,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { private RepositoryDAO repositoryDAO; - { + static { ThreadContext.unbindSubject(); } @@ -121,8 +120,6 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { private NamespaceStrategy namespaceStrategy = mock(NamespaceStrategy.class); - private ScmConfiguration configuration; - private String mockedNamespace = "default_namespace"; @Before @@ -552,11 +549,9 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { handlerSet.add(createRepositoryHandler("hg", "Mercurial")); handlerSet.add(createRepositoryHandler("svn", "SVN")); - this.configuration = new ScmConfiguration(); - when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace); - return new DefaultRepositoryManager(configuration, contextProvider, + return new DefaultRepositoryManager(contextProvider, keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy)); } diff --git a/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java index 11ac8854c2..7e7179198e 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/UpdateEngineTest.java @@ -21,13 +21,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.update; import org.junit.jupiter.api.Test; import sonia.scm.migration.UpdateStep; import sonia.scm.store.ConfigurationEntryStoreFactory; -import sonia.scm.store.InMemoryConfigurationEntryStore; import sonia.scm.store.InMemoryConfigurationEntryStoreFactory; import sonia.scm.version.Version;