using System; using System.IO; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; using MemoryPack; #nullable enable namespace Flow.Launcher.Infrastructure.Storage { /// /// Storage object using binary data /// Normally, it has better performance, but not readable /// /// /// It utilizes MemoryPack, which means the object must be MemoryPackSerializable /// public class BinaryStorage : ISavable { private static readonly string ClassName = "BinaryStorage"; protected T? Data; public const string FileSuffix = ".cache"; protected string FilePath { get; init; } = null!; protected string DirectoryPath { get; init; } = null!; // Let the derived class to set the file path protected BinaryStorage() { } public BinaryStorage(string filename) { DirectoryPath = DataLocation.CacheDirectory; FilesFolders.ValidateDirectory(DirectoryPath); FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } // Let the old Program plugin get this constructor [Obsolete("This constructor is obsolete. Use BinaryStorage(string filename) instead.")] public BinaryStorage(string filename, string directoryPath = null!) { DirectoryPath = directoryPath ?? DataLocation.CacheDirectory; FilesFolders.ValidateDirectory(DirectoryPath); FilePath = Path.Combine(DirectoryPath, $"{filename}{FileSuffix}"); } public T TryLoad(T defaultData) { if (Data != null) return Data; if (File.Exists(FilePath)) { if (new FileInfo(FilePath).Length == 0) { Log.Error(ClassName, $"Zero length cache file <{FilePath}>"); Data = defaultData; Save(); } var bytes = File.ReadAllBytes(FilePath); Data = Deserialize(bytes, defaultData); } else { Log.Info(ClassName, "Cache file not exist, load default data"); Data = defaultData; Save(); } return Data; } private T Deserialize(ReadOnlySpan bytes, T defaultData) { try { var t = MemoryPackSerializer.Deserialize(bytes); return t ?? defaultData; } catch (System.Exception e) { Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e); return defaultData; } } public async ValueTask TryLoadAsync(T defaultData) { if (Data != null) return Data; if (File.Exists(FilePath)) { if (new FileInfo(FilePath).Length == 0) { Log.Error(ClassName, $"Zero length cache file <{FilePath}>"); Data = defaultData; await SaveAsync(); } await using var stream = new FileStream(FilePath, FileMode.Open); Data = await DeserializeAsync(stream, defaultData); } else { Log.Info(ClassName, "Cache file not exist, load default data"); Data = defaultData; await SaveAsync(); } return Data; } private async ValueTask DeserializeAsync(Stream stream, T defaultData) { try { var t = await MemoryPackSerializer.DeserializeAsync(stream); return t ?? defaultData; } catch (System.Exception e) { Log.Exception(ClassName, $"Deserialize error for file <{FilePath}>", e); return defaultData; } } public void Save() { Save(Data.NonNull()); } public void Save(T data) { // User may delete the directory, so we need to check it FilesFolders.ValidateDirectory(DirectoryPath); var serialized = MemoryPackSerializer.Serialize(data); File.WriteAllBytes(FilePath, serialized); } public async ValueTask SaveAsync() { await SaveAsync(Data.NonNull()); } public async ValueTask SaveAsync(T data) { // User may delete the directory, so we need to check it FilesFolders.ValidateDirectory(DirectoryPath); await using var stream = new FileStream(FilePath, FileMode.Create); await MemoryPackSerializer.SerializeAsync(stream, data); } // ImageCache need to convert data into concurrent dictionary for usage, // so we would better to clear the data public void ClearData() { Data = default; } } }