Code/Backups/BackupsController.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;

namespace SandbankDatabase;

internal static class Backups
{
	public const string BACKUP_DATE_FORMAT = "yyyy-MM-dd htt";

	/// <summary>
	/// Only allow one backup at once.
	/// </summary>
	public static object BackupLock = new();

	/// <summary>
	/// Check if we should do a backup. If we should, do one. Also check if we have too many backups and
	/// need to delete one.
	/// </summary>
	public static void CheckBackupStatus()
	{
		lock ( BackupLock )
		{
			try
			{
				var backups = GetBackups();

				// Do backups maybe.
				if ( ShouldDoBackup( backups ) )
					DoBackup();

				var numberOfBackups = backups.Count + 1;

				// Delete a backup if we have too many.
				if ( numberOfBackups > ConfigController.BACKUPS_TO_KEEP )
					FileController.DeleteBackup( backups.First().FolderName );
			}
			catch ( Exception e )
			{
				Logging.Warn( "checking backups failed: " + Logging.ExtractExceptionString( e ) );
			}
		}
	}

	/// <summary>
	/// Returns the existing backup records in order from oldest to newest.
	/// </summary>
	private static List<Backup> GetBackups()
	{
		var backupFolders = FileController.ListBackupFolders();

		var invalidNames = backupFolders
			.Where( x => DateTime.TryParseExact( x, BACKUP_DATE_FORMAT, CultureInfo.CurrentCulture, DateTimeStyles.None, out _ ) == false );

		if ( invalidNames.Any() )
			Logging.Warn( $"backup folder {invalidNames.First()} has an invalid name, ignoring..." );

		backupFolders = backupFolders
			.Where( x => DateTime.TryParseExact( x, BACKUP_DATE_FORMAT, CultureInfo.CurrentCulture, DateTimeStyles.None, out _ ) == true )
			.ToList();

		return backupFolders
			.Select( x => new Backup
			{
				BackupTime = DateTime.ParseExact( x, BACKUP_DATE_FORMAT, CultureInfo.CurrentCulture ),
				FolderName = x
			} )
			.OrderBy( x => x.BackupTime )
			.ToList();
	}

	private static bool ShouldDoBackup( List<Backup> backups )
	{
		var mostRecentBackupTime = backups.Count > 0 ? backups.Last().BackupTime : DateTime.MinValue;

		switch ( ConfigController.BACKUP_FREQUENCY )
		{
			case BackupFrequency.Never:
				return false;
			case BackupFrequency.Hourly:
				return DateTime.UtcNow.Subtract( mostRecentBackupTime ).TotalHours >= 1;
			case BackupFrequency.Daily:
				return DateTime.UtcNow.Subtract( mostRecentBackupTime ).TotalDays >= 1;
			case BackupFrequency.Weekly:
				return DateTime.UtcNow.Subtract( mostRecentBackupTime ).TotalDays >= 7;
			default:
				throw new SandbankException( $"unknown backup frequency {ConfigController.BACKUP_FREQUENCY}" );
		}
	}

	private static void DoBackup()
	{
		Logging.Info( "performing backup..." );

		var stopwatch = Stopwatch.StartNew();

		var backupFolderName = DateTime.UtcNow.Date.AddHours( DateTime.UtcNow.Hour ).ToString( BACKUP_DATE_FORMAT );
		var collections = Cache.GetAllCollections();

		foreach ( var collection in collections )
		{
			Logging.Info( $"backing up collection {collection.CollectionName}..." );

			FileController.CreateBackupCollectionFolder( backupFolderName, collection );
			FileController.SaveBackupCollectionDefinition( backupFolderName, collection );

			foreach ( var document in collection.CachedDocuments )
			{
				FileController.SaveBackupDocument( backupFolderName, collection, document.Value );
			}
		}

		Logging.Log( $"backup took {stopwatch.Elapsed.TotalSeconds} seconds" );

		Logging.Info( "backup complete!" );
	}
}

struct Backup
{
	public DateTime BackupTime { get; set; }
	public string FolderName;
}