mirror of
https://github.com/airsonic/airsonic.git
synced 2025-10-03 09:49:17 +02:00
Add logic for seamless HSQLDB upgrades
This commit is contained in:
parent
81c23e4806
commit
0ffc0f262a
5 changed files with 204 additions and 20 deletions
|
@ -1,6 +1,7 @@
|
||||||
package org.airsonic.player;
|
package org.airsonic.player;
|
||||||
|
|
||||||
import org.airsonic.player.filter.*;
|
import org.airsonic.player.filter.*;
|
||||||
|
import org.airsonic.player.util.LegacyHsqlUtil;
|
||||||
import org.directwebremoting.servlet.DwrServlet;
|
import org.directwebremoting.servlet.DwrServlet;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -13,11 +14,13 @@ import org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
|
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
|
import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration;
|
||||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||||
|
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
|
||||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||||
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
||||||
|
@ -163,6 +166,11 @@ public class Application extends SpringBootServletInitializer implements WebServ
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SpringApplicationBuilder doConfigure(SpringApplicationBuilder application) {
|
private static SpringApplicationBuilder doConfigure(SpringApplicationBuilder application) {
|
||||||
|
// Handle HSQLDB database upgrades from 1.8 to 2.x before any beans are started.
|
||||||
|
application.application().addListeners((ApplicationListener<ApplicationEnvironmentPreparedEvent>) event -> {
|
||||||
|
LegacyHsqlUtil.upgradeHsqldbDatabaseSafely();
|
||||||
|
});
|
||||||
|
|
||||||
// Customize the application or call application.sources(...) to add sources
|
// Customize the application or call application.sources(...) to add sources
|
||||||
// Since our example is itself a @Configuration class (via @SpringBootApplication)
|
// Since our example is itself a @Configuration class (via @SpringBootApplication)
|
||||||
// we actually don't need to override this method.
|
// we actually don't need to override this method.
|
||||||
|
|
|
@ -31,29 +31,26 @@ public class LegacyHsqlDaoHelper extends GenericDaoHelper {
|
||||||
LOG.debug("Database checkpoint complete.");
|
LOG.debug("Database checkpoint complete.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PreDestroy
|
/**
|
||||||
public void onDestroy() {
|
* Shutdown the embedded HSQLDB database. After this has run, the database cannot be accessed again from the same DataSource.
|
||||||
Connection conn = null;
|
*/
|
||||||
|
private void shutdownHsqldbDatabase() {
|
||||||
try {
|
try {
|
||||||
// Properly shutdown the embedded HSQLDB database.
|
|
||||||
LOG.debug("Database shutdown in progress...");
|
LOG.debug("Database shutdown in progress...");
|
||||||
JdbcTemplate jdbcTemplate = getJdbcTemplate();
|
JdbcTemplate jdbcTemplate = getJdbcTemplate();
|
||||||
conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
|
try (Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource())) {
|
||||||
conn.setAutoCommit(true);
|
conn.setAutoCommit(true);
|
||||||
jdbcTemplate.execute("SHUTDOWN");
|
jdbcTemplate.execute("SHUTDOWN");
|
||||||
|
}
|
||||||
LOG.debug("Database shutdown complete.");
|
LOG.debug("Database shutdown complete.");
|
||||||
|
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOG.error("Database shutdown failed: " + e);
|
LOG.error("Database shutdown failed", e);
|
||||||
e.printStackTrace();
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} finally {
|
@PreDestroy
|
||||||
try {
|
public void onDestroy() {
|
||||||
if (conn != null)
|
shutdownHsqldbDatabase();
|
||||||
conn.close();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -286,14 +286,26 @@ public class SettingsService {
|
||||||
return home.contains("libresonic") ? "libresonic" : "airsonic";
|
return home.contains("libresonic") ? "libresonic" : "airsonic";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getDefaultJDBCPath() {
|
||||||
|
return getAirsonicHome().getPath() + "/db/" + getFileSystemAppName();
|
||||||
|
}
|
||||||
|
|
||||||
public static String getDefaultJDBCUrl() {
|
public static String getDefaultJDBCUrl() {
|
||||||
return "jdbc:hsqldb:file:" + getAirsonicHome().getPath() + "/db/" + getFileSystemAppName() + ";sql.enforce_size=false";
|
return "jdbc:hsqldb:file:" + getDefaultJDBCPath() + ";sql.enforce_size=false";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getDefaultUPnPPort() {
|
public static int getDefaultUPnPPort() {
|
||||||
return Optional.ofNullable(System.getProperty(KEY_UPNP_PORT)).map(x -> Integer.parseInt(x)).orElse(DEFAULT_UPNP_PORT);
|
return Optional.ofNullable(System.getProperty(KEY_UPNP_PORT)).map(x -> Integer.parseInt(x)).orElse(DEFAULT_UPNP_PORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getDefaultJDBCUsername() {
|
||||||
|
return "sa";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDefaultJDBCPassword() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
public static File getLogFile() {
|
public static File getLogFile() {
|
||||||
File airsonicHome = SettingsService.getAirsonicHome();
|
File airsonicHome = SettingsService.getAirsonicHome();
|
||||||
return new File(airsonicHome, getFileSystemAppName() + ".log");
|
return new File(airsonicHome, getFileSystemAppName() + ".log");
|
||||||
|
|
|
@ -51,8 +51,8 @@ public class DatabaseConfiguration {
|
||||||
BasicDataSource dataSource = new BasicDataSource();
|
BasicDataSource dataSource = new BasicDataSource();
|
||||||
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
|
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
|
||||||
dataSource.setUrl(SettingsService.getDefaultJDBCUrl());
|
dataSource.setUrl(SettingsService.getDefaultJDBCUrl());
|
||||||
dataSource.setUsername("sa");
|
dataSource.setUsername(SettingsService.getDefaultJDBCUsername());
|
||||||
dataSource.setPassword("");
|
dataSource.setPassword(SettingsService.getDefaultJDBCPassword());
|
||||||
return dataSource;
|
return dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,167 @@
|
||||||
|
package org.airsonic.player.util;
|
||||||
|
|
||||||
|
import org.airsonic.player.service.SettingsService;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class LegacyHsqlUtil {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(LegacyHsqlUtil.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current version of the HSQLDB database, as reported by the database properties file.
|
||||||
|
*/
|
||||||
|
public static String getHsqldbDatabaseVersion() {
|
||||||
|
Properties prop = new Properties();
|
||||||
|
File configFile = new File(SettingsService.getDefaultJDBCPath() + ".properties");
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
LOG.debug("HSQLDB database doesn't exist, cannot determine version");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (InputStream stream = new FileInputStream(configFile)) {
|
||||||
|
prop.load(stream);
|
||||||
|
return prop.getProperty("version");
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Failed to determine HSQLDB database version", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new connection to the HSQLDB database.
|
||||||
|
*/
|
||||||
|
public static Connection getHsqldbDatabaseConnection() throws SQLException {
|
||||||
|
String url = SettingsService.getDefaultJDBCUrl();
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.put("user", SettingsService.getDefaultJDBCUsername());
|
||||||
|
properties.put("password", SettingsService.getDefaultJDBCPassword());
|
||||||
|
return DriverManager.getConnection(url, properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a HSQLDB database upgrade will occur and backups are needed.
|
||||||
|
*
|
||||||
|
* DB Driver Likely reason Decision
|
||||||
|
* null - new db or non-legacy false
|
||||||
|
* - null or !2 something went wrong, we better make copies true
|
||||||
|
* 1.x 2.x this is the big upgrade true
|
||||||
|
* 2.x 2.x already up to date false
|
||||||
|
*
|
||||||
|
* @return true if a database backup/migration should be performed
|
||||||
|
*/
|
||||||
|
public static boolean isHsqldbDatabaseUpgradeNeeded() {
|
||||||
|
// Check the current database version
|
||||||
|
String currentVersion = getHsqldbDatabaseVersion();
|
||||||
|
if (currentVersion == null) {
|
||||||
|
LOG.debug("HSQLDB database not found, skipping upgrade checks");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the database driver version
|
||||||
|
String driverVersion = null;
|
||||||
|
try {
|
||||||
|
Driver driver = DriverManager.getDriver(SettingsService.getDefaultJDBCUrl());
|
||||||
|
driverVersion = String.format("%d.%d", driver.getMajorVersion(), driver.getMinorVersion());
|
||||||
|
if (driver.getMajorVersion() != 2) {
|
||||||
|
LOG.warn("HSQLDB database driver version {} is untested ; trying to connect anyway, this may upgrade the database from version {}", driverVersion, currentVersion);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.warn("HSQLDB database driver version cannot be determined ; trying to connect anyway, this may upgrade the database from version {}", currentVersion, e);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log what we're about to do and determine if we should perform a controlled upgrade with backups.
|
||||||
|
if (currentVersion.startsWith(driverVersion)) {
|
||||||
|
// If we're already on the same version as the driver, nothing should happen.
|
||||||
|
LOG.debug("HSQLDB database upgrade unneeded, already on version {}", driverVersion);
|
||||||
|
return false;
|
||||||
|
} else if (currentVersion.startsWith("2.")) {
|
||||||
|
// If the database version is 2.x but older than the driver, the upgrade should be relatively painless.
|
||||||
|
LOG.debug("HSQLDB database will be silently upgraded from version {} to {}", currentVersion, driverVersion);
|
||||||
|
return false;
|
||||||
|
} else if ("1.8.0".equals(currentVersion) || "1.8.1".equals(currentVersion)) {
|
||||||
|
// If we're on a 1.8.0 or 1.8.1 database and upgrading to 2.x, we're going to handle this manually and check what we're doing.
|
||||||
|
LOG.info("HSQLDB database upgrade needed, from version {} to {}", currentVersion, driverVersion);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// If this happens we're on a completely untested version and we don't know what will happen.
|
||||||
|
LOG.warn("HSQLDB database upgrade needed, from version {} to {}", currentVersion, driverVersion);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a backup of the HSQLDB database, to a timestamped directory.
|
||||||
|
* @return the path to the backup directory
|
||||||
|
*/
|
||||||
|
public static Path performHsqldbDatabaseBackup() throws IOException {
|
||||||
|
|
||||||
|
String timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
|
||||||
|
Path source = Paths.get(SettingsService.getDefaultJDBCPath()).getParent();
|
||||||
|
Path destination = Paths.get(String.format("%s.backup.%s", SettingsService.getDefaultJDBCPath(), timestamp));
|
||||||
|
|
||||||
|
LOG.debug("Performing HSQLDB database backup...");
|
||||||
|
FileUtils.copyDirectory(source.toFile(), destination.toFile());
|
||||||
|
LOG.info("HSQLDB database backed up to {}", destination.toString());
|
||||||
|
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform an in-place database upgrade from HSQLDB 1.x to 2.x.
|
||||||
|
*/
|
||||||
|
public static void performHsqldbDatabaseUpgrade() throws SQLException {
|
||||||
|
|
||||||
|
LOG.debug("Performing HSQLDB database upgrade...");
|
||||||
|
|
||||||
|
// This will upgrade HSQLDB on the first connection. This does not
|
||||||
|
// use Spring's DataSource, as running SHUTDOWN against it will
|
||||||
|
// prevent further connections to the database.
|
||||||
|
try (Connection conn = getHsqldbDatabaseConnection()) {
|
||||||
|
LOG.debug("Database connection established. Current version is: {}", conn.getMetaData().getDatabaseProductVersion());
|
||||||
|
// On upgrade, the official documentation recommends that we
|
||||||
|
// run 'SHUTDOWN SCRIPT' to compact all the database into a
|
||||||
|
// single SQL file.
|
||||||
|
//
|
||||||
|
// In practice, if we don't do that, we did not observe issues
|
||||||
|
// immediately but after the upgrade.
|
||||||
|
LOG.debug("Shutting down database (SHUTDOWN SCRIPT)...");
|
||||||
|
try (Statement st = conn.createStatement()) {
|
||||||
|
st.execute("SHUTDOWN SCRIPT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.info("HSQLDB database has been upgraded to version {}", getHsqldbDatabaseVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If needed, perform an in-place database upgrade from HSQLDB 1.x to 2.x after having created backups.
|
||||||
|
*/
|
||||||
|
public static void upgradeHsqldbDatabaseSafely() {
|
||||||
|
if (LegacyHsqlUtil.isHsqldbDatabaseUpgradeNeeded()) {
|
||||||
|
try {
|
||||||
|
performHsqldbDatabaseBackup();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to backup HSQLDB database before upgrade", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
performHsqldbDatabaseUpgrade();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("Failed to upgrade HSQLDB database", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue