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:
@@ -0,0 +1,59 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace PowderCoating.Application.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Shared file validation and content-type resolution used across all blob storage services.
|
||||
/// </summary>
|
||||
public static class BlobFileHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates an uploaded file against an extension allowlist and a maximum size.
|
||||
/// Returns the normalized (lowercase) extension on success so callers do not re-derive it.
|
||||
/// </summary>
|
||||
public static (bool IsValid, string Extension, string Error) ValidateUpload(
|
||||
IFormFile? file,
|
||||
string[] allowedExtensions,
|
||||
long maxBytes)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
return (false, string.Empty, "No file provided.");
|
||||
|
||||
if (file.Length > maxBytes)
|
||||
return (false, string.Empty, $"File exceeds the {maxBytes / 1024 / 1024} MB limit.");
|
||||
|
||||
var extension = Path.GetExtension(file.FileName).ToLowerInvariant();
|
||||
if (string.IsNullOrEmpty(extension) || !allowedExtensions.Contains(extension))
|
||||
return (false, string.Empty, $"File type not allowed. Allowed: {string.Join(", ", allowedExtensions)}.");
|
||||
|
||||
return (true, extension, string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a file extension to its MIME content type, covering common image formats and
|
||||
/// document types. Falls back to <c>application/octet-stream</c>.
|
||||
/// </summary>
|
||||
public static string GetContentType(string extension) => extension switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".png" => "image/png",
|
||||
".gif" => "image/gif",
|
||||
".webp" => "image/webp",
|
||||
".svg" => "image/svg+xml",
|
||||
".pdf" => "application/pdf",
|
||||
".doc" => "application/msword",
|
||||
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".txt" => "text/plain",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Strips OS-invalid filename characters from a base filename (no extension), replacing
|
||||
/// them with underscores to produce a safe blob path segment.
|
||||
/// </summary>
|
||||
public static string SanitizeFileName(string fileName)
|
||||
{
|
||||
var sanitized = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars()));
|
||||
return string.IsNullOrWhiteSpace(sanitized) ? "file" : sanitized;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user