Compare commits
2
Commits
backup
..
3366cc25eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3366cc25eb | ||
|
|
ff71e9fb7f |
@@ -28,8 +28,6 @@ namespace Elwig {
|
|||||||
public static bool ForceShutdown { get; private set; } = false;
|
public static bool ForceShutdown { get; private set; } = false;
|
||||||
|
|
||||||
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
|
private readonly DispatcherTimer _autoUpdateTimer = new() { Interval = TimeSpan.FromHours(1) };
|
||||||
private readonly DispatcherTimer _autoBackupTimer = new() { Interval = TimeSpan.FromHours(1) };
|
|
||||||
private DatabaseBackupService? _databaseBackupService;
|
|
||||||
public readonly SerialPortWatcher SerialPortWatcher = new();
|
public readonly SerialPortWatcher SerialPortWatcher = new();
|
||||||
|
|
||||||
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Elwig");
|
public static readonly string DataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Elwig");
|
||||||
@@ -103,13 +101,6 @@ namespace Elwig {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Config.DatabaseBackup) {
|
|
||||||
_databaseBackupService = new DatabaseBackupService(Config.DatabaseFile);
|
|
||||||
await _databaseBackupService.RunIfNeededAsync();
|
|
||||||
_autoBackupTimer.Tick += OnAutoBackupTimer;
|
|
||||||
_autoBackupTimer.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
LastChanged = CurrentLastWrite;
|
LastChanged = CurrentLastWrite;
|
||||||
ContextTimer.Start();
|
ContextTimer.Start();
|
||||||
|
|
||||||
@@ -200,7 +191,6 @@ namespace Elwig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async void Application_Exit(object sender, ExitEventArgs evt) {
|
private async void Application_Exit(object sender, ExitEventArgs evt) {
|
||||||
_autoBackupTimer.Stop();
|
|
||||||
SerialPortWatcher.Dispose();
|
SerialPortWatcher.Dispose();
|
||||||
foreach (var s in EventScales) {
|
foreach (var s in EventScales) {
|
||||||
s.Dispose();
|
s.Dispose();
|
||||||
@@ -208,12 +198,6 @@ namespace Elwig {
|
|||||||
await Pdf.Cleanup();
|
await Pdf.Cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnAutoBackupTimer(object? sender, EventArgs evt) {
|
|
||||||
if (_databaseBackupService != null) {
|
|
||||||
await _databaseBackupService.RunIfNeededAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SetBranch(Branch b) {
|
public static void SetBranch(Branch b) {
|
||||||
SetBranch((b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
|
SetBranch((b.ZwstId, b.Name, b.PostalDest?.AtPlz?.Plz, b.PostalDest?.AtPlz?.Ort.Name, b.Address, b.PhoneNr, b.FaxNr, b.MobileNr));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ namespace Elwig.Documents {
|
|||||||
.AddCell(NewDeliveryMainTd(part.Quality.Name))
|
.AddCell(NewDeliveryMainTd(part.Quality.Name))
|
||||||
.AddCell(NewDeliveryMainTd($"{part.Oe:N0}", center: true))
|
.AddCell(NewDeliveryMainTd($"{part.Oe:N0}", center: true))
|
||||||
.AddCell(NewDeliveryMainTd($"{part.Kmw:N1}", center: true))
|
.AddCell(NewDeliveryMainTd($"{part.Kmw:N1}", center: true))
|
||||||
.AddCell(NewDeliveryMainTd($"{part.Weight:N0}", center: true));
|
.AddCell(NewDeliveryMainTd($"{part.Weight:N0}", right: true));
|
||||||
|
|
||||||
if (part.Cultivation != null) {
|
if (part.Cultivation != null) {
|
||||||
var cult = new KernedParagraph(8);
|
var cult = new KernedParagraph(8);
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ namespace Elwig.Helpers {
|
|||||||
|
|
||||||
public bool Debug;
|
public bool Debug;
|
||||||
public string DatabaseFile = App.DataPath + "database.sqlite3";
|
public string DatabaseFile = App.DataPath + "database.sqlite3";
|
||||||
public bool DatabaseBackup = true;
|
|
||||||
public string? DatabaseLog = null;
|
public string? DatabaseLog = null;
|
||||||
public string? Branch = null;
|
public string? Branch = null;
|
||||||
public WeighingMode? WeighingMode;
|
public WeighingMode? WeighingMode;
|
||||||
@@ -77,8 +76,6 @@ namespace Elwig.Helpers {
|
|||||||
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
|
var config = new ConfigurationBuilder().AddIniFile(FileName).Build();
|
||||||
|
|
||||||
DatabaseFile = Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, config["database:file"] ?? "database.sqlite3");
|
DatabaseFile = Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, config["database:file"] ?? "database.sqlite3");
|
||||||
var backup = config["database:backup"];
|
|
||||||
DatabaseBackup = backup == null || TrueValues.Contains(backup.ToLower());
|
|
||||||
var log = config["database:log"];
|
var log = config["database:log"];
|
||||||
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
|
DatabaseLog = log != null ? Path.Combine(Path.GetDirectoryName(FileName) ?? App.DataPath, log) : null;
|
||||||
Branch = config["general:branch"];
|
Branch = config["general:branch"];
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Elwig.Helpers {
|
|
||||||
internal sealed class DatabaseBackupService {
|
|
||||||
|
|
||||||
private const int DailyBackupCount = 7;
|
|
||||||
private const int WeeklyBackupCount = 5;
|
|
||||||
|
|
||||||
private readonly string DatabaseFile;
|
|
||||||
private readonly string BackupDirectory;
|
|
||||||
private readonly string DatabaseName;
|
|
||||||
private readonly string BackupPrefix;
|
|
||||||
private readonly string LockFile;
|
|
||||||
private readonly Func<DateTime> Now;
|
|
||||||
private readonly SemaphoreSlim Gate = new(1, 1);
|
|
||||||
|
|
||||||
internal DatabaseBackupService(string databaseFile, Func<DateTime>? now = null) {
|
|
||||||
DatabaseFile = Path.GetFullPath(databaseFile);
|
|
||||||
BackupDirectory = Path.Combine(Path.GetDirectoryName(DatabaseFile)!, "backup");
|
|
||||||
DatabaseName = Path.GetFileNameWithoutExtension(DatabaseFile);
|
|
||||||
BackupPrefix = $"{DatabaseName}_";
|
|
||||||
LockFile = Path.Combine(BackupDirectory, $".{DatabaseName}.backup.lock");
|
|
||||||
Now = now ?? (() => DateTime.Now);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task RunIfNeededAsync() {
|
|
||||||
if (!await Gate.WaitAsync(0)) return;
|
|
||||||
try {
|
|
||||||
var slotDate = GetSlotDate(Now());
|
|
||||||
await Task.Run(() => Run(slotDate));
|
|
||||||
} catch {
|
|
||||||
// Automatic backups dont prevent Elwig from starting or running.
|
|
||||||
} finally {
|
|
||||||
Gate.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Run(DateOnly slotDate) {
|
|
||||||
Directory.CreateDirectory(BackupDirectory);
|
|
||||||
|
|
||||||
FileStream lockStream;
|
|
||||||
try {
|
|
||||||
lockStream = new FileStream(LockFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None);
|
|
||||||
} catch (IOException) {
|
|
||||||
// Another Elwig instance is currently creating/pruning backups.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (lockStream) {
|
|
||||||
CleanupTemporaryFiles();
|
|
||||||
|
|
||||||
var backupFile = GetBackupFile(slotDate);
|
|
||||||
if (!File.Exists(backupFile)) {
|
|
||||||
CreateBackup(backupFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (File.Exists(backupFile)) {
|
|
||||||
PruneBackups(slotDate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CreateBackup(string backupFile) {
|
|
||||||
var id = Guid.NewGuid().ToString("N");
|
|
||||||
var temporaryDatabase = Path.Combine(BackupDirectory, $".{DatabaseName}.backup-{id}.sqlite3.tmp");
|
|
||||||
var temporaryArchive = Path.Combine(BackupDirectory, $".{DatabaseName}.backup-{id}.zip.tmp");
|
|
||||||
|
|
||||||
try {
|
|
||||||
var sourceConnectionString = new SqliteConnectionStringBuilder {
|
|
||||||
DataSource = DatabaseFile,
|
|
||||||
Mode = SqliteOpenMode.ReadOnly,
|
|
||||||
Cache = SqliteCacheMode.Default,
|
|
||||||
Pooling = false,
|
|
||||||
}.ToString();
|
|
||||||
var destinationConnectionString = new SqliteConnectionStringBuilder {
|
|
||||||
DataSource = temporaryDatabase,
|
|
||||||
Mode = SqliteOpenMode.ReadWriteCreate,
|
|
||||||
Cache = SqliteCacheMode.Default,
|
|
||||||
Pooling = false,
|
|
||||||
}.ToString();
|
|
||||||
|
|
||||||
using (var source = new SqliteConnection(sourceConnectionString))
|
|
||||||
using (var destination = new SqliteConnection(destinationConnectionString)) {
|
|
||||||
source.Open();
|
|
||||||
destination.Open();
|
|
||||||
source.BackupDatabase(destination);
|
|
||||||
|
|
||||||
using var check = destination.CreateCommand();
|
|
||||||
check.CommandText = "PRAGMA quick_check";
|
|
||||||
if (!string.Equals(check.ExecuteScalar()?.ToString(), "ok", StringComparison.OrdinalIgnoreCase)) {
|
|
||||||
throw new InvalidDataException("The automatic database backup failed its SQLite integrity check.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
using (var archive = ZipFile.Open(temporaryArchive, ZipArchiveMode.Create)) {
|
|
||||||
archive.CreateEntryFromFile(temporaryDatabase, "database.sqlite3", CompressionLevel.SmallestSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
File.Move(temporaryArchive, backupFile, false);
|
|
||||||
} catch (IOException) when (File.Exists(backupFile)) {
|
|
||||||
// A completed backup won a publish race. Its time slot is satisfied.
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
DeleteFile(temporaryDatabase);
|
|
||||||
DeleteFile(temporaryArchive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CleanupTemporaryFiles() {
|
|
||||||
foreach (var file in Directory.EnumerateFiles(BackupDirectory, $".{DatabaseName}.backup-*.tmp")) {
|
|
||||||
DeleteFile(file);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void PruneBackups(DateOnly slotDate) {
|
|
||||||
var backups = GetManagedBackups().ToArray();
|
|
||||||
var retained = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
var dailyStart = slotDate.AddDays(-(DailyBackupCount - 1));
|
|
||||||
foreach (var backup in backups.Where(b => b.Date >= dailyStart && b.Date <= slotDate)) {
|
|
||||||
retained.Add(backup.File);
|
|
||||||
}
|
|
||||||
|
|
||||||
var daysSinceMonday = ((int)slotDate.DayOfWeek + 6) % 7;
|
|
||||||
var currentWeekStart = slotDate.AddDays(-daysSinceMonday);
|
|
||||||
for (var i = 1; i <= WeeklyBackupCount; i++) {
|
|
||||||
var weekStart = currentWeekStart.AddDays(-7 * i);
|
|
||||||
RetainNewest(backups, retained, weekStart, weekStart.AddDays(7));
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentMonthStart = new DateOnly(slotDate.Year, slotDate.Month, 1);
|
|
||||||
foreach (var month in backups
|
|
||||||
.Where(b => b.Date < currentMonthStart)
|
|
||||||
.GroupBy(b => (b.Date.Year, b.Date.Month))) {
|
|
||||||
retained.Add(month.OrderByDescending(b => b.Date).First().File);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clock corrections should not cause future-dated backups to be deleted.
|
|
||||||
foreach (var backup in backups.Where(b => b.Date > slotDate)) {
|
|
||||||
retained.Add(backup.File);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var backup in backups.Where(b => !retained.Contains(b.File))) {
|
|
||||||
DeleteFile(backup.File);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void RetainNewest(
|
|
||||||
IEnumerable<(string File, DateOnly Date)> backups,
|
|
||||||
ISet<string> retained,
|
|
||||||
DateOnly start,
|
|
||||||
DateOnly end
|
|
||||||
) {
|
|
||||||
var newest = backups
|
|
||||||
.Where(b => b.Date >= start && b.Date < end)
|
|
||||||
.OrderByDescending(b => b.Date)
|
|
||||||
.FirstOrDefault();
|
|
||||||
if (newest.File != null) retained.Add(newest.File);
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<(string File, DateOnly Date)> GetManagedBackups() {
|
|
||||||
foreach (var file in Directory.EnumerateFiles(BackupDirectory, $"{BackupPrefix}*.sqlite3.zip")) {
|
|
||||||
var name = Path.GetFileName(file);
|
|
||||||
if (!name.StartsWith(BackupPrefix, StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
!name.EndsWith(".sqlite3.zip", StringComparison.OrdinalIgnoreCase)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateText = name[BackupPrefix.Length..^".sqlite3.zip".Length];
|
|
||||||
if (DateOnly.TryParseExact(dateText, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) {
|
|
||||||
yield return (file, date);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetBackupFile(DateOnly date) {
|
|
||||||
return Path.Combine(BackupDirectory, $"{BackupPrefix}{date:yyyy-MM-dd}.sqlite3.zip");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void DeleteFile(string file) {
|
|
||||||
try {
|
|
||||||
File.Delete(file);
|
|
||||||
} catch {
|
|
||||||
// Cleanup and retention are best-effort and will be retried later.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static DateOnly GetSlotDate(DateTime timestamp) {
|
|
||||||
return DateOnly.FromDateTime(timestamp.Hour >= 3 ? timestamp : timestamp.AddDays(-1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1379,7 +1379,7 @@ namespace Elwig.Windows {
|
|||||||
var kl = mod.Where(m => m.Name.StartsWith("Klasse ")).Select(m => m.ModId).LastOrDefault("_")[0];
|
var kl = mod.Where(m => m.Name.StartsWith("Klasse ")).Select(m => m.ModId).LastOrDefault("_")[0];
|
||||||
if (ViewModel.IsUnloadingPumped && (kl == 'A' || kl == '_')) {
|
if (ViewModel.IsUnloadingPumped && (kl == 'A' || kl == '_')) {
|
||||||
kl = 'B';
|
kl = 'B';
|
||||||
} else if (ViewModel.IsUnloadingDumper && kl == '_') {
|
} else if (ViewModel.IsUnloadingDumper && (kl == 'B' || kl == '_')) {
|
||||||
kl = 'A';
|
kl = 'A';
|
||||||
} else {
|
} else {
|
||||||
kl = '_';
|
kl = '_';
|
||||||
|
|||||||
@@ -7,8 +7,6 @@
|
|||||||
[database]
|
[database]
|
||||||
; Relative or absolute path to database file
|
; Relative or absolute path to database file
|
||||||
file = database.sqlite3
|
file = database.sqlite3
|
||||||
; Enables automatic compressed database backups
|
|
||||||
backup = true
|
|
||||||
; Enables database logging
|
; Enables database logging
|
||||||
;log = db.log
|
;log = db.log
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user