feat: Add API endpoints for project backups and folder management

This commit is contained in:
Miguel 2025-09-03 10:16:06 +02:00
parent 958d8ac994
commit 92f8ff8f62
4 changed files with 434 additions and 33 deletions

45
debug_project_id.py Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env python
"""Debug script to check project IDs"""
import sys
from pathlib import Path
# Add src directory to path
current_dir = Path(__file__).parent
src_dir = current_dir / "src"
sys.path.insert(0, str(src_dir))
from models.config_model import Config
from models.project_model import ProjectManager
from services.project_discovery_service import ProjectDiscoveryService
# Initialize services
config = Config()
config.load_config()
project_manager = ProjectManager()
project_discovery_service = ProjectDiscoveryService(config, project_manager)
# Discover projects
projects = project_discovery_service.discover_all_projects()
print("Available project IDs:")
found = False
for project_id, project in project_manager.projects.items():
if "Ssae0452 Last Version Walter" in project.name:
print(f"Found project:")
print(f" ID: {project_id}")
print(f" Name: {project.name}")
print(f" Path: {project.path}")
found = True
break
if not found:
print('Project "Ssae0452 Last Version Walter" not found!')
print('Available projects containing "Ssae":')
count = 0
for project_id, project in project_manager.projects.items():
if "Ssae" in project.name:
print(f" ID: {project_id}, Name: {project.name}")
count += 1
if count >= 10: # Limit output
break

View File

@ -3,6 +3,8 @@ API Routes - AutoBackups
Rutas de la API REST para operaciones AJAX
"""
import os
from datetime import datetime
from flask import request, jsonify
@ -543,3 +545,134 @@ def register_api_routes(app, autobackups_instance):
f"Error in Everything-only discovery: {e}"
)
return jsonify({"error": str(e)}), 500
@app.route("/api/projects/<project_id>/backups")
def get_project_backups(project_id):
"""Get list of backup files for a specific project"""
try:
# Get the project
project = autobackups_instance.project_manager.get_project(project_id)
if not project:
return jsonify({"error": f"Project {project_id} not found"}), 404
# Get backup destination path for this project
backup_destination = autobackups_instance.config.backup_destination
backup_path = project.backup_path
full_backup_path = os.path.join(backup_destination, backup_path)
backups = []
if os.path.exists(full_backup_path):
# List date directories (format: YYYY-MM-DD)
try:
date_dirs = [
d
for d in os.listdir(full_backup_path)
if os.path.isdir(os.path.join(full_backup_path, d))
and len(d) == 10
and d.count("-") == 2
]
for date_dir in sorted(date_dirs, reverse=True):
date_path = os.path.join(full_backup_path, date_dir)
# List backup files in date directory
try:
backup_files = [
f for f in os.listdir(date_path) if f.endswith(".zip")
]
for backup_file in sorted(backup_files, reverse=True):
backup_file_path = os.path.join(date_path, backup_file)
try:
stat = os.stat(backup_file_path)
backups.append(
{
"filename": backup_file,
"date": date_dir,
"size": stat.st_size,
"size_mb": round(
stat.st_size / (1024 * 1024), 2
),
"created": datetime.fromtimestamp(
stat.st_ctime
).isoformat(),
"modified": datetime.fromtimestamp(
stat.st_mtime
).isoformat(),
"full_path": backup_file_path,
}
)
except OSError:
continue
except OSError:
continue
except OSError:
pass
return jsonify(
{
"project_id": project_id,
"project_name": project.name,
"project_path": project.path,
"backup_destination": full_backup_path,
"backup_count": len(backups),
"backups": backups,
}
)
except Exception as e:
autobackups_instance.logger.error(
f"Error getting backups for project {project_id}: {e}"
)
return jsonify({"error": str(e)}), 500
@app.route("/api/projects/<project_id>/open-folder", methods=["POST"])
def open_project_folder(project_id):
"""Open project source or backup folder in Windows Explorer"""
try:
data = request.get_json()
folder_type = data.get("type", "source") # 'source' or 'backup'
# Get the project
project = autobackups_instance.project_manager.get_project(project_id)
if not project:
return jsonify({"error": f"Project {project_id} not found"}), 404
if folder_type == "source":
folder_path = project.path
if not os.path.exists(folder_path):
return jsonify({"error": "Source folder does not exist"}), 404
elif folder_type == "backup":
backup_destination = autobackups_instance.config.backup_destination
backup_path = project.backup_path
folder_path = os.path.join(backup_destination, backup_path)
if not os.path.exists(folder_path):
return jsonify({"error": "Backup folder does not exist"}), 404
else:
return (
jsonify(
{"error": "Invalid folder type. Must be 'source' or 'backup'"}
),
400,
)
# Open folder in Windows Explorer
import subprocess
subprocess.run(["explorer", folder_path], shell=True)
return jsonify(
{
"success": True,
"folder_path": folder_path,
"folder_type": folder_type,
}
)
except Exception as e:
autobackups_instance.logger.error(
f"Error opening folder for project {project_id}: {e}"
)
return jsonify({"error": str(e)}), 500

View File

@ -475,26 +475,27 @@ body {
.directory-header {
display: flex;
align-items: center;
padding: 0.5rem;
border-radius: 0.375rem;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
background-color: var(--hover-bg);
border: 1px solid var(--border-color);
cursor: pointer;
transition: background-color 0.2s ease;
margin-bottom: 0.25rem;
}
.observation-header {
display: flex;
align-items: center;
padding: 0.75rem;
border-radius: 0.5rem;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
background-color: rgba(13, 110, 253, 0.1);
border: 2px solid var(--primary-color);
cursor: pointer;
transition: all 0.2s ease;
font-weight: 600;
color: var(--primary-color);
margin-bottom: 0.75rem;
margin-bottom: 0.5rem;
}
.observation-header:hover {
@ -505,15 +506,15 @@ body {
.collapsed-header {
display: flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
padding: 0.2rem 0.4rem;
border-radius: 0.2rem;
background-color: rgba(108, 117, 125, 0.1);
border: 1px dashed var(--text-muted);
cursor: pointer;
transition: background-color 0.2s ease;
font-style: italic;
color: var(--text-muted);
margin-bottom: 0.25rem;
margin-bottom: 0.2rem;
}
.collapsed-header:hover {
@ -538,8 +539,8 @@ body {
}
.directory-content {
margin-top: 0.5rem;
padding-left: 1rem;
margin-top: 0.25rem;
padding-left: 0.75rem;
border-left: 2px solid var(--border-color);
}

View File

@ -115,7 +115,6 @@
<thead class="table-dark">
<tr>
<th>Proyecto</th>
<th>Ruta</th>
<th>Tipo</th>
<th>Estado</th>
<th>Último Backup</th>
@ -133,12 +132,7 @@
<td>
<strong>{{ project.name or 'Sin nombre' }}</strong>
<br>
<small class="text-muted">ID: {{ project.id }}</small>
</td>
<td>
<span title="{{ project.path }}">
{{ project.path[:50] }}{% if project.path|length > 50 %}...{% endif %}
</span>
<small class="text-muted">{{ project.path }}</small>
</td>
<td>
<span class="badge bg-secondary">{{ project.type or 'N/A' }}</span>
@ -180,8 +174,10 @@
data-project-id="{{ project.id }}">
<i class="bi bi-gear"></i>
</button>
<button class="btn btn-outline-info view-project-btn"
data-project-id="{{ project.id }}">
<button class="btn btn-outline-info view-backups-btn"
data-project-id="{{ project.id }}"
data-project-name="{{ project.name or 'Sin nombre' }}"
data-project-path="{{ project.path }}">
<i class="bi bi-eye"></i>
</button>
</div>
@ -884,14 +880,17 @@ function renderProjectCard(project, level) {
return `
<div class="project-card card mb-2" style="margin-left: ${level * 20}px;">
<div class="card-body p-3">
<div class="card-body p-2">
<div class="row align-items-center">
<div class="col-md-6">
<h6 class="card-title mb-1">
<i class="bi bi-file-earmark-code text-primary me-2"></i>
${projectName}
</h6>
<small class="text-muted">ID: ${projectId}</small>
<small class="text-muted">
<i class="bi bi-folder me-1"></i>
${projectPath}
</small>
</div>
<div class="col-md-2">
<span class="badge bg-secondary">${projectType}</span>
@ -902,28 +901,25 @@ function renderProjectCard(project, level) {
<div class="col-md-2 text-end">
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary backup-project-btn"
data-project-id="${projectId}">
data-project-id="${projectId}"
title="Hacer backup">
<i class="bi bi-archive"></i>
</button>
<button class="btn btn-outline-secondary config-project-btn"
data-project-id="${projectId}">
data-project-id="${projectId}"
title="Configurar">
<i class="bi bi-gear"></i>
</button>
<button class="btn btn-outline-info view-project-btn"
data-project-id="${projectId}">
<button class="btn btn-outline-info view-backups-btn"
data-project-id="${projectId}"
data-project-name="${projectName}"
data-project-path="${projectPath}"
title="Ver backups y carpetas">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
<div class="row mt-2">
<div class="col-12">
<small class="text-muted">
<i class="bi bi-folder me-1"></i>
${projectPath}
</small>
</div>
</div>
</div>
</div>
`;
@ -976,6 +972,46 @@ function displayFlatProjects(filteredProjects) {
row.style.display = (matchesSearch && matchesStatus) ? '' : 'none';
});
// Bind events for flat view buttons
bindFlatViewEvents();
}
function bindFlatViewEvents() {
// Bind backup buttons
document.querySelectorAll('.backup-project-btn').forEach(btn => {
btn.replaceWith(btn.cloneNode(true)); // Remove existing listeners
});
document.querySelectorAll('.backup-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
backupProject(projectId);
});
});
// Bind config buttons
document.querySelectorAll('.config-project-btn').forEach(btn => {
btn.replaceWith(btn.cloneNode(true)); // Remove existing listeners
});
document.querySelectorAll('.config-project-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
openProjectConfig(projectId);
});
});
// Bind view backups buttons
document.querySelectorAll('.view-backups-btn').forEach(btn => {
btn.replaceWith(btn.cloneNode(true)); // Remove existing listeners
});
document.querySelectorAll('.view-backups-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
const projectName = this.dataset.projectName;
const projectPath = this.dataset.projectPath;
showBackupsModal(projectId, projectName, projectPath);
});
});
}
function bindHierarchicalEvents() {
@ -1012,6 +1048,16 @@ function bindHierarchicalEvents() {
openProjectConfig(projectId);
});
});
// Re-bind view backups buttons
document.querySelectorAll('.view-backups-btn').forEach(btn => {
btn.addEventListener('click', function() {
const projectId = this.dataset.projectId;
const projectName = this.dataset.projectName;
const projectPath = this.dataset.projectPath;
showBackupsModal(projectId, projectName, projectPath);
});
});
}
// Global expand/collapse functionality
@ -1052,6 +1098,182 @@ function initializeExpandCollapseButton() {
}
}
// Show backups modal with backup list and folder actions
function showBackupsModal(projectId, projectName, projectPath) {
// Create modal if it doesn't exist
let modal = document.getElementById('backupsModal');
if (!modal) {
modal = createBackupsModal();
document.body.appendChild(modal);
}
// Update modal content
document.getElementById('backupsModalTitle').textContent = `Backups de ${projectName}`;
document.getElementById('backupsProjectPath').textContent = projectPath;
// Set up folder buttons
const openSourceBtn = document.getElementById('openSourceFolderBtn');
const openBackupBtn = document.getElementById('openBackupFolderBtn');
openSourceBtn.onclick = () => openProjectFolder(projectId, 'source');
openBackupBtn.onclick = () => openProjectFolder(projectId, 'backup');
// Load backup list
loadBackupList(projectId);
// Show modal
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
}
function createBackupsModal() {
const modalHTML = `
<div class="modal fade" id="backupsModal" tabindex="-1" aria-labelledby="backupsModalTitle" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="backupsModalTitle">Backups del Proyecto</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<strong>Ruta del proyecto:</strong>
<p class="text-muted mb-2" id="backupsProjectPath"></p>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary btn-sm" id="openSourceFolderBtn">
<i class="bi bi-folder-fill me-1"></i>Abrir Carpeta Origen
</button>
<button type="button" class="btn btn-outline-info btn-sm" id="openBackupFolderBtn">
<i class="bi bi-archive-fill me-1"></i>Abrir Carpeta Backups
</button>
</div>
</div>
<hr>
<div id="backupListContainer">
<div class="text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Cargando...</span>
</div>
<p class="mt-2">Cargando lista de backups...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
`;
const tempDiv = document.createElement('div');
tempDiv.innerHTML = modalHTML;
return tempDiv.firstElementChild;
}
function loadBackupList(projectId) {
const container = document.getElementById('backupListContainer');
fetch(`/api/projects/${projectId}/backups`)
.then(response => response.json())
.then(data => {
if (data.error) {
container.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Error: ${data.error}
</div>
`;
return;
}
if (data.backup_count === 0) {
container.innerHTML = `
<div class="alert alert-info">
<i class="bi bi-info-circle me-2"></i>
No se encontraron backups para este proyecto.
</div>
`;
return;
}
// Build backup list
let html = `
<h6>Backups encontrados (${data.backup_count})</h6>
<div class="table-responsive">
<table class="table table-sm table-striped">
<thead class="table-dark">
<tr>
<th>Fecha</th>
<th>Archivo</th>
<th>Tamaño</th>
<th>Creado</th>
</tr>
</thead>
<tbody>
`;
data.backups.forEach(backup => {
const createdDate = new Date(backup.created).toLocaleString('es-ES');
html += `
<tr>
<td>${backup.date}</td>
<td>
<i class="bi bi-file-zip me-1"></i>
${backup.filename}
</td>
<td>${backup.size_mb} MB</td>
<td>${createdDate}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
<small class="text-muted">
<i class="bi bi-folder me-1"></i>
Ubicación: ${data.backup_destination}
</small>
`;
container.innerHTML = html;
})
.catch(error => {
console.error('Error loading backup list:', error);
container.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle me-2"></i>
Error cargando la lista de backups: ${error.message}
</div>
`;
});
}
function openProjectFolder(projectId, folderType) {
fetch(`/api/projects/${projectId}/open-folder`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ type: folderType })
})
.then(response => response.json())
.then(data => {
if (data.error) {
showNotification(`Error: ${data.error}`, 'error');
} else {
const folderName = folderType === 'source' ? 'origen' : 'backups';
showNotification(`Carpeta ${folderName} abierta en el explorador`, 'success');
}
})
.catch(error => {
console.error('Error opening folder:', error);
showNotification(`Error abriendo la carpeta: ${error.message}`, 'error');
});
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
initializeProjectsView();