Merge pull request 'feature/database-Management' (#37) from feature/database-Management into development
Reviewed-on: daniel-hbn/Watcher#37
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -2,6 +2,10 @@
|
||||
/persistance/*.db
|
||||
/logs
|
||||
*.env
|
||||
/wwwroot/downloads/sqlite/*.sql
|
||||
|
||||
/persistence/*.db-shm
|
||||
/persistence/*.db-wal
|
||||
|
||||
# Build-Ordner
|
||||
bin/
|
||||
|
185
Watcher/Controllers/DatabaseController.cs
Normal file
185
Watcher/Controllers/DatabaseController.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
37
Watcher/Controllers/DownloadController.cs
Normal file
37
Watcher/Controllers/DownloadController.cs
Normal 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);
|
||||
}
|
||||
}
|
@@ -49,7 +49,7 @@ public class ServerController : Controller
|
||||
_context.Servers.Add(server);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return Redirect("Server/Overview");
|
||||
return RedirectToAction(nameof(Overview));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
@@ -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">
|
||||
|
||||
<!-- 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-gear-wide-connected me-1"></i>Datenbank exportieren
|
||||
<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>
|
||||
|
||||
|
54
Watcher/Views/Database/ManageSqlDumps.cshtml
Normal file
54
Watcher/Views/Database/ManageSqlDumps.cshtml
Normal 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>
|
@@ -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.
BIN
Watcher/wwwroot/downloads/Linux/heartbeat
Executable file
BIN
Watcher/wwwroot/downloads/Linux/heartbeat
Executable file
Binary file not shown.
Reference in New Issue
Block a user