From c210a4f10a8525fc101c7a53d903bbc8df6d675d Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 31 Mar 2022 23:22:11 +0200 Subject: [PATCH] Add backup mechanism and some collection cleanup. --- .../Collections/CollectionManager.Active.cs | 52 ++++--- Penumbra/Collections/CollectionManager.cs | 4 +- Penumbra/Mods/ModManager.cs | 13 +- Penumbra/Penumbra.cs | 16 +- Penumbra/Util/Backup.cs | 146 ++++++++++++++++++ 5 files changed, 202 insertions(+), 29 deletions(-) create mode 100644 Penumbra/Util/Backup.cs diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index e1c6f830..fe396ccd 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -15,7 +15,7 @@ public partial class ModCollection public sealed partial class Manager { // Is invoked after the collections actually changed. - public event CollectionChangeDelegate? CollectionChanged; + public event CollectionChangeDelegate CollectionChanged; // The collection currently selected for changing settings. public ModCollection Current { get; private set; } = Empty; @@ -81,7 +81,7 @@ public partial class ModCollection break; } - CollectionChanged?.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); + CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); } public void SetCollection( ModCollection collection, Type type, string? characterName = null ) @@ -96,7 +96,7 @@ public partial class ModCollection } _characters[ characterName ] = Empty; - CollectionChanged?.Invoke( Type.Character, null, Empty, characterName ); + CollectionChanged.Invoke( Type.Character, null, Empty, characterName ); return true; } @@ -107,7 +107,7 @@ public partial class ModCollection { RemoveCache( collection.Index ); _characters.Remove( characterName ); - CollectionChanged?.Invoke( Type.Character, collection, null, characterName ); + CollectionChanged.Invoke( Type.Character, collection, null, characterName ); } } @@ -118,27 +118,13 @@ public partial class ModCollection public static string ActiveCollectionFile => Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); - // Load default, current and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. public void LoadCollections() { - var file = ActiveCollectionFile; - var configChanged = true; - var jObject = new JObject(); - if( File.Exists( file ) ) - { - try - { - jObject = JObject.Parse( File.ReadAllText( file ) ); - configChanged = false; - } - catch( Exception e ) - { - PluginLog.Error( $"Could not read active collections from file {file}:\n{e}" ); - } - } + var configChanged = !ReadActiveCollections( out var jObject ); + // Load the default collection. var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? Empty.Name; var defaultIdx = GetIndexForCollectionName( defaultName ); if( defaultIdx < 0 ) @@ -152,6 +138,7 @@ public partial class ModCollection Default = this[ defaultIdx ]; } + // Load the current collection. var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection; var currentIdx = GetIndexForCollectionName( currentName ); if( currentIdx < 0 ) @@ -182,6 +169,7 @@ public partial class ModCollection } } + // Save any changes and create all required caches. if( configChanged ) { SaveActiveCollections(); @@ -225,6 +213,30 @@ public partial class ModCollection } } + // Read the active collection file into a jObject. + // Returns true if this is successful, false if the file does not exist or it is unsuccessful. + private static bool ReadActiveCollections( out JObject ret ) + { + var file = ActiveCollectionFile; + if( File.Exists( file ) ) + { + try + { + ret = JObject.Parse( File.ReadAllText( file ) ); + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not read active collections from file {file}:\n{e}" ); + } + } + + ret = new JObject(); + return false; + } + + + // Save if any of the active collections is changed. private void SaveOnChange( Type type, ModCollection? _1, ModCollection? _2, string? _3 ) { if( type != Type.Inactive ) diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index a04a36e6..79d7d33c 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -96,7 +96,7 @@ public partial class ModCollection newCollection.Index = _collections.Count; _collections.Add( newCollection ); newCollection.Save(); - CollectionChanged?.Invoke( Type.Inactive, null, newCollection ); + CollectionChanged.Invoke( Type.Inactive, null, newCollection ); SetCollection( newCollection.Index, Type.Current ); return true; } @@ -140,7 +140,7 @@ public partial class ModCollection --_collections[ i ].Index; } - CollectionChanged?.Invoke( Type.Inactive, collection, null ); + CollectionChanged.Invoke( Type.Inactive, collection, null ); return true; } diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 1f39e7c9..19458764 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -101,24 +101,25 @@ public partial class Mod } } + public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), + "sort_order.json" ); + public Manager() { SetBaseDirectory( Config.ModDirectory, true ); // TODO try { - var data = JObject.Parse( File.ReadAllText( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(), - "sort_order.json" ) ) ); - TemporaryModSortOrder = data["Data"]?.ToObject>() ?? new Dictionary(); - + var data = JObject.Parse( File.ReadAllText( SortOrderFile ) ); + TemporaryModSortOrder = data[ "Data" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); } catch { - TemporaryModSortOrder = new Dictionary(); + TemporaryModSortOrder = new Dictionary< string, string >(); } } - public Dictionary TemporaryModSortOrder; + public Dictionary< string, string > TemporaryModSortOrder; private bool SetSortOrderPath( Mod mod, string path ) { diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 322c9f7e..ac6dcfa7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using Dalamud.Game.Command; using Dalamud.Logging; using Dalamud.Plugin; @@ -54,6 +57,7 @@ public class Penumbra : IDalamudPlugin { Dalamud.Initialize( pluginInterface ); GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); + Backup.CreateBackup( PenumbraBackupFiles() ); Config = Configuration.Load(); MusicManager = new MusicManager(); @@ -64,7 +68,7 @@ public class Penumbra : IDalamudPlugin ResidentResources = new ResidentResourceManager(); CharacterUtility = new CharacterUtility(); - MetaFileManager = new MetaFileManager(); + MetaFileManager = new MetaFileManager(); ResourceLoader = new ResourceLoader( this ); ResourceLogger = new ResourceLogger( ResourceLoader ); ModManager = new Mod.Manager(); @@ -337,4 +341,14 @@ public class Penumbra : IDalamudPlugin SettingsInterface.FlipVisibility(); } + + // Collect all relevant files for penumbra configuration. + private static IReadOnlyList< FileInfo > PenumbraBackupFiles() + { + var list = new DirectoryInfo( ModCollection.CollectionDirectory ).EnumerateFiles( "*.json" ).ToList(); + list.Add( Dalamud.PluginInterface.ConfigFile ); + list.Add( new FileInfo( Mod.Manager.SortOrderFile ) ); + list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); + return list; + } } \ No newline at end of file diff --git a/Penumbra/Util/Backup.cs b/Penumbra/Util/Backup.cs new file mode 100644 index 00000000..878731d7 --- /dev/null +++ b/Penumbra/Util/Backup.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using Dalamud.Logging; + +namespace Penumbra.Util; + +public static class Backup +{ + public const int MaxNumBackups = 10; + + // Create a backup named by ISO 8601 of the current time. + // If the newest previously existing backup equals the current state of files, + // do not create a new backup. + // If the maximum number of backups is exceeded afterwards, delete the oldest backup. + public static void CreateBackup( IReadOnlyCollection< FileInfo > files ) + { + try + { + var configDirectory = Dalamud.PluginInterface.ConfigDirectory.Parent!.FullName; + var directory = CreateBackupDirectory(); + var (newestFile, oldestFile, numFiles) = CheckExistingBackups( directory ); + var newBackupName = Path.Combine( directory.FullName, $"{DateTime.Now:yyyyMMddHHmss}.zip" ); + if( newestFile == null || CheckNewestBackup( newestFile, configDirectory, files.Count ) ) + { + CreateBackup( files, newBackupName, configDirectory ); + if( numFiles > MaxNumBackups ) + { + oldestFile!.Delete(); + } + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create backups:\n{e}" ); + } + } + + + // Obtain the backup directory. Create it if it does not exist. + private static DirectoryInfo CreateBackupDirectory() + { + var path = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.Parent!.Parent!.FullName, "backups", + Dalamud.PluginInterface.ConfigDirectory.Name ); + var dir = new DirectoryInfo( path ); + if( !dir.Exists ) + { + dir = Directory.CreateDirectory( dir.FullName ); + } + + return dir; + } + + // Check the already existing backups. + // Only keep MaxNumBackups at once, and delete the oldest if the number would be exceeded. + // Return the newest backup. + private static (FileInfo? Newest, FileInfo? Oldest, int Count) CheckExistingBackups( DirectoryInfo backupDirectory ) + { + var count = 0; + FileInfo? newest = null; + FileInfo? oldest = null; + + foreach( var file in backupDirectory.EnumerateFiles( "*.zip" ) ) + { + ++count; + var time = file.CreationTimeUtc; + if( ( oldest?.CreationTimeUtc ?? DateTime.MinValue ) < time ) + { + oldest = file; + } + + if( ( newest?.CreationTimeUtc ?? DateTime.MaxValue ) > time ) + { + newest = file; + } + } + + return ( newest, oldest, count ); + } + + // Compare the newest backup against the currently existing files. + // If there are any differences, return false, and if they are completely identical, return true. + private static bool CheckNewestBackup( FileInfo newestFile, string configDirectory, int fileCount ) + { + using var oldFileStream = File.Open( newestFile.FullName, FileMode.Open ); + using var oldZip = new ZipArchive( oldFileStream, ZipArchiveMode.Read ); + // Number of stored files is different. + if( fileCount != oldZip.Entries.Count ) + { + return true; + } + + // Since number of files is identical, + // the backups are identical if every file in the old backup + // still exists and is identical. + foreach( var entry in oldZip.Entries ) + { + var file = Path.Combine( configDirectory, entry.FullName ); + if( !File.Exists( file ) ) + { + return true; + } + + using var currentData = File.OpenRead( file ); + using var oldData = entry.Open(); + + if( !Equals( currentData, oldData ) ) + { + return true; + } + } + + return false; + } + + // Create the actual backup, storing all the files relative to the given configDirectory in the zip. + private static void CreateBackup( IEnumerable< FileInfo > files, string fileName, string configDirectory ) + { + using var fileStream = File.Open( fileName, FileMode.Create ); + using var zip = new ZipArchive( fileStream, ZipArchiveMode.Create ); + foreach( var file in files ) + { + zip.CreateEntryFromFile( file.FullName, Path.GetRelativePath( configDirectory, file.FullName ), CompressionLevel.Optimal ); + } + } + + // Compare two streams per byte and return if they are equal. + private static bool Equals( Stream lhs, Stream rhs ) + { + while( true ) + { + var current = lhs.ReadByte(); + var old = rhs.ReadByte(); + if( current != old ) + { + return false; + } + + if( current == -1 ) + { + return true; + } + } + } +} \ No newline at end of file