diff --git a/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java index c2ccb91713..8a8a3b3823 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/update/MoveMigrationStrategy.java @@ -1,21 +1,113 @@ package sonia.scm.repository.update; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import sonia.scm.SCMContextProvider; +import sonia.scm.migration.UpdateException; +import sonia.scm.repository.AbstractSimpleRepositoryHandler; +import sonia.scm.repository.RepositoryLocationResolver; import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; class MoveMigrationStrategy implements MigrationStrategy.Instance { + private static final Logger LOG = LoggerFactory.getLogger(MoveMigrationStrategy.class); + private final SCMContextProvider contextProvider; + private final RepositoryLocationResolver locationResolver; @Inject - public MoveMigrationStrategy(SCMContextProvider contextProvider) { + public MoveMigrationStrategy(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { this.contextProvider = contextProvider; + this.locationResolver = locationResolver; } @Override public Path migrate(String id, String name, String type) { - return null; + Path repositoryBasePath = locationResolver.forClass(Path.class).getLocation(id); + Path targetDataPath = repositoryBasePath + .resolve(AbstractSimpleRepositoryHandler.REPOSITORIES_NATIVE_DIRECTORY); + Path sourceDataPath = getSourceDataPath(name, type); + moveData(sourceDataPath, targetDataPath); + deleteOldDataDir(getTypeDependentPath(type), name); + return repositoryBasePath; + } + + private void deleteOldDataDir(Path rootPath, String name) { + delete(rootPath, asList(name.split("/"))); + } + + private void delete(Path rootPath, List directories) { + if (directories.isEmpty()) { + return; + } + Path directory = rootPath.resolve(directories.get(0)); + delete(directory, directories.subList(1, directories.size())); + try { + Files.deleteIfExists(directory); + } catch (IOException e) { + LOG.warn("could not delete source repository directory {}", directory); + } + } + + private Path getSourceDataPath(String name, String type) { + return Arrays.stream(name.split("/")) + .reduce(getTypeDependentPath(type), (path, namePart) -> path.resolve(namePart), (p1, p2) -> p1); + } + + private Path getTypeDependentPath(String type) { + return contextProvider.getBaseDirectory().toPath().resolve("repositories").resolve(type); + } + + private void moveData(Path sourceDirectory, Path targetDirectory) { + createDataDirectory(targetDirectory); + Stream list = listSourceDirectory(sourceDirectory); + list.forEach( + sourceFile -> { + Path targetFile = targetDirectory.resolve(sourceFile.getFileName()); + if (Files.isDirectory(sourceFile)) { + moveData(sourceFile, targetFile); + } else { + moveFile(sourceFile, targetFile); + } + } + ); + try { + Files.delete(sourceDirectory); + } catch (IOException e) { + LOG.warn("could not delete source repository directory {}", sourceDirectory); + } + } + + private Stream listSourceDirectory(Path sourceDirectory) { + try { + return Files.list(sourceDirectory); + } catch (IOException e) { + throw new UpdateException("could not read original directory", e); + } + } + + private void moveFile(Path sourceFile, Path targetFile) { + try { + Files.move(sourceFile, targetFile); + } catch (IOException e) { + throw new UpdateException("could not move data file from " + sourceFile + " to " + targetFile, e); + } + } + + private void createDataDirectory(Path target) { + try { + Files.createDirectories(target); + } catch (IOException e) { + throw new UpdateException("could not create data directory " + target, e); + } } } diff --git a/scm-webapp/src/test/java/sonia/scm/repository/update/MoveMigrationStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/repository/update/MoveMigrationStrategyTest.java new file mode 100644 index 0000000000..ce894516df --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/repository/update/MoveMigrationStrategyTest.java @@ -0,0 +1,62 @@ +package sonia.scm.repository.update; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junitpioneer.jupiter.TempDirectory; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; + +import java.io.IOException; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(TempDirectory.class) +@ExtendWith(MockitoExtension.class) +class MoveMigrationStrategyTest { + + @Mock + SCMContextProvider contextProvider; + @Mock + RepositoryLocationResolver locationResolver; + + @BeforeEach + void mockContextProvider(@TempDirectory.TempDir Path tempDir) { + when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + } + + @BeforeEach + void createV1Home(@TempDirectory.TempDir Path tempDir) throws IOException { + V1RepositoryFileSystem.createV1Home(tempDir); + } + + @BeforeEach + void mockLocationResolver(@TempDirectory.TempDir Path tempDir) { + RepositoryLocationResolver.RepositoryLocationResolverInstance instanceMock = mock(RepositoryLocationResolver.RepositoryLocationResolverInstance.class); + when(locationResolver.forClass(Path.class)).thenReturn(instanceMock); + when(instanceMock.getLocation(anyString())).thenAnswer(invocation -> tempDir.resolve((String) invocation.getArgument(0))); + } + + @Test + void shouldUseStandardDirectory(@TempDirectory.TempDir Path tempDir) { + Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + assertThat(target).isEqualTo(tempDir.resolve("b4f-a9f0-49f7-ad1f-37d3aae1c55f")); + } + + @Test + void shouldMoveDataDirectory(@TempDirectory.TempDir Path tempDir) { + Path target = new MoveMigrationStrategy(contextProvider, locationResolver).migrate("b4f-a9f0-49f7-ad1f-37d3aae1c55f", "some/more/directories/than/one", "git"); + assertThat(target.resolve("data")).exists(); + Path originalDataDir = tempDir + .resolve("repositories") + .resolve("git") + .resolve("some"); + assertThat(originalDataDir).doesNotExist(); + } +}