diff --git a/Elsa.sln b/Elsa.sln index 262964ec7..b27c57c75 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -358,6 +358,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ElsaDashboard.Samples.Blazo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elsa.Samples.HttpEndpointSecurity", "src\samples\aspnet\Elsa.Samples.HttpEndpointSecurity\Elsa.Samples.HttpEndpointSecurity.csproj", "{82B115DA-E3D0-49D7-AD08-DE9387656756}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.WatchDirectoryWorker", "src\samples\worker\Elsa.Samples.WatchDirectoryWorker\Elsa.Samples.WatchDirectoryWorker.csproj", "{8EC3CF51-EBC1-4B45-881E-4B1B8190D579}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Samples.InfiniteLoopDetection", "src\samples\aspnet\Elsa.Samples.InfiniteLoopDetection\Elsa.Samples.InfiniteLoopDetection.csproj", "{A2572DBF-AF11-4A02-AF99-B343871F2D85}" EndProject Global @@ -846,6 +847,10 @@ Global {82B115DA-E3D0-49D7-AD08-DE9387656756}.Debug|Any CPU.Build.0 = Debug|Any CPU {82B115DA-E3D0-49D7-AD08-DE9387656756}.Release|Any CPU.ActiveCfg = Release|Any CPU {82B115DA-E3D0-49D7-AD08-DE9387656756}.Release|Any CPU.Build.0 = Release|Any CPU + {8EC3CF51-EBC1-4B45-881E-4B1B8190D579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8EC3CF51-EBC1-4B45-881E-4B1B8190D579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8EC3CF51-EBC1-4B45-881E-4B1B8190D579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8EC3CF51-EBC1-4B45-881E-4B1B8190D579}.Release|Any CPU.Build.0 = Release|Any CPU {A2572DBF-AF11-4A02-AF99-B343871F2D85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A2572DBF-AF11-4A02-AF99-B343871F2D85}.Debug|Any CPU.Build.0 = Debug|Any CPU {A2572DBF-AF11-4A02-AF99-B343871F2D85}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1009,6 +1014,7 @@ Global {DCB3C4DD-2B7D-44E9-A366-F36F4ABFF488} = {D86B94DC-A53C-4A67-A820-828DD359C49B} {9A4B1C48-16FB-4A65-AE98-5CDE7BCB506C} = {D86B94DC-A53C-4A67-A820-828DD359C49B} {82B115DA-E3D0-49D7-AD08-DE9387656756} = {22E75696-6FE9-436A-9097-EE21C603F818} + {8EC3CF51-EBC1-4B45-881E-4B1B8190D579} = {E42743A0-FBDD-4150-9D53-6000496D9B87} {A2572DBF-AF11-4A02-AF99-B343871F2D85} = {22E75696-6FE9-436A-9097-EE21C603F818} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/activities/Elsa.Activities.File/Activities/DeleteFile/DeleteFile.cs b/src/activities/Elsa.Activities.File/Activities/DeleteFile/DeleteFile.cs index 5858d71dd..f5d67cb2f 100644 --- a/src/activities/Elsa.Activities.File/Activities/DeleteFile/DeleteFile.cs +++ b/src/activities/Elsa.Activities.File/Activities/DeleteFile/DeleteFile.cs @@ -2,6 +2,7 @@ using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Services; using Elsa.Services.Models; +using System.ComponentModel.DataAnnotations; // ReSharper disable once CheckNamespace namespace Elsa.Activities.File @@ -11,8 +12,9 @@ namespace Elsa.Activities.File Outcomes = new[] { OutcomeNames.Done })] public class DeleteFile : Activity { + [Required] [ActivityInput(Hint = "Path of the file to delete.")] - public string Path { get; set; } = default!; + public string? Path { get; set; } protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context) { diff --git a/src/activities/Elsa.Activities.File/Activities/EnumeratreFiles/EnumerateFiles.cs b/src/activities/Elsa.Activities.File/Activities/EnumeratreFiles/EnumerateFiles.cs index 414ae1748..4ebb5292b 100644 --- a/src/activities/Elsa.Activities.File/Activities/EnumeratreFiles/EnumerateFiles.cs +++ b/src/activities/Elsa.Activities.File/Activities/EnumeratreFiles/EnumerateFiles.cs @@ -3,6 +3,7 @@ using Elsa.Attributes; using Elsa.Services; using Elsa.Services.Models; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.IO; // ReSharper disable once CheckNamespace @@ -13,20 +14,21 @@ namespace Elsa.Activities.File Outcomes = new[] { OutcomeNames.Done })] public class EnumerateFiles : Activity { + [Required] [ActivityInput(Hint = "Path of the folder to enumerate files.")] public string Path { get; set; } = default!; [ActivityInput(Hint = "Pattern for files to return.")] public string? Pattern { get; set; } - [ActivityInput(Hint = "Ignore inaccessible files.", Label = "Ignore Inaccessible")] - public bool IgnoreInaccessible { get; set; } = true; + [ActivityInput(Hint = "Ignore inaccessible files.", Label = "Ignore Inaccessible", DefaultValue = true)] + public bool IgnoreInaccessible { get; set; } - [ActivityInput(Hint = "Set case sensitivity.", Label = "Match Casing")] - public MatchCasing MatchCasing { get; set; } = MatchCasing.CaseInsensitive; + [ActivityInput(Hint = "Set case sensitivity.", Label = "Match Casing", DefaultValue = MatchCasing.CaseInsensitive)] + public MatchCasing MatchCasing { get; set; } - [ActivityInput(Hint = "Return files from sub directories", Label = "Sub Directories")] - public bool SubDirectories { get; set; } = false; + [ActivityInput(Hint = "Return files from sub directories", Label = "Sub Directories", DefaultValue = false)] + public bool SubDirectories { get; set; } [ActivityOutput(Hint = "List of files.")] public IEnumerable? Files { get; set; } diff --git a/src/activities/Elsa.Activities.File/Activities/FileExists/FileExists.cs b/src/activities/Elsa.Activities.File/Activities/FileExists/FileExists.cs index f9ccc880d..c16e94515 100644 --- a/src/activities/Elsa.Activities.File/Activities/FileExists/FileExists.cs +++ b/src/activities/Elsa.Activities.File/Activities/FileExists/FileExists.cs @@ -1,7 +1,9 @@ using Elsa.ActivityResults; using Elsa.Attributes; +using Elsa.Design; using Elsa.Services; using Elsa.Services.Models; +using System.ComponentModel.DataAnnotations; // ReSharper disable once CheckNamespace namespace Elsa.Activities.File @@ -11,8 +13,9 @@ namespace Elsa.Activities.File Outcomes = new[] { OutcomeNames.True, OutcomeNames.False })] public class FileExists : Activity { + [Required] [ActivityInput(Hint = "Path of the file to delete.")] - public string Path { get; set; } = default!; + public string? Path { get; set; } protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context) { diff --git a/src/activities/Elsa.Activities.File/Activities/OutFile/OutFile.cs b/src/activities/Elsa.Activities.File/Activities/OutFile/OutFile.cs index 66987e3b2..2b6beb822 100644 --- a/src/activities/Elsa.Activities.File/Activities/OutFile/OutFile.cs +++ b/src/activities/Elsa.Activities.File/Activities/OutFile/OutFile.cs @@ -5,6 +5,7 @@ using Elsa.Expressions; using Elsa.Services; using Elsa.Services.Models; using System; +using System.ComponentModel.DataAnnotations; using System.IO; using System.Threading.Tasks; @@ -16,14 +17,16 @@ namespace Elsa.Activities.File Outcomes = new[] { OutcomeNames.Done })] public class OutFile : Activity { + [Required] [ActivityInput(Hint = "Bytes to write to file.", SupportedSyntaxes = new[] { SyntaxNames.JavaScript }, DefaultSyntax = SyntaxNames.JavaScript)] - public byte[] Bytes { get; set; } = default!; + public byte[]? Bytes { get; set; } + [Required] [ActivityInput(Hint = "Path to create file at.", UIHint = ActivityInputUIHints.SingleLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] - public string Path { get; set; } = default!; + public string? Path { get; set; } - [ActivityInput(Hint = "How the output file should be written to.", UIHint = ActivityInputUIHints.Dropdown)] - public CopyMode Mode { get; set; } = CopyMode.CreateNew; + [ActivityInput(Hint = "How the output file should be written to.", UIHint = ActivityInputUIHints.Dropdown, DefaultValue = CopyMode.CreateNew)] + public CopyMode Mode { get; set; } protected override async ValueTask OnExecuteAsync(ActivityExecutionContext context) { diff --git a/src/activities/Elsa.Activities.File/Activities/ReadFile/ReadFile.cs b/src/activities/Elsa.Activities.File/Activities/ReadFile/ReadFile.cs index 3a7a145bf..ec653f5cf 100644 --- a/src/activities/Elsa.Activities.File/Activities/ReadFile/ReadFile.cs +++ b/src/activities/Elsa.Activities.File/Activities/ReadFile/ReadFile.cs @@ -4,6 +4,7 @@ using Elsa.Design; using Elsa.Expressions; using Elsa.Services; using Elsa.Services.Models; +using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; // ReSharper disable once CheckNamespace @@ -14,6 +15,7 @@ namespace Elsa.Activities.File Outcomes = new[] { OutcomeNames.Done })] public class ReadFile : Activity { + [Required] [ActivityInput(Hint = "Path to read content from.", UIHint = ActivityInputUIHints.SingleLine, SupportedSyntaxes = new[] { SyntaxNames.JavaScript, SyntaxNames.Liquid })] public string Path { get; set; } = default!; @@ -23,7 +25,7 @@ namespace Elsa.Activities.File public override async ValueTask ExecuteAsync(ActivityExecutionContext context) { Bytes = await System.IO.File.ReadAllBytesAsync(Path); - return Done(); + return Done(Bytes); } } } diff --git a/src/activities/Elsa.Activities.File/Activities/ReadFile/ReadFileBuilderExtensions.cs b/src/activities/Elsa.Activities.File/Activities/ReadFile/ReadFileBuilderExtensions.cs index c6f7bfc40..600169471 100644 --- a/src/activities/Elsa.Activities.File/Activities/ReadFile/ReadFileBuilderExtensions.cs +++ b/src/activities/Elsa.Activities.File/Activities/ReadFile/ReadFileBuilderExtensions.cs @@ -10,34 +10,34 @@ namespace Elsa.Activities.File { public static class ReadFileBuilderExtensions { - public static IActivityBuilder ReadFile(this IBuilder builder, Action> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => builder.Then(setup, null, lineNumber, sourceFile); + public static IActivityBuilder ReadFile(this IBuilder builder, Action> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => builder.Then(setup, null, lineNumber, sourceFile); public static IActivityBuilder ReadFile(this IBuilder builder, Func path, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => - builder.OutFile(activity => activity + builder.ReadFile(activity => activity .Set(x => x.Path, path), lineNumber, sourceFile); public static IActivityBuilder ReadFile(this IBuilder builder, Func> path, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => - builder.OutFile(activity => activity + builder.ReadFile(activity => activity .Set(x => x.Path, path), lineNumber, sourceFile); public static IActivityBuilder ReadFile(this IBuilder builder, Func path, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => - builder.OutFile(activity => activity + builder.ReadFile(activity => activity .Set(x => x.Path, path), lineNumber, sourceFile); public static IActivityBuilder ReadFile(this IBuilder builder, Func> path, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => - builder.OutFile(activity => activity + builder.ReadFile(activity => activity .Set(x => x.Path, path), lineNumber, sourceFile); public static IActivityBuilder ReadFile(this IBuilder builder, string? path, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => - builder.OutFile(activity => activity + builder.ReadFile(activity => activity .Set(x => x.Path, path), lineNumber, sourceFile); diff --git a/src/activities/Elsa.Activities.File/Activities/TempFile/TempFile.cs b/src/activities/Elsa.Activities.File/Activities/TempFile/TempFile.cs index 00c2bbd3c..95fb27e0a 100644 --- a/src/activities/Elsa.Activities.File/Activities/TempFile/TempFile.cs +++ b/src/activities/Elsa.Activities.File/Activities/TempFile/TempFile.cs @@ -1,6 +1,7 @@ using Elsa.ActivityResults; using Elsa.Attributes; using Elsa.Services; +using System.ComponentModel.DataAnnotations; // ReSharper disable once CheckNamespace namespace Elsa.Activities.File @@ -10,6 +11,7 @@ namespace Elsa.Activities.File Outcomes = new[] { OutcomeNames.Done })] public class TempFile : Activity { + [Required] [ActivityOutput(Hint = "Path of the created temporary file.")] public string Path { get; set; } = default!; diff --git a/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectory.cs b/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectory.cs new file mode 100644 index 000000000..51ba5542f --- /dev/null +++ b/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectory.cs @@ -0,0 +1,52 @@ +using Elsa.Activities.File.Models; +using Elsa.ActivityResults; +using Elsa.Attributes; +using Elsa.Design; +using Elsa.Services; +using Elsa.Services.Models; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Elsa.Activities.File +{ + [Trigger(Category = "File", + Description = "Triggers when files are created/modified in the given folder", + Outcomes = new[] { OutcomeNames.Done })] + public class WatchDirectory : Activity + { + [Required] + [ActivityInput(Hint = "The path of the directory to watch")] + public string? Path { get; set; } + + [ActivityInput(Hint = "The file pattern for interested files")] + public string? Pattern { get; set; } + + [ActivityInput(Label = "Change Types", Hint = "The types of file system events to subscribe to", UIHint = ActivityInputUIHints.CheckList, DefaultValue = WatcherChangeTypes.All)] + public WatcherChangeTypes ChangeTypes { get; set; } + + [ActivityInput(Label = "Notify Filters", UIHint = ActivityInputUIHints.CheckList, DefaultValue = NotifyFilters.Attributes | + NotifyFilters.CreationTime | + NotifyFilters.DirectoryName | + NotifyFilters.FileName | + NotifyFilters.LastAccess | + NotifyFilters.LastWrite | + NotifyFilters.Security | + NotifyFilters.Size)] + public NotifyFilters NotifyFilters { get; set; } + + [ActivityOutput] + public FileSystemEvent? Output { get; set; } + + protected override IActivityExecutionResult OnExecute(ActivityExecutionContext context) + { + var model = (FileSystemEvent)context.Input!; + Output = model; + return Done(Output); + } + } +} diff --git a/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectoryBuilderExtensions.cs b/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectoryBuilderExtensions.cs new file mode 100644 index 000000000..aad122344 --- /dev/null +++ b/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectoryBuilderExtensions.cs @@ -0,0 +1,13 @@ +using Elsa.Builders; +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Elsa.Activities.File +{ + public static class WatchDirectoryBuilderExtensions + { + public static IActivityBuilder WatchDirectory(this IActivityBuilder builder, Action> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => builder.Then(setup, null, lineNumber, sourceFile); + } +} diff --git a/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectoryExtensions.cs b/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectoryExtensions.cs new file mode 100644 index 000000000..f5339b141 --- /dev/null +++ b/src/activities/Elsa.Activities.File/Activities/WatchDirectory/WatchDirectoryExtensions.cs @@ -0,0 +1,45 @@ +using Elsa.Builders; +using Elsa.Services.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; + +namespace Elsa.Activities.File +{ + public static class WatchDirectoryExtensions + { + #region ChangeTypes + public static ISetupActivity WithChangeTypes(this ISetupActivity setup, Func> changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes); + public static ISetupActivity WithChangeTypes(this ISetupActivity setup, Func changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes); + public static ISetupActivity WithChangeTypes(this ISetupActivity setup, Func> changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes); + public static ISetupActivity WithChangeTypes(this ISetupActivity setup, Func changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes); + public static ISetupActivity WithChangeTypes(this ISetupActivity setup, WatcherChangeTypes changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes); + #endregion + + #region NotifyFilters + public static ISetupActivity WithNotifyFilters(this ISetupActivity setup, Func> notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters); + public static ISetupActivity WithNotifyFilters(this ISetupActivity setup, Func notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters); + public static ISetupActivity WithNotifyFilters(this ISetupActivity setup, Func> notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters); + public static ISetupActivity WithNotifyFilters(this ISetupActivity setup, Func notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters); + public static ISetupActivity WithNotifyFilters(this ISetupActivity setup, NotifyFilters notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters); + #endregion + + #region Path + public static ISetupActivity WithPath(this ISetupActivity setup, Func> path) => setup.Set(x => x.Path, path); + public static ISetupActivity WithPath(this ISetupActivity setup, Func path) => setup.Set(x => x.Path, path); + public static ISetupActivity WithPath(this ISetupActivity setup, Func> path) => setup.Set(x => x.Path, path); + public static ISetupActivity WithPath(this ISetupActivity setup, Func path) => setup.Set(x => x.Path, path); + public static ISetupActivity WithPath(this ISetupActivity setup, string? path) => setup.Set(x => x.Path, path); + #endregion + + #region Pattern + public static ISetupActivity WithPattern(this ISetupActivity setup, Func> pattern) => setup.Set(x => x.Pattern, pattern); + public static ISetupActivity WithPattern(this ISetupActivity setup, Func pattern) => setup.Set(x => x.Pattern, pattern); + public static ISetupActivity WithPattern(this ISetupActivity setup, Func> pattern) => setup.Set(x => x.Pattern, pattern); + public static ISetupActivity WithPattern(this ISetupActivity setup, Func pattern) => setup.Set(x => x.Pattern, pattern); + public static ISetupActivity WithPattern(this ISetupActivity setup, string? pattern) => setup.Set(x => x.Pattern, pattern); + #endregion + } +} diff --git a/src/activities/Elsa.Activities.File/Bookmarks/FileSystemEventBookmark.cs b/src/activities/Elsa.Activities.File/Bookmarks/FileSystemEventBookmark.cs new file mode 100644 index 000000000..3f716e483 --- /dev/null +++ b/src/activities/Elsa.Activities.File/Bookmarks/FileSystemEventBookmark.cs @@ -0,0 +1,43 @@ +using Elsa.Services; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Elsa.Activities.File.Bookmarks +{ + public class FileSystemEventBookmark : IBookmark + { + public FileSystemEventBookmark() + { } + + public FileSystemEventBookmark(string? path, string? pattern, WatcherChangeTypes changeTypes, NotifyFilters notifyFilters) + { + ChangeTypes = changeTypes; + NotifyFilters = notifyFilters; + Path = path; + Pattern = pattern; + } + + public WatcherChangeTypes ChangeTypes { get; set; } + + public NotifyFilters NotifyFilters { get; set; } + + public string? Path { get; set; } + + public string? Pattern { get; set; } + } + + public class FileCreatedBookmarkProvider : BookmarkProvider + { + public override async ValueTask> GetBookmarksAsync(BookmarkProviderContext context, CancellationToken cancellationToken) + { + var changeTypes = await context.ReadActivityPropertyAsync(a => a.ChangeTypes); + var notifyFilters = await context.ReadActivityPropertyAsync(a => a.NotifyFilters); + var path = await context.ReadActivityPropertyAsync(a => a.Path); + var pattern = await context.ReadActivityPropertyAsync(a => a.Pattern); + var result = Result(new FileSystemEventBookmark(path, pattern, changeTypes, notifyFilters)); + return new[] { result }; + } + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.File/Extensions/ServiceCollectionExtensions.cs b/src/activities/Elsa.Activities.File/Extensions/ServiceCollectionExtensions.cs index f3667ebf0..f588bdb6b 100644 --- a/src/activities/Elsa.Activities.File/Extensions/ServiceCollectionExtensions.cs +++ b/src/activities/Elsa.Activities.File/Extensions/ServiceCollectionExtensions.cs @@ -1,7 +1,13 @@ using Elsa; using Elsa.Activities.File; +using Elsa.Activities.File.Bookmarks; +using Elsa.Activities.File.Services; +using Elsa.Activities.File.StartupTasks; +using Elsa.Runtime; using System; using Elsa.Options; +using Elsa.Activities.File.MapperProfiles; +using Elsa.Services; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection @@ -23,7 +29,14 @@ namespace Microsoft.Extensions.DependencyInjection .AddActivity() .AddActivity() .AddActivity() - .AddActivity(); + .AddActivity() + .AddActivity(); + + builder.Services.AddBookmarkProvider() + .AddAutoMapperProfile() + .AddSingleton() + .AddSingleton>() + .AddStartupTask(); return builder; } diff --git a/src/activities/Elsa.Activities.File/MapperProfiles/FileSystemEventProfile.cs b/src/activities/Elsa.Activities.File/MapperProfiles/FileSystemEventProfile.cs new file mode 100644 index 000000000..c28bcc647 --- /dev/null +++ b/src/activities/Elsa.Activities.File/MapperProfiles/FileSystemEventProfile.cs @@ -0,0 +1,34 @@ +using AutoMapper; +using Elsa.Activities.File.Models; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Elsa.Activities.File.MapperProfiles +{ + public class FileSystemEventProfile : Profile + { + public FileSystemEventProfile() + { + CreateMap() + .ForMember(d => d.ChangeType, + o => o.MapFrom(s => s.ChangeType)) + .ForMember(d => d.Directory, + o => o.MapFrom(s => Path.GetDirectoryName(s.FullPath))) + .ForMember(d => d.FileName, + o => o.MapFrom(s => s.Name)) + .ForMember(d => d.FullPath, + o => o.MapFrom(s => s.FullPath)) + .ForMember(d => d.TimeStamp, + o => o.MapFrom(s => DateTime.Now)); + + CreateMap() + .IncludeBase() + .ForMember(d => d.OldFileName, + o => o.MapFrom(s => s.OldName)) + .ForMember(d => d.OldFullPath, + o => o.MapFrom(s => Path.GetDirectoryName(s.OldFullPath))); + } + } +} diff --git a/src/activities/Elsa.Activities.File/Models/FileSystemChanged.cs b/src/activities/Elsa.Activities.File/Models/FileSystemChanged.cs deleted file mode 100644 index 37d1ab4c9..000000000 --- a/src/activities/Elsa.Activities.File/Models/FileSystemChanged.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.IO; - -namespace Elsa.Activities.File.Models -{ - public class FileSystemChanged - { - public FileSystemChanged() - { - } - - public FileSystemChanged(WatcherChangeTypes changeType, string directory, string fileName, string fullPath) - { - ChangeType = changeType; - Directory = directory; - FileName = fileName; - FullPath = fullPath; - TimeStamp = DateTime.Now; - } - - public WatcherChangeTypes ChangeType { get; set; } - - public string Directory { get; set; } = default!; - - public string FileName { get; set; } = default!; - - public string FullPath { get; set; } = default!; - - public DateTime TimeStamp { get; set; } = default!; - } -} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.File/Models/FileSystemEvent.cs b/src/activities/Elsa.Activities.File/Models/FileSystemEvent.cs new file mode 100644 index 000000000..4f99e5d6a --- /dev/null +++ b/src/activities/Elsa.Activities.File/Models/FileSystemEvent.cs @@ -0,0 +1,22 @@ +using System; +using System.IO; + +namespace Elsa.Activities.File.Models +{ + public class FileSystemEvent + { + public WatcherChangeTypes ChangeType { get; set; } + + public string Directory { get; set; } = default!; + + public string FileName { get; set; } = default!; + + public string FullPath { get; set; } = default!; + + public string OldFileName { get; set; } = default!; + + public string OldFullPath { get; set; } = default!; + + public DateTime TimeStamp { get; set; } = default!; + } +} \ No newline at end of file diff --git a/src/activities/Elsa.Activities.File/Services/FileSystemWatchersStarter.cs b/src/activities/Elsa.Activities.File/Services/FileSystemWatchersStarter.cs new file mode 100644 index 000000000..ac84035a9 --- /dev/null +++ b/src/activities/Elsa.Activities.File/Services/FileSystemWatchersStarter.cs @@ -0,0 +1,173 @@ +using AutoMapper; +using Elsa.Activities.File.Bookmarks; +using Elsa.Activities.File.Models; +using Elsa.Models; +using Elsa.Services; +using Elsa.Services.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Elsa.Activities.File.Services +{ + public class FileSystemWatchersStarter + { + private readonly ILogger _logger; + private readonly IMapper _mapper; + private readonly IServiceScopeFactory _scopeFactory; + private readonly SemaphoreSlim _semaphore = new(1); + private readonly IServiceProvider _serviceProvider; + private readonly ICollection _watchers; + private readonly Scoped _workflowLaunchpad; + + public FileSystemWatchersStarter(ILogger logger, + IMapper mapper, + IServiceScopeFactory scopeFactory, + IServiceProvider serviceProvider, + Scoped workflowLaunchpad) + { + _logger = logger; + _mapper = mapper; + _scopeFactory = scopeFactory; + _serviceProvider = serviceProvider; + _watchers = new List(); + _workflowLaunchpad = workflowLaunchpad; + } + + public async Task CreateAndAddWatchersAsync(CancellationToken cancellationToken = default) + { + await _semaphore.WaitAsync(); + + try + { + var activities = GetActivityInstancesAsync(cancellationToken); + await foreach (var a in activities) + { + var changeTypes = await a.EvaluatePropertyValueAsync(x => x.ChangeTypes, cancellationToken); + var notifyFilters = await a.EvaluatePropertyValueAsync(x => x.NotifyFilters, cancellationToken); + var path = await a.EvaluatePropertyValueAsync(x => x.Path, cancellationToken); + var pattern = await a.EvaluatePropertyValueAsync(x => x.Pattern, cancellationToken); + CreateAndAddWatcher(path, pattern, changeTypes, notifyFilters); + } + } + finally + { + _semaphore.Release(); + } + } + + private void CreateAndAddWatcher(string? path, string? pattern, WatcherChangeTypes changeTypes, NotifyFilters notifyFilters) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("File watcher path must not be null or empty"); + + EnsurePathExists(path); + + var watcher = new FileSystemWatcher() + { + Path = path, + Filter = pattern, + NotifyFilter = notifyFilters + }; + + if (changeTypes == WatcherChangeTypes.Created || changeTypes == WatcherChangeTypes.All) + watcher.Created += FileCreated; + + if (changeTypes == WatcherChangeTypes.Changed || changeTypes == WatcherChangeTypes.All) + watcher.Changed += FileChanged; + + if (changeTypes == WatcherChangeTypes.Deleted || changeTypes == WatcherChangeTypes.All) + watcher.Deleted += FileDeleted; + + if (changeTypes == WatcherChangeTypes.Renamed || changeTypes == WatcherChangeTypes.All) + watcher.Renamed += FileRenamed; + + watcher.EnableRaisingEvents = true; + _watchers.Add(watcher); + } + + private async IAsyncEnumerable> GetActivityInstancesAsync([EnumeratorCancellation] CancellationToken cancellationToken) where TActivity : IActivity + { + using (var scope = _scopeFactory.CreateScope()) + { + var workflowRegistry = scope.ServiceProvider.GetRequiredService(); + var workflowBlueprintReflector = scope.ServiceProvider.GetRequiredService(); + var workflows = await workflowRegistry.ListActiveAsync(cancellationToken); + + var query = from workflow in workflows + from activity in workflow.Activities + where activity.Type == nameof(WatchDirectory) + select workflow; + + foreach (var workflow in query) + { + var workflowBlueprintWrapper = await workflowBlueprintReflector.ReflectAsync(scope.ServiceProvider, workflow, cancellationToken); + + foreach (var activity in workflowBlueprintWrapper.Filter()) + { + yield return activity; + } + } + } + } + + private void EnsurePathExists(string? path) + { + _logger.LogDebug($"Checking ${path} exists"); + + if (!Directory.Exists(path)) + { + _logger.LogInformation($"Creating directory {path}"); + Directory.CreateDirectory(path); + } + } + + #region Watcher delegates + private void FileCreated(object sender, FileSystemEventArgs e) + { + StartWorkflow((FileSystemWatcher)sender, e); + } + + private void FileChanged(object sender, FileSystemEventArgs e) + { + if (e.ChangeType != WatcherChangeTypes.Changed) + { + return; + } + + StartWorkflow((FileSystemWatcher)sender, e); + } + + private void FileDeleted(object sender, FileSystemEventArgs e) + { + StartWorkflow((FileSystemWatcher)sender, e); + } + + private void FileRenamed(object sender, RenamedEventArgs e) + { + StartWorkflow((FileSystemWatcher)sender, e); + } + + private void StartWorkflow(FileSystemWatcher watcher, FileSystemEventArgs e) + { + var changeTypes = e.ChangeType; + var notifyFilter = watcher.NotifyFilter; + var path = watcher.Path; + var pattern = watcher.Filter; + + var model = _mapper.Map(e); + var bookmark = new FileSystemEventBookmark(path, pattern, changeTypes, notifyFilter); + var launchContext = new WorkflowsQuery(nameof(WatchDirectory), bookmark); + _workflowLaunchpad.UseService(s => s.CollectAndDispatchWorkflowsAsync(launchContext, new WorkflowInput(model))); + } + #endregion + } +} diff --git a/src/activities/Elsa.Activities.File/StartupTasks/StartFileSystemWatchers.cs b/src/activities/Elsa.Activities.File/StartupTasks/StartFileSystemWatchers.cs new file mode 100644 index 000000000..190c73a5a --- /dev/null +++ b/src/activities/Elsa.Activities.File/StartupTasks/StartFileSystemWatchers.cs @@ -0,0 +1,21 @@ +using Elsa.Activities.File.Services; +using Elsa.Services; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Elsa.Activities.File.StartupTasks +{ + public class StartFileSystemWatchers : IStartupTask + { + private readonly FileSystemWatchersStarter _starter; + + public StartFileSystemWatchers(FileSystemWatchersStarter starter) => _starter = starter; + + public int Order => 2000; + + public Task ExecuteAsync(CancellationToken cancellationToken = default) => _starter.CreateAndAddWatchersAsync(cancellationToken); + } +} diff --git a/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Elsa.Samples.WatchDirectoryWorker.csproj b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Elsa.Samples.WatchDirectoryWorker.csproj new file mode 100644 index 000000000..f1891d124 --- /dev/null +++ b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Elsa.Samples.WatchDirectoryWorker.csproj @@ -0,0 +1,39 @@ + + + + Exe + net5.0 + + + + + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Program.cs b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Program.cs new file mode 100644 index 000000000..650766a4e --- /dev/null +++ b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Program.cs @@ -0,0 +1,38 @@ +using Elsa.Samples.WatchDirectoryWorker.Workflows; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System; +using System.IO; + +namespace Elsa.Samples.WatchDirectoryWorker +{ + class Program + { + private static readonly string _directory; + private static readonly string _systemRoot; + + static Program() + { + _systemRoot = Path.GetPathRoot(Environment.SystemDirectory); + _directory = Path.Combine(_systemRoot, "Temp\\FileWatchers"); + } + + static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services + .AddElsa(options => options + .AddConsoleActivities() + .AddFileActivities() + .AddQuartzTemporalActivities() + .AddWorkflow(sp => ActivatorUtilities.CreateInstance(services.BuildServiceProvider(), _directory)) + .AddWorkflow(sp => new WatchDirectoryCreatedWorkflow(_directory))); + }); + } +} diff --git a/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Workflows/TimerCreateFile.cs b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Workflows/TimerCreateFile.cs new file mode 100644 index 000000000..5e40b7555 --- /dev/null +++ b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Workflows/TimerCreateFile.cs @@ -0,0 +1,37 @@ +using Elsa.Activities.File; +using Elsa.Activities.Temporal; +using Elsa.Builders; +using NodaTime; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Elsa.Samples.WatchDirectoryWorker.Workflows +{ + public class TimerCreateFile : IWorkflow + { + private IClock _clock; + private string _directory; + + public TimerCreateFile(IClock clock, string directory) + { + _clock = clock; + _directory = directory; + } + + public void Build(IWorkflowBuilder builder) => builder + .Timer(Duration.FromSeconds(10)) + .OutFile(setup => + { + var guid = Guid.NewGuid().ToString(); + var filename = Path.Combine(_directory, $"{guid}.txt"); + + setup.WithPath(filename); + setup.WithBytes(Encoding.UTF8.GetBytes("Hello World")); + }); + } +} diff --git a/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Workflows/WatchDirectoryCreatedWorkflow.cs b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Workflows/WatchDirectoryCreatedWorkflow.cs new file mode 100644 index 000000000..dfcd36411 --- /dev/null +++ b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/Workflows/WatchDirectoryCreatedWorkflow.cs @@ -0,0 +1,47 @@ +using Elsa.Activities.Console; +using Elsa.Activities.File; +using Elsa.Activities.File.Models; +using Elsa.Builders; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.Unicode; +using System.Threading.Tasks; + +namespace Elsa.Samples.WatchDirectoryWorker.Workflows +{ + public class WatchDirectoryCreatedWorkflow : IWorkflow + { + private readonly string _directory; + private readonly string _systemRoot = Path.GetPathRoot(Environment.SystemDirectory); + + public WatchDirectoryCreatedWorkflow(string directory) + { + _directory = directory; + } + + public void Build(IWorkflowBuilder builder) => builder.WatchDirectory(setup => + { + setup.WithPath(Path.Combine(_systemRoot, "Temp\\FileWatchers")) + .WithPattern("*.txt") + .WithChangeTypes(WatcherChangeTypes.Created); + }) + .ReadFile(setup => + { + setup.WithPath(context => context.GetInput().FullPath); + }) + .WriteLine(setup => + { + setup.WithText(async context => + { + var fsEvent = await context.WorkflowExecutionContext.GetActivityPropertyAsync("activity-1", a => a.Output); + var data = await context.WorkflowExecutionContext.GetActivityPropertyAsync("activity-2", a => a.Bytes); + var line = $"{GetType().Name}-{fsEvent.FullPath}-{fsEvent.ChangeType}-{Encoding.UTF8.GetString(data)}"; + return line; + }); + }) + .DeleteFile(context => context.GetInput().FullPath); + } +} diff --git a/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/appsettings.json b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/appsettings.json new file mode 100644 index 000000000..89bdcb790 --- /dev/null +++ b/src/samples/worker/Elsa.Samples.WatchDirectoryWorker/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + }, + "Console": { + "FormatterName": "simple", + "FormatterOptions": { + "TimestampFormat": "yyyy-MM-ddTHH:mm:ss " + } + } + } +} \ No newline at end of file