diff --git a/Dalamud/Plugin/Services/IReliableFileStorage.cs b/Dalamud/Plugin/Services/IReliableFileStorage.cs
new file mode 100644
index 000000000..3a3c070f0
--- /dev/null
+++ b/Dalamud/Plugin/Services/IReliableFileStorage.cs
@@ -0,0 +1,163 @@
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+
+using Dalamud.Storage;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// Service to interact with the file system, as a replacement for standard C# file I/O.
+/// Writes and reads using this service are, to the best of our ability, atomic and reliable.
+///
+/// All data is synced to disk immediately and written to a database, additionally to files on disk. This means
+/// that in case of file corruption, data can likely be recovered from the database.
+///
+/// However, this also means that operations using this service duplicate data on disk, so we don't recommend
+/// performing large file operations. The service will not permit files larger than
+/// (64MB) to be written.
+///
+public interface IReliableFileStorage : IDalamudService
+{
+ ///
+ /// Gets the maximum file size, in bytes, that can be written using this service.
+ ///
+ ///
+ /// The service enforces this limit when writing files and fails with an appropriate exception
+ /// (for example or a custom exception) when a caller attempts to write
+ /// more than this number of bytes.
+ ///
+ long MaxFileSizeBytes { get; }
+
+ ///
+ /// Check whether a file exists either on the local filesystem or in the transparent backup database.
+ ///
+ /// The file system path to check. Must not be null or empty.
+ ///
+ /// True if the file exists on disk or a backup copy exists in the storage's internal journal/backup database;
+ /// otherwise false.
+ ///
+ /// Thrown when is null or empty.
+ bool Exists(string path);
+
+ ///
+ /// Write the given text into a file using UTF-8 encoding. The write is performed atomically and is persisted to
+ /// both the filesystem and the internal backup database used by this service.
+ ///
+ /// The file path to write to. Must not be null or empty.
+ /// The string contents to write. May be null, in which case an empty file is written.
+ /// A that completes when the write has finished and been flushed to disk and the backup.
+ /// Thrown when is null or empty.
+ Task WriteAllTextAsync(string path, string? contents);
+
+ ///
+ /// Write the given text into a file using the provided . The write is performed
+ /// atomically (to the extent possible) and is persisted to both the filesystem and the internal backup database
+ /// used by this service.
+ ///
+ /// The file path to write to. Must not be null or empty.
+ /// The string contents to write. May be null, in which case an empty file is written.
+ /// The text encoding to use when serializing the string to bytes. Must not be null.
+ /// A that completes when the write has finished and been flushed to disk and the backup.
+ /// Thrown when is null or empty.
+ /// Thrown when is null.
+ Task WriteAllTextAsync(string path, string? contents, Encoding encoding);
+
+ ///
+ /// Write the given bytes to a file. The write is persisted to both the filesystem and the service's internal
+ /// backup database. Avoid writing extremely large byte arrays because this service duplicates data on disk.
+ ///
+ /// The file path to write to. Must not be null or empty.
+ /// The raw bytes to write. Must not be null.
+ /// A that completes when the write has finished and been flushed to disk and the backup.
+ /// Thrown when is null or empty.
+ /// Thrown when is null.
+ Task WriteAllBytesAsync(string path, byte[] bytes);
+
+ ///
+ /// Read all text from a file using UTF-8 encoding. If the file is unreadable or missing on disk, the service
+ /// attempts to return a backed-up copy from its internal journal/backup database.
+ ///
+ /// The file path to read. Must not be null or empty.
+ ///
+ /// When true the service prefers the internal backup database and returns backed-up contents if available. When
+ /// false the service tries the filesystem first and falls back to the backup only on error or when the file is missing.
+ ///
+ /// The textual contents of the file, decoded using UTF-8.
+ /// Thrown when is null or empty.
+ /// Thrown when the file does not exist on disk and no backup copy is available.
+ Task ReadAllTextAsync(string path, bool forceBackup = false);
+
+ ///
+ /// Read all text from a file using the specified . If the file is unreadable or
+ /// missing on disk, the service attempts to return a backed-up copy from its internal journal/backup database.
+ ///
+ /// The file path to read. Must not be null or empty.
+ /// The encoding to use when decoding the stored bytes into text. Must not be null.
+ ///
+ /// When true the service prefers the internal backup database and returns backed-up contents if available. When
+ /// false the service tries the filesystem first and falls back to the backup only on error or when the file is missing.
+ ///
+ /// The textual contents of the file decoded using the provided .
+ /// Thrown when is null or empty.
+ /// Thrown when is null.
+ /// Thrown when the file does not exist on disk and no backup copy is available.
+ Task ReadAllTextAsync(string path, Encoding encoding, bool forceBackup = false);
+
+ ///
+ /// Read all text from a file and invoke the provided callback with the string
+ /// contents. If the reader throws or the initial read fails, the service attempts a backup read and invokes the
+ /// reader again with the backup contents. If both reads fail the service surfaces an exception to the caller.
+ ///
+ /// The file path to read. Must not be null or empty.
+ ///
+ /// A callback invoked with the file's textual contents. Must not be null.
+ /// If the callback throws an exception the service treats that as a signal to retry the read using the
+ /// internal backup database and will invoke the callback again with the backup contents when available.
+ /// For example, the callback can throw when JSON deserialization fails to request the backup copy instead of
+ /// silently accepting corrupt data.
+ ///
+ /// A that completes when the read (and any attempted fallback) and callback invocation have finished.
+ /// Thrown when is null or empty.
+ /// Thrown when is null.
+ /// Thrown when the file does not exist on disk and no backup copy is available.
+ /// Thrown when both the filesystem read and the backup read fail for other reasons.
+ Task ReadAllTextAsync(string path, Action reader);
+
+ ///
+ /// Read all text from a file using the specified and invoke the provided
+ /// callback with the decoded string contents. If the reader throws or the initial
+ /// read fails, the service attempts a backup read and invokes the reader again with the backup contents. If
+ /// both reads fail the service surfaces an exception to the caller.
+ ///
+ /// The file path to read. Must not be null or empty.
+ /// The encoding to use when decoding the stored bytes into text. Must not be null.
+ ///
+ /// A callback invoked with the file's textual contents. Must not be null.
+ /// If the callback throws an exception the service treats that as a signal to retry the read using the
+ /// internal backup database and will invoke the callback again with the backup contents when available.
+ /// For example, the callback can throw when JSON deserialization fails to request the backup copy instead of
+ /// silently accepting corrupt data.
+ ///
+ /// A that completes when the read (and any attempted fallback) and callback invocation have finished.
+ /// Thrown when is null or empty.
+ /// Thrown when or is null.
+ /// Thrown when the file does not exist on disk and no backup copy is available.
+ /// Thrown when both the filesystem read and the backup read fail for other reasons.
+ Task ReadAllTextAsync(string path, Encoding encoding, Action reader);
+
+ ///
+ /// Read all bytes from a file. If the file is unreadable or missing on disk, the service may try to return a
+ /// backed-up copy from its internal journal/backup database.
+ ///
+ /// The file path to read. Must not be null or empty.
+ ///
+ /// When true the service prefers the internal backup database and returns the backed-up contents
+ /// if available. When false the service tries the filesystem first and falls back to the backup only
+ /// on error or when the file is missing.
+ ///
+ /// The raw bytes stored in the file.
+ /// Thrown when is null or empty.
+ /// Thrown when the file does not exist on disk and no backup copy is available.
+ Task ReadAllBytesAsync(string path, bool forceBackup = false);
+}
diff --git a/Dalamud/Storage/ReliableFileStoragePluginScoped.cs b/Dalamud/Storage/ReliableFileStoragePluginScoped.cs
new file mode 100644
index 000000000..1f1992da6
--- /dev/null
+++ b/Dalamud/Storage/ReliableFileStoragePluginScoped.cs
@@ -0,0 +1,120 @@
+using System.Threading.Tasks;
+using System.Text;
+
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Plugin.Internal.Types;
+using Dalamud.Plugin.Services;
+
+namespace Dalamud.Storage;
+
+[PluginInterface]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+public class ReliableFileStoragePluginScoped : IReliableFileStorage, IServiceType
+{
+ // TODO: Make sure pending writes are finalized on plugin unload?
+
+ private readonly LocalPlugin plugin;
+
+ [ServiceManager.ServiceDependency]
+ private readonly ReliableFileStorage storage = Service.Get();
+
+ [ServiceManager.ServiceConstructor]
+ internal ReliableFileStoragePluginScoped(LocalPlugin plugin)
+ {
+ this.plugin = plugin;
+ }
+
+ ///
+ public long MaxFileSizeBytes => 64 * 1024 * 1024;
+
+ ///
+ public bool Exists(string path)
+ {
+ return this.storage.Exists(path, this.plugin.EffectiveWorkingPluginId);
+ }
+
+ ///
+ public Task WriteAllTextAsync(string path, string? contents)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ var bytes = Encoding.UTF8.GetBytes(contents ?? string.Empty);
+ if (bytes.LongLength > this.MaxFileSizeBytes)
+ throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(contents));
+
+ return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId);
+ }
+
+ ///
+ public Task WriteAllTextAsync(string path, string? contents, Encoding encoding)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+ ArgumentNullException.ThrowIfNull(encoding);
+
+ var bytes = encoding.GetBytes(contents ?? string.Empty);
+ if (bytes.LongLength > this.MaxFileSizeBytes)
+ throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(contents));
+
+ return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId);
+ }
+
+ ///
+ public Task WriteAllBytesAsync(string path, byte[] bytes)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+ ArgumentNullException.ThrowIfNull(bytes);
+
+ if (bytes.LongLength > this.MaxFileSizeBytes)
+ throw new ArgumentException($"The provided data exceeds the maximum allowed size of {this.MaxFileSizeBytes} bytes.", nameof(bytes));
+
+ return this.storage.WriteAllBytesAsync(path, bytes, this.plugin.EffectiveWorkingPluginId);
+ }
+
+ ///
+ public Task ReadAllTextAsync(string path, bool forceBackup = false)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ return this.storage.ReadAllTextAsync(path, forceBackup, this.plugin.EffectiveWorkingPluginId);
+ }
+
+ ///
+ public Task ReadAllTextAsync(string path, Encoding encoding, bool forceBackup = false)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+ ArgumentNullException.ThrowIfNull(encoding);
+
+ return this.storage.ReadAllTextAsync(path, encoding, forceBackup, this.plugin.EffectiveWorkingPluginId);
+ }
+
+ ///
+ public Task ReadAllTextAsync(string path, Action reader)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+ ArgumentNullException.ThrowIfNull(reader);
+
+ return this.storage.ReadAllTextAsync(path, reader, this.plugin.EffectiveWorkingPluginId);
+ }
+
+ ///
+ public Task ReadAllTextAsync(string path, Encoding encoding, Action reader)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+ ArgumentNullException.ThrowIfNull(encoding);
+ ArgumentNullException.ThrowIfNull(reader);
+
+ return this.storage.ReadAllTextAsync(path, encoding, reader, this.plugin.EffectiveWorkingPluginId);
+ }
+
+ ///
+ public Task ReadAllBytesAsync(string path, bool forceBackup = false)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ return this.storage.ReadAllBytesAsync(path, forceBackup, this.plugin.EffectiveWorkingPluginId);
+ }
+}