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 1 commit
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
121 changes: 121 additions & 0 deletions src/XIVLauncher.Common/Support/DalamudAndPluginConfigImportExport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.IO;
using System.Threading.Tasks;

namespace XIVLauncher.Common.Support
{
public class DalamudAndPluginConfigImportExport
{
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");

AddIfExist(accountsListFile, archive);
AddIfExist(dalamudConfigFile, archive);
AddIfExist(dalamudVfsFile, archive);
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

AddIfExist(backupsFolder, archive);
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;
Copy link
Member

Choose a reason for hiding this comment

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

Path.Combine()?

// 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);
}
}

private static void AddIfExist(string entryPath, ZipArchive zip)
Copy link
Member

Choose a reason for hiding this comment

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

We already have this function in the tspack code. Maybe move to common util class?

private static void AddIfExist(FileInfo file, ZipArchive zip)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one's a little different from tspack code. I had to get a little hacky for directory handling. But that could probably still be used for tspacks too. I've moved it to a util method in xivlauncher.common.util, but the WPF app didn't really like me doing that and VS stopped giving me intellisense when I tried. I might need some handholding to get that moved properly. (It could just be my machine being annoying)

{
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