From edef8db0212f8149ca85ed46f2451bc8434c3ff7 Mon Sep 17 00:00:00 2001 From: Robert Goldmann <deadlocker@gmx.de> Date: Sun, 17 Apr 2022 19:12:57 +0200 Subject: [PATCH] #663 - run migrator from BudgetMaster --- .../migration/MigrationArguments.java | 64 +++++++++ .../migration/MigrationController.java | 33 ++++- .../migration/MigrationException.java | 15 ++ .../migration/MigrationService.java | 86 +++++++++++- .../resources/languages/base_de.properties | 1 + .../resources/languages/base_en.properties | 1 + .../main/resources/templates/migration.ftl | 129 +++++++++--------- 7 files changed, 260 insertions(+), 69 deletions(-) create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationArguments.java create mode 100644 BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationException.java diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationArguments.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationArguments.java new file mode 100644 index 000000000..b19d71f50 --- /dev/null +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationArguments.java @@ -0,0 +1,64 @@ +package de.deadlocker8.budgetmaster.migration; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +public class MigrationArguments +{ + static class MigrationArgumentBuilder + { + private String sourceUrl; + private String destinationUrl; + private String destinationUsername; + private String destinationPassword; + + public MigrationArgumentBuilder withSourceUrl(String databasePath) + { + databasePath = databasePath.replace("\\", "/"); + this.sourceUrl = MessageFormat.format("jdbc:h2:/{0}", databasePath); + return this; + } + + public MigrationArgumentBuilder withDestinationUrl(String hostname, Integer port, String databaseName) + { + this.destinationUrl = MessageFormat.format("jdbc:postgresql://{0}:{1,number,#}/{2}", hostname, port, databaseName); + return this; + } + + public MigrationArgumentBuilder withDestinationCredentials(String username, String password) + { + this.destinationUsername = username; + this.destinationPassword = password; + return this; + } + + public MigrationArguments build() + { + return new MigrationArguments(sourceUrl, destinationUrl, destinationUsername, destinationPassword); + } + } + + private final String sourceUrl; + private final String destinationUrl; + private final String destinationUsername; + private final String destinationPassword; + + private MigrationArguments(String sourceUrl, String destinationUrl, String destinationUsername, String destinationPassword) + { + this.sourceUrl = sourceUrl; + this.destinationUrl = destinationUrl; + this.destinationUsername = destinationUsername; + this.destinationPassword = destinationPassword; + } + + public List<String> getArguments() + { + final ArrayList<String> arguments = new ArrayList<>(); + arguments.add(MessageFormat.format("--spring.datasource.jdbc-url={0}", sourceUrl)); + arguments.add(MessageFormat.format("--spring.seconddatasource.jdbc-url={0}", destinationUrl)); + arguments.add(MessageFormat.format("--spring.seconddatasource.username={0}", destinationUsername)); + arguments.add(MessageFormat.format("--spring.seconddatasource.password={0}", destinationPassword)); + return arguments; + } +} diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationController.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationController.java index 59e06098b..2c4da4a00 100644 --- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationController.java +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationController.java @@ -5,12 +5,17 @@ import de.deadlocker8.budgetmaster.controller.BaseController; import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.utils.Mappings; import de.deadlocker8.budgetmaster.utils.Strings; +import de.deadlocker8.budgetmaster.utils.WebRequestUtils; +import de.deadlocker8.budgetmaster.utils.notification.Notification; +import de.deadlocker8.budgetmaster.utils.notification.NotificationType; +import de.thecodelabs.utils.util.Localization; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.WebRequest; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; @@ -31,12 +36,14 @@ public class MigrationController extends BaseController public static final String MIGRATION_SETTINGS = "migration"; } + private final MigrationService migrationService; private final SettingsService settingsService; private final UserService userService; @Autowired - public MigrationController(SettingsService settingsService, UserService userService) + public MigrationController(MigrationService migrationService, SettingsService settingsService, UserService userService) { + this.migrationService = migrationService; this.settingsService = settingsService; this.userService = userService; } @@ -56,7 +63,8 @@ public class MigrationController extends BaseController } @PostMapping - public String post(Model model, + public String post(WebRequest request, + Model model, @ModelAttribute("MigrationSettings") @Valid MigrationSettings migrationSettings, BindingResult bindingResult, @RequestParam(value = "verificationPassword") String verificationPassword) { @@ -70,14 +78,31 @@ public class MigrationController extends BaseController bindingResult.addError(verificationError); } + model.addAttribute(ModelAttributes.MIGRATION_SETTINGS, migrationSettings); + if(bindingResult.hasErrors()) { model.addAttribute(ModelAttributes.ERROR, bindingResult); - model.addAttribute(ModelAttributes.MIGRATION_SETTINGS, migrationSettings); return ReturnValues.MIGRATION_SETTINGS; } - // TODO + try + { + final MigrationArguments migrationArguments = new MigrationArguments.MigrationArgumentBuilder() + .withSourceUrl(migrationService.getDatabaseFromPreviousVersionPathWithoutExtension().toString()) + .withDestinationUrl(migrationSettings.hostname(), migrationSettings.port(), migrationSettings.databaseName()) + .withDestinationCredentials(migrationSettings.username(), migrationSettings.password()) + .build(); + // TODO: run non-blocking and redirect to progress page + migrationService.runMigration(migrationArguments); + } + catch(MigrationException e) + { + WebRequestUtils.putNotification(request, new Notification(Localization.getString("notification.migration.error", e.getMessage()), NotificationType.ERROR)); + return ReturnValues.MIGRATION_SETTINGS; + } + + // TODO: redirect to success page return ReturnValues.MIGRATION_SETTINGS; } } \ No newline at end of file diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationException.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationException.java new file mode 100644 index 000000000..182d76107 --- /dev/null +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationException.java @@ -0,0 +1,15 @@ +package de.deadlocker8.budgetmaster.migration; + +public class MigrationException extends Exception +{ + public MigrationException(String message) + { + super(message); + } + + public MigrationException(String message, Throwable cause) + { + super(message, cause); + } +} + diff --git a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationService.java b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationService.java index 2b47ba27d..32507fb55 100644 --- a/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationService.java +++ b/BudgetMasterServer/src/main/java/de/deadlocker8/budgetmaster/migration/MigrationService.java @@ -7,16 +7,28 @@ import de.deadlocker8.budgetmaster.settings.SettingsService; import de.deadlocker8.budgetmaster.templates.TemplateRepository; import de.deadlocker8.budgetmaster.transactions.TransactionRepository; import de.deadlocker8.budgetmaster.utils.Mappings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; import java.nio.file.Files; import java.nio.file.Path; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; @Service public class MigrationService { + private static final Logger LOGGER = LoggerFactory.getLogger(MigrationService.class); + public static final String PREVIOUS_DATABASE_FILE_NAME = "budgetmaster.mv.db"; + public static final String PREVIOUS_DATABASE_FILE_NAME_WITHOUT_EXTENSION = "budgetmaster"; private final SettingsService settingsService; private final Path applicationSupportFolder; private final AccountRepository accountRepository; @@ -76,9 +88,79 @@ public class MigrationService return templateRepository.findAll().isEmpty(); } + private Path getDatabaseFromPreviousVersionPath() + { + return applicationSupportFolder.resolve(PREVIOUS_DATABASE_FILE_NAME); + } + + public Path getDatabaseFromPreviousVersionPathWithoutExtension() + { + return applicationSupportFolder.resolve(PREVIOUS_DATABASE_FILE_NAME_WITHOUT_EXTENSION); + } + private boolean isDatabaseFromPreviousVersionExisting() { - final Path previousDatabasePath = applicationSupportFolder.resolve(PREVIOUS_DATABASE_FILE_NAME); - return Files.exists(previousDatabasePath); + return Files.exists(getDatabaseFromPreviousVersionPath()); + } + + public String runMigration(MigrationArguments migrationArguments) throws MigrationException + { + // TODO: extract BudgetMasterMigrator.jar from resources to tmp folder + + return runMigrator(migrationArguments); + } + + private String runMigrator(MigrationArguments migrationArguments) throws MigrationException + { + final String javaCommand = determineJavaCommand(); + + final List<String> command = new ArrayList<>(); + command.add(MessageFormat.format("\"{0}\"", javaCommand)); + command.add("-jar"); + command.add("C:/Programmierung/BudgetMaster/BudgetMasterDatabaseMigrator/target/BudgetMasterDatabaseMigrator-v2.10.0.jar"); + command.addAll(migrationArguments.getArguments()); + LOGGER.debug("Starting migration with command: {}", command); + + try + { + final ProcessBuilder processBuilder = new ProcessBuilder(command).redirectErrorStream(true); + final Process process = processBuilder.start(); + + final StringBuilder collectedStdout = new StringBuilder(); + try(BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream()))) + { + while(true) + { + String line = in.readLine(); + if(line == null) + { + break; + } + + LOGGER.debug("[MIGRATOR] {}", line); + + collectedStdout.append(line); + collectedStdout.append("\n"); + } + } + + LOGGER.debug("Migration process finished"); + return collectedStdout.toString(); + } + catch(IOException e) + { + throw new MigrationException("Error during migration process", e); + } + } + + private String determineJavaCommand() throws MigrationException + { + final Optional<String> commandResultOptional = ProcessHandle.current().info().command(); + if(commandResultOptional.isEmpty()) + { + throw new MigrationException("Could not determine java executable"); + } + + return commandResultOptional.get().replace("\\", "/"); } } diff --git a/BudgetMasterServer/src/main/resources/languages/base_de.properties b/BudgetMasterServer/src/main/resources/languages/base_de.properties index 4db577044..ed8465f8a 100644 --- a/BudgetMasterServer/src/main/resources/languages/base_de.properties +++ b/BudgetMasterServer/src/main/resources/languages/base_de.properties @@ -172,6 +172,7 @@ notification.settings.update.available=BudgetMaster Update "{0}" verfügbar notification.settings.database.delete.success=Datenbank erfolgreich gelöscht notification.settings.backup.run.success=Backup erfolgreich notification.settings.backup.run.error=Backup fehlgeschlagen +notification.migration.error=Migration fehlgeschlagen: {0} upload.image.success=Erfolgreich hochgeladen upload.image.error=Fehler: {0} upload.image.error.no.file=Fehler: Keine Datei für Upload angegeben diff --git a/BudgetMasterServer/src/main/resources/languages/base_en.properties b/BudgetMasterServer/src/main/resources/languages/base_en.properties index 763c84573..6e81d84b5 100644 --- a/BudgetMasterServer/src/main/resources/languages/base_en.properties +++ b/BudgetMasterServer/src/main/resources/languages/base_en.properties @@ -173,6 +173,7 @@ notification.settings.update.available=BudgetMaster update "{0}" available notification.settings.database.delete.success=Successfully deleted database notification.settings.backup.run.success=Backup successful notification.settings.backup.run.error=Backup failed +notification.migration.error=Migration failed: {0} upload.image.success=Upload successful upload.image.error=Error: {0} upload.image.error.no.file=Error: No file provided for upload diff --git a/BudgetMasterServer/src/main/resources/templates/migration.ftl b/BudgetMasterServer/src/main/resources/templates/migration.ftl index ea4cd54f4..08aab2420 100644 --- a/BudgetMasterServer/src/main/resources/templates/migration.ftl +++ b/BudgetMasterServer/src/main/resources/templates/migration.ftl @@ -17,92 +17,95 @@ <div class="headline">${locale.getString("title.migration")}</div> </div> </div> - <div class="container"> - <form name="MigrationSettings" action="<@s.url '/migration'/>" method="post"> - <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> - <div class="row"> - <div class="col s12 m12 l8 offset-l2"> - ${locale.getString("migration.settings.description")} + <@header.content> + <div class="container"> + <form name="MigrationSettings" action="<@s.url '/migration'/>" method="post"> + <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> + + <div class="row"> + <div class="col s12 m12 l8 offset-l2"> + ${locale.getString("migration.settings.description")} + </div> </div> - </div> - <div class="row notification-row"> - <div class="col s12 center-align"> - <div class="notification-wrapper"> - <div class="notification background-yellow text-black"> - <i class="fas fa-exclamation-triangle notification-item"></i> - <span class="notification-item left-align">${locale.getString("migration.settings.description.warning")}</span> + <div class="row notification-row"> + <div class="col s12 center-align"> + <div class="notification-wrapper"> + <div class="notification background-yellow text-black"> + <i class="fas fa-exclamation-triangle notification-item"></i> + <span class="notification-item left-align">${locale.getString("migration.settings.description.warning")}</span> + </div> </div> </div> </div> - </div> - <div class="row"> - <div class="input-field col s12 m12 l8 offset-l2"> - <i class="material-icons prefix">public</i> - <input id="migration-hostname" type="text" name="hostname" <@validation.validation "hostname"/> value="<#if migrationSettings.hostname()??>${migrationSettings.hostname()}</#if>" placeholder="localhost"> - <label for="migration-hostname">${locale.getString("migration.settings.hostname")}</label> + <div class="row"> + <div class="input-field col s12 m12 l8 offset-l2"> + <i class="material-icons prefix">public</i> + <input id="migration-hostname" type="text" name="hostname" <@validation.validation "hostname"/> value="<#if migrationSettings.hostname()??>${migrationSettings.hostname()}</#if>" placeholder="localhost"> + <label for="migration-hostname">${locale.getString("migration.settings.hostname")}</label> + </div> </div> - </div> - <div class="row"> - <div class="input-field col s12 m12 l8 offset-l2"> - <i class="material-icons prefix">dns</i> - <input id="migration-port" type="number" min="1" max="65535" name="port" <@validation.validation "port"/> value="<#if migrationSettings.port()??>${migrationSettings.port()?c}</#if>" placeholder="5432"> - <label for="migration-port">${locale.getString("migration.settings.port")}</label> + <div class="row"> + <div class="input-field col s12 m12 l8 offset-l2"> + <i class="material-icons prefix">dns</i> + <input id="migration-port" type="number" min="1" max="65535" name="port" <@validation.validation "port"/> value="<#if migrationSettings.port()??>${migrationSettings.port()?c}</#if>" placeholder="5432"> + <label for="migration-port">${locale.getString("migration.settings.port")}</label> + </div> </div> - </div> - <div class="row"> - <div class="input-field col s12 m12 l8 offset-l2"> - <i class="material-icons prefix">inventory</i> - <input id="migration-database-name" type="text" name="databaseName" <@validation.validation "databaseName"/> value="<#if migrationSettings.databaseName()??>${migrationSettings.databaseName()}</#if>"> - <label for="migration-database-name">${locale.getString("migration.settings.databaseName")}</label> + <div class="row"> + <div class="input-field col s12 m12 l8 offset-l2"> + <i class="material-icons prefix">inventory</i> + <input id="migration-database-name" type="text" name="databaseName" <@validation.validation "databaseName"/> value="<#if migrationSettings.databaseName()??>${migrationSettings.databaseName()}</#if>"> + <label for="migration-database-name">${locale.getString("migration.settings.databaseName")}</label> + </div> </div> - </div> - <div class="row"> - <div class="input-field col s12 m12 l8 offset-l2"> - <i class="material-icons prefix">person</i> - <input id="migration-username" type="text" name="username" <@validation.validation "username"/> value="<#if migrationSettings.username()??>${migrationSettings.username()}</#if>"> - <label for="migration-username">${locale.getString("migration.settings.username")}</label> + <div class="row"> + <div class="input-field col s12 m12 l8 offset-l2"> + <i class="material-icons prefix">person</i> + <input id="migration-username" type="text" name="username" <@validation.validation "username"/> value="<#if migrationSettings.username()??>${migrationSettings.username()}</#if>"> + <label for="migration-username">${locale.getString("migration.settings.username")}</label> + </div> </div> - </div> - <div class="row"> - <div class="input-field col s12 m12 l8 offset-l2"> - <i class="material-icons prefix">vpn_key</i> - <input id="migration-password" type="text" name="password" <@validation.validation "password"/> value="<#if migrationSettings.password()??>${migrationSettings.password()}</#if>"> - <label for="migration-password">${locale.getString("migration.settings.password")}</label> + <div class="row"> + <div class="input-field col s12 m12 l8 offset-l2"> + <i class="material-icons prefix">vpn_key</i> + <input id="migration-password" type="text" name="password" <@validation.validation "password"/> value="<#if migrationSettings.password()??>${migrationSettings.password()}</#if>"> + <label for="migration-password">${locale.getString("migration.settings.password")}</label> + </div> </div> - </div> - <div class="row"> - <div class="col s12 m12 l8 offset-l2"> - ${locale.getString("migration.settings.verification.password.description")} + <div class="row"> + <div class="col s12 m12 l8 offset-l2"> + ${locale.getString("migration.settings.verification.password.description")} + </div> </div> - </div> - <div class="row"> - <div class="input-field col s12 m12 l8 offset-l2"> - <i class="material-icons prefix">lock_open</i> - <input id="migration-verification-password" type="password" name="verificationPassword" <@validation.validation "verificationPassword"/> value=""> - <label for="migration-verification-password">${locale.getString("migration.settings.verification.password")}</label> + <div class="row"> + <div class="input-field col s12 m12 l8 offset-l2"> + <i class="material-icons prefix">lock_open</i> + <input id="migration-verification-password" type="password" name="verificationPassword" <@validation.validation "verificationPassword"/> value=""> + <label for="migration-verification-password">${locale.getString("migration.settings.verification.password")}</label> + </div> </div> - </div> - <div class="row"> - <div class="col m6 l4 offset-l2 right-align"> - <@header.buttonLink url='/' icon='clear' localizationKey='cancel' color='red'/> - </div> + <div class="row"> + <div class="col m6 l4 offset-l2 right-align"> + <@header.buttonLink url='/' icon='clear' localizationKey='cancel' color='red'/> + </div> - <div class="col m6 l4 left-align"> - <@header.buttonSubmit name='action' icon='merge' localizationKey='info.button.migration.start' id='buttonMigrate' color='green'/> + <div class="col m6 l4 left-align"> + <@header.buttonSubmit name='action' icon='merge' localizationKey='info.button.migration.start' id='buttonMigrate' color='green'/> + </div> </div> - </div> - </form> - </div> + </form> + </div> + </@header.content> </div> </main> -- GitLab