# Description External DB support for Stirling PDF. You can now choose between the default H2 or PostgreSQL by setting the new `enableCustomDatabase` property to `true` or `false`. To enable your own custom (PostgreSQL) database: - Set `enableCustomDatabase` to `true` - Add your database url to `customDatabaseUrl` - Set your `username` and `password` Closes #2270 ## Checklist - [x] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [x] I have performed a self-review of my own code - [x] I have attached images of the change if it is UI based - [x] I have commented my code, particularly in hard-to-understand areas - [ ] If my code has heavily changed functionality I have updated relevant docs on [Stirling-PDFs doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) - [x] My changes generate no new warnings - [x] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only)
This commit is contained in:
committed by
GitHub
parent
7382efd80d
commit
41dce06804
@@ -1,232 +0,0 @@
|
||||
package stirling.software.SPDF.config.security.database;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.sql.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.interfaces.DatabaseBackupInterface;
|
||||
import stirling.software.SPDF.utils.FileInfo;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class DatabaseBackupHelper implements DatabaseBackupInterface {
|
||||
|
||||
@Value("${spring.datasource.url}")
|
||||
private String url;
|
||||
|
||||
@Value("${spring.datasource.username}")
|
||||
private String databaseUsername;
|
||||
|
||||
@Value("${spring.datasource.password}")
|
||||
private String databasePassword;
|
||||
|
||||
private Path backupPath = Paths.get("configs/db/backup/");
|
||||
|
||||
@Override
|
||||
public boolean hasBackup() {
|
||||
// Check if there is at least one backup
|
||||
return !getBackupList().isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FileInfo> getBackupList() {
|
||||
// Check if the backup directory exists, and create it if it does not
|
||||
ensureBackupDirectoryExists();
|
||||
|
||||
List<FileInfo> backupFiles = new ArrayList<>();
|
||||
|
||||
// Read the backup directory and filter for files with the prefix "backup_" and suffix
|
||||
// ".sql"
|
||||
try (DirectoryStream<Path> stream =
|
||||
Files.newDirectoryStream(
|
||||
backupPath,
|
||||
path ->
|
||||
path.getFileName().toString().startsWith("backup_")
|
||||
&& path.getFileName().toString().endsWith(".sql"))) {
|
||||
for (Path entry : stream) {
|
||||
BasicFileAttributes attrs = Files.readAttributes(entry, BasicFileAttributes.class);
|
||||
LocalDateTime modificationDate =
|
||||
LocalDateTime.ofInstant(
|
||||
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault());
|
||||
LocalDateTime creationDate =
|
||||
LocalDateTime.ofInstant(
|
||||
attrs.creationTime().toInstant(), ZoneId.systemDefault());
|
||||
long fileSize = attrs.size();
|
||||
backupFiles.add(
|
||||
new FileInfo(
|
||||
entry.getFileName().toString(),
|
||||
entry.toString(),
|
||||
modificationDate,
|
||||
fileSize,
|
||||
creationDate));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading backup directory: {}", e.getMessage(), e);
|
||||
}
|
||||
return backupFiles;
|
||||
}
|
||||
|
||||
// Imports a database backup from the specified file.
|
||||
public boolean importDatabaseFromUI(String fileName) throws IOException {
|
||||
return this.importDatabaseFromUI(getBackupFilePath(fileName));
|
||||
}
|
||||
|
||||
// Imports a database backup from the specified path.
|
||||
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
|
||||
boolean success = executeDatabaseScript(tempTemplatePath);
|
||||
if (success) {
|
||||
LocalDateTime dateNow = LocalDateTime.now();
|
||||
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||
Path insertOutputFilePath =
|
||||
this.getBackupFilePath("backup_user_" + dateNow.format(myFormatObj) + ".sql");
|
||||
Files.copy(tempTemplatePath, insertOutputFilePath);
|
||||
Files.deleteIfExists(tempTemplatePath);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean importDatabase() {
|
||||
if (!this.hasBackup()) return false;
|
||||
|
||||
List<FileInfo> backupList = this.getBackupList();
|
||||
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
|
||||
|
||||
return executeDatabaseScript(Paths.get(backupList.get(0).getFilePath()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportDatabase() throws IOException {
|
||||
// Check if the backup directory exists, and create it if it does not
|
||||
ensureBackupDirectoryExists();
|
||||
|
||||
// Filter and delete old backups if there are more than 5
|
||||
List<FileInfo> filteredBackupList =
|
||||
this.getBackupList().stream()
|
||||
.filter(backup -> !backup.getFileName().startsWith("backup_user_"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (filteredBackupList.size() > 5) {
|
||||
filteredBackupList.sort(
|
||||
Comparator.comparing(
|
||||
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
|
||||
Files.deleteIfExists(Paths.get(filteredBackupList.get(0).getFilePath()));
|
||||
log.info("Deleted oldest backup: {}", filteredBackupList.get(0).getFileName());
|
||||
}
|
||||
|
||||
LocalDateTime dateNow = LocalDateTime.now();
|
||||
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||
Path insertOutputFilePath =
|
||||
this.getBackupFilePath("backup_" + dateNow.format(myFormatObj) + ".sql");
|
||||
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
||||
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, insertOutputFilePath.toString());
|
||||
stmt.execute();
|
||||
log.info("Database export completed: {}", insertOutputFilePath);
|
||||
} catch (SQLException e) {
|
||||
log.error("Error during database export: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieves the H2 database version.
|
||||
public String getH2Version() {
|
||||
String version = "Unknown";
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword)) {
|
||||
try (Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
||||
if (rs.next()) {
|
||||
version = rs.getString("version");
|
||||
log.info("H2 Database Version: {}", version);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
// Deletes a backup file.
|
||||
public boolean deleteBackupFile(String fileName) throws IOException {
|
||||
if (!isValidFileName(fileName)) {
|
||||
log.error("Invalid file name: {}", fileName);
|
||||
return false;
|
||||
}
|
||||
Path filePath = this.getBackupFilePath(fileName);
|
||||
if (Files.deleteIfExists(filePath)) {
|
||||
log.info("Deleted backup file: {}", fileName);
|
||||
return true;
|
||||
} else {
|
||||
log.error("File not found or could not be deleted: {}", fileName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the Path object for a given backup file name.
|
||||
public Path getBackupFilePath(String fileName) {
|
||||
Path filePath = Paths.get(backupPath.toString(), fileName).normalize();
|
||||
if (!filePath.startsWith(backupPath)) {
|
||||
throw new SecurityException("Path traversal detected");
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private boolean executeDatabaseScript(Path scriptPath) {
|
||||
String query = "RUNSCRIPT from ?;";
|
||||
|
||||
try (Connection conn =
|
||||
DriverManager.getConnection(url, databaseUsername, databasePassword);
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, scriptPath.toString());
|
||||
stmt.execute();
|
||||
log.info("Database import completed: {}", scriptPath);
|
||||
return true;
|
||||
} catch (SQLException e) {
|
||||
log.error("Error during database import: {}", e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureBackupDirectoryExists() {
|
||||
if (Files.notExists(backupPath)) {
|
||||
try {
|
||||
Files.createDirectories(backupPath);
|
||||
} catch (IOException e) {
|
||||
log.error("Error creating directories: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidFileName(String fileName) {
|
||||
// Check for invalid characters or sequences
|
||||
return fileName != null
|
||||
&& !fileName.contains("..")
|
||||
&& !fileName.contains("/")
|
||||
&& !fileName.contains("\\")
|
||||
&& !fileName.contains(":")
|
||||
&& !fileName.contains("*")
|
||||
&& !fileName.contains("?")
|
||||
&& !fileName.contains("\"")
|
||||
&& !fileName.contains("<")
|
||||
&& !fileName.contains(">")
|
||||
&& !fileName.contains("|");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package stirling.software.SPDF.config.security.database;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.jdbc.DataSourceBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
|
||||
@Slf4j
|
||||
@Getter
|
||||
@Configuration
|
||||
public class DatabaseConfig {
|
||||
|
||||
public static final String DATASOURCE_DEFAULT_URL =
|
||||
"jdbc:h2:file:./configs/stirling-pdf-DB-2.3.232;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE";
|
||||
public static final String DATASOURCE_URL_TEMPLATE = "jdbc:%s://%s:%4d/%s";
|
||||
public static final String DEFAULT_DRIVER = "org.h2.Driver";
|
||||
public static final String DEFAULT_USERNAME = "sa";
|
||||
public static final String POSTGRES_DRIVER = "org.postgresql.Driver";
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final boolean runningEE;
|
||||
|
||||
public DatabaseConfig(ApplicationProperties applicationProperties, boolean runningEE) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.runningEE = runningEE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the <code>DataSource</code> for the connection to the DB. If <code>useDefault</code>
|
||||
* is set to <code>true</code>, it will use the default H2 DB. If it is set to <code>false
|
||||
* </code>, it will use the user's custom configuration set in the settings.yml.
|
||||
*
|
||||
* @return a <code>DataSource</code> using the configuration settings in the settings.yml
|
||||
* @throws UnsupportedProviderException if the type of database selected is not supported
|
||||
*/
|
||||
@Bean
|
||||
@Qualifier("dataSource")
|
||||
public DataSource dataSource() throws UnsupportedProviderException {
|
||||
DataSourceBuilder<?> dataSourceBuilder = DataSourceBuilder.create();
|
||||
|
||||
if (!runningEE) {
|
||||
return useDefaultDataSource(dataSourceBuilder);
|
||||
}
|
||||
|
||||
ApplicationProperties.System system = applicationProperties.getSystem();
|
||||
ApplicationProperties.Datasource datasource = system.getDatasource();
|
||||
|
||||
if (!datasource.isEnableCustomDatabase()) {
|
||||
return useDefaultDataSource(dataSourceBuilder);
|
||||
}
|
||||
|
||||
log.info("Using custom database configuration");
|
||||
|
||||
if (!datasource.getCustomDatabaseUrl().isBlank()) {
|
||||
if (datasource.getCustomDatabaseUrl().contains("postgresql")) {
|
||||
dataSourceBuilder.driverClassName(POSTGRES_DRIVER);
|
||||
}
|
||||
|
||||
dataSourceBuilder.url(datasource.getCustomDatabaseUrl());
|
||||
} else {
|
||||
dataSourceBuilder.driverClassName(getDriverClassName(datasource.getType()));
|
||||
dataSourceBuilder.url(
|
||||
generateCustomDataSourceUrl(
|
||||
datasource.getType(),
|
||||
datasource.getHostName(),
|
||||
datasource.getPort(),
|
||||
datasource.getName()));
|
||||
}
|
||||
dataSourceBuilder.username(datasource.getUsername());
|
||||
dataSourceBuilder.password(datasource.getPassword());
|
||||
|
||||
return dataSourceBuilder.build();
|
||||
}
|
||||
|
||||
private DataSource useDefaultDataSource(DataSourceBuilder<?> dataSourceBuilder) {
|
||||
log.info("Using default H2 database");
|
||||
|
||||
dataSourceBuilder.url(DATASOURCE_DEFAULT_URL);
|
||||
dataSourceBuilder.username(DEFAULT_USERNAME);
|
||||
|
||||
return dataSourceBuilder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the URL the <code>DataSource</code> will use to connect to the database
|
||||
*
|
||||
* @param dataSourceType the type of the database
|
||||
* @param hostname the host name
|
||||
* @param port the port number to use for the database
|
||||
* @param dataSourceName the name the database to connect to
|
||||
* @return the <code>DataSource</code> URL
|
||||
*/
|
||||
private String generateCustomDataSourceUrl(
|
||||
String dataSourceType, String hostname, Integer port, String dataSourceName) {
|
||||
return DATASOURCE_URL_TEMPLATE.formatted(dataSourceType, hostname, port, dataSourceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the database driver based on the type of database chosen.
|
||||
*
|
||||
* @param driverName the type of the driver (e.g. 'h2', 'postgresql')
|
||||
* @return the fully qualified driver for the database chosen
|
||||
* @throws UnsupportedProviderException when an unsupported database is selected
|
||||
*/
|
||||
private String getDriverClassName(String driverName) throws UnsupportedProviderException {
|
||||
try {
|
||||
ApplicationProperties.Driver driver =
|
||||
ApplicationProperties.Driver.valueOf(driverName.toUpperCase());
|
||||
|
||||
switch (driver) {
|
||||
case H2 -> {
|
||||
log.debug("H2 driver selected");
|
||||
return DEFAULT_DRIVER;
|
||||
}
|
||||
case POSTGRESQL -> {
|
||||
log.debug("Postgres driver selected");
|
||||
return POSTGRES_DRIVER;
|
||||
}
|
||||
default -> {
|
||||
log.warn("{} driver selected", driverName);
|
||||
throw new UnsupportedProviderException(
|
||||
driverName + " is not currently supported");
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Unknown driver: {}", driverName);
|
||||
throw new UnsupportedProviderException(driverName + " is not currently supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
package stirling.software.SPDF.config.security.database;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.jdbc.datasource.init.CannotReadScriptException;
|
||||
import org.springframework.jdbc.datasource.init.ScriptException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.exception.BackupNotFoundException;
|
||||
import stirling.software.SPDF.utils.FileInfo;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DatabaseService implements DatabaseInterface {
|
||||
|
||||
public static final String BACKUP_PREFIX = "backup_";
|
||||
public static final String SQL_SUFFIX = ".sql";
|
||||
private static final String BACKUP_DIR = "configs/db/backup/";
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final DataSource dataSource;
|
||||
|
||||
public DatabaseService(ApplicationProperties applicationProperties, DataSource dataSource) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is at least one backup. First checks if the directory exists, then checks if
|
||||
* there are backup scripts within the directory
|
||||
*
|
||||
* @return true if there are backup scripts, false if there are not
|
||||
*/
|
||||
@Override
|
||||
public boolean hasBackup() {
|
||||
Path filePath = Paths.get(BACKUP_DIR);
|
||||
|
||||
if (Files.exists(filePath)) {
|
||||
return !getBackupList().isEmpty();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the backup directory and filter for files with the prefix "backup_" and suffix ".sql"
|
||||
*
|
||||
* @return a <code>List</code> of backup files
|
||||
*/
|
||||
@Override
|
||||
public List<FileInfo> getBackupList() {
|
||||
List<FileInfo> backupFiles = new ArrayList<>();
|
||||
|
||||
if (isH2Database()) {
|
||||
Path backupPath = Paths.get(BACKUP_DIR);
|
||||
|
||||
try (DirectoryStream<Path> stream =
|
||||
Files.newDirectoryStream(
|
||||
backupPath,
|
||||
path ->
|
||||
path.getFileName().toString().startsWith(BACKUP_PREFIX)
|
||||
&& path.getFileName()
|
||||
.toString()
|
||||
.endsWith(SQL_SUFFIX))) {
|
||||
for (Path entry : stream) {
|
||||
BasicFileAttributes attrs =
|
||||
Files.readAttributes(entry, BasicFileAttributes.class);
|
||||
LocalDateTime modificationDate =
|
||||
LocalDateTime.ofInstant(
|
||||
attrs.lastModifiedTime().toInstant(), ZoneId.systemDefault());
|
||||
LocalDateTime creationDate =
|
||||
LocalDateTime.ofInstant(
|
||||
attrs.creationTime().toInstant(), ZoneId.systemDefault());
|
||||
long fileSize = attrs.size();
|
||||
backupFiles.add(
|
||||
new FileInfo(
|
||||
entry.getFileName().toString(),
|
||||
entry.toString(),
|
||||
modificationDate,
|
||||
fileSize,
|
||||
creationDate));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("Error reading backup directory: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return backupFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importDatabase() {
|
||||
if (!hasBackup()) throw new BackupNotFoundException("No backup scripts were found.");
|
||||
|
||||
List<FileInfo> backupList = this.getBackupList();
|
||||
backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed());
|
||||
|
||||
Path latestExport = Paths.get(backupList.get(0).getFilePath());
|
||||
|
||||
executeDatabaseScript(latestExport);
|
||||
}
|
||||
|
||||
/** Imports a database backup from the specified file. */
|
||||
public boolean importDatabaseFromUI(String fileName) {
|
||||
try {
|
||||
importDatabaseFromUI(getBackupFilePath(fileName));
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
log.error(
|
||||
"Error importing database from file: {}, message: {}",
|
||||
fileName,
|
||||
e.getMessage(),
|
||||
e.getCause());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Imports a database backup from the specified path. */
|
||||
public boolean importDatabaseFromUI(Path tempTemplatePath) throws IOException {
|
||||
executeDatabaseScript(tempTemplatePath);
|
||||
LocalDateTime dateNow = LocalDateTime.now();
|
||||
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||
Path insertOutputFilePath =
|
||||
this.getBackupFilePath(
|
||||
BACKUP_PREFIX + "user_" + dateNow.format(myFormatObj) + SQL_SUFFIX);
|
||||
Files.copy(tempTemplatePath, insertOutputFilePath);
|
||||
Files.deleteIfExists(tempTemplatePath);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportDatabase() {
|
||||
List<FileInfo> filteredBackupList =
|
||||
this.getBackupList().stream()
|
||||
.filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_"))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (filteredBackupList.size() > 5) {
|
||||
deleteOldestBackup(filteredBackupList);
|
||||
}
|
||||
|
||||
LocalDateTime dateNow = LocalDateTime.now();
|
||||
DateTimeFormatter myFormatObj = DateTimeFormatter.ofPattern("yyyyMMddHHmm");
|
||||
Path insertOutputFilePath =
|
||||
this.getBackupFilePath(BACKUP_PREFIX + dateNow.format(myFormatObj) + SQL_SUFFIX);
|
||||
|
||||
if (isH2Database()) {
|
||||
String query = "SCRIPT SIMPLE COLUMNS DROP to ?;";
|
||||
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, insertOutputFilePath.toString());
|
||||
stmt.execute();
|
||||
} catch (SQLException e) {
|
||||
log.error("Error during database export: {}", e.getMessage(), e);
|
||||
} catch (CannotReadScriptException e) {
|
||||
log.error("Error during database export: File {} not found", insertOutputFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Database export completed: {}", insertOutputFilePath);
|
||||
}
|
||||
|
||||
private static void deleteOldestBackup(List<FileInfo> filteredBackupList) {
|
||||
try {
|
||||
filteredBackupList.sort(
|
||||
Comparator.comparing(
|
||||
p -> p.getFileName().substring(7, p.getFileName().length() - 4)));
|
||||
|
||||
FileInfo oldestFile = filteredBackupList.get(0);
|
||||
Files.deleteIfExists(Paths.get(oldestFile.getFilePath()));
|
||||
log.info("Deleted oldest backup: {}", oldestFile.getFileName());
|
||||
} catch (IOException e) {
|
||||
log.error("Unable to delete oldest backup, message: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the H2 database version.
|
||||
*
|
||||
* @return <code>String</code> of the H2 version
|
||||
*/
|
||||
public String getH2Version() {
|
||||
String version = "Unknown";
|
||||
|
||||
if (isH2Database()) {
|
||||
try (Connection conn = dataSource.getConnection()) {
|
||||
try (Statement stmt = conn.createStatement();
|
||||
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() AS version")) {
|
||||
if (rs.next()) {
|
||||
version = rs.getString("version");
|
||||
log.info("H2 Database Version: {}", version);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
log.error("Error retrieving H2 version: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private boolean isH2Database() {
|
||||
ApplicationProperties.Datasource datasource =
|
||||
applicationProperties.getSystem().getDatasource();
|
||||
return !datasource.isEnableCustomDatabase()
|
||||
|| datasource.getType().equals(ApplicationProperties.Driver.H2.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup file.
|
||||
*
|
||||
* @return true if successful, false if not
|
||||
*/
|
||||
public boolean deleteBackupFile(String fileName) throws IOException {
|
||||
if (!isValidFileName(fileName)) {
|
||||
log.error("Invalid file name: {}", fileName);
|
||||
return false;
|
||||
}
|
||||
Path filePath = this.getBackupFilePath(fileName);
|
||||
if (Files.deleteIfExists(filePath)) {
|
||||
log.info("Deleted backup file: {}", fileName);
|
||||
return true;
|
||||
} else {
|
||||
log.error("File not found or could not be deleted: {}", fileName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Path for a given backup file name.
|
||||
*
|
||||
* @return the <code>Path</code> object for the given file name
|
||||
*/
|
||||
public Path getBackupFilePath(String fileName) {
|
||||
Path filePath = Paths.get(BACKUP_DIR, fileName).normalize();
|
||||
if (!filePath.startsWith(BACKUP_DIR)) {
|
||||
throw new SecurityException("Path traversal detected");
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private void executeDatabaseScript(Path scriptPath) {
|
||||
if (isH2Database()) {
|
||||
String query = "RUNSCRIPT from ?;";
|
||||
|
||||
try (Connection conn = dataSource.getConnection();
|
||||
PreparedStatement stmt = conn.prepareStatement(query)) {
|
||||
stmt.setString(1, scriptPath.toString());
|
||||
stmt.execute();
|
||||
} catch (SQLException e) {
|
||||
log.error("Error during database import: {}", e.getMessage(), e);
|
||||
} catch (ScriptException e) {
|
||||
log.error("Error: File {} not found", scriptPath.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Database import completed: {}", scriptPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for invalid characters or sequences
|
||||
*
|
||||
* @return true if it contains no invalid characters, false if it does
|
||||
*/
|
||||
private boolean isValidFileName(String fileName) {
|
||||
return fileName != null
|
||||
&& !fileName.contains("..")
|
||||
&& !fileName.contains("/")
|
||||
&& !fileName.contains("\\")
|
||||
&& !fileName.contains(":")
|
||||
&& !fileName.contains("*")
|
||||
&& !fileName.contains("?")
|
||||
&& !fileName.contains("\"")
|
||||
&& !fileName.contains("<")
|
||||
&& !fileName.contains(">")
|
||||
&& !fileName.contains("|");
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,24 @@
|
||||
package stirling.software.SPDF.config.security.database;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.DatabaseInterface;
|
||||
import stirling.software.SPDF.model.provider.UnsupportedProviderException;
|
||||
|
||||
@Component
|
||||
public class ScheduledTasks {
|
||||
|
||||
private final DatabaseBackupHelper databaseBackupService;
|
||||
private final DatabaseInterface databaseService;
|
||||
|
||||
public ScheduledTasks(DatabaseBackupHelper databaseBackupService) {
|
||||
this.databaseBackupService = databaseBackupService;
|
||||
public ScheduledTasks(DatabaseInterface databaseService) {
|
||||
this.databaseService = databaseService;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void performBackup() throws IOException {
|
||||
databaseBackupService.exportDatabase();
|
||||
public void performBackup() throws SQLException, UnsupportedProviderException {
|
||||
databaseService.exportDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user