Merge pull request #1513 from MilkyWare/elsa-2.0/fileactivities

Elsa 2.0/fileactivities
This commit is contained in:
Sipke Schoorstra 2021-09-12 12:27:37 +02:00 committed by GitHub
commit b98528a95a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 632 additions and 51 deletions

View file

@ -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

View file

@ -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)
{

View file

@ -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<string>? Files { get; set; }

View file

@ -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)
{

View file

@ -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<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{

View file

@ -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<IActivityExecutionResult> ExecuteAsync(ActivityExecutionContext context)
{
Bytes = await System.IO.File.ReadAllBytesAsync(Path);
return Done();
return Done(Bytes);
}
}
}

View file

@ -10,34 +10,34 @@ namespace Elsa.Activities.File
{
public static class ReadFileBuilderExtensions
{
public static IActivityBuilder ReadFile(this IBuilder builder, Action<ISetupActivity<OutFile>> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => builder.Then(setup, null, lineNumber, sourceFile);
public static IActivityBuilder ReadFile(this IBuilder builder, Action<ISetupActivity<ReadFile>> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => builder.Then(setup, null, lineNumber, sourceFile);
public static IActivityBuilder ReadFile(this IBuilder builder, Func<ActivityExecutionContext, 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);
public static IActivityBuilder ReadFile(this IBuilder builder, Func<ActivityExecutionContext, ValueTask<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);
public static IActivityBuilder ReadFile(this IBuilder builder, Func<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);
public static IActivityBuilder ReadFile(this IBuilder builder, Func<ValueTask<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);
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);

View file

@ -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!;

View file

@ -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);
}
}
}

View file

@ -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<ISetupActivity<WatchDirectory>> setup, [CallerLineNumber] int lineNumber = default, [CallerFilePath] string? sourceFile = default) => builder.Then(setup, null, lineNumber, sourceFile);
}
}

View file

@ -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<WatchDirectory> WithChangeTypes(this ISetupActivity<WatchDirectory> setup, Func<ActivityExecutionContext, ValueTask<WatcherChangeTypes>> changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes);
public static ISetupActivity<WatchDirectory> WithChangeTypes(this ISetupActivity<WatchDirectory> setup, Func<ActivityExecutionContext, WatcherChangeTypes> changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes);
public static ISetupActivity<WatchDirectory> WithChangeTypes(this ISetupActivity<WatchDirectory> setup, Func<ValueTask<WatcherChangeTypes>> changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes);
public static ISetupActivity<WatchDirectory> WithChangeTypes(this ISetupActivity<WatchDirectory> setup, Func<WatcherChangeTypes> changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes);
public static ISetupActivity<WatchDirectory> WithChangeTypes(this ISetupActivity<WatchDirectory> setup, WatcherChangeTypes changeTypes) => setup.Set(x => x.ChangeTypes, changeTypes);
#endregion
#region NotifyFilters
public static ISetupActivity<WatchDirectory> WithNotifyFilters(this ISetupActivity<WatchDirectory> setup, Func<ActivityExecutionContext, ValueTask<NotifyFilters>> notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters);
public static ISetupActivity<WatchDirectory> WithNotifyFilters(this ISetupActivity<WatchDirectory> setup, Func<ActivityExecutionContext, NotifyFilters> notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters);
public static ISetupActivity<WatchDirectory> WithNotifyFilters(this ISetupActivity<WatchDirectory> setup, Func<ValueTask<NotifyFilters>> notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters);
public static ISetupActivity<WatchDirectory> WithNotifyFilters(this ISetupActivity<WatchDirectory> setup, Func<NotifyFilters> notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters);
public static ISetupActivity<WatchDirectory> WithNotifyFilters(this ISetupActivity<WatchDirectory> setup, NotifyFilters notifyFilters) => setup.Set(x => x.NotifyFilters, notifyFilters);
#endregion
#region Path
public static ISetupActivity<WatchDirectory> WithPath(this ISetupActivity<WatchDirectory> setup, Func<ActivityExecutionContext, ValueTask<string?>> path) => setup.Set(x => x.Path, path);
public static ISetupActivity<WatchDirectory> WithPath(this ISetupActivity<WatchDirectory> setup, Func<ActivityExecutionContext, string?> path) => setup.Set(x => x.Path, path);
public static ISetupActivity<WatchDirectory> WithPath(this ISetupActivity<WatchDirectory> setup, Func<ValueTask<string?>> path) => setup.Set(x => x.Path, path);
public static ISetupActivity<WatchDirectory> WithPath(this ISetupActivity<WatchDirectory> setup, Func<string?> path) => setup.Set(x => x.Path, path);
public static ISetupActivity<WatchDirectory> WithPath(this ISetupActivity<WatchDirectory> setup, string? path) => setup.Set(x => x.Path, path);
#endregion
#region Pattern
public static ISetupActivity<WatchDirectory> WithPattern(this ISetupActivity<WatchDirectory> setup, Func<ActivityExecutionContext, ValueTask<string?>> pattern) => setup.Set(x => x.Pattern, pattern);
public static ISetupActivity<WatchDirectory> WithPattern(this ISetupActivity<WatchDirectory> setup, Func<ActivityExecutionContext, string?> pattern) => setup.Set(x => x.Pattern, pattern);
public static ISetupActivity<WatchDirectory> WithPattern(this ISetupActivity<WatchDirectory> setup, Func<ValueTask<string?>> pattern) => setup.Set(x => x.Pattern, pattern);
public static ISetupActivity<WatchDirectory> WithPattern(this ISetupActivity<WatchDirectory> setup, Func<string?> pattern) => setup.Set(x => x.Pattern, pattern);
public static ISetupActivity<WatchDirectory> WithPattern(this ISetupActivity<WatchDirectory> setup, string? pattern) => setup.Set(x => x.Pattern, pattern);
#endregion
}
}

View file

@ -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<FileSystemEventBookmark, WatchDirectory>
{
public override async ValueTask<IEnumerable<BookmarkResult>> GetBookmarksAsync(BookmarkProviderContext<WatchDirectory> 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 };
}
}
}

View file

@ -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<FileExists>()
.AddActivity<OutFile>()
.AddActivity<ReadFile>()
.AddActivity<TempFile>();
.AddActivity<TempFile>()
.AddActivity<WatchDirectory>();
builder.Services.AddBookmarkProvider<FileCreatedBookmarkProvider>()
.AddAutoMapperProfile<FileSystemEventProfile>()
.AddSingleton<FileSystemWatchersStarter>()
.AddSingleton<Scoped<IWorkflowLaunchpad>>()
.AddStartupTask<StartFileSystemWatchers>();
return builder;
}

View file

@ -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<FileSystemEventArgs, FileSystemEvent>()
.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<RenamedEventArgs, FileSystemEvent>()
.IncludeBase<FileSystemEventArgs, FileSystemEvent>()
.ForMember(d => d.OldFileName,
o => o.MapFrom(s => s.OldName))
.ForMember(d => d.OldFullPath,
o => o.MapFrom(s => Path.GetDirectoryName(s.OldFullPath)));
}
}
}

View file

@ -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!;
}
}

View file

@ -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!;
}
}

View file

@ -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<FileSystemWatchersStarter> _logger;
private readonly IMapper _mapper;
private readonly IServiceScopeFactory _scopeFactory;
private readonly SemaphoreSlim _semaphore = new(1);
private readonly IServiceProvider _serviceProvider;
private readonly ICollection<FileSystemWatcher> _watchers;
private readonly Scoped<IWorkflowLaunchpad> _workflowLaunchpad;
public FileSystemWatchersStarter(ILogger<FileSystemWatchersStarter> logger,
IMapper mapper,
IServiceScopeFactory scopeFactory,
IServiceProvider serviceProvider,
Scoped<IWorkflowLaunchpad> workflowLaunchpad)
{
_logger = logger;
_mapper = mapper;
_scopeFactory = scopeFactory;
_serviceProvider = serviceProvider;
_watchers = new List<FileSystemWatcher>();
_workflowLaunchpad = workflowLaunchpad;
}
public async Task CreateAndAddWatchersAsync(CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync();
try
{
var activities = GetActivityInstancesAsync<WatchDirectory>(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<IActivityBlueprintWrapper<WatchDirectory>> GetActivityInstancesAsync<TActivity>([EnumeratorCancellation] CancellationToken cancellationToken) where TActivity : IActivity
{
using (var scope = _scopeFactory.CreateScope())
{
var workflowRegistry = scope.ServiceProvider.GetRequiredService<IWorkflowRegistry>();
var workflowBlueprintReflector = scope.ServiceProvider.GetRequiredService<IWorkflowBlueprintReflector>();
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<WatchDirectory>())
{
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<FileSystemEvent>(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
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Console\Elsa.Activities.Console.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.File\Elsa.Activities.File.csproj" />
<ProjectReference Include="..\..\..\activities\Elsa.Activities.Temporal.Quartz\Elsa.Activities.Temporal.Quartz.csproj" />
<ProjectReference Include="..\..\..\core\Elsa\Elsa.csproj" />
</ItemGroup>
<ItemGroup>
<Resource Include="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Compile Update="Program.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Compile>
</ItemGroup>
</Project>

View file

@ -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<TimerCreateFile>(sp => ActivatorUtilities.CreateInstance<TimerCreateFile>(services.BuildServiceProvider(), _directory))
.AddWorkflow<WatchDirectoryCreatedWorkflow>(sp => new WatchDirectoryCreatedWorkflow(_directory)));
});
}
}

View file

@ -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"));
});
}
}

View file

@ -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<FileSystemEvent>().FullPath);
})
.WriteLine(setup =>
{
setup.WithText(async context =>
{
var fsEvent = await context.WorkflowExecutionContext.GetActivityPropertyAsync<WatchDirectory, FileSystemEvent>("activity-1", a => a.Output);
var data = await context.WorkflowExecutionContext.GetActivityPropertyAsync<ReadFile, byte[]>("activity-2", a => a.Bytes);
var line = $"{GetType().Name}-{fsEvent.FullPath}-{fsEvent.ChangeType}-{Encoding.UTF8.GetString(data)}";
return line;
});
})
.DeleteFile(context => context.GetInput<FileSystemEvent>().FullPath);
}
}

View file

@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
},
"Console": {
"FormatterName": "simple",
"FormatterOptions": {
"TimestampFormat": "yyyy-MM-ddTHH:mm:ss "
}
}
}
}