diff --git a/.vscode/launch.json b/.vscode/launch.json index efebb01..84b47b6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,17 +13,42 @@ "args": [ "s", "X", - "D:/5-Other-Small/Kanban/DEP08CEPIEPSILON", + "D:/6-Other-Large-Z/https-linux-ubuntu-server/etc", + "Day-Helper-2024-12-24", + "dorico.phares.duckdns.org", + "php", + "444", + "555", + "666", + "777", + "888", + "999", + "s", + "X", + "D:/5-Other-Small/Kanban/Year-Season", "Day-Helper-2024-06-23", "*.md", - "## Sub-tasks", + "##_Sub-tasks", "code-insiders", "index.md", - "- [,](", - "## Done", + "-_[,](", + "##_Done", ".kan", + "D:/5-Other-Small/Kanban/Year-Season", + "316940400000", "s", "X", + "L:/", + "Day-Helper-2024-12-17", + "job.json", + "333", + "444", + "555", + "666", + "777", + "888", + "999", + "X", "L:/Git/Linux-Ubuntu-Server/etc/nginx/include", "Day-Helper-2024-09-16", "*.conf", diff --git a/.vscode/settings.json b/.vscode/settings.json index 8289181..76b0107 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,7 @@ "Kanban", "kanbn", "Kofax", + "mesfs", "NpgSql", "NSFX", "OBJE", @@ -34,6 +35,7 @@ "Permyriad", "pged", "Phares", + "Renci", "Reparse", "Rijndael", "Serilog", diff --git a/ADO2024/PI2/Helper-2024-06-23.cs b/ADO2024/PI2/Helper-2024-06-23.cs index 9587d5e..65e5fce 100644 --- a/ADO2024/PI2/Helper-2024-06-23.cs +++ b/ADO2024/PI2/Helper-2024-06-23.cs @@ -1,31 +1,96 @@ using File_Folder_Helper.Helpers; +using File_Folder_Helper.Models; using Microsoft.Extensions.Logging; using System.Collections.ObjectModel; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; namespace File_Folder_Helper.ADO2024.PI2; internal static partial class Helper20240623 { - private record SubTaskLine(string Text, bool Done, long? Ticks, int? Line); - private record Record(int? CodeInsidersLine, string File, string[] Lines, int? StopLine, int? SubTasksLine); + [GeneratedRegex("([A-Z]+(.))")] + private static partial Regex UpperCase(); - private static List GetRecords(string sourceDirectory, string searchPattern, string codeInsiders, string subTasks, string directoryFilter) + [GeneratedRegex("[\\s!?.,@:;|\\\\/\"'`£$%\\^&*{}[\\]()<>~#+\\-=_¬]+")] + private static partial Regex InvalidCharacter(); + + private record H1AndParamCase(string H1, string ParamCase); + private record SubTaskLine(string Text, bool Done, long? Ticks, int? Line); + private record Record(int? CodeInsidersLine, FileInfo FileInfo, LineNumber LineNumber, int? StopLine, int? SubTasksLine); + + private record Input(long? AfterEpochTotalMilliseconds, + string CodeInsiders, + string? DestinationDirectory, + string DirectoryFilter, + string Done, + string IndexFile, + string SearchPattern, + string SubTasks, + string SourceDirectory, + ReadOnlyCollection Tasks); + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(Input))] + private partial class InputSourceGenerationContext : JsonSerializerContext + { + } + + private static Record GetRecord(Input input, FileInfo fileInfo) + { + Record result; + int? stopLine = null; + int? subTasksLine = null; + int? codeInsidersLine = null; + LineNumber lineNumber = HelperMarkdown.GetLineNumbers(fileInfo); + for (int i = 0; i < lineNumber.Lines.Count; i++) + { + if (lineNumber.Lines[i].StartsWith(input.CodeInsiders) && lineNumber.Lines[i][^1] == '"') + { + if (lineNumber.Lines.Count > i + 1 && lineNumber.Lines[i + 1] == "```") + codeInsidersLine = i; + } + if (lineNumber.Lines[i] != input.SubTasks) + continue; + subTasksLine = i; + if (codeInsidersLine is null) + break; + if (lineNumber.Lines.Count > i) + { + for (int j = i + 1; j < lineNumber.Lines.Count; j++) + { + if (lineNumber.Lines[j].Length > 0 && lineNumber.Lines[j][0] == '#') + { + stopLine = j; + break; + } + } + } + stopLine ??= lineNumber.Lines.Count; + break; + } + result = new(codeInsidersLine, fileInfo, lineNumber, stopLine, subTasksLine); + return result; + } + + private static List GetRecords(Input input) { List results = []; - int? stopLine; - string[] lines; - int? subTasksLine; - int? codeInsidersLine; - ReadOnlyCollection directoryNames = HelperDirectory.GetDirectoryNames(sourceDirectory); - if (!directoryNames.Any(l => l.StartsWith(directoryFilter, StringComparison.CurrentCultureIgnoreCase))) + Record record; + FileInfo fileInfo; + string sourceDirectory = input.SourceDirectory; + ReadOnlyCollection directoryNames = HelperDirectory.GetDirectoryNames(input.SourceDirectory); + if (!directoryNames.Any(l => l.StartsWith(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase))) { string directoryName; - string[] checkDirectories = Directory.GetDirectories(sourceDirectory, "*", SearchOption.TopDirectoryOnly); + string[] checkDirectories = Directory.GetDirectories(input.SourceDirectory, "*", SearchOption.TopDirectoryOnly); foreach (string checkDirectory in checkDirectories) { directoryName = Path.GetFileName(checkDirectory); - if (directoryName.StartsWith(directoryFilter, StringComparison.CurrentCultureIgnoreCase)) + if (directoryName.StartsWith(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase)) { sourceDirectory = checkDirectory; break; @@ -33,204 +98,506 @@ internal static partial class Helper20240623 } } string[] subDirectories = Directory.GetDirectories(sourceDirectory, "*", SearchOption.TopDirectoryOnly); - List files = Directory.GetFiles(sourceDirectory, searchPattern, SearchOption.TopDirectoryOnly).ToList(); + List files = Directory.GetFiles(sourceDirectory, input.SearchPattern, SearchOption.TopDirectoryOnly).ToList(); foreach (string subDirectory in subDirectories) - files.AddRange(Directory.GetFiles(subDirectory, searchPattern, SearchOption.TopDirectoryOnly)); + files.AddRange(Directory.GetFiles(subDirectory, input.SearchPattern, SearchOption.TopDirectoryOnly)); foreach (string file in files) { - stopLine = null; - subTasksLine = null; - codeInsidersLine = null; - lines = File.ReadAllLines(file); - for (int i = 0; i < lines.Length; i++) - { - if (lines[i].StartsWith(codeInsiders) && lines[i][^1] == '"') - { - if (lines.Length > i + 1 && lines[i + 1] == "```") - codeInsidersLine = i; - } - if (lines[i] != subTasks) - continue; - subTasksLine = i; - if (codeInsidersLine is null) - break; - if (lines.Length > i) - { - for (int j = i + 1; j < lines.Length; j++) - { - if (lines[j].Length > 0 && lines[j][0] == '#') - { - stopLine = j; - break; - } - } - } - stopLine ??= lines.Length; - break; - } - results.Add(new(codeInsidersLine, file, lines, stopLine, subTasksLine)); + fileInfo = new(file); + record = GetRecord(input, fileInfo); + results.Add(record); } return results; } - private static ReadOnlyCollection GetSubTasks(string subTasks, string[] tasks, bool? foundDone, string fallbackLine, FileInfo fileInfo) + private static string GetParamCase(string value) + { + string result; + StringBuilder stringBuilder = new(value); + Match[] matches = UpperCase().Matches(value).ToArray(); + for (int i = matches.Length - 1; i > -1; i--) + _ = stringBuilder.Insert(matches[i].Index, '-'); + string[] segments = InvalidCharacter().Split(stringBuilder.ToString().ToLower()); + result = string.Join('-', segments).Trim('-'); + return result; + } + + private static ReadOnlyCollection GetSubTaskLines(Input input, bool? foundDone, string fallbackLine, Record record) { List results = []; + char done; string line; + string text; bool doneValue; - string? h1 = null; SubTaskLine subTaskLine; bool foundSubTasks = false; - int tasksZeroLength = tasks[0].Length; - string[] lines = File.ReadAllLines(fileInfo.FullName); - for (int i = 0; i < lines.Length; i++) + int tasksZeroLength = input.Tasks[0].Length; + long ticks = record.FileInfo.LastWriteTime.Ticks; + for (int i = 0; i < record.LineNumber.Lines.Count; i++) { - line = lines[i]; - if (line.StartsWith("# ")) - h1 = line[2..]; - if (!foundSubTasks && line == subTasks) + line = record.LineNumber.Lines[i]; + if (!foundSubTasks && line == input.SubTasks) foundSubTasks = true; if (!foundSubTasks) continue; - if (line.Length <= tasksZeroLength || !line.StartsWith(tasks[0]) || line[tasksZeroLength] is not ' ' and not 'x' || line[tasksZeroLength + 1] != ']') + if (line.Length <= tasksZeroLength || !line.StartsWith(input.Tasks[0]) || line[tasksZeroLength] is not ' ' and not 'x' || line[tasksZeroLength + 1] != ']') continue; doneValue = foundDone is not null && foundDone.Value; - subTaskLine = new($" {line}", doneValue, fileInfo.LastWriteTime.Ticks, i); + subTaskLine = new($" {line}", doneValue, ticks, i); results.Add(subTaskLine); } doneValue = foundDone is not null && foundDone.Value; - if (h1 is null) - subTaskLine = new(fallbackLine, doneValue, fileInfo.LastWriteTime.Ticks, Line: null); + if (record.LineNumber.H1 is null) + subTaskLine = new(fallbackLine, doneValue, ticks, Line: null); else { - fallbackLine = foundDone is null || !foundDone.Value ? $"- [ ] {fileInfo.LastWriteTime.Ticks} ~~~ {h1}" : $"- [x] {fileInfo.LastWriteTime.Ticks} ~~~ {h1}"; - subTaskLine = new(fallbackLine, doneValue, fileInfo.LastWriteTime.Ticks, Line: 0); + done = foundDone is null || !foundDone.Value ? ' ' : 'x'; + string codeInsidersLine = record.CodeInsidersLine is null ? string.Empty : $" ~~{record.LineNumber.Lines[record.CodeInsidersLine.Value]}~~"; + text = $"- [{done}] {ticks} {record.LineNumber.Lines[record.LineNumber.H1.Value]}{codeInsidersLine}"; + subTaskLine = new(text, doneValue, ticks, Line: 0); } results.Add(subTaskLine); return new(results); } - internal static void UpdateSubTasksInMarkdownFiles(ILogger logger, List args) + private static string GetSeasonName(int dayOfYear) { - int lineCheck; + string result = dayOfYear switch + { + < 78 => "0.Winter", + < 124 => "1.Spring", + < 171 => "2.Spring", + < 217 => "3.Summer", + < 264 => "4.Summer", + < 309 => "5.Fall", + < 354 => "6.Fall", + _ => "7.Winter" + }; + return result; + } + + private static string[] GetIndexLines(string h1, ReadOnlyCollection h1ParamCaseCollection) => + [ + "---", + "startedColumns:", + " - 'In Progress'", + "completedColumns:", + " - Done", + "---", + string.Empty, + $"# {h1}", + string.Empty, + "## Backlog", + string.Empty, + string.Join(Environment.NewLine, h1ParamCaseCollection.Select(l => $"- [{l.ParamCase}](tasks/{l.ParamCase}.md)")), + string.Empty, + "## Todo", + string.Empty, + "## In Progress", + string.Empty, + "## Done", + string.Empty + ]; + + private static string[] GetCascadingStyleSheetsLines() => + [ + ".kanbn-column-done .kanbn-column-task-list {", + " border-color: #198038;", + "}", + string.Empty, + ".kanbn-task-data-created {", + " display: none;", + "}", + string.Empty, + ".kanbn-task-data-workload {", + " display: none;", + "}" + ]; + + private static string GetSettingsLines() => + /*lang=json,strict*/ """ + { + "[markdown]": { + "editor.wordWrap": "off" + }, + "cSpell.words": [ + "kanbn" + ] + } + """; + + private static string GetTasksLines(string directory) => + /*lang=json,strict*/ """ + { + "version": "2.0.0", + "tasks": [ + { + "label": "File-Folder-Helper AOT s X Day-Helper-2024-06-23", + "type": "shell", + "command": "L:/DevOps/Mesa_FI/File-Folder-Helper/bin/Release/net8.0/win-x64/publish/File-Folder-Helper.exe", + "args": [ + "s", + "X", + "{}", + "Day-Helper-2024-06-23", + "*.md", + "##_Sub-tasks", + "code-insiders", + "index.md", + "-_[,](", + "##_Done", + ".kan", + "D:/5-Other-Small/Kanban/Year-Season", + "316940400000" + ], + "problemMatcher": [] + } + ] + } + """.Replace("{}", directory.Replace('\\', '/')); + + private static void FileWriteAllText(string path, string contents) + { + // string checkJson = Regex.Replace(File.ReadAllText(path), @"\s+", " ", RegexOptions.Multiline); + // if (Regex.Replace(singletonJson, @"\s+", " ", RegexOptions.Multiline) != checkJson) + // File.WriteAllText(path, singletonJson); + string old = !File.Exists(path) ? string.Empty : File.ReadAllText(path); + if (old != contents) + File.WriteAllText(path, contents); + } + + private static void FileWriteAllText(string path, string[] contents) => + FileWriteAllText(path, string.Join(Environment.NewLine, contents)); + + private static ReadOnlyCollection GetH1ParamCaseCollection(Input input, ReadOnlyCollection lines) + { + List results = []; + string h1; + string line; + string paramCase; + bool foundSubTasks = false; + H1AndParamCase h1AndParamCase; + int tasksZeroLength = input.Tasks[0].Length; + for (int i = 0; i < lines.Count; i++) + { + line = lines[i]; + if (!foundSubTasks && line == input.SubTasks) + foundSubTasks = true; + if (!foundSubTasks) + continue; + if (line.Length <= tasksZeroLength || !line.StartsWith(input.Tasks[0]) || line[tasksZeroLength] is not ' ' and not 'x' || line[tasksZeroLength + 1] != ']') + continue; + h1 = line[(tasksZeroLength + 3)..]; + if (string.IsNullOrEmpty(h1)) + continue; + paramCase = GetParamCase(h1); + h1AndParamCase = new(h1, paramCase); + results.Add(h1AndParamCase); + } + return results.AsReadOnly(); + } + + private static void CreateFiles(string directory, ReadOnlyCollection h1ParamCaseCollection) + { + foreach (H1AndParamCase h1ParamCase in h1ParamCaseCollection) + FileWriteAllText(Path.Combine(directory, $"{h1ParamCase.ParamCase}.md"), $"# {h1ParamCase.H1}"); + } + + private static string WriteAndGetIndexFile(string destinationDirectory, string h1, long verified, ReadOnlyCollection h1ParamCaseCollection) + { + string result; + string[] indexLines = GetIndexLines(h1, h1ParamCaseCollection); + DateTime utcEpochDateTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + DateTime dateTime = utcEpochDateTime.AddMilliseconds(verified).ToLocalTime(); + string seasonName = GetSeasonName(dateTime.DayOfYear); + string verifiedDirectory = Path.Combine(destinationDirectory, $"{dateTime.Year}", $"{dateTime.Year}-{seasonName}", verified.ToString()); + string kanbanDirectory = Path.Combine(verifiedDirectory, ".kanbn"); + string tasksKanbanDirectory = Path.Combine(kanbanDirectory, "tasks"); + if (!Directory.Exists(tasksKanbanDirectory)) + _ = Directory.CreateDirectory(tasksKanbanDirectory); + string verifiedVisualStudioCodeDirectory = Path.Combine(verifiedDirectory, ".vscode"); + if (!Directory.Exists(verifiedVisualStudioCodeDirectory)) + _ = Directory.CreateDirectory(verifiedVisualStudioCodeDirectory); + result = Path.Combine(kanbanDirectory, "index.md"); + CreateFiles(tasksKanbanDirectory, h1ParamCaseCollection); + FileWriteAllText(result, indexLines); + FileWriteAllText(Path.Combine(kanbanDirectory, "board.css"), GetCascadingStyleSheetsLines()); + FileWriteAllText(Path.Combine(verifiedVisualStudioCodeDirectory, "settings.json"), GetSettingsLines()); + FileWriteAllText(Path.Combine(verifiedVisualStudioCodeDirectory, "tasks.json"), GetTasksLines(verifiedDirectory)); + return result; + } + + private static ReadOnlyCollection GetSubTaskLines(Input input, FileInfo fileInfo, LineNumber lineNumber) + { + List results = []; + char done; + FileInfo f; + Record record; bool doneValue; - bool? foundDone; - FileInfo fileInfo; - string[] newLines; string[] segments; - List lines; string fallbackLine; - string[] indexLines; - string checkDirectory; - string done = args[7]; - List indexFiles; - SubTaskLine subTaskLine; - string subTasks = args[3]; - List oldLines = []; + bool? foundDone = null; + ReadOnlyCollection subTaskLines; + for (int i = 0; i < lineNumber.Lines.Count; i++) + { + if (lineNumber.Lines[i] == input.Done) + foundDone = true; + segments = lineNumber.Lines[i].Split(input.Tasks[1]); + doneValue = foundDone is not null && foundDone.Value; + if (segments.Length > 2 || !segments[0].StartsWith(input.Tasks[0])) + continue; + done = foundDone is null || !foundDone.Value ? ' ' : 'x'; + fallbackLine = $"- [{done}] {segments[0][input.Tasks[0].Length..]} ~~FallbackLine~~"; + if (string.IsNullOrEmpty(fileInfo.DirectoryName)) + continue; + f = new(Path.GetFullPath(Path.Combine(fileInfo.DirectoryName, segments[1][..^1]))); + if (!f.Exists) + { + results.Add(new(fallbackLine, doneValue, Ticks: null, Line: null)); + continue; + } + record = GetRecord(input, f); + if (lineNumber.H1 is not null && record.LineNumber.H1 is not null) + { + string a = lineNumber.Lines[lineNumber.H1.Value]; + string b = record.LineNumber.Lines[record.LineNumber.H1.Value]; + if (b != a) + { + if (b != a) + { + } + } + } + subTaskLines = GetSubTaskLines(input, doneValue, fallbackLine, record); + for (int j = subTaskLines.Count - 1; j >= 0; j--) + results.Add(subTaskLines[j]); + } + return results.AsReadOnly(); + } + + private static Input GetInput(List args) + { string indexFile = args[5]; string searchPattern = args[2]; string directoryFilter = args[8]; - string[] tasks = args[6].Split(','); string codeInsiders = $"{args[4]} \""; - List allSubTaskLines = []; - ReadOnlyCollection subTaskLines; + string done = args[7].Replace('_', ' '); + string subTasks = args[3].Replace('_', ' '); string sourceDirectory = Path.GetFullPath(args[0]); - List records = GetRecords(sourceDirectory, searchPattern, codeInsiders, subTasks, directoryFilter); - foreach (Record record in from l in records orderby l.SubTasksLine is null, l.CodeInsidersLine is null select l) + string? destinationDirectory = args.Count < 8 ? null : Path.GetFullPath(args[9]); + long? afterEpochTotalMilliseconds = args.Count < 9 ? null : long.Parse(args[10]); + ReadOnlyCollection tasks = args[6].Split(',').Select(l => l.Replace('_', ' ')).ToArray().AsReadOnly(); + Input input = new(afterEpochTotalMilliseconds, codeInsiders, destinationDirectory, directoryFilter, done, indexFile, searchPattern, subTasks, sourceDirectory, tasks); + if (input.Tasks[0] != "- [" || input.Tasks[1] != "](") + throw new Exception(JsonSerializer.Serialize(input, InputSourceGenerationContext.Default.Input)); + return input; + } + + private static string? MaybeWriteAndGetIndexFile(Input input, Record record, string? checkDirectory) + { + string? result; + if (string.IsNullOrEmpty(checkDirectory) || input.AfterEpochTotalMilliseconds is null || string.IsNullOrEmpty(input.DestinationDirectory) || !checkDirectory.Contains(input.DestinationDirectory)) + result = null; + else { - if (record.SubTasksLine is null) - continue; - if (record.CodeInsidersLine is not null) - logger.LogInformation("<{file}> has [{subTasks}]", Path.GetFileNameWithoutExtension(record.File), subTasks); + if (record.LineNumber.H1 is null) + result = null; else { - logger.LogWarning("<{file}> has [{subTasks}] but doesn't have [{codeInsiders}]!", Path.GetFileNameWithoutExtension(record.File), subTasks, codeInsiders); - continue; - } - if (record.StopLine is null) - continue; - checkDirectory = record.Lines[record.CodeInsidersLine.Value][codeInsiders.Length..^1]; - if (!Directory.Exists(checkDirectory)) - { - logger.LogError("<{checkDirectory}> doesn't exist", Path.GetFileName(checkDirectory)); - continue; - } - indexFiles = Directory.GetFiles(checkDirectory, indexFile, SearchOption.AllDirectories).ToList(); - if (indexFiles.Count != 1) - { - for (int i = indexFiles.Count - 1; i > -1; i--) + string segment = Path.GetFileName(checkDirectory); + DateTime utcEpochDateTime = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + long utcEpochTotalMilliseconds = (long)Math.Floor(DateTime.UtcNow.Subtract(utcEpochDateTime).TotalMilliseconds); + if (!long.TryParse(segment, out long check) || check < input.AfterEpochTotalMilliseconds || check > utcEpochTotalMilliseconds) + result = null; + else { - if (!indexFiles[i].Contains(directoryFilter, StringComparison.CurrentCultureIgnoreCase)) - indexFiles.RemoveAt(i); - } - if (indexFiles.Count != 1) - { - logger.LogError("<{checkDirectory}> doesn't have a [{indexFile}]", Path.GetFileName(checkDirectory), indexFile); - continue; + ReadOnlyCollection h1ParamCaseCollection = GetH1ParamCaseCollection(input, record.LineNumber.Lines); + if (h1ParamCaseCollection.Count == 0) + result = null; + else + result = WriteAndGetIndexFile(input.DestinationDirectory, record.LineNumber.Lines[record.LineNumber.H1.Value], check, h1ParamCaseCollection); } } - foundDone = null; - oldLines.Clear(); - allSubTaskLines.Clear(); - indexLines = File.ReadAllLines(indexFiles[0]); - checkDirectory = Path.GetDirectoryName(indexFiles[0]) ?? throw new Exception(); - for (int i = 0; i < indexLines.Length; i++) + } + return result; + } + + private static bool FileWrite(Record record, List newLines, double percent) + { + bool result = false; + if (record.StopLine is not null && record.SubTasksLine is not null) + { + string contents; + string progressLine; + List resultLines; + resultLines = record.LineNumber.Lines.ToList(); + if (record.LineNumber.FrontMatterYamlEnd is not null) { - if (indexLines[i] == done) - foundDone = true; - segments = indexLines[i].Split(tasks[1]); - doneValue = foundDone is not null && foundDone.Value; - if (segments.Length > 2 || !segments[0].StartsWith(tasks[0])) + progressLine = $"progress: {percent}"; + if (record.LineNumber.Progress is not null) + resultLines[record.LineNumber.Progress.Value] = progressLine; + else + { + resultLines.Insert(record.LineNumber.FrontMatterYamlEnd.Value, progressLine); + contents = string.Join(Environment.NewLine, resultLines); + FileWriteAllText(record.FileInfo.FullName, contents); + result = true; + } + if (!result && record.LineNumber.Completed is null && percent > 99.9) + { + resultLines.Insert(record.LineNumber.FrontMatterYamlEnd.Value, $"completed: {DateTime.Now:yyyy-MM-dd}"); + contents = string.Join(Environment.NewLine, resultLines); + FileWriteAllText(record.FileInfo.FullName, contents); + result = true; + } + if (!result && record.LineNumber.Completed is not null && percent < 99.9) + { + resultLines.RemoveAt(record.LineNumber.Completed.Value); + contents = string.Join(Environment.NewLine, resultLines); + FileWriteAllText(record.FileInfo.FullName, contents); + result = true; + } + } + if (!result) + { + for (int i = record.StopLine.Value - 1; i > record.SubTasksLine.Value + 1; i--) + resultLines.RemoveAt(i); + if (record.StopLine.Value == record.LineNumber.Lines.Count && resultLines[^1].Length == 0) + resultLines.RemoveAt(resultLines.Count - 1); + for (int i = 0; i < newLines.Count; i++) + resultLines.Insert(record.SubTasksLine.Value + 1 + i, newLines[i]); + resultLines.Insert(record.SubTasksLine.Value + 1, string.Empty); + contents = string.Join(Environment.NewLine, resultLines); + FileWriteAllText(record.FileInfo.FullName, contents); + } + } + return result; + } + + private static FileInfo GetIndexFileInfo(Input input, Record record, string codeInsidersLine) + { + FileInfo result; + string? indexFile; + List results; + string? checkDirectory = codeInsidersLine[input.CodeInsiders.Length..^1]; + if (!Directory.Exists(checkDirectory)) + { + if (!string.IsNullOrEmpty(input.DestinationDirectory) && checkDirectory.Contains(input.DestinationDirectory)) + _ = Directory.CreateDirectory(checkDirectory); + } + if (!Directory.Exists(checkDirectory)) + results = []; + else + { + results = Directory.GetFiles(checkDirectory, input.IndexFile, SearchOption.AllDirectories).ToList(); + if (results.Count != 1) + { + for (int i = results.Count - 1; i > -1; i--) + { + if (!results[i].Contains(input.DirectoryFilter, StringComparison.CurrentCultureIgnoreCase)) + results.RemoveAt(i); + } + } + if (results.Count == 0) + { + indexFile = MaybeWriteAndGetIndexFile(input, record, checkDirectory); + if (!string.IsNullOrEmpty(indexFile)) + results.Add(indexFile); + } + } + result = results.Count == 0 ? new(Path.Combine(checkDirectory, input.IndexFile)) : new(results[0]); + return result; + } + + internal static void UpdateSubTasksInMarkdownFiles(ILogger logger, List args) + { + bool reload; + int allCount; + int lineCheck; + double percent; + double doneCount; + FileInfo fileInfo; + List records; + LineNumber lineNumber; + List newLines; + bool reloadAny = false; + string? checkDirectory; + string codeInsidersLine; + List oldLines = []; + Input input = GetInput(args); + string fileNameWithoutExtension; + ReadOnlyCollection subTaskLines; + for (int z = 0; z < 9; z++) + { + records = GetRecords(input); + foreach (Record record in from l in records orderby l.SubTasksLine is null, l.CodeInsidersLine is null select l) + { + if (record.SubTasksLine is null) continue; - fallbackLine = foundDone is null || !foundDone.Value ? $"- [ ] {segments[0][tasks[0].Length..]}" : $"- [x] {segments[0][tasks[0].Length..]}"; - fileInfo = new(Path.GetFullPath(Path.Combine(checkDirectory, segments[1][..^1]))); + fileNameWithoutExtension = Path.GetFileNameWithoutExtension(record.FileInfo.FullName); + if (record.CodeInsidersLine is not null) + logger.LogInformation("<{file}> has [{subTasks}]", fileNameWithoutExtension, input.SubTasks); + else + { + logger.LogWarning("<{file}> has [{subTasks}] but doesn't have [{codeInsiders}]!", fileNameWithoutExtension, input.SubTasks, input.CodeInsiders); + continue; + } + if (record.StopLine is null) + continue; + codeInsidersLine = record.LineNumber.Lines[record.CodeInsidersLine.Value]; + fileInfo = GetIndexFileInfo(input, record, codeInsidersLine); if (!fileInfo.Exists) { - allSubTaskLines.Add(new(fallbackLine, doneValue, Ticks: null, Line: null)); + checkDirectory = codeInsidersLine[input.CodeInsiders.Length..^1]; + logger.LogError("<{checkDirectory}> doesn't have a [{indexFile}]", Path.GetFileName(checkDirectory), input.IndexFile); continue; } - subTaskLines = GetSubTasks(subTasks, tasks, doneValue, fallbackLine, fileInfo); - if (subTaskLines.Count > 0) + oldLines.Clear(); + checkDirectory = fileInfo.DirectoryName; + lineNumber = HelperMarkdown.GetLineNumbers(fileInfo); + subTaskLines = GetSubTaskLines(input, fileInfo, lineNumber); + if (subTaskLines.Count == 0) + continue; + lineCheck = 0; + for (int i = record.SubTasksLine.Value + 1; i < record.StopLine.Value - 1; i++) + oldLines.Add(record.LineNumber.Lines[i]); + if (subTaskLines.Any(l => l.Ticks is null)) + newLines = (from l in subTaskLines select l.Text).ToList(); + else + newLines = (from l in subTaskLines orderby l.Done descending, l.Ticks, l.Line select l.Text).ToList(); + if (subTaskLines.Count == 0) + percent = 0; + else { - subTaskLine = new($"", false, null, null); - allSubTaskLines.Add(subTaskLine); - for (int j = subTaskLines.Count - 1; j >= 0; j--) - allSubTaskLines.Add(subTaskLines[j]); + allCount = (from l in subTaskLines where l.Line is not null && l.Line.Value == 0 select 1).Count(); + doneCount = (from l in subTaskLines where l.Line is not null && l.Line.Value == 0 && l.Done select 1).Count(); + // done = allCount != doneCount ? ' ' : 'x'; + percent = allCount == 0 ? 0 : Math.Round(doneCount / allCount, 3); + // newLines.Insert(0, $"- [{done}] Sub-tasks {doneCount} of {allCount} [{percent * 100}%]"); } - } - if (allSubTaskLines.Count == 0) - continue; - lineCheck = 0; - for (int i = record.SubTasksLine.Value + 1; i < record.StopLine.Value - 1; i++) - oldLines.Add(record.Lines[i]); - if (allSubTaskLines.Any(l => l.Ticks is null)) - newLines = (from l in allSubTaskLines select l.Text).ToArray(); - else - newLines = (from l in allSubTaskLines orderby l.Done descending, l.Ticks, l.Line select l.Text).ToArray(); - if (newLines.Length == oldLines.Count) - { - for (int i = 0; i < newLines.Length; i++) + if (newLines.Count == oldLines.Count) { - if (newLines[i] != record.Lines[record.SubTasksLine.Value + 1 + i]) + for (int i = 0; i < newLines.Count; i++) + { + if (newLines[i] != record.LineNumber.Lines[record.SubTasksLine.Value + 1 + i]) + continue; + lineCheck++; + } + if (lineCheck == newLines.Count) continue; - lineCheck++; } - if (lineCheck == newLines.Length) + if (string.IsNullOrEmpty(checkDirectory)) continue; + checkDirectory = Path.Combine(checkDirectory, DateTime.Now.Ticks.ToString()); + _ = Directory.CreateDirectory(checkDirectory); + Thread.Sleep(500); + Directory.Delete(checkDirectory); + reload = FileWrite(record, newLines, percent); + if (!reloadAny && reload) + reloadAny = true; } - checkDirectory = Path.Combine(checkDirectory, DateTime.Now.Ticks.ToString()); - _ = Directory.CreateDirectory(checkDirectory); - Thread.Sleep(500); - Directory.Delete(checkDirectory); - lines = record.Lines.ToList(); - for (int i = record.StopLine.Value - 1; i > record.SubTasksLine.Value + 1; i--) - lines.RemoveAt(i); - if (record.StopLine.Value == record.Lines.Length && lines[^1].Length == 0) - lines.RemoveAt(lines.Count - 1); - for (int i = 0; i < newLines.Length; i++) - lines.Insert(record.SubTasksLine.Value + 1 + i, newLines[i]); - lines.Insert(record.SubTasksLine.Value + 1, string.Empty); - File.WriteAllLines(record.File, lines); + if (!reloadAny) + break; } } diff --git a/ADO2024/PI2/Helper-2024-07-24.cs b/ADO2024/PI2/Helper-2024-07-24.cs index 6580215..e69de29 100644 --- a/ADO2024/PI2/Helper-2024-07-24.cs +++ b/ADO2024/PI2/Helper-2024-07-24.cs @@ -1,206 +0,0 @@ -using File_Folder_Helper.Models; -using Microsoft.Extensions.Logging; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Globalization; -using System.Text.Json; - -namespace File_Folder_Helper.ADO2024.PI2; - -internal static partial class Helper20240724 -{ - - private record FileConnectorConfigurationSystem(string AlternateTargetFolder, - string FileAgeThreshold, - string[] SourceFileFilters, - string TargetFileLocation); - -#pragma warning disable IDE0028, IDE0056, IDE0300, IDE0240, IDE0241 - - private static readonly HttpClient _HttpClient = new(); - private static readonly string _StaticFileServer = "localhost:5054"; - private static readonly FileConnectorConfigurationSystem _FileConnectorConfiguration = new( - "D:/Tmp/Phares/AlternateTargetFolder", - "000:20:00:01", - [".txt"], - "D:/Tmp/Phares/TargetFileLocation"); - - private static string[] GetValidDays(DateTime fileAgeThresholdDateTime) - { - DateTime dateTime = DateTime.Now; - return new string[] { dateTime.ToString("yyyy-MM-dd"), fileAgeThresholdDateTime.ToString("yyyy-MM-dd") }.Distinct().ToArray(); - } - - private static string[] GetValidWeeks(DateTime fileAgeThresholdDateTime) - { - DateTime dateTime = DateTime.Now; - Calendar calendar = new CultureInfo("en-US").Calendar; - string weekOfYear = $"{dateTime:yyyy}_Week_{calendar.GetWeekOfYear(dateTime, CalendarWeekRule.FirstDay, DayOfWeek.Sunday):00}"; - string lastWeekOfYear = $"{fileAgeThresholdDateTime:yyyy}_Week_{calendar.GetWeekOfYear(fileAgeThresholdDateTime, CalendarWeekRule.FirstDay, DayOfWeek.Sunday):00}"; - return new string[] { weekOfYear, lastWeekOfYear }.Distinct().ToArray(); - } - - private static ReadOnlyCollection GetDayNginxFileSystemCollection(DateTime fileAgeThresholdDateTime, string week, string day, string dayUrl, NginxFileSystem[] dayNginxFileSystemCollection) - { - List results = new(); - DateTime dateTime; - string nginxFormat = "ddd, dd MMM yyyy HH:mm:ss zzz"; - foreach (NginxFileSystem dayNginxFileSystem in dayNginxFileSystemCollection) - { - if (!DateTime.TryParseExact(dayNginxFileSystem.MTime.Replace("GMT", "+00:00"), nginxFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTime)) - continue; - if (dateTime < fileAgeThresholdDateTime) - continue; - results.Add(new( - Path.GetFullPath(Path.Combine(_FileConnectorConfiguration.TargetFileLocation, week, day, dayNginxFileSystem.Name)), - string.Concat(dayUrl, '/', dayNginxFileSystem.Name), - dateTime.ToString(), - dayNginxFileSystem.Size)); - } - return results.AsReadOnly(); - } - - private static DateTime GetFileAgeThresholdDateTime(string fileAgeThreshold) - { - DateTime result = DateTime.Now; - string[] segments = fileAgeThreshold.Split(':'); - for (int i = 0; i < segments.Length; i++) - { - result = i switch - { - 0 => result.AddDays(double.Parse(segments[i]) * -1), - 1 => result.AddHours(double.Parse(segments[i]) * -1), - 2 => result.AddMinutes(double.Parse(segments[i]) * -1), - 3 => result.AddSeconds(double.Parse(segments[i]) * -1), - _ => throw new Exception(), - }; - } - return result; - } - - private static ReadOnlyCollection GetDayNginxFileSystemCollection(DateTime fileAgeThresholdDateTime) - { -#nullable enable - List results = new(); - string dayUrl; - string dayJson; - string weekJson; - string checkWeek; - Task task; - NginxFileSystem[]? dayNginxFileSystemCollection; - NginxFileSystem[]? weekNginxFileSystemCollection; - string[] days = GetValidDays(fileAgeThresholdDateTime); - string[] weeks = GetValidWeeks(fileAgeThresholdDateTime); - foreach (string week in weeks) - { - checkWeek = string.Concat("http://", _StaticFileServer, '/', week); - task = _HttpClient.GetAsync(checkWeek); - task.Wait(); - if (!task.Result.IsSuccessStatusCode) - continue; - weekJson = _HttpClient.GetStringAsync(checkWeek).Result; - weekNginxFileSystemCollection = JsonSerializer.Deserialize(weekJson, NginxFileSystemCollectionSourceGenerationContext.Default.NginxFileSystemArray); - if (weekNginxFileSystemCollection is null) - continue; - foreach (NginxFileSystem weekNginxFileSystem in weekNginxFileSystemCollection) - { - if (!(from l in days where weekNginxFileSystem.Name == l select false).Any()) - continue; - dayUrl = string.Concat(checkWeek, '/', weekNginxFileSystem.Name); - dayJson = _HttpClient.GetStringAsync(dayUrl).Result; - dayNginxFileSystemCollection = JsonSerializer.Deserialize(dayJson, NginxFileSystemCollectionSourceGenerationContext.Default.NginxFileSystemArray); - if (dayNginxFileSystemCollection is null) - continue; - results.AddRange(GetDayNginxFileSystemCollection(fileAgeThresholdDateTime, week, weekNginxFileSystem.Name, dayUrl, dayNginxFileSystemCollection)); - } - } - return results.AsReadOnly(); -#nullable disable - } - - private static ReadOnlyCollection> GetPossible() - { - List> results = new(); - DateTime dateTime; - FileInfo targetFileInfo; - FileInfo alternateFileInfo; - DateTime fileAgeThresholdDateTime = GetFileAgeThresholdDateTime(_FileConnectorConfiguration.FileAgeThreshold); - ReadOnlyCollection dayNginxFileSystemCollection = GetDayNginxFileSystemCollection(fileAgeThresholdDateTime); - foreach (NginxFileSystem nginxFileSystem in dayNginxFileSystemCollection) - { - targetFileInfo = new FileInfo(nginxFileSystem.Name); - if (targetFileInfo.Directory is null) - continue; - if (!Directory.Exists(targetFileInfo.Directory.FullName)) - _ = Directory.CreateDirectory(targetFileInfo.Directory.FullName); - if (!DateTime.TryParse(nginxFileSystem.MTime, out dateTime)) - continue; - if (targetFileInfo.Exists && targetFileInfo.LastWriteTime == dateTime) - continue; - alternateFileInfo = new(Path.Combine(_FileConnectorConfiguration.AlternateTargetFolder, nginxFileSystem.Name)); - results.Add(new(dateTime, targetFileInfo, alternateFileInfo, nginxFileSystem.Type)); - } - return (from l in results orderby l.Item1 select l).ToList().AsReadOnly(); - } - - private static void Test() - { -#nullable enable - if (_HttpClient is null) - throw new Exception(); - if (string.IsNullOrEmpty(_StaticFileServer)) - throw new Exception(); - if (string.IsNullOrEmpty(_StaticFileServer)) - { - ReadOnlyCollection> possibleDownload = GetPossible(); - if (possibleDownload.Count > 0) - { - string targetFileName = possibleDownload[0].Item4; - FileInfo targetFileInfo = possibleDownload[0].Item2; - FileInfo alternateFileInfo = possibleDownload[0].Item3; - DateTime matchNginxFileSystemDateTime = possibleDownload[0].Item1; - // if (alternateFileInfo.Exists) - // File.Delete(alternateFileInfo.FullName); - if (targetFileInfo.Exists) - File.Delete(targetFileInfo.FullName); - string targetJson = _HttpClient.GetStringAsync(targetFileName).Result; - File.WriteAllText(targetFileInfo.FullName, targetJson); - targetFileInfo.LastWriteTime = matchNginxFileSystemDateTime; - // File.Copy(targetFileInfo.FullName, alternateFileInfo.FullName); - File.AppendAllText(alternateFileInfo.FullName, targetJson); - } - } -#nullable disable - } - - internal static void CopyDirectories(ILogger logger, List args) - { - Test(); - string[] files; - Process process; - string checkDirectory; - string filter = args[3]; - string replaceWith = args[4]; - string searchPattern = args[2]; - string sourceDirectory = Path.GetFullPath(args[0]); - string[] foundDirectories = Directory.GetDirectories(sourceDirectory, searchPattern, SearchOption.AllDirectories); - logger.LogInformation($"Found {foundDirectories.Length} directories"); - foreach (string foundDirectory in foundDirectories) - { - if (!foundDirectory.Contains(filter)) - continue; - logger.LogDebug(foundDirectory); - checkDirectory = foundDirectory.Replace(filter, replaceWith); - if (Directory.Exists(checkDirectory)) - { - files = Directory.GetFiles(checkDirectory, "*", SearchOption.AllDirectories); - if (files.Length > 0) - continue; - Directory.Delete(checkDirectory); - } - process = Process.Start("cmd.exe", $"/c xCopy \"{foundDirectory}\" \"{checkDirectory}\" /S /E /I /H /Y"); - process.WaitForExit(); - } - } - -} \ No newline at end of file diff --git a/ADO2024/PI4/Helper-2024-12-17.cs b/ADO2024/PI4/Helper-2024-12-17.cs new file mode 100644 index 0000000..73d18ce --- /dev/null +++ b/ADO2024/PI4/Helper-2024-12-17.cs @@ -0,0 +1,383 @@ +using DiscUtils.Iso9660; +using Microsoft.Extensions.Logging; +using Renci.SshNet; +using Renci.SshNet.Sftp; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO.Compression; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace File_Folder_Helper.ADO2024.PI4; + +internal static partial class Helper20241217 +{ + + private record Record(string Directory, Job? Job, string Path); + private record Job(string AlternatePath, string Directory, string Extension, File[] Files, int FilesCount, double FilesTotalLength, int Keep, Target[] Targets); + private record SecureShell(string Host, string Key, string Path, bool Required, string User); + private record ServerMessageBlock(string Path, bool Required); + private record Target(SecureShell? SecureShell, ServerMessageBlock? ServerMessageBlock); + private record File(long LastWriteTicks, long Length, string RelativePath); + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(Job))] + private partial class JobSourceGenerationContext : JsonSerializerContext + { + } + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(File[]))] + private partial class FilesSourceGenerationContext : JsonSerializerContext + { + } + + private static IEnumerable GetRecords(string directory, string searchPattern) + { + Job? job; + string json; + Record record; + string fileName; + string directoryName; + IEnumerable files = Directory.EnumerateFiles(directory, searchPattern, new EnumerationOptions { IgnoreInaccessible = true, RecurseSubdirectories = true }); + foreach (string file in files) + { + fileName = Path.GetFileName(file); + directoryName = Path.GetDirectoryName(file) ?? throw new Exception(); + if (!fileName.StartsWith('.')) + { + System.IO.File.Move(file, Path.Combine(directoryName, $".{fileName}")); + continue; + } + json = System.IO.File.ReadAllText(file); + job = JsonSerializer.Deserialize(json, JobSourceGenerationContext.Default.Job); + record = new(directoryName, job, file); + yield return record; + } + } + + private static ReadOnlyCollection GetFiles(string directory, string searchPattern, string[] ignoreFileNames) + { + List results = []; + File file; + string relativePath; + string[] files = Directory.GetFiles(directory, "*", SearchOption.AllDirectories); + FileInfo[] fileInfoCollection = files.Select(l => new FileInfo(l)).ToArray(); + foreach (FileInfo fileInfo in fileInfoCollection) + { + if (fileInfo.Name == searchPattern) + continue; + if (ignoreFileNames.Any(l => l == fileInfo.Name)) + continue; + if (!string.IsNullOrEmpty(fileInfo.LinkTarget)) + continue; + relativePath = Path.GetRelativePath(directory, fileInfo.FullName).Replace(';', '_'); + if (relativePath.StartsWith("..")) + relativePath = relativePath[3..]; + file = new(fileInfo.LastWriteTime.Ticks, fileInfo.Length, relativePath); + results.Add(file); + } + return results.AsReadOnly(); + } + + private static ReadOnlyCollection GetFiles(string searchPattern, string[] ignoreFileNames, Record record) => + GetFiles(record.Directory, searchPattern, ignoreFileNames); + + private static string? GetJsonIfNotEqual(string searchPattern, string[] ignoreFileNames, Record record, Job job, ReadOnlyCollection files) + { + string? result; + string? jsonNew; + string? jsonOld; + string fileName; + int ignoreCount = 0; + double filesTotalLengthNew = 0; + File[] filesArray = files.ToArray(); + double filesTotalLengthOld = job.FilesTotalLength; + foreach (File file in files) + filesTotalLengthNew += file.Length; + Job jobNew = new(job.AlternatePath, + record.Directory, + job.Extension, + filesArray, + files.Count, + filesTotalLengthNew, + job.Keep, + job.Targets); + result = JsonSerializer.Serialize(jobNew, JobSourceGenerationContext.Default.Job); + if (filesTotalLengthNew != filesTotalLengthOld) + { + filesTotalLengthOld = 0; + foreach (File file in job.Files) + { + fileName = Path.GetFileName(file.RelativePath); + if (fileName == searchPattern || ignoreFileNames.Any(l => l == fileName)) + { + ignoreCount += 1; + continue; + } + if (file.Length == 0) + { + ignoreCount += 1; + continue; + } + filesTotalLengthOld += file.Length; + } + } + if (filesTotalLengthNew != filesTotalLengthOld || files.Count != (job.Files.Length - ignoreCount)) + { + jsonNew = null; + jsonOld = null; + } + else + { + jsonNew = JsonSerializer.Serialize((from l in filesArray orderby l.RelativePath.Length, l.RelativePath select l).ToArray(), FilesSourceGenerationContext.Default.FileArray); + jsonOld = JsonSerializer.Serialize((from l in job.Files orderby l.RelativePath.Length, l.RelativePath where l.RelativePath != searchPattern select l).ToArray(), FilesSourceGenerationContext.Default.FileArray); + } + if (!string.IsNullOrEmpty(jsonNew) && !string.IsNullOrEmpty(jsonOld) && jsonNew == jsonOld) + result = null; + return result; + } + + private static void WriteISO(Record record, ReadOnlyCollection files, string path, string directoryName) + { + CDBuilder builder = new() { UseJoliet = true, VolumeIdentifier = directoryName.Length < 25 ? directoryName : directoryName[..25] }; + foreach (File file in files) + _ = builder.AddFile(file.RelativePath, Path.Combine(record.Directory, file.RelativePath)); + builder.Build(path); + } + + private static void WriteZIP(Record record, ReadOnlyCollection files, string path) + { + using ZipArchive zip = ZipFile.Open(path, ZipArchiveMode.Create); + string directoryEntry; + List directoryEntries = []; + foreach (File file in files) + { + directoryEntry = Path.GetDirectoryName(file.RelativePath) ?? throw new Exception(); + if (!directoryEntries.Contains(directoryEntry)) + continue; + directoryEntries.Add(directoryEntry); + _ = zip.CreateEntry(file.RelativePath); + } + foreach (File file in files) + _ = zip.CreateEntryFromFile(Path.Combine(record.Directory, file.RelativePath), file.RelativePath); + } + + private static void WriteExtension(Record record, Job job, ReadOnlyCollection files, string path) + { + string directoryName = Path.GetFileName(record.Directory); + if (job.Extension.Equals(".iso", StringComparison.OrdinalIgnoreCase)) + WriteISO(record, files, path, directoryName); + else if (job.Extension.Equals(".zip", StringComparison.OrdinalIgnoreCase)) + WriteZIP(record, files, path); + else + throw new NotImplementedException(); + } + + private static void PushTo(Job job, SecureShell secureShell, string path) + { + string remotePath = string.Concat(secureShell.Path, '/', Path.GetFileName(path)); + using SftpClient client = new(secureShell.Host, secureShell.User, new PrivateKeyFile(secureShell.Key)); + client.Connect(); + if (job.Files.Length == 0) + { + string directoryName = Path.GetDirectoryName(secureShell.Path) ?? throw new Exception(); + try + { client.CreateDirectory(Path.GetDirectoryName(directoryName) ?? throw new Exception()); } + catch (Exception) { } + try + { client.CreateDirectory(directoryName); } + catch (Exception) { } + try + { client.CreateDirectory(secureShell.Path); } + catch (Exception) { } + } + using FileStream fileStream = System.IO.File.OpenRead(path); + client.UploadFile(fileStream, remotePath); + } + + private static void PushTo(ServerMessageBlock serverMessageBlock, string path) + { + string remotePath = Path.Combine(serverMessageBlock.Path, Path.GetFileName(path)); + System.IO.File.Copy(path, remotePath); + } + + private static void PushTo(string directory, string path) + { + string remotePath = Path.Combine(directory, Path.GetFileName(path)); + System.IO.File.Copy(path, remotePath); + } + + private static ReadOnlyCollection PushTo(Job job, string path) + { + List results = []; + foreach (Target target in job.Targets) + { + if (target.SecureShell is not null) + { + try + { PushTo(job, target.SecureShell, path); } + catch (Exception ex) + { + if (target.SecureShell.Required) + results.Add(ex); + } + } + else if (target.ServerMessageBlock is not null) + { + try + { PushTo(target.ServerMessageBlock, path); } + catch (Exception ex) + { + if (target.ServerMessageBlock.Required) + results.Add(ex); + } + } + else + throw new NotImplementedException(); + } + return results.AsReadOnly(); + } + + private static void DeleteOld(Job job, SecureShell secureShell, string path) + { + List results = []; + using SftpClient client = new(secureShell.Host, secureShell.User, new PrivateKeyFile(secureShell.Key)); + client.Connect(); + foreach (ISftpFile file in client.ListDirectory(secureShell.Path)) + { + if (file.Name == path) + continue; + if (!file.Name.EndsWith(job.Extension, StringComparison.OrdinalIgnoreCase)) + continue; + results.Add(file.FullName); + } + for (int i = job.Keep - 1; i < results.Count; i++) + client.DeleteFile(results[i]); + } + + private static void DeleteOld(Job job, ServerMessageBlock serverMessageBlock, string path) + { + List results = []; + string[] files = Directory.GetFiles(serverMessageBlock.Path, $"*{job.Extension}", SearchOption.TopDirectoryOnly); + foreach (string file in files) + { + if (file == path) + continue; + results.Add(file); + } + for (int i = job.Keep - 1; i < results.Count; i++) + System.IO.File.Delete(results[i]); + } + + private static ReadOnlyCollection DeleteOld(Job job, string path) + { + List results = []; + foreach (Target target in job.Targets) + { + if (target.SecureShell is not null) + { + try + { DeleteOld(job, target.SecureShell, path); } + catch (Exception ex) + { + if (target.SecureShell.Required) + results.Add(ex); + } + } + else if (target.ServerMessageBlock is not null) + { + try + { DeleteOld(job, target.ServerMessageBlock, path); } + catch (Exception ex) + { + if (target.ServerMessageBlock.Required) + results.Add(ex); + } + } + else + throw new NotImplementedException(); + } + return results.AsReadOnly(); + } + + private static void Verify(string searchPattern, string[] ignoreFileNames) + { + List targets = [ + new(new SecureShell("free.file.sync.root", "C:/Users/phares/.ssh/id_ed25519", "\\home", true, "root"), null), + new(null, new ServerMessageBlock("\\\\mesfs.infineon.com\\EC_APC\\DEV", true)) + ]; + string directory = Path.Combine(Environment.CurrentDirectory, ".vscode", "helper"); + if (!Directory.Exists(directory)) + _ = Directory.CreateDirectory(directory); + ReadOnlyCollection files = GetFiles(directory, searchPattern, ignoreFileNames); + double filesTotalLength = 0; + foreach (File file in files) + filesTotalLength += file.Length; + Job job = new( + "C:/Users/phares", + directory, + "*.iso", + files.ToArray(), + files.Count, + filesTotalLength, + 3, + targets.ToArray()); + string json = JsonSerializer.Serialize(job, JobSourceGenerationContext.Default.Job); + System.IO.File.WriteAllText(Path.Combine(directory, "verify.json"), json); + } + + internal static void Backup(ILogger logger, List args) + { + string path; + string? json; + string directoryName; + ReadOnlyCollection files; + string searchPattern = args[2]; + ReadOnlyCollection exceptions; + string[] ignoreFileNames = args[3].Split('|'); + string sourceDirectory = Path.GetFullPath(args[0]); + logger.LogInformation("Searching <{sourceDirectory}> with search pattern {searchPattern}", args[0], searchPattern); + if (Debugger.IsAttached) + Verify(searchPattern, ignoreFileNames); + IEnumerable records = GetRecords(sourceDirectory, searchPattern); + foreach (Record record in records) + { + if (record.Job is null || record.Job.Targets.Length == 0 || string.IsNullOrEmpty(record.Job.Extension)) + continue; + logger.LogInformation("Searching <{directory}>", record.Directory); + files = GetFiles(searchPattern, ignoreFileNames, record); + json = GetJsonIfNotEqual(searchPattern, ignoreFileNames, record, record.Job, files); + if (string.IsNullOrEmpty(json)) + continue; + directoryName = Path.GetFileName(record.Directory); + path = Path.Combine(record.Directory, $"{directoryName}-{DateTime.Now:yyyy-MM-dd-HH-mm-ss-fff}{record.Job.Extension}"); + logger.LogInformation("Writing <{directory}> extension", record.Directory); + WriteExtension(record, record.Job, files, path); + logger.LogInformation("Pushing <{directory}> extension", record.Directory); + exceptions = PushTo(record.Job, path); + if (exceptions.Count != 0) + { + foreach (Exception exception in exceptions) + logger.LogError(exception, exception.Message); + PushTo(record.Job.AlternatePath, path); + } + System.IO.File.WriteAllText(record.Path, json); + System.IO.File.Delete(path); + logger.LogInformation("Deleting old <{directory}> extension", record.Directory); + exceptions = DeleteOld(record.Job, path); + if (exceptions.Count != 0) + { + foreach (Exception exception in exceptions) + logger.LogError(exception, exception.Message); + } + } + if (Debugger.IsAttached && records.Count() == 0) + { + files = GetFiles(sourceDirectory, searchPattern, ignoreFileNames); + json = JsonSerializer.Serialize(files.ToArray(), FilesSourceGenerationContext.Default.FileArray); + System.IO.File.WriteAllText(Path.Combine(Environment.CurrentDirectory, ".vscode", "helper", ".json"), json); + } + } + +} \ No newline at end of file diff --git a/ADO2024/PI4/Helper-2024-12-24.cs b/ADO2024/PI4/Helper-2024-12-24.cs new file mode 100644 index 0000000..8bb5602 --- /dev/null +++ b/ADO2024/PI4/Helper-2024-12-24.cs @@ -0,0 +1,195 @@ +using File_Folder_Helper.Models; +using Microsoft.Extensions.Logging; +#if ShellProgressBar +using ShellProgressBar; +#endif +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Text.Json; + +namespace File_Folder_Helper.ADO2024.PI4; + +internal static partial class Helper20241224 +{ + + private static readonly HttpClient _HttpClient = new(); + + private record Record(Uri URI, string Path, DateTime LastModified); + + private static ReadOnlyCollection? GetRecursiveCollection(string host, string page) + { + List? results; + Uri uri = new($"https://{host}/{page}"); + string format = NginxFileSystem.GetFormat(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.Local; + Task taskHttpResponseMessage = _HttpClient.GetAsync(uri); + taskHttpResponseMessage.Wait(); + if (!taskHttpResponseMessage.Result.IsSuccessStatusCode) + results = null; + else + { + Task taskString = taskHttpResponseMessage.Result.Content.ReadAsStringAsync(); + taskString.Wait(); + NginxFileSystem[]? nginxFileSystems = JsonSerializer.Deserialize(taskString.Result, NginxFileSystemCollectionSourceGenerationContext.Default.NginxFileSystemArray); + if (nginxFileSystems is null) + results = null; + else + { + results = []; + NginxFileSystem nginxFileSystem; + ReadOnlyCollection? directory; + for (int i = 0; i < nginxFileSystems.Length; i++) + { + nginxFileSystem = NginxFileSystem.Get(format, timeZoneInfo, uri, nginxFileSystems[i]); + if (nginxFileSystem.Type == "file") + results.Add(nginxFileSystem); + else + { + directory = GetRecursiveCollection(host, $"{page}/{nginxFileSystem.Name}"); + if (directory is null) + continue; + results.AddRange(directory); + } + } + } + } + return results?.AsReadOnly(); + } + + private static ReadOnlyCollection? GetCollection(string format, TimeZoneInfo timeZoneInfo, Uri uri) + { + List? results; + Task taskHttpResponseMessage = _HttpClient.GetAsync(uri); + taskHttpResponseMessage.Wait(); + if (!taskHttpResponseMessage.Result.IsSuccessStatusCode) + results = null; + else + { + Task taskString = taskHttpResponseMessage.Result.Content.ReadAsStringAsync(); + taskString.Wait(); + NginxFileSystem[]? nginxFileSystems = JsonSerializer.Deserialize(taskString.Result, NginxFileSystemCollectionSourceGenerationContext.Default.NginxFileSystemArray); + if (nginxFileSystems is null) + results = null; + else + { + results = []; + NginxFileSystem nginxFileSystem; + for (int i = 0; i < nginxFileSystems.Length; i++) + { + nginxFileSystem = NginxFileSystem.Get(format, timeZoneInfo, uri, nginxFileSystems[i]); + results.Add(nginxFileSystem); + } + } + } + return results?.AsReadOnly(); + } + + private static Record? CompareFile(string host, ReadOnlyCollection directoryNames, string compareDirectory, NginxFileSystem nginxFileSystem) + { + Record? result; + if (nginxFileSystem.LastModified is null || nginxFileSystem.Length is null) + result = null; + else + { + Uri uri = new($"https://{host}/{string.Join('/', directoryNames)}/{nginxFileSystem.Name}"); + FileInfo fileInfo = new($"{compareDirectory}\\{string.Join('\\', directoryNames)}\\{nginxFileSystem.Name}"); + if (!fileInfo.Exists || fileInfo.Length != nginxFileSystem.Length.Value) + result = new(uri, fileInfo.FullName, nginxFileSystem.LastModified.Value); + else + { + double totalSeconds = new TimeSpan(fileInfo.LastWriteTime.Ticks - nginxFileSystem.LastModified.Value.Ticks).TotalSeconds; + if (totalSeconds is < 2 and > -2) + result = null; + else + result = new(uri, fileInfo.FullName, nginxFileSystem.LastModified.Value); + } + } + return result; + } + + private static ReadOnlyCollection CompareDirectory(string format, TimeZoneInfo timeZoneInfo, string host, ReadOnlyCollection directoryNames, string compareDirectory, NginxFileSystem nginxFileSystem) + { + ReadOnlyCollection results; + List collection = directoryNames.ToList(); + collection.Add(nginxFileSystem.Name); + results = GetRecord(format, timeZoneInfo, host, collection.AsReadOnly(), compareDirectory); + return results; + } + + private static ReadOnlyCollection GetRecord(string format, TimeZoneInfo timeZoneInfo, string host, ReadOnlyCollection directoryNames, string compareDirectory) + { + List results = []; + Uri uri = new($"https://{host}/{string.Join('/', directoryNames)}"); + ReadOnlyCollection? nginxFileSystems = GetCollection(format, timeZoneInfo, uri); + if (nginxFileSystems is not null) + { + NginxFileSystem nginxFileSystem; + ReadOnlyCollection records; + string checkDirectory = $"{compareDirectory}\\{string.Join('\\', directoryNames)}"; + if (!Directory.Exists(checkDirectory)) + _ = Directory.CreateDirectory(checkDirectory); + for (int i = 0; i < nginxFileSystems.Count; i++) + { + nginxFileSystem = NginxFileSystem.Get(format, timeZoneInfo, uri, nginxFileSystems[i]); + if (nginxFileSystem.Type == "file") + { + Record? record = CompareFile(host, directoryNames, compareDirectory, nginxFileSystem); + if (record is not null) + results.Add(record); + } + else + { + records = CompareDirectory(format, timeZoneInfo, host, directoryNames, compareDirectory, nginxFileSystem); + foreach (Record record in records) + results.Add(record); + } + } + } + return results.AsReadOnly(); + } + + private static void Download(Record record) + { + Task taskHttpResponseMessage = _HttpClient.GetAsync(record.URI); + taskHttpResponseMessage.Wait(); + if (taskHttpResponseMessage.Result.IsSuccessStatusCode) + { + Task taskString = taskHttpResponseMessage.Result.Content.ReadAsStringAsync(); + taskString.Wait(); + File.WriteAllText(record.Path, taskString.Result); + File.SetLastWriteTime(record.Path, record.LastModified); + } + } + + internal static void Compare(ILogger logger, List args) + { + string host = args[2]; + string rootDirectoryName = args[3]; + string format = NginxFileSystem.GetFormat(); + TimeZoneInfo timeZoneInfo = TimeZoneInfo.Local; + string compareDirectory = Path.GetFullPath(args[0]); + logger.LogInformation("Comparing files on {host}", host); + ReadOnlyCollection records = GetRecord(format, timeZoneInfo, host, new([rootDirectoryName]), compareDirectory); +#if ShellProgressBar + ProgressBar progressBar = new(records.Count, "Downloading", new ProgressBarOptions() { ProgressCharacter = '─', ProgressBarOnBottom = true, DisableBottomPercentage = true }); +#endif + foreach (Record record in records) + { +#if ShellProgressBar + progressBar.Tick(); +#endif + Download(record); + } +#if ShellProgressBar + progressBar.Dispose(); +#endif + if (Debugger.IsAttached) + { + ReadOnlyCollection? recursiveCollection = GetRecursiveCollection(host, rootDirectoryName); + string? json = recursiveCollection is null ? null : JsonSerializer.Serialize(recursiveCollection.ToArray(), NginxFileSystemCollectionSourceGenerationContext.Default.NginxFileSystemArray); + if (!string.IsNullOrEmpty(json)) + File.WriteAllText(Path.Combine(Environment.CurrentDirectory, ".vscode", "helper", ".json"), json); + } + } + +} \ No newline at end of file diff --git a/Day/HelperDay.cs b/Day/HelperDay.cs index 38ebeaa..8de337b 100644 --- a/Day/HelperDay.cs +++ b/Day/HelperDay.cs @@ -81,8 +81,6 @@ internal static class HelperDay ADO2024.PI2.Helper20240711.GitRemoteRemove(logger, args); else if (args[1] == "Day-Helper-2024-07-18") ADO2024.PI2.Helper20240718.JsonToMarkdown(logger, args); - else if (args[1] == "Day-Helper-2024-07-24") - ADO2024.PI2.Helper20240724.CopyDirectories(logger, args); else if (args[1] == "Day-Helper-2024-07-28") ADO2024.PI2.Helper20240728.DownloadSslCertificates(logger, args); else if (args[1] == "Day-Helper-2024-08-05") @@ -123,6 +121,10 @@ internal static class HelperDay ADO2024.PI4.Helper20241204.ConvertToUTF8(logger, args); else if (args[1] == "Day-Helper-2024-12-12") ADO2024.PI4.Helper20241212.Rename(logger, args); + else if (args[1] == "Day-Helper-2024-12-17") + ADO2024.PI4.Helper20241217.Backup(logger, args); + else if (args[1] == "Day-Helper-2024-12-24") + ADO2024.PI4.Helper20241224.Compare(logger, args); else throw new Exception(appSettings.Company); } diff --git a/File-Folder-Helper.csproj b/File-Folder-Helper.csproj index cae3ae1..4ec475a 100644 --- a/File-Folder-Helper.csproj +++ b/File-Folder-Helper.csproj @@ -1,4 +1,4 @@ - + enable Exe @@ -18,9 +18,10 @@ + - + \ No newline at end of file diff --git a/Helpers/HelperKanbanMetadata.cs b/Helpers/HelperKanbanMetadata.cs index dc88549..ea4e46c 100644 --- a/Helpers/HelperKanbanMetadata.cs +++ b/Helpers/HelperKanbanMetadata.cs @@ -133,31 +133,30 @@ internal static partial class HelperKanbanMetadata List results = [kanbanIndexH1, string.Empty]; string h1; TimeSpan timeSpan; - List lines; LineNumber lineNumber; Record[] sorted = (from l in records orderby l.GroupCount, l.FileInfo.LastWriteTime descending select l).ToArray(); foreach (Record record in sorted) { if (record.ItemLineNumber == 0) throw new NotSupportedException(); - (lines, lineNumber) = HelperMarkdown.GetStatusAndFrontMatterYamlEndLineNumbers(record.FileInfo); - if (lines.Count == 0) + lineNumber = HelperMarkdown.GetLineNumbers(record.FileInfo); + if (lineNumber.Lines.Count == 0) continue; timeSpan = new(record.FileInfo.LastWriteTime.Ticks - record.FileInfo.CreationTime.Ticks); - h1 = lineNumber.H1 is null ? Path.GetFileNameWithoutExtension(record.FileInfo.Name) : lines[lineNumber.H1.Value]; + h1 = lineNumber.H1 is null ? Path.GetFileNameWithoutExtension(record.FileInfo.Name) : lineNumber.Lines[lineNumber.H1.Value]; results.Add($"#{h1}"); results.Add(string.Empty); results.Add("```yaml"); results.Add($"CreationTime: {record.FileInfo.CreationTime:yyyy-MM-dd}"); results.Add($"LastWriteTime: {record.FileInfo.LastWriteTime:yyyy-MM-dd}"); results.Add($"TotalDays: {Math.Round(timeSpan.TotalDays, 2)}"); - if (lineNumber.FrontMatterYamlEnd is not null && lines.Count >= lineNumber.FrontMatterYamlEnd.Value) + if (lineNumber.FrontMatterYamlEnd is not null && lineNumber.Lines.Count >= lineNumber.FrontMatterYamlEnd.Value) { for (int i = 0; i < lineNumber.FrontMatterYamlEnd; i++) { - if (lines[i] == "---") + if (lineNumber.Lines[i] == "---") continue; - results.Add(lines[i]); + results.Add(lineNumber.Lines[i]); } } results.Add($"status: {record.GroupCount}-{record.Group}"); @@ -175,7 +174,7 @@ internal static partial class HelperKanbanMetadata File.WriteAllText(file, string.Join(Environment.NewLine, results)); } - internal static void SetMetadata(string sourceDirectory, ReadOnlyCollection kanbanIndexFileLines, LineNumber kanbanIndexFileLineNumber, ReadOnlyCollection gitOthersModifiedAndDeletedExcludingStandardFiles) + internal static void SetMetadata(string sourceDirectory, LineNumber kanbanIndexFileLineNumber, ReadOnlyCollection gitOthersModifiedAndDeletedExcludingStandardFiles) { bool? match; bool gitCheck; @@ -184,23 +183,24 @@ internal static partial class HelperKanbanMetadata List lines; LineNumber lineNumber; string? directory = Path.GetDirectoryName(sourceDirectory); - List records = GetCollectionFromIndex(sourceDirectory, kanbanIndexFileLines); + List records = GetCollectionFromIndex(sourceDirectory, kanbanIndexFileLineNumber.Lines); if (directory is not null && kanbanIndexFileLineNumber.H1 is not null) { string checkDirectory = Path.Combine(directory, ".vscode", "helper"); if (Directory.Exists(checkDirectory)) { - WriteKanbanBoardFile(checkDirectory, records, kanbanIndexFileLines[kanbanIndexFileLineNumber.H1.Value]); - WriteKanbanBoardYmlView(checkDirectory, records, kanbanIndexFileLines[kanbanIndexFileLineNumber.H1.Value]); + WriteKanbanBoardFile(checkDirectory, records, kanbanIndexFileLineNumber.Lines[kanbanIndexFileLineNumber.H1.Value]); + WriteKanbanBoardYmlView(checkDirectory, records, kanbanIndexFileLineNumber.Lines[kanbanIndexFileLineNumber.H1.Value]); } } foreach (Record record in records) { if (record.ItemLineNumber == 0) throw new NotSupportedException(); - (lines, lineNumber) = HelperMarkdown.GetStatusAndFrontMatterYamlEndLineNumbers(record.FileInfo); - if (lines.Count == 0) + lineNumber = HelperMarkdown.GetLineNumbers(record.FileInfo); + if (lineNumber.Lines.Count == 0) continue; + lines = lineNumber.Lines.ToList(); statusLine = $"status: {record.GroupCount}-{record.Group}"; paramCase = lineNumber.H1 is null ? null : GetParamCase(lines[lineNumber.H1.Value]); match = lineNumber.H1 is null || paramCase is null ? null : Path.GetFileNameWithoutExtension(record.FileInfo.Name) == paramCase; @@ -235,8 +235,8 @@ internal static partial class HelperKanbanMetadata else { FileInfo fileInfo = new(indexFile); - (List lines, LineNumber lineNumber) = HelperMarkdown.GetStatusAndFrontMatterYamlEndLineNumbers(fileInfo); - SetMetadata(fullPath, new(lines), lineNumber, gitOthersModifiedAndDeletedExcludingStandardFiles: new([])); + LineNumber lineNumber = HelperMarkdown.GetLineNumbers(fileInfo); + SetMetadata(fullPath, lineNumber, gitOthersModifiedAndDeletedExcludingStandardFiles: new([])); } } diff --git a/Helpers/HelperMarkdown.cs b/Helpers/HelperMarkdown.cs index 759752b..6006331 100644 --- a/Helpers/HelperMarkdown.cs +++ b/Helpers/HelperMarkdown.cs @@ -193,7 +193,7 @@ internal static partial class HelperMarkdown return result; } - private static ReadOnlyCollection GetFromMatterYamlLines(List lines, LineNumber lineNumber) + private static ReadOnlyCollection GetFromMatterYamlLines(ReadOnlyCollection lines, LineNumber lineNumber) { List results = []; if (lineNumber.FrontMatterYamlEnd is not null && lines.Count >= lineNumber.FrontMatterYamlEnd.Value) @@ -307,7 +307,7 @@ internal static partial class HelperMarkdown return new(results); } - internal static (List, LineNumber) GetStatusAndFrontMatterYamlEndLineNumbers(FileInfo fileInfo) + internal static LineNumber GetLineNumbers(FileInfo fileInfo) { string line; int? h1LineNumber = null; @@ -315,6 +315,8 @@ internal static partial class HelperMarkdown int? statusLineNumber = null; int? createdLineNumber = null; int? updatedLineNumber = null; + int? progressLineNumber = null; + int? completedLineNumber = null; int? frontMatterYamlEndLineNumber = null; Encoding? encoding = GetEncoding(fileInfo.FullName) ?? Encoding.Default; string[] lines = File.ReadAllLines(fileInfo.FullName, encoding); @@ -350,6 +352,16 @@ internal static partial class HelperMarkdown updatedLineNumber = i; continue; } + if (line.Length > 10 && line[..10] == "progress: ") + { + progressLineNumber = i; + continue; + } + if (line.Length > 11 && line[..11] == "completed: ") + { + completedLineNumber = i; + continue; + } if (h1LineNumber is null && line.Length > 2 && line[0] == '#' && line[1] == ' ') { h1LineNumber = i; @@ -357,12 +369,15 @@ internal static partial class HelperMarkdown } } LineNumber lineNumber = new(createdLineNumber, + completedLineNumber, h1LineNumber, frontMatterYamlEndLineNumber, + lines.AsReadOnly(), + progressLineNumber, statusLineNumber, typeLineNumber, updatedLineNumber); - return (lines.ToList(), lineNumber); + return lineNumber; } private static Dictionary GetFromMatterYaml(ReadOnlyCollection frontMatterYamlLines) @@ -387,7 +402,7 @@ internal static partial class HelperMarkdown return results; } - private static ReadOnlyDictionary GetFromMatterYaml(List lines, LineNumber lineNumber) + private static ReadOnlyDictionary GetFromMatterYaml(ReadOnlyCollection lines, LineNumber lineNumber) { Dictionary results = []; #pragma warning disable IL3050 @@ -660,12 +675,12 @@ internal static partial class HelperMarkdown string key; string type; bool isKanbanIndex; - List lines; bool isWithinSource; bool isKanbanMarkdown; LineNumber lineNumber; MarkdownFile markdownFile; string fileNameWithoutExtension; + ReadOnlyCollection lines; ReadOnlyDictionary frontMatterYaml; bool isGitOthersModifiedAndDeletedExcludingStandard; ReadOnlyCollection files = GetFiles(appSettings, input); @@ -678,7 +693,8 @@ internal static partial class HelperMarkdown isGitOthersModifiedAndDeletedExcludingStandard = gitOthersModifiedAndDeletedExcludingStandardFiles.Contains(fileInfo.FullName); if (!isWithinSource && results.ContainsKey(key)) continue; - (lines, lineNumber) = GetStatusAndFrontMatterYamlEndLineNumbers(fileInfo); + lineNumber = GetLineNumbers(fileInfo); + lines = lineNumber.Lines; fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileInfo.FullName); h1 = fileNameWithoutExtension.ToLower().Replace("%20", "-").Replace(' ', '-'); frontMatterYaml = GetFromMatterYaml(lines, lineNumber); @@ -690,7 +706,7 @@ internal static partial class HelperMarkdown continue; type = appSettings.DefaultNoteType; File.WriteAllLines(fileInfo.FullName, ["---", $"type: {type}\"", "---", string.Empty, $"# {h1}"]); - lines = File.ReadAllLines(fileInfo.FullName).ToList(); + lines = File.ReadAllLines(fileInfo.FullName).AsReadOnly(); } isKanbanMarkdown = fileInfo.Name.EndsWith(".knb.md"); isKanbanIndex = fileNameWithoutExtension == "index" && type.StartsWith("kanb", StringComparison.OrdinalIgnoreCase); @@ -1042,7 +1058,7 @@ internal static partial class HelperMarkdown } } - private static (string type, string h1) GetTypeAndH1(AppSettings appSettings, string h1, List lines, LineNumber lineNumber) + private static (string type, string h1) GetTypeAndH1(AppSettings appSettings, string h1, ReadOnlyCollection lines, LineNumber lineNumber) { string type = lineNumber.Type is null ? appSettings.DefaultNoteType : lines[lineNumber.Type.Value][5..].Trim().Trim('"'); string h1FromFile = lineNumber.H1 is null ? h1 : lines[lineNumber.H1.Value][2..]; @@ -1080,7 +1096,7 @@ internal static partial class HelperMarkdown createdLine = $"created: {creationDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}"; updatedLine = $"updated: {markdownFile.LastWriteDateTime.ToUniversalTime():yyyy-MM-ddTHH:mm:ss.fffZ}"; if (markdownFile.IsKanbanIndex) - HelperKanbanMetadata.SetMetadata(markdownFile.Directory, new(lines), markdownFile.LineNumber, gitOthersModifiedAndDeletedExcludingStandardFiles); + HelperKanbanMetadata.SetMetadata(markdownFile.Directory, markdownFile.LineNumber, gitOthersModifiedAndDeletedExcludingStandardFiles); if (markdownFile.LineNumber.FrontMatterYamlEnd is null) { if (markdownFile.LineNumber.H1 is not null) diff --git a/Models/LineNumber.cs b/Models/LineNumber.cs index e617d9c..bc1fc64 100644 --- a/Models/LineNumber.cs +++ b/Models/LineNumber.cs @@ -1,10 +1,14 @@ +using System.Collections.ObjectModel; using System.Text.Json.Serialization; namespace File_Folder_Helper.Models; internal record LineNumber(int? Created, + int? Completed, int? H1, int? FrontMatterYamlEnd, + ReadOnlyCollection Lines, + int? Progress, int? Status, int? Type, int? Updated); diff --git a/Models/NginxFileSystem.cs b/Models/NginxFileSystem.cs index 22f091d..db92c41 100644 --- a/Models/NginxFileSystem.cs +++ b/Models/NginxFileSystem.cs @@ -1,12 +1,15 @@ +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; namespace File_Folder_Helper.Models; -internal record NginxFileSystem(string Name, - string Type, - string MTime, - float Size) +internal record NginxFileSystem([property: JsonPropertyName("name")] string Name, + DateTime? LastModified, + [property: JsonPropertyName("mtime")] string MTime, + Uri? URI, + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("size")] float? Length) { public override string ToString() @@ -15,6 +18,25 @@ internal record NginxFileSystem(string Name, return result; } + public static NginxFileSystem Get(string format, TimeZoneInfo timeZoneInfo, string name, string mTime, Uri uri, string type, float? size) + { + NginxFileSystem result; + DateTime dateTime; + DateTime? nullableDateTime; + if (mTime.Length != format.Length + 4 || !DateTime.TryParseExact(mTime[..format.Length], format, CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTime)) + nullableDateTime = null; + else + nullableDateTime = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(dateTime, mTime[(format.Length + 1)..], timeZoneInfo.Id); + result = new(name, nullableDateTime, mTime, uri, type, size); + return result; + } + + public static string GetFormat() => + "ddd, dd MMM yyyy HH:mm:ss"; + + public static NginxFileSystem Get(string format, TimeZoneInfo timeZoneInfo, Uri uri, NginxFileSystem nginxFileSystem) => + Get(format, timeZoneInfo, nginxFileSystem.Name, nginxFileSystem.MTime, uri, nginxFileSystem.Type, nginxFileSystem.Length); + } [JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] diff --git a/Scripts/epoch.js b/Scripts/epoch.js index 3bebc14..aef1654 100644 --- a/Scripts/epoch.js +++ b/Scripts/epoch.js @@ -14,6 +14,8 @@ // console.log("ticks: " + ticks); // var dateText = ticks + " - " + date.toString(); // console.log("dateText: " + dateText); +// DateTime utcMeDateTime = new(1980, 1, 17, 0, 0, 0, DateTimeKind.Utc); +// long meTotalSeconds = (long)Math.Floor(fileInfo.LastWriteTime.ToUniversalTime().Subtract(utcMeDateTime).TotalSeconds); let date = new Date(); let timezoneOffset = date.getTimezoneOffset(); let seconds = date.getTime().valueOf() + timezoneOffset; @@ -27,5 +29,5 @@ console.log("end"); // dateText: 638665132483790000 - Wed Nov 06 2024 10:55:58 GMT-0700 (Mountain Standard Time) // Now - To: 638665132334594771 // 638665135325760000 -// 638665136814890000 +// 638665136814890000 // utc1970DateTime: 621355968000000000 \ No newline at end of file