feat: fold the TTFolderPath refresh into --output

TL.db syncs between machines whose absolute paths differ, so a single
TTFolderPath column cannot be correct on both at once -- the stored paths are
only trustworthy on the machine that wrote them. That made --updatepaths a
mandatory prelude to every --output rather than the one-time setup step it
looks like.

--output now refreshes the column from the TumblThree Index metadata before
exporting. The root comes from the first non-flag argument, else
appSettings:PathTTRoot. With no root available it says so and exports whatever
TL.db already holds; a root whose Index folder is missing is a hard stop, since
silently exporting stale paths is the failure this change exists to prevent.
--norefresh skips the refresh for a pure export.

The scan is extracted from UpdateBlogPathsRunner.Run into a reusable Scan() that
returns counts instead of only printing them, so --updatepaths keeps its
per-file detail while --output prints a single summary line rather than a few
hundred lines ahead of the export.

Verified against a throwaway database: a stale cross-machine path is repaired
and the export lands in the correct local folder; no configured root warns and
continues (exit 0); a missing Index folder stops (exit 1); --norefresh skips the
refresh and exports (exit 0).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
jim
2026-08-05 12:09:55 -05:00
co-authored by Claude Opus 5
parent 721224bc13
commit 8f4177a0c9
3 changed files with 136 additions and 27 deletions
+49 -3
View File
@@ -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<string>()))
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 <root> on this machine to populate it from <root>\\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 <root>,");
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<string>("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 <root> 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
+4 -2
View File
@@ -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 <root>\\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");
+83 -22
View File
@@ -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;
}
}
}