From 641bdbe75cb0128c09e27f1b92709c86574026ac Mon Sep 17 00:00:00 2001 From: vnugent Date: Sun, 28 Jul 2024 18:52:54 -0400 Subject: Squashed commit of the following: commit 87887c0a45c84458e81fa38f7f4dca31faa49d7c Author: vnugent Date: Sat Jul 27 22:41:17 2024 -0400 package updates commit d52772c010b10357d194239056d19c0d22d414fe Author: vnugent Date: Sat Jul 27 20:50:12 2024 -0400 update secrets and remove deprecated and unused apis commit c8567e58dc1d4135da1f6cefa6fa66af5fcd7b19 Author: vnugent Date: Mon Jul 15 19:05:01 2024 -0400 feat: Smiplify configuration helpers commit 640ee6760c07b628529e3160c16641773c76e800 Author: vnugent Date: Thu Jul 4 23:02:56 2024 -0400 package updates commit db5747a20600a2e2c5e8d915cf0bdbe4ec6df6a2 Author: vnugent Date: Fri Jun 21 17:08:16 2024 -0400 configuration validation updates commit 9bc24801735884e0c03aa00e83804448c466bdf2 Author: vnugent Date: Sun Jun 16 13:16:18 2024 -0400 update plugins array instead of single path commit 1229ed75549de1c56aaee42c921acbd96c4d4c9b Author: vnugent Date: Tue Jun 11 22:13:58 2024 -0400 feat: Stage some mvc stuff commit 35815081df2149741a6a79a880a57d63c5938a34 Author: vnugent Date: Sun Jun 9 13:06:45 2024 -0400 package updates commit bcbe51bef546458cb7fee0d8f1dfd00cf936545a Author: vnugent Date: Fri Jun 7 22:03:19 2024 -0400 feat: Allow S3Config type inheritence commit 2e55ac437f44eb5f9d66f7d7fd47b5670dedc2cb Merge: 27fb538 1350c98 Author: vnugent Date: Wed May 22 15:29:47 2024 -0400 Merge branch 'master' into develop commit 27fb5382d80d9bcfb4c65974bbae20c5e7b8ccbc Author: vnugent Date: Wed May 22 00:57:34 2024 -0400 feat: Vault environment vars commit 69f13e43dfdd8069459800ccc3039f45fc884814 Author: vnugent Date: Wed May 15 22:04:43 2024 -0400 fix: #3 Defer vault loading until a secret actually needs it commit c848787d4830a73e9ba93898897282be2f3752f2 Author: vnugent Date: Wed May 15 22:02:02 2024 -0400 package updates commit 21c6c85f540740ac29536a7091346a731aa85148 Author: vnugent Date: Wed May 15 22:01:16 2024 -0400 fix: #3 Error raised when managed password type disposed commit 8e77289041349b16536497f48f0c0a4ec6fe30f5 Author: vnugent Date: Thu May 2 15:44:42 2024 -0400 feat: #2 Middleware helpers, proj cleanup, fix sync secrets, vault client commit e0a5c85297516188e57b54d9b530b2482cb03eb0 Merge: a977dab 5ad520e Author: vnugent Date: Sat Apr 27 17:44:09 2024 -0400 Merge branch 'master' into develop commit a977dabef1dec915e00f755cb3ee3363aa9985f1 Author: vnugent Date: Sat Apr 27 17:26:35 2024 -0400 chore: package updates commit a2e2c3c4152d000b8df25c3c3fee14d491aab2c6 Merge: f03b727 87bfa83 Author: vnugent Date: Sat Apr 20 12:11:45 2024 -0400 Merge branch 'master' into develop commit f03b727d8f8e52f1dbd6293ea5c5a492c6d8e2da Author: vnugent Date: Sat Apr 20 12:02:07 2024 -0400 chore: Package updates --- .../src/DbModelBase.cs | 5 +- .../src/SQL/DbExtensions.cs | 521 --------------------- .../src/SQL/EnumerableTable.cs | 118 ----- .../src/SQL/SqlColumnNameAttribute.cs | 54 --- .../src/SQL/SqlTableNameAttribute.cs | 40 -- .../src/SQL/SqlVariable.cs | 58 --- .../src/SQL/TableManager.cs | 66 --- .../src/Storage/Blob.cs | 245 ---------- .../src/Storage/BlobExtensions.cs | 67 --- .../src/Storage/BlobStore.cs | 162 ------- .../Exceptions/LWDecriptorCreationException.cs | 45 -- .../Exceptions/LWStorageRemoveFailedException.cs | 44 -- .../Exceptions/LWStorageUpdateFailedException.cs | 43 -- .../Exceptions/UndefinedBlobStateException.cs | 43 -- .../src/Storage/LWStorageContext.cs | 49 -- .../src/Storage/LWStorageDescriptor.cs | 225 --------- .../src/Storage/LWStorageEntry.cs | 44 -- .../src/Storage/LWStorageManager.cs | 255 ---------- .../src/VNLib.Plugins.Extensions.Data.csproj | 2 +- .../VNLib.Plugins.Extensions.Loading.Sql.csproj | 2 +- .../src/Configuration/ConfigScope.cs | 28 +- .../src/Configuration/IOnConfigValidation.cs | 4 +- .../src/Configuration/Validate.cs | 114 +++++ .../src/ConfigurationExtensions.cs | 214 +++++---- .../src/Exceptions/ConfigurationException.cs | 42 ++ .../Exceptions/ConfigurationValidationException.cs | 11 +- .../src/LoadingExtensions.cs | 53 ++- .../src/Routing/EndpointLogNameAttribute.cs | 41 ++ .../src/Routing/EndpointPathAttribute.cs | 40 ++ .../src/Routing/Mvc/HttpControllerAttribute.cs | 36 ++ .../src/Routing/Mvc/HttpEndpointAttribute.cs | 52 ++ .../src/Routing/RoutingExtensions.cs | 126 ++++- .../src/S3Config.cs | 9 +- .../src/Secrets/HCVaultClient.cs | 18 +- .../src/VNLib.Plugins.Extensions.Validation.csproj | 2 +- ...ib.Plugins.Extensions.Loading.Sql.SQLite.csproj | 2 +- ...Plugins.Extensions.Loading.Sql.SQLServer.csproj | 2 +- 37 files changed, 666 insertions(+), 2216 deletions(-) delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/SQL/DbExtensions.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/SQL/EnumerableTable.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlColumnNameAttribute.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlTableNameAttribute.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlVariable.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/SQL/TableManager.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/Blob.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobExtensions.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobStore.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageEntry.cs delete mode 100644 lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Configuration/Validate.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationException.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpControllerAttribute.cs create mode 100644 lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpEndpointAttribute.cs diff --git a/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs b/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs index cdf763c..a396ab9 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs +++ b/lib/VNLib.Plugins.Extensions.Data/src/DbModelBase.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Data @@ -38,12 +38,15 @@ namespace VNLib.Plugins.Extensions.Data { /// public abstract string Id { get; set; } + /// [Timestamp] [JsonIgnore] public virtual byte[]? Version { get; set; } + /// public abstract DateTime Created { get; set; } + /// public abstract DateTime LastModified { get; set; } } diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/DbExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/DbExtensions.cs deleted file mode 100644 index 2682ed2..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/SQL/DbExtensions.cs +++ /dev/null @@ -1,521 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: DbExtensions.cs -* -* DbExtensions.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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Data; -using System.Reflection; -using System.Data.Common; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -using VNLib.Utils; -using VNLib.Utils.Memory.Caching; - -namespace VNLib.Plugins.Extensions.Data.SQL -{ - /// - /// Provides basic extension methods for ADO.NET abstract classes - /// for rapid development - /// - public static class DbExtensions - { - /* - * Object rental for propery dictionaries used for custom result objects - */ - private static ObjectRental> DictStore { get; } = ObjectRental.Create>(null, static dict => dict.Clear(), 20); - - - /// - /// Creates a new configured for with the specified value - /// and adds it to the command. - /// - /// - /// The parameter name - /// The value of the parameter - /// The of the column - /// Are null types allowed in the value parameter - /// The created parameter - public static DbParameter AddParameter(this DbCommand cmd, string @name, T @value, DbType @type, bool @nullable = false) - { - //Create the new parameter from command - DbParameter param = cmd.CreateParameter(); - //Set parameter variables - param.ParameterName = name; - param.Value = value; - param.DbType = type; - //Force non null mapping - param.SourceColumnNullMapping = nullable; - //Specify input parameter - param.Direction = ParameterDirection.Input; - //Add param to list - cmd.Parameters.Add(param); - return param; - } - /// - /// Creates a new configured for with the specified value - /// and adds it to the command. - /// - /// - /// The parameter name - /// The value of the parameter - /// The of the column - /// Size of the data value - /// Are null types allowed in the value parameter - /// The created parameter - public static DbParameter AddParameter(this DbCommand cmd, string @name, T @value, DbType @type, int @size, bool @nullable = false) - { - DbParameter param = AddParameter(cmd, name, value, type, nullable); - //Set size parameter - param.Size = size; - return param; - } - /// - /// Creates a new configured for with the specified value - /// and adds it to the command. - /// - /// - /// The parameter name - /// The value of the parameter - /// The of the column - /// Are null types allowed in the value parameter - /// The created parameter - public static DbParameter AddOutParameter(this DbCommand cmd, string @name, T @value, DbType @type, bool @nullable = false) - { - //Create the new parameter from command - DbParameter param = AddParameter(cmd, name, value, type, nullable); - //Specify output parameter - param.Direction = ParameterDirection.Output; - return param; - } - /// - /// Creates a new configured for with the specified value - /// and adds it to the command. - /// - /// - /// The parameter name - /// The value of the parameter - /// The of the column - /// Size of the data value - /// Are null types allowed in the value parameter - /// The created parameter - public static DbParameter AddOutParameter(this DbCommand cmd, string @name, T @value, DbType @type, int @size, bool @nullable = false) - { - DbParameter param = AddOutParameter(cmd, name, value, type, nullable); - //Set size parameter - param.Size = size; - return param; - } - - /// - /// Creates a new for with the specified command - /// - /// - /// The command to run against the connection - /// The initalized - public static DbCommand CreateTextCommand(this DbConnection db, string cmdText) - { - //Create the new command - DbCommand cmd = db.CreateCommand(); - cmd.CommandText = cmdText; - cmd.CommandType = CommandType.Text; //Specify text command - return cmd; - } - /// - /// Creates a new for with the specified procedure name - /// - /// - /// The name of the stored proecedure to execute - /// The initalized - public static DbCommand CreateProcedureCommand(this DbConnection db, string procedureName) - { - //Create the new command - DbCommand cmd = db.CreateCommand(); - cmd.CommandText = procedureName; - cmd.CommandType = CommandType.StoredProcedure; //Specify stored procedure - return cmd; - } - - /// - /// Creates a new for with the specified command - /// on a given transaction - /// - /// - /// The command to run against the connection - /// The transaction to execute on - /// The initalized - public static DbCommand CreateTextCommand(this DbConnection db, string cmdText, DbTransaction transaction) - { - return CreateCommand(db, transaction, CommandType.Text, cmdText); - } - /// - /// Shortcut to create a command on a transaction with the specifed command type and command - /// - /// - /// The transaction to complete the operation on - /// The command type - /// The command to execute - /// The intialized db command - public static DbCommand CreateCommand(this DbConnection db, DbTransaction transaction, CommandType type, string command) - { - //Create the new command - DbCommand cmd = db.CreateCommand(); - cmd.Transaction = transaction; - cmd.CommandText = command; - cmd.CommandType = type; - return cmd; - } - /// - /// Creates a new for with the specified procedure name - /// - /// - /// The name of the stored proecedure to execute - /// The transaction to execute on - /// The initalized - public static DbCommand CreateProcedureCommand(this DbConnection db, string procedureName, DbTransaction transaction) - { - return CreateCommand(db, transaction, CommandType.StoredProcedure, procedureName); - } - - /// - /// Reads all available rows from the reader, adapts columns to public properties with - /// attributes, and adds them to the collection - /// - /// - /// - /// The container to write created objects to - /// The number of objects created and written to the collection - public static int GetAllObjects(this DbDataReader reader, ICollection container) where T : new() - { - //make sure its worth collecting object meta - if (!reader.HasRows) - { - return 0; - } - Type objectType = typeof(T); - //Rent a dict of properties that have the column attribute set so we can load the proper results - Dictionary avialbleProps = DictStore.Rent(); - //Itterate through public properties - foreach (PropertyInfo prop in objectType.GetProperties()) - { - //try to get the column name attribute of the propery - SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute(true); - //Attribute is valid and coumn name is not empty - if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName)) - { - //Store the property for later - avialbleProps[colAtt.ColumnName] = prop; - } - } - //Get the column schema - ReadOnlyCollection columns = reader.GetColumnSchema(); - int count = 0; - //Read - while (reader.Read()) - { - //Create the new object - T ret = new(); - //Iterate through columns - foreach (DbColumn col in columns) - { - //Get the propery if its specified by its column-name attribute - if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop)) - { - //make sure the column has a value - if (col.ColumnOrdinal.HasValue) - { - //Get the object - object val = reader.GetValue(col.ColumnOrdinal.Value); - //Set check if the row is DB null, if so set it, otherwise set the value - prop.SetValue(ret, Convert.IsDBNull(val) ? null : val); - } - } - } - //Add the object to the collection - container.Add(ret); - //Increment count - count++; - } - //return dict (if an error occurs, just let the dict go and create a new one next time, no stress setting up a try/finally block) - DictStore.Return(avialbleProps); - return count; - } - /// - /// Reads all available rows from the reader, adapts columns to public properties with - /// attributes, and adds them to the collection - /// - /// - /// - /// The container to write created objects to - /// The number of objects created and written to the collection - public static async ValueTask GetAllObjectsAsync(this DbDataReader reader, ICollection container) where T : new() - { - //make sure its worth collecting object meta - if (!reader.HasRows) - { - return 0; - } - Type objectType = typeof(T); - //Rent a dict of properties that have the column attribute set so we can load the proper results - Dictionary avialbleProps = DictStore.Rent(); - //Itterate through public properties - foreach (PropertyInfo prop in objectType.GetProperties()) - { - //try to get the column name attribute of the propery - SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute(true); - //Attribute is valid and coumn name is not empty - if (!string.IsNullOrWhiteSpace(colAtt?.ColumnName)) - { - //Store the property for later - avialbleProps[colAtt.ColumnName] = prop; - } - } - //Get the column schema - ReadOnlyCollection columns = await reader.GetColumnSchemaAsync(); - int count = 0; - //Read - while (await reader.ReadAsync()) - { - //Create the new object - T ret = new(); - //Iterate through columns - foreach (DbColumn col in columns) - { - //Get the propery if its specified by its column-name attribute - if (avialbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop)) - { - //make sure the column has a value - if (col.ColumnOrdinal.HasValue) - { - //Get the object - object val = reader.GetValue(col.ColumnOrdinal.Value); - //Set check if the row is DB null, if so set it, otherwise set the value - prop.SetValue(ret, Convert.IsDBNull(val) ? null : val); - } - } - } - //Add the object to the collection - container.Add(ret); - //Increment count - count++; - } - //return dict (if an error occurs, just let the dict go and create a new one next time, no stress setting up a try/finally block) - DictStore.Return(avialbleProps); - return count; - } - /// - /// Reads the first available row from the reader, adapts columns to public properties with - /// - /// - /// - /// The created object, or default if no rows are available - public static T? GetFirstObject(this DbDataReader reader) where T : new() - { - //make sure its worth collecting object meta - if (!reader.HasRows) - { - return default; - } - //Get the object type - Type objectType = typeof(T); - //Get the column schema - ReadOnlyCollection columns = reader.GetColumnSchema(); - //Read - if (reader.Read()) - { - //Rent a dict of properties that have the column attribute set so we can load the proper results - Dictionary availbleProps = DictStore.Rent(); - //Itterate through public properties - foreach (PropertyInfo prop in objectType.GetProperties()) - { - //try to get the column name attribute of the propery - SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute(true); - //Attribute is valid and coumn name is not empty - if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName)) - { - //Store the property for later - availbleProps[colAtt.ColumnName] = prop; - } - } - //Create the new object - T ret = new(); - //Iterate through columns - foreach (DbColumn col in columns) - { - //Get the propery if its specified by its column-name attribute - if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop) && col.ColumnOrdinal.HasValue) - { - //Get the object - object val = reader.GetValue(col.ColumnOrdinal.Value); - //Set check if the row is DB null, if so set it, otherwise set the value - prop.SetValue(ret, Convert.IsDBNull(val) ? null : val); - } - } - //Return dict, no stress if error occurs, the goal is lower overhead - DictStore.Return(availbleProps); - //Return the new object - return ret; - } - return default; - } - /// - /// Reads the first available row from the reader, adapts columns to public properties with - /// - /// - /// - /// The created object, or default if no rows are available - public static async Task GetFirstObjectAsync(this DbDataReader reader) where T : new() - { - //Read - if (await reader.ReadAsync()) - { - //Get the object type - Type objectType = typeof(T); - //Get the column schema - ReadOnlyCollection columns = await reader.GetColumnSchemaAsync(); - //Rent a dict of properties that have the column attribute set so we can load the proper results - Dictionary availbleProps = DictStore.Rent(); - //Itterate through public properties - foreach (PropertyInfo prop in objectType.GetProperties()) - { - //try to get the column name attribute of the propery - SqlColumnNameAttribute? colAtt = prop.GetCustomAttribute(true); - //Attribute is valid and coumn name is not empty - if (colAtt != null && !string.IsNullOrWhiteSpace(colAtt.ColumnName)) - { - //Store the property for later - availbleProps[colAtt.ColumnName] = prop; - } - } - //Create the new object - T ret = new(); - //Iterate through columns - foreach (DbColumn col in columns) - { - //Get the propery if its specified by its column-name attribute - if (availbleProps.TryGetValue(col.ColumnName, out PropertyInfo? prop) && col.ColumnOrdinal.HasValue) - { - //Get the object - object val = reader.GetValue(col.ColumnOrdinal.Value); - //Set check if the row is DB null, if so set it, otherwise set the value - prop.SetValue(ret, Convert.IsDBNull(val) ? null : val); - } - } - //Return dict, no stress if error occurs, the goal is lower overhead - DictStore.Return(availbleProps); - //Return the new object - return ret; - } - return default; - } - /// - /// Executes a nonquery operation with the specified command using the object properties set with the - /// attributes - /// - /// - /// - /// The object containing the properties to write to command variables - /// The number of rows affected - /// - /// - /// - /// - /// - /// - public static ERRNO ExecuteNonQuery(this DbCommand cmd, T obj) where T : notnull - { - if (obj == null) - { - throw new ArgumentNullException(nameof(obj)); - } - //Get the objec type - Type objtype = typeof(T); - //Itterate through public properties - foreach (PropertyInfo prop in objtype.GetProperties()) - { - //try to get the variable attribute of the propery - SqlVariableAttribute varprops = prop.GetCustomAttribute(true); - //This property is an sql variable, so lets add it - if (varprops == null) - { - continue; - } - //If the command type is text, then make sure the variable is actually in the command, if not, ignore it - if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName)) - { - //Add the parameter to the command list - cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.IsNullable).Direction = varprops.Direction; - } - } - //Prepare the sql statement - cmd.Prepare(); - //Exect the query and return the results - return cmd.ExecuteNonQuery(); - } - /// - /// Executes a nonquery operation with the specified command using the object properties set with the - /// attributes - /// - /// - /// - /// The object containing the properties to write to command variables - /// The number of rows affected - /// - /// - /// - /// - /// - /// - public static async Task ExecuteNonQueryAsync(this DbCommand cmd, T obj) where T : notnull - { - if (obj == null) - { - throw new ArgumentNullException(nameof(obj)); - } - //Get the objec type - Type objtype = typeof(T); - //Itterate through public properties - foreach (PropertyInfo prop in objtype.GetProperties()) - { - //try to get the variable attribute of the propery - SqlVariableAttribute? varprops = prop.GetCustomAttribute(true); - //This property is an sql variable, so lets add it - if (varprops == null) - { - continue; - } - //If the command type is text, then make sure the variable is actually in the command, if not, ignore it - if (cmd.CommandType != CommandType.Text || cmd.CommandText.Contains(varprops.VariableName)) - { - //Add the parameter to the command list - cmd.AddParameter(varprops.VariableName, prop.GetValue(obj), varprops.DataType, varprops.Size, varprops.IsNullable).Direction = varprops.Direction; - } - } - //Prepare the sql statement - await cmd.PrepareAsync(); - //Exect the query and return the results - return await cmd.ExecuteNonQueryAsync(); - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/EnumerableTable.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/EnumerableTable.cs deleted file mode 100644 index 0d289b6..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/SQL/EnumerableTable.cs +++ /dev/null @@ -1,118 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: EnumerableTable.cs -* -* EnumerableTable.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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Data; -using System.Threading; -using System.Data.Common; -using System.Threading.Tasks; -using System.Collections.Generic; - -namespace VNLib.Plugins.Extensions.Data.SQL -{ - /// - /// A base class for client side async enumerable SQL queries - /// - /// The entity type - public abstract class EnumerableTable : TableManager, IAsyncEnumerable - { - const string DEFAULT_ENUM_STATMENT = "SELECT *\r\nFROM @table\r\n;"; - - public EnumerableTable(Func factory, string tableName) : base(factory, tableName) - { - //Build the default select all statment - Enumerate = DEFAULT_ENUM_STATMENT.Replace("@table", tableName); - } - public EnumerableTable(Func factory) : base(factory) - { } - - /// - /// The command that will be run against the database to return rows for enumeration - /// - protected string Enumerate { get; set; } - - /// - /// The isolation level to use when creating the transaction during enumerations - /// - protected IsolationLevel TransactionIsolationLevel { get; set; } = IsolationLevel.ReadUncommitted; - - IAsyncEnumerator IAsyncEnumerable.GetAsyncEnumerator(CancellationToken cancellationToken) - { - return GetAsyncEnumerator(cancellationToken: cancellationToken); - } - - /// - /// Transforms a row from the into the item type - /// to be returned when yielded. - /// - /// The reader to get the item data from - /// A token to cancel the operation - /// A task that returns the transformed item - /// The position is set before this method is invoked - protected abstract Task GetItemAsync(DbDataReader reader, CancellationToken cancellationToken); - /// - /// Invoked when an item is no longer in the enumerator scope, in the enumeration process. - /// - /// The item to cleanup - /// A token to cancel the operation - /// A ValueTask that represents the cleanup process - protected abstract ValueTask CleanupItemAsync(T item, CancellationToken cancellationToken); - - /// - /// Gets an to enumerate items within the backing store. - /// - /// Cleanup items after each item is enumerated and the enumeration scope has - /// returned to the enumerator - /// A token to cancel the enumeration - /// A to enumerate records within the store - public virtual async IAsyncEnumerator GetAsyncEnumerator(bool closeItems = true, CancellationToken cancellationToken = default) - { - await using DbConnection db = GetConnection(); - await db.OpenAsync(cancellationToken); - await using DbTransaction transaction = await db.BeginTransactionAsync(cancellationToken); - //Start the enumeration command - await using DbCommand cmd = db.CreateTextCommand(Enumerate, transaction); - await cmd.PrepareAsync(cancellationToken); - await using DbDataReader reader = await cmd.ExecuteReaderAsync(cancellationToken); - //loop through results and transform each element - while (reader.Read()) - { - //get the item - T item = await GetItemAsync(reader, cancellationToken); - try - { - yield return item; - } - finally - { - if (closeItems) - { - //Cleanup the item - await CleanupItemAsync(item, cancellationToken); - } - } - } - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlColumnNameAttribute.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlColumnNameAttribute.cs deleted file mode 100644 index f7628d6..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlColumnNameAttribute.cs +++ /dev/null @@ -1,54 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: SqlColumnNameAttribute.cs -* -* SqlColumnNameAttribute.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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; - -namespace VNLib.Plugins.Extensions.Data.SQL -{ - /// - /// Property attribute that specifies the property represents an SQL column in the database - /// - [AttributeUsage(AttributeTargets.Property)] - public sealed class SqlColumnNameAttribute : Attribute - { - public bool Nullable { get; } - public bool Unique { get; } - public bool PrimaryKey { get; } - public string ColumnName { get; } - /// - /// Specifies the property is an SQL column name - /// - /// Name of the SQL column - /// - /// - /// - public SqlColumnNameAttribute(string columnName, bool primaryKey = false, bool nullable = true, bool unique = false) - { - this.ColumnName = columnName; - this.PrimaryKey = primaryKey; - this.Nullable = nullable; - this.Unique = unique; - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlTableNameAttribute.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlTableNameAttribute.cs deleted file mode 100644 index 9668e91..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlTableNameAttribute.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: SqlTableNameAttribute.cs -* -* SqlTableNameAttribute.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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; - -namespace VNLib.Plugins.Extensions.Data.SQL -{ - - /// - /// Allows a type to declare itself as a with the specified name - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple =false, Inherited = true)] - public sealed class SqlTableNameAttribute : Attribute - { - public string TableName { get; } - - public SqlTableNameAttribute(string tableName) => TableName = tableName; - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlVariable.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlVariable.cs deleted file mode 100644 index ef2e170..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/SQL/SqlVariable.cs +++ /dev/null @@ -1,58 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: SqlVariable.cs -* -* SqlVariable.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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Data; - -namespace VNLib.Plugins.Extensions.Data.SQL -{ - /// - /// Property attribute that specifies the property is to be used for a given command variable - /// - [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] - public sealed class SqlVariableAttribute : Attribute - { - public string VariableName { get; } - public DbType DataType { get; } - public ParameterDirection Direction { get; } - public int Size { get; } - public bool IsNullable { get; } - /// - /// Specifies the property to be used as an SQL variable - /// - /// Sql statement variable this property will substitute - /// The sql data the property will represent - /// Data direction during execution - /// Column size - /// Is this property allowed to be null - public SqlVariableAttribute(string variableName, DbType dataType, ParameterDirection direction, int size, bool isNullable) - { - this.VariableName = variableName; - this.DataType = dataType; - this.Direction = direction; - this.Size = size; - this.IsNullable = isNullable; - } - } -} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/SQL/TableManager.cs b/lib/VNLib.Plugins.Extensions.Data/src/SQL/TableManager.cs deleted file mode 100644 index da1d8a7..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/SQL/TableManager.cs +++ /dev/null @@ -1,66 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: TableManager.cs -* -* TableManager.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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Data.Common; - -namespace VNLib.Plugins.Extensions.Data.SQL -{ - /// - /// A class that contains basic structures for interacting with an SQL driven database - /// - public abstract class TableManager - { - private readonly Func Factory; - protected string Insert { get; set; } - protected string Select { get; set; } - protected string Update { get; set; } - protected string Delete { get; set; } - - /// - /// The name of the table specified during initialized - /// - protected string TableName { get; } - - protected TableManager(Func factory, string tableName) - { - this.Factory = factory ?? throw new ArgumentNullException(nameof(factory)); - this.TableName = !string.IsNullOrWhiteSpace(tableName) ? tableName : throw new ArgumentNullException(nameof(tableName)); - } - - protected TableManager(Func factory) - { - this.Factory = factory ?? throw new ArgumentNullException(nameof(factory)); - this.TableName = ""; - } - /// - /// Opens a new by invoking the factory callback method - /// - /// The open connection - protected DbConnection GetConnection() - { - return Factory(); - } - } -} diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Blob.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Blob.cs deleted file mode 100644 index 0edf653..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Blob.cs +++ /dev/null @@ -1,245 +0,0 @@ -/* -* Copyright (c) 2023 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using System.Runtime.Versioning; - -using VNLib.Utils; -using VNLib.Utils.IO; -using VNLib.Utils.Async; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - /// - /// Represents a stream of arbitrary binary data - /// - public class Blob : BackingStream, IObjectStorage, IAsyncExclusiveResource - { - protected readonly LWStorageDescriptor Descriptor; - - /// - /// The current blob's unique ID - /// - public string BlobId => Descriptor.DescriptorID; - /// - /// A value indicating if the has been modified - /// - public bool Modified { get; protected set; } - /// - /// A valid indicating if the blob was flagged for deletiong - /// - public bool Deleted { get; protected set; } - - /// - /// The name of the file (does not change the actual file system name) - /// - public string Name - { - get => Descriptor.GetName(); - set => Descriptor.SetName(value); - } - /// - /// The UTC time the was last modified - /// - public DateTimeOffset LastWriteTimeUtc => Descriptor.LastModified; - /// - /// The UTC time the was created - /// - public DateTimeOffset CreationTimeUtc => Descriptor.Created; - - internal Blob(LWStorageDescriptor descriptor, in FileStream file) - { - this.Descriptor = descriptor; - base.BaseStream = file; - } - - /// - /// Prevents other processes from reading from or writing to the - /// - /// The begining position of the range to lock - /// The range to be locked - /// - /// - /// - [UnsupportedOSPlatform("ios")] - [UnsupportedOSPlatform("macos")] - [UnsupportedOSPlatform("tvos")] - public void Lock(long position, long length) => BaseStream.Lock(position, length); - /// - /// Prevents other processes from reading from or writing to the - /// - /// - /// - /// - [UnsupportedOSPlatform("ios")] - [UnsupportedOSPlatform("macos")] - [UnsupportedOSPlatform("tvos")] - public void Lock() => BaseStream.Lock(0, BaseStream.Length); - /// - /// Allows access by other processes to all or part of the that was previously locked - /// - /// The begining position of the range to unlock - /// The range to be unlocked - /// - [UnsupportedOSPlatform("ios")] - [UnsupportedOSPlatform("macos")] - [UnsupportedOSPlatform("tvos")] - public void Unlock(long position, long length) => BaseStream.Unlock(position, length); - /// - /// Allows access by other processes to the entire - /// - /// - [UnsupportedOSPlatform("ios")] - [UnsupportedOSPlatform("macos")] - [UnsupportedOSPlatform("tvos")] - public void Unlock() => BaseStream.Unlock(0, BaseStream.Length); - /// - public override void SetLength(long value) - { - base.SetLength(value); - //Set modified flag - Modified |= true; - } - - /* - * Capture on-write calls to set the modified flag - */ - /// - protected override void OnWrite(int count) => Modified |= true; - - T IObjectStorage.GetObject(string key) => ((IObjectStorage)Descriptor).GetObject(key); - void IObjectStorage.SetObject(string key, T obj) => ((IObjectStorage)Descriptor).SetObject(key, obj); - - public string this[string index] - { - get => Descriptor[index]; - set => Descriptor[index] = value; - } - - - /// - /// Marks the file for deletion and will be deleted when the is disposed - /// - public void Delete() - { - //Set deleted flag - Deleted |= true; - Descriptor.Delete(); - } - /// - public bool IsReleased => Descriptor.IsReleased; - - - /// - /// - /// If the was opened with writing enabled, - /// and file was modified, changes are flushed to the backing store - /// and the stream is set to readonly. - /// - /// - /// If calls to this method succeed the stream is placed into a read-only mode - /// which will cause any calls to Write to throw a - /// - /// - /// A that may be awaited until the operation completes - /// - /// This method may be called to avoid flushing changes to the backing store - /// when the is disposed (i.e. lifetime is manged outside of the desired scope) - /// - /// - /// - /// - public async ValueTask FlushChangesAndSetReadonlyAsync() - { - if (Deleted) - { - throw new InvalidOperationException("The blob has been deleted and must be closed!"); - } - if (Modified) - { - //flush the base stream - await BaseStream.FlushAsync(); - //Update the file length in the store - Descriptor.SetLength(BaseStream.Length); - } - //flush changes, this will cause the dispose method to complete synchronously when closing - await Descriptor.WritePendingChangesAsync(); - //Clear modified flag - Modified = false; - //Set to readonly mode - base.ForceReadOnly = true; - } - - - /* - * Override the dispose async to manually dispose the - * base stream and avoid the syncrhonous (OnClose) - * method and allow awaiting the descriptor release - */ - /// - public override async ValueTask DisposeAsync() - { - await ReleaseAsync(); - GC.SuppressFinalize(this); - } - /// - public async ValueTask ReleaseAsync(CancellationToken cancellation = default) - { - 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(cancellation); - } - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobExtensions.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobExtensions.cs deleted file mode 100644 index 425645c..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobExtensions.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* -* 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; - -using VNLib.Utils; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - public static class BlobExtensions - { - public const string USER_ID_ENTRY = "__.uid"; - public const string VERSION_ENTRY = "__.vers"; - - private const string FILE_SIZE = "__.size"; - private const string FILE_NAME = "__.name"; - private const string ERROR_FLAG = "__.err"; - - public static string GetUserId(this Blob blob) => blob[USER_ID_ENTRY]; - /// - /// Gets the stored in the current - /// - /// The sored version if previously set, thows otherwise - /// - public static Version GetVersion(this Blob blob) => Version.Parse(blob[VERSION_ENTRY]); - /// - /// Sets a for the current - /// - /// - /// The of the - public static void SetVersion(this Blob blob, Version version) => blob[VERSION_ENTRY] = version.ToString(); - - /// - /// Gets a value indicating if the last operation left the in an undefined state - /// - /// True if the state is undefined, false otherwise - public static bool IsError(this Blob blob) => bool.TrueString.Equals(blob[ERROR_FLAG]); - internal static void IsError(this LWStorageDescriptor blob, bool value) => blob[ERROR_FLAG] = value ? bool.TrueString : null; - - internal static long GetLength(this LWStorageDescriptor blob) => (blob as IObjectStorage).GetObject(FILE_SIZE); - internal static void SetLength(this LWStorageDescriptor blob, long length) => (blob as IObjectStorage).SetObject(FILE_SIZE, length); - - internal static string GetName(this LWStorageDescriptor blob) => blob[FILE_NAME]; - internal static string SetName(this LWStorageDescriptor blob, string filename) => blob[FILE_NAME] = filename; - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobStore.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobStore.cs deleted file mode 100644 index e59997c..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/BlobStore.cs +++ /dev/null @@ -1,162 +0,0 @@ -/* -* 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.IO; -using System.Security.Cryptography; -using System.Threading.Tasks; - -using VNLib.Utils.Extensions; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - - /// - /// Stores s to the local file system backed with a single table - /// that tracks changes - /// - public class BlobStore - { - /// - /// The root directory all blob files are stored - /// - public DirectoryInfo RootDir { get; } - /// - /// The backing store for blob meta-data - /// - protected LWStorageManager BlobTable { get; } - /// - /// Creates a new that accesses files - /// within the specified root directory. - /// - /// The root directory containing the blob file contents - /// The db backing store - public BlobStore(DirectoryInfo rootDir, LWStorageManager blobStoreMan) - { - RootDir = rootDir; - BlobTable = blobStoreMan; - } - - private string GetPath(string fileId) => Path.Combine(RootDir.FullName, fileId); - - /* - * Creates a repeatable unique identifier for the file - * name and allows for lookups - */ - internal static string CreateFileHash(string fileName) - { - throw new NotImplementedException(); - //return ManagedHash.ComputeBase64Hash(fileName, HashAlg.SHA1); - } - - /// - /// Opens an existing from the current store - /// - /// The id of the file being requested - /// Access level of the file - /// The sharing option of the underlying file - /// The size of the file buffer - /// If found, the requested , null otherwise. Throws exceptions if the file is opened in a non-sharable state - /// - /// - /// - /// - /// - public virtual async Task OpenBlobAsync(string fileId, FileAccess access, FileShare share, int bufferSize = 4096) - { - //Get the file's data descriptor - LWStorageDescriptor fileDescriptor = await BlobTable.GetDescriptorFromIDAsync(fileId); - //return null if not found - if (fileDescriptor == null) - { - return null; - } - try - { - string fsSafeName = GetPath(fileDescriptor.DescriptorID); - //try to open the file - FileStream file = new(fsSafeName, FileMode.Open, access, share, bufferSize, FileOptions.Asynchronous); - //Create the new blob - return new Blob(fileDescriptor, file); - } - catch (FileNotFoundException) - { - //If the file was not found but the descriptor was, delete the descriptor from the db - fileDescriptor.Delete(); - //Flush changes - await fileDescriptor.ReleaseAsync(); - //return null since this is a desync issue and the file technically does not exist - return null; - } - catch - { - //Release the descriptor and pass the exception - await fileDescriptor.ReleaseAsync(); - throw; - } - } - - /// - /// Creates a new for the specified file sharing permissions - /// - /// The name of the original file - /// The blob sharing permissions - /// - /// The newly created - /// - /// - /// - /// - public virtual async Task CreateBlobAsync(string name, FileShare share = FileShare.None, int bufferSize = 4096) - { - //hash the file name to create a unique id for the file name - LWStorageDescriptor newFile = await BlobTable.CreateDescriptorAsync(CreateFileHash(name)); - //if the descriptor was not created, return null - if (newFile == null) - { - return null; - } - try - { - string fsSafeName = GetPath(newFile.DescriptorID); - //Open/create the new file - FileStream file = new(fsSafeName, FileMode.OpenOrCreate, FileAccess.ReadWrite, share, bufferSize, FileOptions.Asynchronous); - //If the file already exists, make sure its zero'd - file.SetLength(0); - //Save the original name of the file - newFile.SetName(name); - //Create and return the new blob - return new Blob(newFile, file); - } - catch - { - //If an exception occurs, remove the descritor from the db - newFile.Delete(); - await newFile.ReleaseAsync(); - //Pass exception - throw; - } - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs deleted file mode 100644 index 7ea50d0..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWDecriptorCreationException.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* -* 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - /// - /// Raised when an operation to create a new - /// fails - /// - public class LWDescriptorCreationException : Exception - { - /// - public LWDescriptorCreationException() - {} - /// - public LWDescriptorCreationException(string? message) : base(message) - {} - /// - public LWDescriptorCreationException(string? message, Exception? innerException) : base(message, innerException) - {} - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs deleted file mode 100644 index d91019c..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageRemoveFailedException.cs +++ /dev/null @@ -1,44 +0,0 @@ -/* -* 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using VNLib.Utils.Resources; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - /// - /// The exception raised when an open removal operation fails. The - /// property may contain any nested exceptions that caused the removal to fail. - /// - public class LWStorageRemoveFailedException : ResourceDeleteFailedException - { - internal LWStorageRemoveFailedException(string error, Exception inner) : base(error, inner) { } - - public LWStorageRemoveFailedException() - {} - - public LWStorageRemoveFailedException(string message) : base(message) - {} - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs deleted file mode 100644 index f13792d..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/LWStorageUpdateFailedException.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* -* 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using VNLib.Utils.Resources; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - /// - /// The exception raised when an open update operation fails. The - /// property may contain any nested exceptions that caused the update to fail. - /// - public class LWStorageUpdateFailedException : ResourceUpdateFailedException - { - internal LWStorageUpdateFailedException(string error, Exception inner) : base(error, inner) { } - - public LWStorageUpdateFailedException() - {} - public LWStorageUpdateFailedException(string message) : base(message) - {} - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs deleted file mode 100644 index c52fed9..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/Exceptions/UndefinedBlobStateException.cs +++ /dev/null @@ -1,43 +0,0 @@ -/* -* Copyright (c) 2024 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; - - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - /// - /// Raised to signal that the requested was left in an undefined state - /// when previously accessed - /// - public class UndefinedBlobStateException : Exception - { - public UndefinedBlobStateException() - {} - public UndefinedBlobStateException(string message) : base(message) - {} - public UndefinedBlobStateException(string message, Exception innerException) : base(message, innerException) - {} - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs deleted file mode 100644 index 032d380..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageContext.cs +++ /dev/null @@ -1,49 +0,0 @@ -/* -* Copyright (c) 2023 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: LWStorageContext.cs -* -* LWStorageContext.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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; - -using Microsoft.EntityFrameworkCore; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ -#nullable disable - internal sealed class LWStorageContext : DBContextBase - { - private readonly string TableName; - public DbSet Descriptors { get; set; } - - public LWStorageContext(DbContextOptions options, string tableName) - :base(options) - { - TableName = tableName; - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - if(TableName != null) - throw new NotImplementedException("Table/relational package requires development not yet implemented"); - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs deleted file mode 100644 index 78fdfa0..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageDescriptor.cs +++ /dev/null @@ -1,225 +0,0 @@ -/* -* Copyright (c) 2023 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Text.Json; -using System.Threading; -using System.Collections; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -using VNLib.Utils; -using VNLib.Utils.Async; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - /// - /// Represents an open storage object, that when released or disposed, will flush its changes to the underlying table - /// for which this descriptor represents - /// - public sealed class LWStorageDescriptor : AsyncUpdatableResource, IObjectStorage, IEnumerable>, IIndexable - { - - private static readonly JsonSerializerOptions SerializerOptions = new() - { - DictionaryKeyPolicy = JsonNamingPolicy.CamelCase, - NumberHandling = JsonNumberHandling.Strict, - ReadCommentHandling = JsonCommentHandling.Disallow, - WriteIndented = false, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - IgnoreReadOnlyFields = true, - DefaultBufferSize = Environment.SystemPageSize, - }; - - - internal LWStorageEntry Entry { get; } - - private readonly Lazy> StringStorage; - - protected override IAsyncResourceStateHandler AsyncHandler { get; } - - /// - /// The currnt descriptor's identifier string within its backing table. Usually the primary key. - /// - public string DescriptorID => Entry.Id; - - /// - /// The identifier of the user for which this descriptor belongs to - /// - public string UserID => Entry.UserId!; - - /// - /// The when the descriptor was created - /// - public DateTimeOffset Created => Entry.Created; - - /// - /// The last time this descriptor was updated - /// - public DateTimeOffset LastModified => Entry.LastModified; - - internal LWStorageDescriptor(IAsyncResourceStateHandler handler, LWStorageEntry entry) - { - Entry = entry; - AsyncHandler = handler; - StringStorage = new(OnStringStoreLoad); - } - - internal Dictionary OnStringStoreLoad() - { - if(Entry.Data == null || Entry.Data.Length == 0) - { - return new(StringComparer.OrdinalIgnoreCase); - } - else - { - //Decode and deserialize the data - return JsonSerializer.Deserialize>(Entry.Data, SerializerOptions) ?? new(StringComparer.OrdinalIgnoreCase); - } - } - - /// - /// - /// - /// - /// - public T? GetObject(string key) - { - Check(); - //De-serialize and return object - return StringStorage.Value.TryGetValue(key, out string? val) ? JsonSerializer.Deserialize(val, SerializerOptions) : default; - } - - /// - /// - /// - public void SetObject(string key, T obj) - { - //Remove the object from storage if its null - if (obj == null) - { - SetStringValue(key, null); - } - else - { - //Serialize the object to a string - string value = JsonSerializer.Serialize(obj, SerializerOptions); - //Attempt to store string in storage - SetStringValue(key, value); - } - } - - /// - /// Gets a string value from string storage matching a given key - /// - /// Key for storage - /// Value associaetd with key if exists, otherwise - /// If key is null - /// - public string GetStringValue(string key) - { - Check(); - return StringStorage.Value.TryGetValue(key, out string? val) ? val : string.Empty; - } - - /// - /// Creates, overwrites, or removes a string value identified by key. - /// - /// Entry key - /// String to store or overwrite, set to null or string.Empty to remove a property - /// - /// If key is null - public void SetStringValue(string key, string? value) - { - if (string.IsNullOrEmpty(key)) - { - throw new ArgumentNullException(nameof(key)); - } - Check(); - //If the value is null, see if the the properties are null - if (string.IsNullOrWhiteSpace(value)) - { - //If the value is null and properies exist, remove the entry - StringStorage.Value.Remove(key); - Modified |= true; - } - else - { - //Set the value - StringStorage.Value[key] = value; - //Set modified flag - Modified |= true; - } - } - - /// - /// Gets or sets a string value from string storage matching a given key - /// - /// Key for storage - /// Value associaetd with key if exists, otherwise - /// - /// If key is null - public string this[string key] - { - get => GetStringValue(key); - set => SetStringValue(key, value); - } - - /// - /// Flushes all pending changes to the backing store asynchronously - /// - /// - public ValueTask WritePendingChangesAsync() - { - Check(); - return Modified ? (new(FlushPendingChangesAsync())) : ValueTask.CompletedTask; - } - - /// - public override async ValueTask ReleaseAsync(CancellationToken cancellation = default) - { - await base.ReleaseAsync(cancellation); - - //Cleanup dict on exit - if (StringStorage.IsValueCreated) - { - StringStorage.Value.Clear(); - } - } - - /// - public IEnumerator> GetEnumerator() => StringStorage.Value.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - /// - protected override object GetResource() - { - //Serlaize the state data and store it in the data entry - Entry.Data = JsonSerializer.SerializeToUtf8Bytes(StringStorage.Value, SerializerOptions); - return Entry; - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageEntry.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageEntry.cs deleted file mode 100644 index 07622e5..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageEntry.cs +++ /dev/null @@ -1,44 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Extensions.Data -* File: LWStorageEntry.cs -* -* LWStorageEntry.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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; - -using VNLib.Plugins.Extensions.Data.Abstractions; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - - internal sealed class LWStorageEntry : DbModelBase, IUserEntity - { - public override string Id { get; set; } - - public override DateTime Created { get; set; } - - public override DateTime LastModified { get; set; } - - public string? UserId { get; set; } - - public byte[]? Data { get; set; } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs b/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs deleted file mode 100644 index f098def..0000000 --- a/lib/VNLib.Plugins.Extensions.Data/src/Storage/LWStorageManager.cs +++ /dev/null @@ -1,255 +0,0 @@ -/* -* Copyright (c) 2024 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 Affero General Public License as -* published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Data; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Microsoft.EntityFrameworkCore; - -using VNLib.Utils; -using VNLib.Utils.Async; - -namespace VNLib.Plugins.Extensions.Data.Storage -{ - - /// - /// Provides single table database object storage services - /// - public sealed class LWStorageManager : IAsyncResourceStateHandler - { - - /// - /// The generator function that is invoked when a new is to - /// be created without an explicit id - /// - public Func NewDescriptorIdGenerator { get; init; } = static () => Guid.NewGuid().ToString("N"); - - private readonly DbContextOptions DbOptions; - private readonly string TableName; - - private LWStorageContext GetContext() => new(DbOptions, TableName); - - /// - /// Creates a new with - /// - /// The db context options to create database connections with - /// The name of the table to operate on - /// - public LWStorageManager(DbContextOptions options, string tableName) - { - DbOptions = options ?? throw new ArgumentNullException(nameof(options)); - TableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); - } - - /// - /// Creates a new fror a given user - /// - /// Id of user - /// An override to specify the new descriptor's id - /// A token to cancel the operation - /// A new if successfully created, null otherwise - /// - /// - public async Task CreateDescriptorAsync(string userId, string? descriptorIdOverride = null, CancellationToken cancellation = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(userId); - - //If no override id was specified, generate a new one - descriptorIdOverride ??= NewDescriptorIdGenerator(); - - DateTime createdOrModifedTime = DateTime.UtcNow; - - await using LWStorageContext ctx = GetContext(); - - //Make sure the descriptor doesnt exist only by its descriptor id - if (await ctx.Descriptors.AnyAsync(d => d.Id == descriptorIdOverride, cancellation)) - { - throw new LWDescriptorCreationException($"A descriptor with id {descriptorIdOverride} already exists"); - } - - //Cache time - DateTime now = DateTime.UtcNow; - - //Create the new descriptor - LWStorageEntry entry = new() - { - Created = now, - LastModified = now, - Id = descriptorIdOverride, - UserId = userId, - }; - - //Add and save changes - ctx.Descriptors.Add(entry); - - ERRNO result = await ctx.SaveAndCloseAsync(true, cancellation); - - return result - ? new LWStorageDescriptor(this, entry) - : throw new LWDescriptorCreationException("Failed to create descriptor, because changes could not be saved"); - } - - /// - /// Attempts to retrieve for a given user-id. The caller is responsible for - /// consitancy state of the descriptor - /// - /// User's id - /// A token to cancel the operation - /// The descriptor belonging to the user, or null if not found or error occurs - /// - public async Task GetDescriptorFromUIDAsync(string userid, CancellationToken cancellation = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(userid); - - //Init db - await using LWStorageContext db = GetContext(); - - //Get entry - LWStorageEntry? entry = await (from s in db.Descriptors - where s.UserId == userid - select s) - .SingleOrDefaultAsync(cancellation); - - await db.SaveAndCloseAsync(true, cancellation); - - //Close transactions and return - return entry == null ? null : new (this, entry); - } - - /// - /// Attempts to retrieve the for the given descriptor id. The caller is responsible for - /// consitancy state of the descriptor - /// - /// Unique identifier for the descriptor - /// A token to cancel the opreeaiton - /// The descriptor belonging to the user, or null if not found or error occurs - /// - public async Task GetDescriptorFromIDAsync(string descriptorId, CancellationToken cancellation = default) - { - ArgumentException.ThrowIfNullOrWhiteSpace(descriptorId); - - //Init db - await using LWStorageContext db = GetContext(); - - //Get entry - LWStorageEntry? entry = await (from s in db.Descriptors - where s.Id == descriptorId - select s) - .SingleOrDefaultAsync(cancellation); - - await db.SaveAndCloseAsync(true, cancellation); - - //Close transactions and return - return entry == null ? null : new(this, entry); - } - - /// - /// Cleanup entries before the specified . Entires are store in UTC time - /// - /// Time before to compare against - /// A token to cancel the operation - /// The number of entires cleanedS - public Task CleanupTableAsync(TimeSpan compareTime, CancellationToken cancellation = default) => CleanupTableAsync(DateTime.UtcNow.Subtract(compareTime), cancellation); - - /// - /// Cleanup entries before the specified . Entires are store in UTC time - /// - /// UTC time to compare entires against - /// A token to cancel the operation - /// The number of entires cleaned - public async Task CleanupTableAsync(DateTime compareTime, CancellationToken cancellation = default) - { - //Init db - await using LWStorageContext db = GetContext(); - - //Get all expired entires - LWStorageEntry[] expired = await (from s in db.Descriptors - where s.Created < compareTime - select s) - .ToArrayAsync(cancellation); - - //Delete - db.Descriptors.RemoveRange(expired); - - //Commit transaction - return await db.SaveAndCloseAsync(true, cancellation); - } - - async Task IAsyncResourceStateHandler.UpdateAsync(AsyncUpdatableResource resource, object state, CancellationToken cancellation) - { - LWStorageEntry entry = (state as LWStorageEntry)!; - ERRNO result = 0; - try - { - await using LWStorageContext ctx = GetContext(); - - //Begin tracking - ctx.Descriptors.Attach(entry); - - //Update modified time - entry.LastModified = DateTime.UtcNow; - - //Save changes - result = await ctx.SaveAndCloseAsync(true, cancellation); - } - catch (Exception ex) - { - throw new LWStorageUpdateFailedException("", ex); - } - //If the result is 0 then the update failed - if (!result) - { - throw new LWStorageUpdateFailedException($"Descriptor {entry.Id} failed to update"); - } - } - - async Task IAsyncResourceStateHandler.DeleteAsync(AsyncUpdatableResource resource, CancellationToken cancellation) - { - LWStorageEntry descriptor = (resource as LWStorageDescriptor)!.Entry; - ERRNO result; - try - { - //Init db - await using LWStorageContext db = GetContext(); - - //Delete the user from the database - db.Descriptors.Remove(descriptor); - - //Save changes and commit if successful - result = await db.SaveAndCloseAsync(true, cancellation); - } - catch (Exception ex) - { - throw new LWStorageRemoveFailedException("", ex); - } - if (!result) - { - throw new LWStorageRemoveFailedException("Failed to delete the user account because of a database failure, the user may already be deleted"); - } - } - } -} \ No newline at end of file diff --git a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj index c957b26..94e7137 100644 --- a/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj +++ b/lib/VNLib.Plugins.Extensions.Data/src/VNLib.Plugins.Extensions.Data.csproj @@ -48,7 +48,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj index 5292a5d..d622a05 100644 --- a/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj +++ b/lib/VNLib.Plugins.Extensions.Loading.Sql/src/VNLib.Plugins.Extensions.Loading.Sql.csproj @@ -40,7 +40,7 @@ - + diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs index 7f5c09c..d8f4347 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/ConfigScope.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -29,13 +29,15 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using VNLib.Utils.Resources; + namespace VNLib.Plugins.Extensions.Loading { internal sealed class ConfigScope: IConfigScope { - private readonly Lazy> _config; + private readonly LazyInitializer> _config; private readonly JsonElement _element; @@ -48,36 +50,40 @@ namespace VNLib.Plugins.Extensions.Loading private IReadOnlyDictionary LoadTable() { - return _element.EnumerateObject().ToDictionary(static k => k.Name, static k => k.Value); + return _element.EnumerateObject() + .ToDictionary( + static k => k.Name, + static k => k.Value + ); } /// - public JsonElement this[string key] => _config.Value[key]; + public JsonElement this[string key] => _config.Instance[key]; /// - public IEnumerable Keys => _config.Value.Keys; + public IEnumerable Keys => _config.Instance.Keys; /// - public IEnumerable Values => _config.Value.Values; + public IEnumerable Values => _config.Instance.Values; /// - public int Count => _config.Value.Count; + public int Count => _config.Instance.Count; /// public string ScopeName { get; } /// - public bool ContainsKey(string key) => _config.Value.ContainsKey(key); + public bool ContainsKey(string key) => _config.Instance.ContainsKey(key); /// public T Deserialze() => _element.Deserialize()!; /// - public IEnumerator> GetEnumerator() => _config.Value.GetEnumerator(); + public IEnumerator> GetEnumerator() => _config.Instance.GetEnumerator(); /// - public bool TryGetValue(string key, [MaybeNullWhen(false)] out JsonElement value) => _config.Value.TryGetValue(key, out value); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out JsonElement value) => _config.Instance.TryGetValue(key, out value); - IEnumerator IEnumerable.GetEnumerator() => _config.Value.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => _config.Instance.GetEnumerator(); } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs index 6d4641b..6eeba78 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/IOnConfigValidation.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -33,6 +33,6 @@ namespace VNLib.Plugins.Extensions.Loading /// /// Validates a json configuration during deserialzation /// - void Validate(); + void OnValidate(); } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/Validate.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/Validate.cs new file mode 100644 index 0000000..1bb6787 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Configuration/Validate.cs @@ -0,0 +1,114 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: Validate.cs +* +* Validate.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +using VNLib.Utils.IO; + +namespace VNLib.Plugins.Extensions.Loading.Configuration +{ + /// + /// A class that allows for easy configuration validation + /// + public sealed class Validate + { + /// + /// Ensures the object is not null and not an empty string, + /// otherwise a is raised + /// + /// + /// The object to test + /// The message to display to the user on loading + /// + [DoesNotReturn] + public static void NotNull(T? obj, string message) where T : class + { + if (obj is null) + { + throw new ConfigurationValidationException(message); + } + + if (obj is string s && string.IsNullOrWhiteSpace(s)) + { + throw new ConfigurationValidationException(message); + } + } + + /// + /// + /// + /// + /// + /// + public static void Assert([DoesNotReturnIf(false)] bool condition, string message) + { + if (!condition) + { + throw new ConfigurationValidationException(message); + } + } + + public static void NotEqual(T a, T b, string message) + { + if (a is null || b is null) + { + throw new ConfigurationValidationException(message); + } + + if (a.Equals(b)) + { + throw new ConfigurationValidationException(message); + } + } + + public static void Range2(T value, T min, T max, string message) + where T : IComparable + { + //Compare the value against min/max calues and raise exception if it is + if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0) + { + throw new ConfigurationValidationException(message); + } + } + + + public static void Range(T value, T min, T max, [CallerArgumentExpression(nameof(value))] string? paramName = null) + where T : IComparable + { + + Range2(value, min, max, $"Value for {paramName} must be between {min} and {max}. Value: {value}"); + } + + + public static void FileExists(string path) + { + if (!FileOperations.FileExists(path)) + { + throw new ConfigurationValidationException($"Required file: {path} not found"); + } + } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs index 0337fbd..e838822 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/ConfigurationExtensions.cs @@ -24,12 +24,14 @@ using System; using System.IO; +using System.Linq; using System.Text.Json; using System.Reflection; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using VNLib.Utils.Extensions; +using VNLib.Plugins.Extensions.Loading.Configuration; namespace VNLib.Plugins.Extensions.Loading { @@ -37,22 +39,17 @@ namespace VNLib.Plugins.Extensions.Loading /// Specifies a configuration variable name in the plugin's configuration /// containing data specific to the type /// + /// + /// Initializes a new + /// + /// The name of the configuration variable for the class [AttributeUsage(AttributeTargets.Class)] - public sealed class ConfigurationNameAttribute : Attribute + public sealed class ConfigurationNameAttribute(string configVarName) : Attribute { /// /// /// - public string ConfigVarName { get; } - - /// - /// Initializes a new - /// - /// The name of the configuration variable for the class - public ConfigurationNameAttribute(string configVarName) - { - ConfigVarName = configVarName; - } + public string ConfigVarName { get; } = configVarName; /// /// When true or not configured, signals that the type requires a configuration scope @@ -71,7 +68,6 @@ namespace VNLib.Plugins.Extensions.Loading public const string S3_SECRET_KEY = "s3_secret"; public const string PLUGIN_ASSET_KEY = "assets"; public const string PLUGINS_HOST_KEY = "plugins"; - public const string PLUGIN_PATH_KEY = "path"; /// /// Retrieves a top level configuration dictionary of elements for the specified type. @@ -80,7 +76,7 @@ namespace VNLib.Plugins.Extensions.Loading /// The type to get the configuration of /// /// A of top level configuration elements for the type - /// + /// /// public static IConfigScope GetConfigForType(this PluginBase plugin) { @@ -97,26 +93,12 @@ namespace VNLib.Plugins.Extensions.Loading /// /// The config property name to retrieve /// A of top level configuration elements for the type - /// + /// /// public static IConfigScope GetConfig(this PluginBase plugin, string propName) - { - plugin.ThrowIfUnloaded(); - try - { - //Try to get the element from the plugin config first - if (!plugin.PluginConfig.TryGetProperty(propName, out JsonElement el)) - { - //Fallback to the host config - el = plugin.HostConfig.GetProperty(propName); - } - //Get the top level config as a dictionary - return new ConfigScope(el, propName); - } - catch (KeyNotFoundException) - { - throw new KeyNotFoundException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); - } + { + return TryGetConfig(plugin, propName) + ?? throw new ConfigurationException($"Missing required top level configuration object '{propName}', in host/plugin configuration files"); } /// @@ -154,7 +136,7 @@ namespace VNLib.Plugins.Extensions.Loading /// public static IConfigScope GetConfigForType(this PluginBase plugin, Type type) { - _ = type ?? throw new ArgumentNullException(nameof(type)); + ArgumentNullException.ThrowIfNull(type); string? configName = GetConfigNameForType(type); @@ -178,11 +160,12 @@ namespace VNLib.Plugins.Extensions.Loading public static T? GetProperty(this IConfigScope config, string property, Func getter) { //Check null - _ = config ?? throw new ArgumentNullException(nameof(config)); - _ = property ?? throw new ArgumentNullException(nameof(property)); - _ = getter ?? throw new ArgumentNullException(nameof(getter)); - - return !config.TryGetValue(property, out JsonElement el) ? default : getter(el); + ArgumentNullException.ThrowIfNull(config); + ArgumentNullException.ThrowIfNull(getter); + ArgumentException.ThrowIfNullOrWhiteSpace(property); + return !config.TryGetValue(property, out JsonElement el) + ? default + : getter(el); } /// @@ -194,7 +177,7 @@ namespace VNLib.Plugins.Extensions.Loading /// A function to get the value from the json type /// The property value /// - /// + /// public static T GetRequiredProperty(this IConfigScope config, string property, Func getter) { //Check null @@ -203,13 +186,32 @@ namespace VNLib.Plugins.Extensions.Loading ArgumentNullException.ThrowIfNull(getter); //Get the property - if (!config.TryGetValue(property, out JsonElement el)) - { - throw new KeyNotFoundException($"Missing required configuration property '{property}' in config {config.ScopeName}"); - } + bool hasValue = config.TryGetValue(property, out JsonElement el); + Validate.Assert(hasValue, $"Missing required configuration property '{property}' in config {config.ScopeName}"); + + T? value = getter(el); + Validate.Assert(value is not null, $"Missing required configuration property '{property}' in config {config.ScopeName}"); - //Even if the getter returns null, throw - return getter(el) ?? throw new KeyNotFoundException($"Missing required configuration property '{property}' in config {config.ScopeName}"); + //Attempt to validate if the configuration inherits the interface + TryValidateConfig(value); + + return value; + } + + + /// + /// Gets a required configuration property from the specified configuration scope + /// and deserializes the json type. + /// + /// + /// + /// The name of the property to get + /// The property value deserialzied into the desired object + /// + /// + public static T GetRequiredProperty(this IConfigScope config, string property) + { + return GetRequiredProperty(config, property, static p => p.Deserialize()!); } /// @@ -260,6 +262,28 @@ namespace VNLib.Plugins.Extensions.Loading return TryGetProperty(config, property, getter, out T? value) ? value : defaultValue; } + /// + /// Gets a configuration property from the specified configuration scope + /// and invokes your callback function on the element if found to transform the + /// output value, or returns the default value if the property is not found. + /// + /// + /// + /// The name of the configuration element to get + /// The default value to return + /// The property value returned from your getter callback, or the default value if not found + /// + [return: NotNullIfNotNull(nameof(defaultValue))] + public static T? GetValueOrDefault(this IConfigScope config, string property, T defaultValue) + { + return GetValueOrDefault( + config, + property, + static p => p.Deserialize(), + defaultValue + ); + } + /// /// Gets the configuration property name for the type /// @@ -289,7 +313,7 @@ namespace VNLib.Plugins.Extensions.Loading /// for missing configuration for a given type /// /// The type to raise exception for - /// + /// [DoesNotReturn] public static void ThrowConfigNotFoundForType(Type type) { @@ -297,11 +321,11 @@ namespace VNLib.Plugins.Extensions.Loading string? configName = GetConfigNameForType(type); if (configName != null) { - throw new KeyNotFoundException($"Missing required configuration key '{configName}' for type {type.Name}"); + throw new ConfigurationException($"Missing required configuration key '{configName}' for type {type.Name}"); } else { - throw new KeyNotFoundException($"Missing required configuration key for type {type.Name}"); + throw new ConfigurationException($"Missing required configuration key for type {type.Name}"); } } @@ -321,7 +345,7 @@ namespace VNLib.Plugins.Extensions.Loading /// /// Deserialzes the configuration to the desired object and calls its - /// method. Validation exceptions + /// method. Validation exceptions /// are wrapped in a /// /// @@ -331,14 +355,9 @@ namespace VNLib.Plugins.Extensions.Loading public static T DeserialzeAndValidate(this IConfigScope scope) where T : IOnConfigValidation { T conf = scope.Deserialze(); - try - { - conf.Validate(); - } - catch(Exception ex) - { - throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(T).Name}", ex); - } + + TryValidateConfig(conf); + return conf; } @@ -351,7 +370,6 @@ namespace VNLib.Plugins.Extensions.Loading /// True if the plugin config contains the require configuration property public static bool HasConfigForType(this PluginBase plugin) => HasConfigForType(plugin, typeof(T)); - /// /// Determines if the current plugin configuration contains the require properties to initialize /// the type @@ -373,7 +391,7 @@ namespace VNLib.Plugins.Extensions.Loading /// Gets a given configuration element from the global configuration scope /// and deserializes it into the desired type. /// - /// If the type inherits the + /// If the type inherits the /// method is invoked, and exceptions are warpped in /// /// @@ -388,23 +406,13 @@ namespace VNLib.Plugins.Extensions.Loading public static TConfig GetConfigElement(this PluginBase plugin) { //Deserialze the element - TConfig config = plugin.GetConfigForType().Deserialze(); + TConfig config = plugin.GetConfigForType() + .Deserialze(); - //If the type is validatable, validate it - if(config is IOnConfigValidation conf) - { - try - { - conf.Validate(); - } - catch (Exception ex) - { - throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); - } - } + TryValidateConfig(config); //If async config, load async - if(config is IAsyncConfigurable ac) + if (config is IAsyncConfigurable ac) { _ = plugin.ConfigureServiceAsync(ac); } @@ -416,7 +424,7 @@ namespace VNLib.Plugins.Extensions.Loading /// Gets a given configuration element from the global configuration scope /// and deserializes it into the desired type. /// - /// If the type inherits the + /// If the type inherits the /// method is invoked, and exceptions are warpped in /// /// @@ -432,31 +440,36 @@ namespace VNLib.Plugins.Extensions.Loading public static TConfig GetConfigElement(this PluginBase plugin, string elementName) { //Deserialze the element - TConfig config = plugin.GetConfig(elementName).Deserialze(); + TConfig config = plugin.GetConfig(elementName) + .Deserialze(); + + TryValidateConfig(config); + + //If async config, load async + if (config is IAsyncConfigurable ac) + { + _ = plugin.ConfigureServiceAsync(ac); + } + return config; + } + + private static void TryValidateConfig(TConfig config) + { //If the type is validatable, validate it if (config is IOnConfigValidation conf) { try { - conf.Validate(); + conf.OnValidate(); } catch (Exception ex) { throw new ConfigurationValidationException($"Configuration validation failed for type {typeof(TConfig).Name}", ex); } } - - //If async config, load async - if (config is IAsyncConfigurable ac) - { - _ = plugin.ConfigureServiceAsync(ac); - } - - return config; } - /// /// Attempts to load the basic S3 configuration variables required /// for S3 client access @@ -492,16 +505,39 @@ namespace VNLib.Plugins.Extensions.Loading /// /// /// The absolute path to the directory containing all plugins - public static string GetPluginsPath(this PluginBase plugin) + public static string[] GetPluginSearchDirs(this PluginBase plugin) { //Get global plugin config element IConfigScope config = plugin.GetConfig(PLUGINS_HOST_KEY); - //Get the plugins path or throw because it should ALWAYS be defined if this method is called - string pluginsPath = config[PLUGIN_PATH_KEY].GetString()!; + /* + * Hosts are allowed to define mutliple plugin loading paths. A + * single path is supported for compat. Multi path takes precidence + * of course so attempt to load a string array first + */ + + if (!config.TryGetValue("paths", out JsonElement searchPaths) + && !config.TryGetValue("path", out searchPaths)) + { + return []; + } - //Get absolute path - return Path.GetFullPath(pluginsPath); + if (searchPaths.ValueKind == JsonValueKind.Array) + { + //Get the plugins path or throw because it should ALWAYS be defined if this method is called + return searchPaths.EnumerateArray() + .Select(static p => p.GetString()!) + .Select(Path.GetFullPath) //Get absolute file paths + .ToArray(); + } + else if (searchPaths.ValueKind == JsonValueKind.String) + { + return [Path.GetFullPath(searchPaths.GetString()!)]; + } + else + { + return []; + } } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationException.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationException.cs new file mode 100644 index 0000000..edfb002 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationException.cs @@ -0,0 +1,42 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigurationException.cs +* +* ConfigurationException.cs is part of VNLib.Plugins.Extensions.Loading which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Extensions.Loading +{ + /// + /// A base plugin configuration exception + /// + public class ConfigurationException : Exception + { + public ConfigurationException(string message) : base(message) + { } + + public ConfigurationException(string message, Exception innerException) : base(message, innerException) + { } + public ConfigurationException() + { } + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationValidationException.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationValidationException.cs index ebf4d9e..cedc41a 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationValidationException.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Exceptions/ConfigurationValidationException.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -26,17 +26,18 @@ using System; namespace VNLib.Plugins.Extensions.Loading { + /// /// An exception raised when a configuration validation exception has occured /// - public class ConfigurationValidationException : Exception + public class ConfigurationValidationException : ConfigurationException { public ConfigurationValidationException(string message) : base(message) - {} + { } public ConfigurationValidationException(string message, Exception innerException) : base(message, innerException) - {} + { } public ConfigurationValidationException() - {} + { } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs index b65c5e6..ca897e6 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/LoadingExtensions.cs @@ -26,6 +26,7 @@ using System; using System.IO; using System.Linq; using System.Text.Json; +using System.Threading; using System.Reflection; using System.Runtime.Loader; using System.Threading.Tasks; @@ -104,22 +105,30 @@ namespace VNLib.Plugins.Extensions.Loading plugin.ThrowIfUnloaded(); ArgumentNullException.ThrowIfNull(assemblyName); + string[] searchDirs; + /* * Allow an assets directory to limit the scope of the search for the desired * assembly, otherwise search all plugins directories */ string? assetDir = plugin.GetAssetsPath(); - assetDir ??= plugin.GetPluginsPath(); + + searchDirs = assetDir is null + ? plugin.GetPluginSearchDirs() + : ([assetDir]); /* - * This should never happen since this method can only be called from a - * plugin context, which means this path was used to load the current plugin - */ - ArgumentNullException.ThrowIfNull(assetDir, "No plugin asset directory is defined for the current host configuration, this is likely a bug"); + * This should never happen since this method can only be called from a + * plugin context, which means this path was used to load the current plugin + */ + if (searchDirs.Length == 0) + { + throw new ConfigurationException("No plugin asset directory is defined for the current host configuration, this is likely a bug"); + } //Get the first file that matches the search file - return Directory.EnumerateFiles(assetDir, assemblyName, searchOption).FirstOrDefault(); + return searchDirs.SelectMany(d => Directory.EnumerateFiles(d, assemblyName, searchOption)).FirstOrDefault(); } /// @@ -260,9 +269,10 @@ namespace VNLib.Plugins.Extensions.Loading /// /// /// - public static void ThrowIfUnloaded(this PluginBase plugin) + public static void ThrowIfUnloaded(this PluginBase? plugin) { //See if the plugin was unlaoded + ArgumentNullException.ThrowIfNull(plugin); ObjectDisposedException.ThrowIf(plugin.UnloadToken.IsCancellationRequested, plugin); } @@ -294,6 +304,12 @@ namespace VNLib.Plugins.Extensions.Loading //Optional delay await Task.Delay(delayMs); + //If plugin unloads during delay, bail + if (plugin.UnloadToken.IsCancellationRequested) + { + return; + } + //Run on ts Task deferred = Task.Run(asyncTask); @@ -348,7 +364,7 @@ namespace VNLib.Plugins.Extensions.Loading static async Task WaitForUnload(PluginBase pb, Action callback) { //Wait for unload as a task on the threadpool to avoid deadlocks - await pb.UnloadToken.WaitHandle.WaitAsync() + await pb.UnloadToken.WaitHandle.NoSpinWaitAsync(Timeout.Infinite) .ConfigureAwait(false); callback(); @@ -649,9 +665,15 @@ namespace VNLib.Plugins.Extensions.Loading } catch(TargetInvocationException te) when (te.InnerException != null) { + FindNestedConfigurationException(te); FindAndThrowInnerException(te); throw; } + catch(Exception ex) + { + FindNestedConfigurationException(ex); + throw; + } Task? loading = null; @@ -747,6 +769,21 @@ namespace VNLib.Plugins.Extensions.Loading } } + internal static void FindNestedConfigurationException(Exception ex) + { + if(ex is ConfigurationException ce) + { + ExceptionDispatchInfo.Throw(ce); + } + + //Recurse + if(ex.InnerException is not null) + { + FindNestedConfigurationException(ex.InnerException); + } + + //No more exceptions + } private sealed class PluginLocalCache { diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs new file mode 100644 index 0000000..d47be22 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointLogNameAttribute.cs @@ -0,0 +1,41 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigurationExtensions.cs +* +* ConfigurationExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Extensions.Loading.Routing +{ + + /// + /// Defines configurable settings for an endpoint + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class EndpointLogNameAttribute(string logName) : Attribute + { + /// + /// The name of the logging scope for the endpoint + /// + public string LogName { get; } = logName; + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs new file mode 100644 index 0000000..a5ab355 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/EndpointPathAttribute.cs @@ -0,0 +1,40 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: ConfigurationExtensions.cs +* +* ConfigurationExtensions.cs is part of VNLib.Plugins.Extensions.Loading which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Extensions.Loading.Routing +{ + /// + /// Defines configurable settings for an endpoint + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class EndpointPathAttribute(string path) : Attribute + { + /// + /// Sets the endpoint path (or configuration template if set) + /// + public string Path { get; } = path; + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpControllerAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpControllerAttribute.cs new file mode 100644 index 0000000..94771ab --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpControllerAttribute.cs @@ -0,0 +1,36 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: HttpControllerAttribute.cs +* +* HttpControllerAttribute.cs is part of VNLib.Plugins.Extensions.Loading which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Plugins.Extensions.Loading.Routing.Mvc +{ + /// + /// Attribute to define a controller for http routing. The class must be decorated + /// with this attribute to be recognized as a controller + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class HttpControllerAttribute : Attribute + { } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpEndpointAttribute.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpEndpointAttribute.cs new file mode 100644 index 0000000..d89df63 --- /dev/null +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/Mvc/HttpEndpointAttribute.cs @@ -0,0 +1,52 @@ +/* +* Copyright (c) 2024 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Extensions.Loading +* File: HttpEndpointAttribute.cs +* +* HttpEndpointAttribute.cs is part of VNLib.Plugins.Extensions.Loading which is +* part of the larger VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Extensions.Loading is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Extensions.Loading 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 Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Net.Http; + +namespace VNLib.Plugins.Extensions.Loading.Routing.Mvc +{ + + /// + /// Attribute to define an http endpoint for a controller. The class + /// must be decorated with the attribute + /// + /// The endpoint path + /// The method (or methods) allowed to be filtered by this endpoint + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] + public sealed class HttpEndpointAttribute(string path, HttpMethod method) : Attribute + { + /// + /// The path of the endpoint + /// + public string Path { get; } = path; + + /// + /// The http method of the endpoint. You may set more than one method + /// for a given endpoint + /// + public HttpMethod Method { get; } = method; + } +} diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs index a1817a8..6665a75 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Routing/RoutingExtensions.cs @@ -24,10 +24,19 @@ using System; using System.Reflection; +using System.Threading.Tasks; +using System.Collections.Frozen; using System.Collections.Generic; +using System.Text.RegularExpressions; using System.Runtime.CompilerServices; +using VNLib.Net.Http; +using VNLib.Utils.Logging; +using VNLib.Utils.Resources; using VNLib.Plugins.Essentials.Runtime; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Extensions.Loading.Routing.Mvc; namespace VNLib.Plugins.Extensions.Loading.Routing { @@ -35,7 +44,7 @@ namespace VNLib.Plugins.Extensions.Loading.Routing /// /// Provides advanced QOL features to plugin loading /// - public static class RoutingExtensions + public static partial class RoutingExtensions { private static readonly ConditionalWeakTable _pluginRefs = new(); private static readonly ConditionalWeakTable _pluginEndpoints = new(); @@ -52,11 +61,14 @@ namespace VNLib.Plugins.Extensions.Loading.Routing T endpoint = plugin.CreateService(); //Route the endpoint - plugin.Route(endpoint); + Route(plugin, endpoint); //Store ref to plugin for endpoint _pluginRefs.Add(endpoint, plugin); + //Function that initalizes the endpoint's path and logging variables + InitEndpointSettings(plugin, endpoint); + return endpoint; } @@ -96,15 +108,119 @@ namespace VNLib.Plugins.Extensions.Loading.Routing { _ = _pluginRefs.TryGetValue(ep, out PluginBase? pBase); return pBase ?? throw new InvalidOperationException("Endpoint was not dynamically routed"); - } - + } + + private static readonly Regex ConfigSyntaxParser = ParserRegex(); + private delegate void InitFunc(string path, ILogProvider log); + + [GeneratedRegex("{{(.*?)}}", RegexOptions.Compiled)] + private static partial Regex ParserRegex(); + + private static void InitEndpointSettings(PluginBase plugin, T endpoint) where T : IEndpoint + { + //Load optional config + IConfigScope config = plugin.GetConfigForType(); + + ILogProvider logger = plugin.Log; + + EndpointPathAttribute? pathAttr = typeof(T).GetCustomAttribute(); + + /* + * gets the protected function for assigning the endpoint path + * and logger instance. + */ + InitFunc? initPathAndLog = ManagedLibrary.TryGetMethod(endpoint, "InitPathAndLog", BindingFlags.NonPublic); + + if (pathAttr is null || initPathAndLog is null) + { + return; + } + + string? logName = typeof(T).GetCustomAttribute()?.LogName; + + if (!string.IsNullOrWhiteSpace(logName)) + { + logger = plugin.Log.CreateScope(SubsituteValue(logName, config)); + } + try + { + + //Invoke init function and pass in variable names + initPathAndLog( + path: SubsituteValue(pathAttr.Path, config), + logger + ); + } + catch (ConfigurationException) + { + throw; + } + catch(Exception e) + { + throw new ConfigurationException($"Failed to initalize endpoint {endpoint.GetType().Name}", e); + } + + static string SubsituteValue(string pathVar, IConfigScope? config) + { + if (config is null) + { + return pathVar; + } + + // Replace the matched pattern with the corresponding value from the dictionary + return ConfigSyntaxParser.Replace(pathVar, match => + { + string varName = match.Groups[1].Value; + + //Get the value from the config scope or return the original variable unmodified + return config.GetValueOrDefault(varName, varName); + }); + } + } private sealed class EndpointCollection : IVirtualEndpointDefinition { public List Endpoints { get; } = new(); /// - IEnumerable IVirtualEndpointDefinition.GetEndpoints() => Endpoints; + IEnumerable IVirtualEndpointDefinition.GetEndpoints() => Endpoints; + } + + + private delegate ValueTask EndpointWorkFunc(HttpEntity entity); + + sealed record class HttpControllerEndpoint(MethodInfo MethodInfo, HttpEndpointAttribute Attr) + { + public string Path => Attr.Path; + + public HttpMethod Method => Attr.Method; + + public EndpointWorkFunc Func { get; } = MethodInfo.CreateDelegate(); + } + + private sealed class EndpointWrapper + : ResourceEndpointBase + { + + private readonly FrozenDictionary _wrappers; + + public EndpointWrapper(FrozenDictionary table, string path, ILogProvider log) + { + _wrappers = table; + InitPathAndLog(path, log); + } + + protected override ValueTask OnProcessAsync(HttpEntity entity) + { + ref readonly EndpointWorkFunc func = ref _wrappers.GetValueRefOrNullRef(entity.Server.Method); + + if (Unsafe.IsNullRef(in func)) + { + return ValueTask.FromResult(VfReturnType.ProcessAsFile); + } + + return func(entity); + } } } } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs index 11f101f..0574cb0 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/S3Config.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Extensions.Loading @@ -26,7 +26,12 @@ using System.Text.Json.Serialization; namespace VNLib.Plugins.Extensions.Loading { - public sealed class S3Config + + /// + /// A common json-serializable configuration for S3 storage + /// in an attempt to unify S3 configuration. + /// + public class S3Config { [JsonPropertyName("server_address")] public string? ServerAddress { get; init; } diff --git a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs index 885f22f..fae22c8 100644 --- a/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs +++ b/lib/VNLib.Plugins.Extensions.Loading/src/Secrets/HCVaultClient.cs @@ -163,7 +163,7 @@ namespace VNLib.Plugins.Extensions.Loading using HttpResponseMessage response = await _client.SendAsync(ms, HttpCompletionOption.ResponseHeadersRead); //Check if an error occured in the response - await ProcessVaultErrorResponseAsync(response); + await ProcessVaultErrorResponseAsync(secretName, response); //Read the response async using SecretResponse res = await ReadSecretResponse(response.Content); @@ -266,7 +266,7 @@ namespace VNLib.Plugins.Extensions.Loading return null; } - private static ValueTask ProcessVaultErrorResponseAsync(HttpResponseMessage response) + private static ValueTask ProcessVaultErrorResponseAsync(string secretName, HttpResponseMessage response) { if (response.IsSuccessStatusCode) { @@ -278,7 +278,7 @@ namespace VNLib.Plugins.Extensions.Loading if(!ctLen.HasValue || ctLen.Value == 0) { return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secret from vault with error code {response.StatusCode}") + new HttpRequestException($"Failed to fetch secret '{secretName}' from vault with error code {response.StatusCode}") ); } @@ -300,15 +300,15 @@ namespace VNLib.Plugins.Extensions.Loading ); } - return ExceptionsFromContentAsync(response); + return ExceptionsFromContentAsync(secretName, response); - static ValueTask ExceptionFromVaultErrors(HttpStatusCode code, VaultErrorMessage? errs) + static ValueTask ExceptionFromVaultErrors(string secretName, HttpStatusCode code, VaultErrorMessage? errs) { //If the error message is null, raise an exception if (errs?.Errors is null || errs.Errors.Length == 0) { return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secret from vault with error code {code}") + new HttpRequestException($"Failed to fetch secret '{secretName}' from vault with error code {code}") ); } @@ -318,17 +318,17 @@ namespace VNLib.Plugins.Extensions.Loading //Finally raise the exception with all the returned errors return ValueTask.FromException( - new HttpRequestException($"Failed to fetch secre from vault with {code}, errors:\n {errStr}") + new HttpRequestException($"Failed to fetch secret `{secretName}` from vault with {code}, errors:\n {errStr}") ); } - static async ValueTask ExceptionsFromContentAsync(HttpResponseMessage response) + static async ValueTask ExceptionsFromContentAsync(string secretName, HttpResponseMessage response) { //Read stream async and deserialize async using Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); VaultErrorMessage? errs = await JsonSerializer.DeserializeAsync(stream); - await ExceptionFromVaultErrors(response.StatusCode, errs); + await ExceptionFromVaultErrors(secretName, response.StatusCode, errs); } } diff --git a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj index 754bec6..d39cd98 100644 --- a/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj +++ b/lib/VNLib.Plugins.Extensions.Validation/src/VNLib.Plugins.Extensions.Validation.csproj @@ -48,7 +48,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj index 753deaf..b27fc00 100644 --- a/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj +++ b/lib/sql-providers/sqlite/VNLib.Plugins.Extensions.Loading.Sql.SQLite/src/VNLib.Plugins.Extensions.Loading.Sql.SQLite.csproj @@ -49,7 +49,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj index 97c6096..7015b8c 100644 --- a/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj +++ b/lib/sql-providers/sqlserver/VNLib.Plugins.Extensions.Loading.Sql.SQLServer/src/VNLib.Plugins.Extensions.Loading.Sql.SQLServer.csproj @@ -49,7 +49,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + -- cgit