mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 11:05:56 +01:00
Fix export of read-only repositories
- Fix startup issue with unfinished export of deleted repository - Add withIgnoreReadOnly setting - Build store with ignore read only flag - Add logging
This commit is contained in:
committed by
Rene Pfeuffer
parent
3062851e32
commit
25c09a3b0f
4
gradle/changelog/export-issues.yaml
Normal file
4
gradle/changelog/export-issues.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
- type: fixed
|
||||||
|
description: Startup issues, if an unfinished export of a deleted repository exists
|
||||||
|
- type: fixed
|
||||||
|
description: Archived repositories can now be exported
|
||||||
@@ -28,9 +28,17 @@ public interface StoreParameters {
|
|||||||
String getRepositoryId();
|
String getRepositoryId();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns optional namespace to which the store is related
|
* Returns optional namespace to which the store is related.
|
||||||
* @return namespace
|
* @return namespace
|
||||||
* @since 2.44.0
|
* @since 2.44.0
|
||||||
*/
|
*/
|
||||||
String getNamespace();
|
String getNamespace();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the store that is supposed to be built, should ignore that the repository is read-only or not.
|
||||||
|
* @return true if it should ignore the read-only property of a repository
|
||||||
|
*/
|
||||||
|
default boolean isIgnoreReadOnly() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public final class StoreParametersBuilder<S> {
|
|||||||
private final String name;
|
private final String name;
|
||||||
private String repositoryId;
|
private String repositoryId;
|
||||||
private String namespace;
|
private String namespace;
|
||||||
|
private boolean ignoreReadOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -73,6 +74,20 @@ public final class StoreParametersBuilder<S> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this to create or get a store, that can write data into a repository store,
|
||||||
|
* even if the repository itself is read-only.
|
||||||
|
* One use-case example is the 'ExportService' within scm-webapp.
|
||||||
|
* Only use this feature, if you are sure that your service needs to write data into a repository,
|
||||||
|
* even if it is read-only.
|
||||||
|
*
|
||||||
|
* @return Floating API to finish the call.
|
||||||
|
*/
|
||||||
|
public StoreParametersBuilder<S> withReadOnlyIgnore() {
|
||||||
|
parameters.ignoreReadOnly = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or gets the store with the given name and (if specified) the given repository. If no
|
* Creates or gets the store with the given name and (if specified) the given repository. If no
|
||||||
* repository is given, the store will be global.
|
* repository is given, the store will be global.
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package sonia.scm.store.file;
|
package sonia.scm.store.file;
|
||||||
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import sonia.scm.SCMContextProvider;
|
import sonia.scm.SCMContextProvider;
|
||||||
@@ -75,7 +74,23 @@ abstract class FileBasedStoreFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected boolean mustBeReadOnly(StoreParameters storeParameters) {
|
protected boolean mustBeReadOnly(StoreParameters storeParameters) {
|
||||||
return storeParameters.getRepositoryId() != null && readOnlyChecker.isReadOnly(storeParameters.getRepositoryId());
|
if (storeParameters.isIgnoreReadOnly()) {
|
||||||
|
LOG.debug("ignoring if store should be readonly");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storeParameters.getRepositoryId() == null) {
|
||||||
|
LOG.debug("store cannot be readonly, because it is not built for a repository");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!readOnlyChecker.isReadOnly(storeParameters.getRepositoryId())) {
|
||||||
|
LOG.debug("created writeable store for repository {}", storeParameters.getRepositoryId());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.debug("created readonly store for repository {}", storeParameters.getRepositoryId());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +107,7 @@ abstract class FileBasedStoreFactory {
|
|||||||
/**
|
/**
|
||||||
* Get the store directory of a specific namespace
|
* Get the store directory of a specific namespace
|
||||||
*
|
*
|
||||||
* @param store the type of the store
|
* @param store the type of the store
|
||||||
* @param namespace the name of the namespace
|
* @param namespace the name of the namespace
|
||||||
* @return the store directory of a specific namespace
|
* @return the store directory of a specific namespace
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,9 +17,11 @@
|
|||||||
package sonia.scm.importexport;
|
package sonia.scm.importexport;
|
||||||
|
|
||||||
import jakarta.inject.Inject;
|
import jakarta.inject.Inject;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.SecurityUtils;
|
import org.apache.shiro.SecurityUtils;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
import sonia.scm.repository.RepositoryPermissions;
|
import sonia.scm.repository.RepositoryPermissions;
|
||||||
import sonia.scm.repository.api.ExportFailedException;
|
import sonia.scm.repository.api.ExportFailedException;
|
||||||
import sonia.scm.store.Blob;
|
import sonia.scm.store.Blob;
|
||||||
@@ -37,10 +39,10 @@ import java.time.temporal.ChronoUnit;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
import static sonia.scm.ContextEntry.ContextBuilder.entity;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class ExportService {
|
public class ExportService {
|
||||||
|
|
||||||
static final String STORE_NAME = "repository-export";
|
static final String STORE_NAME = "repository-export";
|
||||||
@@ -48,16 +50,23 @@ public class ExportService {
|
|||||||
private final DataStoreFactory dataStoreFactory;
|
private final DataStoreFactory dataStoreFactory;
|
||||||
private final ExportFileExtensionResolver fileExtensionResolver;
|
private final ExportFileExtensionResolver fileExtensionResolver;
|
||||||
private final ExportNotificationHandler notificationHandler;
|
private final ExportNotificationHandler notificationHandler;
|
||||||
|
private final RepositoryManager repositoryManager;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ExportService(BlobStoreFactory blobStoreFactory, DataStoreFactory dataStoreFactory, ExportFileExtensionResolver fileExtensionResolver, ExportNotificationHandler notificationHandler) {
|
public ExportService(BlobStoreFactory blobStoreFactory,
|
||||||
|
DataStoreFactory dataStoreFactory,
|
||||||
|
ExportFileExtensionResolver fileExtensionResolver,
|
||||||
|
ExportNotificationHandler notificationHandler,
|
||||||
|
RepositoryManager repositoryManager) {
|
||||||
this.blobStoreFactory = blobStoreFactory;
|
this.blobStoreFactory = blobStoreFactory;
|
||||||
this.dataStoreFactory = dataStoreFactory;
|
this.dataStoreFactory = dataStoreFactory;
|
||||||
this.fileExtensionResolver = fileExtensionResolver;
|
this.fileExtensionResolver = fileExtensionResolver;
|
||||||
this.notificationHandler = notificationHandler;
|
this.notificationHandler = notificationHandler;
|
||||||
|
this.repositoryManager = repositoryManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OutputStream store(Repository repository, boolean withMetadata, boolean compressed, boolean encrypted) {
|
public OutputStream store(Repository repository, boolean withMetadata, boolean compressed, boolean encrypted) {
|
||||||
|
log.debug("Start storing export for repository {}", repository);
|
||||||
RepositoryPermissions.export(repository).check();
|
RepositoryPermissions.export(repository).check();
|
||||||
storeExportInformation(repository.getId(), withMetadata, compressed, encrypted);
|
storeExportInformation(repository.getId(), withMetadata, compressed, encrypted);
|
||||||
try {
|
try {
|
||||||
@@ -104,12 +113,14 @@ public class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void clear(String repositoryId) {
|
public void clear(String repositoryId) {
|
||||||
|
log.debug("Clearing export for repository {}", repositoryId);
|
||||||
RepositoryPermissions.export(repositoryId).check();
|
RepositoryPermissions.export(repositoryId).check();
|
||||||
createDataStore().remove(repositoryId);
|
createDataStore().remove(repositoryId);
|
||||||
createBlobStore(repositoryId).clear();
|
createBlobStore(repositoryId).clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setExportFinished(Repository repository) {
|
public void setExportFinished(Repository repository) {
|
||||||
|
log.debug("Setting export as finished for repository {}", repository);
|
||||||
RepositoryPermissions.export(repository).check();
|
RepositoryPermissions.export(repository).check();
|
||||||
DataStore<RepositoryExportInformation> dataStore = createDataStore();
|
DataStore<RepositoryExportInformation> dataStore = createDataStore();
|
||||||
RepositoryExportInformation info = dataStore.get(repository.getId());
|
RepositoryExportInformation info = dataStore.get(repository.getId());
|
||||||
@@ -121,31 +132,48 @@ public class ExportService {
|
|||||||
public boolean isExporting(Repository repository) {
|
public boolean isExporting(Repository repository) {
|
||||||
RepositoryPermissions.export(repository).check();
|
RepositoryPermissions.export(repository).check();
|
||||||
RepositoryExportInformation info = createDataStore().get(repository.getId());
|
RepositoryExportInformation info = createDataStore().get(repository.getId());
|
||||||
return info != null && info.getStatus() == ExportStatus.EXPORTING;
|
boolean isExporting = info != null && info.getStatus() == ExportStatus.EXPORTING;
|
||||||
|
|
||||||
|
if (isExporting) {
|
||||||
|
log.debug("Repository {} is still exporting", repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isExporting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cleanupUnfinishedExports() {
|
public void cleanupUnfinishedExports() {
|
||||||
DataStore<RepositoryExportInformation> dataStore = createDataStore();
|
DataStore<RepositoryExportInformation> dataStore = createDataStore();
|
||||||
List<Map.Entry<String, RepositoryExportInformation>> unfinishedExports = dataStore.getAll().entrySet().stream()
|
List<Map.Entry<String, RepositoryExportInformation>> unfinishedExports = dataStore.getAll().entrySet().stream()
|
||||||
.filter(e -> e.getValue().getStatus() == ExportStatus.EXPORTING)
|
.filter(e -> e.getValue().getStatus() == ExportStatus.EXPORTING)
|
||||||
.collect(Collectors.toList());
|
.toList();
|
||||||
|
|
||||||
for (Map.Entry<String, RepositoryExportInformation> export : unfinishedExports) {
|
for (Map.Entry<String, RepositoryExportInformation> export : unfinishedExports) {
|
||||||
createBlobStore(export.getKey()).clear();
|
log.debug("Cleaning up export for repository {}", export.getKey());
|
||||||
RepositoryExportInformation info = dataStore.get(export.getKey());
|
if (isRepositoryExisting(export.getKey())) {
|
||||||
info.setStatus(ExportStatus.INTERRUPTED);
|
createBlobStore(export.getKey()).clear();
|
||||||
dataStore.put(export.getKey(), info);
|
RepositoryExportInformation info = dataStore.get(export.getKey());
|
||||||
|
info.setStatus(ExportStatus.INTERRUPTED);
|
||||||
|
dataStore.put(export.getKey(), info);
|
||||||
|
log.debug("Export for repository {} has been cleaned up", export.getKey());
|
||||||
|
} else {
|
||||||
|
dataStore.remove(export.getKey());
|
||||||
|
log.debug("Repository {} has already been deleted. Deleting dangling export.", export.getKey());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cleanupOutdatedExports() {
|
void cleanupOutdatedExports() {
|
||||||
|
log.debug("Cleaning up outdated exports");
|
||||||
DataStore<RepositoryExportInformation> dataStore = createDataStore();
|
DataStore<RepositoryExportInformation> dataStore = createDataStore();
|
||||||
List<String> outdatedExportIds = collectOutdatedExportIds(dataStore);
|
List<String> outdatedExportIds = collectOutdatedExportIds(dataStore);
|
||||||
|
|
||||||
for (String id : outdatedExportIds) {
|
for (String id : outdatedExportIds) {
|
||||||
createBlobStore(id).clear();
|
createBlobStore(id).clear();
|
||||||
|
log.debug("Cleaned up blob of outdated export for repository {}", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
outdatedExportIds.forEach(dataStore::remove);
|
outdatedExportIds.forEach(dataStore::remove);
|
||||||
|
log.debug("Cleaned up outdated exports");
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> collectOutdatedExportIds(DataStore<RepositoryExportInformation> dataStore) {
|
private List<String> collectOutdatedExportIds(DataStore<RepositoryExportInformation> dataStore) {
|
||||||
@@ -190,6 +218,10 @@ public class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private BlobStore createBlobStore(String repositoryId) {
|
private BlobStore createBlobStore(String repositoryId) {
|
||||||
return blobStoreFactory.withName(STORE_NAME).forRepository(repositoryId).build();
|
return blobStoreFactory.withName(STORE_NAME).forRepository(repositoryId).withReadOnlyIgnore().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isRepositoryExisting(String repositoryId) {
|
||||||
|
return this.repositoryManager.get(repositoryId) != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ import org.mockito.InjectMocks;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import sonia.scm.NotFoundException;
|
import sonia.scm.NotFoundException;
|
||||||
import sonia.scm.notifications.Type;
|
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
import sonia.scm.repository.RepositoryManager;
|
||||||
import sonia.scm.repository.RepositoryTestData;
|
import sonia.scm.repository.RepositoryTestData;
|
||||||
import sonia.scm.store.Blob;
|
import sonia.scm.store.Blob;
|
||||||
import sonia.scm.store.BlobStore;
|
import sonia.scm.store.BlobStore;
|
||||||
@@ -48,7 +48,6 @@ import java.util.List;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -73,6 +72,9 @@ class ExportServiceTest {
|
|||||||
@Mock
|
@Mock
|
||||||
private ExportNotificationHandler notificationHandler;
|
private ExportNotificationHandler notificationHandler;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private RepositoryManager repositoryManager;
|
||||||
|
|
||||||
private BlobStore blobStore;
|
private BlobStore blobStore;
|
||||||
private DataStore<RepositoryExportInformation> dataStore;
|
private DataStore<RepositoryExportInformation> dataStore;
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ class ExportServiceTest {
|
|||||||
);
|
);
|
||||||
|
|
||||||
blobStore = new InMemoryBlobStore();
|
blobStore = new InMemoryBlobStore();
|
||||||
when(blobStoreFactory.withName(STORE_NAME).forRepository(REPOSITORY.getId()).build())
|
when(blobStoreFactory.withName(STORE_NAME).forRepository(REPOSITORY.getId()).withReadOnlyIgnore().build())
|
||||||
.thenReturn(blobStore);
|
.thenReturn(blobStore);
|
||||||
|
|
||||||
dataStore = new InMemoryDataStore<>();
|
dataStore = new InMemoryDataStore<>();
|
||||||
@@ -205,6 +207,7 @@ class ExportServiceTest {
|
|||||||
);
|
);
|
||||||
when(blobStoreFactory.withName(STORE_NAME).forRepository(finishedExport.getId()).build())
|
when(blobStoreFactory.withName(STORE_NAME).forRepository(finishedExport.getId()).build())
|
||||||
.thenReturn(finishedExportBlobStore);
|
.thenReturn(finishedExportBlobStore);
|
||||||
|
when(repositoryManager.get(REPOSITORY.getId())).thenReturn(REPOSITORY);
|
||||||
|
|
||||||
exportService.cleanupUnfinishedExports();
|
exportService.cleanupUnfinishedExports();
|
||||||
|
|
||||||
@@ -214,6 +217,21 @@ class ExportServiceTest {
|
|||||||
assertThat(dataStore.get(finishedExport.getId()).getStatus()).isEqualTo(ExportStatus.FINISHED);
|
assertThat(dataStore.get(finishedExport.getId()).getStatus()).isEqualTo(ExportStatus.FINISHED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteUnfinishedExportsOfDeletedRepository() {
|
||||||
|
RepositoryExportInformation info = new RepositoryExportInformation();
|
||||||
|
info.setStatus(ExportStatus.EXPORTING);
|
||||||
|
dataStore.put(
|
||||||
|
REPOSITORY.getId(),
|
||||||
|
info
|
||||||
|
);
|
||||||
|
when(repositoryManager.get(REPOSITORY.getId())).thenReturn(null);
|
||||||
|
|
||||||
|
exportService.cleanupUnfinishedExports();
|
||||||
|
|
||||||
|
assertThat(dataStore.getOptional(REPOSITORY.getId())).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldOnlyCleanupOutdatedExports() {
|
void shouldOnlyCleanupOutdatedExports() {
|
||||||
blobStore.create(REPOSITORY.getId());
|
blobStore.create(REPOSITORY.getId());
|
||||||
@@ -229,7 +247,7 @@ class ExportServiceTest {
|
|||||||
Instant old = Instant.now().minus(11, ChronoUnit.DAYS);
|
Instant old = Instant.now().minus(11, ChronoUnit.DAYS);
|
||||||
oldExportInfo.setCreated(old);
|
oldExportInfo.setCreated(old);
|
||||||
dataStore.put(oldExportRepo.getId(), oldExportInfo);
|
dataStore.put(oldExportRepo.getId(), oldExportInfo);
|
||||||
when(blobStoreFactory.withName(STORE_NAME).forRepository(oldExportRepo.getId()).build())
|
when(blobStoreFactory.withName(STORE_NAME).forRepository(oldExportRepo.getId()).withReadOnlyIgnore().build())
|
||||||
.thenReturn(oldExportBlobStore);
|
.thenReturn(oldExportBlobStore);
|
||||||
|
|
||||||
exportService.cleanupOutdatedExports();
|
exportService.cleanupOutdatedExports();
|
||||||
|
|||||||
Reference in New Issue
Block a user