wait for database files to be released before trying to write in them
continuous-integration/drone/push Build is passing Details

master
Maxime BATISTA 2 years ago
parent e6ccd28f20
commit d472579ccf

@ -1,161 +1,188 @@
using Models; using Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LocalEndpoint.Data namespace LocalEndpoint.Data
{ {
/// <summary> /// <summary>
/// Database implementation with catastrophic performances. /// 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. /// This database implementation persists data in xml and will save all the data in their files on each mutable requests.
/// </summary> /// </summary>
internal class CatastrophicPerformancesDatabase : Database internal class CatastrophicPerformancesDatabase : Database
{ {
private static readonly DataContractSerializer RECIPES_SERIALIZER = new DataContractSerializer(typeof(Dictionary<Guid, RecipeData>)); private static readonly DataContractSerializer RECIPES_SERIALIZER = new DataContractSerializer(typeof(Dictionary<Guid, RecipeData>));
private static readonly DataContractSerializer USERS_SERIALIZER = new DataContractSerializer(typeof(Dictionary<Guid, UserData>)); private static readonly DataContractSerializer USERS_SERIALIZER = new DataContractSerializer(typeof(Dictionary<Guid, UserData>));
private static readonly DataContractSerializer ACCOUNTS_SERIALIZER = new DataContractSerializer(typeof(Dictionary<string, AccountData>)); private static readonly DataContractSerializer ACCOUNTS_SERIALIZER = new DataContractSerializer(typeof(Dictionary<string, AccountData>));
private static readonly string RECIPES_FILENAME = "recipes_data.xml"; private static readonly string RECIPES_FILENAME = "recipes_data.xml";
private static readonly string USERS_FILENAME = "users_data.xml"; private static readonly string USERS_FILENAME = "users_data.xml";
private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml"; private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml";
private readonly Dictionary<Guid, RecipeData> recipesData; private readonly Dictionary<Guid, RecipeData> recipesData;
private readonly Dictionary<Guid, UserData> usersData; private readonly Dictionary<Guid, UserData> usersData;
private readonly Dictionary<string, AccountData> accountsData; private readonly Dictionary<string, AccountData> accountsData;
private readonly string dbPath; private readonly string dbPath;
public CatastrophicPerformancesDatabase(string folderPath) public CatastrophicPerformancesDatabase(string folderPath)
{ {
dbPath = folderPath; dbPath = folderPath;
if (!Directory.Exists(folderPath)) if (!Directory.Exists(folderPath))
Directory.CreateDirectory(folderPath); Directory.CreateDirectory(folderPath);
usersData = Load<Guid, UserData>(USERS_FILENAME, USERS_SERIALIZER); usersData = Load<Guid, UserData>(USERS_FILENAME, USERS_SERIALIZER);
recipesData = Load<Guid, RecipeData>(RECIPES_FILENAME, RECIPES_SERIALIZER); recipesData = Load<Guid, RecipeData>(RECIPES_FILENAME, RECIPES_SERIALIZER);
accountsData = Load<string, AccountData>(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER); accountsData = Load<string, AccountData>(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER);
} }
public bool IsEmpty() public bool IsEmpty()
{ {
return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0; return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0;
} }
public Account? GetAccount(string email, string passwordHash) public Account? GetAccount(string email, string passwordHash)
{ {
if (!accountsData.TryGetValue(email, out AccountData? data)) if (!accountsData.TryGetValue(email, out AccountData? data))
return null; return null;
if (data.PasswordHash != passwordHash) return null; if (data.PasswordHash != passwordHash) return null;
return new Account(usersData[data.UserId].User, data.Email); return new Account(usersData[data.UserId].User, data.Email);
} }
public void InsertAccount(Account account, string passwordHash) public void InsertAccount(Account account, string passwordHash)
{ {
accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash); accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash);
Save(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER, accountsData); Save(ACCOUNTS_FILENAME, ACCOUNTS_SERIALIZER, accountsData);
} }
public Recipe? GetRecipe(Guid id) public Recipe? GetRecipe(Guid id)
{ {
if (recipesData.TryGetValue(id, out RecipeData? data)) if (recipesData.TryGetValue(id, out RecipeData? data))
return ConvertRecipeDataToRecipe(data); return ConvertRecipeDataToRecipe(data);
return null; return null;
} }
public RecipeRate GetRecipeRate(Guid user, Guid recipe) public RecipeRate GetRecipeRate(Guid user, Guid recipe)
{ {
return usersData[user].Rates[recipe]; return usersData[user].Rates[recipe];
} }
public async void InsertInUserList(Guid userId, Guid recipeId, uint persAmount) public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount)
{ {
usersData[userId].RecipesList[recipeId] = persAmount; usersData[userId].RecipesList[recipeId] = persAmount;
Save(USERS_FILENAME, USERS_SERIALIZER, usersData); Save(USERS_FILENAME, USERS_SERIALIZER, usersData);
} }
public async void RemoveFromUserList(Guid userId, Guid recipeId) public void RemoveFromUserList(Guid userId, Guid recipeId)
{ {
usersData[userId].RecipesList.Remove(recipeId); usersData[userId].RecipesList.Remove(recipeId);
Save(USERS_FILENAME, USERS_SERIALIZER, usersData); Save(USERS_FILENAME, USERS_SERIALIZER, usersData);
} }
public async void InsertRecipe(Recipe recipe) public void InsertRecipe(Recipe recipe)
{ {
recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps); recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps);
Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData); Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData);
} }
public async void InsertUser(User user) public void InsertUser(User user)
{ {
usersData[user.Id] = new UserData(user, new Dictionary<Guid, RecipeRate>(), new Dictionary<Guid, uint>()); usersData[user.Id] = new UserData(user, new Dictionary<Guid, RecipeRate>(), new Dictionary<Guid, uint>());
Save(USERS_FILENAME, USERS_SERIALIZER, usersData); Save(USERS_FILENAME, USERS_SERIALIZER, usersData);
} }
public async void InsertRate(Guid userId, Guid recipeId, RecipeRate rate) public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate)
{ {
usersData[userId].Rates[recipeId] = rate; usersData[userId].Rates[recipeId] = rate;
Save(USERS_FILENAME, USERS_SERIALIZER, usersData); Save(USERS_FILENAME, USERS_SERIALIZER, usersData);
} }
public async void RemoveRecipe(Guid id) public void RemoveRecipe(Guid id)
{ {
recipesData.Remove(id); recipesData.Remove(id);
Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData); Save(RECIPES_FILENAME, RECIPES_SERIALIZER, recipesData);
} }
public ImmutableList<Recipe> ListAllRecipes() public ImmutableList<Recipe> ListAllRecipes()
{ {
return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe); return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe);
} }
public ImmutableDictionary<Guid, RecipeRate> ListRatesOf(Guid user) public ImmutableDictionary<Guid, RecipeRate> ListRatesOf(Guid user)
{ {
return usersData[user].Rates.ToImmutableDictionary(); return usersData[user].Rates.ToImmutableDictionary();
} }
public ImmutableDictionary<Guid, uint> GetRecipeListOf(Guid user) public ImmutableDictionary<Guid, uint> GetRecipeListOf(Guid user)
{ {
return usersData[user].RecipesList.ToImmutableDictionary(); return usersData[user].RecipesList.ToImmutableDictionary();
} }
private Recipe ConvertRecipeDataToRecipe(RecipeData rd) private Recipe ConvertRecipeDataToRecipe(RecipeData rd)
{ {
var owner = usersData[rd.OwnerID].User; var owner = usersData[rd.OwnerID].User;
return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps); return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps);
} }
private Dictionary<K, V> Load<K, V>(string fileName, DataContractSerializer deserializer) private Dictionary<K, V> Load<K, V>(string fileName, DataContractSerializer deserializer)
{ {
var file = dbPath + "/" + fileName; var file = dbPath + "/" + fileName;
var fileInfo = new FileInfo(file); var fileInfo = new FileInfo(file);
if (!fileInfo.Exists) if (!fileInfo.Exists)
fileInfo.Create(); fileInfo.Create();
if (fileInfo.Length == 0) if (fileInfo.Length == 0)
return new Dictionary<K, V>(); //file is empty thus there is nothing to deserialize return new Dictionary<K, V>(); //file is empty thus there is nothing to deserialize
string text = File.ReadAllText(file); string text = File.ReadAllText(file);
return JsonSerializer.Deserialize<Dictionary<K, V>>(text); return JsonSerializer.Deserialize<Dictionary<K, V>>(text);
} }
private async void Save<K, T>(string fileName, DataContractSerializer serializer, Dictionary<K, T> dict) private async void Save<K, T>(string fileName, DataContractSerializer serializer, Dictionary<K, T> dict)
{ {
string json = JsonSerializer.Serialize(dict); string json = JsonSerializer.Serialize(dict);
File.WriteAllText(dbPath + "/" + fileName, json); 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.");
}
}
}

Loading…
Cancel
Save