Add delete method for configuration store (#1814)

Add method to delete configuration stores completely.
This commit is contained in:
Eduard Heimbuch
2021-09-30 08:54:22 +02:00
committed by GitHub
parent f6de626cd5
commit 922dc27c49
11 changed files with 144 additions and 52 deletions

View File

@@ -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)))

View File

@@ -29,10 +29,9 @@ import java.util.function.BooleanSupplier;
/** /**
* Base class for {@link ConfigurationStore}. * Base class for {@link ConfigurationStore}.
* *
* @param <T> type of store objects
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 1.16 * @since 1.16
*
* @param <T> type of store objects
*/ */
public abstract class AbstractStore<T> implements ConfigurationStore<T> { public abstract class AbstractStore<T> implements ConfigurationStore<T> {
@@ -64,10 +63,19 @@ public abstract class AbstractStore<T> implements ConfigurationStore<T> {
this.storeObject = object; this.storeObject = object;
} }
@Override
public void delete() {
if (readOnly.getAsBoolean()) {
throw new StoreReadOnlyException();
}
deleteObject();
this.storeObject = null;
}
/** /**
* Read the stored object. * Read the stored object.
* *
*
* @return stored object * @return stored object
*/ */
protected abstract T readObject(); protected abstract T readObject();
@@ -75,8 +83,14 @@ public abstract class AbstractStore<T> implements ConfigurationStore<T> {
/** /**
* Write object to the store. * Write object to the store.
* *
*
* @param object object to write * @param object object to write
*/ */
protected abstract void writeObject(T object); protected abstract void writeObject(T object);
/**
* Deletes store object.
*
* @since 2.24.0
*/
protected abstract void deleteObject();
} }

View File

@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
package sonia.scm.store; package sonia.scm.store;
import java.util.Optional; import java.util.Optional;
@@ -32,17 +32,14 @@ import static java.util.Optional.ofNullable;
* ConfigurationStore for configuration objects. <strong>Note:</strong> the default * ConfigurationStore for configuration objects. <strong>Note:</strong> the default
* implementation use JAXB to marshall the configuration objects. * implementation use JAXB to marshall the configuration objects.
* *
* @author Sebastian Sdorra
*
* @param <T> type of the configuration objects * @param <T> type of the configuration objects
* @author Sebastian Sdorra
*/ */
public interface ConfigurationStore<T> public interface ConfigurationStore<T> {
{
/** /**
* Returns the configuration object from store. * Returns the configuration object from store.
* *
*
* @return configuration object from store * @return configuration object from store
*/ */
T get(); T get();
@@ -50,20 +47,24 @@ public interface ConfigurationStore<T>
/** /**
* Returns the configuration object from store. * Returns the configuration object from store.
* *
*
* @return configuration object from store * @return configuration object from store
*/ */
default Optional<T> getOptional() { default Optional<T> getOptional() {
return ofNullable(get()); return ofNullable(get());
} }
//~--- set methods ----------------------------------------------------------
/** /**
* Stores the given configuration object to the store. * Stores the given configuration object to the store.
* *
*
* @param object configuration object to store * @param object configuration object to store
*/ */
void set(T object); 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");
}
} }

View File

@@ -46,6 +46,11 @@ public class StoreReadOnlyException extends ExceptionWithContext {
LOG.error(getMessage()); LOG.error(getMessage());
} }
public StoreReadOnlyException() {
super(noContext(), "Store is read only, could not delete store");
LOG.error(getMessage());
}
@Override @Override
public String getCode () { public String getCode () {
return CODE; return CODE;

View File

@@ -26,8 +26,10 @@ package sonia.scm.store;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.util.IOUtil;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
/** /**
@@ -64,7 +66,6 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
} }
@Override @Override
@SuppressWarnings("unchecked")
protected T readObject() { protected T readObject() {
LOG.debug("load {} from store {}", type, configFile); LOG.debug("load {} from store {}", type, configFile);
@@ -82,4 +83,14 @@ public class JAXBConfigurationStore<T> extends AbstractStore<T> {
configFile.toPath() 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);
}
}
} }

View File

@@ -28,6 +28,7 @@ import org.junit.Test;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryReadOnlyChecker; import sonia.scm.repository.RepositoryReadOnlyChecker;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
@@ -44,15 +45,13 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class); private final RepositoryReadOnlyChecker readOnlyChecker = mock(RepositoryReadOnlyChecker.class);
@Override @Override
protected ConfigurationStoreFactory createStoreFactory() protected JAXBConfigurationStoreFactory createStoreFactory() {
{
return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, readOnlyChecker); return new JAXBConfigurationStoreFactory(contextProvider, repositoryLocationResolver, readOnlyChecker);
} }
@Test @Test
public void shouldStoreAndLoadInRepository() public void shouldStoreAndLoadInRepository() {
{
Repository repository = new Repository("id", "git", "ns", "n"); Repository repository = new Repository("id", "git", "ns", "n");
ConfigurationStore<StoreObject> store = createStoreFactory() ConfigurationStore<StoreObject> store = createStoreFactory()
.withType(StoreObject.class) .withType(StoreObject.class)
@@ -69,8 +68,7 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
@Test @Test
public void shouldNotWriteArchivedRepository() public void shouldNotWriteArchivedRepository() {
{
Repository repository = new Repository("id", "git", "ns", "n"); Repository repository = new Repository("id", "git", "ns", "n");
when(readOnlyChecker.isReadOnly("id")).thenReturn(true); when(readOnlyChecker.isReadOnly("id")).thenReturn(true);
ConfigurationStore<StoreObject> store = createStoreFactory() ConfigurationStore<StoreObject> store = createStoreFactory()
@@ -82,4 +80,38 @@ public class JAXBConfigurationStoreTest extends StoreTestBase {
StoreObject storeObject = new StoreObject("value"); StoreObject storeObject = new StoreObject("value");
assertThrows(RuntimeException.class, () -> store.set(storeObject)); assertThrows(RuntimeException.class, () -> store.set(storeObject));
} }
@Test
public void shouldDeleteConfigStore() {
Repository repository = new Repository("id", "git", "ns", "n");
ConfigurationStore<StoreObject> 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<StoreObject> 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();
}
} }

View File

@@ -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;

View File

@@ -57,6 +57,7 @@ export { default as Paginator } from "./Paginator";
export { default as LinkPaginator } from "./LinkPaginator"; export { default as LinkPaginator } from "./LinkPaginator";
export { default as StatePaginator } from "./StatePaginator"; export { default as StatePaginator } from "./StatePaginator";
export { default as DangerZone } from "./DangerZone";
export { default as FileSize } from "./FileSize"; export { default as FileSize } from "./FileSize";
export { default as ProtectedRoute } from "./ProtectedRoute"; export { default as ProtectedRoute } from "./ProtectedRoute";
export { default as Help } from "./Help"; export { default as Help } from "./Help";

View File

@@ -24,9 +24,8 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Branch, Repository } from "@scm-manager/ui-types"; 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 { useTranslation } from "react-i18next";
import { DangerZoneContainer } from "../../containers/RepositoryDangerZone";
import DeleteBranch from "./DeleteBranch"; import DeleteBranch from "./DeleteBranch";
type Props = { type Props = {
@@ -51,7 +50,7 @@ const BranchDangerZone: FC<Props> = ({ repository, branch }) => {
<> <>
<hr /> <hr />
<Subtitle subtitle={t("branch.dangerZone")} /> <Subtitle subtitle={t("branch.dangerZone")} />
<DangerZoneContainer className="px-4 py-5">{dangerZone}</DangerZoneContainer> <DangerZone className="px-4 py-5">{dangerZone}</DangerZone>
</> </>
); );
}; };

View File

@@ -23,9 +23,8 @@
*/ */
import React, { FC } from "react"; import React, { FC } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styled from "styled-components";
import { Repository } from "@scm-manager/ui-types"; 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 RenameRepository from "./RenameRepository";
import DeleteRepo from "./DeleteRepo"; import DeleteRepo from "./DeleteRepo";
import ArchiveRepo from "./ArchiveRepo"; import ArchiveRepo from "./ArchiveRepo";
@@ -35,28 +34,6 @@ type Props = {
repository: Repository; 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<Props> = ({ repository }) => { const RepositoryDangerZone: FC<Props> = ({ repository }) => {
const [t] = useTranslation("repos"); const [t] = useTranslation("repos");
@@ -81,7 +58,7 @@ const RepositoryDangerZone: FC<Props> = ({ repository }) => {
<> <>
<hr /> <hr />
<Subtitle subtitle={t("repositoryForm.dangerZone")} /> <Subtitle subtitle={t("repositoryForm.dangerZone")} />
<DangerZoneContainer className="px-4 py-5">{dangerZone}</DangerZoneContainer> <DangerZone className="px-4 py-5">{dangerZone}</DangerZone>
</> </>
); );
}; };

View File

@@ -24,9 +24,8 @@
import React, { FC } from "react"; import React, { FC } from "react";
import { Repository, Tag } from "@scm-manager/ui-types"; 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 { useTranslation } from "react-i18next";
import { DangerZoneContainer } from "../../containers/RepositoryDangerZone";
import DeleteTag from "./DeleteTag"; import DeleteTag from "./DeleteTag";
type Props = { type Props = {
@@ -51,7 +50,7 @@ const TagDangerZone: FC<Props> = ({ repository, tag }) => {
<> <>
<hr /> <hr />
<Subtitle subtitle={t("tag.dangerZone")} /> <Subtitle subtitle={t("tag.dangerZone")} />
<DangerZoneContainer className="px-4 py-5">{dangerZone}</DangerZoneContainer> <DangerZone className="px-4 py-5">{dangerZone}</DangerZone>
</> </>
); );
}; };