Files
Nebula-Auth/src/NebulaAuth/Model/Mafiles/Storage.cs
T
achiez f15632f2d0 Refactored MAAC error/status handling and UI feedback
- Introduced centralized MAACRequestHandler for error tracking, retries, and status escalation (Ok/Warning/Error) per account
- PortableMaClient now uses new Status property (PortableMaClientStatus) instead of IsError; error history tracked in PortableMaClientErrorData
- All MAAC requests now routed through MAACRequestHandler, providing unified error handling and retry logic
- MultiAccountAutoConfirmer now filters accounts by Status.StatusType == Ok
- Updated MainWindow UI: account name color reflects status (Success/Warning/Error); added "Unattach proxy" to context menu
- Removed obsolete PortableMaClientStatusToColorConverter and related bindings
- Settings: removed IgnorePatchTuesdayErrors, added MaacErrorThreshold and MaacRetryInterval for error escalation control
- Refactored SessionHandler: split into logic, API, and helpers; improved exception propagation and session management
- AdmissionHelper methods now handle null sessions and always transfer community cookies if session is missing
- Mafile export: always preserves SteamId, even if session data is not exported
- ViewModel: RemoveProxy command now parameterized and only enabled if Mafile has a proxy
- Enabled concurrent log writing in NLog config
- Minor code cleanups and improved exception signatures
2026-01-25 18:48:11 +02:00

287 lines
10 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AchiesUtilities.Extensions;
using NebulaAuth.Model.Entities;
using NebulaAuth.Model.Exceptions;
using NebulaAuth.Model.MAAC;
using SteamLib;
using SteamLib.Exceptions.Authorization;
using SteamLib.SteamMobile;
namespace NebulaAuth.Model.Mafiles;
public static class Storage
{
public const string DIR_MAFILES = "maFiles";
public const string DIR_REMOVED_MAFILES = "maFiles_removed";
public const string DIR_BACKUP_MAFILES = "maFiles_backup";
public static readonly string[] MafilesDirectories;
public static string MafilesDirectory { get; } = Path.GetFullPath(DIR_MAFILES);
public static string RemovedMafilesDirectory { get; } = Path.GetFullPath(DIR_REMOVED_MAFILES);
public static string BackupMafilesDirectory { get; } = Path.GetFullPath(DIR_BACKUP_MAFILES);
public static ObservableCollection<Mafile> MaFiles { get; private set; } = [];
static Storage()
{
MafilesDirectories = [MafilesDirectory, RemovedMafilesDirectory, BackupMafilesDirectory];
}
public static async Task Initialize(int threadCount, CancellationToken token = default)
{
Directory.CreateDirectory(MafilesDirectory);
Directory.CreateDirectory(RemovedMafilesDirectory);
Directory.CreateDirectory(BackupMafilesDirectory);
var files = Directory
.GetFiles(MafilesDirectory)
.Where(file => Path.GetExtension(file).EqualsIgnoreCase(".mafile"))
.ToList();
var localList = new ConcurrentBag<Mafile>();
await Task.Run(() =>
{
return Parallel.ForEachAsync(files,
new ParallelOptions {CancellationToken = token, MaxDegreeOfParallelism = threadCount},
async (file, ct) =>
{
try
{
var data = await ReadMafileAsync(file);
localList.Add(data);
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Can't load mafile {file}", Path.GetFileName(file));
}
});
}, token);
MaFiles = new ObservableCollection<Mafile>(localList.OrderBy(m => m.AccountName));
}
/// <summary>
/// </summary>
/// <param name="path"></param>
/// <param name="overwrite"></param>
/// <exception cref="FormatException"></exception>
/// <exception cref="SessionInvalidException"></exception>
/// <exception cref="IOException"></exception>
public static async Task AddNewMafile(string path, bool overwrite)
{
Mafile data;
try
{
data = await ReadMafileAsync(path);
data.Filename = null;
}
catch (Exception ex)
when (ex is not MafileNeedReloginException)
{
Shell.Logger.Warn(ex, "Can't load mafile");
throw new FormatException("File data is not valid", ex);
}
if (string.IsNullOrWhiteSpace(data.AccountName))
throw new FormatException("File data is not valid. Missing AccountName");
try
{
_ = SteamGuardCodeGenerator.GenerateCode(data.SharedSecret);
}
catch (Exception ex)
{
throw new FormatException("Can't generate code on this mafile", ex);
}
if (!overwrite && File.Exists(MafilesStorageHelper.GetOrUpdateMafilePath(data)))
{
throw new IOException("File already exist and overwrite is False"); //TODO: Custom Exception
}
await SaveMafileAsync(data);
}
public static async Task<Mafile> ReadMafileAsync(string path)
{
var str = await File.ReadAllTextAsync(path);
return NebulaSerializer.Deserialize(str, path);
}
public static async Task SaveMafileAsync(Mafile data)
{
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
// Replace if exist
for (var i = 0; i < MaFiles.Count; i++)
{
var existed = MaFiles[i];
if (ReferenceEquals(existed, data) || (existed.Filename != null && existed.Filename == data.Filename))
{
MaFiles[i] = data;
return;
}
}
MaFiles.Add(data);
}
public static void UpdateMafile(Mafile data)
{
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
File.WriteAllText(Path.GetFullPath(path), str);
}
public static async Task UpdateMafileAsync(Mafile data)
{
var path = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var str = NebulaSerializer.SerializeMafile(data);
await File.WriteAllTextAsync(Path.GetFullPath(path), str);
}
public static void MoveToRemoved(Mafile data)
{
var sourcePath = MafilesStorageHelper.GetOrUpdateMafilePath(data);
var fileName = Path.GetFileNameWithoutExtension(data.Filename!);
var destinationPathBase = Path.Combine(DIR_REMOVED_MAFILES, fileName);
var destinationPathFinal = Path.ChangeExtension(destinationPathBase, MafileNamingStrategy.DEF_EXTENSION);
var i = 0;
while (File.Exists(destinationPathFinal))
{
i++;
destinationPathFinal = destinationPathBase + $" ({i})";
destinationPathFinal = Path.ChangeExtension(destinationPathFinal, MafileNamingStrategy.DEF_EXTENSION);
}
File.Copy(sourcePath, destinationPathFinal, false);
File.Delete(sourcePath);
MaFiles.Remove(data);
}
public static string? TryGetMafilePath(Mafile data)
{
if (data.Filename == null) return null;
return Path.Combine(MafilesDirectory, data.Filename);
}
public static void WriteBackup(MobileDataExtended data)
{
var json = NebulaSerializer.SerializeMafile(data, null);
WriteBackup(data.AccountName, json);
}
public static void WriteBackup(string accountName, string data)
{
Directory.CreateDirectory(DIR_BACKUP_MAFILES);
File.WriteAllText(Path.Combine(DIR_BACKUP_MAFILES, accountName + MafileNamingStrategy.DEF_EXTENSION), data);
}
public static async Task<MafilesBulkRenameResult> RenameMafiles(bool loginAsFileName,
IProgress<double>? progress = null)
{
if (MaFiles.Count == 0) return new MafilesBulkRenameResult();
var now = DateTime.Now;
var backupFileName = $"rename_backup_{now:yyyy-MM-dd_HH-mm-ss}.zip";
var zipPath = Path.Combine(BackupMafilesDirectory, backupFileName);
await using (var zipStream = new FileStream(zipPath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Create, false);
var files = Directory
.EnumerateFiles(MafilesDirectory, "*.*", SearchOption.TopDirectoryOnly)
.Where(f => Path.GetExtension(f).Equals(".mafile", StringComparison.OrdinalIgnoreCase))
.ToList();
var counter = 0;
foreach (var file in files)
{
counter++;
var entry = archive.CreateEntry(Path.GetFileName(file), CompressionLevel.Optimal);
await using var entryStream = entry.Open();
await using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read, 4096,
true);
await fileStream.CopyToAsync(entryStream);
if (counter % 5 == 0)
{
progress?.Report(counter / (double) files.Count);
await Task.Delay(10);
}
}
}
var mafiles = MaFiles.ToList();
var res = new MafilesBulkRenameResult
{
Total = mafiles.Count,
BackupFileName = backupFileName
};
var dic = new Dictionary<string, string>();
foreach (var mafile in mafiles)
{
try
{
var targetFileName = MafilesStorageHelper.CreateMafileFileName(mafile, loginAsFileName);
if (mafile.Filename == targetFileName || mafile.Filename == null)
{
res.Renamed += 1;
continue;
}
var sourceFileName = mafile.Filename;
var fullSourcePath = Path.Combine(MafilesDirectory, sourceFileName);
var fullTargetPath = Path.Combine(MafilesDirectory, targetFileName);
if (!File.Exists(fullSourcePath))
{
Shell.Logger.Warn("Can't rename mafile {old} to {new} because source file not found",
mafile.Filename, targetFileName);
IncErrors();
continue;
}
if (File.Exists(fullTargetPath))
{
Shell.Logger.Warn("Can't rename mafile {old} to {new} because target file already exist",
mafile.Filename, targetFileName);
res.Conflict += 1;
continue;
}
File.Move(fullSourcePath, fullTargetPath);
dic[sourceFileName] = targetFileName;
res.Renamed += 1;
mafile.Filename = targetFileName;
}
catch (Exception ex)
{
Shell.Logger.Error(ex, "Error renaming mafile {file} {accountName}", mafile.Filename,
mafile.AccountName);
IncErrors();
}
}
MAACStorage.NotifyMafilesRenamed(dic);
return res;
void IncErrors()
{
res.Errors += 1;
}
}
}