diff --git a/BasicNodes/Functions/Function.cs b/BasicNodes/Functions/Function.cs index 89476355..d961928c 100644 --- a/BasicNodes/Functions/Function.cs +++ b/BasicNodes/Functions/Function.cs @@ -3,28 +3,40 @@ using FileFlows.Plugin; using FileFlows.Plugin.Attributes; using System.ComponentModel.DataAnnotations; - namespace FileFlows.BasicNodes.Functions; +/// +/// A flow element that executes custom code +/// public class Function : Node { + /// public override int Inputs => 1; + /// public override FlowElementType Type => FlowElementType.Logic; + /// public override string Icon => "fas fa-code"; + /// public override bool FailureNode => true; - + /// public override string HelpUrl => "https://fileflows.com/docs/plugins/basic-nodes/function"; + /// + /// Gets or sets the number of outputs + /// [DefaultValue(1)] [NumberInt(1)] public new int Outputs { get; set; } + /// + /// Gets or sets the code to execute + /// [Required] [DefaultValue("// Custom javascript code that you can run against the flow file.\n// Flow contains helper functions for the Flow.\n// Variables contain variables available to this node from previous nodes.\n// Logger lets you log messages to the flow output.\n\n// return 0 to complete the flow.\n// return -1 to signal an error in the flow\n// return 1+ to select which output node will be processed next\n\nif(Variables.file.Size === 0)\n\treturn -1;\n\nreturn 1;")] [Code(2)] public string Code { get; set; } - - delegate void LogDelegate(params object[] values); + + /// public override int Execute(NodeParameters args) { if (string.IsNullOrEmpty(Code)) diff --git a/BasicNodes/Functions/Matches.cs b/BasicNodes/Functions/Matches.cs new file mode 100644 index 00000000..3f38c239 --- /dev/null +++ b/BasicNodes/Functions/Matches.cs @@ -0,0 +1,81 @@ +using FileFlows.Plugin; +using FileFlows.Plugin.Attributes; +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; +using FileFlows.BasicNodes.Helpers; + +namespace FileFlows.BasicNodes.Functions; + +/// +/// A flow element that matches different conditions +/// +public class Matches : Node +{ + /// + public override int Inputs => 1; + /// + public override FlowElementType Type => FlowElementType.Logic; + /// + public override string Icon => "fas fa-equals"; + /// + public override bool FailureNode => true; + /// + public override string HelpUrl => "https://fileflows.com/docs/plugins/basic-nodes/matches"; + + /// + /// Gets or sets replacements to replace + /// + [KeyValue(1, showVariables: true, allowDuplicates: true)] + [Required] + public List> MatchConditions { get; set; } + + /// + public override int Execute(NodeParameters args) + { + if (MatchConditions?.Any() != true) + { + args.FailureReason = "No matches defined"; + args.Logger.ELog(args.FailureReason); + return -1; + } + + int output = 0; + foreach (var match in MatchConditions) + { + output++; + try + { + var value = args.ReplaceVariables(match.Key, stripMissing: true); + if (GeneralHelper.IsRegex(match.Value)) + { + if (Regex.IsMatch(value, match.Value, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)) + { + args.Logger?.ILog($"Match found '{match.Value}' = {value}"); + return output; + } + } + + if (match.Value == value) + { + args.Logger?.ILog($"Match found '{match.Value}' = {value}"); + return output; + } + + if (MathHelper.IsMathOperation(match.Value)) + { + if (MathHelper.IsTrue(value, match.Value)) + { + args.Logger?.ILog($"Match found '{match.Value}' = {value}"); + return output; + } + } + } + catch (Exception) + { + } + } + + args.Logger?.ILog("No matches found"); + return MatchConditions.Count + 1; + } +} diff --git a/BasicNodes/Helpers/GeneralHelper.cs b/BasicNodes/Helpers/GeneralHelper.cs new file mode 100644 index 00000000..3576c144 --- /dev/null +++ b/BasicNodes/Helpers/GeneralHelper.cs @@ -0,0 +1,17 @@ +namespace FileFlows.BasicNodes.Helpers; + +/// +/// General helper +/// +public class GeneralHelper +{ + /// + /// Checks if the input string represents a regular expression. + /// + /// The input string to check. + /// True if the input is a regular expression, otherwise false. + public static bool IsRegex(string input) + { + return new[] { "?", "|", "^", "$", "*" }.Any(ch => input.Contains(ch)); + } +} \ No newline at end of file diff --git a/BasicNodes/Helpers/MathHelper.cs b/BasicNodes/Helpers/MathHelper.cs new file mode 100644 index 00000000..88e00ba5 --- /dev/null +++ b/BasicNodes/Helpers/MathHelper.cs @@ -0,0 +1,166 @@ +using System.Globalization; + +namespace FileFlows.BasicNodes.Helpers; + +/// +/// Helper for math operations +/// +public class MathHelper +{ + /// + /// Checks if the comparison string represents a mathematical operation. + /// + /// The comparison string to check. + /// True if the comparison is a mathematical operation, otherwise false. + public static bool IsMathOperation(string comparison) + { + // Check if the comparison string starts with <=, <, >, >=, ==, or = + return new[] { "<=", "<", ">", ">=", "==", "=" }.Any(comparison.StartsWith); + } + + + /// + /// Tests if a math operation is true + /// + /// The value to apply the operation to. + /// The operation string representing the mathematical operation. + /// True if the mathematical operation is successful, otherwise false. + public static bool IsTrue(string value, string operation) + { + // This is a basic example; you may need to handle different operators + switch (operation[..2]) + { + case "<=": + return Convert.ToDouble(value) <= Convert.ToDouble(AdjustComparisonValue(operation[2..].Trim())); + case ">=": + return Convert.ToDouble(value) >= Convert.ToDouble(AdjustComparisonValue(operation[2..].Trim())); + case "==": + return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[2..].Trim()))) < 0.05f; + case "!=": + case "<>": + return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[2..].Trim()))) > 0.05f; + } + + switch (operation[..1]) + { + case "<": + return Convert.ToDouble(value) < Convert.ToDouble(AdjustComparisonValue(operation[1..].Trim())); + case ">": + return Convert.ToDouble(value) > Convert.ToDouble(AdjustComparisonValue(operation[1..].Trim())); + case "=": + return Math.Abs(Convert.ToDouble(value) - Convert.ToDouble(AdjustComparisonValue(operation[1..].Trim()))) < 0.05f; + } + + return false; + } + + /// + /// Adjusts the comparison string by handling common mistakes in units and converting them into full numbers. + /// + /// The original comparison string to be adjusted. + /// The adjusted comparison string with corrected units or the original comparison if no adjustments are made. + private static string AdjustComparisonValue(string comparisonValue) + { + if (string.IsNullOrWhiteSpace(comparisonValue)) + return string.Empty; + + string adjustedComparison = comparisonValue.ToLower().Trim(); + + // Handle common mistakes in units + if (adjustedComparison.EndsWith("mbps")) + { + // Make an educated guess for Mbps to kbps conversion + return adjustedComparison[..^4] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("kbps")) + { + // Make an educated guess for kbps to bps conversion + return adjustedComparison[..^4] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("kb")) + { + return adjustedComparison[..^2] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000 ) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("mb")) + { + return adjustedComparison[..^2] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000 ) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("gb")) + { + return adjustedComparison[..^2] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000_000 ) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("tb")) + { + return adjustedComparison[..^2] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000_000_000) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + + if (adjustedComparison.EndsWith("kib")) + { + return adjustedComparison[..^3] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_024 ) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("mib")) + { + return adjustedComparison[..^3] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_048_576 ) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("gib")) + { + return adjustedComparison[..^3] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_099_511_627_776 ) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + if (adjustedComparison.EndsWith("tib")) + { + return adjustedComparison[..^3] switch + { + { } value when double.TryParse(value, out var numericValue) => (numericValue * 1_000_000_000_000) + .ToString(CultureInfo.InvariantCulture), + _ => comparisonValue + }; + } + return comparisonValue; + } + +} \ No newline at end of file diff --git a/BasicNodes/Tests/MatchesTests.cs b/BasicNodes/Tests/MatchesTests.cs new file mode 100644 index 00000000..3a29fb95 --- /dev/null +++ b/BasicNodes/Tests/MatchesTests.cs @@ -0,0 +1,92 @@ +#if(DEBUG) + +using FileFlows.BasicNodes.Functions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BasicNodes.Tests; + +[TestClass] +public class MatchesTests +{ + [TestMethod] + public void Matches_Math() + { + var logger = new TestLogger(); + Matches ele = new (); + ele.MatchConditions = new() + { + new("{file.Size}", "<=100KB"), + new("{file.Size}", ">100KB"), + new("{file.Size}", ">10MB"), + }; + var args = new FileFlows.Plugin.NodeParameters(null, logger, + false, string.Empty, new LocalFileService()); + args.Variables["file.Size"] = 120_000; // 120KB + + var result = ele.Execute(args); + var log = logger.ToString(); + Assert.AreEqual(2, result); + } + + [TestMethod] + public void Matches_String() + { + var logger = new TestLogger(); + Matches ele = new (); + ele.MatchConditions = new() + { + new("{file.Name}", "triggerthis"), + new("{file.Name}", "DontTriggerThis"), + new("{file.Name}", "TriggerThis"), + }; + var args = new FileFlows.Plugin.NodeParameters(null, logger, + false, string.Empty, new LocalFileService()); + args.Variables["file.Name"] = "TriggerThis"; + + var result = ele.Execute(args); + var log = logger.ToString(); + Assert.AreEqual(3, result); + } + [TestMethod] + public void Matches_NoMatch() + { + var logger = new TestLogger(); + Matches ele = new (); + ele.MatchConditions = new() + { + new("{file.Name}", "triggerthis"), + new("{file.Name}", "DontTriggerThis"), + new("{file.Name}", "TriggerThis"), + }; + var args = new FileFlows.Plugin.NodeParameters(null, logger, + false, string.Empty, new LocalFileService()); + args.Variables["file.Name"] = "Nothing"; + + var result = ele.Execute(args); + var log = logger.ToString(); + Assert.AreEqual(4, result); + } + + [TestMethod] + public void Matches_Regex() + { + var logger = new TestLogger(); + Matches ele = new (); + ele.MatchConditions = new() + { + new("{file.Name}", "triggerthis"), + new("{file.Name}", ".*batman.*"), + new("{file.Name}", "TriggerThis"), + }; + var args = new FileFlows.Plugin.NodeParameters(null, logger, + false, string.Empty, new LocalFileService()); + args.Variables["file.Name"] = "Superman vs Batman (2017)"; + + var result = ele.Execute(args); + var log = logger.ToString(); + Assert.AreEqual(2, result); + } +} + + +#endif \ No newline at end of file diff --git a/BasicNodes/i18n/en.json b/BasicNodes/i18n/en.json index 79cf6904..355651e0 100644 --- a/BasicNodes/i18n/en.json +++ b/BasicNodes/i18n/en.json @@ -212,6 +212,15 @@ "Message": "Message" } }, + "Matches": { + "Description": "Compares a set of values and matches conditions to see which output should be called", + "Fields": { + "MatchConditions": "", + "MatchConditionsKey": "Value", + "MatchConditionsValue": "Expression", + "MatchConditions-Help": "The matches to test which output should be called." + } + }, "MoveFile": { "Description": "Moves a file to the destination folder", "Outputs": { diff --git a/FileFlows.Plugin.dll b/FileFlows.Plugin.dll index fa793002..4c7b184f 100644 Binary files a/FileFlows.Plugin.dll and b/FileFlows.Plugin.dll differ diff --git a/FileFlows.Plugin.pdb b/FileFlows.Plugin.pdb index e1f43fd7..aca62b35 100644 Binary files a/FileFlows.Plugin.pdb and b/FileFlows.Plugin.pdb differ