diff --git a/URLNotesGrabberCORE/OutputMode.cs b/URLNotesGrabberCORE/OutputMode.cs index 5096edd..fedff90 100644 --- a/URLNotesGrabberCORE/OutputMode.cs +++ b/URLNotesGrabberCORE/OutputMode.cs @@ -8,13 +8,16 @@ namespace URLNotesGrabberCORE // field order). Reads from TL.db via DataAccess.GetAllPostsForBlog. public static class OutputMode { - public static int Run(IConfiguration config) + public static int Run(IConfiguration config, string[]? args = null) { DataAccess.EnsureTTFileHelperColumnsExist(); string dbPath = DataAccess.GetActiveDbPath(); Console.WriteLine($"Database: {Path.GetFullPath(dbPath)}"); + if (!RefreshPaths(config, args ?? Array.Empty())) + return 1; + var blogs = DataAccess.GetAllBlogsWithTTFolderPath(); int activeBlogs = DataAccess.CountActiveBlogs(); Console.WriteLine($"{blogs.Count} of {activeBlogs} active blog(s) have a TTFolderPath."); @@ -22,8 +25,8 @@ namespace URLNotesGrabberCORE if (blogs.Count == 0) { Console.WriteLine($"\nNothing to export: no blog in {Path.GetFullPath(dbPath)} has a TTFolderPath."); - Console.WriteLine("Run --updatepaths on this machine to populate it from \\Index\\*.tumblr / *.tmblrpriv,"); - Console.WriteLine("or set appSettings:PathTTRoot and run --updatepaths with no argument."); + Console.WriteLine("Point --output at a TumblThree root so it can populate them: --output ,"); + Console.WriteLine("or set appSettings:PathTTRoot so the refresh runs automatically."); return 1; } @@ -93,6 +96,49 @@ namespace URLNotesGrabberCORE return 0; } + // Re-reads the TumblThree Index metadata into Blogs.TTFolderPath before exporting. + // A TL.db synced between machines cannot hold one absolute path that is valid on + // both, so the stored paths are only trustworthy on the machine that wrote them -- + // which makes this refresh part of a normal export rather than a separate chore. + // Returns false only when the run should stop. + private static bool RefreshPaths(IConfiguration config, string[] args) + { + var settings = config.GetSection("appSettings"); + + if (args.Any(a => string.Equals(a, "--norefresh", StringComparison.OrdinalIgnoreCase))) + { + Console.WriteLine("Path refresh skipped (--norefresh); exporting to whatever paths TL.db already holds."); + return true; + } + + string? root = args.FirstOrDefault(a => !a.StartsWith("--", StringComparison.Ordinal)) + ?? settings.GetValue("PathTTRoot"); + + var result = UpdateBlogPathsRunner.Scan(root, verbose: false); + + switch (result.Outcome) + { + case UpdateBlogPathsRunner.ScanOutcome.NoRootConfigured: + Console.WriteLine("No TumblThree root configured (appSettings:PathTTRoot is empty and none was passed),"); + Console.WriteLine("so TTFolderPath was not refreshed. Pass one as --output to refresh it."); + return true; + + case UpdateBlogPathsRunner.ScanOutcome.IndexFolderMissing: + // Silently exporting stale paths here would defeat the point of folding + // the refresh in, so a bad root is a hard stop. + Console.WriteLine($"Index folder not found at: {result.IndexPath}"); + Console.WriteLine("Fix the root (or pass --norefresh to export the paths already in TL.db)."); + return false; + + default: + Console.WriteLine($"Refreshed paths from {result.IndexPath}: " + + $"{result.MetadataFiles} metadata file(s), {result.Written} written, " + + $"{result.Unchanged} already correct, {result.NoLocation} without a location, " + + $"{result.NoMatchingRow} without a blog row, {result.Errors} error(s)."); + return true; + } + } + private static void RenameExistingTxtFilesToBak(string folderPath) { try diff --git a/URLNotesGrabberCORE/Program.cs b/URLNotesGrabberCORE/Program.cs index 389faa5..dddfbcd 100644 --- a/URLNotesGrabberCORE/Program.cs +++ b/URLNotesGrabberCORE/Program.cs @@ -343,7 +343,7 @@ namespace URLNotesGrabberCORE break; case "--output": - exitCode = OutputMode.Run(config); + exitCode = OutputMode.Run(config, args.Skip(1).ToArray()); break; case "--revert": @@ -439,7 +439,9 @@ namespace URLNotesGrabberCORE Console.WriteLine("--ingest [blogname]\t Ingest Tumblr .txt exports from appSettings:PathTTRoot into TL.db (all blogs, or single blog if name given)"); - Console.WriteLine("--output\t Export posts from TL.db back to .txt files in each blog's TTFolderPath"); + Console.WriteLine("--output [rootPath]\t Refresh Blogs.TTFolderPath from \\Index (or appSettings:PathTTRoot), then export posts from TL.db back to .txt files in each blog's folder"); + + Console.WriteLine("--output --norefresh\t Export without refreshing TTFolderPath first"); Console.WriteLine("--revert [blogname]\t Recursively scan the PathInput tree and restore *.bak back to *.txt (current .txt saved as next-free .bkN); optional blogname filters by path substring"); diff --git a/URLNotesGrabberCORE/UpdateBlogPathsRunner.cs b/URLNotesGrabberCORE/UpdateBlogPathsRunner.cs index ce516ea..dbbf340 100644 --- a/URLNotesGrabberCORE/UpdateBlogPathsRunner.cs +++ b/URLNotesGrabberCORE/UpdateBlogPathsRunner.cs @@ -4,32 +4,58 @@ namespace URLNotesGrabberCORE { // Port of ThreeTxtFileHelper/UpdateBlogPaths.cs. Reads .tumblr / .tmblrpriv metadata // files from a root\Index folder and populates Blogs.TTFolderPath in TL.db. + // + // Scan() is the reusable engine: --updatepaths wraps it as a standalone command and + // --output calls it as a refresh step, because a TL.db synced between machines cannot + // hold one absolute path that is correct on both. public static class UpdateBlogPathsRunner { - public static int Run(string rootPath) + public enum ScanOutcome + { + Completed, + NoRootConfigured, + IndexFolderMissing + } + + public sealed class ScanResult + { + public ScanOutcome Outcome { get; init; } + public string RootPath { get; init; } = string.Empty; + public string IndexPath { get; init; } = string.Empty; + public int MetadataFiles { get; init; } + public int Written { get; init; } + public int Unchanged { get; init; } + public int NoLocation { get; init; } + public int NoMatchingRow { get; init; } + public int Errors { get; init; } + } + + // verbose: log a line per metadata file. --updatepaths wants that detail; --output + // only wants the counts, since a few hundred lines before the export would bury it. + public static ScanResult Scan(string? rootPath, bool verbose) { if (string.IsNullOrWhiteSpace(rootPath)) - { - Console.WriteLine("UpdateBlogPaths: rootPath is required."); - return 1; - } + return new ScanResult { Outcome = ScanOutcome.NoRootConfigured }; DataAccess.EnsureTTFileHelperColumnsExist(); string indexPath = Path.Combine(rootPath, "Index"); if (!Directory.Exists(indexPath)) { - Console.WriteLine($"Index folder not found at: {indexPath}"); - return 1; + return new ScanResult + { + Outcome = ScanOutcome.IndexFolderMissing, + RootPath = rootPath, + IndexPath = indexPath + }; } - Console.WriteLine($"Scanning Index folder: {indexPath}"); - var blogFiles = Directory.GetFiles(indexPath, "*.tumblr") .Concat(Directory.GetFiles(indexPath, "*.tmblrpriv")) .ToList(); - Console.WriteLine($"Found {blogFiles.Count} blog metadata files"); + if (verbose) + Console.WriteLine($"Found {blogFiles.Count} blog metadata files"); int updatedCount = 0; int unchangedCount = 0; @@ -55,7 +81,8 @@ namespace URLNotesGrabberCORE if (DataAccess.SetBlogTTFolderPath(blogName, fileDownloadLocation)) { updatedCount++; - Console.WriteLine($"Updated {blogName}: {fileDownloadLocation}"); + if (verbose) + Console.WriteLine($"Updated {blogName}: {fileDownloadLocation}"); } else if (DataAccess.BlogExists(blogName)) { @@ -70,13 +97,15 @@ namespace URLNotesGrabberCORE else { noLocationCount++; - Console.WriteLine($"Empty FileDownloadLocation in {blogFile}"); + if (verbose) + Console.WriteLine($"Empty FileDownloadLocation in {blogFile}"); } } else { noLocationCount++; - Console.WriteLine($"No FileDownloadLocation found in {blogFile}"); + if (verbose) + Console.WriteLine($"No FileDownloadLocation found in {blogFile}"); } } catch (Exception ex) @@ -86,18 +115,50 @@ namespace URLNotesGrabberCORE } } + return new ScanResult + { + Outcome = ScanOutcome.Completed, + RootPath = rootPath, + IndexPath = indexPath, + MetadataFiles = blogFiles.Count, + Written = updatedCount, + Unchanged = unchangedCount, + NoLocation = noLocationCount, + NoMatchingRow = noRowCount, + Errors = errorCount + }; + } + + public static int Run(string rootPath) + { + if (string.IsNullOrWhiteSpace(rootPath)) + { + Console.WriteLine("UpdateBlogPaths: rootPath is required."); + return 1; + } + + string indexPath = Path.Combine(rootPath, "Index"); + Console.WriteLine($"Scanning Index folder: {indexPath}"); + + var result = Scan(rootPath, verbose: true); + + if (result.Outcome == ScanOutcome.IndexFolderMissing) + { + Console.WriteLine($"Index folder not found at: {result.IndexPath}"); + return 1; + } + Console.WriteLine($"\n========== UpdateBlogPaths summary =========="); - Console.WriteLine($"Metadata files: {blogFiles.Count}"); - Console.WriteLine($"TTFolderPath written: {updatedCount}"); - Console.WriteLine($"Already correct: {unchangedCount}"); - Console.WriteLine($"No FileDownloadLocation: {noLocationCount}"); - Console.WriteLine($"No matching blog row: {noRowCount}"); - Console.WriteLine($"Errors: {errorCount}"); + Console.WriteLine($"Metadata files: {result.MetadataFiles}"); + Console.WriteLine($"TTFolderPath written: {result.Written}"); + Console.WriteLine($"Already correct: {result.Unchanged}"); + Console.WriteLine($"No FileDownloadLocation: {result.NoLocation}"); + Console.WriteLine($"No matching blog row: {result.NoMatchingRow}"); + Console.WriteLine($"Errors: {result.Errors}"); - int stored = DataAccess.CountBlogsWithTTFolderPath(); - Console.WriteLine($"\nBlogs now holding a TTFolderPath: {stored}"); + Console.WriteLine($"\nBlogs now holding a TTFolderPath: {DataAccess.CountBlogsWithTTFolderPath()}"); - return errorCount == 0 ? 0 : 2; + return result.Errors == 0 ? 0 : 2; } } }