From c9d9e6d23ad7b6fdf25f30de9b4a84be23885e16 Mon Sep 17 00:00:00 2001 From: vman Date: Wed, 30 Nov 2022 14:59:09 -0500 Subject: Project cleanup + analyzer updates --- VNLib.Plugins.Extensions.Data/Storage/Blob.cs | 244 +++++++++++++ .../Storage/BlobExtensions.cs | 67 ++++ VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs | 162 +++++++++ .../Storage/LWDecriptorCreationException.cs | 45 +++ .../Storage/LWStorageDescriptor.cs | 204 +++++++++++ .../Storage/LWStorageManager.cs | 379 +++++++++++++++++++++ .../Storage/LWStorageRemoveFailedException.cs | 38 +++ .../Storage/LWStorageUpdateFailedException.cs | 38 +++ .../Storage/UndefinedBlobStateException.cs | 45 +++ 9 files changed, 1222 insertions(+) create mode 100644 VNLib.Plugins.Extensions.Data/Storage/Blob.cs create mode 100644 VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs create mode 100644 VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs create mode 100644 VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs create mode 100644 VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs create mode 100644 VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs create mode 100644 VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs create mode 100644 VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs create mode 100644 VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs (limited to 'VNLib.Plugins.Extensions.Data/Storage') diff --git a/VNLib.Plugins.Extensions.Data/Storage/Blob.cs b/VNLib.Plugins.Extensions.Data/Storage/Blob.cs new file mode 100644 index 0000000..ab18eeb --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/Blob.cs @@ -0,0 +1,244 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: Blob.cs +* +* Blob.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.Threading.Tasks; +using System.Runtime.Versioning; + +using VNLib.Utils; +using VNLib.Utils.IO; +using VNLib.Utils.Async; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// + /// Represents a stream of arbitrary binary data + /// + public class Blob : BackingStream, IObjectStorage, IAsyncExclusiveResource + { + protected readonly LWStorageDescriptor Descriptor; + + /// + /// The current blob's unique ID + /// + public string BlobId => Descriptor.DescriptorID; + /// + /// A value indicating if the has been modified + /// + public bool Modified { get; protected set; } + /// + /// A valid indicating if the blob was flagged for deletiong + /// + public bool Deleted { get; protected set; } + + /// + /// The name of the file (does not change the actual file system name) + /// + public string Name + { + get => Descriptor.GetName(); + set => Descriptor.SetName(value); + } + /// + /// The UTC time the was last modified + /// + public DateTimeOffset LastWriteTimeUtc => Descriptor.LastModified; + /// + /// The UTC time the was created + /// + public DateTimeOffset CreationTimeUtc => Descriptor.Created; + + internal Blob(LWStorageDescriptor descriptor, in FileStream file) + { + this.Descriptor = descriptor; + base.BaseStream = file; + } + + /// + /// Prevents other processes from reading from or writing to the + /// + /// The begining position of the range to lock + /// The range to be locked + /// + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Lock(long position, long length) => BaseStream.Lock(position, length); + /// + /// Prevents other processes from reading from or writing to the + /// + /// + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Lock() => BaseStream.Lock(0, BaseStream.Length); + /// + /// Allows access by other processes to all or part of the that was previously locked + /// + /// The begining position of the range to unlock + /// The range to be unlocked + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Unlock(long position, long length) => BaseStream.Unlock(position, length); + /// + /// Allows access by other processes to the entire + /// + /// + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Unlock() => BaseStream.Unlock(0, BaseStream.Length); + /// + public override void SetLength(long value) + { + base.SetLength(value); + //Set modified flag + Modified |= true; + } + + /* + * Capture on-write calls to set the modified flag + */ + /// + protected override void OnWrite(int count) => Modified |= true; + + T IObjectStorage.GetObject(string key) => ((IObjectStorage)Descriptor).GetObject(key); + void IObjectStorage.SetObject(string key, T obj) => ((IObjectStorage)Descriptor).SetObject(key, obj); + + public string this[string index] + { + get => Descriptor[index]; + set => Descriptor[index] = value; + } + + + /// + /// Marks the file for deletion and will be deleted when the is disposed + /// + public void Delete() + { + //Set deleted flag + Deleted |= true; + Descriptor.Delete(); + } + /// + public bool IsReleased => Descriptor.IsReleased; + + + /// + /// + /// If the was opened with writing enabled, + /// and file was modified, changes are flushed to the backing store + /// and the stream is set to readonly. + /// + /// + /// If calls to this method succeed the stream is placed into a read-only mode + /// which will cause any calls to Write to throw a + /// + /// + /// A that may be awaited until the operation completes + /// + /// This method may be called to avoid flushing changes to the backing store + /// when the is disposed (i.e. lifetime is manged outside of the desired scope) + /// + /// + /// + /// + public async ValueTask FlushChangesAndSetReadonlyAsync() + { + if (Deleted) + { + throw new InvalidOperationException("The blob has been deleted and must be closed!"); + } + if (Modified) + { + //flush the base stream + await BaseStream.FlushAsync(); + //Update the file length in the store + Descriptor.SetLength(BaseStream.Length); + } + //flush changes, this will cause the dispose method to complete synchronously when closing + await Descriptor.WritePendingChangesAsync(); + //Clear modified flag + Modified = false; + //Set to readonly mode + base.ForceReadOnly = true; + } + + + /* + * Override the dispose async to manually dispose the + * base stream and avoid the syncrhonous (OnClose) + * method and allow awaiting the descriptor release + */ + /// + public override async ValueTask DisposeAsync() + { + await ReleaseAsync(); + GC.SuppressFinalize(this); + } + /// + public async ValueTask ReleaseAsync() + { + try + { + //Check for deleted + if (Deleted) + { + //Dispose the base stream explicitly + await BaseStream.DisposeAsync(); + //Try to delete the file + File.Delete(BaseStream.Name); + } + //Check to see if the file was modified + else if (Modified) + { + //Set the file size in bytes + Descriptor.SetLength(BaseStream.Length); + } + } + catch + { + //Set the error flag + Descriptor.IsError(true); + //propagate the exception + throw; + } + finally + { + //Dispose the stream + await BaseStream.DisposeAsync(); + //Release the descriptor + await Descriptor.ReleaseAsync(); + } + } + } +} \ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs b/VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs new file mode 100644 index 0000000..468a66d --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs @@ -0,0 +1,67 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: BlobExtensions.cs +* +* BlobExtensions.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 VNLib.Utils; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + public static class BlobExtensions + { + public const string USER_ID_ENTRY = "__.uid"; + public const string VERSION_ENTRY = "__.vers"; + + private const string FILE_SIZE = "__.size"; + private const string FILE_NAME = "__.name"; + private const string ERROR_FLAG = "__.err"; + + public static string GetUserId(this Blob blob) => blob[USER_ID_ENTRY]; + /// + /// Gets the stored in the current + /// + /// The sored version if previously set, thows otherwise + /// + public static Version GetVersion(this Blob blob) => Version.Parse(blob[VERSION_ENTRY]); + /// + /// Sets a for the current + /// + /// + /// The of the + public static void SetVersion(this Blob blob, Version version) => blob[VERSION_ENTRY] = version.ToString(); + + /// + /// Gets a value indicating if the last operation left the in an undefined state + /// + /// True if the state is undefined, false otherwise + public static bool IsError(this Blob blob) => bool.TrueString.Equals(blob[ERROR_FLAG]); + internal static void IsError(this LWStorageDescriptor blob, bool value) => blob[ERROR_FLAG] = value ? bool.TrueString : null; + + internal static long GetLength(this LWStorageDescriptor blob) => (blob as IObjectStorage).GetObject(FILE_SIZE); + internal static void SetLength(this LWStorageDescriptor blob, long length) => (blob as IObjectStorage).SetObject(FILE_SIZE, length); + + internal static string GetName(this LWStorageDescriptor blob) => blob[FILE_NAME]; + internal static string SetName(this LWStorageDescriptor blob, string filename) => blob[FILE_NAME] = filename; + } +} \ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs b/VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs new file mode 100644 index 0000000..6897516 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs @@ -0,0 +1,162 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: BlobStore.cs +* +* BlobStore.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.Security.Cryptography; +using System.Threading.Tasks; + +using VNLib.Utils.Extensions; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + + /// + /// Stores s to the local file system backed with a single table + /// that tracks changes + /// + public class BlobStore + { + /// + /// The root directory all blob files are stored + /// + public DirectoryInfo RootDir { get; } + /// + /// The backing store for blob meta-data + /// + protected LWStorageManager BlobTable { get; } + /// + /// Creates a new that accesses files + /// within the specified root directory. + /// + /// The root directory containing the blob file contents + /// The db backing store + public BlobStore(DirectoryInfo rootDir, LWStorageManager blobStoreMan) + { + RootDir = rootDir; + BlobTable = blobStoreMan; + } + + private string GetPath(string fileId) => Path.Combine(RootDir.FullName, fileId); + + /* + * Creates a repeatable unique identifier for the file + * name and allows for lookups + */ + internal static string CreateFileHash(string fileName) + { + throw new NotImplementedException(); + //return ManagedHash.ComputeBase64Hash(fileName, HashAlg.SHA1); + } + + /// + /// Opens an existing from the current store + /// + /// The id of the file being requested + /// Access level of the file + /// The sharing option of the underlying file + /// The size of the file buffer + /// If found, the requested , null otherwise. Throws exceptions if the file is opened in a non-sharable state + /// + /// + /// + /// + /// + public virtual async Task OpenBlobAsync(string fileId, FileAccess access, FileShare share, int bufferSize = 4096) + { + //Get the file's data descriptor + LWStorageDescriptor fileDescriptor = await BlobTable.GetDescriptorFromIDAsync(fileId); + //return null if not found + if (fileDescriptor == null) + { + return null; + } + try + { + string fsSafeName = GetPath(fileDescriptor.DescriptorID); + //try to open the file + FileStream file = new(fsSafeName, FileMode.Open, access, share, bufferSize, FileOptions.Asynchronous); + //Create the new blob + return new Blob(fileDescriptor, file); + } + catch (FileNotFoundException) + { + //If the file was not found but the descriptor was, delete the descriptor from the db + fileDescriptor.Delete(); + //Flush changes + await fileDescriptor.ReleaseAsync(); + //return null since this is a desync issue and the file technically does not exist + return null; + } + catch + { + //Release the descriptor and pass the exception + await fileDescriptor.ReleaseAsync(); + throw; + } + } + + /// + /// Creates a new for the specified file sharing permissions + /// + /// The name of the original file + /// The blob sharing permissions + /// + /// The newly created + /// + /// + /// + /// + public virtual async Task CreateBlobAsync(string name, FileShare share = FileShare.None, int bufferSize = 4096) + { + //hash the file name to create a unique id for the file name + LWStorageDescriptor newFile = await BlobTable.CreateDescriptorAsync(CreateFileHash(name)); + //if the descriptor was not created, return null + if (newFile == null) + { + return null; + } + try + { + string fsSafeName = GetPath(newFile.DescriptorID); + //Open/create the new file + FileStream file = new(fsSafeName, FileMode.OpenOrCreate, FileAccess.ReadWrite, share, bufferSize, FileOptions.Asynchronous); + //If the file already exists, make sure its zero'd + file.SetLength(0); + //Save the original name of the file + newFile.SetName(name); + //Create and return the new blob + return new Blob(newFile, file); + } + catch + { + //If an exception occurs, remove the descritor from the db + newFile.Delete(); + await newFile.ReleaseAsync(); + //Pass exception + throw; + } + } + } +} \ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs b/VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs new file mode 100644 index 0000000..db0dbbb --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWDecriptorCreationException.cs +* +* LWDecriptorCreationException.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; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// + /// Raised when an operation to create a new + /// fails + /// + public class LWDescriptorCreationException : Exception + { + /// + public LWDescriptorCreationException() + {} + /// + public LWDescriptorCreationException(string? message) : base(message) + {} + /// + public LWDescriptorCreationException(string? message, Exception? innerException) : base(message, innerException) + {} + } +} \ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs new file mode 100644 index 0000000..3766a97 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs @@ -0,0 +1,204 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWStorageDescriptor.cs +* +* LWStorageDescriptor.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.Text.Json; +using System.Collections; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Utils; +using VNLib.Utils.Async; +using VNLib.Utils.Extensions; +using System.Text.Json.Serialization; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// + /// Represents an open storage object, that when released or disposed, will flush its changes to the underlying table + /// for which this descriptor represents + /// + public sealed class LWStorageDescriptor : AsyncUpdatableResource, IObjectStorage, IEnumerable>, IIndexable + { + + public static readonly JsonSerializerOptions SerializerOptions = new() + { + DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, + NumberHandling = JsonNumberHandling.Strict, + ReadCommentHandling = JsonCommentHandling.Disallow, + WriteIndented = false, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + IgnoreReadOnlyFields = true, + DefaultBufferSize = Environment.SystemPageSize, + }; + + private Dictionary StringStorage; + + /// + /// The currnt descriptor's identifier string within its backing table. Usually the primary key. + /// + public string DescriptorID { get; init; } + /// + /// The identifier of the user for which this descriptor belongs to + /// + public string UserID { get; init; } + /// + /// The when the descriptor was created + /// + public DateTimeOffset Created { get; init; } + /// + /// The last time this descriptor was updated + /// + public DateTimeOffset LastModified { get; init; } + + /// + protected override AsyncUpdateCallback UpdateCb { get; } + /// + protected override AsyncDeleteCallback DeleteCb { get; } + /// + protected override JsonSerializerOptions JSO => SerializerOptions; + + internal LWStorageDescriptor(LWStorageManager manager) + { + UpdateCb = manager.UpdateDescriptorAsync; + DeleteCb = manager.RemoveDescriptorAsync; + } + + internal async ValueTask PrepareAsync(Stream data) + { + try + { + //Deserialze async + StringStorage = await VnEncoding.JSONDeserializeFromBinaryAsync>(data, SerializerOptions); + } + //Ignore a json exceton, a new store will be generated + catch (JsonException) + { } + StringStorage ??= new(); + } + + /// + /// + /// + /// + /// + public T? GetObject(string key) + { + //De-serialize and return object + return StringStorage.TryGetValue(key, out string? val) ? val.AsJsonObject(SerializerOptions) : default; + } + + /// + /// + /// + public void SetObject(string key, T obj) + { + //Remove the object from storage if its null + if (obj == null) + { + SetStringValue(key, null); + } + else + { + //Serialize the object to a string + string value = obj.ToJsonString(SerializerOptions)!; + //Attempt to store string in storage + SetStringValue(key, value); + } + } + + + /// + /// Gets a string value from string storage matching a given key + /// + /// Key for storage + /// Value associaetd with key if exists, otherwise + /// If key is null + /// + public string GetStringValue(string key) + { + Check(); + return StringStorage.TryGetValue(key, out string? val) ? val : string.Empty; + } + + /// + /// Creates, overwrites, or removes a string value identified by key. + /// + /// Entry key + /// String to store or overwrite, set to null or string.Empty to remove a property + /// + /// If key is null + public void SetStringValue(string key, string? value) + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + Check(); + //If the value is null, see if the the properties are null + if (string.IsNullOrWhiteSpace(value)) + { + //If the value is null and properies exist, remove the entry + StringStorage.Remove(key); + Modified |= true; + } + else + { + //Set the value + StringStorage[key] = value; + //Set modified flag + Modified |= true; + } + } + + /// + /// Gets or sets a string value from string storage matching a given key + /// + /// Key for storage + /// Value associaetd with key if exists, otherwise + /// + /// If key is null + public string this[string key] + { + get => GetStringValue(key); + set => SetStringValue(key, value); + } + + /// + /// Flushes all pending changes to the backing store asynchronously + /// + /// + public ValueTask WritePendingChangesAsync() + { + Check(); + return Modified ? (new(FlushPendingChangesAsync())) : ValueTask.CompletedTask; + } + /// + public IEnumerator> GetEnumerator() => StringStorage.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + /// + protected override object GetResource() => StringStorage; + } +} \ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs new file mode 100644 index 0000000..63d41af --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs @@ -0,0 +1,379 @@ +/* +* 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.Threading; +using System.Data.Common; +using System.Threading.Tasks; + +using VNLib.Utils; + +using VNLib.Plugins.Extensions.Data.SQL; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + + /// + /// Provides single table database object storage services + /// + public sealed class LWStorageManager : EnumerableTable + { + const int DTO_SIZE = 7; + const int MAX_DATA_SIZE = 8000; + + //Mssql statments + private const string GET_DESCRIPTOR_STATMENT_ID_MSQQL = "SELECT TOP 1\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE Id=@Id;"; + private const string GET_DESCRIPTOR_STATMENT_UID_MSQL = "SELECT TOP 1\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE UserID=@UserID;"; + + private const string GET_DESCRIPTOR_STATMENT_ID = "SELECT\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE Id=@Id\r\nLIMIT 1;"; + private const string GET_DESCRIPTOR_STATMENT_UID = "SELECT\r\n[Id],\r\n[UserID],\r\n[Data],\r\n[Created],\r\n[LastModified]\r\nFROM @table\r\nWHERE UserID=@UserID\r\nLIMIT 1;"; + + private const string CREATE_DESCRIPTOR_STATMENT = "INSERT INTO @table\r\n(UserID,Id,Created,LastModified)\r\nVALUES (@UserID,@Id,@Created,@LastModified);"; + + private const string UPDATE_DESCRIPTOR_STATMENT = "UPDATE @table\r\nSET [Data]=@Data\r\n,[LastModified]=@LastModified\r\nWHERE Id=@Id;"; + private const string REMOVE_DESCRIPTOR_STATMENT = "DELETE FROM @table\r\nWHERE Id=@Id"; + private const string CLEANUP_STATEMENT = "DELETE FROM @table\r\nWHERE [created]<@timeout;"; + private const string ENUMERATION_STATMENT = "SELECT [Id],\r\n[UserID],\r\n[Data],\r\n[LastModified],\r\n[Created]\r\nFROM @table;"; + + private readonly string GetFromUD; + private readonly string Cleanup; + private readonly int keySize; + + /// + /// 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"); + + /// + /// Creates a new with + /// + /// A factory function that will generate and open connections to a database + /// The name of the table to operate on + /// The maximum number of characters of the DescriptorID and + /// + /// + public LWStorageManager(Func factory, string tableName, int pkCharSize) : base(factory, tableName) + { + //Compile statments with specified tableid + Insert = CREATE_DESCRIPTOR_STATMENT.Replace("@table", tableName); + + //Test connector type to compile MSSQL statments vs Sqlite/Mysql + using (DbConnection testConnection = GetConnection()) + { + //Determine if MSSql connections are being used + bool isMsSql = testConnection.GetType().FullName!.Contains("SqlConnection", StringComparison.OrdinalIgnoreCase); + + if (isMsSql) + { + GetFromUD = GET_DESCRIPTOR_STATMENT_UID_MSQL.Replace("@table", tableName); + Select = GET_DESCRIPTOR_STATMENT_ID_MSQQL.Replace("@table", tableName); + } + else + { + Select = GET_DESCRIPTOR_STATMENT_ID.Replace("@table", tableName); + GetFromUD = GET_DESCRIPTOR_STATMENT_UID.Replace("@table", tableName); + } + } + + Update = UPDATE_DESCRIPTOR_STATMENT.Replace("@table", tableName); + Delete = REMOVE_DESCRIPTOR_STATMENT.Replace("@table", tableName); + Cleanup = CLEANUP_STATEMENT.Replace("@table", tableName); + //Set key size + keySize = pkCharSize; + //Set default generator + Enumerate = ENUMERATION_STATMENT.Replace("@table", 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(); + //Set created time + DateTimeOffset now = DateTimeOffset.UtcNow; + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(cancellation); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellation); + //Create command for text command + await using DbCommand cmd = Database.CreateTextCommand(Insert, transaction); + //add parameters + _ = cmd.AddParameter("@Id", descriptorIdOverride, DbType.String, keySize); + _ = cmd.AddParameter("@UserID", userId, DbType.String, keySize); + _ = cmd.AddParameter("@Created", now, DbType.DateTimeOffset, DTO_SIZE); + _ = cmd.AddParameter("@LastModified", now, DbType.DateTimeOffset, DTO_SIZE); + //Prepare operation + await cmd.PrepareAsync(cancellation); + //Exec and if successful will return > 0, so we can properly return a descriptor + int result = await cmd.ExecuteNonQueryAsync(cancellation); + //Commit transaction + await transaction.CommitAsync(cancellation); + if (result <= 0) + { + throw new LWDescriptorCreationException("Failed to create the new descriptor because the database retuned an invalid update row count"); + } + //Rent new descriptor + LWStorageDescriptor desciptor = new(this) + { + DescriptorID = descriptorIdOverride, + UserID = userId, + Created = now, + LastModified = now + }; + //Set data to null + await desciptor.PrepareAsync(null); + return desciptor; + } + /// + /// 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) + { + if (string.IsNullOrWhiteSpace(userid)) + { + throw new ArgumentNullException(nameof(userid)); + } + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(cancellation); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellation); + //Create a new command based on the command text + await using DbCommand cmd = Database.CreateTextCommand(GetFromUD, transaction); + //Add userid parameter + _ = cmd.AddParameter("@UserID", userid, DbType.String, keySize); + //Prepare operation + await cmd.PrepareAsync(cancellation); + //Get the reader + DbDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellation); + try + { + //Make sure the record was found + if (!await reader.ReadAsync(cancellation)) + { + return null; + } + return await GetItemAsync(reader, CancellationToken.None); + } + finally + { + //Close the reader + await reader.CloseAsync(); + //Commit the transaction + await transaction.CommitAsync(cancellation); + } + } + /// + /// Attempts to retrieve the for the given descriptor id. The caller is responsible for + /// consitancy state of the descriptor + /// + /// Unique identifier for the descriptor + /// 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)); + } + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(cancellation); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellation); + //We dont have the routine stored + await using DbCommand cmd = Database.CreateTextCommand(Select, transaction); + //Set userid (unicode length) + _ = cmd.AddParameter("@Id", descriptorId, DbType.String, keySize); + //Prepare operation + await cmd.PrepareAsync(cancellation); + //Get the reader + DbDataReader reader = await cmd.ExecuteReaderAsync(CommandBehavior.SingleRow, cancellation); + try + { + if (!await reader.ReadAsync(cancellation)) + { + return null; + } + return await GetItemAsync(reader, CancellationToken.None); + } + finally + { + //Close the reader + await reader.CloseAsync(); + //Commit the transaction + await transaction.CommitAsync(cancellation); + } + } + /// + /// 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) + { + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(cancellation); + //Begin a new transaction + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable, cancellation); + //Setup the cleanup command for the current database + await using DbCommand cmd = Database.CreateTextCommand(Cleanup, transaction); + //Setup timeout parameter as a datetime + cmd.AddParameter("@timeout", compareTime, DbType.DateTime); + await cmd.PrepareAsync(cancellation); + //Exec and if successful will return > 0, so we can properly return a descriptor + int result = await cmd.ExecuteNonQueryAsync(cancellation); + //Commit transaction + await transaction.CommitAsync(cancellation); + return result; + } + + /// + /// Updates a descriptor's data field + /// + /// Descriptor to update + /// Data string to store to descriptor record + /// + internal async Task UpdateDescriptorAsync(object descriptorObj, Stream data) + { + LWStorageDescriptor descriptor = (descriptorObj as LWStorageDescriptor)!; + int result = 0; + try + { + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable); + //Create command for stored procedure + await using DbCommand cmd = Database.CreateTextCommand(Update, transaction); + //Add parameters + _ = cmd.AddParameter("@Id", descriptor.DescriptorID, DbType.String, keySize); + _ = cmd.AddParameter("@Data", data, DbType.Binary, MAX_DATA_SIZE); + _ = cmd.AddParameter("@LastModified", DateTime.UtcNow, DbType.DateTime2, DTO_SIZE); + //Prepare operation + await cmd.PrepareAsync(); + //exec and store result + result = await cmd.ExecuteNonQueryAsync(); + //Commit + await transaction.CommitAsync(); + } + catch (Exception ex) + { + throw new LWStorageUpdateFailedException("", ex); + } + //If the result is 0 then the update failed + if (result <= 0) + { + throw new LWStorageUpdateFailedException($"Descriptor {descriptor.DescriptorID} failed to update", null); + } + } + /// + /// Function to remove the specified descriptor + /// + /// The active descriptor to remove from the database + /// + internal async Task RemoveDescriptorAsync(object descriptorObj) + { + LWStorageDescriptor descriptor = (descriptorObj as LWStorageDescriptor)!; + try + { + //Open a new sql client + await using DbConnection Database = GetConnection(); + await Database.OpenAsync(); + //Setup transaction with repeatable read iso level + await using DbTransaction transaction = await Database.BeginTransactionAsync(IsolationLevel.Serializable); + //Create sql command + await using DbCommand cmd = Database.CreateTextCommand(Delete, transaction); + //set descriptor id + _ = cmd.AddParameter("@Id", descriptor.DescriptorID, DbType.String, keySize); + //Prepare operation + await cmd.PrepareAsync(); + //Execute (the descriptor my already be removed, as long as the transaction doesnt fail we should be okay) + _ = await cmd.ExecuteNonQueryAsync(); + //Commit + await transaction.CommitAsync(); + } + catch (Exception ex) + { + throw new LWStorageRemoveFailedException("", ex); + } + } + + /// + protected async override Task GetItemAsync(DbDataReader reader, CancellationToken cancellationToken) + { + //Open binary stream for the data column + await using Stream data = reader.GetStream("Data"); + //Create new descriptor + LWStorageDescriptor desciptor = new(this) + { + //Set desctiptor data + DescriptorID = reader.GetString("Id"), + UserID = reader.GetString("UserID"), + Created = reader.GetDateTime("Created"), + LastModified = reader.GetDateTime("LastModified") + }; + //Load the descriptor's data + await desciptor.PrepareAsync(data); + return desciptor; + } + /// + protected override ValueTask CleanupItemAsync(LWStorageDescriptor item, CancellationToken cancellationToken) + { + return item.ReleaseAsync(); + } + } +} \ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs new file mode 100644 index 0000000..8e36d6c --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWStorageRemoveFailedException.cs +* +* LWStorageRemoveFailedException.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 VNLib.Utils; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// + /// The exception raised when an open removal operation fails. The + /// property may contain any nested exceptions that caused the removal to fail. + /// + public class LWStorageRemoveFailedException : ResourceDeleteFailedException + { + internal LWStorageRemoveFailedException(string error, Exception inner) : base(error, inner) { } + } +} \ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs b/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs new file mode 100644 index 0000000..96ea4eb --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: LWStorageUpdateFailedException.cs +* +* LWStorageUpdateFailedException.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 VNLib.Utils; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// + /// The exception raised when an open update operation fails. The + /// property may contain any nested exceptions that caused the update to fail. + /// + public class LWStorageUpdateFailedException : ResourceUpdateFailedException + { + internal LWStorageUpdateFailedException(string error, Exception inner) : base(error, inner) { } + } +} \ No newline at end of file diff --git a/VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs b/VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs new file mode 100644 index 0000000..e845372 --- /dev/null +++ b/VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs @@ -0,0 +1,45 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Data +* File: UndefinedBlobStateException.cs +* +* UndefinedBlobStateException.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.Runtime.Serialization; + +namespace VNLib.Plugins.Extensions.Data.Storage +{ + /// + /// Raised to signal that the requested was left in an undefined state + /// when previously accessed + /// + public class UndefinedBlobStateException : Exception + { + public UndefinedBlobStateException() + {} + public UndefinedBlobStateException(string message) : base(message) + {} + public UndefinedBlobStateException(string message, Exception innerException) : base(message, innerException) + {} + protected UndefinedBlobStateException(SerializationInfo info, StreamingContext context) : base(info, context) + {} + } +} \ No newline at end of file -- cgit