diff options
author | vman <public@vaughnnugent.com> | 2022-11-30 14:59:09 -0500 |
---|---|---|
committer | vman <public@vaughnnugent.com> | 2022-11-30 14:59:09 -0500 |
commit | c9d9e6d23ad7b6fdf25f30de9b4a84be23885e16 (patch) | |
tree | 6f8336e55da2b06bfac2204510bf661dfa1a1476 /VNLib.Plugins.Extensions.Data/Storage | |
parent | e8a846c83ca9922761db56373bc93fe4ea3f4021 (diff) |
Project cleanup + analyzer updates
Diffstat (limited to 'VNLib.Plugins.Extensions.Data/Storage')
9 files changed, 1222 insertions, 0 deletions
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 +{ + /// <summary> + /// Represents a stream of arbitrary binary data + /// </summary> + public class Blob : BackingStream<FileStream>, IObjectStorage, IAsyncExclusiveResource + { + protected readonly LWStorageDescriptor Descriptor; + + /// <summary> + /// The current blob's unique ID + /// </summary> + public string BlobId => Descriptor.DescriptorID; + /// <summary> + /// A value indicating if the <see cref="Blob"/> has been modified + /// </summary> + public bool Modified { get; protected set; } + /// <summary> + /// A valid indicating if the blob was flagged for deletiong + /// </summary> + public bool Deleted { get; protected set; } + + /// <summary> + /// The name of the file (does not change the actual file system name) + /// </summary> + public string Name + { + get => Descriptor.GetName(); + set => Descriptor.SetName(value); + } + /// <summary> + /// The UTC time the <see cref="Blob"/> was last modified + /// </summary> + public DateTimeOffset LastWriteTimeUtc => Descriptor.LastModified; + /// <summary> + /// The UTC time the <see cref="Blob"/> was created + /// </summary> + public DateTimeOffset CreationTimeUtc => Descriptor.Created; + + internal Blob(LWStorageDescriptor descriptor, in FileStream file) + { + this.Descriptor = descriptor; + base.BaseStream = file; + } + + /// <summary> + /// Prevents other processes from reading from or writing to the <see cref="Blob"/> + /// </summary> + /// <param name="position">The begining position of the range to lock</param> + /// <param name="length">The range to be locked</param> + /// <exception cref="IOException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Lock(long position, long length) => BaseStream.Lock(position, length); + /// <summary> + /// Prevents other processes from reading from or writing to the <see cref="Blob"/> + /// </summary> + /// <exception cref="IOException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Lock() => BaseStream.Lock(0, BaseStream.Length); + /// <summary> + /// Allows access by other processes to all or part of the <see cref="Blob"/> that was previously locked + /// </summary> + /// <param name="position">The begining position of the range to unlock</param> + /// <param name="length">The range to be unlocked</param> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Unlock(long position, long length) => BaseStream.Unlock(position, length); + /// <summary> + /// Allows access by other processes to the entire <see cref="Blob"/> + /// </summary> + /// <exception cref="ArgumentOutOfRangeException"></exception> + [UnsupportedOSPlatform("ios")] + [UnsupportedOSPlatform("macos")] + [UnsupportedOSPlatform("tvos")] + public void Unlock() => BaseStream.Unlock(0, BaseStream.Length); + ///<inheritdoc/> + public override void SetLength(long value) + { + base.SetLength(value); + //Set modified flag + Modified |= true; + } + + /* + * Capture on-write calls to set the modified flag + */ + ///<inheritdoc/> + protected override void OnWrite(int count) => Modified |= true; + + T IObjectStorage.GetObject<T>(string key) => ((IObjectStorage)Descriptor).GetObject<T>(key); + void IObjectStorage.SetObject<T>(string key, T obj) => ((IObjectStorage)Descriptor).SetObject(key, obj); + + public string this[string index] + { + get => Descriptor[index]; + set => Descriptor[index] = value; + } + + + /// <summary> + /// Marks the file for deletion and will be deleted when the <see cref="Blob"/> is disposed + /// </summary> + public void Delete() + { + //Set deleted flag + Deleted |= true; + Descriptor.Delete(); + } + ///<inheritdoc/> + public bool IsReleased => Descriptor.IsReleased; + + + /// <summary> + /// <para> + /// If the <see cref="Blob"/> was opened with writing enabled, + /// and file was modified, changes are flushed to the backing store + /// and the stream is set to readonly. + /// </para> + /// <para> + /// 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 <see cref="NotSupportedException"/> + /// </para> + /// </summary> + /// <returns>A <see cref="ValueTask"/> that may be awaited until the operation completes</returns> + /// <remarks> + /// This method may be called to avoid flushing changes to the backing store + /// when the <see cref="Blob"/> is disposed (i.e. lifetime is manged outside of the desired scope) + /// </remarks> + /// <exception cref="IOException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="InvalidOperationException"></exception> + 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 + */ + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + await ReleaseAsync(); + GC.SuppressFinalize(this); + } + ///<inheritdoc/> + 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]; + /// <summary> + /// Gets the <see cref="Version"/> stored in the current <see cref="Blob"/> + /// </summary> + /// <returns>The sored version if previously set, thows otherwise</returns> + /// <exception cref="FormatException"></exception> + public static Version GetVersion(this Blob blob) => Version.Parse(blob[VERSION_ENTRY]); + /// <summary> + /// Sets a <see cref="Version"/> for the current <see cref="Blob"/> + /// </summary> + /// <param name="blob"></param> + /// <param name="version">The <see cref="Version"/> of the <see cref="Blob"/></param> + public static void SetVersion(this Blob blob, Version version) => blob[VERSION_ENTRY] = version.ToString(); + + /// <summary> + /// Gets a value indicating if the last operation left the <see cref="Blob"/> in an undefined state + /// </summary> + /// <returns>True if the <see cref="Blob"/> state is undefined, false otherwise</returns> + 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<long>(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 +{ + + /// <summary> + /// Stores <see cref="Blob"/>s to the local file system backed with a single table <see cref="LWStorageManager"/> + /// that tracks changes + /// </summary> + public class BlobStore + { + /// <summary> + /// The root directory all blob files are stored + /// </summary> + public DirectoryInfo RootDir { get; } + /// <summary> + /// The backing store for blob meta-data + /// </summary> + protected LWStorageManager BlobTable { get; } + /// <summary> + /// Creates a new <see cref="BlobStore"/> that accesses files + /// within the specified root directory. + /// </summary> + /// <param name="rootDir">The root directory containing the blob file contents</param> + /// <param name="blobStoreMan">The db backing store</param> + 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); + } + + /// <summary> + /// Opens an existing <see cref="Blob"/> from the current store + /// </summary> + /// <param name="fileId">The id of the file being requested</param> + /// <param name="access">Access level of the file</param> + /// <param name="share">The sharing option of the underlying file</param> + /// <param name="bufferSize">The size of the file buffer</param> + /// <returns>If found, the requested <see cref="Blob"/>, null otherwise. Throws exceptions if the file is opened in a non-sharable state</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="UnauthorizedAccessException"></exception> + /// <exception cref="UndefinedBlobStateException"></exception> + public virtual async Task<Blob> 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; + } + } + + /// <summary> + /// Creates a new <see cref="Blob"/> for the specified file sharing permissions + /// </summary> + /// <param name="name">The name of the original file</param> + /// <param name="share">The blob sharing permissions</param> + /// <param name="bufferSize"></param> + /// <returns>The newly created <see cref="Blob"/></returns> + /// <exception cref="IoExtensions"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="UnauthorizedAccessException"></exception> + public virtual async Task<Blob> 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 +{ + /// <summary> + /// Raised when an operation to create a new <see cref="LWStorageDescriptor"/> + /// fails + /// </summary> + public class LWDescriptorCreationException : Exception + { + ///<inheritdoc/> + public LWDescriptorCreationException() + {} + ///<inheritdoc/> + public LWDescriptorCreationException(string? message) : base(message) + {} + ///<inheritdoc/> + 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 +{ + /// <summary> + /// Represents an open storage object, that when released or disposed, will flush its changes to the underlying table + /// for which this descriptor represents + /// </summary> + public sealed class LWStorageDescriptor : AsyncUpdatableResource, IObjectStorage, IEnumerable<KeyValuePair<string, string>>, IIndexable<string, string> + { + + 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<string, string> StringStorage; + + /// <summary> + /// The currnt descriptor's identifier string within its backing table. Usually the primary key. + /// </summary> + public string DescriptorID { get; init; } + /// <summary> + /// The identifier of the user for which this descriptor belongs to + /// </summary> + public string UserID { get; init; } + /// <summary> + /// The <see cref="DateTime"/> when the descriptor was created + /// </summary> + public DateTimeOffset Created { get; init; } + /// <summary> + /// The last time this descriptor was updated + /// </summary> + public DateTimeOffset LastModified { get; init; } + + ///<inheritdoc/> + protected override AsyncUpdateCallback UpdateCb { get; } + ///<inheritdoc/> + protected override AsyncDeleteCallback DeleteCb { get; } + ///<inheritdoc/> + 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<Dictionary<string,string>>(data, SerializerOptions); + } + //Ignore a json exceton, a new store will be generated + catch (JsonException) + { } + StringStorage ??= new(); + } + + /// <inheritdoc/> + /// <exception cref="JsonException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public T? GetObject<T>(string key) + { + //De-serialize and return object + return StringStorage.TryGetValue(key, out string? val) ? val.AsJsonObject<T>(SerializerOptions) : default; + } + + /// <inheritdoc/> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + public void SetObject<T>(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); + } + } + + + /// <summary> + /// Gets a string value from string storage matching a given key + /// </summary> + /// <param name="key">Key for storage</param> + /// <returns>Value associaetd with key if exists, <see cref="string.Empty"/> otherwise</returns> + /// <exception cref="ArgumentNullException">If key is null</exception> + /// <exception cref="ObjectDisposedException"></exception> + public string GetStringValue(string key) + { + Check(); + return StringStorage.TryGetValue(key, out string? val) ? val : string.Empty; + } + + /// <summary> + /// Creates, overwrites, or removes a string value identified by key. + /// </summary> + /// <param name="key">Entry key</param> + /// <param name="value">String to store or overwrite, set to null or string.Empty to remove a property</param> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentNullException">If key is null</exception> + 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; + } + } + + /// <summary> + /// Gets or sets a string value from string storage matching a given key + /// </summary> + /// <param name="key">Key for storage</param> + /// <returns>Value associaetd with key if exists, <seealso cref="string.Empty "/> otherwise</returns> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentNullException">If key is null</exception> + public string this[string key] + { + get => GetStringValue(key); + set => SetStringValue(key, value); + } + + /// <summary> + /// Flushes all pending changes to the backing store asynchronously + /// </summary> + /// <exception cref="ObjectDisposedException"></exception> + public ValueTask WritePendingChangesAsync() + { + Check(); + return Modified ? (new(FlushPendingChangesAsync())) : ValueTask.CompletedTask; + } + ///<inheritdoc/> + public IEnumerator<KeyValuePair<string, string>> GetEnumerator() => StringStorage.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + ///<inheritdoc/> + 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 +{ + + /// <summary> + /// Provides single table database object storage services + /// </summary> + public sealed class LWStorageManager : EnumerableTable<LWStorageDescriptor> + { + 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; + + /// <summary> + /// The generator function that is invoked when a new <see cref="LWStorageDescriptor"/> is to + /// be created without an explicit id + /// </summary> + public Func<string> NewDescriptorIdGenerator { get; init; } = static () => Guid.NewGuid().ToString("N"); + + /// <summary> + /// Creates a new <see cref="LWStorageManager"/> with + /// </summary> + /// <param name="factory">A <see cref="DbConnection"/> factory function that will generate and open connections to a database</param> + /// <param name="tableName">The name of the table to operate on</param> + /// <param name="pkCharSize">The maximum number of characters of the DescriptorID and </param> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public LWStorageManager(Func<DbConnection> 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); + } + + /// <summary> + /// Creates a new <see cref="LWStorageDescriptor"/> fror a given user + /// </summary> + /// <param name="userId">Id of user</param> + /// <param name="descriptorIdOverride">An override to specify the new descriptor's id</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>A new <see cref="LWStorageDescriptor"/> if successfully created, null otherwise</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="LWDescriptorCreationException"></exception> + public async Task<LWStorageDescriptor> 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; + } + /// <summary> + /// Attempts to retrieve <see cref="LWStorageDescriptor"/> for a given user-id. The caller is responsible for + /// consitancy state of the descriptor + /// </summary> + /// <param name="userid">User's id</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns> + /// <exception cref="ArgumentNullException"></exception> + public async Task<LWStorageDescriptor?> 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); + } + } + /// <summary> + /// Attempts to retrieve the <see cref="LWStorageDescriptor"/> for the given descriptor id. The caller is responsible for + /// consitancy state of the descriptor + /// </summary> + /// <param name="descriptorId">Unique identifier for the descriptor</param> + /// <returns>The descriptor belonging to the user, or null if not found or error occurs</returns> + /// <exception cref="ArgumentNullException"></exception> + public async Task<LWStorageDescriptor?> 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); + } + } + /// <summary> + /// Cleanup entries before the specified <see cref="TimeSpan"/>. Entires are store in UTC time + /// </summary> + /// <param name="compareTime">Time before <see cref="DateTime.UtcNow"/> to compare against</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The number of entires cleaned</returns>S + public Task<ERRNO> CleanupTableAsync(TimeSpan compareTime, CancellationToken cancellation = default) => CleanupTableAsync(DateTime.UtcNow.Subtract(compareTime), cancellation); + /// <summary> + /// Cleanup entries before the specified <see cref="DateTime"/>. Entires are store in UTC time + /// </summary> + /// <param name="compareTime">UTC time to compare entires against</param> + /// <param name="cancellation">A token to cancel the operation</param> + /// <returns>The number of entires cleaned</returns> + public async Task<ERRNO> 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; + } + + /// <summary> + /// Updates a descriptor's data field + /// </summary> + /// <param name="descriptorObj">Descriptor to update</param> + /// <param name="data">Data string to store to descriptor record</param> + /// <exception cref="LWStorageUpdateFailedException"></exception> + 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); + } + } + /// <summary> + /// Function to remove the specified descriptor + /// </summary> + /// <param name="descriptorObj">The active descriptor to remove from the database</param> + /// <exception cref="LWStorageRemoveFailedException"></exception> + 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); + } + } + + ///<inheritdoc/> + protected async override Task<LWStorageDescriptor> 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; + } + ///<inheritdoc/> + 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 +{ + /// <summary> + /// The exception raised when an open <see cref="LWStorageDescriptor"/> removal operation fails. The + /// <see cref="Exception.InnerException"/> property may contain any nested exceptions that caused the removal to fail. + /// </summary> + 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 +{ + /// <summary> + /// The exception raised when an open <see cref="LWStorageDescriptor"/> update operation fails. The + /// <see cref="Exception.InnerException"/> property may contain any nested exceptions that caused the update to fail. + /// </summary> + 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 +{ + /// <summary> + /// Raised to signal that the requested <see cref="Blob"/> was left in an undefined state + /// when previously accessed + /// </summary> + 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 |