You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
6.6 KiB
186 lines
6.6 KiB
using Models;
|
|
using System.Collections.Immutable;
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace LocalServices.Data
|
|
{
|
|
/// <summary>
|
|
/// Database implementation with catastrophic performances.
|
|
/// This database implementation persists data in json and will save all the data in their files for each mutable requests.
|
|
/// </summary>
|
|
public class CatastrophicPerformancesDatabase : IDatabase
|
|
{
|
|
|
|
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<Guid, RecipeData> recipesData;
|
|
private readonly Dictionary<Guid, UserData> usersData;
|
|
private readonly Dictionary<string, AccountData> accountsData;
|
|
|
|
private readonly string dbPath;
|
|
|
|
|
|
public CatastrophicPerformancesDatabase(string folderPath)
|
|
{
|
|
dbPath = folderPath;
|
|
if (!Directory.Exists(folderPath))
|
|
Directory.CreateDirectory(folderPath);
|
|
|
|
usersData = Load<Guid, UserData>(USERS_FILENAME);
|
|
recipesData = Load<Guid, RecipeData>(RECIPES_FILENAME);
|
|
accountsData = Load<string, AccountData>(ACCOUNTS_FILENAME);
|
|
}
|
|
|
|
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, accountsData);
|
|
InsertUser(account.User);
|
|
}
|
|
|
|
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, usersData);
|
|
}
|
|
|
|
public void RemoveFromUserList(Guid userId, Guid recipeId)
|
|
{
|
|
usersData[userId].RecipesList.Remove(recipeId);
|
|
Save(USERS_FILENAME, usersData);
|
|
}
|
|
|
|
|
|
public void InsertRecipe(Recipe recipe)
|
|
{
|
|
recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps);
|
|
Save(RECIPES_FILENAME, recipesData);
|
|
}
|
|
|
|
public void InsertUser(User user)
|
|
{
|
|
usersData[user.Id] = new UserData(user, new Dictionary<Guid, RecipeRate>(), new Dictionary<Guid, uint>());
|
|
Save(USERS_FILENAME, usersData);
|
|
}
|
|
|
|
public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate)
|
|
{
|
|
usersData[userId].Rates[recipeId] = rate;
|
|
Save(USERS_FILENAME, usersData);
|
|
}
|
|
|
|
public void RemoveRecipe(Guid id)
|
|
{
|
|
recipesData.Remove(id);
|
|
Save(RECIPES_FILENAME, recipesData);
|
|
}
|
|
|
|
public ImmutableList<Recipe> ListAllRecipes()
|
|
{
|
|
return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe);
|
|
}
|
|
|
|
|
|
public ImmutableDictionary<Guid, RecipeRate> ListRatesOf(Guid user)
|
|
{
|
|
return usersData[user].Rates.ToImmutableDictionary();
|
|
}
|
|
|
|
public ImmutableDictionary<Guid, uint> 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<K, V> Load<K, V>(string fileName)
|
|
{
|
|
var file = dbPath + "/" + fileName;
|
|
var fileInfo = new FileInfo(file);
|
|
|
|
if (!fileInfo.Exists)
|
|
fileInfo.Create();
|
|
|
|
if (fileInfo.Length == 0)
|
|
return new Dictionary<K, V>(); //file is empty thus there is nothing to deserialize
|
|
|
|
string text = File.ReadAllText(file);
|
|
|
|
return JsonSerializer.Deserialize<Dictionary<K, V>>(text);
|
|
}
|
|
|
|
private async void Save<K, T>(string fileName, Dictionary<K, T> dict)
|
|
{
|
|
string json = JsonSerializer.Serialize(dict);
|
|
using (var stream = WaitForFile(dbPath + "/" + 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 < 40; attempt++)
|
|
{
|
|
FileStream? fs = null;
|
|
try
|
|
{
|
|
fs = new FileStream(fullPath, mode, access, share);
|
|
return fs;
|
|
}
|
|
catch (FileNotFoundException e)
|
|
{
|
|
throw e;
|
|
}
|
|
catch (IOException e)
|
|
{
|
|
Debug.WriteLine(e + " in thread " + Thread.CurrentThread);
|
|
if (fs != null)
|
|
fs.Dispose();
|
|
|
|
Thread.Sleep(100);
|
|
}
|
|
}
|
|
throw new TimeoutException("Could not access file '" + fullPath + "', maximum attempts reached.");
|
|
}
|
|
}
|
|
}
|