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;
|
||||
|
||||
import org.airsonic.player.filter.*;
|
||||
import org.airsonic.player.util.LegacyHsqlUtil;
|
||||
import org.directwebremoting.servlet.DwrServlet;
|
||||
import org.slf4j.Logger;
|
||||
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.web.servlet.MultipartAutoConfiguration;
|
||||
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.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
|
@ -163,6 +166,11 @@ public class Application extends SpringBootServletInitializer implements WebServ
|
|||
}
|
||||
|
||||
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
|
||||
// Since our example is itself a @Configuration class (via @SpringBootApplication)
|
||||
// we actually don't need to override this method.
|
||||
|
|
|
@ -31,29 +31,26 @@ public class LegacyHsqlDaoHelper extends GenericDaoHelper {
|
|||
LOG.debug("Database checkpoint complete.");
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void onDestroy() {
|
||||
Connection conn = null;
|
||||
/**
|
||||
* Shutdown the embedded HSQLDB database. After this has run, the database cannot be accessed again from the same DataSource.
|
||||
*/
|
||||
private void shutdownHsqldbDatabase() {
|
||||
try {
|
||||
// Properly shutdown the embedded HSQLDB database.
|
||||
LOG.debug("Database shutdown in progress...");
|
||||
JdbcTemplate jdbcTemplate = getJdbcTemplate();
|
||||
conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource());
|
||||
try (Connection conn = DataSourceUtils.getConnection(jdbcTemplate.getDataSource())) {
|
||||
conn.setAutoCommit(true);
|
||||
jdbcTemplate.execute("SHUTDOWN");
|
||||
}
|
||||
LOG.debug("Database shutdown complete.");
|
||||
|
||||
} catch (SQLException e) {
|
||||
LOG.error("Database shutdown failed: " + e);
|
||||
e.printStackTrace();
|
||||
LOG.error("Database shutdown failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
try {
|
||||
if (conn != null)
|
||||
conn.close();
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
@PreDestroy
|
||||
public void onDestroy() {
|
||||
shutdownHsqldbDatabase();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -286,14 +286,26 @@ public class SettingsService {
|
|||
return home.contains("libresonic") ? "libresonic" : "airsonic";
|
||||
}
|
||||
|
||||
public static String getDefaultJDBCPath() {
|
||||
return getAirsonicHome().getPath() + "/db/" + getFileSystemAppName();
|
||||
}
|
||||
|
||||
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() {
|
||||
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() {
|
||||
File airsonicHome = SettingsService.getAirsonicHome();
|
||||
return new File(airsonicHome, getFileSystemAppName() + ".log");
|
||||
|
|
|
@ -51,8 +51,8 @@ public class DatabaseConfiguration {
|
|||
BasicDataSource dataSource = new BasicDataSource();
|
||||
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
|
||||
dataSource.setUrl(SettingsService.getDefaultJDBCUrl());
|
||||
dataSource.setUsername("sa");
|
||||
dataSource.setPassword("");
|
||||
dataSource.setUsername(SettingsService.getDefaultJDBCUsername());
|
||||
dataSource.setPassword(SettingsService.getDefaultJDBCPassword());
|
||||
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