diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index 245a2a9ac..df84f9545 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -91,7 +91,7 @@ internal sealed partial class TextureManager throw new NullReferenceException($"{nameof(path)} cannot be null."); using var wrapAux = new WrapAux(wrap, true); - var pathTemp = Util.GetTempFileNameForFileReplacement(path); + var pathTemp = Util.GetReplaceableFileName(path); var trashfire = new List(); try { diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 5cab004ed..ccae6dd06 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -48,6 +48,8 @@ internal class PluginManager : IInternalDisposableService /// public const int PluginWaitBeforeFreeDefault = 1000; // upped from 500ms, seems more stable + private const string BrokenMarkerFileName = ".broken"; + private static readonly ModuleLog Log = ModuleLog.Create(); private readonly object pluginListLock = new(); @@ -874,7 +876,7 @@ internal class PluginManager : IInternalDisposableService } /// - /// Cleanup disabled plugins. Does not target devPlugins. + /// Cleanup disabled and broken plugins. Does not target devPlugins. /// public void CleanupPlugins() { @@ -882,6 +884,13 @@ internal class PluginManager : IInternalDisposableService { try { + if (File.Exists(Path.Combine(pluginDir.FullName, BrokenMarkerFileName))) + { + Log.Warning("Cleaning up broken plugin {Name}", pluginDir.Name); + pluginDir.Delete(true); + continue; + } + var versionDirs = pluginDir.GetDirectories(); versionDirs = versionDirs @@ -1423,7 +1432,8 @@ internal class PluginManager : IInternalDisposableService else { // If we are doing anything other than a fresh install, not having a workingPluginId is an error that must be fixed - Debug.Assert(inheritedWorkingPluginId != null, "inheritedWorkingPluginId != null"); + if (inheritedWorkingPluginId != null) + throw new InvalidOperationException("Inherited WorkingPluginId must not be null"); } // Ensure that we have a testing opt-in for this plugin if we are installing a testing version @@ -1434,31 +1444,28 @@ internal class PluginManager : IInternalDisposableService this.configuration.QueueSave(); } - var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty)); + var pluginVersionsDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName)); + var tempOutputDir = new DirectoryInfo(FilesystemUtil.GetTempFileName()); + var outputDir = new DirectoryInfo(Path.Combine(pluginVersionsDir.FullName, version?.ToString() ?? string.Empty)); + + FilesystemUtil.DeleteAndRecreateDirectory(tempOutputDir); + FilesystemUtil.DeleteAndRecreateDirectory(outputDir); + + Log.Debug("Extracting plugin to {TempOutputDir}", tempOutputDir); try { - if (outputDir.Exists) - outputDir.Delete(true); + using var archive = new ZipArchive(zipStream); - outputDir.Create(); - } - catch - { - // ignored, since the plugin may be loaded already - } - - Log.Debug("Extracting to {OutputDir}", outputDir); - - using (var archive = new ZipArchive(zipStream)) - { foreach (var zipFile in archive.Entries) { - var outputFile = new FileInfo(Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName))); + var outputFile = new FileInfo( + Path.GetFullPath(Path.Combine(tempOutputDir.FullName, zipFile.FullName))); - if (!outputFile.FullName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase)) + if (!outputFile.FullName.StartsWith(tempOutputDir.FullName, StringComparison.OrdinalIgnoreCase)) { - throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); + throw new IOException( + "Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); } if (outputFile.Directory == null) @@ -1469,70 +1476,88 @@ internal class PluginManager : IInternalDisposableService if (zipFile.Name.IsNullOrEmpty()) { // Assuming Empty for Directory - Log.Verbose($"ZipFile name is null or empty, treating as a directory: {outputFile.Directory.FullName}"); + Log.Verbose( + "ZipFile name is null or empty, treating as a directory: {Path}", outputFile.Directory.FullName); Directory.CreateDirectory(outputFile.Directory.FullName); continue; } // Ensure directory is created Directory.CreateDirectory(outputFile.Directory.FullName); - - try - { - zipFile.ExtractToFile(outputFile.FullName, true); - } - catch (Exception ex) - { - if (outputFile.Extension.EndsWith("dll")) - { - throw new IOException($"Could not overwrite {zipFile.Name}: {ex.Message}"); - } - - Log.Error($"Could not overwrite {zipFile.Name}: {ex.Message}"); - } + zipFile.ExtractToFile(outputFile.FullName, true); } + + var tempDllFile = LocalPluginManifest.GetPluginFile(tempOutputDir, repoManifest); + var tempManifestFile = LocalPluginManifest.GetManifestFile(tempDllFile); + + // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use. + FilesystemUtil.WriteAllTextSafe( + tempManifestFile.FullName, + JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); + + // Reload as a local manifest, add some attributes, and save again. + var tempManifest = LocalPluginManifest.Load(tempManifestFile); + + if (tempManifest == null) + throw new Exception("Plugin had no valid manifest"); + + if (tempManifest.InternalName != repoManifest.InternalName) + { + throw new Exception( + $"Distributed internal name does not match repo internal name: {tempManifest.InternalName} - {repoManifest.InternalName}"); + } + + if (tempManifest.WorkingPluginId != Guid.Empty) + throw new Exception("Plugin shall not specify a WorkingPluginId"); + + tempManifest.WorkingPluginId = inheritedWorkingPluginId ?? Guid.NewGuid(); + + if (useTesting) + { + tempManifest.Testing = true; + } + + // Document the url the plugin was installed from + tempManifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty + ? repoManifest.SourceRepo.PluginMasterUrl + : SpecialPluginSource.MainRepo; + + tempManifest.Save(tempManifestFile, "installation"); + + // Copy the directory to the final location + Log.Debug("Copying plugin from {TempOutputDir} to {OutputDir}", tempOutputDir, outputDir); + FilesystemUtil.CopyFilesRecursively(tempOutputDir, outputDir); + + var finalDllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest); + var finalManifestFile = LocalPluginManifest.GetManifestFile(finalDllFile); + var finalManifest = LocalPluginManifest.Load(finalManifestFile) ?? + throw new Exception("Plugin had no valid manifest after copy"); + + Log.Information("Installed plugin {InternalName} (testing={UseTesting})", tempManifest.Name, useTesting); + var plugin = await this.LoadPluginAsync(finalDllFile, finalManifest, reason); + + this.NotifyinstalledPluginsListChanged(); + return plugin; } - - var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest); - var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - - // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use. - Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); - - // Reload as a local manifest, add some attributes, and save again. - var manifest = LocalPluginManifest.Load(manifestFile); - - if (manifest == null) - throw new Exception("Plugin had no valid manifest"); - - if (manifest.InternalName != repoManifest.InternalName) + catch { - Directory.Delete(outputDir.FullName, true); - throw new Exception( - $"Distributed internal name does not match repo internal name: {manifest.InternalName} - {repoManifest.InternalName}"); + // Attempt to clean up if we can + try + { + outputDir.Delete(true); + } + catch + { + // Write marker file if we can't, we'll try to do it at the next start + File.WriteAllText(Path.Combine(pluginVersionsDir.FullName, BrokenMarkerFileName), string.Empty); + } + + throw; } - - if (manifest.WorkingPluginId != Guid.Empty) - throw new Exception("Plugin shall not specify a WorkingPluginId"); - - manifest.WorkingPluginId = inheritedWorkingPluginId ?? Guid.NewGuid(); - - if (useTesting) + finally { - manifest.Testing = true; + tempOutputDir.Delete(true); } - - // Document the url the plugin was installed from - manifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : SpecialPluginSource.MainRepo; - - manifest.Save(manifestFile, "installation"); - - Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})"); - - var plugin = await this.LoadPluginAsync(dllFile, manifest, reason); - - this.NotifyinstalledPluginsListChanged(); - return plugin; } /// @@ -1547,7 +1572,6 @@ internal class PluginManager : IInternalDisposableService /// The loaded plugin. private async Task LoadPluginAsync(FileInfo dllFile, LocalPluginManifest manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false) { - var name = manifest?.Name ?? dllFile.Name; var loadPlugin = !doNotLoad; LocalPlugin? plugin; @@ -1560,7 +1584,7 @@ internal class PluginManager : IInternalDisposableService if (isDev) { - Log.Information($"Loading dev plugin {name}"); + Log.Information("Loading dev plugin {Name}", manifest.InternalName); plugin = new LocalDevPlugin(dllFile, manifest); // This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet @@ -1569,7 +1593,7 @@ internal class PluginManager : IInternalDisposableService } else { - Log.Information($"Loading plugin {name}"); + Log.Information("Loading plugin {Name}", manifest.InternalName); plugin = new LocalPlugin(dllFile, manifest); } @@ -1663,7 +1687,7 @@ internal class PluginManager : IInternalDisposableService } else { - Log.Verbose($"{name} not loaded, wantToLoad:{wantedByAnyProfile} orphaned:{plugin.IsOrphaned}"); + Log.Verbose("{Name} not loaded, wantToLoad:{WantedByAnyProfile} orphaned:{IsOrphaned}", manifest.InternalName, wantedByAnyProfile, plugin.IsOrphaned); } } catch (InvalidPluginException) diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index e36e9908b..8a1711b0d 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -57,7 +57,7 @@ internal class ProfileManager : IServiceType /// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins. /// public bool IsBusy => this.isBusy; - + /// /// Get a disposable that will lock the profile list while it is not disposed. /// You must NEVER use this in async code. @@ -77,7 +77,7 @@ internal class ProfileManager : IServiceType { var want = false; var wasInAnyProfile = false; - + lock (this.profiles) { foreach (var profile in this.profiles) @@ -93,7 +93,7 @@ internal class ProfileManager : IServiceType if (!wasInAnyProfile && addIfNotDeclared) { - Log.Warning("'{Guid}'('{InternalName}') was not in any profile, adding to default with {Default}", workingPluginId, internalName, defaultState); + Log.Warning("{Guid}({InternalName}) was not in any profile, adding to default with {Default}", workingPluginId, internalName, defaultState); await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, internalName, defaultState, false); return defaultState; @@ -175,7 +175,7 @@ internal class ProfileManager : IServiceType { // Disable it modelV1.IsEnabled = false; - + // Try to find matching plugins for all plugins in the profile var pm = Service.Get(); foreach (var plugin in modelV1.Plugins) @@ -313,7 +313,7 @@ internal class ProfileManager : IServiceType profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid); } } - + /// /// Validate profiles for errors. /// @@ -328,7 +328,7 @@ internal class ProfileManager : IServiceType { if (seenIds.Contains(pluginEntry.WorkingPluginId)) throw new Exception($"Plugin '{pluginEntry.WorkingPluginId}'('{pluginEntry.InternalName}') is twice in profile '{profile.Guid}'('{profile.Name}')"); - + seenIds.Add(pluginEntry.WorkingPluginId); } } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index a86a39905..1b9025538 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -605,6 +605,10 @@ internal class LocalPlugin : IAsyncDisposable if (this.loader != null) return; + this.DllFile.Refresh(); + if (!this.DllFile.Exists) + throw new Exception($"Plugin DLL file at '{this.DllFile.FullName}' did not exist, cannot load."); + try { this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig); diff --git a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs index dc05409b0..3aededa18 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs @@ -57,15 +57,15 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest /// The reason the manifest was saved. public void Save(FileInfo manifestFile, string reason) { - Log.Verbose("Saving manifest for '{PluginName}' because '{Reason}'", this.InternalName, reason); + Log.Verbose("Saving manifest for {PluginName} because {Reason}", this.InternalName, reason); try { - Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented)); + FilesystemUtil.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(this, Formatting.Indented)); } catch { - Log.Error("Could not write out manifest for '{PluginName}' because '{Reason}'", this.InternalName, reason); + Log.Error("Could not write out manifest for {PluginName} because {Reason}", this.InternalName, reason); throw; } } @@ -78,7 +78,7 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest public static LocalPluginManifest? Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName)); /// - /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist. + /// A standardized way to get the plugin DLL name that should accompany a manifest file. /// /// Manifest directory. /// The manifest. @@ -86,7 +86,7 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest public static FileInfo GetPluginFile(DirectoryInfo dir, PluginManifest manifest) => new(Path.Combine(dir.FullName, $"{manifest.InternalName}.dll")); /// - /// A standardized way to get the manifest file that should accompany a plugin DLL. May not exist. + /// A standardized way to get the manifest file that should accompany a plugin DLL. /// /// The plugin DLL. /// The file. diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index b78d16cc7..9b87a71a0 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -29,7 +29,7 @@ internal class ReliableFileStorage : IInternalDisposableService private readonly object syncRoot = new(); private SQLiteConnection? db; - + /// /// Initializes a new instance of the class. /// @@ -37,7 +37,7 @@ internal class ReliableFileStorage : IInternalDisposableService public ReliableFileStorage(string vfsDbPath) { var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db"); - + Log.Verbose("Initializing VFS database at {Path}", databasePath); try @@ -52,7 +52,7 @@ internal class ReliableFileStorage : IInternalDisposableService { if (File.Exists(databasePath)) File.Delete(databasePath); - + this.SetupDb(databasePath); } catch (Exception) @@ -79,13 +79,13 @@ internal class ReliableFileStorage : IInternalDisposableService if (this.db == null) return false; - + // If the file doesn't actually exist on the FS, but it does in the DB, we can say YES and read operations will read from the DB instead var normalizedPath = NormalizePath(path); var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); return file != null; } - + /// /// Write all text to a file. /// @@ -94,7 +94,7 @@ internal class ReliableFileStorage : IInternalDisposableService /// Container to write to. public void WriteAllText(string path, string? contents, Guid containerId = default) => this.WriteAllText(path, contents, Encoding.UTF8, containerId); - + /// /// Write all text to a file. /// @@ -107,7 +107,7 @@ internal class ReliableFileStorage : IInternalDisposableService var bytes = encoding.GetBytes(contents ?? string.Empty); this.WriteAllBytes(path, bytes, containerId); } - + /// /// Write all bytes to a file. /// @@ -122,10 +122,10 @@ internal class ReliableFileStorage : IInternalDisposableService { if (this.db == null) { - Util.WriteAllBytesSafe(path, bytes); + FilesystemUtil.WriteAllBytesSafe(path, bytes); return; } - + this.db.RunInTransaction(() => { var normalizedPath = NormalizePath(path); @@ -145,8 +145,8 @@ internal class ReliableFileStorage : IInternalDisposableService file.Data = bytes; this.db.Update(file); } - - Util.WriteAllBytesSafe(path, bytes); + + FilesystemUtil.WriteAllBytesSafe(path, bytes); }); } } @@ -180,7 +180,7 @@ internal class ReliableFileStorage : IInternalDisposableService var bytes = this.ReadAllBytes(path, forceBackup, containerId); return encoding.GetString(bytes); } - + /// /// Read all text from a file, and automatically try again with the backup if the file does not exist or /// the function throws an exception. If the backup read also throws an exception, @@ -208,11 +208,11 @@ internal class ReliableFileStorage : IInternalDisposableService public void ReadAllText(string path, Encoding encoding, Action reader, Guid containerId = default) { ArgumentException.ThrowIfNullOrEmpty(path); - + // TODO: We are technically reading one time too many here, if the file does not exist on the FS, ReadAllText // fails over to the backup, and then the backup fails to read in the lambda. We should do something about that, // but it's not a big deal. Would be nice if ReadAllText could indicate if it did fail over. - + // 1.) Try without using the backup try { @@ -229,7 +229,7 @@ internal class ReliableFileStorage : IInternalDisposableService { Log.Verbose(ex, "First chance read from {Path} failed, trying backup", path); } - + // 2.) Try using the backup try { @@ -256,13 +256,13 @@ internal class ReliableFileStorage : IInternalDisposableService public byte[] ReadAllBytes(string path, bool forceBackup = false, Guid containerId = default) { ArgumentException.ThrowIfNullOrEmpty(path); - + if (forceBackup) { // If the db failed to load, act as if the file does not exist if (this.db == null) throw new FileNotFoundException("Backup database was not available"); - + var normalizedPath = NormalizePath(path); var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); if (file == null) @@ -274,7 +274,7 @@ internal class ReliableFileStorage : IInternalDisposableService // If the file doesn't exist, immediately check the backup db if (!File.Exists(path)) return this.ReadAllBytes(path, true, containerId); - + try { return File.ReadAllBytes(path); @@ -302,10 +302,10 @@ internal class ReliableFileStorage : IInternalDisposableService // Replace users folder var usersFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); path = path.Replace(usersFolder, "%USERPROFILE%"); - + return path; } - + private void SetupDb(string path) { this.db = new SQLiteConnection(path, @@ -320,9 +320,9 @@ internal class ReliableFileStorage : IInternalDisposableService [PrimaryKey] [AutoIncrement] public int Id { get; set; } - + public Guid ContainerId { get; set; } - + public string Path { get; set; } = null!; public byte[] Data { get; set; } = null!; diff --git a/Dalamud/Utility/FilesystemUtil.cs b/Dalamud/Utility/FilesystemUtil.cs new file mode 100644 index 000000000..3b4298b37 --- /dev/null +++ b/Dalamud/Utility/FilesystemUtil.cs @@ -0,0 +1,116 @@ +using System.ComponentModel; +using System.IO; +using System.Text; + +using Windows.Win32.Storage.FileSystem; + +namespace Dalamud.Utility; + +/// +/// Helper functions for filesystem operations. +/// +public static class FilesystemUtil +{ + /// + /// Overwrite text in a file by first writing it to a temporary file, and then + /// moving that file to the path specified. + /// + /// The path of the file to write to. + /// The text to write. + public static void WriteAllTextSafe(string path, string text) + { + WriteAllTextSafe(path, text, Encoding.UTF8); + } + + /// + /// Overwrite text in a file by first writing it to a temporary file, and then + /// moving that file to the path specified. + /// + /// The path of the file to write to. + /// The text to write. + /// Encoding to use. + public static void WriteAllTextSafe(string path, string text, Encoding encoding) + { + WriteAllBytesSafe(path, encoding.GetBytes(text)); + } + + /// + /// Overwrite data in a file by first writing it to a temporary file, and then + /// moving that file to the path specified. + /// + /// The path of the file to write to. + /// The data to write. + public static unsafe void WriteAllBytesSafe(string path, byte[] bytes) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + // Open the temp file + var tempPath = path + ".tmp"; + + using var tempFile = Windows.Win32.PInvoke.CreateFile( + tempPath, + (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), + FILE_SHARE_MODE.FILE_SHARE_NONE, + null, + FILE_CREATION_DISPOSITION.CREATE_ALWAYS, + FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, + null); + + if (tempFile.IsInvalid) + throw new Win32Exception(); + + // Write the data + uint bytesWritten = 0; + if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan(bytes), &bytesWritten, null)) + throw new Win32Exception(); + + if (bytesWritten != bytes.Length) + throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); + + if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile)) + throw new Win32Exception(); + + tempFile.Close(); + + if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) + throw new Win32Exception(); + } + + /// + /// Generates a temporary file name. + /// + /// A temporary file name that is almost guaranteed to be unique. + internal static string GetTempFileName() + { + // https://stackoverflow.com/a/50413126 + return Path.Combine(Path.GetTempPath(), "dalamud_" + Guid.NewGuid()); + } + + /// + /// Copy files recursively from one directory to another. + /// + /// The source directory. + /// The target directory. + internal static void CopyFilesRecursively(DirectoryInfo source, DirectoryInfo target) + { + foreach (var dir in source.GetDirectories()) + CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name)); + + foreach (var file in source.GetFiles()) + file.CopyTo(Path.Combine(target.FullName, file.Name)); + } + + /// + /// Delete and recreate a directory. + /// + /// The directory to delete and recreate. + internal static void DeleteAndRecreateDirectory(DirectoryInfo dir) + { + if (dir.Exists) + { + dir.Delete(true); + } + + dir.Create(); + } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 87cb86e1c..966fa1e11 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -604,10 +604,9 @@ public static class Util /// /// The path of the file to write to. /// The text to write. - public static void WriteAllTextSafe(string path, string text) - { - WriteAllTextSafe(path, text, Encoding.UTF8); - } + [Api13ToDo("Remove.")] + [Obsolete("Replaced with FilesystemUtil.WriteAllTextSafe()")] + public static void WriteAllTextSafe(string path, string text) => FilesystemUtil.WriteAllTextSafe(path, text); /// /// Overwrite text in a file by first writing it to a temporary file, and then @@ -616,10 +615,9 @@ public static class Util /// The path of the file to write to. /// The text to write. /// Encoding to use. - public static void WriteAllTextSafe(string path, string text, Encoding encoding) - { - WriteAllBytesSafe(path, encoding.GetBytes(text)); - } + [Api13ToDo("Remove.")] + [Obsolete("Replaced with FilesystemUtil.WriteAllTextSafe()")] + public static void WriteAllTextSafe(string path, string text, Encoding encoding) => FilesystemUtil.WriteAllTextSafe(path, text, encoding); /// /// Overwrite data in a file by first writing it to a temporary file, and then @@ -627,41 +625,9 @@ public static class Util /// /// The path of the file to write to. /// The data to write. - public static unsafe void WriteAllBytesSafe(string path, byte[] bytes) - { - ArgumentException.ThrowIfNullOrEmpty(path); - - // Open the temp file - var tempPath = path + ".tmp"; - - using var tempFile = Windows.Win32.PInvoke.CreateFile( - tempPath, - (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), - FILE_SHARE_MODE.FILE_SHARE_NONE, - null, - FILE_CREATION_DISPOSITION.CREATE_ALWAYS, - FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, - null); - - if (tempFile.IsInvalid) - throw new Win32Exception(); - - // Write the data - uint bytesWritten = 0; - if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan(bytes), &bytesWritten, null)) - throw new Win32Exception(); - - if (bytesWritten != bytes.Length) - throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); - - if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile)) - throw new Win32Exception(); - - tempFile.Close(); - - if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) - throw new Win32Exception(); - } + [Api13ToDo("Remove.")] + [Obsolete("Replaced with FilesystemUtil.WriteAllBytesSafe()")] + public static void WriteAllBytesSafe(string path, byte[] bytes) => FilesystemUtil.WriteAllBytesSafe(path, bytes); /// Gets a temporary file name, for use as the sourceFileName in /// . @@ -669,7 +635,7 @@ public static class Util /// A temporary file name that should be usable with . /// /// No write operation is done on the filesystem. - public static string GetTempFileNameForFileReplacement(string targetFile) + public static string GetReplaceableFileName(string targetFile) { Span buf = stackalloc byte[9]; Random.Shared.NextBytes(buf);