diff --git a/ConvertExtensions.cs b/Extensions/ConvertExtensions.cs similarity index 99% rename from ConvertExtensions.cs rename to Extensions/ConvertExtensions.cs index 31a9ee2..58007db 100644 --- a/ConvertExtensions.cs +++ b/Extensions/ConvertExtensions.cs @@ -1,4 +1,4 @@ -namespace Xorog.UniversalExtensions; +namespace Xorog.UniversalExtensions.Converters; public static class ConvertExtensions { diff --git a/Extensions/ExceptionExtensions.cs b/Extensions/ExceptionExtensions.cs new file mode 100644 index 0000000..8a77f65 --- /dev/null +++ b/Extensions/ExceptionExtensions.cs @@ -0,0 +1,5 @@ +namespace Xorog.UniversalExtensions; + +public static class ExceptionExtensions +{ +} diff --git a/Extensions/FileExtensions.cs b/Extensions/FileExtensions.cs new file mode 100644 index 0000000..2479a21 --- /dev/null +++ b/Extensions/FileExtensions.cs @@ -0,0 +1,98 @@ +namespace Xorog.UniversalExtensions; + +public static class FileExtensions +{ + /// + /// Copy a directory recursively + /// + /// + /// + /// + /// + public static void CopyRecursively(this DirectoryInfo sourceDir, DirectoryInfo destDir, bool copySubDirs = true) + { + if (!sourceDir.Exists) + { + throw new DirectoryNotFoundException($"Source directory does not exist or could not be found: {sourceDir.FullName}") + .AttachData("sourceDir", sourceDir) + .AttachData("destDir", destDir) + .AttachData("copySubDirs", copySubDirs); + } + + DirectoryInfo[] dirs = sourceDir.GetDirectories(); + + destDir.Create(); + + FileInfo[] files = sourceDir.GetFiles(); + foreach (FileInfo file in files) + { + var newFilePath = Path.Combine(destDir.FullName, file.Name); + + _logger?.LogDebug("Copying '{file}' to '{dest}'", file.FullName, newFilePath); + file.CopyTo(newFilePath, false); + } + + if (copySubDirs) + foreach (DirectoryInfo subdir in dirs) + CopyRecursively(subdir, new DirectoryInfo(Path.Combine(destDir.FullName, subdir.Name)), copySubDirs); + } + + /// + /// Try deleting the given files and directories until able to or reaching maximum retry count. + /// + /// A list of directories to clean up + /// A list of files to clean up + /// The maximum amount of retries + /// + public static async Task CleanupFilesAndDirectories(List? directoryPaths, List? filePaths, int maxRetryCount = 100) + { + var failCount = 0; + + List exceptions = new(); + + if (directoryPaths?.IsNotNullAndNotEmpty() ?? false) + foreach (string DirectoryPath in directoryPaths) + { + while (Directory.Exists(DirectoryPath)) + { + try + { + Directory.Delete(DirectoryPath, true); + } + catch (Exception ex) + { + exceptions.Add(ex); + await Task.Delay(5000); + failCount++; + + if (failCount > maxRetryCount) + throw new AggregateException("Failed to delete directory", exceptions); + } + } + } + + failCount = 0; + exceptions.Clear(); + + if (filePaths?.IsNotNullAndNotEmpty() ?? false) + foreach (string file in filePaths) + { + while (File.Exists(file)) + { + try + { + File.Delete(file); + } + catch (Exception ex) + { + exceptions.Add(ex); + await Task.Delay(5000); + failCount++; + + if (failCount > maxRetryCount) + throw new AggregateException("Failed to delete file", exceptions); + } + } + } + } +} diff --git a/Extensions/HashingExtensions.cs b/Extensions/HashingExtensions.cs new file mode 100644 index 0000000..16c9d3a --- /dev/null +++ b/Extensions/HashingExtensions.cs @@ -0,0 +1,27 @@ +namespace Xorog.UniversalExtensions; + +public static class HashingExtensions +{ + /// + /// Compute the SHA256-Hash for the given string + /// + /// + /// + public static string ComputeSHA256Hash(string str) + { + using SHA256 _SHA256 = SHA256.Create(); + return BitConverter.ToString(_SHA256.ComputeHash(Encoding.ASCII.GetBytes(str))).Replace("-", "").ToLowerInvariant(); + } + + /// + /// Compute the SHA256-Hash for a given file + /// + /// + /// + public static string ComputeSHA256Hash(FileInfo filePath) + { + using SHA256 _SHA256 = SHA256.Create(); + using FileStream fileStream = filePath.OpenRead(); + return BitConverter.ToString(_SHA256.ComputeHash(fileStream)).Replace("-", "").ToLowerInvariant(); + } +} diff --git a/Extensions/ListExtensions.cs b/Extensions/ListExtensions.cs new file mode 100644 index 0000000..d405b28 --- /dev/null +++ b/Extensions/ListExtensions.cs @@ -0,0 +1,40 @@ +namespace Xorog.UniversalExtensions; + +public static class ListExtensions +{ + /// + /// Select a random item from a list + /// + /// + /// + /// The randomly selected item + /// The list is null + /// The list is empty + public static T SelectRandom(this IEnumerable? obj) + { + if (obj == null) + { + throw new ArgumentNullException(); + } + + if (!obj.Any()) + { + throw new ArgumentException("The sequence is empty."); + } + + + int rng = new Random().Next(0, obj.Count()); + return obj.ElementAt(rng) ?? throw new ArgumentNullException(); + } + + + + /// + /// Check whether a list contains elements and is not null + /// + /// + /// + /// Whether the list contains elements and is not null + public static bool IsNotNullAndNotEmpty(this IEnumerable? obj) + => obj is not null && obj.Any(); +} diff --git a/Extensions/ScheduledTaskExtensions.cs b/Extensions/ScheduledTaskExtensions.cs new file mode 100644 index 0000000..395d9b1 --- /dev/null +++ b/Extensions/ScheduledTaskExtensions.cs @@ -0,0 +1,111 @@ +namespace Xorog.UniversalExtensions; +public static class ScheduledTaskExtensions +{ + /// + /// Create a scheduled task + /// + /// The task to run + /// The time to run the task + /// Any custom data you wish to provide. + /// An unique identifier of the task + + public static string CreateScheduledTask(this Task task, DateTime runTime, object? customData = null) + { + if (task.Status != TaskStatus.WaitingForActivation) + throw new InvalidOperationException("The task is already being executed or has been scheduled for execution.") + .AttachData("Task", task) + .AttachData("RunTime", runTime) + .AttachData("CustomData", customData); + + string UID = Guid.NewGuid().ToString(); + CancellationTokenSource CancellationToken = new CancellationTokenSource(); + + if (Math.Ceiling(runTime.GetTimespanUntil().TotalMilliseconds) < 0) + runTime = DateTime.UtcNow.AddSeconds(1); + + _ = LongDelay(runTime.GetTimespanUntil(), CancellationToken).ContinueWith(x => + { + lock (RegisteredScheduledTasks) + { + RegisteredScheduledTasks.Remove(UID); + } + + _logger?.LogDebug("Running scheduled task with UID '{UID}'", UID, runTime.GetTimespanUntil().GetHumanReadable()); + + if (x.IsCompletedSuccessfully) + task.Start(); + }); + + lock (RegisteredScheduledTasks) + { + _logger?.LogDebug("Creating scheduled task with UID '{UID}' running in {RunTime}", UID, runTime.GetTimespanUntil().GetHumanReadable()); + + RegisteredScheduledTasks.Add(UID, new ScheduledTask + { + Uid = UID, + RunTime = runTime, + TokenSource = CancellationToken, + CustomData = customData, + }); + } + return UID; + } + + + + /// + /// Deletes a scheduled task + /// + /// The task's unique identifier + /// Throws if the task hasn't been found or if an internal error occurred + public static void DeleteScheduledTask(string UID) + { + if (!RegisteredScheduledTasks.ContainsKey(UID)) + throw new KeyNotFoundException($"No scheduled task has been found with UID '{UID}'"); + + if (RegisteredScheduledTasks[UID].TokenSource is null) + throw new Exception($"Internal: There is no token source registered the specified task."); + + _logger?.LogDebug("Deleting scheduled task with UID '{UID}'", UID); + + lock (RegisteredScheduledTasks) + { + RegisteredScheduledTasks[UID].TokenSource?.Cancel(); + RegisteredScheduledTasks.Remove(UID); + } + return; + } + + + + /// + /// Gets a list of all registered tasks + /// + /// A list of all registered tasks + public static IReadOnlyList? GetScheduledTasks() + => RegisteredScheduledTasks.Select(x => x.Value).ToList().AsReadOnly(); + + /// + /// Gets a specific task + /// + /// The unique identifier of what task to get + /// The task + /// Throws if the task has not been found + public static ScheduledTask GetScheduledTask(string UID) + => RegisteredScheduledTasks[UID]; + + internal static async Task LongDelay(TimeSpan delay, CancellationTokenSource token) + { + var st = new Stopwatch(); + st.Start(); + while (true && !token.IsCancellationRequested) + { + var remaining = (delay - st.Elapsed).TotalMilliseconds; + if (remaining <= 0) + break; + if (remaining > Int16.MaxValue) + remaining = Int16.MaxValue; + await Task.Delay(TimeSpan.FromMilliseconds(remaining), token.Token); + } + } +} diff --git a/Extensions/StringExtensions.cs b/Extensions/StringExtensions.cs new file mode 100644 index 0000000..342f561 --- /dev/null +++ b/Extensions/StringExtensions.cs @@ -0,0 +1,105 @@ +namespace Xorog.UniversalExtensions; + +public static class StringExtensions +{ + /// + /// Extensions for string.IsNullOrWhiteSpace + /// + /// + /// Whether the string is null, empty or only contains whitespaces + public static bool IsNullOrWhiteSpace(this string str) + => string.IsNullOrWhiteSpace(str); + + /// + /// Extensions for string.IsNullOrEmpty + /// + /// + /// Whether the string is null or empty + public static bool IsNullOrEmpty(this string str) + => string.IsNullOrEmpty(str); + + /// + /// Check if a string contains only digits + /// + /// + /// + public static bool IsDigitsOnly(this string str) + { + foreach (char c in str) + { + if (c is < '0' or > '9') + return false; + } + + return true; + } + + /// + /// Get all digits from a string + /// + /// + /// + public static string GetAllDigits(this string str) => + new(str.Where(Char.IsDigit).ToArray()); + + /// + /// Get country flag emoji based on Iso Country Code + /// + /// + /// + public static string IsoCountryCodeToFlagEmoji(this string country) + { + return string.Concat(country.ToUpper().Select(x => char.ConvertFromUtf32(x + 0x1F1A5))); + } + + /// + /// Shorten a string to the given length + /// + /// + /// + /// + public static string Truncate(this string value, int maxLength) + { + if (string.IsNullOrEmpty(value)) + return value; + return value.Length <= maxLength ? value : value[..maxLength]; + } + + /// + /// Shorten a string to the given length and add ".." at the end + /// + /// + /// + /// + public static string TruncateWithIndication(this string value, int maxLength) + { + if (string.IsNullOrEmpty(value)) + return value; + + return value.Length <= maxLength ? value : $"{value[..(maxLength - 2)]}.."; + } + + /// + /// Remove unsupported characters from string to generate a valid filename + /// + /// The string with potentionally unwanted characters + /// The character the unwanted characters get replaced with (default: _) + /// A valid filename + public static string MakeValidFileName(this string name, char replace_char = '_') + { + string invalidChars = System.Text.RegularExpressions.Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars())); + string invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars); + + return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, replace_char.ToString()).Replace('&', replace_char); + } + + /// + /// Changes the first letter to upper. + /// + /// The string to modify. + /// The string with the first letter changed to upper. + public static string FirstLetterToUpper(this string str) + { + return $"{str.First().ToString().ToUpper()}{str.Remove(0, 1)}"; + } +} diff --git a/Extensions/TimeExtensions.cs b/Extensions/TimeExtensions.cs new file mode 100644 index 0000000..7f2e88d --- /dev/null +++ b/Extensions/TimeExtensions.cs @@ -0,0 +1,112 @@ +namespace Xorog.UniversalExtensions; + +public static class TimeExtensions +{ + /// + /// Get a timespan from now to the given time. Negative on values in the past. + /// + /// + /// + public static TimeSpan GetTimespanUntil(this DateTime until) => + (until.ToUniversalTime() - DateTime.UtcNow); + + /// + /// + /// + /// + /// + public static TimeSpan GetTimespanUntil(this DateTimeOffset until) => + (until.ToUniversalTime() - DateTime.UtcNow); + + /// + /// Get the total seconds until a given DateTime. Negative on values in the past. + /// + /// + /// + public static long GetTotalSecondsUntil(this DateTime until) => + ((long)Math.Ceiling((until.ToUniversalTime() - DateTime.UtcNow).TotalSeconds)); + + /// + /// + /// + /// + /// + public static long GetTotalSecondsUntil(this DateTimeOffset until) => + ((long)Math.Ceiling((until.ToUniversalTime() - DateTime.UtcNow).TotalSeconds)); + + /// + /// Get a timespan from now to the given time. Negative on values in the future. + /// + /// + /// + public static TimeSpan GetTimespanSince(this DateTime until) => + (DateTime.UtcNow - until.ToUniversalTime()); + + /// + /// + /// + /// + /// + public static TimeSpan GetTimespanSince(this DateTimeOffset until) => + (DateTime.UtcNow - until.ToUniversalTime()); + + /// + /// Get the total seconds since a given DateTime. Negative on values in the future. + /// + /// + /// + public static long GetTotalSecondsSince(this DateTime until) => + ((long)Math.Ceiling((DateTime.UtcNow - until.ToUniversalTime()).TotalSeconds)); + + /// + /// + /// + /// + /// + public static long GetTotalSecondsSince(this DateTimeOffset until) => + ((long)Math.Ceiling((DateTime.UtcNow - until.ToUniversalTime()).TotalSeconds)); + + /// + /// Get a short human readable string for the given amount of seconds + /// + /// + /// + /// + public static string GetShortHumanReadable(this int seconds, TimeFormat timeFormat = TimeFormat.DAYS) => + TimeSpan.FromSeconds(seconds).GetShortTimeFormat(timeFormat); + + /// + /// + /// + /// + /// + /// + public static string GetShortHumanReadable(this long seconds, TimeFormat timeFormat = TimeFormat.DAYS) => + TimeSpan.FromSeconds(seconds).GetShortTimeFormat(timeFormat); + + /// + /// Get a short human readable string for the given amount of time + /// + /// + /// + /// + public static string GetShortHumanReadable(this TimeSpan timespan, TimeFormat timeFormat = TimeFormat.DAYS) => + timespan.GetShortTimeFormat(timeFormat); + + /// + /// Get a human readable string for the given amount of time. + /// + /// + /// + /// + public static string GetHumanReadable(this int seconds, TimeFormat timeFormat = TimeFormat.DAYS, HumanReadableTimeFormatConfig? config = null) => + TimeSpan.FromSeconds(seconds).GetTimeFormat(timeFormat, config); + + /// + public static string GetHumanReadable(this long seconds, TimeFormat timeFormat = TimeFormat.DAYS, HumanReadableTimeFormatConfig? config = null) => + TimeSpan.FromSeconds(seconds).GetTimeFormat(timeFormat, config); + + /// + public static string GetHumanReadable(this TimeSpan timeSpan, TimeFormat timeFormat = TimeFormat.DAYS, HumanReadableTimeFormatConfig? config = null) => + timeSpan.GetTimeFormat(timeFormat, config); +} diff --git a/Internal/Internal.cs b/Internal/Internal.cs index 597b952..69b4697 100644 --- a/Internal/Internal.cs +++ b/Internal/Internal.cs @@ -114,7 +114,7 @@ public class InternalSheduler /// Delete this task. /// public void Delete() => - UniversalExtensions.DeleteScheduledTask(Uid); + ScheduledTaskExtensions.DeleteScheduledTask(Uid); } } diff --git a/Tools/ColorTools.cs b/Tools/ColorTools.cs new file mode 100644 index 0000000..71b4789 --- /dev/null +++ b/Tools/ColorTools.cs @@ -0,0 +1,38 @@ +namespace Xorog.UniversalExtensions; + +public static class ColorTools +{ + /// + /// Get closest Color to given Color + /// + /// + /// + /// + public static Color GetClosestColor(List colorArray, Color baseColor) + { + var colors = colorArray.Select(x => new { Value = x, Diff = Internal.GetDiff(x, baseColor) }).ToList(); + var min = colors.Min(x => x.Diff); + return colors.Find(x => x.Diff == min).Value; + } + + /// + /// Convert Hex to Color + /// + /// The converted color + public static Color ToColor(this string str) + { + return ColorTranslator.FromHtml(str); + } + + /// + /// Convert RGB Value to Hex + /// + /// Red + /// Green + /// Blue + /// A string that represents the color in hex (e.g. 255, 0, 0 -> #FF0000) + public static string ToHex(int R, int G, int B) + { + return "#" + R.ToString("X2") + G.ToString("X2") + B.ToString("X2"); + } +} diff --git a/Tools/MathTools.cs b/Tools/MathTools.cs new file mode 100644 index 0000000..5a8010c --- /dev/null +++ b/Tools/MathTools.cs @@ -0,0 +1,20 @@ +namespace Xorog.UniversalExtensions; + +public static class MathTools +{ + /// + /// Calculates the percentage of the given 2 values. + /// + /// The current value. + /// The maximum value. + /// The percentage. + /// + public static int CalculatePercentage(double current, double max) + { + if (max == 0) + throw new ArgumentException("Max cannot be zero."); + + double percentage = (current / max) * 100; + return Convert.ToInt32(percentage); + } +} diff --git a/Tools/StringTools.cs b/Tools/StringTools.cs new file mode 100644 index 0000000..04dacdd --- /dev/null +++ b/Tools/StringTools.cs @@ -0,0 +1,34 @@ +namespace Xorog.UniversalExtensions; + +public static class StringTools +{ + /// + /// Generate an ASCII Progressbar + /// + /// The current progress + /// The maximum progress + /// How long the ASCII Progressbar should be (default: 44) + /// What character the filled part should be (default: ) + /// What character the not-filled part should be (default: ) + /// What character the start-part should be (default: [) + /// What character the end-part part should be (default: ]) + /// A progressbar + public static string GenerateASCIIProgressbar(double current, double max, int charlength = 44, char fill = '█', char empty = '∙', char start = '[', char end = ']') + { + long first = (long)Math.Round((current / max) * charlength, 0); + + long second = charlength - first; + + string mediadisplay = start.ToString(); + + for (long i = 0; i < first; i++) + mediadisplay += fill; + + for (long i = 0; i < second; i++) + mediadisplay += empty; + + mediadisplay += end; + + return mediadisplay; + } +} diff --git a/Tools/WebTools.cs b/Tools/WebTools.cs new file mode 100644 index 0000000..4135731 --- /dev/null +++ b/Tools/WebTools.cs @@ -0,0 +1,78 @@ +namespace Xorog.UniversalExtensions; + +public static class WebTools +{ + /// + /// Get the URL a redirect leads to (limited to StatusCodes 301, 303, 307, 308) + /// + /// The shortened URL + /// The URL the redirect leads to + public static async Task UnshortenUrl(string url, bool UseHeadMethod = true) + { + _logger?.LogDebug("Unshortening Url '{Url}', using head method: {UseHeadMethod}", url, UseHeadMethod); + + HttpClient client = new(new HttpClientHandler() + { + AllowAutoRedirect = false, + AutomaticDecompression = DecompressionMethods.GZip, + + }); + client.Timeout = TimeSpan.FromSeconds(60); + client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36"); + client.DefaultRequestHeaders.Add("upgrade-insecure-requests", "1"); + client.DefaultRequestHeaders.Add("accept-encoding", "gzip, deflate, br"); + client.DefaultRequestHeaders.Add("accept-language", "en-US,en;q=0.9"); + client.MaxResponseContentBufferSize = 4096; + + HttpRequestMessage requestMessage = new HttpRequestMessage((UseHeadMethod ? HttpMethod.Head : HttpMethod.Get), url); + + CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + var request_task = client.SendAsync(requestMessage, cancellationTokenSource.Token); + + try + { + await request_task.WaitAsync(TimeSpan.FromSeconds(3)); + } + catch (Exception) + { + if (UseHeadMethod) + return await UnshortenUrl(url, false); + + throw; + } + + if (!request_task.IsCompleted) + cancellationTokenSource.Cancel(); + + if (UseHeadMethod && request_task.IsFaulted && request_task.Exception.InnerException.GetType() == typeof(HttpRequestException)) + { + _logger?.LogWarning("Unshortening Url '{Url}' failed, falling back to non-head method", url); + return await UnshortenUrl(url, false); + } + + var statuscode = request_task.Result.StatusCode; + var header = request_task.Result.Headers; + + if (UseHeadMethod && statuscode is HttpStatusCode.NotFound or HttpStatusCode.InternalServerError) + { + _logger?.LogWarning("Unshortening Url '{Url}' failed, falling back to non-head method", url); + return await UnshortenUrl(url, false); + } + + if (statuscode is HttpStatusCode.Found + or HttpStatusCode.Redirect + or HttpStatusCode.SeeOther + or HttpStatusCode.RedirectKeepVerb + or HttpStatusCode.RedirectMethod + or HttpStatusCode.PermanentRedirect + or HttpStatusCode.TemporaryRedirect) + { + if (header is not null && header.Location is not null) + return await UnshortenUrl(header.Location.AbsoluteUri); + else + return url; + } + else + return url; + } +} diff --git a/WindowsAPI/WindowsUtils.cs b/Tools/WindowsAPI/WindowsUtils.cs similarity index 100% rename from WindowsAPI/WindowsUtils.cs rename to Tools/WindowsAPI/WindowsUtils.cs diff --git a/WindowsAPI/kernel32.cs b/Tools/WindowsAPI/kernel32.cs similarity index 100% rename from WindowsAPI/kernel32.cs rename to Tools/WindowsAPI/kernel32.cs diff --git a/WindowsAPI/psapi.cs b/Tools/WindowsAPI/psapi.cs similarity index 100% rename from WindowsAPI/psapi.cs rename to Tools/WindowsAPI/psapi.cs diff --git a/UniversalExtensions.cs b/UniversalExtensions.cs index 5c74aec..1391efd 100644 --- a/UniversalExtensions.cs +++ b/UniversalExtensions.cs @@ -4,7 +4,21 @@ namespace Xorog.UniversalExtensions; public static class UniversalExtensions { - public static void LoadAllReferencedAssemblies(AppDomain domain) + /// + /// Attaches a logger to UniversalExtensions. Used for Debugging. + /// + /// + public static void AttachLogger(ILogger logger) + { + _logger = logger; + } + + /// + /// Loads all referenced dependencies in an . + /// Prevents s that are caused by utilizing not yet loaded assemblies after the system has been updated. + /// + /// The to load all dependencies from. + public static void LoadAllReferencedAssemblies(this AppDomain domain) { _logger?.LogDebug("Loading all assemblies.."); @@ -38,70 +52,19 @@ public static class UniversalExtensions } /// - /// Attaches a logger to UniversalExtensions. Used for Debug purposes. - /// - /// - public static void AttachLogger(ILogger logger) - { - _logger = logger; - } - - - /// - /// Extensions for string.IsNullOrWhiteSpace - /// - /// - /// Whether the string is null, empty or only contains whitespaces - public static bool IsNullOrWhiteSpace(this string str) => string.IsNullOrWhiteSpace(str); - - - - /// - /// Extensions for string.IsNullOrEmpty - /// - /// - /// Whether the string is null or empty - public static bool IsNullOrEmpty(this string str) => string.IsNullOrEmpty(str); - - - - /// - /// Select a random item from a list + /// Adds additional data to an exception. /// /// - /// - /// The randomly selected item - /// The list is null - /// The list is empty - public static T SelectRandom(this IEnumerable obj) + /// + /// + /// + /// + public static T AttachData(this T exception, string Key, object? Data) where T : Exception { - if (obj == null) - { - throw new ArgumentNullException(); - } - - if (!obj.Any()) - { - throw new ArgumentException("The sequence is empty."); - } - - - int rng = new Random().Next(0, obj.Count()); - return obj.ElementAt(rng) ?? throw new ArgumentNullException(); + exception.Data.Add(Key, Data); + return exception; } - - - /// - /// Check whether a list contains elements and is not null - /// - /// - /// - /// Whether the list contains elements and is not null - public static bool IsNotNullAndNotEmpty(this IEnumerable obj) => obj is not null && obj.Any(); - - - /// /// Get the current CPU Usage on all platforms /// @@ -119,616 +82,4 @@ public static class UniversalExtensions var cpuUsageTotal = cpuUsedMs / (Environment.ProcessorCount * totalMsPassed); return cpuUsageTotal * 100; } - - - - /// - /// Copy a directory recursively - /// - /// - /// - /// - /// - public static void DirectoryCopy(string sourceDirName, string destDirName, bool copySubDirs) - { - // Get the subdirectories for the specified directory. - DirectoryInfo dir = new(sourceDirName); - - if (!dir.Exists) - { - throw new DirectoryNotFoundException( - "Source directory does not exist or could not be found: " - + sourceDirName); - } - - DirectoryInfo[] dirs = dir.GetDirectories(); - - // If the destination directory doesn't exist, create it. - Directory.CreateDirectory(destDirName); - - // Get the files in the directory and copy them to the new location. - FileInfo[] files = dir.GetFiles(); - foreach (FileInfo file in files) - { - string tempPath = Path.Combine(destDirName, file.Name); - file.CopyTo(tempPath, false); - } - - // If copying subdirectories, copy them and their contents to new location. - if (copySubDirs) - { - foreach (DirectoryInfo subdir in dirs) - { - string tempPath = Path.Combine(destDirName, subdir.Name); - DirectoryCopy(subdir.FullName, tempPath, copySubDirs); - } - } - } - - - - /// - /// Generate an ASCII Progressbar - /// - /// The current progress - /// The maximum progress - /// How long the ASCII Progressbar should be (default: 44) - /// What character the filled part should be (default: ) - /// What character the not-filled part should be (default: ) - /// What character the start-part should be (default: [) - /// What character the end-part part should be (default: ]) - /// A progressbar - public static string GenerateASCIIProgressbar(double current, double max, int charlength = 44, char fill = '█', char empty = '∙', char start = '[', char end = ']') - { - long first = (long)Math.Round((current / max) * charlength, 0); - - long second = charlength - first; - - string mediadisplay = start.ToString(); - - for (long i = 0; i < first; i++) - mediadisplay += fill; - - for (long i = 0; i < second; i++) - mediadisplay += empty; - - mediadisplay += end; - - return mediadisplay; - } - - - - /// - /// Get the URL a redirect leads to (limited to StatusCodes 301, 303, 307, 308) - /// - /// The shortened URL - /// The URL the redirect leads to - public static async Task UnshortenUrl(string url, bool UseHeadMethod = true) - { - _logger?.LogDebug("Unshortening Url '{Url}', using head method: {UseHeadMethod}", url, UseHeadMethod); - - HttpClient client = new(new HttpClientHandler() - { - AllowAutoRedirect = false, - AutomaticDecompression = DecompressionMethods.GZip, - - }); - client.Timeout = TimeSpan.FromSeconds(60); - client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36"); - client.DefaultRequestHeaders.Add("upgrade-insecure-requests", "1"); - client.DefaultRequestHeaders.Add("accept-encoding", "gzip, deflate, br"); - client.DefaultRequestHeaders.Add("accept-language", "en-US,en;q=0.9"); - client.MaxResponseContentBufferSize = 4096; - - HttpRequestMessage requestMessage = new HttpRequestMessage((UseHeadMethod ? HttpMethod.Head : HttpMethod.Get), url); - - CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - var request_task = client.SendAsync(requestMessage, cancellationTokenSource.Token); - - try - { - await request_task.WaitAsync(TimeSpan.FromSeconds(3)); - } - catch (Exception) - { - if (UseHeadMethod) - return await UnshortenUrl(url, false); - - throw; - } - - if (!request_task.IsCompleted) - cancellationTokenSource.Cancel(); - - if (UseHeadMethod && request_task.IsFaulted && request_task.Exception.InnerException.GetType() == typeof(HttpRequestException)) - { - _logger?.LogWarning("Unshortening Url '{Url}' failed, falling back to non-head method", url); - return await UnshortenUrl(url, false); - } - - var statuscode = request_task.Result.StatusCode; - var header = request_task.Result.Headers; - - if (UseHeadMethod && statuscode is HttpStatusCode.NotFound or HttpStatusCode.InternalServerError) - { - _logger?.LogWarning("Unshortening Url '{Url}' failed, falling back to non-head method", url); - return await UnshortenUrl(url, false); - } - - if (statuscode is HttpStatusCode.Found - or HttpStatusCode.Redirect - or HttpStatusCode.SeeOther - or HttpStatusCode.RedirectKeepVerb - or HttpStatusCode.RedirectMethod - or HttpStatusCode.PermanentRedirect - or HttpStatusCode.TemporaryRedirect) - { - if (header is not null && header.Location is not null) - return await UnshortenUrl(header.Location.AbsoluteUri); - else - return url; - } - else - return url; - } - - - - /// - /// Try deleting the given files and directories until able to - /// - /// A list of directories to clean up - /// A list of files to clean up - /// - public static async Task CleanupFilesAndDirectories(List DirectoryPaths, List FilePaths) - { - foreach (string DirectoryPath in DirectoryPaths) - { - while (Directory.Exists(DirectoryPath)) - { - try - { - Directory.Delete(DirectoryPath, true); - } - catch (Exception) - { - await Task.Delay(5000); - } - } - } - - foreach (string FilePath in FilePaths) - { - while (File.Exists(FilePath)) - { - try - { - File.Delete(FilePath); - } - catch (Exception) - { - await Task.Delay(5000); - } - } - } - } - - - - /// - /// Runs a long non-blocking delay, a work-around for Task.Delay only supporting Int32 - /// - /// A timespan of how long the delay should last - /// A cancellation token source to cancel the action - /// - internal static async Task LongDelay(TimeSpan delay, CancellationTokenSource token) - { - var st = new Stopwatch(); - st.Start(); - while (true && !token.IsCancellationRequested) - { - var remaining = (delay - st.Elapsed).TotalMilliseconds; - if (remaining <= 0) - break; - if (remaining > Int16.MaxValue) - remaining = Int16.MaxValue; - await Task.Delay(TimeSpan.FromMilliseconds(remaining), token.Token); - } - } - - - - /// - /// Create a scheduled task - /// - /// The task to run - /// The time to run the task - /// Any custom data you wish to provide. - /// An unique identifier of the task - - public static string CreateScheduledTask(this Task task, DateTime runTime, object? customData = null) - { - string UID = Guid.NewGuid().ToString(); - CancellationTokenSource CancellationToken = new CancellationTokenSource(); - - if (Math.Ceiling(runTime.GetTimespanUntil().TotalMilliseconds) < 0) - runTime = DateTime.UtcNow.AddSeconds(1); - - _ = LongDelay(runTime.GetTimespanUntil(), CancellationToken).ContinueWith(x => - { - lock (RegisteredScheduledTasks) - { - RegisteredScheduledTasks.Remove(UID); - } - - _logger?.LogDebug("Running scheduled task with UID '{UID}'", UID, runTime.GetTimespanUntil().GetHumanReadable()); - - if (x.IsCompletedSuccessfully) - task.Start(); - }); - - lock (RegisteredScheduledTasks) - { - _logger?.LogDebug("Creating scheduled task with UID '{UID}' running in {RunTime}", UID, runTime.GetTimespanUntil().GetHumanReadable()); - - RegisteredScheduledTasks.Add(UID, new ScheduledTask - { - Uid = UID, - RunTime = runTime, - TokenSource = CancellationToken, - CustomData = customData, - }); - } - return UID; - } - - - - /// - /// Deletes a scheduled task - /// - /// The task's unique identifier - /// Throws if the task hasn't been found or if an internal error occurred - public static void DeleteScheduledTask(string UID) - { - if (!RegisteredScheduledTasks.ContainsKey(UID)) - throw new KeyNotFoundException($"No scheduled task has been found with UID '{UID}'"); - - if (RegisteredScheduledTasks[UID].TokenSource is null) - throw new Exception($"Internal: There is no token source registered the specified task."); - - _logger?.LogDebug("Deleting scheduled task with UID '{UID}'", UID); - - lock (RegisteredScheduledTasks) - { - RegisteredScheduledTasks[UID].TokenSource?.Cancel(); - RegisteredScheduledTasks.Remove(UID); - } - return; - } - - - - /// - /// Gets a list of all registered tasks - /// - /// A list of all registered tasks - public static IReadOnlyList? GetScheduledTasks() - => RegisteredScheduledTasks.Select(x => x.Value).ToList().AsReadOnly(); - - /// - /// Gets a specific task - /// - /// The unique identifier of what task to get - /// The task - /// Throws if the task has not been found - public static ScheduledTask GetScheduledTask(string UID) - => RegisteredScheduledTasks[UID]; - - - - /// - /// Compute the SHA256-Hash for the given string - /// - /// - /// - public static string ComputeSHA256Hash(string str) - { - using SHA256 _SHA256 = SHA256.Create(); - return BitConverter.ToString(_SHA256.ComputeHash(Encoding.ASCII.GetBytes(str))).Replace("-", "").ToLowerInvariant(); - } - - - - /// - /// Compute the SHA256-Hash for a given file - /// - /// - /// - public static string ComputeSHA256Hash(FileInfo filePath) - { - using SHA256 _SHA256 = SHA256.Create(); - using FileStream fileStream = filePath.OpenRead(); - return BitConverter.ToString(_SHA256.ComputeHash(fileStream)).Replace("-", "").ToLowerInvariant(); - } - - - - /// - /// Get a timespan from now to the given time. Negative on values in the past. - /// - /// - /// - public static TimeSpan GetTimespanUntil(this DateTime until) => - (until.ToUniversalTime() - DateTime.UtcNow); - - - - /// - /// - /// - /// - /// - public static TimeSpan GetTimespanUntil(this DateTimeOffset until) => - (until.ToUniversalTime() - DateTime.UtcNow); - - - /// - /// Get the total seconds until a given DateTime. Negative on values in the past. - /// - /// - /// - public static long GetTotalSecondsUntil(this DateTime until) => - ((long)Math.Ceiling((until.ToUniversalTime() - DateTime.UtcNow).TotalSeconds)); - - - - /// - /// - /// - /// - /// - public static long GetTotalSecondsUntil(this DateTimeOffset until) => - ((long)Math.Ceiling((until.ToUniversalTime() - DateTime.UtcNow).TotalSeconds)); - - - - /// - /// Get a timespan from now to the given time. Negative on values in the future. - /// - /// - /// - public static TimeSpan GetTimespanSince(this DateTime until) => - (DateTime.UtcNow - until.ToUniversalTime()); - - - - /// - /// - /// - /// - /// - public static TimeSpan GetTimespanSince(this DateTimeOffset until) => - (DateTime.UtcNow - until.ToUniversalTime()); - - - - /// - /// Get the total seconds since a given DateTime. Negative on values in the future. - /// - /// - /// - public static long GetTotalSecondsSince(this DateTime until) => - ((long)Math.Ceiling((DateTime.UtcNow - until.ToUniversalTime()).TotalSeconds)); - - - - /// - /// - /// - /// - /// - public static long GetTotalSecondsSince(this DateTimeOffset until) => - ((long)Math.Ceiling((DateTime.UtcNow - until.ToUniversalTime()).TotalSeconds)); - - - - /// - /// Get a short human readable string for the given amount of seconds - /// - /// - /// - /// - public static string GetShortHumanReadable(this int seconds, TimeFormat timeFormat = TimeFormat.DAYS) => - TimeSpan.FromSeconds(seconds).GetShortTimeFormat(timeFormat); - - - - /// - /// - /// - /// - /// - /// - public static string GetShortHumanReadable(this long seconds, TimeFormat timeFormat = TimeFormat.DAYS) => - TimeSpan.FromSeconds(seconds).GetShortTimeFormat(timeFormat); - - public static string GetShortHumanReadable(this TimeSpan timespan, TimeFormat timeFormat = TimeFormat.DAYS) => - timespan.GetShortTimeFormat(timeFormat); - - - - /// - /// Get a human readable string for the given amount of time. - /// - /// - /// - /// - public static string GetHumanReadable(this int seconds, TimeFormat timeFormat = TimeFormat.DAYS, HumanReadableTimeFormatConfig? config = null) => - TimeSpan.FromSeconds(seconds).GetTimeFormat(timeFormat,config); - - - - /// - public static string GetHumanReadable(this long seconds, TimeFormat timeFormat = TimeFormat.DAYS, HumanReadableTimeFormatConfig? config = null) => - TimeSpan.FromSeconds(seconds).GetTimeFormat(timeFormat, config); - - /// - public static string GetHumanReadable(this TimeSpan timeSpan, TimeFormat timeFormat = TimeFormat.DAYS, HumanReadableTimeFormatConfig? config = null) => - timeSpan.GetTimeFormat(timeFormat, config); - - - - /// - /// Check if a string contains only digits - /// - /// - /// - public static bool IsDigitsOnly(this string str) - { - foreach (char c in str) - { - if (c is < '0' or > '9') - return false; - } - - return true; - } - - - - /// - /// Get all digits from a string - /// - /// - /// - public static string GetAllDigits(this string str) => - new(str.Where(Char.IsDigit).ToArray()); - - - - /// - /// Get country flag emoji based on Iso Country Code - /// - /// - /// - public static string IsoCountryCodeToFlagEmoji(this string country) - { - return string.Concat(country.ToUpper().Select(x => char.ConvertFromUtf32(x + 0x1F1A5))); - } - - - /// - /// Get closest Color to given Color - /// - /// - /// - /// - public static Color GetClosestColor(List colorArray, Color baseColor) - { - var colors = colorArray.Select(x => new { Value = x, Diff = Internal.GetDiff(x, baseColor) }).ToList(); - var min = colors.Min(x => x.Diff); - return colors.Find(x => x.Diff == min).Value; - } - - - /// - /// Convert Hex to Color - /// - /// The converted color - public static Color ToColor(this string str) - { - return ColorTranslator.FromHtml(str); - } - - - /// - /// Convert RGB Value to Hex - /// - /// Red - /// Green - /// Blue - /// A string that represents the color in hex (e.g. 255, 0, 0 -> #FF0000) - public static string ToHex(int R, int G, int B) - { - return "#" + R.ToString("X2") + G.ToString("X2") + B.ToString("X2"); - } - - - - /// - /// Calculates the percentage of the given 2 values. - /// - /// The current value. - /// The maximum value. - /// The percentage. - /// - public static int CalculatePercentage(double current, double max) - { - if (max == 0) - throw new ArgumentException("Max cannot be zero."); - - double percentage = (current / max) * 100; - return Convert.ToInt32(percentage); - } - - - - /// - /// Shorten a string to the given length - /// - /// - /// - /// - public static string Truncate(this string value, int maxLength) - { - if (string.IsNullOrEmpty(value)) - return value; - return value.Length <= maxLength ? value : value[..maxLength]; - } - - - - /// - /// Shorten a string to the given length and add ".." at the end - /// - /// - /// - /// - public static string TruncateWithIndication(this string value, int maxLength) - { - if (string.IsNullOrEmpty(value)) - return value; - - return value.Length <= maxLength ? value : $"{value[..(maxLength - 2)]}.."; - } - - - - /// - /// Remove unsupported characters from string to generate a valid filename - /// - /// The string with potentionally unwanted characters - /// The character the unwanted characters get replaced with (default: _) - /// A valid filename - public static string MakeValidFileName(this string name, char replace_char = '_') - { - string invalidChars = System.Text.RegularExpressions.Regex.Escape(new string(System.IO.Path.GetInvalidFileNameChars())); - string invalidRegStr = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars); - - return System.Text.RegularExpressions.Regex.Replace(name, invalidRegStr, replace_char.ToString()).Replace('&', replace_char); - } - - - - /// - /// Changes the first letter to upper. - /// - /// The string to modify. - /// The string with the first letter changed to upper. - public static string FirstLetterToUpper(this string str) - { - return $"{str.First().ToString().ToUpper()}{str.Remove(0, 1)}"; - } } \ No newline at end of file