fix backup restoration
This commit is contained in:
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user