diff --git a/.gitignore b/.gitignore index 26adfcc..ff87601 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,10 @@ /persistance/*.db /logs *.env +/wwwroot/downloads/sqlite/*.sql + +/persistence/*.db-shm +/persistence/*.db-wal # Build-Ordner bin/ diff --git a/Watcher/Controllers/DatabaseController.cs b/Watcher/Controllers/DatabaseController.cs new file mode 100644 index 0000000..0bfcd27 --- /dev/null +++ b/Watcher/Controllers/DatabaseController.cs @@ -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"); + } + } + +} \ No newline at end of file diff --git a/Watcher/Controllers/DownloadController.cs b/Watcher/Controllers/DownloadController.cs new file mode 100644 index 0000000..d544652 --- /dev/null +++ b/Watcher/Controllers/DownloadController.cs @@ -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); + } +} diff --git a/Watcher/Controllers/ServerController.cs b/Watcher/Controllers/ServerController.cs index c6f4859..b75678e 100644 --- a/Watcher/Controllers/ServerController.cs +++ b/Watcher/Controllers/ServerController.cs @@ -49,7 +49,7 @@ public class ServerController : Controller _context.Servers.Add(server); await _context.SaveChangesAsync(); - return Redirect("Server/Overview"); + return RedirectToAction(nameof(Overview)); } [HttpPost] diff --git a/Watcher/Views/Auth/UserSettings.cshtml b/Watcher/Views/Auth/UserSettings.cshtml index 49c029f..d2c1bbd 100644 --- a/Watcher/Views/Auth/UserSettings.cshtml +++ b/Watcher/Views/Auth/UserSettings.cshtml @@ -70,14 +70,43 @@
MySQL Dump aktuell nicht möglich
+ } + + + @if (TempData["DumpMessage"] != null) + { +Dateiname | +Größe (KB) | +Erstellt | +Aktionen | +
---|---|---|---|
@dump.FileName | +@dump.SizeKb | +@dump.Created.ToString("dd.MM.yyyy HH:mm") | ++ + Download + + + + + + | +