diff --git a/commafeed-server/pom.xml b/commafeed-server/pom.xml index 6a75467d..07b40915 100644 --- a/commafeed-server/pom.xml +++ b/commafeed-server/pom.xml @@ -405,9 +405,13 @@ com.h2database h2 - - 2.1.214 + + com.manticore-projects.tools + h2migrationtool + 1.4 + + com.mysql mysql-connector-j diff --git a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java index 7f8e79a3..bee52d18 100644 --- a/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/commafeed-server/src/main/java/com/commafeed/CommaFeedApplication.java @@ -1,6 +1,8 @@ package com.commafeed; import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Instant; import java.util.EnumSet; import java.util.Set; @@ -24,8 +26,9 @@ import com.commafeed.backend.model.FeedSubscription; import com.commafeed.backend.model.User; import com.commafeed.backend.model.UserRole; import com.commafeed.backend.model.UserSettings; -import com.commafeed.backend.service.DatabaseStartupService; import com.commafeed.backend.service.UserService; +import com.commafeed.backend.service.db.DatabaseStartupService; +import com.commafeed.backend.service.db.H2MigrationService; import com.commafeed.backend.task.ScheduledTask; import com.commafeed.frontend.auth.PasswordConstraintValidator; import com.commafeed.frontend.auth.SecurityCheckFactoryProvider; @@ -58,6 +61,7 @@ import io.dropwizard.configuration.DefaultConfigurationFactoryFactory; import io.dropwizard.configuration.EnvironmentVariableSubstitutor; import io.dropwizard.configuration.SubstitutingSourceProvider; import io.dropwizard.core.Application; +import io.dropwizard.core.ConfiguredBundle; import io.dropwizard.core.setup.Bootstrap; import io.dropwizard.core.setup.Environment; import io.dropwizard.db.DataSourceFactory; @@ -93,6 +97,30 @@ public class CommaFeedApplication extends Application { configureEnvironmentSubstitutor(bootstrap); configureObjectMapper(bootstrap.getObjectMapper()); + // run h2 migration as the first bundle because we need to migrate before hibernate is initialized + bootstrap.addBundle(new ConfiguredBundle() { + @Override + public void run(CommaFeedConfiguration config, Environment environment) throws Exception { + DataSourceFactory dataSourceFactory = config.getDataSourceFactory(); + String url = dataSourceFactory.getUrl(); + if (isFileBasedH2(url)) { + Path path = getFilePath(url); + String user = dataSourceFactory.getUser(); + String password = dataSourceFactory.getPassword(); + new H2MigrationService().migrateIfNeeded(path, user, password); + } + } + + private boolean isFileBasedH2(String url) { + return url.startsWith("jdbc:h2:") && !url.startsWith("jdbc:h2:mem:"); + } + + private Path getFilePath(String url) { + String name = url.substring("jdbc:h2:".length()).split(";")[0]; + return Paths.get(name + ".mv.db"); + } + }); + bootstrap.addBundle(hibernateBundle = new HibernateBundle<>(AbstractModel.class, Feed.class, FeedCategory.class, FeedEntry.class, FeedEntryContent.class, FeedEntryStatus.class, FeedEntryTag.class, FeedSubscription.class, User.class, UserRole.class, UserSettings.class) { diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/DatabaseCleaningService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java similarity index 96% rename from commafeed-server/src/main/java/com/commafeed/backend/service/DatabaseCleaningService.java rename to commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java index 336a05cf..a8f54d4e 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/DatabaseCleaningService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseCleaningService.java @@ -1,4 +1,4 @@ -package com.commafeed.backend.service; +package com.commafeed.backend.service.db; import java.time.Instant; import java.util.List; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/DatabaseStartupService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java similarity index 94% rename from commafeed-server/src/main/java/com/commafeed/backend/service/DatabaseStartupService.java rename to commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java index 6675b67a..865118e6 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/service/DatabaseStartupService.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/DatabaseStartupService.java @@ -1,4 +1,4 @@ -package com.commafeed.backend.service; +package com.commafeed.backend.service.db; import java.util.HashMap; import java.util.Map; @@ -9,6 +9,7 @@ import org.hibernate.SessionFactory; import com.commafeed.CommaFeedConfiguration; import com.commafeed.backend.dao.UnitOfWork; import com.commafeed.backend.dao.UserDAO; +import com.commafeed.backend.service.UserService; import io.dropwizard.lifecycle.Managed; import jakarta.inject.Inject; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java b/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java new file mode 100644 index 00000000..39e9e9a4 --- /dev/null +++ b/commafeed-server/src/main/java/com/commafeed/backend/service/db/H2MigrationService.java @@ -0,0 +1,76 @@ +package com.commafeed.backend.service.db; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; + +import com.manticore.h2.H2MigrationTool; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RequiredArgsConstructor +@Slf4j +public class H2MigrationService { + + public void migrateIfNeeded(Path path, String user, String password) { + int format; + try { + format = getH2FileFormat(path); + } catch (IOException e) { + throw new RuntimeException("could not detect H2 format", e); + } + + if (format == 2) { + try { + migrate(path, user, password, "2.1.214", "2.2.224"); + } catch (Exception e) { + throw new RuntimeException("could not migrate H2 to format 3", e); + } + } + } + + public int getH2FileFormat(Path path) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(path)) { + String headers = reader.readLine(); + + return Stream.of(headers.split(",")) + .filter(h -> h.startsWith("format:")) + .map(h -> h.split(":")[1]) + .map(Integer::parseInt) + .findFirst() + .orElseThrow(() -> new RuntimeException("could not find format in H2 file headers")); + } + } + + private void migrate(Path path, String user, String password, String fromVersion, String toVersion) throws Exception { + log.info("migrating H2 database at {} from format {} to format {}", path, fromVersion, toVersion); + + Path scriptPath = path.resolveSibling("script-" + System.currentTimeMillis() + ".sql"); + Path newVersionPath = path.resolveSibling(path.getFileName() + "." + getPatchVersion(toVersion) + ".mv.db"); + Path oldVersionBackupPath = path.resolveSibling(path.getFileName() + "." + getPatchVersion(fromVersion) + ".backup"); + + H2MigrationTool.readDriverRecords(); + new H2MigrationTool().migrate(fromVersion, toVersion, path.toAbsolutePath().toString(), user, password, + scriptPath.toAbsolutePath().toString(), "", "", false, false, ""); + if (!Files.exists(newVersionPath)) { + throw new RuntimeException("H2 migration failed, new version file not found"); + } + + Files.move(path, oldVersionBackupPath); + Files.move(newVersionPath, path); + Files.delete(oldVersionBackupPath); + Files.delete(scriptPath); + + log.info("migrated H2 database from format {} to format {}", fromVersion, toVersion); + } + + private String getPatchVersion(String version) { + return StringUtils.substringAfterLast(version, "."); + } + +} diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java index 98b82191..5508cbd7 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/EntriesExceedingFeedCapacityCleanupTask.java @@ -3,7 +3,7 @@ package com.commafeed.backend.task; import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.service.DatabaseCleaningService; +import com.commafeed.backend.service.db.DatabaseCleaningService; import jakarta.inject.Inject; import jakarta.inject.Singleton; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java index 76328ef3..fad42a75 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OldEntriesCleanupTask.java @@ -5,7 +5,7 @@ import java.time.Instant; import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.service.DatabaseCleaningService; +import com.commafeed.backend.service.db.DatabaseCleaningService; import jakarta.inject.Inject; import jakarta.inject.Singleton; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java index 82c12de1..0c19afe3 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OldStatusesCleanupTask.java @@ -4,7 +4,7 @@ import java.time.Instant; import java.util.concurrent.TimeUnit; import com.commafeed.CommaFeedConfiguration; -import com.commafeed.backend.service.DatabaseCleaningService; +import com.commafeed.backend.service.db.DatabaseCleaningService; import jakarta.inject.Inject; import jakarta.inject.Singleton; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java index aa5731f9..7b2afcaa 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedContentsCleanupTask.java @@ -2,7 +2,7 @@ package com.commafeed.backend.task; import java.util.concurrent.TimeUnit; -import com.commafeed.backend.service.DatabaseCleaningService; +import com.commafeed.backend.service.db.DatabaseCleaningService; import jakarta.inject.Inject; import jakarta.inject.Singleton; diff --git a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java index 4405e49a..4606dad3 100644 --- a/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java +++ b/commafeed-server/src/main/java/com/commafeed/backend/task/OrphanedFeedsCleanupTask.java @@ -2,7 +2,7 @@ package com.commafeed.backend.task; import java.util.concurrent.TimeUnit; -import com.commafeed.backend.service.DatabaseCleaningService; +import com.commafeed.backend.service.db.DatabaseCleaningService; import jakarta.inject.Inject; import jakarta.inject.Singleton; diff --git a/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java b/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java new file mode 100644 index 00000000..578cbc3e --- /dev/null +++ b/commafeed-server/src/test/java/com/commafeed/backend/service/db/H2MigrationServiceTest.java @@ -0,0 +1,29 @@ +package com.commafeed.backend.service.db; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class H2MigrationServiceTest { + + @TempDir + private Path root; + + @Test + void testMigrateIfNeeded() throws IOException { + Path path = root.resolve("database.mv.db"); + Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/h2-migration/database-v2.1.214.mv.db")), path); + + H2MigrationService service = new H2MigrationService(); + Assertions.assertEquals(2, service.getH2FileFormat(path)); + + service.migrateIfNeeded(path, "sa", "sa"); + Assertions.assertEquals(3, service.getH2FileFormat(path)); + } + +} \ No newline at end of file diff --git a/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db b/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db new file mode 100644 index 00000000..b11490bc Binary files /dev/null and b/commafeed-server/src/test/resources/h2-migration/database-v2.1.214.mv.db differ