diff --git a/ImageNodes/Images/ImageBaseNode.cs b/ImageNodes/Images/ImageBaseNode.cs index 0bde6fde..f869e28b 100644 --- a/ImageNodes/Images/ImageBaseNode.cs +++ b/ImageNodes/Images/ImageBaseNode.cs @@ -3,14 +3,37 @@ using SixLabors.ImageSharp.Formats; namespace FileFlows.ImageNodes.Images; +/// +/// Represents an abstract base class for nodes related to image processing. +/// public abstract class ImageBaseNode:Node { - + /// + /// Represents the key for storing image information in a context or dictionary. + /// private const string IMAGE_INFO = "ImageInfo"; + + /// + /// Gets or sets the current format of the image. + /// protected string CurrentFormat { get; private set; } - protected int CurrentWidth{ get; private set; } + + /// + /// Gets or sets the current width of the image. + /// + protected int CurrentWidth { get; private set; } + + /// + /// Gets or sets the current height of the image. + /// protected int CurrentHeight { get; private set; } + + /// + /// Calls any pre-execute setup code + /// + /// The NodeParameters + /// true if successful, otherwise false public override bool PreExecute(NodeParameters args) { var localFile = args.FileService.GetLocalPath(args.WorkingFile); @@ -41,7 +64,12 @@ public abstract class ImageBaseNode:Node return true; } - + + /// + /// Updates information about an image based on the provided NodeParameters and optional variables. + /// + /// The NodeParameters + /// Additional variables associated with the image (optional). protected void UpdateImageInfo(NodeParameters args, Dictionary variables = null) { string extension = FileHelper.GetExtension(args.WorkingFile).ToLowerInvariant().TrimStart('.'); @@ -65,7 +93,7 @@ public abstract class ImageBaseNode:Node else { if (string.IsNullOrWhiteSpace(dateTimeOriginalString) == false && - DateTime.TryParse(dateTimeOriginalString, out DateTime dateTimeOriginal)) + TryParseDateTime(dateTimeOriginalString, out DateTime? dateTimeOriginal)) { dateTaken = dateTimeOriginal; args.Logger?.ILog("DateTimeOriginal: " + dateTimeOriginal); @@ -84,6 +112,16 @@ public abstract class ImageBaseNode:Node UpdateImageInfo(args, image.Width, image.Height, format.Name, variables: variables, dateTaken: dateTaken); } } + + /// + /// Updates information about an image based on the provided NodeParameters, width, height, format, variables, and dateTaken. + /// + /// The NodeParameters + /// The width of the image. + /// The height of the image. + /// The format of the image. + /// Additional variables associated with the image (optional). + /// The date when the image was taken (optional). protected void UpdateImageInfo(NodeParameters args, int width, int height, string format, Dictionary variables = null, DateTime? dateTaken = null) { var imageInfo = new ImageInfo @@ -104,6 +142,7 @@ public abstract class ImageBaseNode:Node if (dateTaken != null) { + variables.AddOrUpdate("img.DateTaken.Value", dateTaken.Value); variables.AddOrUpdate("img.DateTaken.Year", dateTaken.Value.Year); variables.AddOrUpdate("img.DateTaken.Month", dateTaken.Value.Month); variables.AddOrUpdate("img.DateTaken.Day", dateTaken.Value.Day); @@ -118,6 +157,14 @@ public abstract class ImageBaseNode:Node args.UpdateVariables(variables); } + + /// + /// Gets information about an image based on the provided NodeParameters. + /// + /// The NodeParameters + /// + /// An ImageInfo object representing information about the image, or null if information could not be retrieved. + /// internal ImageInfo? GetImageInfo(NodeParameters args) { if (args.Parameters.ContainsKey(IMAGE_INFO) == false) @@ -155,4 +202,42 @@ public abstract class ImageBaseNode:Node return args.WorkingFile; } + + /// + /// Tries to parse a DateTime from a string, attempting different formats. + /// + /// The string representation of the DateTime. + /// When this method returns, contains the parsed DateTime if successful; otherwise, null. + /// + /// True if the parsing was successful; otherwise, false. + /// + static bool TryParseDateTime(string dateTimeString, out DateTime? dateTime) + { + DateTime parsedDateTime; + + // Try parsing using DateTime.TryParse + if (DateTime.TryParse(dateTimeString, out parsedDateTime)) + { + dateTime = parsedDateTime; + return true; + } + + // Define an array of possible date formats for additional attempts + string[] dateFormats = { "yyyy:MM:dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss" /* Add more formats if needed */ }; + + // Attempt to parse using different formats + foreach (var format in dateFormats) + { + if (DateTime.TryParseExact(dateTimeString, format, null, System.Globalization.DateTimeStyles.None, out parsedDateTime)) + { + dateTime = parsedDateTime; + return true; + } + } + + // Set dateTime to null if parsing fails with all formats + dateTime = null; + return false; + } + } diff --git a/ImageNodes/Tests/ImageTests.cs b/ImageNodes/Tests/ImageTests.cs index eb655e30..9003ec62 100644 --- a/ImageNodes/Tests/ImageTests.cs +++ b/ImageNodes/Tests/ImageTests.cs @@ -14,7 +14,7 @@ public class ImageNodesTests string TestImage2; string TestImageHeic; string TempDir; - string TestCropImage1, TestCropImage2, TestCropImage3, TestCropImage4, TestCropImageNoCrop; + string TestCropImage1, TestCropImage2, TestCropImage3, TestCropImage4, TestCropImageNoCrop, TestExif; public ImageNodesTests() { @@ -39,6 +39,7 @@ public class ImageNodesTests TestImage1 = "/home/john/Pictures/circle.jpg"; TestImage2 = "/home/john/Pictures/36410427.png"; TempDir = "/home/john/temp/"; + TestExif = "/home/john/Pictures/exif_test.jpg"; } } @@ -291,6 +292,26 @@ public class ImageNodesTests string log = logger.ToString(); Assert.AreEqual(2, result); } + + + [TestMethod] + public void ImageNodes_Basic_Exif() + { + var logger = new TestLogger(); + var args = new NodeParameters(TestExif, logger, false, string.Empty, new LocalFileService()) + { + TempPath = TempDir + }; + + var node = new ImageFile(); + var result = node.Execute(args); + Assert.AreEqual(1, result); + if(node.Variables.TryGetValue("img.DateTaken.Value", out object oDate) == false) + Assert.Fail("Failed to get date time"); + + if(oDate is DateTime dt == false) + Assert.Fail("oDate is not a DateTime"); + } } #endif \ No newline at end of file diff --git a/ImageNodes/Tests/_LocalFileService.cs b/ImageNodes/Tests/_LocalFileService.cs new file mode 100644 index 00000000..83b57493 --- /dev/null +++ b/ImageNodes/Tests/_LocalFileService.cs @@ -0,0 +1,483 @@ +#if(DEBUG) +using FileFlows.Plugin; +using FileFlows.Plugin.Models; +using FileFlows.Plugin.Services; +using System.IO; +using System.Linq; + +namespace FileFlows.ImageNodes.Tests; + +public class LocalFileService : IFileService +{ + /// + /// Gets or sets the path separator for the file system + /// + public char PathSeparator { get; init; } = Path.DirectorySeparatorChar; + + /// + /// Gets or sets the allowed paths the file service can access + /// + public string[] AllowedPaths { get; init; } + + /// + /// Gets or sets a function for replacing variables in a string. + /// + /// + /// The function takes a string input, a boolean indicating whether to strip missing variables, + /// and a boolean indicating whether to clean special characters. + /// + public ReplaceVariablesDelegate ReplaceVariables { get; set; } + + /// + /// Gets or sets the permissions to use for files + /// + public int? Permissions { get; set; } + + /// + /// Gets or sets the owner:group to use for files + /// + public string OwnerGroup { get; set; } + + /// + /// Gets or sets the logger used for logging + /// + public ILogger? Logger { get; set; } + + public Result GetFiles(string path, string searchPattern = "", bool recursive = false) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + return Directory.GetFiles(path, searchPattern ?? string.Empty, + recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); + } + catch (Exception) + { + return new string[] { }; + } + } + + public Result GetDirectories(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + return Directory.GetDirectories(path); + } + catch (Exception) + { + return new string[] { }; + } + } + + public Result DirectoryExists(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + return Directory.Exists(path); + } + catch (Exception) + { + return false; + } + } + + public Result DirectoryDelete(string path, bool recursive = false) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + Directory.Delete(path, recursive); + return true; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result DirectoryMove(string path, string destination) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + if (IsProtectedPath(ref destination)) + return Result.Fail("Cannot access protected path: " + destination); + try + { + Directory.Move(path, destination); + SetPermissions(destination); + return true; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result DirectoryCreate(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + var dirInfo = new DirectoryInfo(path); + if (dirInfo.Exists == false) + dirInfo.Create(); + SetPermissions(path); + return true; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result FileExists(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + return File.Exists(path); + } + catch (Exception) + { + return false; + } + } + + public Result FileInfo(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + FileInfo fileInfo = new FileInfo(path); + + return new FileInformation + { + CreationTime = fileInfo.CreationTime, + CreationTimeUtc = fileInfo.CreationTimeUtc, + LastWriteTime = fileInfo.LastWriteTime, + LastWriteTimeUtc = fileInfo.LastWriteTimeUtc, + Extension = fileInfo.Extension.TrimStart('.'), + Name = fileInfo.Name, + FullName = fileInfo.FullName, + Length = fileInfo.Length, + Directory = fileInfo.DirectoryName + }; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result FileDelete(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + var fileInfo = new FileInfo(path); + if(fileInfo.Exists) + fileInfo.Delete(); + return true; + } + catch (Exception) + { + return false; + } + } + + public Result FileSize(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + var fileInfo = new FileInfo(path); + if (fileInfo.Exists == false) + return Result.Fail("File does not exist"); + return fileInfo.Length; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result FileCreationTimeUtc(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + var fileInfo = new FileInfo(path); + if (fileInfo.Exists == false) + return Result.Fail("File does not exist"); + return fileInfo.CreationTimeUtc; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result FileLastWriteTimeUtc(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + var fileInfo = new FileInfo(path); + if (fileInfo.Exists == false) + return Result.Fail("File does not exist"); + return fileInfo.LastWriteTimeUtc; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result FileMove(string path, string destination, bool overwrite = true) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + if (IsProtectedPath(ref destination)) + return Result.Fail("Cannot access protected path: " + destination); + try + { + var fileInfo = new FileInfo(path); + if (fileInfo.Exists == false) + return Result.Fail("File does not exist"); + var destDir = new FileInfo(destination).Directory; + if (destDir.Exists == false) + { + destDir.Create(); + SetPermissions(destDir.FullName); + } + + fileInfo.MoveTo(destination, overwrite); + SetPermissions(destination); + return true; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result FileCopy(string path, string destination, bool overwrite = true) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + if (IsProtectedPath(ref destination)) + return Result.Fail("Cannot access protected path: " + destination); + try + { + var fileInfo = new FileInfo(path); + if (fileInfo.Exists == false) + return Result.Fail("File does not exist"); + + var destDir = new FileInfo(destination).Directory; + if (destDir.Exists == false) + { + destDir.Create(); + SetPermissions(destDir.FullName); + } + + fileInfo.CopyTo(destination, overwrite); + SetPermissions(destination); + return true; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public Result FileAppendAllText(string path, string text) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + File.AppendAllText(path, text); + SetPermissions(path); + return true; + } + catch (Exception ex) + { + return Result.Fail(ex.Message); + } + } + + public bool FileIsLocal(string path) => true; + + /// + /// Gets the local path + /// + /// the path + /// the local path to the file + public Result GetLocalPath(string path) + => Result.Success(path); + + public Result Touch(string path) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + + if (DirectoryExists(path).Is(true)) + { + try + { + Directory.SetLastWriteTimeUtc(path, DateTime.UtcNow); + return true; + } + catch (Exception ex) + { + return Result.Fail("Failed to touch directory: " + ex.Message); + } + } + + try + { + if (File.Exists(path)) + File.SetLastWriteTimeUtc(path, DateTime.UtcNow); + else + { + File.Create(path); + SetPermissions(path); + } + + return true; + } + catch (Exception ex) + { + return Result.Fail($"Failed to touch file: '{path}' => {ex.Message}"); + } + } + + public Result SetCreationTimeUtc(string path, DateTime date) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + if (!File.Exists(path)) + return Result.Fail("File not found."); + + File.SetCreationTimeUtc(path, date); + return Result.Success(true); + } + catch (Exception ex) + { + return Result.Fail($"Error setting creation time: {ex.Message}"); + } + } + + public Result SetLastWriteTimeUtc(string path, DateTime date) + { + if (IsProtectedPath(ref path)) + return Result.Fail("Cannot access protected path: " + path); + try + { + if (!File.Exists(path)) + return Result.Fail("File not found."); + + File.SetLastWriteTimeUtc(path, date); + return Result.Success(true); + } + catch (Exception ex) + { + return Result.Fail($"Error setting last write time: {ex.Message}"); + } + } + + /// + /// Checks if a path is accessible by the file server + /// + /// the path to check + /// true if accessible, otherwise false + private bool IsProtectedPath(ref string path) + { + if (OperatingSystem.IsWindows()) + path = path.Replace("/", "\\"); + else + path = path.Replace("\\", "/"); + + if(ReplaceVariables != null) + path = ReplaceVariables(path, true); + + if (FileHelper.IsSystemDirectory(path)) + return true; // a system directory, no access + + if (AllowedPaths?.Any() != true) + return false; // no allowed paths configured, allow all + + if (OperatingSystem.IsWindows()) + path = path.ToLowerInvariant(); + + for(int i=0;i logMethod = null) + { + logMethod ??= (string message) => Logger?.ILog(message); + + permissions = permissions != null && permissions > 0 ? permissions : Permissions; + if (permissions == null || permissions < 1) + permissions = 777; + + + if ((File.Exists(path) == false && Directory.Exists(path) == false)) + { + logMethod("SetPermissions: File doesnt existing, skipping"); + return; + } + + //StringLogger stringLogger = new StringLogger(); + var logger = new TestLogger(); + + bool isFile = new FileInfo(path).Exists; + + FileHelper.SetPermissions(logger, path, file: isFile, + permissions: permissions.Value.ToString("D3")); + + FileHelper.ChangeOwner(logger, path, file: isFile, ownerGroup: OwnerGroup); + + logMethod(logger.ToString()); + + return; + + + if (OperatingSystem.IsLinux()) + { + var filePermissions = FileHelper.ConvertLinuxPermissionsToUnixFileMode(permissions.Value); + if (filePermissions == UnixFileMode.None) + { + logMethod("SetPermissions: Invalid file permissions: " + permissions.Value); + return; + } + + File.SetUnixFileMode(path, filePermissions); + logMethod($"SetPermissions: Permission [{filePermissions}] set on file: " + path); + } + + } +} +#endif \ No newline at end of file