44
55using System . Collections . Concurrent ;
66using System . IO . Abstractions ;
7+ using Elastic . Documentation . Extensions ;
78using Microsoft . Extensions . Logging ;
89
910namespace Elastic . Documentation . Configuration . Diagram ;
1011
1112/// <summary>
1213/// Information about a diagram that needs to be cached
1314/// </summary>
14- /// <param name="LocalSvgPath">Local SVG path relative to output directory </param>
15+ /// <param name="OutputFile">The intended cache output file location </param>
1516/// <param name="EncodedUrl">Encoded Kroki URL for downloading</param>
16- /// <param name="OutputDirectory">Full path to output directory</param>
17- public record DiagramCacheInfo ( string LocalSvgPath , string EncodedUrl , string OutputDirectory ) ;
17+ public record DiagramCacheInfo ( IFileInfo OutputFile , string EncodedUrl ) ;
1818
19- /// <summary>
2019/// Registry to track active diagrams and manage cleanup of outdated cached files
21- /// </summary>
22- /// <param name="writeFileSystem">File system for write/delete operations during cleanup</param>
23- public class DiagramRegistry ( IFileSystem writeFileSystem ) : IDisposable
20+ public class DiagramRegistry ( ILoggerFactory logFactory , BuildContext context ) : IDisposable
2421{
25- private readonly ConcurrentDictionary < string , bool > _activeDiagrams = new ( ) ;
22+ private readonly ILogger < DiagramRegistry > _logger = logFactory . CreateLogger < DiagramRegistry > ( ) ;
2623 private readonly ConcurrentDictionary < string , DiagramCacheInfo > _diagramsToCache = new ( ) ;
27- private readonly IFileSystem _writeFileSystem = writeFileSystem ;
24+ private readonly IFileSystem _writeFileSystem = context . WriteFileSystem ;
25+ private readonly IFileSystem _readFileSystem = context . ReadFileSystem ;
2826 private readonly HttpClient _httpClient = new ( ) { Timeout = TimeSpan . FromSeconds ( 30 ) } ;
2927
3028 /// <summary>
3129 /// Register a diagram for caching (collects info for later batch processing)
3230 /// </summary>
33- /// <param name="localSvgPath">The local SVG path relative to output directory</param>
31+ /// <param name="localSvgPath">The local SVG path relative to the output directory</param>
3432 /// <param name="encodedUrl">The encoded Kroki URL for downloading</param>
35- /// <param name="outputDirectory">The full path to output directory</param>
36- public void RegisterDiagramForCaching ( string localSvgPath , string encodedUrl , string outputDirectory )
33+ /// <param name="outputDirectory">The full path to the output directory</param>
34+ public void RegisterDiagramForCaching ( IFileInfo outputFile , string encodedUrl )
3735 {
38- if ( string . IsNullOrEmpty ( localSvgPath ) || string . IsNullOrEmpty ( encodedUrl ) )
36+ if ( string . IsNullOrEmpty ( encodedUrl ) )
3937 return ;
4038
41- _ = _activeDiagrams . TryAdd ( localSvgPath , true ) ;
42- _ = _diagramsToCache . TryAdd ( localSvgPath , new DiagramCacheInfo ( localSvgPath , encodedUrl , outputDirectory ) ) ;
43- }
39+ if ( ! outputFile . IsSubPathOf ( context . DocumentationOutputDirectory ) )
40+ return ;
4441
45- /// <summary>
46- /// Clear all registered diagrams (called at start of build)
47- /// </summary>
48- public void Clear ( )
49- {
50- _activeDiagrams . Clear ( ) ;
51- _diagramsToCache . Clear ( ) ;
42+ _ = _diagramsToCache . TryAdd ( outputFile . FullName , new DiagramCacheInfo ( outputFile , encodedUrl ) ) ;
5243 }
5344
5445 /// <summary>
5546 /// Create cached diagram files by downloading from Kroki in parallel
5647 /// </summary>
57- /// <param name="logger">Logger for reporting download activity</param>
58- /// <param name="readFileSystem">File system for checking existing files</param>
5948 /// <returns>Number of diagrams downloaded</returns>
60- public async Task < int > CreateDiagramCachedFiles ( ILogger logger , IFileSystem readFileSystem )
49+ public async Task < int > CreateDiagramCachedFiles ( Cancel ctx )
6150 {
6251 if ( _diagramsToCache . IsEmpty )
6352 return 0 ;
@@ -67,87 +56,87 @@ public async Task<int> CreateDiagramCachedFiles(ILogger logger, IFileSystem read
6756 await Parallel . ForEachAsync ( _diagramsToCache . Values , new ParallelOptions
6857 {
6958 MaxDegreeOfParallelism = Environment . ProcessorCount ,
70- CancellationToken = CancellationToken . None
59+ CancellationToken = ctx
7160 } , async ( diagramInfo , ct ) =>
7261 {
62+ var localPath = _readFileSystem . Path . GetRelativePath ( context . DocumentationOutputDirectory . FullName , diagramInfo . OutputFile . FullName ) ;
63+
7364 try
7465 {
75- var fullPath = _writeFileSystem . Path . Combine ( diagramInfo . OutputDirectory , diagramInfo . LocalSvgPath ) ;
66+ if ( ! diagramInfo . OutputFile . IsSubPathOf ( context . DocumentationOutputDirectory ) )
67+ return ;
7668
77- // Skip if file already exists
78- if ( readFileSystem . File . Exists ( fullPath ) )
69+ // Skip if the file already exists
70+ if ( _readFileSystem . File . Exists ( diagramInfo . OutputFile . FullName ) )
7971 return ;
8072
81- // Create directory if needed
82- var directory = _writeFileSystem . Path . GetDirectoryName ( fullPath ) ;
73+ // Create the directory if needed
74+ var directory = _writeFileSystem . Path . GetDirectoryName ( diagramInfo . OutputFile . FullName ) ;
8375 if ( directory != null && ! _writeFileSystem . Directory . Exists ( directory ) )
84- {
8576 _ = _writeFileSystem . Directory . CreateDirectory ( directory ) ;
86- }
8777
8878 // Download SVG content
8979 var svgContent = await _httpClient . GetStringAsync ( diagramInfo . EncodedUrl , ct ) ;
9080
9181 // Validate SVG content
9282 if ( string . IsNullOrWhiteSpace ( svgContent ) || ! svgContent . Contains ( "<svg" , StringComparison . OrdinalIgnoreCase ) )
9383 {
94- logger . LogWarning ( "Invalid SVG content received for diagram {LocalPath}" , diagramInfo . LocalSvgPath ) ;
84+ _logger . LogWarning ( "Invalid SVG content received for diagram {LocalPath}" , localPath ) ;
9585 return ;
9686 }
9787
98- // Write atomically using temp file
99- var tempPath = fullPath + " .tmp";
88+ // Write atomically using a temp file
89+ var tempPath = $ " { diagramInfo . OutputFile . FullName } .tmp";
10090 await _writeFileSystem . File . WriteAllTextAsync ( tempPath , svgContent , ct ) ;
101- _writeFileSystem . File . Move ( tempPath , fullPath ) ;
91+ _writeFileSystem . File . Move ( tempPath , diagramInfo . OutputFile . FullName ) ;
10292
10393 _ = Interlocked . Increment ( ref downloadCount ) ;
104- logger . LogDebug ( "Downloaded diagram: {LocalPath}" , diagramInfo . LocalSvgPath ) ;
94+ _logger . LogDebug ( "Downloaded diagram: {LocalPath}" , localPath ) ;
10595 }
10696 catch ( HttpRequestException ex )
10797 {
108- logger . LogWarning ( "Failed to download diagram {LocalPath}: {Error}" , diagramInfo . LocalSvgPath , ex . Message ) ;
98+ _logger . LogWarning ( "Failed to download diagram {LocalPath}: {Error}" , localPath , ex . Message ) ;
10999 }
110100 catch ( TaskCanceledException ex ) when ( ex . InnerException is TimeoutException )
111101 {
112- logger . LogWarning ( "Timeout downloading diagram {LocalPath}" , diagramInfo . LocalSvgPath ) ;
102+ _logger . LogWarning ( "Timeout downloading diagram {LocalPath}" , localPath ) ;
113103 }
114104 catch ( Exception ex )
115105 {
116- logger . LogWarning ( "Unexpected error downloading diagram {LocalPath}: {Error}" , diagramInfo . LocalSvgPath , ex . Message ) ;
106+ _logger . LogWarning ( "Unexpected error downloading diagram {LocalPath}: {Error}" , localPath , ex . Message ) ;
117107 }
118108 } ) ;
119109
120110 if ( downloadCount > 0 )
121- {
122- logger . LogInformation ( "Downloaded {DownloadCount} diagram files from Kroki" , downloadCount ) ;
123- }
111+ _logger . LogInformation ( "Downloaded {DownloadCount} diagram files from Kroki" , downloadCount ) ;
124112
125113 return downloadCount ;
126114 }
127115
128116 /// <summary>
129117 /// Clean up unused diagram files from the cache directory
130118 /// </summary>
131- /// <param name="outputDirectory">The output directory containing cached diagrams</param>
132119 /// <returns>Number of files cleaned up</returns>
133- public int CleanupUnusedDiagrams ( IDirectoryInfo outputDirectory )
120+ public int CleanupUnusedDiagrams ( )
134121 {
135- var graphsDir = _writeFileSystem . Path . Combine ( outputDirectory . FullName , "images" , "generated-graphs" ) ;
136- if ( ! _writeFileSystem . Directory . Exists ( graphsDir ) )
122+ if ( ! _readFileSystem . Directory . Exists ( context . DocumentationOutputDirectory . FullName ) )
123+ return 0 ;
124+ var folders = _writeFileSystem . Directory . GetDirectories ( context . DocumentationOutputDirectory . FullName , "generated-graphs" , SearchOption . AllDirectories ) ;
125+ var existingFiles = folders
126+ . Select ( f => ( Folder : f , Files : _writeFileSystem . Directory . GetFiles ( f , "*.svg" , SearchOption . TopDirectoryOnly ) ) )
127+ . ToArray ( ) ;
128+ if ( existingFiles . Length == 0 )
137129 return 0 ;
138-
139- var existingFiles = _writeFileSystem . Directory . GetFiles ( graphsDir , "*.svg" , SearchOption . AllDirectories ) ;
140130 var cleanedCount = 0 ;
141131
142132 try
143133 {
144- foreach ( var file in existingFiles )
134+ foreach ( var ( folder , files ) in existingFiles )
145135 {
146- var relativePath = _writeFileSystem . Path . GetRelativePath ( outputDirectory . FullName , file ) ;
147- var normalizedPath = relativePath . Replace ( _writeFileSystem . Path . DirectorySeparatorChar , '/' ) ;
148-
149- if ( ! _activeDiagrams . ContainsKey ( normalizedPath ) )
136+ foreach ( var file in files )
150137 {
138+ if ( _diagramsToCache . ContainsKey ( file ) )
139+ continue ;
151140 try
152141 {
153142 _writeFileSystem . File . Delete ( file ) ;
@@ -158,10 +147,9 @@ public int CleanupUnusedDiagrams(IDirectoryInfo outputDirectory)
158147 // Silent failure - cleanup is opportunistic
159148 }
160149 }
150+ // Clean up empty directories
151+ CleanupEmptyDirectories ( folder ) ;
161152 }
162-
163- // Clean up empty directories
164- CleanupEmptyDirectories ( graphsDir ) ;
165153 }
166154 catch
167155 {
@@ -175,22 +163,26 @@ private void CleanupEmptyDirectories(string directory)
175163 {
176164 try
177165 {
178- foreach ( var subDir in _writeFileSystem . Directory . GetDirectories ( directory ) )
179- {
180- CleanupEmptyDirectories ( subDir ) ;
166+ var folder = _writeFileSystem . DirectoryInfo . New ( directory ) ;
167+ if ( ! folder . IsSubPathOf ( context . DocumentationOutputDirectory ) )
168+ return ;
181169
182- if ( ! _writeFileSystem . Directory . EnumerateFileSystemEntries ( subDir ) . Any ( ) )
183- {
184- try
185- {
186- _writeFileSystem . Directory . Delete ( subDir ) ;
187- }
188- catch
189- {
190- // Silent failure - cleanup is opportunistic
191- }
192- }
193- }
170+ if ( folder . Name != "generated-graphs" )
171+ return ;
172+
173+ if ( _writeFileSystem . Directory . EnumerateFileSystemEntries ( folder . FullName ) . Any ( ) )
174+ return ;
175+
176+ _writeFileSystem . Directory . Delete ( folder . FullName ) ;
177+
178+ var parentFolder = folder . Parent ;
179+ if ( parentFolder is null || parentFolder . Name != "images" )
180+ return ;
181+
182+ if ( _writeFileSystem . Directory . EnumerateFileSystemEntries ( parentFolder . FullName ) . Any ( ) )
183+ return ;
184+
185+ _writeFileSystem . Directory . Delete ( folder . FullName ) ;
194186 }
195187 catch
196188 {
0 commit comments