首页 > 解决方案 > Nestjs - 使用 fastify multipart 上传文件

问题描述

我正在尝试使用 fastify 适配器使用 nestjs 上传多个文件。我可以按照此链接中的教程进行操作-关于上传的文章

现在这是使用 fastify-multipart 完成文件上传的工作,但我无法在上传之前使用请求验证,例如,这是我的规则文件模型(后来我想保存到 postgre)

    import {IsUUID, Length, IsEnum, IsString, Matches, IsOptional} from "class-validator";
    import { FileExtEnum } from "./enums/file-ext.enum";
    import { Updatable } from "./updatable.model";
    import {Expose, Type} from "class-transformer";
    
    export class RuleFile {
      @Expose()
      @IsUUID("4", { always: true })
      id: string;
    
      @Expose()
      @Length(2, 50, {
        always: true,
        each: true,
        context: {
          errorCode: "REQ-000",
          message: `Filename shouldbe within 2 and can reach a max of 50 characters`,
        },
      })
      fileNames: string[];
    
      @Expose()
      @IsEnum(FileExtEnum, { always: true, each: true })
      fileExts: string[];
    
      @IsOptional({each: true, message: 'File is corrupated'})
      @Type(() => Buffer)
      file: Buffer;
    }
    
    export class RuleFileDetail extends RuleFile implements Updatable {
      @IsString()
      @Matches(/[aA]{1}[\w]{6}/)
      recUpdUser: string;
    }

我想验证多部分请求,看看这些设置是否正确。我无法使用基于事件订阅的方法。这是我尝试过的一些事情 - 添加拦截器,以检查请求

    @Injectable()
    export class FileUploadValidationInterceptor implements NestInterceptor {
      intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    
        const req: FastifyRequest = context.switchToHttp().getRequest();
        console.log('inside interceptor', req.body);
        // content type cmes with multipart/form-data;boundary----. we dont need to valdidate the boundary
        // TODO: handle split errors based on semicolon
        const contentType = req.headers['content-type'].split(APP_CONSTANTS.CHAR.SEMI_COLON)[0];
    
        console.log(APP_CONSTANTS.REGEX.MULTIPART_CONTENT_TYPE.test(contentType));
        const isHeaderMultipart = contentType != null?
            this.headerValidation(contentType): this.throwError(contentType);
        
      **// CANNOT check fir req.file() inside this, as it throws undefined**
        return next.handle();
      }
    
      headerValidation(contentType) {
        return APP_CONSTANTS.REGEX.MULTIPART_CONTENT_TYPE.test(contentType) ? true : this.throwError(contentType);
      }
      throwError(contentType: string) {
        throw AppConfigService.getCustomError('FID-HEADERS', `Request header does not contain multipart type: 
        Provided incorrect type - ${contentType}`);
      }
    }

我无法在上面的拦截器中检查 req.file() 。它抛出未定义。我试图遵循fastify-multipart

但是我无法在 fastify-multipart 文档中提供的预处理器中获取请求数据

    fastify.post('/', async function (req, reply) {
      // process a single file
      // also, consider that if you allow to upload multiple files
      // you must consume all files othwise the promise will never fulfill
      const data = await req.file()
     
      data.file // stream
      data.fields // other parsed parts
      data.fieldname
      data.filename
      data.encoding
      data.mimetype
     
      // to accumulate the file in memory! Be careful!
      //
      // await data.toBuffer() // Buffer
      //
      // or
     
      await pump(data.file, fs.createWriteStream(data.filename))

我尝试通过像这样注册我自己的预处理程序钩子来通过(作为 iife 执行)

    (async function bootstrap() {
      const appConfig = AppConfigService.getAppCommonConfig();
      const fastifyInstance = SERVERADAPTERINSTANCE.configureFastifyServer();
      // @ts-ignore
      const fastifyAdapter = new FastifyAdapter(fastifyInstance);
      app = await NestFactory.create<NestFastifyApplication>(
        AppModule,
        fastifyAdapter
      ).catch((err) => {
        console.log("err in creating adapter", err);
        process.exit(1);
      });
    
      .....
      app.useGlobalPipes(
        new ValidationPipe({
          errorHttpStatusCode: 500,
          transform: true,
          validationError: {
            target: true,
            value: true,
          },
          exceptionFactory: (errors: ValidationError[]) => {
            // send it to the global exception filter\
            AppConfigService.validationExceptionFactory(errors);
          },
        }),
    
      );
      
      app.register(require('fastify-multipart'), {
        limits: {
          fieldNameSize: 100, // Max field name size in bytes
          fieldSize: 1000000, // Max field value size in bytes
          fields: 10, // Max number of non-file fields
          fileSize: 100000000000, // For multipart forms, the max file size
          files: 3, // Max number of file fields
          headerPairs: 2000, // Max number of header key=>value pairs
        },
      });
    
      
    
      (app.getHttpAdapter().getInstance() as FastifyInstance).addHook('onRoute', (routeOptions) => {
        console.log('all urls:', routeOptions.url);
    
        if(routeOptions.url.includes('upload')) {

    // The registration actually works, but I cant use the req.file() in the prehandler
          console.log('###########################');
          app.getHttpAdapter().getInstance().addHook('preHandler', FilePrehandlerService.fileHandler);
        }
    
      });
    
      SERVERADAPTERINSTANCE.configureSecurity(app);
    
      //Connect to database
      await SERVERADAPTERINSTANCE.configureDbConn(app);
    
      app.useStaticAssets({
        root: join(__dirname, "..", "public"),
        prefix: "/public/",
      });
      app.setViewEngine({
        engine: {
          handlebars: require("handlebars"),
        },
        templates: join(__dirname, "..", "views"),
      });
    
      await app.listen(appConfig.port, appConfig.host, () => {
        console.log(`Server listening on port - ${appConfig.port}`);
      });
    })();

这是预处理器,

    export class FilePrehandlerService {
      constructor() {}
    
      static fileHandler = async (req, reply) => {
          console.log('coming inside prehandler');
    
              console.log('req is a multipart req',await req.file);
              const data = await req.file();
              console.log('data received -filename:', data.filename);
              console.log('data received- fieldname:', data.fieldname);
              console.log('data received- fields:', data.fields);
    
          return;
      };
    }

这种使用 preHandler 注册和获取文件的模式适用于裸 fastify 应用程序。我尝试过这个

裸 fastify 服务器:

    export class FileController {
        constructor() {}
    
        async testHandler(req: FastifyRequest, reply: FastifyReply) {
            reply.send('test reading dne');
        }
    
        async fileReadHandler(req, reply: FastifyReply) {
            const data = await req.file();
    
            console.log('field val:', data.fields);
            console.log('field filename:', data.filename);
            console.log('field fieldname:', data.fieldname);
            reply.send('done');
        }
    }
    
    export const FILE_CONTROLLER_INSTANCE = new FileController();

这是我的路线文件

    const testRoute: RouteOptions<Server, IncomingMessage, ServerResponse, RouteGenericInterface, unknown> = {
        method: 'GET',
        url: '/test',
        handler: TESTCONTROLLER_INSTANCE.testMethodRouteHandler,
    };
    
    const fileRoute: RouteOptions = {
        method: 'GET',
        url: '/fileTest',
        preHandler: fileInterceptor,
        handler: FILE_CONTROLLER_INSTANCE.testHandler,
    };
    
    const fileUploadRoute: RouteOptions = {
        method: 'POST',
        url: '/fileUpload',
        preHandler: fileInterceptor,
        handler: FILE_CONTROLLER_INSTANCE.fileReadHandler,
    };
    
    const apiRoutes = [testRoute, fileRoute, fileUploadRoute];
    export default apiRoutes;

有人可以让我知道获取字段名的正确方法吗,在 Nestjs 中调用服务之前验证它们

标签: file-uploadnestjsfastify

解决方案


好吧,我做了这样的事情,它对我很有用。也许它也可以为你工作。

// main.ts
import multipart from "fastify-multipart";

const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
);
app.register(multipart);
// upload.guard.ts
import {
    Injectable,
    CanActivate,
    ExecutionContext,
    BadRequestException,
} from "@nestjs/common";
import { FastifyRequest } from "fastify";

@Injectable()
export class UploadGuard implements CanActivate {
    public async canActivate(ctx: ExecutionContext): Promise<boolean> {
        const req = ctx.switchToHttp().getRequest() as FastifyRequest;
        const isMultipart = req.isMultipart();
        if (!isMultipart)
            throw new BadRequestException("multipart/form-data expected.");
        const file = await req.file();
        if (!file) throw new BadRequestException("file expected");
        req.incomingFile = file;
        return true;
    }
}
// file.decorator.ts
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
import { FastifyRequest } from "fastify";

export const File = createParamDecorator(
    (_data: unknown, ctx: ExecutionContext) => {
        const req = ctx.switchToHttp().getRequest() as FastifyRequest;
        const file = req.incomingFile;
        return file
    },
);
// post controller
@Post("upload")
@UseGuards(UploadGuard)
uploadFile(@File() file: Storage.MultipartFile) {
    console.log(file); // logs MultipartFile from "fastify-multipart"
    return "File uploaded"
}

最后是我的打字文件

declare global {
    namespace Storage {
        interface MultipartFile {
            toBuffer: () => Promise<Buffer>;
            file: NodeJS.ReadableStream;
            filepath: string;
            fieldname: string;
            filename: string;
            encoding: string;
            mimetype: string;
            fields: import("fastify-multipart").MultipartFields;
        }
    }
}

declare module "fastify" {
    interface FastifyRequest {
        incomingFile: Storage.MultipartFile;
    }
}

推荐阅读