| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307 |
- import { AssetStorageStrategy, InjectorFn, LocalAssetStorageStrategy, VendureConfig, VendurePlugin } from '@vendure/core';
- import express, { NextFunction, Request, Response } from 'express';
- import { Server } from 'http';
- import proxy from 'http-proxy-middleware';
- import path from 'path';
- import { SharpAssetPreviewStrategy } from './sharp-asset-preview-strategy';
- import { transformImage } from './transform-image';
- /**
- * @description
- * Specifies the way in which an asset preview image will be resized to fit in the
- * proscribed dimensions:
- *
- * * crop: crops the image to cover both provided dimensions
- * * resize: Preserving aspect ratio, resizes the image to be as large as possible
- * while ensuring its dimensions are less than or equal to both those specified.
- *
- * @docsCategory AssetServerPlugin
- */
- export type ImageTransformMode = 'crop' | 'resize';
- /**
- * @description
- * A configuration option for an image size preset for the AssetServerPlugin.
- *
- * Presets allow a shorthand way to generate a thumbnail preview of an asset. For example,
- * the built-in "tiny" preset generates a 50px x 50px cropped preview, which can be accessed
- * by appending the string `preset=tiny` to the asset url:
- *
- * `http://localhost:3000/assets/some-asset.jpg?preset=tiny`
- *
- * is equivalent to:
- *
- * `http://localhost:3000/assets/some-asset.jpg?w=50&h=50&mode=crop`
- *
- * @docsCategory AssetServerPlugin
- */
- export interface ImageTransformPreset {
- name: string;
- width: number;
- height: number;
- mode: ImageTransformMode;
- }
- /**
- * @description
- * The configuration options for the AssetServerPlugin.
- *
- * @docsCategory AssetServerPlugin
- */
- export interface AssetServerOptions {
- hostname?: string;
- /**
- * @description
- * The local port that the server will run on. Note that the AssetServerPlugin
- * includes a proxy server which allows the asset server to be accessed on the same
- * port as the main Vendure server.
- */
- port: number;
- /**
- * @description
- * The proxy route to the asset server.
- */
- route: string;
- /**
- * @description
- * The local directory to which assets will be uploaded.
- */
- assetUploadDir: string;
- /**
- * @description
- * The max width in pixels of a generated preview image.
- *
- * @default 1600
- */
- previewMaxWidth?: number;
- /**
- * @description
- * The max height in pixels of a generated preview image.
- *
- * @default 1600
- */
- previewMaxHeight?: number;
- /**
- * @description
- * An array of additional {@link ImageTransformPreset} objects.
- */
- presets?: ImageTransformPreset[];
- }
- /**
- * @description
- * The `AssetServerPlugin` serves assets (images and other files) from the local file system. It can also perform on-the-fly image transformations
- * and caches the results for subsequent calls.
- *
- * @example
- * ```ts
- * const config: VendureConfig = {
- * // Add an instance of the plugin to the plugins array
- * plugins: [
- * new AssetServerPlugin({
- * route: 'assets',
- * assetUploadDir: path.join(__dirname, 'assets'),
- * port: 4000,
- * }),
- * ],
- * };
- * ```
- *
- * The full configuration is documented at [AssetServerOptions]({{< relref "asset-server-options" >}})
- *
- * ## Image transformation
- *
- * Asset preview images can be transformed (resized & cropped) on the fly by appending query parameters to the url:
- *
- * `http://localhost:3000/assets/some-asset.jpg?w=500&h=300&mode=resize`
- *
- * The above URL will return `some-asset.jpg`, resized to fit in the bounds of a 500px x 300px rectangle.
- *
- * ### Preview mode
- *
- * The `mode` parameter can be either `crop` or `resize`. See the [ImageTransformMode]({{< relref "image-transform-mode" >}}) docs for details.
- *
- * ### Transform presets
- *
- * Presets can be defined which allow a single preset name to be used instead of specifying the width, height and mode. Presets are
- * configured via the AssetServerOptions [presets property]({{< relref "asset-server-options" >}}#presets).
- *
- * For example, defining the following preset:
- *
- * ```ts
- * new AssetServerPlugin({
- * // ...
- * presets: [
- * { name: 'my-preset', width: 85, height: 85, mode: 'crop' },
- * ],
- * }),
- * ```
- *
- * means that a request to:
- *
- * `http://localhost:3000/assets/some-asset.jpg?preset=my-preset`
- *
- * is equivalent to:
- *
- * `http://localhost:3000/assets/some-asset.jpg?w=85&h=85&mode=crop`
- *
- * The AssetServerPlugin comes pre-configured with the following presets:
- *
- * name | width | height | mode
- * -----|-------|--------|-----
- * tiny | 50px | 50px | crop
- * thumb | 150px | 150px | crop
- * small | 300px | 300px | resize
- * medium | 500px | 500px | resize
- * large | 800px | 800px | resize
- *
- * @docsCategory AssetServerPlugin
- */
- export class AssetServerPlugin implements VendurePlugin {
- private server: Server;
- private assetStorage: AssetStorageStrategy;
- private readonly cacheDir = 'cache';
- private readonly presets: ImageTransformPreset[] = [
- { name: 'tiny', width: 50, height: 50, mode: 'crop' },
- { name: 'thumb', width: 150, height: 150, mode: 'crop' },
- { name: 'small', width: 300, height: 300, mode: 'resize' },
- { name: 'medium', width: 500, height: 500, mode: 'resize' },
- { name: 'large', width: 800, height: 800, mode: 'resize' },
- ];
- constructor(private options: AssetServerOptions) {
- if (options.presets) {
- for (const preset of options.presets) {
- const existingIndex = this.presets.findIndex(p => p.name === preset.name);
- if (-1 < existingIndex) {
- this.presets.splice(existingIndex, 1, preset);
- } else {
- this.presets.push(preset);
- }
- }
- }
- }
- configure(config: Required<VendureConfig>) {
- this.assetStorage = this.createAssetStorageStrategy();
- config.assetOptions.assetPreviewStrategy = new SharpAssetPreviewStrategy({
- maxWidth: this.options.previewMaxWidth || 1600,
- maxHeight: this.options.previewMaxHeight || 1600,
- });
- config.assetOptions.assetStorageStrategy = this.assetStorage;
- config.middleware.push({
- handler: createProxyHandler(this.options, !config.silent),
- route: this.options.route,
- });
- return config;
- }
- onBootstrap(inject: InjectorFn): void | Promise<void> {
- this.createAssetServer();
- }
- onClose(): Promise<void> {
- return new Promise(resolve => { this.server.close(() => resolve()); });
- }
- private createAssetStorageStrategy() {
- const toAbsoluteUrlFn = (request: Request, identifier: string): string => {
- if (!identifier) {
- return '';
- }
- return `${request.protocol}://${request.get('host')}/${this.options.route}/${identifier}`;
- };
- return new LocalAssetStorageStrategy(this.options.assetUploadDir, toAbsoluteUrlFn);
- }
- /**
- * Creates the image server instance
- */
- private createAssetServer() {
- const assetServer = express();
- assetServer.use(this.serveStaticFile(), this.generateTransformedImage());
- this.server = assetServer.listen(this.options.port);
- }
- /**
- * Sends the file requested to the broswer.
- */
- private serveStaticFile() {
- return (req: Request, res: Response) => {
- const filePath = path.join(this.options.assetUploadDir, this.getFileNameFromRequest(req));
- res.sendFile(filePath);
- };
- }
- /**
- * If an exception was thrown by the first handler, then it may be because a transformed image
- * is being requested which does not yet exist. In this case, this handler will generate the
- * transformed image, save it to cache, and serve the result as a response.
- */
- private generateTransformedImage() {
- return async (err: any, req: Request, res: Response, next: NextFunction) => {
- if (err && err.status === 404) {
- if (req.query) {
- let file: Buffer;
- try {
- file = await this.assetStorage.readFileToBuffer(req.path);
- } catch (err) {
- res.status(404).send('Resource not found');
- return;
- }
- const image = await transformImage(file, req.query, this.presets || []);
- const imageBuffer = await image.toBuffer();
- const cachedFileName = this.getFileNameFromRequest(req);
- await this.assetStorage.writeFileFromBuffer(cachedFileName, imageBuffer);
- res.set('Content-Type', `image/${(await image.metadata()).format}`);
- res.send(imageBuffer);
- }
- }
- next();
- };
- }
- private getFileNameFromRequest(req: Request): string {
- if (req.query.w || req.query.h) {
- const width = req.query.w || '';
- const height = req.query.h || '';
- const mode = req.query.mode || '';
- return this.cacheDir + '/' + this.addSuffix(req.path, `_transform_w${width}_h${height}_m${mode}`);
- } else if (req.query.preset) {
- if (this.presets && !!this.presets.find(p => p.name === req.query.preset)) {
- return this.cacheDir + '/' + this.addSuffix(req.path, `_transform_pre_${req.query.preset}`);
- }
- }
- return req.path;
- }
- private addSuffix(fileName: string, suffix: string): string {
- const ext = path.extname(fileName);
- const baseName = path.basename(fileName, ext);
- return `${baseName}${suffix}${ext}`;
- }
- }
- export interface ProxyOptions {
- route: string;
- port: number;
- hostname?: string;
- }
- /**
- * Configures the proxy middleware which will be passed to the main Vendure server. This
- * will proxy all asset requests to the dedicated asset server.
- */
- function createProxyHandler(options: ProxyOptions, logging: boolean) {
- const route = options.route.charAt(0) === '/' ? options.route : '/' + options.route;
- const proxyHostname = options.hostname || 'localhost';
- return proxy({
- // TODO: how do we detect https?
- target: `http://${proxyHostname}:${options.port}`,
- pathRewrite: {
- [`^${route}`]: `/`,
- },
- logLevel: logging ? 'info' : 'silent',
- });
- }
|