Case-Sensitivity Migration Review: Windows to Linux
This document summarizes the observations and suggestions regarding the case-sensitivity file access issue when migrating the ERP Crystal MFG system from Windows to Linux, based on the provided shared links and a verification of the local codebase.
1. Observations from the Shared Links
Link 1: ChatGPT Chat
The ChatGPT discussion addresses the core issue: Windows (NTFS) is case-insensitive, while Linux (ext4/xfs) is case-sensitive. ChatGPT proposed several mitigation strategies:
- Option 1 & 6 (Standardization): Standardize all filenames and folder names (e.g., all lowercase) and update references.
- Option 2 & 3 (Case-Insensitive Resolver): Programmatically search for a matching file case-insensitively using
Directory.GetFilesandStringComparison.OrdinalIgnoreCase. - Option 4 (Startup Caching): Load all filenames into a case-insensitive
Dictionaryat application startup. - Option 5 (Middleware): Use HTTP middleware to rewrite static asset request paths.
- Option 7 (Git Casing): Force Git to track case-only changes (using
git mv).
Link 2: MDShare Document
The MDShare link presents the selected recommendation and implementation scope:
- Go with Option 2 (Case-insensitive resolver in
FilePathService) + fix theNormalizePathbug. - Rationale:
- Minimal code impact (avoids rewriting ~400 file I/O calls).
- Safely handles QuestPDF PNG assets (e.g.,
Invoice_Header.png), which are placed at deployment time rather than checked into the repository. - Offers a natural transition path to standardized lowercase naming.
- Zero startup overhead compared to startup indexing.
- Proposed Action Items:
- Implement
ResolvePath()inFilePathService.csin both the API and Web projects. - Fix the
NormalizePathbug in the Web project. - Update the ~25 QuestPDF controller references to utilize
ResolvePath().
- Implement
2. Codebase Verification & Observations
A. The NormalizePath Bug in Blazor Web
A review of the codebase confirms that NormalizePath is indeed broken on Linux for the Blazor Web application.
In ErpCrystal_MFG.Api/Services/FilePathService.cs (L62-76):
public string NormalizePath(string? path)
{
if (string.IsNullOrEmpty(path))
return path ?? string.Empty;
// Use forward slashes by default or as configured
var normalized = path.Replace("\\", "/");
// Only force backslashes if explicitly requested (usually Windows dev)
if (!_useForwardSlashes && OperatingSystem.IsWindows())
{
normalized = normalized.Replace("/", "\\");
}
return normalized;
}In ErpCrystal_MFG.Web/Services/FilePathService.cs (L59-71):
public string NormalizePath(string? path)
{
if (string.IsNullOrEmpty(path))
return path ?? string.Empty;
var normalized = path.Replace("\\", "/");
if (!_useForwardSlashes)
{
// WARNING: This forces backslashes on Linux too if _useForwardSlashes is false!
normalized = normalized.Replace("/", "\\");
}
return normalized;
}Important
Because the Web version lacks the OperatingSystem.IsWindows() check, any server running the Blazor Web app on Linux with default settings will convert all / path separators into \\. On Linux, backslashes are treated as part of the filename itself, rather than directory separators, causing all file lookups to fail.
B. Direct Path.Combine and File.Exists Checks
A search through controllers and Razor pages shows that paths are frequently combined using Path.Combine outside of the file path service, and then checked directly:
- In API Controllers (e.g., InvoiceController.cs):
And QuestPDF immediately loads it:
var headerImg = Path.Combine(pathFileName1, $"Invoice_Header{unitSuffix}{specialSuffix}.png"); var footerImg = Path.Combine(pathFileName1, $"Invoice_Footer{unitSuffix}.png");byte[] imageData = File.ReadAllBytes(Model.HeaderImg); - In Web Razor Pages (e.g., DncnDetails.razor):
_DncnPrint.HeaderImg = Path.Combine(pathFileName, Dncn_Header); if(!File.Exists(_DncnPrint.HeaderImg) || !File.Exists(_DncnPrint.FooterImg))
Because these paths are constructed on-the-fly and checked via direct File operations, standardizing file accesses through a ResolvePath service method is the most robust and minimally invasive approach.
3. Suggestions & Recommendations
Recommendation 1: Implement ResolvePath in FilePathService
We suggest adding ResolvePath directly to IFilePathService in both projects. The implementation should normalize the path, check if the file exists as-is (for optimal performance), and fallback to a case-insensitive search if it doesn’t.
Interface definition:
string ResolvePath(string? path);Service Implementation:
public string ResolvePath(string? path)
{
if (string.IsNullOrEmpty(path))
return string.Empty;
var normalized = NormalizePath(path);
// If file or directory exists under the exact casing, return it immediately (fast path)
if (File.Exists(normalized) || Directory.Exists(normalized))
return normalized;
var directory = Path.GetDirectoryName(normalized);
var fileName = Path.GetFileName(normalized);
if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory))
return normalized; // Fallback to normalized path if directory does not exist
// Case-insensitive search on files in the target directory
var matchedFile = Directory.GetFiles(directory)
.FirstOrDefault(f => string.Equals(Path.GetFileName(f), fileName, StringComparison.OrdinalIgnoreCase));
if (matchedFile != null)
{
return NormalizePath(matchedFile);
}
return normalized; // Fallback to normalized path if no match found
}Recommendation 2: Correct the NormalizePath Bug in Web
Update NormalizePath in the Web project’s FilePathService.cs to match the API project’s check:
if (!_useForwardSlashes && OperatingSystem.IsWindows())
{
normalized = normalized.Replace("/", "\\");
}Recommendation 3: Apply ResolvePath at Path Construction Sites
Wherever path constructions for dynamic assets (such as QuestPDF headers/footers) are completed, wrap them with _IFilePathService.ResolvePath().
For example, in InvoiceController.cs:
// Before:
var headerImg = Path.Combine(pathFileName1, $"Invoice_Header{unitSuffix}{specialSuffix}.png");
// After:
var headerImg = _IFilePathService.ResolvePath(Path.Combine(pathFileName1, $"Invoice_Header{unitSuffix}{specialSuffix}.png"));This ensures that:
File.Exists(headerImg)checks inside Blazor pages succeed.File.ReadAllBytes(Model.HeaderImg)calls inside QuestPDF components load files successfully under case-sensitive Linux file systems.- No complex recursive path resolution is required since directory casing is already standard.