using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using Cysharp.Threading.Tasks; using MoralisUnity.Platform.Abstractions; using MoralisUnity.Platform.Objects; using MoralisUnity.Platform.Utilities; using UnityEngine; using static MoralisUnity.Platform.ResourceWrapper; #pragma warning disable CS1998 // This async method lacks 'await' operators and will run synchronously namespace MoralisUnity.Platform.Services.Infrastructure { /// /// Implements `IStorageController` for PCL targets, based off of PCLStorage. /// public class MoralisCacheService : IDiskFileCacheService where TUser : MoralisUser { class FileBackedCache : IDataCache { public FileBackedCache(FileInfo file) => File = file; internal void Save() { File.WriteContent(JsonUtilities.Encode(Storage)); } internal async UniTask LoadAsync() { Storage = new Dictionary { }; #if !UNITY_WEBGL if (File.Exists) { string data = string.Empty; try { data = await File.ReadAllTextAsync(); } catch (Exception exp) { Debug.Log($"File read error: {exp.Message}"); } lock (Mutex) { try { Storage = JsonUtilities.Parse(data) as Dictionary; } catch { Storage = new Dictionary { }; } } } else { Storage = new Dictionary { }; using (File.Create()) { } } #endif } internal void Update(IDictionary contents) => Lock(() => Storage = contents.ToDictionary(element => element.Key, element => element.Value)); public async UniTask AddAsync(string key, object value) { Storage[key] = value; Save(); } public async UniTask RemoveAsync(string key) { Storage.Remove(key); Save(); } public void Add(string key, object value) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); public bool Remove(string key) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); public void Add(KeyValuePair item) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); public bool Remove(KeyValuePair item) => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); public bool ContainsKey(string key) => Lock(() => Storage.ContainsKey(key)); public bool TryGetValue(string key, out object value) { lock (Mutex) { return (Result: Storage.TryGetValue(key, out object found), value = found).Result; } } public void Clear() => Lock(() => Storage.Clear()); public bool Contains(KeyValuePair item) => Lock(() => Elements.Contains(item)); public void CopyTo(KeyValuePair[] array, int arrayIndex) => Lock(() => Elements.CopyTo(array, arrayIndex)); public IEnumerator> GetEnumerator() => Storage.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => Storage.GetEnumerator(); public FileInfo File { get; set; } public object Mutex { get; set; } = new object { }; // ALTNAME: Operate TResult Lock(Func operation) { lock (Mutex) { return operation.Invoke(); } } void Lock(Action operation) { lock (Mutex) { operation.Invoke(); } } ICollection> Elements => Storage as ICollection>; Dictionary Storage { get; set; } = new Dictionary { }; public ICollection Keys => Storage.Keys; public ICollection Values => Storage.Values; public int Count => Storage.Count; public bool IsReadOnly => Elements.IsReadOnly; public object this[string key] { get => Storage[key]; set => throw new NotSupportedException(FileBackedCacheSynchronousMutationNotSupportedMessage); } } /// /// Set this for systems (Unity based Android, iOs, etc.) that may not /// be able to access Environment.SpecialFolder.LocalApplicationData /// public static string BaseFilePath { get; set; } FileInfo File { get; set; } FileBackedCache Cache { get; set; } /// /// Creates a Moralis storage controller and attempts to extract a previously created settings storage file from the persistent storage location. /// public MoralisCacheService() { } /// /// Creates a Moralis storage controller with the provided wrapper. /// /// The file wrapper that the storage controller instance should target public MoralisCacheService(FileInfo file) => EnsureCacheExists(file); FileBackedCache EnsureCacheExists(FileInfo file = default) => Cache ??= new FileBackedCache(file ?? (File ??= PersistentCacheFile)); /// /// Loads a settings dictionary from the file wrapped by . /// /// A storage dictionary containing the deserialized content of the storage file targeted by the instance public async UniTask> LoadAsync() { // Check if storage dictionary is already created from the controllers file (create if not) EnsureCacheExists(); // Load storage dictionary content async and return the resulting dictionary type //return Queue.Enqueue(toAwait => toAwait.ContinueWith(_ => Cache.LoadAsync().OnSuccess(_ => Cache as IDataCache)).Unwrap(), CancellationToken.None); await Cache.LoadAsync(); return Cache as IDataCache; } /// /// Saves the requested data. /// /// The data to be saved. /// A data cache containing the saved data. public async UniTask> SaveAsync(IDictionary contents) { EnsureCacheExists(); Cache.Save(); return Cache as IDataCache; } /// /// /// public void RefreshPaths() => Cache = new FileBackedCache(File = PersistentCacheFile); /// /// Clears the data controlled by this class. /// public void Clear() { if (new FileInfo(FallbackRelativeCacheFilePath) is { Exists: true } file) { file.Delete(); } } /// /// /// public string RelativeCacheFilePath { get; set; } /// /// /// public string AbsoluteCacheFilePath { get => StoredAbsoluteCacheFilePath ?? Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), RelativeCacheFilePath ?? FallbackRelativeCacheFilePath)); set => StoredAbsoluteCacheFilePath = value; } string StoredAbsoluteCacheFilePath { get; set; } /// /// Gets the calculated persistent storage file fallback path for this app execution. /// public string FallbackRelativeCacheFilePath => StoredFallbackRelativeCacheFilePath ??= IdentifierBasedRelativeCacheLocationGenerator.Fallback.GetRelativeCacheFilePath(new MutableServiceHub { CacheService = this }); string StoredFallbackRelativeCacheFilePath { get; set; } /// /// Gets or creates the file pointed to by and returns it's wrapper as a instance. /// public FileInfo PersistentCacheFile { get { Directory.CreateDirectory(AbsoluteCacheFilePath.Substring(0, AbsoluteCacheFilePath.LastIndexOf(Path.DirectorySeparatorChar))); FileInfo file = new FileInfo(AbsoluteCacheFilePath); if (!file.Exists) using (file.Create()) { } // Hopefully the JIT doesn't no-op this. The behaviour of the "using" clause should dictate how the stream is closed, to make sure it happens properly. return file; } } /// /// Gets the file wrapper for the specified . /// /// The relative path to the target file /// An instance of wrapping the the value public FileInfo GetRelativeFile(string path) { return DefineRelativeFilePath(path); } public static FileInfo DefineRelativeFilePath(string path) { string basePath = BaseFilePath ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); Directory.CreateDirectory((path = Path.GetFullPath(Path.Combine(basePath, path))).Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar))); return new FileInfo(path); } // MoveAsync /// /// Transfers a file from to . /// /// /// /// A task that completes once the file move operation form to completes. public async UniTask TransferAsync(string originFilePath, string targetFilePath) { if (!String.IsNullOrWhiteSpace(originFilePath) && !String.IsNullOrWhiteSpace(targetFilePath)) { FileInfo originFile = new FileInfo(originFilePath); FileInfo targetFile = new FileInfo(targetFilePath); if (originFile.Exists && targetFile != null) { using StreamWriter writer = new StreamWriter(targetFile.OpenWrite(), Encoding.Unicode); using StreamReader reader = new StreamReader(originFile.OpenRead(), Encoding.Unicode); await writer.WriteAsync(await reader.ReadToEndAsync()); } } } } }