mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add backup mechanism and some collection cleanup.
This commit is contained in:
parent
d906e5aedf
commit
c210a4f10a
5 changed files with 202 additions and 29 deletions
|
|
@ -15,7 +15,7 @@ public partial class ModCollection
|
||||||
public sealed partial class Manager
|
public sealed partial class Manager
|
||||||
{
|
{
|
||||||
// Is invoked after the collections actually changed.
|
// Is invoked after the collections actually changed.
|
||||||
public event CollectionChangeDelegate? CollectionChanged;
|
public event CollectionChangeDelegate CollectionChanged;
|
||||||
|
|
||||||
// The collection currently selected for changing settings.
|
// The collection currently selected for changing settings.
|
||||||
public ModCollection Current { get; private set; } = Empty;
|
public ModCollection Current { get; private set; } = Empty;
|
||||||
|
|
@ -81,7 +81,7 @@ public partial class ModCollection
|
||||||
break;
|
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 )
|
public void SetCollection( ModCollection collection, Type type, string? characterName = null )
|
||||||
|
|
@ -96,7 +96,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
|
|
||||||
_characters[ characterName ] = Empty;
|
_characters[ characterName ] = Empty;
|
||||||
CollectionChanged?.Invoke( Type.Character, null, Empty, characterName );
|
CollectionChanged.Invoke( Type.Character, null, Empty, characterName );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,7 +107,7 @@ public partial class ModCollection
|
||||||
{
|
{
|
||||||
RemoveCache( collection.Index );
|
RemoveCache( collection.Index );
|
||||||
_characters.Remove( characterName );
|
_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
|
public static string ActiveCollectionFile
|
||||||
=> Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "active_collections.json" );
|
=> Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "active_collections.json" );
|
||||||
|
|
||||||
|
|
||||||
// Load default, current and character collections from config.
|
// Load default, current and character collections from config.
|
||||||
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
|
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
|
||||||
public void LoadCollections()
|
public void LoadCollections()
|
||||||
{
|
{
|
||||||
var file = ActiveCollectionFile;
|
var configChanged = !ReadActiveCollections( out var jObject );
|
||||||
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}" );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Load the default collection.
|
||||||
var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? Empty.Name;
|
var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? Empty.Name;
|
||||||
var defaultIdx = GetIndexForCollectionName( defaultName );
|
var defaultIdx = GetIndexForCollectionName( defaultName );
|
||||||
if( defaultIdx < 0 )
|
if( defaultIdx < 0 )
|
||||||
|
|
@ -152,6 +138,7 @@ public partial class ModCollection
|
||||||
Default = this[ defaultIdx ];
|
Default = this[ defaultIdx ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the current collection.
|
||||||
var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection;
|
var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection;
|
||||||
var currentIdx = GetIndexForCollectionName( currentName );
|
var currentIdx = GetIndexForCollectionName( currentName );
|
||||||
if( currentIdx < 0 )
|
if( currentIdx < 0 )
|
||||||
|
|
@ -182,6 +169,7 @@ public partial class ModCollection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save any changes and create all required caches.
|
||||||
if( configChanged )
|
if( configChanged )
|
||||||
{
|
{
|
||||||
SaveActiveCollections();
|
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 )
|
private void SaveOnChange( Type type, ModCollection? _1, ModCollection? _2, string? _3 )
|
||||||
{
|
{
|
||||||
if( type != Type.Inactive )
|
if( type != Type.Inactive )
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ public partial class ModCollection
|
||||||
newCollection.Index = _collections.Count;
|
newCollection.Index = _collections.Count;
|
||||||
_collections.Add( newCollection );
|
_collections.Add( newCollection );
|
||||||
newCollection.Save();
|
newCollection.Save();
|
||||||
CollectionChanged?.Invoke( Type.Inactive, null, newCollection );
|
CollectionChanged.Invoke( Type.Inactive, null, newCollection );
|
||||||
SetCollection( newCollection.Index, Type.Current );
|
SetCollection( newCollection.Index, Type.Current );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +140,7 @@ public partial class ModCollection
|
||||||
--_collections[ i ].Index;
|
--_collections[ i ].Index;
|
||||||
}
|
}
|
||||||
|
|
||||||
CollectionChanged?.Invoke( Type.Inactive, collection, null );
|
CollectionChanged.Invoke( Type.Inactive, collection, null );
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,24 +101,25 @@ public partial class Mod
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string SortOrderFile = Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
|
||||||
|
"sort_order.json" );
|
||||||
|
|
||||||
public Manager()
|
public Manager()
|
||||||
{
|
{
|
||||||
SetBaseDirectory( Config.ModDirectory, true );
|
SetBaseDirectory( Config.ModDirectory, true );
|
||||||
// TODO
|
// TODO
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var data = JObject.Parse( File.ReadAllText( Path.Combine( Dalamud.PluginInterface.GetPluginConfigDirectory(),
|
var data = JObject.Parse( File.ReadAllText( SortOrderFile ) );
|
||||||
"sort_order.json" ) ) );
|
TemporaryModSortOrder = data[ "Data" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >();
|
||||||
TemporaryModSortOrder = data["Data"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
TemporaryModSortOrder = new Dictionary<string, string>();
|
TemporaryModSortOrder = new Dictionary< string, string >();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Dictionary<string, string> TemporaryModSortOrder;
|
public Dictionary< string, string > TemporaryModSortOrder;
|
||||||
|
|
||||||
private bool SetSortOrderPath( Mod mod, string path )
|
private bool SetSortOrderPath( Mod mod, string path )
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Dalamud.Game.Command;
|
using Dalamud.Game.Command;
|
||||||
using Dalamud.Logging;
|
using Dalamud.Logging;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
|
@ -54,6 +57,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
{
|
{
|
||||||
Dalamud.Initialize( pluginInterface );
|
Dalamud.Initialize( pluginInterface );
|
||||||
GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage );
|
GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage );
|
||||||
|
Backup.CreateBackup( PenumbraBackupFiles() );
|
||||||
Config = Configuration.Load();
|
Config = Configuration.Load();
|
||||||
|
|
||||||
MusicManager = new MusicManager();
|
MusicManager = new MusicManager();
|
||||||
|
|
@ -64,7 +68,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
|
|
||||||
ResidentResources = new ResidentResourceManager();
|
ResidentResources = new ResidentResourceManager();
|
||||||
CharacterUtility = new CharacterUtility();
|
CharacterUtility = new CharacterUtility();
|
||||||
MetaFileManager = new MetaFileManager();
|
MetaFileManager = new MetaFileManager();
|
||||||
ResourceLoader = new ResourceLoader( this );
|
ResourceLoader = new ResourceLoader( this );
|
||||||
ResourceLogger = new ResourceLogger( ResourceLoader );
|
ResourceLogger = new ResourceLogger( ResourceLoader );
|
||||||
ModManager = new Mod.Manager();
|
ModManager = new Mod.Manager();
|
||||||
|
|
@ -337,4 +341,14 @@ public class Penumbra : IDalamudPlugin
|
||||||
|
|
||||||
SettingsInterface.FlipVisibility();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
146
Penumbra/Util/Backup.cs
Normal file
146
Penumbra/Util/Backup.cs
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue