fix backup restoration
This commit is contained in:
@@ -178,7 +178,10 @@ public class DatabaseManagementService {
|
|||||||
/**
|
/**
|
||||||
* Restore from complete backup (ZIP format)
|
* 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 {
|
public void restoreFromCompleteBackup(InputStream backupStream) throws IOException, SQLException {
|
||||||
String currentLibraryId = libraryService.getCurrentLibraryId();
|
String currentLibraryId = libraryService.getCurrentLibraryId();
|
||||||
System.err.println("Starting complete backup restore for library: " + currentLibraryId);
|
System.err.println("Starting complete backup restore for library: " + currentLibraryId);
|
||||||
@@ -498,92 +501,6 @@ public class DatabaseManagementService {
|
|||||||
return totalDeleted;
|
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)
|
* 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
|
* Add database dump to ZIP archive
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user