Refactor: extract shared helpers, fix field drift, add assembly services
- IJobItemAssemblyService / IQuotePricingAssemblyService: centralize job item and quote pricing construction that was duplicated across create, rework copy, and quote-to-job conversion paths - BlobFileHelper: single ValidateUpload/GetContentType/SanitizeFileName used by 6 blob services (JobPhoto, QuotePhoto, ProfilePhoto, CompanyLogo, Equipment, Catalog) and BillsController + ExpensesController, removing 8 private copies - PagedResult<T>.From(): static factory eliminates 6-line boilerplate in 11 controllers (Appointments, Customers, Equipment, Inventory, Invoices, Jobs, Maintenance, CompanyUsers, PlatformUsers, Quotes, Vendors) - AccountingDropdownHelper: single LoadAsync() call replaces duplicate vendor/account/job queries in BillsController and ExpensesController - JobTemplateItem: add IsSalesItem + Sku fields with migration; propagate through JobTemplatesController snapshot copy and GetTemplatesJson projection, and JobsController template-application path - Test assertions updated for standardized BlobFileHelper error messages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,25 +56,16 @@ public class EquipmentManualService : IEquipmentManualService
|
||||
/// </returns>
|
||||
public async Task<(bool Success, string FilePath, string ErrorMessage)> SaveEquipmentManualAsync(IFormFile file, int companyId, int equipmentId)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return (false, string.Empty, "No file provided");
|
||||
|
||||
if (file.Length > MaxFileSize)
|
||||
return (false, string.Empty, $"File size exceeds maximum allowed size of {MaxFileSize / 1024 / 1024} MB");
|
||||
|
||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
if (!AllowedExtensions.Contains(extension))
|
||||
return (false, string.Empty, $"File type not allowed. Allowed types: {string.Join(", ", AllowedExtensions)}");
|
||||
var (isValid, extension, error) = BlobFileHelper.ValidateUpload(file, AllowedExtensions, MaxFileSize);
|
||||
if (!isValid)
|
||||
return (false, string.Empty, error);
|
||||
|
||||
// Sanitize filename — replace OS-invalid characters with underscores to
|
||||
// prevent path traversal and blob naming errors in Azure.
|
||||
var fileName = Path.GetFileNameWithoutExtension(file.FileName);
|
||||
fileName = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars()));
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
fileName = "manual";
|
||||
var fileName = BlobFileHelper.SanitizeFileName(Path.GetFileNameWithoutExtension(file.FileName));
|
||||
|
||||
var blobName = $"{companyId}/equipment-manuals/{equipmentId}/{fileName}{extension}";
|
||||
var contentType = GetContentType(extension);
|
||||
var contentType = BlobFileHelper.GetContentType(extension);
|
||||
|
||||
using var stream = file.OpenReadStream();
|
||||
var result = await _blobService.UploadAsync(_settings.Containers.Manuals, blobName, stream, contentType);
|
||||
@@ -130,19 +121,4 @@ public class EquipmentManualService : IEquipmentManualService
|
||||
return await _blobService.ExistsAsync(_settings.Containers.Manuals, filePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a lowercase file extension to its canonical MIME content type.
|
||||
/// Correct MIME types are required so browsers open PDFs inline and
|
||||
/// Word documents prompt a compatible application rather than a raw download.
|
||||
/// </summary>
|
||||
/// <param name="extension">Lowercase file extension including the leading dot.</param>
|
||||
/// <returns>MIME type string, or <c>application/octet-stream</c> as a safe fallback.</returns>
|
||||
private static string GetContentType(string extension) => extension switch
|
||||
{
|
||||
".pdf" => "application/pdf",
|
||||
".doc" => "application/msword",
|
||||
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".txt" => "text/plain",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user