diff --git a/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java b/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java index 55107123e99..18fbf788308 100644 --- a/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java +++ b/src/main/java/stirling/software/SPDF/config/interfaces/DatabaseInterface.java @@ -9,5 +9,9 @@ public interface DatabaseInterface { void exportDatabase() throws SQLException, UnsupportedProviderException; + void importDatabase(); + + boolean hasBackup(); + List getBackupList(); } diff --git a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java index 103181fc4be..7d0d5df2609 100644 --- a/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java +++ b/src/main/java/stirling/software/SPDF/config/security/InitialSecuritySetup.java @@ -26,6 +26,10 @@ public class InitialSecuritySetup { @PostConstruct public void init() { try { + if (databaseService.hasBackup()) { + databaseService.importDatabase(); + } + if (!userService.hasUsers()) { initializeAdminUser(); } diff --git a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java index 3a0b01d2c22..8d52b3dd205 100644 --- a/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java +++ b/src/main/java/stirling/software/SPDF/config/security/database/DatabaseService.java @@ -28,6 +28,7 @@ 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.model.provider.UnsupportedProviderException; import stirling.software.SPDF.utils.FileInfo; @@ -37,19 +38,35 @@ public class DatabaseService implements DatabaseInterface { public static final String BACKUP_PREFIX = "backup_"; public static final String SQL_SUFFIX = ".sql"; - private static final Path BACKUP_PATH = Paths.get("configs/db/backup/"); + private static final String BACKUP_DIR = "configs/db/backup/"; @Autowired private DatabaseConfig databaseConfig; + /** + * Checks if there is at least one backup + * + * @return true if there are backup scripts, false if there are not + */ + @Override + public boolean hasBackup() { + Path filePath = Paths.get(BACKUP_DIR + "*"); + + return Files.exists(filePath); + } + + /** + * Read the backup directory and filter for files with the prefix "backup_" and suffix ".sql" + * + * @return a List of backup files + */ @Override public List getBackupList() { List backupFiles = new ArrayList<>(); + Path backupPath = Paths.get(BACKUP_DIR); - // Read the backup directory and filter for files with the prefix "backup_" and suffix - // ".sql" try (DirectoryStream stream = Files.newDirectoryStream( - BACKUP_PATH, + backupPath, path -> path.getFileName().toString().startsWith(BACKUP_PREFIX) && path.getFileName().toString().endsWith(SQL_SUFFIX))) { @@ -77,7 +94,17 @@ public List getBackupList() { return backupFiles; } - // Imports a database backup from the specified file. + @Override + public void importDatabase() { + if (!hasBackup()) throw new BackupNotFoundException("No backup scripts were found."); + + List backupList = this.getBackupList(); + backupList.sort(Comparator.comparing(FileInfo::getModificationDate).reversed()); + + executeDatabaseScript(Paths.get(backupList.get(0).getFilePath())); + } + + /** Imports a database backup from the specified file. */ public boolean importDatabaseFromUI(String fileName) { try { importDatabaseFromUI(getBackupFilePath(fileName)); @@ -92,7 +119,7 @@ public boolean importDatabaseFromUI(String fileName) { } } - // Imports a database backup from the specified path. + /** Imports a database backup from the specified path. */ private void importDatabaseFromUI(Path tempTemplatePath) throws IOException { executeDatabaseScript(tempTemplatePath); LocalDateTime dateNow = LocalDateTime.now(); @@ -104,9 +131,9 @@ private void importDatabaseFromUI(Path tempTemplatePath) throws IOException { Files.deleteIfExists(tempTemplatePath); } + /** Filter and delete old backups if there are more than 5 */ @Override public void exportDatabase() throws SQLException, UnsupportedProviderException { - // Filter and delete old backups if there are more than 5 List filteredBackupList = this.getBackupList().stream() .filter(backup -> !backup.getFileName().startsWith(BACKUP_PREFIX + "user_")) @@ -149,7 +176,11 @@ private static void deleteOldestBackup(List filteredBackupList) { } } - // Retrieves the H2 database version. + /** + * Retrieves the H2 database version. + * + * @return String of the H2 version + */ public String getH2Version() { String version = "Unknown"; @@ -175,7 +206,11 @@ public String getH2Version() { return version; } - // Deletes a backup file. + /** + * 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); @@ -191,10 +226,14 @@ public boolean deleteBackupFile(String fileName) throws IOException { } } - // Gets the Path object for a given backup file name. + /** + * Gets the Path for a given backup file name. + * + * @return the Path object for the given file name + */ public Path getBackupFilePath(String fileName) { - Path filePath = Paths.get(BACKUP_PATH.toString(), fileName).normalize(); - if (!filePath.startsWith(BACKUP_PATH)) { + Path filePath = Paths.get(BACKUP_DIR, fileName).normalize(); + if (!filePath.startsWith(BACKUP_DIR)) { throw new SecurityException("Path traversal detected"); } return filePath; @@ -212,8 +251,12 @@ private void executeDatabaseScript(Path scriptPath) { } } + /** + * Checks for invalid characters or sequences + * + * @return true if it contains no invalid characters, false if it does + */ private boolean isValidFileName(String fileName) { - // Check for invalid characters or sequences return fileName != null && !fileName.contains("..") && !fileName.contains("/") diff --git a/src/main/java/stirling/software/SPDF/model/exception/BackupNotFoundException.java b/src/main/java/stirling/software/SPDF/model/exception/BackupNotFoundException.java new file mode 100644 index 00000000000..7e0649ce9c3 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/model/exception/BackupNotFoundException.java @@ -0,0 +1,7 @@ +package stirling.software.SPDF.model.exception; + +public class BackupNotFoundException extends RuntimeException { + public BackupNotFoundException(String message) { + super(message); + } +} diff --git a/src/test/java/stirling/software/SPDF/config/security/database/DatabaseServiceTest.java b/src/test/java/stirling/software/SPDF/config/security/database/DatabaseServiceTest.java new file mode 100644 index 00000000000..f6790d08910 --- /dev/null +++ b/src/test/java/stirling/software/SPDF/config/security/database/DatabaseServiceTest.java @@ -0,0 +1,33 @@ +package stirling.software.SPDF.config.security.database; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class DatabaseServiceTest { + + private final Path BACKUP_PATH = Paths.get("configs/db/backup/*"); + + @Mock + private DatabaseConfig databaseConfig; + + @InjectMocks + private DatabaseService databaseService; + + @Test + void testHasBackups() throws IOException { + Files.createDirectories(BACKUP_PATH); + + assertTrue(databaseService.hasBackup()); + } +} \ No newline at end of file