using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq; using Barotrauma.LuaCs.Data; using Barotrauma.Networking; using FluentResults; using FluentResults.LuaCs; using Microsoft.CodeAnalysis; using Microsoft.Toolkit.Diagnostics; using Error = FluentResults.Error; using Path = System.IO.Path; namespace Barotrauma.LuaCs; public class StorageService : IStorageService { public StorageService(IStorageServiceConfig configData) { ConfigData = configData; IsReadOperationAllowedEval = bool (str) => true; IsWriteOperationAllowedEval = bool (str) => true; } private readonly ConcurrentDictionary> _fsCache = new(); protected readonly IStorageServiceConfig ConfigData; protected readonly AsyncReaderWriterLock OperationsLock = new(); private Func _isReadOperationAllowedEval; protected Func IsReadOperationAllowedEval { get => _isReadOperationAllowedEval; set { if (value is not null) _isReadOperationAllowedEval = value; } } private Func _isWriteOperationAllowedEval; protected Func IsWriteOperationAllowedEval { get => _isWriteOperationAllowedEval; set { if (value is not null) _isWriteOperationAllowedEval = value; } } public bool IsDisposed => ModUtils.Threading.GetBool(ref _isDisposed); private int _isDisposed = 0; public virtual void Dispose() { using var lck = OperationsLock.AcquireWriterLock().ConfigureAwait(false).GetAwaiter().GetResult(); if (!ModUtils.Threading.CheckIfClearAndSetBool(ref _isDisposed)) return; _fsCache.Clear(); } public void PurgeCache() { using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); _fsCache.Clear(); } public void PurgeFileFromCache(string absolutePath) { using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); 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) { using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); 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; } } } // --- Local Game Content protected Result GetAbsolutePathForLocal(ContentPackage package, string localFilePath) { if (Path.IsPathRooted(localFilePath)) ThrowHelper.ThrowArgumentException($"{nameof(GetAbsolutePathForLocal)}: The path {localFilePath} is an absolute path."); try { var path = System.IO.Path.GetFullPath(Path.Combine( ConfigData.LocalPackageDataPath.Replace(ConfigData.LocalDataPathRegex, package.Name).CleanUpPathCrossPlatform(), localFilePath.CleanUpPathCrossPlatform())); if (!path.StartsWith(Path.GetFullPath(ConfigData.LocalDataSavePath))) ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(GetAbsolutePathForLocal)}: The local path of '{path}' is not a local path!"); return path; } catch (Exception e) { if (e is ArgumentNullException or ArgumentException or UnauthorizedAccessException) throw; // these are dev errors and should be propagated. return FluentResults.Result.Fail(new ExceptionalError(e)); } } private Result LoadLocalData(ContentPackage package, string localFilePath, Func> dataLoader) { Guard.IsNotNull(package, nameof(package)); Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); var res = GetAbsolutePathForLocal(package, localFilePath); return res is { IsFailed: true } ? res.ToResult() : dataLoader(res.Value); } public Result LoadLocalXml(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadXml); public Result LoadLocalBinary(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadBinary); public Result LoadLocalText(ContentPackage package, string localFilePath) => LoadLocalData(package, localFilePath, TryLoadText); private FluentResults.Result SaveLocalData(ContentPackage package, string localFilePath, in T data, Func dataSaver) { Guard.IsNotNull(package, nameof(package)); Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); var res = GetAbsolutePathForLocal(package, localFilePath); return res is { IsFailed: true } ? res.ToResult() : dataSaver(res.Value, data); } public FluentResults.Result SaveLocalXml(ContentPackage package, string localFilePath, XDocument document) => SaveLocalData(package, localFilePath, document, (path, data) => TrySaveXml(path, in data)); public FluentResults.Result SaveLocalBinary(ContentPackage package, string localFilePath, in byte[] bytes) => SaveLocalData(package, localFilePath, bytes, (path, data) => TrySaveBinary(path, in data)); public FluentResults.Result SaveLocalText(ContentPackage package, string localFilePath, in string text) => SaveLocalData(package, localFilePath, text, (path, data) => TrySaveText(path, in data)); private async Task> LoadLocalDataAsync(ContentPackage package, string localFilePath, Func>> dataLoader) { Guard.IsNotNull(package, nameof(package)); Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); using var lck = await OperationsLock.AcquireReaderLock(); IService.CheckDisposed(this); var res = GetAbsolutePathForLocal(package, localFilePath); return res is { IsFailed: true } ? res.ToResult() : await dataLoader(res.Value); } public async Task> LoadLocalXmlAsync(ContentPackage package, string localFilePath) => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadXmlAsync(path)); public async Task> LoadLocalBinaryAsync(ContentPackage package, string localFilePath) => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadBinaryAsync(path)); public async Task> LoadLocalTextAsync(ContentPackage package, string localFilePath) => await LoadLocalDataAsync(package, localFilePath, async path => await TryLoadTextAsync(path)); private async Task SaveLocalDataAsync(ContentPackage package, string localFilePath, T data, Func> dataSaver) { Guard.IsNotNull(package, nameof(package)); Guard.IsNotNullOrWhiteSpace(localFilePath, nameof(localFilePath)); IService.CheckDisposed(this); using var lck = await OperationsLock.AcquireReaderLock(); var res = GetAbsolutePathForLocal(package, localFilePath); return res is { IsFailed: true } ? res.ToResult() : await dataSaver(res.Value, data); } public async Task SaveLocalXmlAsync(ContentPackage package, string localFilePath, XDocument document) => await SaveLocalDataAsync(package, localFilePath, document, async (path, doc) => await TrySaveXmlAsync(path, doc)); public async Task SaveLocalBinaryAsync(ContentPackage package, string localFilePath, byte[] bytes) => await SaveLocalDataAsync(package, localFilePath, bytes, async (path, bin) => await TrySaveBinaryAsync(path, bin)); public async Task SaveLocalTextAsync(ContentPackage package, string localFilePath, string text) => await SaveLocalDataAsync(package, localFilePath, text, async (path, txt) => await TrySaveTextAsync(path, txt)); private bool IsPackagePathValid(ContentPath contentPath) { return contentPath.FullPath.StartsWith(ConfigData.WorkshopModsDirectory) || contentPath.FullPath.StartsWith(ConfigData.LocalModsDirectory) #if CLIENT || contentPath.FullPath.StartsWith(ConfigData.TempDownloadsDirectory) #endif || contentPath.FullPath.StartsWith(Path.GetFullPath(ContentPackageManager.VanillaCorePackage!.Dir).CleanUpPathCrossPlatform()); } // --- Package Content private Result LoadPackageData(ContentPath contentPath, Func> dataLoader) { Guard.IsNotNull(contentPath, nameof(contentPath)); Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath)); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); if (!IsPackagePathValid(contentPath)) { ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageData)}: The filepath of `{contentPath.FullPath}' is not in a package directory!"); } return dataLoader(contentPath.FullPath); } public Result LoadPackageXml(ContentPath filePath) => LoadPackageData(filePath, path => TryLoadXml(filePath.FullPath)); public Result LoadPackageBinary(ContentPath filePath) => LoadPackageData(filePath, path => TryLoadBinary(filePath.FullPath)); public Result LoadPackageText(ContentPath filePath) => LoadPackageData(filePath, path => TryLoadText(filePath.FullPath)); private ImmutableArray<(ContentPath, Result)> LoadPackageDataFiles(ImmutableArray filePaths, Func> dataLoader) { if (filePaths.IsDefaultOrEmpty) ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!"); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); var builder = ImmutableArray.CreateBuilder<(ContentPath, Result)>(); foreach (var path in filePaths) { builder.Add((path, LoadPackageData(path, dataLoader))); } return builder.ToImmutable(); } public ImmutableArray<(ContentPath, Result)> LoadPackageXmlFiles(ImmutableArray filePaths) => LoadPackageDataFiles(filePaths, TryLoadXml); public ImmutableArray<(ContentPath, Result)> LoadPackageBinaryFiles(ImmutableArray filePaths) => LoadPackageDataFiles(filePaths, TryLoadBinary); public ImmutableArray<(ContentPath, Result)> LoadPackageTextFiles(ImmutableArray filePaths) => LoadPackageDataFiles(filePaths, TryLoadText); public Result> FindFilesInPackage(ContentPackage package, string localSubfolder, string regexFilter, bool searchRecursively) { Guard.IsNotNull(package, nameof(package)); try { var cp = ContentPath.FromRaw(package, package.Dir); var fullPath = localSubfolder.IsNullOrWhiteSpace() ? Path.GetFullPath(cp.FullPath) : Path.GetFullPath(localSubfolder, cp.FullPath); return System.IO.Directory.GetFiles(fullPath, regexFilter, searchRecursively ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly) .ToImmutableArray(); } catch (Exception e) { if (e is ArgumentNullException or ArgumentException) throw; return FluentResults.Result.Fail(new ExceptionalError(e) .WithMetadata(MetadataType.ExceptionObject, this) .WithMetadata(MetadataType.RootObject, package)); } } private async Task> LoadPackageDataAsync(ContentPath contentPath, Func>> dataLoader) { Guard.IsNotNull(contentPath, nameof(contentPath)); Guard.IsNotNullOrWhiteSpace(contentPath.FullPath, nameof(contentPath.FullPath)); using var lck = await OperationsLock.AcquireReaderLock(); IService.CheckDisposed(this); if (!IsPackagePathValid(contentPath)) { ThrowHelper.ThrowUnauthorizedAccessException($"{nameof(LoadPackageDataAsync)}: The filepath of `{contentPath.FullPath}' is not in a package directory!"); } return await dataLoader(contentPath.FullPath); } public async Task> LoadPackageXmlAsync(ContentPath filePath) => await LoadPackageDataAsync(filePath, async path => await TryLoadXmlAsync(path)); public async Task> LoadPackageBinaryAsync(ContentPath filePath) => await LoadPackageDataAsync(filePath, async path => await TryLoadBinaryAsync(path)); public async Task> LoadPackageTextAsync(ContentPath filePath) => await LoadPackageDataAsync(filePath, async path => await TryLoadTextAsync(path)); private async Task)>> LoadPackageDataFilesAsync( ImmutableArray filePaths, Func>> dataLoader) { if (filePaths.IsDefaultOrEmpty) { ThrowHelper.ThrowArgumentNullException($"{nameof(LoadPackageData)}: File paths is empty!"); } using var lck = await OperationsLock.AcquireReaderLock(); var builder = ImmutableArray.CreateBuilder<(ContentPath, Result)>(); foreach (var path in filePaths) { builder.Add((path, await LoadPackageDataAsync(path, dataLoader))); } return builder.ToImmutable(); } public async Task)>> LoadPackageXmlFilesAsync(ImmutableArray filePaths) => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadXmlAsync(path)); public async Task)>> LoadPackageBinaryFilesAsync(ImmutableArray filePaths) => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadBinaryAsync(path)); public async Task)>> LoadPackageTextFilesAsync(ImmutableArray filePaths) => await LoadPackageDataFilesAsync(filePaths, async path => await TryLoadTextAsync(path)); private int _useCaching; public bool UseCaching { get => ModUtils.Threading.GetBool(ref _useCaching); set => ModUtils.Threading.SetBool(ref _useCaching, value); } // Method group redirect private FluentResults.Result TryLoadXml(string filePath) => TryLoadXml(filePath, null); public virtual FluentResults.Result TryLoadXml(string filePath, Encoding encoding) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); 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)); } } // Method group redirect private FluentResults.Result TryLoadText(string filePath) => TryLoadText(filePath, null); public virtual FluentResults.Result TryLoadText(string filePath, Encoding encoding) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); if (IsReadOperationAllowedEval?.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TryLoadText)}: File '{filePath}' is not allowed."); } 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) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); if (IsReadOperationAllowedEval?.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TryLoadBinary)}: File '{filePath}' is not allowed."); } 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) { Guard.IsNotNullOrWhiteSpace(text, nameof(text)); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TrySaveText)}: File '{filePath}' is not allowed."); } 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); Directory.CreateDirectory(Path.GetDirectoryName(fp)!); System.IO.File.WriteAllText(fp, t, encoding ?? Encoding.UTF8); if (UseCaching) _fsCache[filePath] = t; return new FluentResults.Result().WithSuccess($"Saved to file successfully"); }); } public virtual FluentResults.Result TrySaveBinary(string filePath, in byte[] bytes) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); Guard.IsNotNull(bytes, nameof(bytes)); Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes)); using var lck = OperationsLock.AcquireReaderLock().ConfigureAwait(false).GetAwaiter().GetResult(); IService.CheckDisposed(this); if (IsWriteOperationAllowedEval?.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TrySaveBinary)}: File '{filePath}' is not allowed."); } 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); Directory.CreateDirectory(Path.GetDirectoryName(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) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); IService.CheckDisposed(this); // lock not needed if (IsReadOperationAllowedEval?.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(FileExists)}: File '{filePath}' is not allowed."); } 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) { Guard.IsNotNullOrWhiteSpace(directoryPath, nameof(directoryPath)); IService.CheckDisposed(this); // lock not needed if (IsReadOperationAllowedEval?.Invoke(directoryPath) is not true) { return FluentResults.Result.Fail($"{nameof(DirectoryExists)}: File '{directoryPath}' is not allowed."); } 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) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); using var lck = await OperationsLock.AcquireReaderLock(); IService.CheckDisposed(this); if (IsReadOperationAllowedEval.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TryLoadXmlAsync)}: File '{filePath}' is not allowed."); } 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) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); using var lck = await OperationsLock.AcquireReaderLock(); IService.CheckDisposed(this); if (IsReadOperationAllowedEval.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TryLoadTextAsync)}: File '{filePath}' is not allowed."); } 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) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); using var lck = await OperationsLock.AcquireReaderLock(); IService.CheckDisposed(this); if (IsReadOperationAllowedEval.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TryLoadBinaryAsync)}: File '{filePath}' is not allowed."); } 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); }); } // method group overload 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) { Guard.IsNotNullOrWhiteSpace(text, nameof(text)); using var lck = await OperationsLock.AcquireReaderLock(); IService.CheckDisposed(this); if (IsWriteOperationAllowedEval.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TrySaveTextAsync)}: File '{filePath}' is not allowed."); } 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) { Guard.IsNotNullOrWhiteSpace(filePath, nameof(filePath)); Guard.IsNotNull(bytes, nameof(bytes)); Guard.HasSizeGreaterThanOrEqualTo(bytes, 1, nameof(bytes)); using var lck = await OperationsLock.AcquireReaderLock(); IService.CheckDisposed(this); if (IsWriteOperationAllowedEval.Invoke(filePath) is not true) { return FluentResults.Result.Fail($"{nameof(TrySaveBinaryAsync)}: File '{filePath}' is not allowed."); } 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) { if (e is ArgumentException or ArgumentNullException) throw; 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) { if (e is ArgumentException or ArgumentNullException) throw; 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) { if (e is ArgumentException or ArgumentNullException) throw; 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) { if (e is ArgumentException or ArgumentNullException) throw; 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 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)); } }