Merge pull request 'feature/database-Management' (#37) from feature/database-Management into development

Reviewed-on: daniel-hbn/Watcher#37
This commit is contained in:
2025-06-21 18:19:52 +00:00
10 changed files with 321 additions and 8 deletions

4
.gitignore vendored
View File

@@ -2,6 +2,10 @@
/persistance/*.db
/logs
*.env
/wwwroot/downloads/sqlite/*.sql
/persistence/*.db-shm
/persistence/*.db-wal
# Build-Ordner
bin/

View File

@@ -0,0 +1,185 @@
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System.IO;
using System.Diagnostics;
namespace Watcher.Controllers
{
[Authorize]
public class DatabaseController : Controller
{
private readonly string _dbPath = Path.Combine(Directory.GetCurrentDirectory(), "persistence", "watcher.db");
private readonly string _backupFolder = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "downloads", "sqlite");
public class DumpFileInfo
{
public string? FileName { get; set; }
public long SizeKb { get; set; }
public DateTime Created { get; set; }
}
public DatabaseController(IWebHostEnvironment env)
{
_backupFolder = Path.Combine(env.WebRootPath, "downloads", "sqlite");
if (!Directory.Exists(_backupFolder))
Directory.CreateDirectory(_backupFolder);
}
[HttpPost("/maintenance/sqlite-dump")]
public IActionResult CreateSqlDump()
{
try
{
// Zielordner sicherstellen
if (!Directory.Exists(_backupFolder))
Directory.CreateDirectory(_backupFolder);
// Ziel-Dateiname z.B. mit Zeitstempel
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var dumpFileName = $"watcher_dump_{timestamp}.sql";
var dumpFilePath = Path.Combine(_backupFolder, dumpFileName);
using var connection = new SqliteConnection($"Data Source={_dbPath}");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT sql FROM sqlite_master WHERE type='table' AND sql IS NOT NULL;";
using var writer = new StreamWriter(dumpFilePath);
// Write schema
using (var schemaCmd = connection.CreateCommand())
{
schemaCmd.CommandText = "SELECT sql FROM sqlite_master WHERE type='table'";
using var reader = schemaCmd.ExecuteReader();
while (reader.Read())
{
writer.WriteLine(reader.GetString(0) + ";");
}
}
// Write content
using (var tableCmd = connection.CreateCommand())
{
tableCmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'";
using var tableReader = tableCmd.ExecuteReader();
while (tableReader.Read())
{
var tableName = tableReader.GetString(0);
using var dataCmd = connection.CreateCommand();
dataCmd.CommandText = $"SELECT * FROM {tableName}";
using var dataReader = dataCmd.ExecuteReader();
while (dataReader.Read())
{
var columns = new string[dataReader.FieldCount];
var values = new string[dataReader.FieldCount];
for (int i = 0; i < dataReader.FieldCount; i++)
{
columns[i] = dataReader.GetName(i);
var val = dataReader.GetValue(i);
values[i] = val == null || val == DBNull.Value
? "NULL"
: $"'{val.ToString().Replace("'", "''")}'";
}
writer.WriteLine($"INSERT INTO {tableName} ({string.Join(",", columns)}) VALUES ({string.Join(",", values)});");
}
}
}
writer.Flush();
//return Ok($"Dump erfolgreich erstellt: {dumpFileName}");
TempData["DumpMessage"] = "SQLite-Dump erfolgreich erstellt.";
return RedirectToAction("UserSettings", "Auth");
}
catch (Exception ex)
{
//return StatusCode(500, $"Fehler beim Erstellen des Dumps: {ex.Message}");
TempData["DumpError"] = $"Fehler beim Erstellen des Dumps: {ex.Message}";
return RedirectToAction("UserSettings", "Auth");
}
}
public IActionResult ManageSqlDumps()
{
var files = Directory.GetFiles(_backupFolder, "*.sql")
.Select(f => new DumpFileInfo
{
FileName = Path.GetFileName(f),
SizeKb = new FileInfo(f).Length / 1024,
Created = System.IO.File.GetCreationTime(f)
})
.OrderByDescending(f => f.Created)
.ToList();
return View(files);
}
[HttpPost]
public IActionResult Delete(string fileName)
{
var filePath = Path.Combine(_backupFolder, fileName);
if (System.IO.File.Exists(filePath))
{
System.IO.File.Delete(filePath);
TempData["Success"] = $"Backup {fileName} wurde gelöscht.";
}
return RedirectToAction("ManageSqlDumps");
}
// 🔹 4. Dump wiederherstellen
[HttpPost]
public IActionResult Restore(string fileName)
{
var filePath = Path.Combine(_backupFolder, fileName);
if (!System.IO.File.Exists(filePath))
{
TempData["Error"] = "Dump nicht gefunden.";
return RedirectToAction("ManageSqlDumps");
}
try
{
var dbPath = Path.Combine(Directory.GetCurrentDirectory(), "persistence", "watcher.db"); // anpassen
// Leere Datenbank
System.IO.File.WriteAllText(dbPath, "");
var psi = new ProcessStartInfo
{
FileName = "sqlite3",
Arguments = $"\"{dbPath}\" \".read \\\"{filePath}\\\"\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
using var proc = Process.Start(psi);
proc.WaitForExit();
TempData["Success"] = $"Backup {fileName} wurde wiederhergestellt.";
}
catch (Exception ex)
{
TempData["Error"] = $"Fehler beim Wiederherstellen: {ex.Message}";
}
return RedirectToAction("ManageSqlDumps");
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace Watcher.Controllers;
[Authorize]
public class DownloadController : Controller
{
[HttpGet("Download/File/{type}/{filename}")]
public IActionResult FileDownload(string type, string filename)
{
// Nur erlaubte Endungen zulassen (Sicherheit!)
var allowedExtensions = new[] { ".exe", "", ".sql" };
var extension = Path.GetExtension(filename).ToLowerInvariant();
if (!allowedExtensions.Contains(extension))
return BadRequest("Dateityp nicht erlaubt");
var path = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "downloads", type, filename);
if (!System.IO.File.Exists(path))
return NotFound("Datei nicht gefunden");
// .exe MIME-Typ: application/octet-stream
var mimeType = "application/octet-stream";
var fileBytes = System.IO.File.ReadAllBytes(path);
return File(fileBytes, mimeType, filename);
//return File(fileBytes, filename);
}
}

View File

@@ -49,7 +49,7 @@ public class ServerController : Controller
_context.Servers.Add(server);
await _context.SaveChangesAsync();
return Redirect("Server/Overview");
return RedirectToAction(nameof(Overview));
}
[HttpPost]

View File

@@ -70,14 +70,43 @@
<h5>Datenbank-Engine: </h5>
<strong>@(DbEngine ?? "nicht gefunden")</strong>
<form method="get" asp-controller="Auth" asp-action="DbExport" class="text-center mt-4">
<button type="submit" class="btn btn-primary">
<i class="bi bi-gear-wide-connected me-1"></i>Datenbank exportieren
</button>
</form>
<!-- Falls Sqlite verwendet wird können Backups erstellt werden -->
@if (DbEngine == "Microsoft.EntityFrameworkCore.Sqlite")
{
<div class="d-flex gap-2">
<form asp-action="CreateSqlDump" method="post" asp-controller="Database">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i> Backup erstellen
</button>
</form>
<form asp-action="ManageSqlDumps" method="post" asp-controller="Database">
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i> Backups verwalten
</button>
</form>
</div>
}
else if (DbEngine == "Microsoft.EntityFrameworkCore.MySQL")
{
<p> MySQL Dump aktuell nicht möglich </p>
}
<!-- Status für Erstellung eines Backups -->
@if (TempData["DumpMessage"] != null)
{
<div class="alert alert-success">
<i class="bi bi-check-circle me-1"></i>@TempData["DumpMessage"]
</div>
}
@if (TempData["DumpError"] != null)
{
<div class="alert alert-danger">
<i class="bi bi-exclamation-circle me-1"></i>@TempData["DumpError"]
</div>
}
</div>
@@ -95,7 +124,7 @@
<h5>...: </h5>
</div>
</div>

View File

@@ -0,0 +1,54 @@
@model List<Watcher.Controllers.DatabaseController.DumpFileInfo>
@{
ViewData["Title"] = "Datenbank-Dumps";
}
<h2 class="mb-4 text-xl font-bold"><i class="bi bi-hdd me-1"></i>Datenbank-Dumps</h2>
@if (TempData["Success"] != null)
{
<div class="alert alert-success">@TempData["Success"]</div>
}
@if (TempData["Error"] != null)
{
<div class="alert alert-danger">@TempData["Error"]</div>
}
<table class="table table-striped">
<thead>
<tr>
<th>Dateiname</th>
<th>Größe (KB)</th>
<th>Erstellt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
@foreach (var dump in Model)
{
<tr>
<td>@dump.FileName</td>
<td>@dump.SizeKb</td>
<td>@dump.Created.ToString("dd.MM.yyyy HH:mm")</td>
<td class="d-flex gap-2">
<a class="btn btn-outline-primary btn-sm"
href="@Url.Action("FileDownload", "Download", new { type= "sqlite", fileName = dump.FileName })">
<i class="bi bi-download me-1"></i>Download
</a>
<form method="post" asp-action="Delete" asp-controller="Database" asp-route-fileName="@dump.FileName" onsubmit="return confirm('Diesen Dump wirklich löschen?');">
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="bi bi-trash me-1"></i>Löschen
</button>
</form>
<form method="post" asp-action="Restore" asp-controller="Database" asp-route-fileName="@dump.FileName" onsubmit="return confirm('Achtung! Der aktuelle DB-Stand wird überschrieben. Fortfahren?');">
<button type="submit" class="btn btn-outline-warning btn-sm">
<i class="bi bi-arrow-clockwise me-1"></i>Wiederherstellen
</button>
</form>
</td>
</tr>
}
</tbody>
</table>

View File

@@ -45,6 +45,10 @@
<i class="bi bi-trash me-1"></i>Löschen
</button>
</form>
<a href="/Download/File/Linux/heartbeat" class="btn btn-success">
🖥️ Linux Tool herunterladen
</a>
</div>
</div>
}

Binary file not shown.

Binary file not shown.

Binary file not shown.