Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement config import/export buttons for in game addon #1426

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.IO;
using System.Threading.Tasks;
using XIVLauncher.Common.Util;

namespace XIVLauncher.Common.Support
{
public class DalamudAndPluginConfigImportExport : ZipMethods
{
public static async Task<string> ExportConfig(string storagePath)
{
// grab any platform-specific details
// there's none to grab currently :)

// start making our zip
var outFile = new FileInfo(Path.Combine(storagePath, $"XIVLauncher-{DateTime.Now:yyyyMMddhhmmss}.xlconf"));
using var archive = ZipFile.Open(outFile.FullName, ZipArchiveMode.Create);

// add all the stock XIVLauncher based paths
var accountsListFile = Path.Combine(storagePath, "accountsList.json");
var dalamudConfigFile = Path.Combine(storagePath, "dalamudConfig.json");
var dalamudVfsFile = Path.Combine(storagePath, "dalamudVfs.db");
var pluginConfigsFolder = Path.Combine(storagePath, "pluginConfigs");

ZipMethods.AddIfExist(accountsListFile, archive);
ZipMethods.AddIfExist(dalamudConfigFile, archive);
ZipMethods.AddIfExist(dalamudVfsFile, archive);
ZipMethods.AddIfExist(pluginConfigsFolder, archive);

// add some known special exceptions. It might be better to not build these expectations though
var backupsFolder = Path.Combine(storagePath, "backups"); // Otter plugins
var playerTrackBackupsFolder = Path.Combine(storagePath, "playerTrackBackups"); // PlayerTrack

ZipMethods.AddIfExist(backupsFolder, archive);
ZipMethods.AddIfExist(playerTrackBackupsFolder, archive);

// return the folder containing our exported settings
return outFile.FullName;
}

public static void ImportConfig(string zipFilePath, string storagePath)
{
// grab any platform-specific details
// there's none to grab currently :)

// TODO: Decide if we're going to alert on overwriting config.
// Right now, Franz decided against it. The user has to intentionally try to use this feature
// Also, .Net Framework is dumb and will explode if we use ZipArchive.ExtractToDirectory()
// and there are any file conflicts. .Net Core doesn't have this issue though and provides
// an override we could have used to do it anyways. Alternatively, we could just delete
// all of the files/folders we'd be restoring to first, but that also feels bad.



var inFile = new FileInfo(zipFilePath);
using var archive = ZipFile.Open(inFile.FullName, ZipArchiveMode.Read);

// If we weren't on .Net Framework, we could use this...
// ZipFileExtensions.ExtractToDirectory(archive, storagePath, true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better to go through the entries directly. Then you can also choose to delete the file if it exists - which is probably what people expect when importing a backup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My initial vision was to just overwrite from the backup, but ideally yes, a "do you want to replace this file, yes, yes to all, no, cancel" style prompt would be better. My worry here is some plugins generate TONS of files in their config directories and most users are not going to want to click yes to that every time. (ex: CharacterSync making backups of all game config for multiple characters for multiple days/launches)

This was more of a companion to the official launcher's FEA file solution, but for our config. (Although nothing prevents this from later handling game config too if we wanted to do that)


foreach (var entry in archive.Entries)
{
//var extractPath = storagePath + "\\" + entry.FullName;
var extractPath = Path.Combine(storagePath, entry.FullName);


// If we were going to warn about overwriting files, it would go here.
/*
bool promptAlwaysForOverwrite = true;
if (promptAlwaysForOverwrite && File.Exists(extractPath))
{
// Make some prompt. Overwrite? Yes, Yes to All, Cancel

if (result == no) break or something.
if (result == yestoall) promptAlwaysForOverwrite = false;
// yes is the default and needs no special handling.
}
*/
if (!Directory.Exists(Path.GetDirectoryName(extractPath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(extractPath));
}
ZipFileExtensions.ExtractToFile(entry, extractPath, true);
}
}
}
}
46 changes: 46 additions & 0 deletions src/XIVLauncher.Common/Util/ZipMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;

namespace XIVLauncher.Common.Util
{
public class ZipMethods
{

public static void AddIfExist(string entryPath, ZipArchive zip)
{
if (File.Exists(entryPath))
{
using var stream = File.Open(entryPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
var entry = zip.CreateEntry(new FileInfo(entryPath).Name);
using var entryStream = entry.Open();
stream.CopyTo(entryStream);
//zip.CreateEntryFromFile(file.FullName, file.Name);
}
// directory handling solution based on answer from https://stackoverflow.com/a/62797701
else if (Directory.Exists(entryPath))
{
var dir = new DirectoryInfo(entryPath);
var folders = new Stack<string>();
folders.Push(entryPath);

do
{
var currentFolder = folders.Pop();
foreach (var filename in Directory.GetFiles(currentFolder))
{
using var stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
var entry = zip.CreateEntry($"{dir.Name}\\{filename.Substring(entryPath.Length + 1)}");
using var entryStream = entry.Open();
stream.CopyTo(entryStream);
//zip.CreateEntryFromFile(filename, $"{dir.Name}\\{filename.Substring(entryPath.Length + 1)}");
}
foreach (var dirname in Directory.GetDirectories(currentFolder))
{
folders.Push(dirname);
}
} while (folders.Count > 0);
}
}
}
}
62 changes: 61 additions & 1 deletion src/XIVLauncher/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -59,6 +60,12 @@ public class CmdLineOptions
[CommandLine.Option("clientlang", Required = false, HelpText = "Client language to use.")]
public ClientLanguage? ClientLanguage { get; set; }

[CommandLine.Option("exportconfig", Required = false, HelpText = "Export a zip of all config")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these necessary? What's your use case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly needed. I just like having command args for any of the file-based features. Makes it easy for testing when I want to specify custom appdata paths to certify that it unpacks correctly.

public bool DoExportConfig { get; set; }

[CommandLine.Option("importconfig", Required = false, HelpText = "Import a zip of all config")]
public string DoImportConfig { get; set; }

// We don't care about these, just need it so that the parser doesn't error
[CommandLine.Option("squirrel-updated", Hidden = true)]
public string SquirrelUpdated { get; set; }
Expand Down Expand Up @@ -251,6 +258,49 @@ private static void GenerateLocalizables()
Environment.Exit(0);
}

private static void ExportConfig()
{
try
{
var filename= DalamudAndPluginConfigImportExport.ExportConfig(Paths.RoamingPath);

MessageBox.Show($"Exported config as {filename}.\n\nXIVLauncher will now exit.", "XIVLauncher", MessageBoxButton.OK, MessageBoxImage.Asterisk);
Process.Start("explorer.exe", $"/select, \"{filename}\"");
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}

Environment.Exit(0);
}

private static void ImportConfig(string zipFile)
{
try
{
var importPromptResult = MessageBox.Show($"XIVLauncher is going to import config from {zipFile}. "
+ "This will overwrite any files that already exist.", "XIVLauncher",
MessageBoxButton.OKCancel, MessageBoxImage.Asterisk);

if (importPromptResult == MessageBoxResult.OK)
{
DalamudAndPluginConfigImportExport.ImportConfig(zipFile, Paths.RoamingPath);

MessageBox.Show($"Imported config.\n\nXIVLauncher will now exit. Please re-run XIVLauncher.", "XIVLauncher", MessageBoxButton.OK, MessageBoxImage.Asterisk);
Environment.Exit(0);
}

}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
Environment.Exit(0);
}


}

private bool _useFullExceptionHandler = false;

private void TaskSchedulerOnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
Expand Down Expand Up @@ -355,6 +405,16 @@ private void App_OnStartup(object sender, StartupEventArgs e)
{
GenerateLocalizables();
}

if (CommandLine.DoExportConfig)
{
ExportConfig();
}

if (!string.IsNullOrEmpty(CommandLine.DoImportConfig))
{
ImportConfig(CommandLine.DoImportConfig);
}
}
catch (Exception ex)
{
Expand Down
Loading