/* |-------------------------------------------------------------------------- | Provider File - node ace make:provider vinejsProvider |-------------------------------------------------------------------------- |*/ import type { ApplicationService } from '@adonisjs/core/types'; import vine, { symbols, BaseLiteralType, Vine } from '@vinejs/vine'; import type { FieldContext, FieldOptions } from '@vinejs/vine/types'; import type { MultipartFile } from '@adonisjs/core/bodyparser'; import type { FileValidationOptions } from '@adonisjs/core/types/bodyparser'; import { Request, RequestValidator } from '@adonisjs/core/http'; import MimeType from '#models/mime_type'; /** * Validation options accepted by the "file" rule */ export type FileRuleValidationOptions = Partial | ((field: FieldContext) => Partial); /** * Extend VineJS */ declare module '@vinejs/vine' { interface Vine { myfile(options?: FileRuleValidationOptions): VineMultipartFile; } } /** * Extend HTTP request class */ declare module '@adonisjs/core/http' { interface Request extends RequestValidator {} } /** * Checks if the value is an instance of multipart file * from bodyparser. */ export function isBodyParserFile(file: MultipartFile | unknown): file is MultipartFile { return !!(file && typeof file === 'object' && 'isMultipartFile' in file); } /** * Cache for enabled extensions to reduce database queries */ let extensionsCache: string[] | null = null; let cacheTimestamp = 0; const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes /** * Get enabled extensions with caching */ export async function getEnabledExtensions(): Promise { const now = Date.now(); if (extensionsCache && now - cacheTimestamp < CACHE_DURATION) { return extensionsCache; } try { const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec(); const extensions = enabledExtensions .map((extension) => extension.file_extension.split('|')) .flat() .map((ext) => ext.toLowerCase().trim()) .filter((ext) => ext.length > 0); extensionsCache = [...new Set(extensions)]; // Remove duplicates cacheTimestamp = now; return extensionsCache; } catch (error) { console.error('Error fetching enabled extensions:', error); return extensionsCache || []; } } /** * Clear extensions cache */ export function clearExtensionsCache(): void { extensionsCache = null; cacheTimestamp = 0; } /** * VineJS validation rule that validates the file to be an * instance of BodyParser MultipartFile class. */ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, options: FileRuleValidationOptions, field: FieldContext) => { /** * Report error when value is not a field multipart * file object */ if (!isBodyParserFile(file)) { field.report('The {{ field }} must be a file', 'file', field); return; } // At this point, you can use type assertion to explicitly tell TypeScript that file is of type MultipartFile const validatedFile = file as MultipartFile; const validationOptions = typeof options === 'function' ? options(field) : options; /** * Set size when it's defined in the options and missing * on the file instance */ if (validatedFile.sizeLimit === undefined && validationOptions.size) { validatedFile.sizeLimit = validationOptions.size; } /** * Set extensions when it's defined in the options and missing * on the file instance */ if (validatedFile.allowedExtensions === undefined) { if (validationOptions.extnames !== undefined) { validatedFile.allowedExtensions = validationOptions.extnames; } else { validatedFile.allowedExtensions = await getEnabledExtensions(); } } /** * Validate file */ try { validatedFile.validate(); } catch (error) { field.report(`File validation failed: ${error.message}`, 'file.validation_error', field, validationOptions); return; } /** * Report errors */ validatedFile.errors.forEach((error) => { field.report(error.message, `file.${error.type}`, field, validationOptions); }); }); const MULTIPART_FILE: typeof symbols.SUBTYPE = symbols.SUBTYPE; export class VineMultipartFile extends BaseLiteralType { [MULTIPART_FILE]: string; public validationOptions?: FileRuleValidationOptions; // extnames: (18) ['gpkg', 'htm', 'html', 'csv', 'txt', 'asc', 'c', 'cc', 'h', 'srt', 'tiff', 'pdf', 'png', 'zip', 'jpg', 'jpeg', 'jpe', 'xlsx'] // size: '512mb' public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions) { super(options, [isMultipartFile(validationOptions || {})]); this.validationOptions = validationOptions; } public clone(): any { return new VineMultipartFile(this.validationOptions, this.cloneOptions()); } /** * Set maximum file size */ public maxSize(size: string | number): this { const newOptions = { ...this.validationOptions, size }; return new VineMultipartFile(newOptions, this.cloneOptions()) as this; } /** * Set allowed extensions */ public extensions(extnames: string[]): this { const newOptions = { ...this.validationOptions, extnames }; return new VineMultipartFile(newOptions, this.cloneOptions()) as this; } } export default class VinejsProvider { protected app: ApplicationService; constructor(app: ApplicationService) { this.app = app; this.app.usingVineJS = true; } /** * Register bindings to the container */ register() {} /** * The container bindings have booted */ boot(): void { Vine.macro('myfile', function (this: Vine, options?: FileRuleValidationOptions) { return new VineMultipartFile(options); }); /** * The validate method can be used to validate the request * data for the current request using VineJS validators */ Request.macro('validateUsing', function (this: Request, ...args) { if (!this.ctx) { throw new Error('HttpContext is not available'); } return new RequestValidator(this.ctx).validateUsing(...args); }); // Ensure MIME validation macros are loaded this.loadMimeValidationMacros(); this.loadFileScanMacros(); this.loadFileLengthMacros(); } /** * Load MIME validation macros - called during boot to ensure they're available */ private async loadMimeValidationMacros(): Promise { try { // Dynamically import the MIME validation rule to ensure macros are registered await import('#start/rules/allowed_extensions_mimetypes'); } catch (error) { console.warn('Could not load MIME validation macros:', error); } } private async loadFileScanMacros(): Promise { try { // Dynamically import the MIME validation rule to ensure macros are registered await import('#start/rules/file_scan'); } catch (error) { console.warn('Could not load MIME validation macros:', error); } } private async loadFileLengthMacros(): Promise { try { // Dynamically import the MIME validation rule to ensure macros are registered await import('#start/rules/file_length'); } catch (error) { console.warn('Could not load MIME validation macros:', error); } } /** * The application has been booted */ async start() {} /** * The process has been started */ async ready() {} /** * Preparing to shutdown the app */ async shutdown() { clearExtensionsCache(); } }