aboutsummaryrefslogtreecommitdiff
path: root/VNLib.Plugins.Extensions.Data/Storage
diff options
context:
space:
mode:
authorLibravatar vman <public@vaughnnugent.com>2022-11-30 14:59:09 -0500
committerLibravatar vman <public@vaughnnugent.com>2022-11-30 14:59:09 -0500
commitc9d9e6d23ad7b6fdf25f30de9b4a84be23885e16 (patch)
tree6f8336e55da2b06bfac2204510bf661dfa1a1476 /VNLib.Plugins.Extensions.Data/Storage
parente8a846c83ca9922761db56373bc93fe4ea3f4021 (diff)
Project cleanup + analyzer updates
Diffstat (limited to 'VNLib.Plugins.Extensions.Data/Storage')
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/Blob.cs244
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/BlobExtensions.cs67
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/BlobStore.cs162
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWDecriptorCreationException.cs45
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWStorageDescriptor.cs204
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWStorageManager.cs379
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWStorageRemoveFailedException.cs38
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/LWStorageUpdateFailedException.cs38
-rw-r--r--VNLib.Plugins.Extensions.Data/Storage/UndefinedBlobStateException.cs45
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