using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Reflection; using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.LuaCs.Data; using Barotrauma.Steam; using FluentResults; using FluentResults.LuaCs; using Microsoft.Toolkit.Diagnostics; using Error = FluentResults.Error; using Path = Barotrauma.IO.Path; namespace Barotrauma.LuaCs.Services; public class StorageService : IStorageService { public StorageService(IStorageServiceConfig configData) { _configData = configData; } private readonly ConcurrentDictionary> _fsCache = new(); protected readonly IStorageServiceConfig _configData; public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed); private int _isDisposed = 0; public void Dispose() { ModUtils.Threading.SetBool(ref _isDisposed, true); } public void PurgeCache() { ((IService)this).CheckDisposed(); _fsCache.Clear(); } public void PurgeFileFromCache(string absolutePath) { ((IService)this).CheckDisposed(); if (absolutePath.IsNullOrWhiteSpace()) return; try { //sanitation pass absolutePath = System.IO.Path.GetFullPath(absolutePath).CleanUpPath(); _fsCache.Remove(absolutePath, out _); } catch { // ignored return; } } public void PurgeFilesFromCache(params string[] absolutePaths) { ((IService)this).CheckDisposed(); if (absolutePaths.Length < 1) return; foreach (var path in absolutePaths) { try { if (path.IsNullOrWhiteSpace()) continue; //sanitation pass var path2 = System.IO.Path.GetFullPath(path).CleanUpPath(); _fsCache.Remove(path2, out _); } catch { // ignored continue; } } } private int _useCaching; public bool UseCaching { get => ModUtils.Threading.GetBool(ref _useCaching); set => ModUtils.Threading.SetBool(ref _useCaching, value); } public virtual FluentResults.Result LoadLocalXml(ContentPackage package, string localFilePath) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadXml(r.Value) : r.ToResult(); public virtual FluentResults.Result LoadLocalBinary(ContentPackage package, string localFilePath) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadBinary(r.Value) : r.ToResult(); public virtual FluentResults.Result LoadLocalText(ContentPackage package, string localFilePath) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadText(r.Value) : r.ToResult(); public virtual FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TrySaveXml(r.Value, document) : r.ToResult(); public virtual FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TrySaveBinary(r.Value, bytes) : r.ToResult(); public virtual FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TrySaveText(r.Value, text) : r.ToResult(); public virtual async Task> LoadLocalXmlAsync(ContentPackage package, string localFilePath) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadXmlAsync(r.Value) : r.ToResult(); public virtual async Task> LoadLocalBinaryAsync(ContentPackage package, string localFilePath) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadBinaryAsync(r.Value) : r.ToResult(); public virtual async Task> LoadLocalTextAsync(ContentPackage package, string localFilePath) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadTextAsync(r.Value) : r.ToResult(); public virtual async Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TrySaveXmlAsync(r.Value, document) : r.ToResult(); public virtual async Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TrySaveBinaryAsync(r.Value, bytes) : r.ToResult(); public virtual async Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text) => GetAbsoluePathFromLocal(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TrySaveTextAsync(r.Value, text) : r.ToResult(); public virtual FluentResults.Result LoadPackageXml(ContentPackage package, string localFilePath) => GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadXml(r.Value) : r.ToResult(); public virtual FluentResults.Result LoadPackageBinary(ContentPackage package, string localFilePath) => GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadBinary(r.Value) : r.ToResult(); public virtual FluentResults.Result LoadPackageText(ContentPackage package, string localFilePath) => GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? TryLoadText(r.Value) : r.ToResult(); public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageXmlFiles(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); foreach (var path in localFilePaths) builder.Add((path, LoadPackageXml(package, path))); return builder.MoveToImmutable(); } public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageBinaryFiles(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); foreach (var path in localFilePaths) builder.Add((path, LoadPackageBinary(package, path))); return builder.MoveToImmutable(); } public virtual ImmutableArray<(string, FluentResults.Result)> LoadPackageTextFiles(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); foreach (var path in localFilePaths) builder.Add((path, LoadPackageText(package, path))); return builder.MoveToImmutable(); } public virtual FluentResults.Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively) { ((IService)this).CheckDisposed(); var r = GetAbsoluePathFromPackage(package, localSubfolder); if (r is { IsFailed: true }) return r.ToResult(); var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result>)>(); var sOption = searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; string[] arr = Directory.GetFiles(localSubfolder, regexFilter.IsNullOrWhiteSpace() ? "*.*" : regexFilter, sOption); return new FluentResults.Result>().WithSuccess($"Files found.") .WithValue(arr.ToImmutableArray()); } public virtual async Task> LoadPackageXmlAsync(ContentPackage package, string localFilePath) => GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadXmlAsync(r.Value) : r.ToResult(); public virtual async Task> LoadPackageBinaryAsync(ContentPackage package, string localFilePath) => GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadBinaryAsync(r.Value) : r.ToResult(); public virtual async Task> LoadPackageTextAsync(ContentPackage package, string localFilePath) => GetAbsoluePathFromPackage(package, localFilePath) is var r && r is { IsSuccess: true, Value: not null } ? await TryLoadTextAsync(r.Value) : r.ToResult(); public virtual async Task)>> LoadPackageXmlFilesAsync(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); await localFilePaths.ParallelForEachAsync(async path => { builder.Add((path, await LoadPackageXmlAsync(package, path))); }, maxDegreeOfParallelism: 2); return builder.MoveToImmutable(); } public virtual async Task)>> LoadPackageBinaryFilesAsync(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); await localFilePaths.ParallelForEachAsync(async path => { builder.Add((path, await LoadPackageBinaryAsync(package, path))); }, maxDegreeOfParallelism: 2); return builder.MoveToImmutable(); } public virtual async Task)>> LoadPackageTextFilesAsync(ContentPackage package, ImmutableArray localFilePaths) { ((IService)this).CheckDisposed(); if (localFilePaths.IsDefaultOrEmpty) return ImmutableArray<(string, FluentResults.Result)>.Empty; var builder = ImmutableArray.CreateBuilder<(string, FluentResults.Result)>(localFilePaths.Length); await localFilePaths.ParallelForEachAsync(async path => { builder.Add((path, await LoadPackageTextAsync(package, path))); }, maxDegreeOfParallelism: 2); return builder.MoveToImmutable(); } public virtual FluentResults.Result TryLoadXml(string filePath, Encoding encoding = null) { ((IService)this).CheckDisposed(); var r = TryLoadText(filePath, encoding); if (r is { IsSuccess: true, Value: not null }) return XDocument.Parse(r.Value); else { return r.ToResult(s => null) .WithError(GetGeneralError(nameof(LoadLocalXml), filePath)); } } public virtual FluentResults.Result TryLoadText(string filePath, Encoding encoding = null) { ((IService)this).CheckDisposed(); if (UseCaching && _fsCache.TryGetValue(filePath, out var result) && result.TryPickT1(out var cachedVal, out _)) { return FluentResults.Result.Ok(cachedVal); } return IOExceptionsOperationRunner(nameof(TryLoadText), filePath, () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); var fileText = encoding is null ? System.IO.File.ReadAllText(fp) : System.IO.File.ReadAllText(fp, encoding); if (UseCaching) _fsCache[filePath] = fileText; return new FluentResults.Result().WithSuccess($"Loaded file successfully").WithValue(fileText); }); } public virtual FluentResults.Result TryLoadBinary(string filePath) { ((IService)this).CheckDisposed(); if (UseCaching && _fsCache.TryGetValue(filePath, out var result) && result.TryPickT0(out var cachedVal, out _)) { return FluentResults.Result.Ok(cachedVal); } return IOExceptionsOperationRunner(nameof(TryLoadBinary), filePath, () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); var fileData = System.IO.File.ReadAllBytes(fp); if (UseCaching) _fsCache[filePath] = fileData; return new FluentResults.Result().WithSuccess($"Loaded file successfully").WithValue(fileData); }); } public virtual FluentResults.Result TrySaveXml(string filePath, in XDocument document, Encoding encoding = null) => TrySaveText(filePath, document.ToString(), encoding); public virtual FluentResults.Result TrySaveText(string filePath, in string text, Encoding encoding = null) { ((IService)this).CheckDisposed(); if (text.IsNullOrWhiteSpace()) { return FluentResults.Result.Fail($"Contents are empty for {filePath}") .WithError(new Error($"Contents are empty for {filePath}") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.Sources, filePath)); } string t = text; //copy return IOExceptionsOperationRunner(nameof(TrySaveText), filePath, () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); System.IO.File.WriteAllText(fp, t, encoding); if (UseCaching) _fsCache[filePath] = t; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } public virtual FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes) { ((IService)this).CheckDisposed(); if (bytes is null || bytes.Length == 0) { return FluentResults.Result.Fail($"Byte array is null or empty for {filePath}") .WithError(new Error($"Byte array is null or empty for {filePath}") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.Sources, filePath)); } byte[] b = new byte[bytes.Length]; System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length); return IOExceptionsOperationRunner(nameof(TrySaveBinary), filePath, () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); System.IO.File.WriteAllBytes(fp, b); if (UseCaching) _fsCache[filePath] = b; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } public virtual FluentResults.Result FileExists(string filePath) { ((IService)this).CheckDisposed(); return IOExceptionsOperationRunner(nameof(FileExists), filePath, () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); return System.IO.File.Exists(fp); }); } public virtual FluentResults.Result DirectoryExists(string directoryPath) { ((IService)this).CheckDisposed(); try { var di = new DirectoryInfo(directoryPath); return di.Exists; } catch (Exception ex) { return new FluentResults.Result().WithError(ex.Message); } } public virtual async Task> TryLoadXmlAsync(string filePath, Encoding encoding = null) { ((IService)this).CheckDisposed(); if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) && cachedVal.TryPickT2(out var cachedDoc, out _)) return FluentResults.Result.Ok(cachedDoc); try { await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); var doc = await XDocument.LoadAsync(fs, LoadOptions.PreserveWhitespace, CancellationToken.None); if (UseCaching) _fsCache[filePath] = doc; return FluentResults.Result.Ok(doc); } catch (Exception e) { return FluentResults.Result.Fail(GetGeneralError(nameof(TryLoadXmlAsync), filePath)); } } public virtual async Task> TryLoadTextAsync(string filePath, Encoding encoding = null) { ((IService)this).CheckDisposed(); if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) && cachedVal.TryPickT1(out var cachedTxt, out _)) return FluentResults.Result.Ok(cachedTxt); return await IOExceptionsOperationRunnerAsync(nameof(TryLoadTextAsync), filePath, async () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); var txt = await System.IO.File.ReadAllTextAsync(fp); if (UseCaching) _fsCache[filePath] = txt; return FluentResults.Result.Ok(txt); }); } public virtual async Task> TryLoadBinaryAsync(string filePath) { ((IService)this).CheckDisposed(); if (UseCaching && _fsCache.TryGetValue(filePath, out var cachedVal) && cachedVal.TryPickT0(out var cachedBin, out _)) { return cachedBin; } return await IOExceptionsOperationRunnerAsync(nameof(TryLoadTextAsync), filePath, async () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); return await System.IO.File.ReadAllBytesAsync(fp); }); } public virtual async Task TrySaveXmlAsync(string filePath, XDocument document, Encoding encoding = null) => await TrySaveTextAsync(filePath, document.ToString(), encoding); public virtual async Task TrySaveTextAsync(string filePath, string text, Encoding encoding = null) { ((IService)this).CheckDisposed(); if (text.IsNullOrWhiteSpace()) { return FluentResults.Result.Fail($"Contents are empty for {filePath}") .WithError(new Error($"Contents are empty for {filePath}") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.Sources, filePath)); } string t = text.ToString(); //copy return await IOExceptionsOperationRunnerAsync(nameof(TrySaveText), filePath, async () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); await System.IO.File.WriteAllTextAsync(fp, t, encoding); if (UseCaching) _fsCache[filePath] = t; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } public virtual async Task TrySaveBinaryAsync(string filePath, byte[] bytes) { ((IService)this).CheckDisposed(); if (bytes is null || bytes.Length == 0) { return FluentResults.Result.Fail($"Byte array is null or empty for {filePath}") .WithError(new Error($"Byte array is null or empty for {filePath}") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.Sources, filePath)); } byte[] b = new byte[bytes.Length]; System.Buffer.BlockCopy(bytes, 0, b, 0, bytes.Length); return await IOExceptionsOperationRunnerAsync(nameof(TrySaveBinary), filePath, async () => { var fp = filePath.CleanUpPath(); fp = System.IO.Path.IsPathRooted(fp) ? fp : System.IO.Path.GetFullPath(fp); await System.IO.File.WriteAllBytesAsync(fp, b); if (UseCaching) _fsCache[filePath] = b; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } private async Task> IOExceptionsOperationRunnerAsync(string funcName, string filepath, Func>> operation) { try { return await operation?.Invoke()!; } catch (Exception e) { return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } private async Task IOExceptionsOperationRunnerAsync(string funcName, string filepath, Func> operation) { try { return await operation?.Invoke()!; } catch (Exception e) { return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } private FluentResults.Result IOExceptionsOperationRunner(string funcName, string filepath, Func> operation) { try { return operation?.Invoke(); } catch (Exception e) { return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } private FluentResults.Result IOExceptionsOperationRunner(string funcName, string filepath, Func operation) { try { return operation?.Invoke(); } catch (Exception e) { return ReturnException(e, filepath).WithError(GetGeneralError(funcName, filepath)); } } private Error GetGeneralError(string funcName, string localfp, ContentPackage package) => new Error($"{funcName}: Failed to load local file.") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.Sources, localfp) .WithMetadata(MetadataType.RootObject, package); private Error GetGeneralError(string funcName, string localfp) => new Error($"{funcName}: Failed to load local file.") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.Sources, localfp); private FluentResults.Result GetAbsoluePathFromLocal(ContentPackage package, string localFilePath) { if (Path.IsPathRooted(localFilePath)) { return new FluentResults.Result().WithError( new Error($"The path '{localFilePath}' is a rooted path. Must be relative!") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.RootObject, localFilePath)); } Guard.IsNotNull(package, nameof(package)); return new FluentResults.Result().WithSuccess($"Path constructed") .WithValue(System.IO.Path.GetFullPath(System.IO.Path.Combine( _configData.RunLocation, _configData.LocalPackageDataPath.Replace( _configData.LocalDataPathRegex, package.TryExtractSteamWorkshopId(out var id) ? id.Value.ToString() : package.Name), localFilePath))); } public FluentResults.Result GetAbsoluePathFromPackage(ContentPackage package, string localFilePath) { Guard.IsNotNull(package, nameof(package)); if (localFilePath.IsNullOrWhiteSpace()) { return new FluentResults.Result().WithValue(Path.GetFullPath(package.Path.CleanUpPath())); } var path = localFilePath.CleanUpPath(); if (Path.IsPathRooted(path)) { return new FluentResults.Result().WithError( new Error($"The path '{localFilePath}' is a rooted path. Must be relative!") .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.RootObject, localFilePath)); } return new FluentResults.Result().WithSuccess($"Path constructed") .WithValue(Path.Combine(Path.GetFullPath(package.Path.CleanUpPath()), path)); } private FluentResults.Result ReturnException(TException exception, ContentPackage package) where TException : Exception { return new FluentResults.Result().WithError(new ExceptionalError(exception) .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.RootObject, package)); } private FluentResults.Result ReturnException(TException exception, ContentPackage package) where TException : Exception { return new FluentResults.Result().WithError(new ExceptionalError(exception) .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.RootObject, package)); } private FluentResults.Result ReturnException(TException exception, string filePath) where TException : Exception { return new FluentResults.Result().WithError(new ExceptionalError(exception) .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.RootObject, filePath)); } }