diff --git a/gradle/changelog/delete_config_store.yaml b/gradle/changelog/delete_config_store.yaml new file mode 100644 index 0000000000..415a4c23b1 --- /dev/null +++ b/gradle/changelog/delete_config_store.yaml @@ -0,0 +1,4 @@ +- type: Added + description: Add method to delete whole configuration store ([#1814](https://github.com/scm-manager/scm-manager/pull/1814)) +- type: Added + description: Move DangerZone styling to ui-components (([#1814](https://github.com/scm-manager/scm-manager/pull/1814))) diff --git a/scm-core/src/main/java/sonia/scm/store/AbstractStore.java b/scm-core/src/main/java/sonia/scm/store/AbstractStore.java index 73f9d7a1c3..0fa44b218f 100644 --- a/scm-core/src/main/java/sonia/scm/store/AbstractStore.java +++ b/scm-core/src/main/java/sonia/scm/store/AbstractStore.java @@ -29,10 +29,9 @@ import java.util.function.BooleanSupplier; /** * Base class for {@link ConfigurationStore}. * + * @param type of store objects * @author Sebastian Sdorra * @since 1.16 - * - * @param type of store objects */ public abstract class AbstractStore implements ConfigurationStore { @@ -64,10 +63,19 @@ public abstract class AbstractStore implements ConfigurationStore { this.storeObject = object; } + + @Override + public void delete() { + if (readOnly.getAsBoolean()) { + throw new StoreReadOnlyException(); + } + deleteObject(); + this.storeObject = null; + } + /** * Read the stored object. * - * * @return stored object */ protected abstract T readObject(); @@ -75,8 +83,14 @@ public abstract class AbstractStore implements ConfigurationStore { /** * Write object to the store. * - * * @param object object to write */ protected abstract void writeObject(T object); + + /** + * Deletes store object. + * + * @since 2.24.0 + */ + protected abstract void deleteObject(); } diff --git a/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java b/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java index 35cc40642f..5dbd6b58bc 100644 --- a/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java +++ b/scm-core/src/main/java/sonia/scm/store/ConfigurationStore.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.store; import java.util.Optional; @@ -32,17 +32,14 @@ import static java.util.Optional.ofNullable; * ConfigurationStore for configuration objects. Note: the default * implementation use JAXB to marshall the configuration objects. * - * @author Sebastian Sdorra - * * @param type of the configuration objects + * @author Sebastian Sdorra */ -public interface ConfigurationStore -{ +public interface ConfigurationStore { /** * Returns the configuration object from store. * - * * @return configuration object from store */ T get(); @@ -50,20 +47,24 @@ public interface ConfigurationStore /** * Returns the configuration object from store. * - * * @return configuration object from store */ default Optional getOptional() { return ofNullable(get()); } - //~--- set methods ---------------------------------------------------------- - /** * Stores the given configuration object to the store. * - * * @param object configuration object to store */ void set(T object); + + /** + * Deletes the configuration. + * @since 2.24.0 + */ + default void delete() { + throw new StoreException("Delete operation is not implemented by the store"); + } } diff --git a/scm-core/src/main/java/sonia/scm/store/StoreReadOnlyException.java b/scm-core/src/main/java/sonia/scm/store/StoreReadOnlyException.java index afc82e7259..6cc6bd0a1d 100644 --- a/scm-core/src/main/java/sonia/scm/store/StoreReadOnlyException.java +++ b/scm-core/src/main/java/sonia/scm/store/StoreReadOnlyException.java @@ -46,6 +46,11 @@ public class StoreReadOnlyException extends ExceptionWithContext { LOG.error(getMessage()); } + public StoreReadOnlyException() { + super(noContext(), "Store is read only, could not delete store"); + LOG.error(getMessage()); + } + @Override public String getCode () { return CODE; diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java index 1b0de60299..caf55ea046 100644 --- a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBConfigurationStore.java @@ -26,8 +26,10 @@ package sonia.scm.store; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.util.IOUtil; import java.io.File; +import java.io.IOException; import java.util.function.BooleanSupplier; /** @@ -64,7 +66,6 @@ public class JAXBConfigurationStore extends AbstractStore { } @Override - @SuppressWarnings("unchecked") protected T readObject() { LOG.debug("load {} from store {}", type, configFile); @@ -82,4 +83,14 @@ public class JAXBConfigurationStore extends AbstractStore { configFile.toPath() ); } + + @Override + protected void deleteObject() { + LOG.debug("deletes {}", configFile.getPath()); + try { + IOUtil.delete(configFile); + } catch (IOException e) { + throw new StoreException("Failed to delete store object " + configFile.getPath(), e); + } + } } 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 9c417d0114..6d4fea3816 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 @@ -28,6 +28,7 @@ import org.junit.Test; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryReadOnlyChecker; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; @@ -44,15 +45,13 @@ public class JAXBConfigurationStoreTest extends StoreTestBase { private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class); @Override - protected ConfigurationStoreFactory createStoreFactory() - { + protected JAXBConfigurationStoreFactory createStoreFactory() { return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, readOnlyChecker); } @Test - public void shouldStoreAndLoadInRepository() - { + public void shouldStoreAndLoadInRepository() { Repository repository = new Repository("id", "git", "ns", "n"); ConfigurationStore store = createStoreFactory() .withType(StoreObject.class) @@ -69,8 +68,7 @@ public class JAXBConfigurationStoreTest extends StoreTestBase { @Test - public void shouldNotWriteArchivedRepository() - { + public void shouldNotWriteArchivedRepository() { Repository repository = new Repository("id", "git", "ns", "n"); when(readOnlyChecker.isReadOnly("id")).thenReturn(true); ConfigurationStore store = createStoreFactory() @@ -82,4 +80,38 @@ public class JAXBConfigurationStoreTest extends StoreTestBase { StoreObject storeObject = new StoreObject("value"); assertThrows(RuntimeException.class, () -> store.set(storeObject)); } + + @Test + public void shouldDeleteConfigStore() { + Repository repository = new Repository("id", "git", "ns", "n"); + ConfigurationStore store = createStoreFactory() + .withType(StoreObject.class) + .withName("test") + .forRepository(repository) + .build(); + + store.set(new StoreObject("value")); + + store.delete(); + StoreObject storeObject = store.get(); + + assertThat(storeObject).isNull(); + } + + @Test + public void shouldNotDeleteStoreForArchivedRepository() { + Repository repository = new Repository("id", "git", "ns", "n"); + when(readOnlyChecker.isReadOnly("id")).thenReturn(false); + ConfigurationStore store = createStoreFactory() + .withType(StoreObject.class) + .withName("test") + .forRepository(repository) + .build(); + + store.set(new StoreObject()); + when(readOnlyChecker.isReadOnly("id")).thenReturn(true); + + assertThrows(StoreReadOnlyException.class, store::delete); + assertThat(store.getOptional()).isPresent(); + } } diff --git a/scm-ui/ui-components/src/DangerZone.tsx b/scm-ui/ui-components/src/DangerZone.tsx new file mode 100644 index 0000000000..8c94135558 --- /dev/null +++ b/scm-ui/ui-components/src/DangerZone.tsx @@ -0,0 +1,49 @@ +/* + * 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. + */ + +import styled from "styled-components"; + +export const DangerZone = styled.div` + border: 1px solid #ff6a88; + border-radius: 5px; + + > .level { + flex-flow: wrap; + + .level-left { + max-width: 100%; + } + + .level-right { + margin-top: 0.75rem; + } + } + + > *:not(:last-child) { + padding-bottom: 1.5rem; + border-bottom: solid 2px whitesmoke; + } +`; + +export default DangerZone; diff --git a/scm-ui/ui-components/src/index.ts b/scm-ui/ui-components/src/index.ts index 1ba854f193..23db2f91a3 100644 --- a/scm-ui/ui-components/src/index.ts +++ b/scm-ui/ui-components/src/index.ts @@ -57,6 +57,7 @@ export { default as Paginator } from "./Paginator"; export { default as LinkPaginator } from "./LinkPaginator"; export { default as StatePaginator } from "./StatePaginator"; +export { default as DangerZone } from "./DangerZone"; export { default as FileSize } from "./FileSize"; export { default as ProtectedRoute } from "./ProtectedRoute"; export { default as Help } from "./Help"; diff --git a/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx b/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx index 6c761a6691..d47d4ec936 100644 --- a/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx +++ b/scm-ui/ui-webapp/src/repos/branches/containers/BranchDangerZone.tsx @@ -24,9 +24,8 @@ import React, { FC } from "react"; import { Branch, Repository } from "@scm-manager/ui-types"; -import { Subtitle } from "@scm-manager/ui-components"; +import { DangerZone, Subtitle } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; -import { DangerZoneContainer } from "../../containers/RepositoryDangerZone"; import DeleteBranch from "./DeleteBranch"; type Props = { @@ -51,7 +50,7 @@ const BranchDangerZone: FC = ({ repository, branch }) => { <>
- {dangerZone} + {dangerZone} ); }; diff --git a/scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx b/scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx index 11f488d046..e6cec43a85 100644 --- a/scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx +++ b/scm-ui/ui-webapp/src/repos/containers/RepositoryDangerZone.tsx @@ -23,9 +23,8 @@ */ import React, { FC } from "react"; import { useTranslation } from "react-i18next"; -import styled from "styled-components"; import { Repository } from "@scm-manager/ui-types"; -import { Subtitle } from "@scm-manager/ui-components"; +import { DangerZone, Subtitle } from "@scm-manager/ui-components"; import RenameRepository from "./RenameRepository"; import DeleteRepo from "./DeleteRepo"; import ArchiveRepo from "./ArchiveRepo"; @@ -35,28 +34,6 @@ type Props = { repository: Repository; }; -export const DangerZoneContainer = styled.div` - border: 1px solid #ff6a88; - border-radius: 5px; - - > .level { - flex-flow: wrap; - - .level-left { - max-width: 100%; - } - - .level-right { - margin-top: 0.75rem; - } - } - - > *:not(:last-child) { - padding-bottom: 1.5rem; - border-bottom: solid 2px whitesmoke; - } -`; - const RepositoryDangerZone: FC = ({ repository }) => { const [t] = useTranslation("repos"); @@ -81,7 +58,7 @@ const RepositoryDangerZone: FC = ({ repository }) => { <>
- {dangerZone} + {dangerZone} ); }; diff --git a/scm-ui/ui-webapp/src/repos/tags/container/TagDangerZone.tsx b/scm-ui/ui-webapp/src/repos/tags/container/TagDangerZone.tsx index ec8dd79153..15f76a3e5d 100644 --- a/scm-ui/ui-webapp/src/repos/tags/container/TagDangerZone.tsx +++ b/scm-ui/ui-webapp/src/repos/tags/container/TagDangerZone.tsx @@ -24,9 +24,8 @@ import React, { FC } from "react"; import { Repository, Tag } from "@scm-manager/ui-types"; -import { Subtitle } from "@scm-manager/ui-components"; +import { DangerZone, Subtitle } from "@scm-manager/ui-components"; import { useTranslation } from "react-i18next"; -import { DangerZoneContainer } from "../../containers/RepositoryDangerZone"; import DeleteTag from "./DeleteTag"; type Props = { @@ -51,7 +50,7 @@ const TagDangerZone: FC = ({ repository, tag }) => { <>
- {dangerZone} + {dangerZone} ); };