Merge pull request #6 from ComfyAssets/backup-restore

Backup restore
This commit is contained in:
Vito
2025-05-27 14:31:32 -07:00
committed by GitHub
3 changed files with 350 additions and 13 deletions
+170
View File
@@ -200,6 +200,15 @@ class PromptManagerAPI:
async def export_prompts_route(request):
return await self.export_prompts(request)
# Database backup and restore
@routes.get("/prompt_manager/backup")
async def backup_database_route(request):
return await self.backup_database(request)
@routes.post("/prompt_manager/restore")
async def restore_database_route(request):
return await self.restore_database(request)
# Gallery endpoints
@routes.get("/prompt_manager/prompts/{prompt_id}/images")
async def get_prompt_images_route(request):
@@ -1447,3 +1456,164 @@ class PromptManagerAPI:
'success': False,
'error': f'Maintenance failed: {str(e)}'
}, status=500)
async def backup_database(self, request):
"""
Backup the entire prompts.db database file.
GET /prompt_manager/backup
"""
try:
import os
import shutil
import tempfile
from pathlib import Path
# Get the database path
db_path = "prompts.db"
if not os.path.exists(db_path):
return web.json_response({
'success': False,
'error': 'Database file not found'
}, status=404)
# Create a temporary copy of the database
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as temp_file:
temp_path = temp_file.name
# Copy the database file
shutil.copy2(db_path, temp_path)
# Read the file content
with open(temp_path, 'rb') as f:
file_data = f.read()
# Clean up temporary file
os.unlink(temp_path)
# Generate filename with timestamp
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"prompts_backup_{timestamp}.db"
return web.Response(
body=file_data,
content_type='application/octet-stream',
headers={
'Content-Disposition': f'attachment; filename="{filename}"',
'Content-Length': str(len(file_data))
}
)
except Exception as e:
print(f"[PromptManager API] Backup error: {e}")
import traceback
traceback.print_exc()
return web.json_response({
'success': False,
'error': f'Failed to backup database: {str(e)}'
}, status=500)
async def restore_database(self, request):
"""
Restore the prompts.db database from uploaded file.
POST /prompt_manager/restore
"""
try:
import os
import shutil
import tempfile
import sqlite3
from pathlib import Path
# Get the uploaded file
reader = await request.multipart()
field = await reader.next()
if not field or field.name != 'database_file':
return web.json_response({
'success': False,
'error': 'No database file uploaded. Expected field name: database_file'
}, status=400)
# Read the uploaded file content
file_data = await field.read()
if not file_data:
return web.json_response({
'success': False,
'error': 'Uploaded file is empty'
}, status=400)
# Create a temporary file to validate the database
with tempfile.NamedTemporaryFile(delete=False, suffix='.db') as temp_file:
temp_path = temp_file.name
temp_file.write(file_data)
try:
# Validate that it's a valid SQLite database with expected structure
with sqlite3.connect(temp_path) as conn:
conn.row_factory = sqlite3.Row
# Check if it has the prompts table
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='prompts'")
if not cursor.fetchone():
raise ValueError("Database does not contain a 'prompts' table")
# Check basic structure of prompts table
cursor = conn.execute("PRAGMA table_info(prompts)")
columns = [row['name'] for row in cursor.fetchall()]
required_columns = ['id', 'text', 'created_at']
for col in required_columns:
if col not in columns:
raise ValueError(f"Database missing required column: {col}")
# Get basic stats for validation
cursor = conn.execute("SELECT COUNT(*) as count FROM prompts")
prompt_count = cursor.fetchone()['count']
# If validation passes, backup current database and restore
db_path = "prompts.db"
backup_path = f"{db_path}.backup_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}"
# Create backup of current database if it exists
if os.path.exists(db_path):
shutil.copy2(db_path, backup_path)
print(f"[PromptManager] Current database backed up to: {backup_path}")
# Replace current database with uploaded one
shutil.copy2(temp_path, db_path)
# Reinitialize the database connection
self.db = PromptDatabase()
return web.json_response({
'success': True,
'message': f'Database restored successfully. Found {prompt_count} prompts.',
'prompt_count': prompt_count,
'backup_created': backup_path if os.path.exists(db_path) else None
})
except sqlite3.Error as e:
return web.json_response({
'success': False,
'error': f'Invalid SQLite database: {str(e)}'
}, status=400)
except ValueError as e:
return web.json_response({
'success': False,
'error': str(e)
}, status=400)
finally:
# Clean up temporary file
if os.path.exists(temp_path):
os.unlink(temp_path)
except Exception as e:
print(f"[PromptManager API] Restore error: {e}")
import traceback
traceback.print_exc()
return web.json_response({
'success': False,
'error': f'Failed to restore database: {str(e)}'
}, status=500)
+160
View File
@@ -180,6 +180,14 @@
class="px-4 py-2 bg-teal-600 hover:bg-teal-700 text-white text-sm font-medium rounded-lg transition-colors">
🔧 Maintenance
</button>
<button id="backupBtn"
class="px-4 py-2 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium rounded-lg transition-colors">
💾 Backup DB
</button>
<button id="restoreBtn"
class="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white text-sm font-medium rounded-lg transition-colors">
📁 Restore DB
</button>
</div>
</div>
</div>
@@ -297,6 +305,40 @@
</div>
</div>
<!-- Database Restore Modal -->
<div id="restoreModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-gray-800 rounded-xl p-6 max-w-md w-full mx-4 border border-gray-700">
<h3 class="text-xl font-semibold text-gray-100 mb-6">📁 Restore Database</h3>
<div class="mb-4">
<div class="bg-yellow-900/20 border border-yellow-600 rounded-lg p-4 mb-4">
<div class="flex items-start space-x-2">
<span class="text-yellow-400 text-xl">⚠️</span>
<div>
<h4 class="text-yellow-400 font-medium">Warning</h4>
<p class="text-gray-300 text-sm">This will replace your current database. A backup will be created automatically.</p>
</div>
</div>
</div>
<label class="block text-sm font-medium text-gray-400 mb-2">Select Database File (.db)</label>
<input type="file" id="restoreFileInput" accept=".db"
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-100 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
</div>
<div class="flex justify-end space-x-3 mt-6">
<button id="cancelRestore"
class="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
Cancel
</button>
<button id="confirmRestore"
class="px-6 py-2 bg-amber-600 hover:bg-amber-700 text-white font-medium rounded-lg transition-colors">
Restore Database
</button>
</div>
</div>
</div>
<!-- Diagnostics Modal -->
<div id="diagnosticsModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
<div class="bg-gray-800 rounded-xl max-w-4xl w-full max-h-[90vh] overflow-hidden mx-4 border border-gray-700">
@@ -535,6 +577,8 @@
document.getElementById("statsBtn").addEventListener("click", () => this.showStats());
document.getElementById("diagnosticsBtn").addEventListener("click", () => this.showDiagnosticsModal());
document.getElementById("maintenanceBtn").addEventListener("click", () => this.showMaintenanceModal());
document.getElementById("backupBtn").addEventListener("click", () => this.backupDatabase());
document.getElementById("restoreBtn").addEventListener("click", () => this.showRestoreModal());
// Modals
this.bindModalEvents();
@@ -558,6 +602,10 @@
document.getElementById("confirmBulkCategory").addEventListener("click", () => this.confirmBulkCategory());
document.getElementById("cancelBulkCategory").addEventListener("click", () => this.hideModal("bulkCategoryModal"));
// Restore modal
document.getElementById("confirmRestore").addEventListener("click", () => this.confirmRestore());
document.getElementById("cancelRestore").addEventListener("click", () => this.hideModal("restoreModal"));
// Diagnostics modal
document.getElementById("runDiagnosticsBtn").addEventListener("click", () => this.runDiagnostics());
document.getElementById("testImageLinkBtn").addEventListener("click", () => this.testImageLink());
@@ -1755,6 +1803,118 @@
}
}
// Database backup and restore functionality
async backupDatabase() {
try {
const response = await fetch('/prompt_manager/backup');
if (response.ok) {
// Get the filename from the Content-Disposition header
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'prompts_backup.db';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="(.+)"/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// Create blob and download
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
this.showNotification('💾 Database backup downloaded successfully!', 'success');
} else {
const errorData = await response.json();
throw new Error(errorData.error || 'Backup failed');
}
} catch (error) {
console.error('Backup error:', error);
this.showNotification(`❌ Backup failed: ${error.message}`, 'error');
}
}
showRestoreModal() {
// Clear previous file selection
document.getElementById('restoreFileInput').value = '';
this.showModal('restoreModal');
}
async confirmRestore() {
const fileInput = document.getElementById('restoreFileInput');
const file = fileInput.files[0];
if (!file) {
this.showNotification('Please select a database file to restore', 'warning');
return;
}
if (!file.name.endsWith('.db')) {
this.showNotification('Please select a valid .db file', 'warning');
return;
}
// Show confirmation
const confirmed = confirm(
`Are you sure you want to restore from "${file.name}"?\n\n` +
'This will replace your current database. A backup will be created automatically.\n\n' +
'This action cannot be undone.'
);
if (!confirmed) {
return;
}
try {
// Disable the restore button and show loading
const confirmBtn = document.getElementById('confirmRestore');
confirmBtn.disabled = true;
confirmBtn.innerHTML = '🔄 Restoring...';
// Create FormData for file upload
const formData = new FormData();
formData.append('database_file', file);
const response = await fetch('/prompt_manager/restore', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok && data.success) {
this.showNotification(
`✅ Database restored successfully! Found ${data.prompt_count} prompts.`,
'success'
);
this.hideModal('restoreModal');
// Refresh the interface to show new data
setTimeout(() => {
window.location.reload();
}, 2000);
} else {
throw new Error(data.error || 'Restore failed');
}
} catch (error) {
console.error('Restore error:', error);
this.showNotification(`❌ Restore failed: ${error.message}`, 'error');
} finally {
// Re-enable the restore button
const confirmBtn = document.getElementById('confirmRestore');
confirmBtn.disabled = false;
confirmBtn.innerHTML = 'Restore Database';
}
}
// Copy prompt to clipboard functionality
async copyPromptToClipboard(promptId) {
// Find the prompt text from the prompts array
+20 -13
View File
@@ -24,7 +24,7 @@ app.registerExtension({
this.properties.selectedPromptIndex = -1;
this.properties.resultTimeout = 3; // Default 3 seconds
this.properties.showTestButton = false; // Default hide test button
this.properties.webuiDisplayMode = 'popup'; // Default popup mode
this.properties.webuiDisplayMode = "popup"; // Default popup mode
this.resultHideTimer = null;
// Load settings from API
@@ -48,12 +48,12 @@ app.registerExtension({
// Set initial node size, but preserve user resizes
if (!this._userHasResized) {
this.size = [400, 300]; // width=400, height=300 pixels
this.size = [400, 400]; // width=400, height=300 pixels
}
// Hook into resize to track user changes
const originalOnResize = this.onResize;
this.onResize = function(size) {
this.onResize = function (size) {
this._userHasResized = true;
console.log("[PromptManager] User resized to:", size);
if (originalOnResize) {
@@ -63,7 +63,7 @@ app.registerExtension({
// Hook into serialization to preserve resize flag
const originalSerialize = this.serialize;
this.serialize = function() {
this.serialize = function () {
const data = originalSerialize ? originalSerialize.call(this) : {};
data._userHasResized = this._userHasResized;
return data;
@@ -71,7 +71,7 @@ app.registerExtension({
// Hook into configure to restore resize flag
const originalConfigure = this.configure;
this.configure = function(data) {
this.configure = function (data) {
if (originalConfigure) {
originalConfigure.call(this, data);
}
@@ -259,8 +259,8 @@ app.registerExtension({
this.properties.resultTimeout = data.settings.result_timeout || 3;
this.properties.showTestButton =
data.settings.show_test_button || false;
this.properties.webuiDisplayMode =
data.settings.webui_display_mode || 'popup';
this.properties.webuiDisplayMode =
data.settings.webui_display_mode || "popup";
}
}
} catch (error) {
@@ -625,16 +625,21 @@ app.registerExtension({
const currentOrigin = window.location.origin;
const webUrl = `${currentOrigin}/prompt_manager/web`;
if (this.properties.webuiDisplayMode === 'newtab') {
if (this.properties.webuiDisplayMode === "newtab") {
// Open in new tab without window options
const newWindow = window.open(webUrl, "_blank");
if (newWindow) {
console.log("[PromptManager] Web interface opened in new tab");
this.showNotification("Web interface opened in new tab", "success");
this.showNotification(
"Web interface opened in new tab",
"success",
);
} else {
// Fallback if popup was blocked
console.log("[PromptManager] Popup blocked, trying alternative method");
console.log(
"[PromptManager] Popup blocked, trying alternative method",
);
window.location.href = webUrl;
}
} else {
@@ -650,7 +655,9 @@ app.registerExtension({
this.showNotification("Web interface opened in popup", "success");
} else {
// Fallback if popup was blocked
console.log("[PromptManager] Popup blocked, trying alternative method");
console.log(
"[PromptManager] Popup blocked, trying alternative method",
);
window.location.href = webUrl;
}
}