mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f9f4be39f |
+172
-35
@@ -36,6 +36,12 @@ import stirling.software.proprietary.security.model.exception.BackupNotFoundExce
|
||||
@Service
|
||||
public class DatabaseService implements DatabaseServiceInterface {
|
||||
|
||||
private enum Dialect {
|
||||
H2,
|
||||
POSTGRES,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public static final String BACKUP_PREFIX = "backup_";
|
||||
public static final String SQL_SUFFIX = ".sql";
|
||||
private final Path BACKUP_DIR;
|
||||
@@ -45,10 +51,26 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
|
||||
public DatabaseService(
|
||||
ApplicationProperties.Datasource datasourceProps, DataSource dataSource) {
|
||||
this.BACKUP_DIR =
|
||||
Paths.get(InstallationPathConfig.getConfigPath(), "db", "backup").normalize();
|
||||
this.datasourceProps = datasourceProps;
|
||||
this.dataSource = dataSource;
|
||||
this.BACKUP_DIR = resolveBackupDir(datasourceProps);
|
||||
}
|
||||
|
||||
private Path resolveBackupDir(ApplicationProperties.Datasource ds) {
|
||||
String base = InstallationPathConfig.getConfigPath();
|
||||
String sub = "unknown";
|
||||
boolean custom = ds.isEnableCustomDatabase();
|
||||
String type = ds.getType() == null ? "" : ds.getType().toUpperCase();
|
||||
String url =
|
||||
ds.getCustomDatabaseUrl() == null ? "" : ds.getCustomDatabaseUrl().toLowerCase();
|
||||
if (!custom) {
|
||||
sub = "h2";
|
||||
} else if ("H2".equals(type) && url.contains("h2")) {
|
||||
sub = "h2";
|
||||
} else if ("POSTGRESQL".equals(type) && url.contains("postgres")) {
|
||||
sub = "postgres";
|
||||
}
|
||||
return Paths.get(base, "db", "backup", sub).normalize();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,38 +99,33 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
public List<FileInfo> getBackupList() {
|
||||
List<FileInfo> backupFiles = new ArrayList<>();
|
||||
|
||||
if (isH2Database()) {
|
||||
createBackupDirectory();
|
||||
createBackupDirectory();
|
||||
|
||||
try (DirectoryStream<Path> stream =
|
||||
Files.newDirectoryStream(
|
||||
BACKUP_DIR,
|
||||
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);
|
||||
try (DirectoryStream<Path> stream =
|
||||
Files.newDirectoryStream(
|
||||
BACKUP_DIR,
|
||||
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;
|
||||
@@ -195,6 +212,56 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
}
|
||||
|
||||
log.info("Database export completed: {}", insertOutputFilePath);
|
||||
} else if (isPostgresDatabase()) {
|
||||
// Use pg_dump to export PostgreSQL database
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("pg_dump");
|
||||
if (datasourceProps.getHostName() != null) {
|
||||
command.add("-h");
|
||||
command.add(datasourceProps.getHostName());
|
||||
}
|
||||
if (datasourceProps.getPort() != null) {
|
||||
command.add("-p");
|
||||
command.add(String.valueOf(datasourceProps.getPort()));
|
||||
}
|
||||
if (datasourceProps.getUsername() != null) {
|
||||
command.add("-U");
|
||||
command.add(datasourceProps.getUsername());
|
||||
}
|
||||
command.add("-F");
|
||||
command.add("p"); // plain SQL
|
||||
command.add("--inserts");
|
||||
command.add("-f");
|
||||
command.add(insertOutputFilePath.toString());
|
||||
// Database name
|
||||
if (datasourceProps.getName() != null) {
|
||||
command.add(datasourceProps.getName());
|
||||
}
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
// Set password via env var to avoid prompt
|
||||
if (datasourceProps.getPassword() != null) {
|
||||
pb.environment().put("PGPASSWORD", datasourceProps.getPassword());
|
||||
}
|
||||
pb.redirectErrorStream(true);
|
||||
try {
|
||||
Process process = pb.start();
|
||||
int exit = process.waitFor();
|
||||
if (exit != 0) {
|
||||
log.error(
|
||||
"pg_dump exited with code {} while exporting to {}",
|
||||
exit,
|
||||
insertOutputFilePath);
|
||||
} else {
|
||||
log.info("Database export completed: {}", insertOutputFilePath);
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("Error during PostgreSQL database export: {}", e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
log.warn(
|
||||
"Database export not implemented for this driver. Only H2 and PostgreSQL are supported.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,9 +307,7 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
private boolean isH2Database() {
|
||||
boolean isTypeH2 =
|
||||
datasourceProps.getType().equalsIgnoreCase(ApplicationProperties.Driver.H2.name());
|
||||
boolean isDBUrlH2 =
|
||||
datasourceProps.getCustomDatabaseUrl().contains("h2")
|
||||
|| datasourceProps.getCustomDatabaseUrl().contains("H2");
|
||||
boolean isDBUrlH2 = datasourceProps.getCustomDatabaseUrl().toLowerCase().contains("h2");
|
||||
boolean isCustomDatabase = datasourceProps.isEnableCustomDatabase();
|
||||
|
||||
if (isCustomDatabase) {
|
||||
@@ -267,6 +332,35 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
return !isCustomDatabase || isH2;
|
||||
}
|
||||
|
||||
private boolean isPostgresDatabase() {
|
||||
boolean isTypePg =
|
||||
datasourceProps
|
||||
.getType()
|
||||
.equalsIgnoreCase(ApplicationProperties.Driver.POSTGRESQL.name());
|
||||
String url =
|
||||
datasourceProps.getCustomDatabaseUrl() == null
|
||||
? ""
|
||||
: datasourceProps.getCustomDatabaseUrl();
|
||||
boolean isDBUrlPg = url.toLowerCase().contains("postgres");
|
||||
boolean isCustomDatabase = datasourceProps.isEnableCustomDatabase();
|
||||
|
||||
if (isCustomDatabase) {
|
||||
if (isTypePg && !isDBUrlPg) {
|
||||
log.warn(
|
||||
"Datasource type is POSTGRESQL, but the URL does not contain 'postgres'. Please check your configuration.");
|
||||
throw new IllegalStateException(
|
||||
"Datasource type is POSTGRESQL, but the URL does not contain 'postgres'. Please check your configuration.");
|
||||
} else if (!isTypePg && isDBUrlPg) {
|
||||
log.warn(
|
||||
"Datasource URL contains 'postgres', but the type is not POSTGRESQL. Please check your configuration.");
|
||||
throw new IllegalStateException(
|
||||
"Datasource URL contains 'postgres', but the type is not POSTGRESQL. Please check your configuration.");
|
||||
}
|
||||
}
|
||||
boolean isPg = isTypePg && isDBUrlPg;
|
||||
return isCustomDatabase && isPg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a backup file.
|
||||
*
|
||||
@@ -314,6 +408,49 @@ public class DatabaseService implements DatabaseServiceInterface {
|
||||
} catch (ScriptException e) {
|
||||
log.error("Error: File {} not found", scriptPath.toString(), e);
|
||||
}
|
||||
} else if (isPostgresDatabase()) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("psql");
|
||||
|
||||
if (datasourceProps.getHostName() != null) {
|
||||
command.add("-h");
|
||||
command.add(datasourceProps.getHostName());
|
||||
}
|
||||
if (datasourceProps.getPort() != null) {
|
||||
command.add("-p");
|
||||
command.add(String.valueOf(datasourceProps.getPort()));
|
||||
}
|
||||
if (datasourceProps.getUsername() != null) {
|
||||
command.add("-U");
|
||||
command.add(datasourceProps.getUsername());
|
||||
}
|
||||
if (datasourceProps.getName() != null) {
|
||||
command.add("-d");
|
||||
command.add(datasourceProps.getName());
|
||||
}
|
||||
command.add("-f");
|
||||
command.add(scriptPath.toString());
|
||||
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
if (datasourceProps.getPassword() != null) {
|
||||
pb.environment().put("PGPASSWORD", datasourceProps.getPassword());
|
||||
}
|
||||
|
||||
pb.redirectErrorStream(true);
|
||||
|
||||
try {
|
||||
Process process = pb.start();
|
||||
int exit = process.waitFor();
|
||||
if (exit != 0) {
|
||||
log.error("psql exited with code {} while importing from {}", exit, scriptPath);
|
||||
} else {
|
||||
log.info("Database import completed: {}", scriptPath);
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.error("Error during PostgreSQL database import: {}", e.getMessage(), e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("Database import completed: {}", scriptPath);
|
||||
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package stirling.software.proprietary.security.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.sql.SQLException;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.model.ApplicationProperties;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class DatabaseServiceTest {
|
||||
|
||||
private Path tempBaseDir;
|
||||
private MockedStatic<InstallationPathConfig> mockedInstallationConfig;
|
||||
|
||||
@Mock private DataSource dataSource;
|
||||
|
||||
private ApplicationProperties.Datasource h2DatasourceProps;
|
||||
private ApplicationProperties.Datasource pgDatasourceProps;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws IOException {
|
||||
tempBaseDir = Files.createTempDirectory("dbservice-test-");
|
||||
mockedInstallationConfig = Mockito.mockStatic(InstallationPathConfig.class);
|
||||
mockedInstallationConfig
|
||||
.when(InstallationPathConfig::getConfigPath)
|
||||
.thenReturn(tempBaseDir.toString());
|
||||
|
||||
// H2 default (custom disabled)
|
||||
h2DatasourceProps = new ApplicationProperties.Datasource();
|
||||
h2DatasourceProps.setEnableCustomDatabase(false);
|
||||
h2DatasourceProps.setType(ApplicationProperties.Driver.H2.name());
|
||||
h2DatasourceProps.setCustomDatabaseUrl("jdbc:h2:file:./data");
|
||||
|
||||
// PostgreSQL custom enabled
|
||||
pgDatasourceProps = new ApplicationProperties.Datasource();
|
||||
pgDatasourceProps.setEnableCustomDatabase(true);
|
||||
pgDatasourceProps.setType(ApplicationProperties.Driver.POSTGRESQL.name());
|
||||
pgDatasourceProps.setCustomDatabaseUrl("jdbc:postgresql://localhost:5432/mydb");
|
||||
pgDatasourceProps.setHostName("localhost");
|
||||
pgDatasourceProps.setPort(5432);
|
||||
pgDatasourceProps.setName("mydb");
|
||||
pgDatasourceProps.setUsername("user");
|
||||
pgDatasourceProps.setPassword("pass");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
if (mockedInstallationConfig != null) {
|
||||
mockedInstallationConfig.close();
|
||||
}
|
||||
if (tempBaseDir != null) {
|
||||
// Cleanup temp files
|
||||
Files.walk(tempBaseDir)
|
||||
.sorted((a, b) -> b.getNameCount() - a.getNameCount())
|
||||
.forEach(
|
||||
p -> {
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void backupDirectoryResolution_h2() {
|
||||
DatabaseService service = new DatabaseService(h2DatasourceProps, dataSource);
|
||||
Path path = service.getBackupFilePath("backup_test.sql");
|
||||
assertTrue(
|
||||
path.toString().contains("/db/backup/h2/")
|
||||
|| path.toString().contains("\\db\\backup\\h2\\"),
|
||||
"Backup directory should resolve to h2 subfolder");
|
||||
assertTrue(Files.exists(path.getParent()), "Backup directory should be created");
|
||||
}
|
||||
|
||||
@Test
|
||||
void backupDirectoryResolution_postgres() {
|
||||
DatabaseService service = new DatabaseService(pgDatasourceProps, dataSource);
|
||||
Path path = service.getBackupFilePath("backup_test.sql");
|
||||
assertTrue(
|
||||
path.toString().contains("/db/backup/postgres/")
|
||||
|| path.toString().contains("\\db\\backup\\postgres\\"),
|
||||
"Backup directory should resolve to postgres subfolder");
|
||||
assertTrue(Files.exists(path.getParent()), "Backup directory should be created");
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasBackup_false_then_true() throws IOException {
|
||||
DatabaseService service = new DatabaseService(h2DatasourceProps, dataSource);
|
||||
// Initially no backups
|
||||
assertFalse(service.hasBackup());
|
||||
|
||||
// Create a valid backup file
|
||||
Path backup = service.getBackupFilePath("backup_202001010000.sql");
|
||||
Files.createDirectories(backup.getParent());
|
||||
Files.createFile(backup);
|
||||
|
||||
assertTrue(service.hasBackup());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBackupFilePath_preventsPathTraversal() {
|
||||
DatabaseService service = new DatabaseService(h2DatasourceProps, dataSource);
|
||||
assertThrows(SecurityException.class, () -> service.getBackupFilePath("../evil.sql"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void importDatabaseFromUI_copiesAndDeletesTemp_evenIfExecutionFails() throws Exception {
|
||||
// Arrange H2 branch; mock connection acquisition to throw so that executeDatabaseScript
|
||||
// logs error
|
||||
when(dataSource.getConnection()).thenThrow(new SQLException("No DB in unit test"));
|
||||
DatabaseService service = new DatabaseService(h2DatasourceProps, dataSource);
|
||||
|
||||
Path tempSql = Files.createTempFile("temp-import-", ".sql");
|
||||
Files.writeString(tempSql, "-- test content");
|
||||
|
||||
// Act
|
||||
boolean result = service.importDatabaseFromUI(tempSql);
|
||||
|
||||
// Assert
|
||||
assertTrue(result, "Method should return true regardless of execution outcome");
|
||||
assertFalse(
|
||||
Files.exists(tempSql), "Temporary uploaded file should be deleted after import");
|
||||
|
||||
// The copied file should exist in backup directory with user_ prefix
|
||||
Path parent = Files.createDirectories(service.getBackupFilePath("dummy.sql").getParent());
|
||||
// find a file starting with backup_user_
|
||||
boolean found =
|
||||
Files.list(parent)
|
||||
.anyMatch(
|
||||
p ->
|
||||
p.getFileName().toString().startsWith("backup_user_")
|
||||
&& p.getFileName().toString().endsWith(".sql"));
|
||||
assertTrue(found, "A user_ prefixed backup copy should be created");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBackupList_sortsByModificationDateDesc() throws Exception {
|
||||
DatabaseService service = new DatabaseService(h2DatasourceProps, dataSource);
|
||||
Path dir = service.getBackupFilePath("backup_dummy.sql").getParent();
|
||||
Files.createDirectories(dir);
|
||||
|
||||
Path older = dir.resolve("backup_202001010000.sql");
|
||||
Path newer = dir.resolve("backup_202001020000.sql");
|
||||
Files.writeString(older, "-- old");
|
||||
Files.writeString(newer, "-- new");
|
||||
Files.setLastModifiedTime(older, FileTime.fromMillis(1_000L));
|
||||
Files.setLastModifiedTime(newer, FileTime.fromMillis(2_000L));
|
||||
|
||||
var list = service.getBackupList();
|
||||
assertEquals(2, list.size());
|
||||
// Newest should be first after we sort in importDatabase, but getBackupList returns
|
||||
// unsorted.
|
||||
// We'll verify importDatabase picks the latest by modification time by creating a no-op
|
||||
// DataSource
|
||||
when(dataSource.getConnection()).thenThrow(new SQLException("No DB in unit test"));
|
||||
service.importDatabase();
|
||||
// If no exception thrown, method executed and chose a file; functional behavior verified by
|
||||
// no crash.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user