2025-04-05 03:19:01 +00:00
|
|
|
|
using System;
|
2017-02-12 20:03:32 +00:00
|
|
|
|
using System.Globalization;
|
2017-02-07 00:21:39 +00:00
|
|
|
|
using System.IO;
|
2020-12-30 05:40:42 +00:00
|
|
|
|
using System.Text.Json;
|
2023-03-26 07:24:31 +00:00
|
|
|
|
using System.Threading.Tasks;
|
2020-04-21 09:12:17 +00:00
|
|
|
|
using Flow.Launcher.Infrastructure.Logger;
|
2025-04-05 03:19:01 +00:00
|
|
|
|
using Flow.Launcher.Plugin;
|
2025-04-02 12:17:32 +00:00
|
|
|
|
using Flow.Launcher.Plugin.SharedCommands;
|
2014-12-15 14:58:49 +00:00
|
|
|
|
|
2025-04-05 03:19:01 +00:00
|
|
|
|
#nullable enable
|
|
|
|
|
|
|
2020-04-21 09:12:17 +00:00
|
|
|
|
namespace Flow.Launcher.Infrastructure.Storage
|
2014-12-15 14:58:49 +00:00
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Serialize object using json format.
|
|
|
|
|
|
/// </summary>
|
2025-04-05 03:19:01 +00:00
|
|
|
|
public class JsonStorage<T> : ISavable where T : new()
|
2014-12-15 14:58:49 +00:00
|
|
|
|
{
|
2025-04-13 09:11:36 +00:00
|
|
|
|
private static readonly string ClassName = "JsonStorage";
|
|
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
protected T? Data;
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2017-02-07 00:21:39 +00:00
|
|
|
|
// need a new directory name
|
2025-02-23 13:06:21 +00:00
|
|
|
|
public const string DirectoryName = Constant.Settings;
|
2017-02-07 00:21:39 +00:00
|
|
|
|
public const string FileSuffix = ".json";
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
protected string FilePath { get; init; } = null!;
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
private string TempFilePath => $"{FilePath}.tmp";
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
private string BackupFilePath => $"{FilePath}.bak";
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
protected string DirectoryPath { get; init; } = null!;
|
2017-02-07 00:21:39 +00:00
|
|
|
|
|
2023-03-26 07:24:31 +00:00
|
|
|
|
// Let the derived class to set the file path
|
|
|
|
|
|
protected JsonStorage()
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
2025-01-09 18:48:43 +00:00
|
|
|
|
|
2023-03-26 07:24:31 +00:00
|
|
|
|
public JsonStorage(string filePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
FilePath = filePath;
|
2023-03-26 19:04:06 +00:00
|
|
|
|
DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path");
|
2025-01-09 18:48:43 +00:00
|
|
|
|
|
2025-04-02 12:17:32 +00:00
|
|
|
|
FilesFolders.ValidateDirectory(DirectoryPath);
|
2023-03-26 07:24:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-01 03:41:53 +00:00
|
|
|
|
public bool Exists()
|
|
|
|
|
|
{
|
|
|
|
|
|
return File.Exists(FilePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-01 04:49:20 +00:00
|
|
|
|
public void Delete()
|
|
|
|
|
|
{
|
2025-05-01 05:33:24 +00:00
|
|
|
|
foreach (var path in new[] { FilePath, BackupFilePath, TempFilePath })
|
2025-05-01 04:49:20 +00:00
|
|
|
|
{
|
2025-05-01 05:33:24 +00:00
|
|
|
|
if (File.Exists(path))
|
|
|
|
|
|
{
|
|
|
|
|
|
File.Delete(path);
|
|
|
|
|
|
}
|
2025-05-01 04:49:20 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2023-03-26 07:24:31 +00:00
|
|
|
|
public async Task<T> LoadAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Data != null)
|
|
|
|
|
|
return Data;
|
|
|
|
|
|
|
|
|
|
|
|
string? serialized = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (File.Exists(FilePath))
|
|
|
|
|
|
{
|
|
|
|
|
|
serialized = await File.ReadAllTextAsync(FilePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(serialized))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Data = JsonSerializer.Deserialize<T>(serialized) ?? await LoadBackupOrDefaultAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (JsonException)
|
|
|
|
|
|
{
|
|
|
|
|
|
Data = await LoadBackupOrDefaultAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Data = await LoadBackupOrDefaultAsync();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Data.NonNull();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async ValueTask<T> LoadBackupOrDefaultAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
var backup = await TryLoadBackupAsync();
|
|
|
|
|
|
|
|
|
|
|
|
return backup ?? LoadDefault();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private async ValueTask<T?> TryLoadBackupAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!File.Exists(BackupFilePath))
|
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
await using var source = File.OpenRead(BackupFilePath);
|
|
|
|
|
|
var data = await JsonSerializer.DeserializeAsync<T>(source) ?? default;
|
|
|
|
|
|
|
|
|
|
|
|
if (data != null)
|
|
|
|
|
|
RestoreBackup();
|
|
|
|
|
|
|
|
|
|
|
|
return data;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (JsonException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return default;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-01-09 18:48:43 +00:00
|
|
|
|
|
2023-03-26 07:24:31 +00:00
|
|
|
|
private void RestoreBackup()
|
|
|
|
|
|
{
|
2025-04-13 09:11:36 +00:00
|
|
|
|
Log.Info(ClassName, $"Failed to load settings.json, {BackupFilePath} restored successfully");
|
2023-03-26 07:24:31 +00:00
|
|
|
|
|
|
|
|
|
|
if (File.Exists(FilePath))
|
|
|
|
|
|
File.Replace(BackupFilePath, FilePath, null);
|
|
|
|
|
|
else
|
|
|
|
|
|
File.Move(BackupFilePath, FilePath);
|
|
|
|
|
|
}
|
2016-04-21 00:53:21 +00:00
|
|
|
|
|
2017-02-06 22:04:52 +00:00
|
|
|
|
public T Load()
|
2014-12-15 14:58:49 +00:00
|
|
|
|
{
|
2023-01-07 19:11:43 +00:00
|
|
|
|
string? serialized = null;
|
|
|
|
|
|
|
2016-04-21 00:53:21 +00:00
|
|
|
|
if (File.Exists(FilePath))
|
2014-12-15 14:58:49 +00:00
|
|
|
|
{
|
2023-01-07 19:11:43 +00:00
|
|
|
|
serialized = File.ReadAllText(FilePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(serialized))
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
2014-12-15 14:58:49 +00:00
|
|
|
|
{
|
2023-01-25 02:42:53 +00:00
|
|
|
|
Data = JsonSerializer.Deserialize<T>(serialized) ?? TryLoadBackup() ?? LoadDefault();
|
2014-12-15 14:58:49 +00:00
|
|
|
|
}
|
2023-01-07 19:11:43 +00:00
|
|
|
|
catch (JsonException)
|
2014-12-15 14:58:49 +00:00
|
|
|
|
{
|
2023-01-07 19:11:43 +00:00
|
|
|
|
Data = TryLoadBackup() ?? LoadDefault();
|
2014-12-15 14:58:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2023-01-07 19:11:43 +00:00
|
|
|
|
Data = TryLoadBackup() ?? LoadDefault();
|
2014-12-15 14:58:49 +00:00
|
|
|
|
}
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
return Data.NonNull();
|
2014-12-15 14:58:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
private T LoadDefault()
|
2014-12-15 14:58:49 +00:00
|
|
|
|
{
|
2023-01-07 19:11:43 +00:00
|
|
|
|
if (File.Exists(FilePath))
|
2016-04-21 00:53:21 +00:00
|
|
|
|
{
|
2023-01-07 19:11:43 +00:00
|
|
|
|
BackupOriginFile();
|
2016-04-21 00:53:21 +00:00
|
|
|
|
}
|
2017-02-13 09:38:48 +00:00
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
return new T();
|
2021-06-21 02:34:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
private T? TryLoadBackup()
|
2021-06-21 02:34:07 +00:00
|
|
|
|
{
|
2023-01-07 19:11:43 +00:00
|
|
|
|
if (!File.Exists(BackupFilePath))
|
|
|
|
|
|
return default;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
2021-06-21 02:34:07 +00:00
|
|
|
|
{
|
2023-01-07 19:11:43 +00:00
|
|
|
|
var data = JsonSerializer.Deserialize<T>(File.ReadAllText(BackupFilePath));
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
if (data != null)
|
2023-03-26 07:24:31 +00:00
|
|
|
|
RestoreBackup();
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2023-03-26 07:24:31 +00:00
|
|
|
|
return data;
|
2023-01-07 19:11:43 +00:00
|
|
|
|
}
|
|
|
|
|
|
catch (JsonException)
|
|
|
|
|
|
{
|
|
|
|
|
|
return default;
|
2021-06-21 02:34:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void BackupOriginFile()
|
|
|
|
|
|
{
|
|
|
|
|
|
var timestamp = DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss-fffffff", CultureInfo.CurrentUICulture);
|
|
|
|
|
|
var directory = Path.GetDirectoryName(FilePath).NonNull();
|
|
|
|
|
|
var originName = Path.GetFileNameWithoutExtension(FilePath);
|
|
|
|
|
|
var backupName = $"{originName}-{timestamp}{FileSuffix}";
|
|
|
|
|
|
var backupPath = Path.Combine(directory, backupName);
|
|
|
|
|
|
File.Copy(FilePath, backupPath, true);
|
|
|
|
|
|
// todo give user notification for the backup process
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void Save()
|
|
|
|
|
|
{
|
2025-04-11 08:46:45 +00:00
|
|
|
|
// User may delete the directory, so we need to check it
|
|
|
|
|
|
FilesFolders.ValidateDirectory(DirectoryPath);
|
|
|
|
|
|
|
|
|
|
|
|
var serialized = JsonSerializer.Serialize(Data,
|
2025-01-09 18:48:43 +00:00
|
|
|
|
new JsonSerializerOptions { WriteIndented = true });
|
2021-06-21 02:34:07 +00:00
|
|
|
|
|
2023-01-07 19:11:43 +00:00
|
|
|
|
File.WriteAllText(TempFilePath, serialized);
|
2023-01-25 02:42:53 +00:00
|
|
|
|
|
2023-03-26 07:24:31 +00:00
|
|
|
|
AtomicWriteSetting();
|
|
|
|
|
|
}
|
2025-01-09 18:48:43 +00:00
|
|
|
|
|
2023-03-26 07:24:31 +00:00
|
|
|
|
public async Task SaveAsync()
|
|
|
|
|
|
{
|
2025-04-11 08:46:45 +00:00
|
|
|
|
// User may delete the directory, so we need to check it
|
|
|
|
|
|
FilesFolders.ValidateDirectory(DirectoryPath);
|
|
|
|
|
|
|
2025-01-10 19:11:35 +00:00
|
|
|
|
await using var tempOutput = File.OpenWrite(TempFilePath);
|
2023-03-26 07:24:31 +00:00
|
|
|
|
await JsonSerializer.SerializeAsync(tempOutput, Data,
|
2025-01-09 18:48:43 +00:00
|
|
|
|
new JsonSerializerOptions { WriteIndented = true });
|
2023-03-26 07:24:31 +00:00
|
|
|
|
AtomicWriteSetting();
|
|
|
|
|
|
}
|
2025-01-09 18:48:43 +00:00
|
|
|
|
|
2023-03-26 07:24:31 +00:00
|
|
|
|
private void AtomicWriteSetting()
|
|
|
|
|
|
{
|
2023-01-25 02:42:53 +00:00
|
|
|
|
if (!File.Exists(FilePath))
|
|
|
|
|
|
{
|
|
|
|
|
|
File.Move(TempFilePath, FilePath);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-01-09 18:48:43 +00:00
|
|
|
|
var finalFilePath = new FileInfo(FilePath).LinkTarget ?? FilePath;
|
|
|
|
|
|
File.Replace(TempFilePath, finalFilePath, BackupFilePath);
|
2023-01-25 02:42:53 +00:00
|
|
|
|
}
|
2021-06-21 02:34:07 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2014-12-15 14:58:49 +00:00
|
|
|
|
}
|