-
Notifications
You must be signed in to change notification settings - Fork 184
Expand file tree
/
Copy pathSessionFileValidator.cs
More file actions
290 lines (261 loc) · 13.4 KB
/
Copy pathSessionFileValidator.cs
File metadata and controls
290 lines (261 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
using System.Security;
using LogExpert.Core.Interfaces;
namespace LogExpert.Core.Classes.Persister;
/// <summary>
/// Provides static methods for validating project file references, identifying missing or accessible files, and
/// suggesting alternative file paths using available file system plugins.
/// </summary>
/// <remarks>This class is intended for use with project data that includes file references and a project file
/// path. It supports validation of both local file paths and URI-based files through plugin resolution. All methods are
/// thread-safe and do not modify input data. Use the provided methods to check file existence, resolve canonical file
/// paths, and locate possible alternatives for missing files.</remarks>
public static class SessionFileValidator
{
/// <summary>
/// Validates the files referenced by the specified project and identifies missing or accessible files using
/// available file system plugins.
/// </summary>
/// <remarks>Files are considered valid if they exist on disk or if a suitable file system plugin is
/// available for URI-based files. For missing files, possible alternative paths are suggested based on the project
/// file location.</remarks>
/// <param name="sessionData">The project data containing the list of file names to validate and the project file path. Cannot be null.</param>
/// <param name="pluginRegistry">The plugin registry used to resolve file system plugins for URI-based files. Cannot be null.</param>
/// <returns>A SessionValidationResult containing lists of valid files, missing files, and possible alternative file paths
/// for missing files.</returns>
public static SessionValidationResult ValidateSession (SessionData sessionData, IPluginRegistry pluginRegistry)
{
ArgumentNullException.ThrowIfNull(sessionData);
ArgumentNullException.ThrowIfNull(pluginRegistry);
var result = new SessionValidationResult();
// Cache drive letters once to avoid repeated expensive DriveInfo.GetDrives() calls
var cachedDriveLetters = GetFixedDriveLetters();
// Enumerate the session directory and its immediate subdirectories once, instead of
// re-enumerating for every missing file. This is shared across all alternative-path lookups.
var sessionDirectories = GetSessionSearchDirectories(sessionData.SessionFilePath);
foreach (var fileName in sessionData.FileNames)
{
var normalizedPath = NormalizeFilePath(fileName);
if (File.Exists(normalizedPath))
{
result.ValidFiles.Add(fileName);
}
else if (IsUri(fileName))
{
// Check if URI-based file system plugin is available
var fs = pluginRegistry.FindFileSystemForUri(fileName);
if (fs != null)
{
result.ValidFiles.Add(fileName);
}
else
{
result.MissingFiles.Add(fileName);
}
}
else
{
result.MissingFiles.Add(fileName);
var alternativePaths = FindAlternativePaths(fileName, sessionData.SessionFilePath, sessionDirectories, cachedDriveLetters);
result.PossibleAlternatives[fileName] = alternativePaths;
}
}
return result;
}
/// <summary>
/// Normalizes the specified file path by resolving the actual file name if the file is a persistence file.
/// </summary>
/// <remarks>Use this method to obtain the canonical file path for files that may be persisted under a
/// different name. For files that do not have a ".lxp" extension, the input path is returned unchanged.</remarks>
/// <param name="fileName">The path of the file to normalize. If the file has a ".lxp" extension, its persisted file name will be resolved;
/// otherwise, the original path is returned.</param>
/// <returns>The normalized file path. If the input is a persistence file, returns the resolved file name; otherwise, returns
/// the original file path.</returns>
private static string NormalizeFilePath (string fileName)
{
if (fileName.EndsWith(".lxp", StringComparison.OrdinalIgnoreCase))
{
var persistenceData = Persister.Load(fileName);
return persistenceData?.FileName ?? fileName;
}
return fileName;
}
/// <summary>
/// Determines whether the specified string represents an absolute URI with a scheme other than "file".
/// </summary>
/// <remarks>This method returns false for local file paths and URIs with the "file" scheme, treating them
/// as regular files rather than remote resources. Common URI schemes include "http", "https", "ftp", and
/// "sftp".</remarks>
/// <param name="fileName">The string to evaluate as a potential URI. Cannot be null, empty, or consist only of white-space characters.</param>
/// <returns>true if the string is a valid absolute URI with a non-file scheme; otherwise, false.</returns>
private static bool IsUri (string fileName)
{
return !string.IsNullOrWhiteSpace(fileName) &&
Uri.TryCreate(fileName, UriKind.Absolute, out var uri) &&
!string.IsNullOrEmpty(uri.Scheme) &&
!uri.Scheme.Equals("file", StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Returns the directories to search for alternative file locations: the session file's directory
/// followed by its immediate subdirectories. Enumerated once per session so that per-missing-file
/// lookups do not repeatedly hit the file system with the same <see cref="Directory.GetDirectories(string)"/> call.
/// </summary>
/// <param name="sessionFilePath">The full path to the session/project file. May be null or empty.</param>
/// <returns>The session directory and its immediate subdirectories, or an empty list if unavailable.</returns>
private static List<string> GetSessionSearchDirectories (string sessionFilePath)
{
if (string.IsNullOrWhiteSpace(sessionFilePath))
{
return [];
}
try
{
var sessionDir = Path.GetDirectoryName(sessionFilePath);
if (string.IsNullOrEmpty(sessionDir) || !Directory.Exists(sessionDir))
{
return [];
}
var directories = new List<string> { sessionDir };
directories.AddRange(Directory.GetDirectories(sessionDir));
return directories;
}
catch (Exception ex) when (ex is ArgumentException or
ArgumentNullException or
PathTooLongException or
UnauthorizedAccessException or
IOException)
{
// Ignore errors when enumerating the session directory
return [];
}
}
/// <summary>
/// Gets the list of fixed drive letters that are ready.
/// Extracted to avoid repeated expensive DriveInfo.GetDrives() calls.
/// </summary>
private static List<char> GetFixedDriveLetters ()
{
try
{
return [.. DriveInfo.GetDrives()
.Where(d => d.IsReady && d.DriveType == DriveType.Fixed)
.Select(d => d.Name[0])];
}
catch (Exception ex) when (ex is IOException or
UnauthorizedAccessException or
SecurityException or
DriveNotFoundException or
ArgumentNullException)
{
return [];
}
}
/// <summary>
/// Searches for alternative file paths that may correspond to the specified file name, considering common locations
/// such as the project directory, its subdirectories, the user's Documents/LogExpert folder, alternate drive
/// letters, and relative paths from the project directory.
/// </summary>
/// <remarks>This method attempts to locate files that may have been moved, renamed, or exist in typical
/// user or project directories. It ignores errors encountered during directory or file access and does not
/// guarantee that all possible alternative locations are checked. Duplicate paths are excluded from the
/// result.</remarks>
/// <param name="fileName">The name or path of the file to search for. Can be an absolute or relative path. Cannot be null, empty, or
/// whitespace.</param>
/// <param name="sessionFilePath">The full path to the project file used as a reference for searching related directories. Can be null or empty if
/// project context is not available.</param>
/// <param name="sessionDirectories">Pre-enumerated session directory and its immediate subdirectories, shared across all missing files.</param>
/// <param name="cachedDriveLetters">Pre-computed list of fixed drive letters to avoid repeated DriveInfo.GetDrives() calls.</param>
/// <returns>A list of strings containing the full paths of files found that match the specified file name in alternative
/// locations. The list will be empty if no matching files are found.</returns>
private static List<string> FindAlternativePaths (string fileName, string sessionFilePath, List<string> sessionDirectories, List<char> cachedDriveLetters)
{
var alternatives = new List<string>();
if (string.IsNullOrWhiteSpace(fileName))
{
return alternatives;
}
var baseName = Path.GetFileName(fileName);
if (string.IsNullOrWhiteSpace(baseName))
{
return alternatives;
}
// Search in directory of .lxj project file and its immediate subdirectories (pre-enumerated once per session)
alternatives.AddRange(
sessionDirectories
.Select(dir => Path.Join(dir, baseName))
.Where(File.Exists));
// Search in Documents/LogExpert folder
try
{
var documentsPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "LogExpert");
if (Directory.Exists(documentsPath))
{
var docCandidate = Path.Join(documentsPath, baseName);
if (File.Exists(docCandidate) && !alternatives.Contains(docCandidate))
{
alternatives.Add(docCandidate);
}
}
}
catch (Exception ex) when (ex is ArgumentException or
ArgumentNullException or
PathTooLongException or
UnauthorizedAccessException or
IOException)
{
// Ignore errors when searching in Documents folder
}
// If the original path is absolute, try to find the file in the same directory structure
// but on a different drive (useful when drive letters change)
if (Path.IsPathRooted(fileName))
{
try
{
var originalDrive = Path.GetPathRoot(fileName)?[0];
var pathWithoutDrive = fileName.Length > 3 ? fileName[3..] : string.Empty;
foreach (var drive in cachedDriveLetters.Where(drive => drive != originalDrive && !string.IsNullOrEmpty(pathWithoutDrive)))
{
var alternatePath = $"{drive}:\\{pathWithoutDrive}";
if (File.Exists(alternatePath) && !alternatives.Contains(alternatePath))
{
alternatives.Add(alternatePath);
}
}
}
catch (Exception ex) when (ex is ArgumentException or
ArgumentNullException or
PathTooLongException or
UnauthorizedAccessException or
IOException)
{
// Ignore errors when searching on different drives
}
}
// Try relative path resolution from project directory
if (!Path.IsPathRooted(fileName) && !string.IsNullOrWhiteSpace(sessionFilePath))
{
try
{
var sessionDir = Path.GetDirectoryName(sessionFilePath);
if (!string.IsNullOrEmpty(sessionDir))
{
var relativePath = Path.Join(sessionDir, fileName);
var normalizedPath = Path.GetFullPath(relativePath);
if (File.Exists(normalizedPath) && !alternatives.Contains(normalizedPath))
{
alternatives.Add(normalizedPath);
}
}
}
catch (Exception ex) when (ex is ArgumentException or
ArgumentNullException or
PathTooLongException or
UnauthorizedAccessException or
IOException or
NotSupportedException)
{
// Ignore errors with relative path resolution
}
}
return alternatives;
}
}