default-asset-server-plugin.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import * as express from 'express';
  2. import { NextFunction, Request, Response } from 'express';
  3. import * as proxy from 'http-proxy-middleware';
  4. import * as path from 'path';
  5. import { AssetStorageStrategy } from '../../config/asset-storage-strategy/asset-storage-strategy';
  6. import { VendureConfig } from '../../config/vendure-config';
  7. import { VendurePlugin } from '../../config/vendure-plugin/vendure-plugin';
  8. import { DefaultAssetPreviewStrategy } from './default-asset-preview-strategy';
  9. import { DefaultAssetStorageStrategy } from './default-asset-storage-strategy';
  10. import { transformImage } from './transform-image';
  11. export type ImageTransformMode = 'crop' | 'resize';
  12. export interface ImageTransformPreset {
  13. name: string;
  14. width: number;
  15. height: number;
  16. mode: ImageTransformMode;
  17. }
  18. export interface DefaultAssetServerOptions {
  19. hostname: string;
  20. port: number;
  21. route: string;
  22. assetUploadDir: string;
  23. previewMaxWidth: number;
  24. previewMaxHeight: number;
  25. presets?: ImageTransformPreset[];
  26. }
  27. /**
  28. * The DefaultAssetServerPlugin instantiates a static Express server which is used to
  29. * serve the assets. It can also perform on-the-fly image transformations and caches the
  30. * results for subsequent calls.
  31. */
  32. export class DefaultAssetServerPlugin implements VendurePlugin {
  33. private assetStorage: AssetStorageStrategy;
  34. private readonly cacheDir = 'cache';
  35. constructor(private options: DefaultAssetServerOptions) {}
  36. init(config: Required<VendureConfig>) {
  37. this.createAssetServer();
  38. this.assetStorage = new DefaultAssetStorageStrategy(this.options.assetUploadDir, this.options.route);
  39. config.assetOptions.assetPreviewStrategy = new DefaultAssetPreviewStrategy({
  40. maxWidth: this.options.previewMaxWidth,
  41. maxHeight: this.options.previewMaxHeight,
  42. });
  43. config.assetOptions.assetStorageStrategy = this.assetStorage;
  44. config.middleware.push({
  45. handler: this.createProxyHandler(),
  46. route: this.options.route,
  47. });
  48. return config;
  49. }
  50. /**
  51. * Creates the image server instance
  52. */
  53. private createAssetServer() {
  54. const assetServer = express();
  55. assetServer.use(this.serveStaticFile(), this.generateTransformedImage());
  56. assetServer.listen(this.options.port);
  57. }
  58. /**
  59. * Sends the file requested to the broswer.
  60. */
  61. private serveStaticFile() {
  62. return (req: Request, res: Response) => {
  63. const filePath = path.join(this.options.assetUploadDir, this.getFileNameFromRequest(req));
  64. res.sendFile(filePath);
  65. };
  66. }
  67. /**
  68. * If an exception was thrown by the first handler, then it may be because a transformed image
  69. * is being requested which does not yet exist. In this case, this handler will generate the
  70. * transformed image, save it to cache, and serve the result as a response.
  71. */
  72. private generateTransformedImage() {
  73. return async (err, req: Request, res: Response, next: NextFunction) => {
  74. if (err && err.status === 404) {
  75. if (req.query) {
  76. const file = await this.assetStorage.readFileToBuffer(req.path);
  77. const image = await transformImage(file, req.query, this.options.presets || []);
  78. const imageBuffer = await image.toBuffer();
  79. const cachedFileName = this.getFileNameFromRequest(req);
  80. await this.assetStorage.writeFileFromBuffer(cachedFileName, imageBuffer);
  81. res.set('Content-Type', `image/${(await image.metadata()).format}`);
  82. res.send(imageBuffer);
  83. }
  84. }
  85. next();
  86. };
  87. }
  88. private getFileNameFromRequest(req: Request): string {
  89. if (req.query.w || req.query.h) {
  90. const width = req.query.w || '';
  91. const height = req.query.h || '';
  92. const mode = req.query.mode || '';
  93. return this.cacheDir + '/' + this.addSuffix(req.path, `_transform_w${width}_h${height}_m${mode}`);
  94. } else if (req.query.preset) {
  95. if (this.options.presets && !!this.options.presets.find(p => p.name === req.query.preset)) {
  96. return this.cacheDir + '/' + this.addSuffix(req.path, `_transform_pre_${req.query.preset}`);
  97. }
  98. }
  99. return req.path;
  100. }
  101. /**
  102. * Configures the proxy middleware which will be passed to the main Vendure server. This
  103. * will proxy all asset requests to the dedicated asset server.
  104. */
  105. private createProxyHandler() {
  106. const route = this.options.route.charAt(0) === '/' ? this.options.route : '/' + this.options.route;
  107. return proxy({
  108. target: `${this.options.hostname}:${this.options.port}`,
  109. pathRewrite: {
  110. [`^${route}`]: '/',
  111. },
  112. });
  113. }
  114. private addSuffix(fileName: string, suffix: string): string {
  115. const ext = path.extname(fileName);
  116. const baseName = path.basename(fileName, ext);
  117. return `${baseName}${suffix}${ext}`;
  118. }
  119. }