From d472579ccf1ed5ed03802e2413f20a8ad89b83b5 Mon Sep 17 00:00:00 2001 From: Maxime BATISTA Date: Fri, 26 May 2023 11:39:28 +0200 Subject: [PATCH] wait for database files to be released before trying to write in them --- .../Data/CatastrophicPerformancesDatabase.cs | 345 ++++++++++-------- 1 file changed, 186 insertions(+), 159 deletions(-) diff --git a/LocalServices/Data/CatastrophicPerformancesDatabase.cs b/LocalServices/Data/CatastrophicPerformancesDatabase.cs index 451a424..a1d386d 100644 --- a/LocalServices/Data/CatastrophicPerformancesDatabase.cs +++ b/LocalServices/Data/CatastrophicPerformancesDatabase.cs @@ -1,161 +1,188 @@ -using Models; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; +using Models; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.Serialization; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using System.Threading.Tasks; - -namespace LocalEndpoint.Data -{ - /// - /// Database implementation with catastrophic performances. - /// This database implementation persists data in xml and will save all the data in their files on each mutable requests. - /// - internal class CatastrophicPerformancesDatabase : Database - { - - private static readonly DataContractSerializer RECIPES_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); - private static readonly DataContractSerializer USERS_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); - private static readonly DataContractSerializer ACCOUNTS_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); - - private static readonly string RECIPES_FILENAME = "recipes_data.xml"; - private static readonly string USERS_FILENAME = "users_data.xml"; - private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml"; - - private readonly Dictionary recipesData; - private readonly Dictionary usersData; - private readonly Dictionary accountsData; - - private readonly string dbPath; - - - public CatastrophicPerformancesDatabase(string folderPath) - { - dbPath = folderPath; - if (!Directory.Exists(folderPath)) - Directory.CreateDirectory(folderPath); - - usersData = Load(USERS_FILENAME, USERS_SERIALIZER); - recipesData = Load(RECIPES_FILENAME, RECIPES_SERIALIZER); - accountsData = Load(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER); - } - - public bool IsEmpty() - { - return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0; - } - - public Account? GetAccount(string email, string passwordHash) - { - if (!accountsData.TryGetValue(email, out AccountData? data)) - return null; - - if (data.PasswordHash != passwordHash) return null; - return new Account(usersData[data.UserId].User, data.Email); - } - - public void InsertAccount(Account account, string passwordHash) - { - accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash); - Save(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER, accountsData); - } - - public Recipe? GetRecipe(Guid id) - { - if (recipesData.TryGetValue(id, out RecipeData? data)) - return ConvertRecipeDataToRecipe(data); - return null; - } - - public RecipeRate GetRecipeRate(Guid user, Guid recipe) - { - return usersData[user].Rates[recipe]; - } - - public async void InsertInUserList(Guid userId, Guid recipeId, uint persAmount) - { - usersData[userId].RecipesList[recipeId] = persAmount; - Save(USERS_FILENAME, USERS_SERIALIZER, usersData); - } - - public async void RemoveFromUserList(Guid userId, Guid recipeId) - { - usersData[userId].RecipesList.Remove(recipeId); - Save(USERS_FILENAME, USERS_SERIALIZER, usersData); - } - - - public async void InsertRecipe(Recipe recipe) - { - recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps); - Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData); - } - - public async void InsertUser(User user) - { - usersData[user.Id] = new UserData(user, new Dictionary(), new Dictionary()); - Save(USERS_FILENAME, USERS_SERIALIZER, usersData); - } - - public async void InsertRate(Guid userId, Guid recipeId, RecipeRate rate) - { - usersData[userId].Rates[recipeId] = rate; - Save(USERS_FILENAME, USERS_SERIALIZER, usersData); - } - - public async void RemoveRecipe(Guid id) - { - recipesData.Remove(id); - Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData); - } - - public ImmutableList ListAllRecipes() - { - return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe); - } - - - public ImmutableDictionary ListRatesOf(Guid user) - { - return usersData[user].Rates.ToImmutableDictionary(); - } - - public ImmutableDictionary GetRecipeListOf(Guid user) - { - return usersData[user].RecipesList.ToImmutableDictionary(); - } - - private Recipe ConvertRecipeDataToRecipe(RecipeData rd) - { - var owner = usersData[rd.OwnerID].User; - return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps); - } - - private Dictionary Load(string fileName, DataContractSerializer deserializer) - { - var file = dbPath + "/" + fileName; - var fileInfo = new FileInfo(file); - - if (!fileInfo.Exists) - fileInfo.Create(); - - if (fileInfo.Length == 0) - return new Dictionary(); //file is empty thus there is nothing to deserialize - - string text = File.ReadAllText(file); - - return JsonSerializer.Deserialize>(text); - } - - private async void Save(string fileName, DataContractSerializer serializer, Dictionary dict) - { - string json = JsonSerializer.Serialize(dict); - File.WriteAllText(dbPath + "/" + fileName, json); - } - - } -} +using System.Threading.Tasks; + +namespace LocalEndpoint.Data +{ + /// + /// Database implementation with catastrophic performances. + /// This database implementation persists data in xml and will save all the data in their files on each mutable requests. + /// + internal class CatastrophicPerformancesDatabase : Database + { + + private static readonly DataContractSerializer RECIPES_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); + private static readonly DataContractSerializer USERS_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); + private static readonly DataContractSerializer ACCOUNTS_SERIALIZER = new DataContractSerializer(typeof(Dictionary)); + + private static readonly string RECIPES_FILENAME = "recipes_data.xml"; + private static readonly string USERS_FILENAME = "users_data.xml"; + private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml"; + + private readonly Dictionary recipesData; + private readonly Dictionary usersData; + private readonly Dictionary accountsData; + + private readonly string dbPath; + + + public CatastrophicPerformancesDatabase(string folderPath) + { + dbPath = folderPath; + if (!Directory.Exists(folderPath)) + Directory.CreateDirectory(folderPath); + + usersData = Load(USERS_FILENAME, USERS_SERIALIZER); + recipesData = Load(RECIPES_FILENAME, RECIPES_SERIALIZER); + accountsData = Load(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER); + } + + public bool IsEmpty() + { + return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0; + } + + public Account? GetAccount(string email, string passwordHash) + { + if (!accountsData.TryGetValue(email, out AccountData? data)) + return null; + + if (data.PasswordHash != passwordHash) return null; + return new Account(usersData[data.UserId].User, data.Email); + } + + public void InsertAccount(Account account, string passwordHash) + { + accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash); + Save(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER, accountsData); + } + + public Recipe? GetRecipe(Guid id) + { + if (recipesData.TryGetValue(id, out RecipeData? data)) + return ConvertRecipeDataToRecipe(data); + return null; + } + + public RecipeRate GetRecipeRate(Guid user, Guid recipe) + { + return usersData[user].Rates[recipe]; + } + + public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount) + { + usersData[userId].RecipesList[recipeId] = persAmount; + Save(USERS_FILENAME, USERS_SERIALIZER, usersData); + } + + public void RemoveFromUserList(Guid userId, Guid recipeId) + { + usersData[userId].RecipesList.Remove(recipeId); + Save(USERS_FILENAME, USERS_SERIALIZER, usersData); + } + + + public void InsertRecipe(Recipe recipe) + { + recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps); + Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData); + } + + public void InsertUser(User user) + { + usersData[user.Id] = new UserData(user, new Dictionary(), new Dictionary()); + Save(USERS_FILENAME, USERS_SERIALIZER, usersData); + } + + public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate) + { + usersData[userId].Rates[recipeId] = rate; + Save(USERS_FILENAME, USERS_SERIALIZER, usersData); + } + + public void RemoveRecipe(Guid id) + { + recipesData.Remove(id); + Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData); + } + + public ImmutableList ListAllRecipes() + { + return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe); + } + + + public ImmutableDictionary ListRatesOf(Guid user) + { + return usersData[user].Rates.ToImmutableDictionary(); + } + + public ImmutableDictionary GetRecipeListOf(Guid user) + { + return usersData[user].RecipesList.ToImmutableDictionary(); + } + + private Recipe ConvertRecipeDataToRecipe(RecipeData rd) + { + var owner = usersData[rd.OwnerID].User; + return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps); + } + + private Dictionary Load(string fileName, DataContractSerializer deserializer) + { + var file = dbPath + "/" + fileName; + var fileInfo = new FileInfo(file); + + if (!fileInfo.Exists) + fileInfo.Create(); + + if (fileInfo.Length == 0) + return new Dictionary(); //file is empty thus there is nothing to deserialize + + string text = File.ReadAllText(file); + + return JsonSerializer.Deserialize>(text); + } + + private async void Save(string fileName, DataContractSerializer serializer, Dictionary dict) + { + string json = JsonSerializer.Serialize(dict); + using (var stream = WaitForFile(fileName, FileMode.Open, FileAccess.Write, FileShare.Write)) + { + var bytes = Encoding.ASCII.GetBytes(json); + await stream.WriteAsync(bytes, 0, bytes.Length); + } + } + + // This is a workaround function to wait for a file to be released before opening it. + // This was to fix the Save method that used to throw sometimes as the file were oftenly being scanned by the androids' antivirus. + // Simply wait until the file is released and return it. This function will never return until + private static FileStream WaitForFile(string fullPath, FileMode mode, FileAccess access, FileShare share) + { + for (int attempt = 0 ; attempt < 20; attempt++) //20 attempts equals to 2 seconds of wait + { + FileStream? fs = null; + try + { + fs = new FileStream(fullPath, mode, access, share); + return fs; + } + catch (IOException) + { + if (fs != null) + fs.Dispose(); + + Thread.Sleep(100); + } + } + throw new TimeoutException("Could not access file '" + fullPath + "', maximum attempts reached."); + } + } +}