/*
* Copyright (c) 2022 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Extensions.Data
* File: LWStorageManager.cs
*
* LWStorageManager.cs is part of VNLib.Plugins.Extensions.Data which is part of the larger
* VNLib collection of libraries and utilities.
*
* VNLib.Plugins.Extensions.Data is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 2 of the License,
* or (at your option) any later version.
*
* VNLib.Plugins.Extensions.Data is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VNLib.Plugins.Extensions.Data. If not, see http://www.gnu.org/licenses/.
*/
using System;
using System.IO;
using System.Data;
using System.Linq;
using System.Threading;
using System.IO.Compression;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using VNLib.Utils;
using VNLib.Utils.IO;
using VNLib.Utils.Memory;
namespace VNLib.Plugins.Extensions.Data.Storage
{
///
/// Provides single table database object storage services
///
public sealed class LWStorageManager
{
///
/// The generator function that is invoked when a new is to
/// be created without an explicit id
///
public Func NewDescriptorIdGenerator { get; init; } = static () => Guid.NewGuid().ToString("N");
private readonly DbContextOptions DbOptions;
private readonly string TableName;
private LWStorageContext GetContext() => new(DbOptions, TableName);
///
/// Creates a new with
///
/// The db context options to create database connections with
/// The name of the table to operate on
///
public LWStorageManager(DbContextOptions options, string tableName)
{
DbOptions = options ?? throw new ArgumentNullException(nameof(options));
TableName = tableName ?? throw new ArgumentNullException(nameof(tableName));
}
///
/// Creates a new fror a given user
///
/// Id of user
/// An override to specify the new descriptor's id
/// A token to cancel the operation
/// A new if successfully created, null otherwise
///
///
public async Task CreateDescriptorAsync(string userId, string? descriptorIdOverride = null, CancellationToken cancellation = default)
{
if (string.IsNullOrWhiteSpace(userId))
{
throw new ArgumentNullException(nameof(userId));
}
//If no override id was specified, generate a new one
descriptorIdOverride ??= NewDescriptorIdGenerator();
DateTime createdOrModifedTime = DateTime.UtcNow;
await using LWStorageContext ctx = GetContext();
await ctx.OpenTransactionAsync(cancellation);
//Make sure the descriptor doesnt exist only by its descriptor id
if (await ctx.Descriptors.AnyAsync(d => d.Id == descriptorIdOverride, cancellation))
{
throw new LWDescriptorCreationException($"A descriptor with id {descriptorIdOverride} already exists");
}
//Cache time
DateTime now = DateTime.UtcNow;
//Create the new descriptor
LWStorageEntry entry = new()
{
Created = now,
LastModified = now,
Id = descriptorIdOverride,
UserId = userId,
};
//Add and save changes
ctx.Descriptors.Add(entry);
ERRNO result = await ctx.SaveChangesAsync(cancellation);
if (!result)
{
//Rollback and raise exception
await ctx.RollbackTransctionAsync(cancellation);
throw new LWDescriptorCreationException("Failed to create descriptor, because changes could not be saved");
}
else
{
//Commit transaction and return the new descriptor
await ctx.CommitTransactionAsync(cancellation);
return new LWStorageDescriptor(this, entry);
}
}
///
/// Attempts to retrieve for a given user-id. The caller is responsible for
/// consitancy state of the descriptor
///
/// User's id
/// A token to cancel the operation
/// The descriptor belonging to the user, or null if not found or error occurs
///
public async Task GetDescriptorFromUIDAsync(string userid, CancellationToken cancellation = default)
{
//Allow null/empty entrys to just return null
if (string.IsNullOrWhiteSpace(userid))
{
throw new ArgumentNullException(nameof(userid));
}
//Init db
await using LWStorageContext db = GetContext();
//Begin transaction
await db.OpenTransactionAsync(cancellation);
//Get entry
LWStorageEntry? entry = await (from s in db.Descriptors
where s.UserId == userid
select s)
.SingleOrDefaultAsync(cancellation);
//Close transactions and return
if (entry == null)
{
await db.RollbackTransctionAsync(cancellation);
return null;
}
else
{
await db.CommitTransactionAsync(cancellation);
return new (this, entry);
}
}
///
/// Attempts to retrieve the for the given descriptor id. The caller is responsible for
/// consitancy state of the descriptor
///
/// Unique identifier for the descriptor
/// A token to cancel the opreeaiton
/// The descriptor belonging to the user, or null if not found or error occurs
///
public async Task GetDescriptorFromIDAsync(string descriptorId, CancellationToken cancellation = default)
{
//Allow null/empty entrys to just return null
if (string.IsNullOrWhiteSpace(descriptorId))
{
throw new ArgumentNullException(nameof(descriptorId));
}
//Init db
await using LWStorageContext db = GetContext();
//Begin transaction
await db.OpenTransactionAsync(cancellation);
//Get entry
LWStorageEntry? entry = await (from s in db.Descriptors
where s.Id == descriptorId
select s)
.SingleOrDefaultAsync(cancellation);
//Close transactions and return
if (entry == null)
{
await db.RollbackTransctionAsync(cancellation);
return null;
}
else
{
await db.CommitTransactionAsync(cancellation);
return new (this, entry);
}
}
///
/// Cleanup entries before the specified . Entires are store in UTC time
///
/// Time before to compare against
/// A token to cancel the operation
/// The number of entires cleanedS
public Task CleanupTableAsync(TimeSpan compareTime, CancellationToken cancellation = default) => CleanupTableAsync(DateTime.UtcNow.Subtract(compareTime), cancellation);
///
/// Cleanup entries before the specified . Entires are store in UTC time
///
/// UTC time to compare entires against
/// A token to cancel the operation
/// The number of entires cleaned
public async Task CleanupTableAsync(DateTime compareTime, CancellationToken cancellation = default)
{
//Init db
await using LWStorageContext db = GetContext();
//Begin transaction
await db.OpenTransactionAsync(cancellation);
//Get all expired entires
LWStorageEntry[] expired = await (from s in db.Descriptors
where s.Created < compareTime
select s)
.ToArrayAsync(cancellation);
//Delete
db.Descriptors.RemoveRange(expired);
//Save changes
ERRNO count = await db.SaveChangesAsync(cancellation);
//Commit transaction
await db.CommitTransactionAsync(cancellation);
return count;
}
///
/// Updates a descriptor's data field
///
/// Descriptor to update
/// Data string to store to descriptor record
///
internal async Task UpdateDescriptorAsync(object descriptorObj, Stream data)
{
LWStorageEntry entry = (descriptorObj as LWStorageDescriptor)!.Entry;
ERRNO result = 0;
try
{
await using LWStorageContext ctx = GetContext();
await ctx.OpenTransactionAsync(CancellationToken.None);
//Begin tracking
ctx.Descriptors.Attach(entry);
//Convert stream to vnstream
VnMemoryStream vms = (VnMemoryStream)data;
using (IMemoryHandle encBuffer = Memory.SafeAlloc((int)vms.Length))
{
//try to compress
if(!BrotliEncoder.TryCompress(vms.AsSpan(), encBuffer.Span, out int compressed))
{
throw new InvalidDataException("Failed to compress the descriptor data");
}
//Set the data
entry.Data = encBuffer.Span.ToArray();
}
//Update modified time
entry.LastModified = DateTime.UtcNow;
//Save changes
result = await ctx.SaveChangesAsync(CancellationToken.None);
//Commit or rollback
if (result)
{
await ctx.CommitTransactionAsync(CancellationToken.None);
}
else
{
await ctx.RollbackTransctionAsync(CancellationToken.None);
}
}
catch (Exception ex)
{
throw new LWStorageUpdateFailedException("", ex);
}
//If the result is 0 then the update failed
if (!result)
{
throw new LWStorageUpdateFailedException($"Descriptor {entry.Id} failed to update");
}
}
///
/// Function to remove the specified descriptor
///
/// The active descriptor to remove from the database
///
internal async Task RemoveDescriptorAsync(object descriptorObj)
{
LWStorageEntry descriptor = (descriptorObj as LWStorageDescriptor)!.Entry;
ERRNO result;
try
{
//Init db
await using LWStorageContext db = GetContext();
//Begin transaction
await db.OpenTransactionAsync();
//Delete the user from the database
db.Descriptors.Remove(descriptor);
//Save changes and commit if successful
result = await db.SaveChangesAsync();
if (result)
{
await db.CommitTransactionAsync();
}
else
{
await db.RollbackTransctionAsync();
}
}
catch (Exception ex)
{
throw new LWStorageRemoveFailedException("", ex);
}
if (!result)
{
throw new LWStorageRemoveFailedException("Failed to delete the user account because of a database failure, the user may already be deleted");
}
}
}
}