- Updated docker-entrypoint.sh to improve ClamAV service initialization and logging. - Added checks for ClamAV and freshclam daemon status. - Optimized freshclam configuration for container usage, including logging to stdout and setting database directory. - Introduced caching mechanism for enabled file extensions in vinejs_provider.ts to reduce database queries. - Implemented a new command to list datasets needing DataCite DOI updates, with options for verbose output, count only, and IDs only. - Updated package dependencies to include p-limit and pino-pretty. - finalized ace command 'detect:missing-cross-references'
262 lines
8 KiB
TypeScript
262 lines
8 KiB
TypeScript
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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<FileValidationOptions> | ((field: FieldContext) => Partial<FileValidationOptions>);
|
|
|
|
/**
|
|
* 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<string[]> {
|
|
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<MultipartFile, MultipartFile, MultipartFile> {
|
|
[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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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();
|
|
}
|
|
}
|