diff --git a/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java b/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java new file mode 100644 index 0000000000..52207beaa0 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/update/PropertyFileAccess.java @@ -0,0 +1,24 @@ +package sonia.scm.update; + +import java.io.IOException; +import java.nio.file.Path; + +public interface PropertyFileAccess { + Target renameGlobalConfigurationFrom(String oldName); + + interface Target { + void to(String newName) throws IOException; + } + + StoreFileTools forStoreName(String name); + + interface StoreFileTools { + void forStoreFiles(FileConsumer storeFileConsumer) throws IOException; + + void moveAsRepositoryStore(Path storeFile, String repositoryId) throws IOException; + } + + public interface FileConsumer { + void accept(Path file, String repositoryId) throws IOException; + } +} diff --git a/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java new file mode 100644 index 0000000000..8a3b3b96ad --- /dev/null +++ b/scm-dao-xml/src/main/java/sonia/scm/store/JAXBPropertyFileAccess.java @@ -0,0 +1,88 @@ +package sonia.scm.store; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sonia.scm.SCMContextProvider; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.update.PropertyFileAccess; +import sonia.scm.util.IOUtil; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class JAXBPropertyFileAccess implements PropertyFileAccess { + + private static final Logger LOG = LoggerFactory.getLogger(JAXBPropertyFileAccess.class); + + public static final String XML_FILENAME_SUFFIX = ".xml"; + private final SCMContextProvider contextProvider; + private final RepositoryLocationResolver locationResolver; + + @Inject + public JAXBPropertyFileAccess(SCMContextProvider contextProvider, RepositoryLocationResolver locationResolver) { + this.contextProvider = contextProvider; + this.locationResolver = locationResolver; + } + + @Override + public Target renameGlobalConfigurationFrom(String oldName) { + return newName -> { + Path configDir = contextProvider.getBaseDirectory().toPath().resolve(StoreConstants.CONFIG_DIRECTORY_NAME); + Path oldConfigFile = configDir.resolve(oldName + XML_FILENAME_SUFFIX); + Path newConfigFile = configDir.resolve(oldName + XML_FILENAME_SUFFIX); + Files.move(oldConfigFile, newConfigFile); + }; + } + + @Override + public StoreFileTools forStoreName(String storeName) { + return new StoreFileTools() { + @Override + public void forStoreFiles(FileConsumer storeFileConsumer) throws IOException { + Path v1storeDir = computeV1StoreDir(); + if (Files.exists(v1storeDir) && Files.isDirectory(v1storeDir)) { + Files.list(v1storeDir).filter(p -> p.toString().endsWith(XML_FILENAME_SUFFIX)).forEach(p -> { + try { + String storeName = extractStoreName(p); + storeFileConsumer.accept(p, storeName); + } catch (IOException e) { + throw new RuntimeException("could not call consumer for store file " + p + " with name " + storeName, e); + } + }); + } + } + + @Override + public void moveAsRepositoryStore(Path storeFile, String repositoryId) throws IOException { + Path repositoryLocation; + try { + repositoryLocation = locationResolver + .forClass(Path.class) + .getLocation(repositoryId); + } catch (IllegalStateException e) { + LOG.info("ignoring store file {} because there is no repository location for repository id {}", storeFile, repositoryId); + return; + } + Path target = repositoryLocation + .resolve(Store.DATA.getRepositoryStoreDirectory()) + .resolve(storeName); + IOUtil.mkdirs(target.toFile()); + Path resolvedSourceFile = computeV1StoreDir().resolve(storeFile); + Path resolvedTargetFile = target.resolve(storeFile.getFileName()); + LOG.trace("moving file {} to {}", resolvedSourceFile, resolvedTargetFile); + Files.move(resolvedSourceFile, resolvedTargetFile); + } + + private Path computeV1StoreDir() { + return contextProvider.getBaseDirectory().toPath().resolve("var").resolve("data").resolve(storeName); + } + + private String extractStoreName(Path p) { + String fileName = p.getFileName().toString(); + return fileName.substring(0, fileName.length() - XML_FILENAME_SUFFIX.length()); + } + }; + } +} diff --git a/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java new file mode 100644 index 0000000000..69c3e648df --- /dev/null +++ b/scm-dao-xml/src/test/java/sonia/scm/store/JAXBPropertyFileAccessTest.java @@ -0,0 +1,102 @@ +package sonia.scm.store; + +import org.assertj.core.api.Assertions; +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.junitpioneer.jupiter.TempDirectory; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContextProvider; +import sonia.scm.io.DefaultFileSystem; +import sonia.scm.repository.InitialRepositoryLocationResolver; +import sonia.scm.repository.RepositoryLocationResolver; +import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver; +import sonia.scm.update.PropertyFileAccess; +import sonia.scm.util.IOUtil; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; + +@ExtendWith(TempDirectory.class) +@ExtendWith(MockitoExtension.class) +class JAXBPropertyFileAccessTest { + + public static final String REPOSITORY_ID = "repoId"; + public static final String STORE_NAME = "test"; + + @Mock + SCMContextProvider contextProvider; + + RepositoryLocationResolver locationResolver; + + JAXBPropertyFileAccess fileAccess; + + @BeforeEach + void initTempDir(@TempDirectory.TempDir Path tempDir) { + lenient().when(contextProvider.getBaseDirectory()).thenReturn(tempDir.toFile()); + lenient().when(contextProvider.resolve(any())).thenAnswer(invocation -> tempDir.resolve(invocation.getArgument(0).toString())); + + locationResolver = new PathBasedRepositoryLocationResolver(contextProvider, new InitialRepositoryLocationResolver(), new DefaultFileSystem());//new TempDirRepositoryLocationResolver(tempDir.toFile()); + + fileAccess = new JAXBPropertyFileAccess(contextProvider, locationResolver); + } + + @Nested + class ForExistingRepository { + + + @BeforeEach + void createRepositoryLocation() { + locationResolver.forClass(Path.class).createLocation(REPOSITORY_ID); + } + + @Test + void shouldMoveStoreFileToRepositoryBasedLocation(@TempDirectory.TempDir Path tempDir) throws IOException { + createV1StoreFile(tempDir, "myStore.xml"); + + fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID); + + Assertions.assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("myStore.xml")).exists(); + } + + @Test + void shouldMoveAllStoreFilesToRepositoryBasedLocations(@TempDirectory.TempDir Path tempDir) throws IOException { + locationResolver.forClass(Path.class).createLocation("repoId2"); + + createV1StoreFile(tempDir, REPOSITORY_ID + ".xml"); + createV1StoreFile(tempDir, "repoId2.xml"); + + PropertyFileAccess.StoreFileTools statisticStoreAccess = fileAccess.forStoreName(STORE_NAME); + statisticStoreAccess.forStoreFiles(statisticStoreAccess::moveAsRepositoryStore); + + Assertions.assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("repoId.xml")).exists(); + Assertions.assertThat(tempDir.resolve("repositories").resolve("repoId2").resolve("store").resolve("data").resolve(STORE_NAME).resolve("repoId2.xml")).exists(); + } + } + + private void createV1StoreFile(@TempDirectory.TempDir Path tempDir, String name) throws IOException { + Path v1Dir = tempDir.resolve("var").resolve("data").resolve(STORE_NAME); + IOUtil.mkdirs(v1Dir.toFile()); + Files.createFile(v1Dir.resolve(name)); + } + + @Nested + class ForMissingRepository { + + @Test + void shouldIgnoreStoreFile(@TempDirectory.TempDir Path tempDir) throws IOException { + createV1StoreFile(tempDir, "myStore.xml"); + + fileAccess.forStoreName(STORE_NAME).moveAsRepositoryStore(Paths.get("myStore.xml"), REPOSITORY_ID); + + Assertions.assertThat(tempDir.resolve("repositories").resolve(REPOSITORY_ID).resolve("store").resolve("data").resolve(STORE_NAME).resolve("myStore.xml")).doesNotExist(); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java index 29f5770378..4765d9b96e 100644 --- a/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/boot/BootstrapModule.java @@ -24,6 +24,8 @@ import sonia.scm.store.FileBlobStoreFactory; import sonia.scm.store.JAXBConfigurationEntryStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; import sonia.scm.store.JAXBDataStoreFactory; +import sonia.scm.store.JAXBPropertyFileAccess; +import sonia.scm.update.PropertyFileAccess; import sonia.scm.update.V1PropertyDAO; import sonia.scm.update.xml.XmlV1PropertyDAO; @@ -63,6 +65,7 @@ public class BootstrapModule extends AbstractModule { bind(BlobStoreFactory.class, FileBlobStoreFactory.class); bind(PluginLoader.class).toInstance(pluginLoader); bind(V1PropertyDAO.class, XmlV1PropertyDAO.class); + bind(PropertyFileAccess.class, JAXBPropertyFileAccess.class); } private void bind(Class clazz, Class defaultImplementation) {