fix backup restoration

This commit is contained in:
Stefan Hardegger
2026-07-09 09:05:18 +02:00
parent c42d0a5d3a
commit e03031ea05

View File

@@ -178,7 +178,10 @@ public class DatabaseManagementService {
/**
* Restore from complete backup (ZIP format)
*/
@Transactional(timeout = 1800) // 30 minutes timeout for large backup restores
// Not @Transactional: this method launches psql as an external process. If a Spring
// transaction were active here, the ROW EXCLUSIVE locks held by clearAllDataAndFiles()
// would block psql's DROP TABLE IF EXISTS (ACCESS EXCLUSIVE), causing a deadlock.
// Each inner method (clearAllDataAndFiles, restoreFromBackup) manages its own transaction.
public void restoreFromCompleteBackup(InputStream backupStream) throws IOException, SQLException {
String currentLibraryId = libraryService.getCurrentLibraryId();
System.err.println("Starting complete backup restore for library: " + currentLibraryId);
@@ -498,92 +501,6 @@ public class DatabaseManagementService {
return totalDeleted;
}
/**
* Parses SQL content into individual statements, properly handling semicolons inside string literals
*/
private List<String> parseStatements(String sql) {
List<String> statements = new ArrayList<>();
StringBuilder currentStatement = new StringBuilder();
boolean inString = false;
for (int i = 0; i < sql.length(); i++) {
char c = sql.charAt(i);
if (c == '\'' && !inString) {
// Start of string literal
inString = true;
currentStatement.append(c);
} else if (c == '\'' && inString) {
// Potential end of string literal
currentStatement.append(c);
// Check if this is an escaped quote (doubled single quote)
if (i + 1 < sql.length() && sql.charAt(i + 1) == '\'') {
// This is an escaped quote, skip the next quote
i++;
currentStatement.append('\'');
} else {
// End of string literal
inString = false;
}
} else if (c == ';' && !inString) {
// Statement terminator outside of string literal
String statement = currentStatement.toString().trim();
if (!statement.isEmpty()) {
statements.add(statement);
}
currentStatement = new StringBuilder();
} else {
currentStatement.append(c);
}
}
// Add final statement if any
String finalStatement = currentStatement.toString().trim();
if (!finalStatement.isEmpty()) {
statements.add(finalStatement);
}
return statements;
}
/**
* Formats a database value for SQL insertion, handling proper escaping
*/
private String formatSqlValue(Object value) {
if (value == null) {
return "NULL";
}
if (value instanceof Boolean) {
return ((Boolean) value) ? "true" : "false";
}
if (value instanceof Number) {
return value.toString();
}
// Handle all other types as strings (String, UUID, Timestamp, CLOB, TEXT, etc.)
String stringValue;
// Special handling for CLOB types
if (value instanceof Clob) {
Clob clob = (Clob) value;
try {
stringValue = clob.getSubString(1, (int) clob.length());
} catch (SQLException e) {
stringValue = value.toString();
}
} else {
stringValue = value.toString();
}
// Escape single quotes by replacing ' with '' and wrap in quotes
String escapedValue = stringValue.replace("'", "''");
return "'" + escapedValue + "'";
}
/**
* Clear all data AND files (for complete restore)
*/
@@ -654,219 +571,6 @@ public class DatabaseManagementService {
}
}
/**
* Ensure database schema exists before restoring backup data.
* This creates all necessary tables, indexes, and constraints if they don't exist.
*/
private void ensureDatabaseSchemaExists(Connection connection) throws SQLException {
try {
// Check if a key table exists to determine if schema is already created
String checkTableQuery = "SELECT 1 FROM information_schema.tables WHERE table_name = 'stories' LIMIT 1";
try (PreparedStatement stmt = connection.prepareStatement(checkTableQuery);
var resultSet = stmt.executeQuery()) {
if (resultSet.next()) {
System.err.println("Database schema already exists, skipping schema creation.");
return; // Schema exists
}
}
System.err.println("Creating database schema for restore in library: " + libraryService.getCurrentLibraryId());
// Create the schema using the same DDL as LibraryService
String[] createTableStatements = {
// Authors table
"""
CREATE TABLE authors (
author_rating integer,
created_at timestamp(6) not null,
updated_at timestamp(6) not null,
id uuid not null,
avatar_image_path varchar(255),
name varchar(255) not null,
notes TEXT,
primary key (id)
)
""",
// Author URLs table
"""
CREATE TABLE author_urls (
author_id uuid not null,
url varchar(255)
)
""",
// Series table
"""
CREATE TABLE series (
created_at timestamp(6) not null,
id uuid not null,
description varchar(1000),
name varchar(255) not null,
primary key (id)
)
""",
// Tags table
"""
CREATE TABLE tags (
color varchar(7),
created_at timestamp(6) not null,
id uuid not null,
description varchar(500),
name varchar(255) not null unique,
primary key (id)
)
""",
// Tag aliases table
"""
CREATE TABLE tag_aliases (
created_from_merge boolean not null,
created_at timestamp(6) not null,
canonical_tag_id uuid not null,
id uuid not null,
alias_name varchar(255) not null unique,
primary key (id)
)
""",
// Collections table
"""
CREATE TABLE collections (
is_archived boolean not null,
rating integer,
created_at timestamp(6) not null,
updated_at timestamp(6) not null,
id uuid not null,
cover_image_path varchar(500),
name varchar(500) not null,
description TEXT,
primary key (id)
)
""",
// Stories table
"""
CREATE TABLE stories (
is_read boolean,
rating integer,
reading_position integer,
volume integer,
word_count integer,
created_at timestamp(6) not null,
last_read_at timestamp(6),
updated_at timestamp(6) not null,
author_id uuid,
id uuid not null,
series_id uuid,
description varchar(1000),
content_html TEXT,
content_plain TEXT,
cover_path varchar(255),
source_url varchar(255),
summary TEXT,
title varchar(255) not null,
primary key (id)
)
""",
// Reading positions table
"""
CREATE TABLE reading_positions (
chapter_index integer,
character_position integer,
percentage_complete float(53),
word_position integer,
created_at timestamp(6) not null,
updated_at timestamp(6) not null,
id uuid not null,
story_id uuid not null,
context_after varchar(500),
context_before varchar(500),
chapter_title varchar(255),
epub_cfi TEXT,
primary key (id)
)
""",
// Junction tables
"""
CREATE TABLE story_tags (
story_id uuid not null,
tag_id uuid not null,
primary key (story_id, tag_id)
)
""",
"""
CREATE TABLE collection_stories (
position integer not null,
added_at timestamp(6) not null,
collection_id uuid not null,
story_id uuid not null,
primary key (collection_id, story_id),
unique (collection_id, position)
)
""",
"""
CREATE TABLE collection_tags (
collection_id uuid not null,
tag_id uuid not null,
primary key (collection_id, tag_id)
)
"""
};
String[] createIndexStatements = {
"CREATE INDEX idx_reading_position_story ON reading_positions (story_id)"
};
String[] createConstraintStatements = {
// Foreign key constraints
"ALTER TABLE author_urls ADD CONSTRAINT FKdqhp51m0uveybsts098gd79uo FOREIGN KEY (author_id) REFERENCES authors",
"ALTER TABLE stories ADD CONSTRAINT FKhwecpqeaxy40ftrctef1u7gw7 FOREIGN KEY (author_id) REFERENCES authors",
"ALTER TABLE stories ADD CONSTRAINT FK1kulyvy7wwcolp2gkndt57cp7 FOREIGN KEY (series_id) REFERENCES series",
"ALTER TABLE reading_positions ADD CONSTRAINT FKglfhdhflan3pgyr2u0gxi21i5 FOREIGN KEY (story_id) REFERENCES stories",
"ALTER TABLE story_tags ADD CONSTRAINT FKmans33ijt0nf65t0sng2r848j FOREIGN KEY (tag_id) REFERENCES tags",
"ALTER TABLE story_tags ADD CONSTRAINT FKq9guid7swnjxwdpgxj3jo1rsi FOREIGN KEY (story_id) REFERENCES stories",
"ALTER TABLE tag_aliases ADD CONSTRAINT FKqfsawmcj3ey4yycb6958y24ch FOREIGN KEY (canonical_tag_id) REFERENCES tags",
"ALTER TABLE collection_stories ADD CONSTRAINT FKr55ho4vhj0wp03x13iskr1jds FOREIGN KEY (collection_id) REFERENCES collections",
"ALTER TABLE collection_stories ADD CONSTRAINT FK7n41tbbrt7r2e81hpu3612r1o FOREIGN KEY (story_id) REFERENCES stories",
"ALTER TABLE collection_tags ADD CONSTRAINT FKceq7ggev8n8ibjui1x5yo4x67 FOREIGN KEY (tag_id) REFERENCES tags",
"ALTER TABLE collection_tags ADD CONSTRAINT FKq9sa5s8csdpbphrvb48tts8jt FOREIGN KEY (collection_id) REFERENCES collections"
};
// Create tables
for (String sql : createTableStatements) {
try (var statement = connection.createStatement()) {
statement.executeUpdate(sql);
}
}
// Create indexes
for (String sql : createIndexStatements) {
try (var statement = connection.createStatement()) {
statement.executeUpdate(sql);
}
}
// Create constraints
for (String sql : createConstraintStatements) {
try (var statement = connection.createStatement()) {
statement.executeUpdate(sql);
}
}
System.err.println("Database schema created successfully for restore.");
} catch (SQLException e) {
System.err.println("Error creating database schema: " + e.getMessage());
throw e;
}
}
/**
* Add database dump to ZIP archive
*/