aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.Content.Routing/src
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Content.Routing/src')
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/Route.cs71
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/RouteStore.cs68
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/RoutingContext.cs41
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/PageRouterEntry.cs68
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/RouteComparer.cs77
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs161
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj55
7 files changed, 541 insertions, 0 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/Route.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/Route.cs
new file mode 100644
index 0000000..8c52725
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/Route.cs
@@ -0,0 +1,71 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Content.Routing
+* File: Route.cs
+*
+* Route.cs is part of VNLib.Plugins.Essentials.Content.Routing which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Content.Routing 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.Essentials.Content.Routing 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.ComponentModel.DataAnnotations.Schema;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+
+namespace VNLib.Plugins.Essentials.Content.Routing.Model
+{
+ [Index(nameof(Id), IsUnique = true)]
+ internal class Route : DbModelBase
+ {
+ public override string Id { get; set; }
+ public override DateTime Created { get; set; }
+ public override DateTime LastModified { get; set; }
+
+ public string Hostname { get; set; }
+ public string MatchPath { get; set; }
+ [Column("Privilage")]
+ public long _privilage
+ {
+ get => (long)Privilage;
+ set => Privilage = (ulong)value;
+ }
+ [NotMapped]
+ public ulong Privilage { get; set; }
+
+ public string Alternate { get; set; }
+ public FpRoutine Routine { get; set; }
+
+ /// <summary>
+ /// The processing arguments that match the route
+ /// </summary>
+ [NotMapped]
+ public FileProcessArgs MatchArgs
+ {
+ get
+ {
+ return new FileProcessArgs()
+ {
+ Alternate = this.Alternate,
+ Routine = (FpRoutine) Routine
+ };
+ }
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/RouteStore.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/RouteStore.cs
new file mode 100644
index 0000000..e623228
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/RouteStore.cs
@@ -0,0 +1,68 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Content.Routing
+* File: RouteStore.cs
+*
+* RouteStore.cs is part of VNLib.Plugins.Essentials.Content.Routing which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Content.Routing 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.Essentials.Content.Routing 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.Linq;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+
+namespace VNLib.Plugins.Essentials.Content.Routing.Model
+{
+ internal class RouteStore : DbStore<Route>
+ {
+ private readonly DbContextOptions Options;
+
+ public RouteStore(DbContextOptions options)
+ {
+ Options = options;
+ }
+
+ public override string RecordIdBuilder => Guid.NewGuid().ToString("N");
+
+ protected override IQueryable<Route> GetCollectionQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ string hostname = constraints[0];
+ return from route in context.Set<Route>()
+ where route.Hostname == hostname
+ select route;
+ }
+
+ protected override IQueryable<Route> GetSingleQueryBuilder(TransactionalDbContext context, params string[] constraints)
+ {
+ string id = constraints[0];
+ return from route in context.Set<Route>()
+ where route.Id == id
+ select route;
+ }
+
+ public override TransactionalDbContext NewContext() => new RoutingContext(Options);
+
+ protected override void OnRecordUpdate(Route newRecord, Route currentRecord)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/RoutingContext.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/RoutingContext.cs
new file mode 100644
index 0000000..0cbd90f
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/RoutingContext.cs
@@ -0,0 +1,41 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Content.Routing
+* File: RoutingContext.cs
+*
+* RoutingContext.cs is part of VNLib.Plugins.Essentials.Content.Routing which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Content.Routing 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.Essentials.Content.Routing 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;
+
+using VNLib.Plugins.Extensions.Data;
+
+namespace VNLib.Plugins.Essentials.Content.Routing.Model
+{
+ internal class RoutingContext : TransactionalDbContext
+ {
+ public DbSet<Route> Routes { get; set; }
+
+ public RoutingContext(DbContextOptions options) :base(options)
+ {
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/PageRouterEntry.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/PageRouterEntry.cs
new file mode 100644
index 0000000..10b7075
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/PageRouterEntry.cs
@@ -0,0 +1,68 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Content.Routing
+* File: PageRouterEntry.cs
+*
+* PageRouterEntry.cs is part of VNLib.Plugins.Essentials.Content.Routing which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Content.Routing 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.Essentials.Content.Routing 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.Threading.Tasks;
+using System.Collections.Generic;
+
+using VNLib.Utils.Logging;
+
+namespace VNLib.Plugins.Essentials.Content.Routing
+{
+ public sealed class PageRouterEntry : PluginBase, IPageRouter
+ {
+ public override string PluginName => "Essentials.Router";
+
+ private Router PageRouter;
+ public ValueTask<FileProcessArgs> RouteAsync(HttpEntity entity) => PageRouter.RouteAsync(entity);
+
+ protected override void OnLoad()
+ {
+ try
+ {
+ //Init router
+ PageRouter = new(this);
+ Log.Information("Plugin loaded");
+ }
+ catch (KeyNotFoundException knf)
+ {
+ Log.Error("Plugin failed to load, missing required configuration variables {err}", knf.Message);
+ }
+ }
+
+ protected override void OnUnLoad()
+ {
+ Log.Information("Plugin unloaded");
+ }
+
+ protected override void ProcessHostCommand(string cmd)
+ {
+ if(cmd.Contains("reset", StringComparison.OrdinalIgnoreCase))
+ {
+ PageRouter?.ResetRoutes();
+ Log.Information("Routing table reset");
+ }
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/RouteComparer.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/RouteComparer.cs
new file mode 100644
index 0000000..189da62
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/RouteComparer.cs
@@ -0,0 +1,77 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Content.Routing
+* File: RouteComparer.cs
+*
+* RouteComparer.cs is part of VNLib.Plugins.Essentials.Content.Routing which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Content.Routing 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.Essentials.Content.Routing 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.Collections.Generic;
+
+using VNLib.Plugins.Essentials.Content.Routing.Model;
+
+using static VNLib.Plugins.Essentials.Accounts.AccountManager;
+
+namespace VNLib.Plugins.Essentials.Content.Routing
+{
+ /// <summary>
+ /// Sorts routing rules based on closest match path/hostname routing along with privilage priority
+ /// </summary>
+ internal class RouteComparer : IComparer<Route>
+ {
+ //The idea is that hostnames without wildcards are exact, and hostnames with wildcards are "catch all"
+ public int Compare(Route x, Route y)
+ {
+ int val = 0;
+ //If x contains a wildcard in the hostname, then it is less than y
+ if (x.Hostname.Contains('*'))
+ {
+ val--;
+ }
+ //If y containts a wildcard, then y is less than x
+ if (y.Hostname.Contains('*'))
+ {
+ val++;
+ }
+ //If there was no wildcard, check paths
+ if (val == 0)
+ {
+ //If x containts a wildcard in the path, then x is less than y
+ if (x.MatchPath.Contains('*'))
+ {
+ val--;
+ }
+ //If y containts a wildcard in the path, then y is less than x
+ if (y.MatchPath.Contains('*'))
+ {
+ val++;
+
+ }
+ }
+ //If hostnames and paths are stil equal, check privilage level
+ if (val == 0)
+ {
+ //Higher privilage routine is greater than lower privilage
+ val = (x.Privilage & LEVEL_MSK) > (y.Privilage & LEVEL_MSK) ? 1 : -1;
+ }
+ //If both contain (or are) wildcards, then they are equal
+ return val;
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs
new file mode 100644
index 0000000..4dc320a
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs
@@ -0,0 +1,161 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Content.Routing
+* File: Router.cs
+*
+* Router.cs is part of VNLib.Plugins.Essentials.Content.Routing which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Content.Routing 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.Essentials.Content.Routing 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.Linq;
+using System.Buffers;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+using System.Collections.Concurrent;
+using System.Collections.ObjectModel;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Logging;
+using VNLib.Plugins.Extensions.Loading.Sql;
+using VNLib.Plugins.Extensions.Loading.Events;
+using VNLib.Plugins.Essentials.Content.Routing.Model;
+using static VNLib.Plugins.Essentials.Accounts.AccountManager;
+
+namespace VNLib.Plugins.Essentials.Content.Routing
+{
+ internal class Router : IPageRouter, IIntervalScheduleable
+ {
+ private static readonly RouteComparer Comparer = new();
+
+ private readonly RouteStore Store;
+
+ private readonly ConcurrentDictionary<IWebRoot, Task<ReadOnlyCollection<Route>>> RouteTable;
+
+ public Router(PluginBase plugin)
+ {
+ Store = new(plugin.GetContextOptions());
+ plugin.ScheduleInterval(this, TimeSpan.FromSeconds(30));
+ RouteTable = new();
+ }
+
+ ///<inheritdoc/>
+ public async ValueTask<FileProcessArgs> RouteAsync(HttpEntity entity)
+ {
+ ulong privilage = READ_MSK;
+ //Only select privilages for logged-in users
+ if (entity.Session.IsSet && entity.LoginCookieMatches() || entity.TokenMatches())
+ {
+ privilage = entity.Session.Privilages;
+ }
+ //Get the routing table for the current host
+ ReadOnlyCollection<Route> routes = await RouteTable.GetOrAdd(entity.RequestedRoot, LoadRoutesAsync);
+ //Find the proper routine for the connection
+ return FindArgs(routes, entity.RequestedRoot.Hostname, entity.Server.Path, privilage);
+ }
+
+ /// <summary>
+ /// Clears all cached routines from the database
+ /// </summary>
+ public void ResetRoutes() => RouteTable.Clear();
+
+ private async Task<ReadOnlyCollection<Route>> LoadRoutesAsync(IWebRoot root)
+ {
+ List<Route> collection = new();
+ //Load all routes
+ _ = await Store.GetPageAsync(collection, 0, int.MaxValue);
+ //Select only exact match routes, or wildcard routes
+ return (from r in collection
+ where r.Hostname.EndsWith(root.Hostname, StringComparison.OrdinalIgnoreCase) || r.Hostname == "*"
+ //Orderby path "specificity" longer pathts are generally more specific, so filter order
+ orderby r.MatchPath.Length ascending
+ select r)
+ .ToList()
+ .AsReadOnly();
+ }
+
+
+ private static FileProcessArgs FindArgs(ReadOnlyCollection<Route> routes, string hostname, string path, ulong privilages)
+ {
+ //Rent an array to sort routes for the current user
+ Route[] matchArray = ArrayPool<Route>.Shared.Rent(routes.Count);
+ int count = 0;
+ //Search for routes that match
+ for(int i = 0; i < routes.Count; i++)
+ {
+ if(Matches(routes[i], hostname, path, privilages))
+ {
+ //Add to sort array
+ matchArray[count++] = routes[i];
+ }
+ }
+ //If no matches are found, return continue routine
+ if (count == 0)
+ {
+ //Return the array to the pool
+ ArrayPool<Route>.Shared.Return(matchArray);
+ return FileProcessArgs.Continue;
+ }
+ //Get sorting span for matches
+ Span<Route> found = matchArray.AsSpan(0, count);
+ //Sort the found rules
+ found.Sort(Comparer);
+ //Select the last element
+ Route selected = found[^1];
+ //Return array to pool
+ ArrayPool<Route>.Shared.Return(matchArray);
+ return selected.MatchArgs;
+ }
+
+ /// <summary>
+ /// Determines if a route can be matched to a hostname, resource path, and a
+ /// privilage level
+ /// </summary>
+ /// <param name="route">The route to test against</param>
+ /// <param name="hostname">The hostname to test</param>
+ /// <param name="path">The resource path to test</param>
+ /// <param name="privilages">The privialge level to search for</param>
+ /// <returns>True if the route can be matched to the resource and the privialge level</returns>
+ private static bool Matches(Route route, ReadOnlySpan<char> hostname, ReadOnlySpan<char> path, ulong privilages)
+ {
+ //Get span of hostname to stop string heap allocations during comparisons
+ ReadOnlySpan<char> routineHost = route.Hostname;
+ ReadOnlySpan<char> routinePath = route.MatchPath;
+ //Test if hostname hostname matches exactly (may be wildcard) or hostname begins with a wildcard and ends with the request hostname
+ bool hostMatch = routineHost.SequenceEqual(hostname) || (routineHost.Length > 1 && routineHost[0] == '*' && hostname.EndsWith(routineHost[1..]));
+ if (!hostMatch)
+ {
+ return false;
+ }
+ //Test if path is a wildcard, matches exactly, or if the path is a wildcard path, that the begining of the reqest path matches the routine path
+ bool pathMatch = routinePath == "*" || routinePath.SequenceEqual(path) || (routinePath.Length > 1 && routinePath[^1] == '*' && path.StartsWith(routinePath[..^1]));
+ if (!pathMatch)
+ {
+ return false;
+ }
+ //Test if the level and group privilages match for the current routine
+ return (privilages & LEVEL_MSK) >= (route.Privilage & LEVEL_MSK) && (route.Privilage & GROUP_MSK) == (privilages & GROUP_MSK);
+ }
+
+ Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj
new file mode 100644
index 0000000..7052da0
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/VNLib.Plugins.Essentials.Content.Routing.csproj
@@ -0,0 +1,55 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Authors>Vaughn Nugent</Authors>
+ <Version>1.0.1.1</Version>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com</PackageProjectUrl>
+ <AssemblyName>PageRouter</AssemblyName>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <!-- Resolve nuget dll files and store them in the output dir -->
+ <PropertyGroup>
+ <!--Enable dynamic loading-->
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ <Nullable>enable</Nullable>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="ErrorProne.NET.CoreAnalyzers" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="ErrorProne.NET.Structs" Version="0.1.2">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\..\core\lib\Plugins.PluginBase\src\VNLib.Plugins.PluginBase.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Data\src\VNLib.Plugins.Extensions.Data.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Update="PageRouter.json">
+ <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;F:\Programming\vnlib\devplugins\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>