@deenruv/asset-server-plugin 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +103 -0
  3. package/lib/index.d.ts +4 -0
  4. package/lib/index.js +21 -0
  5. package/lib/index.js.map +1 -0
  6. package/lib/src/common.d.ts +4 -0
  7. package/lib/src/common.js +42 -0
  8. package/lib/src/common.js.map +1 -0
  9. package/lib/src/constants.d.ts +2 -0
  10. package/lib/src/constants.js +6 -0
  11. package/lib/src/constants.js.map +1 -0
  12. package/lib/src/default-asset-storage-strategy-factory.d.ts +6 -0
  13. package/lib/src/default-asset-storage-strategy-factory.js +24 -0
  14. package/lib/src/default-asset-storage-strategy-factory.js.map +1 -0
  15. package/lib/src/file-icon.png +0 -0
  16. package/lib/src/hashed-asset-naming-strategy.d.ts +21 -0
  17. package/lib/src/hashed-asset-naming-strategy.js +39 -0
  18. package/lib/src/hashed-asset-naming-strategy.js.map +1 -0
  19. package/lib/src/local-asset-storage-strategy.d.ts +28 -0
  20. package/lib/src/local-asset-storage-strategy.js +68 -0
  21. package/lib/src/local-asset-storage-strategy.js.map +1 -0
  22. package/lib/src/plugin.d.ts +167 -0
  23. package/lib/src/plugin.js +394 -0
  24. package/lib/src/plugin.js.map +1 -0
  25. package/lib/src/s3-asset-storage-strategy.d.ts +159 -0
  26. package/lib/src/s3-asset-storage-strategy.js +289 -0
  27. package/lib/src/s3-asset-storage-strategy.js.map +1 -0
  28. package/lib/src/sharp-asset-preview-strategy.d.ts +99 -0
  29. package/lib/src/sharp-asset-preview-strategy.js +121 -0
  30. package/lib/src/sharp-asset-preview-strategy.js.map +1 -0
  31. package/lib/src/transform-image.d.ts +25 -0
  32. package/lib/src/transform-image.js +145 -0
  33. package/lib/src/transform-image.js.map +1 -0
  34. package/lib/src/types.d.ts +137 -0
  35. package/lib/src/types.js +3 -0
  36. package/lib/src/types.js.map +1 -0
  37. package/package.json +47 -0
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ # License 1
2
+
3
+ The MIT License
4
+
5
+ Copyright (c) 2025-present Aexol
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10
+
11
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
12
+
13
+ # License 2
14
+
15
+ The MIT License
16
+
17
+ Copyright (c) 2018-2025 Michael Bromley
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @deenruv/asset-server-plugin
2
+
3
+ Serves assets (images and other files) from the local file system or cloud storage (e.g. S3), with on-the-fly image transformation (resize, crop, format conversion) and result caching.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @deenruv/asset-server-plugin
9
+ ```
10
+
11
+ ## Configuration
12
+
13
+ ```typescript
14
+ import { AssetServerPlugin } from '@deenruv/asset-server-plugin';
15
+ import path from 'path';
16
+
17
+ const config = {
18
+ plugins: [
19
+ AssetServerPlugin.init({
20
+ route: 'assets',
21
+ assetUploadDir: path.join(__dirname, 'assets'),
22
+ }),
23
+ ],
24
+ };
25
+ ```
26
+
27
+ **Options:**
28
+
29
+ | Option | Type | Default | Description |
30
+ |--------|------|---------|-------------|
31
+ | `route` | `string` | *required* | URL route for serving assets (e.g. `'assets'`) |
32
+ | `assetUploadDir` | `string` | *required* | Local directory for asset storage |
33
+ | `assetUrlPrefix` | `string \| fn` | auto-detected | Complete URL prefix for asset files |
34
+ | `presets` | `ImageTransformPreset[]` | See below | Named image transform presets |
35
+ | `namingStrategy` | `AssetNamingStrategy` | `HashedAssetNamingStrategy` | Strategy for naming uploaded assets |
36
+ | `previewStrategy` | `AssetPreviewStrategy` | `SharpAssetPreviewStrategy` | Strategy for generating preview images |
37
+ | `storageStrategyFactory` | `fn` | `LocalAssetStorageStrategy` | Factory for custom storage backends (e.g. S3) |
38
+ | `cacheHeader` | `string \| CacheConfig` | `'public, max-age=15552000'` | Cache-Control header (default: 6 months) |
39
+
40
+ ## Image Transformation
41
+
42
+ Assets can be transformed on-the-fly via URL query parameters:
43
+
44
+ ```
45
+ http://localhost:3000/assets/photo.jpg?w=500&h=300&mode=resize
46
+ ```
47
+
48
+ **Query Parameters:**
49
+
50
+ | Param | Description |
51
+ |-------|-------------|
52
+ | `w` | Target width in pixels |
53
+ | `h` | Target height in pixels |
54
+ | `mode` | `crop` (cover) or `resize` (contain) |
55
+ | `preset` | Named preset (e.g. `?preset=thumb`) |
56
+ | `fpx`, `fpy` | Focal point (0-1 normalized coordinates) for crop mode |
57
+ | `format` | Output format: `jpg`, `png`, `webp`, `avif` |
58
+ | `q` | Quality (1-100, default: 80 for jpg/webp, 50 for avif) |
59
+ | `cache` | Set to `false` to skip caching |
60
+
61
+ ### Default Presets
62
+
63
+ | Name | Width | Height | Mode |
64
+ |------|-------|--------|------|
65
+ | `tiny` | 50px | 50px | crop |
66
+ | `thumb` | 150px | 150px | crop |
67
+ | `small` | 300px | 300px | resize |
68
+ | `medium` | 500px | 500px | resize |
69
+ | `large` | 800px | 800px | resize |
70
+
71
+ Custom presets can be added via the `presets` option:
72
+
73
+ ```typescript
74
+ AssetServerPlugin.init({
75
+ route: 'assets',
76
+ assetUploadDir: path.join(__dirname, 'assets'),
77
+ presets: [
78
+ { name: 'hero', width: 1200, height: 600, mode: 'crop' },
79
+ ],
80
+ });
81
+ ```
82
+
83
+ ## Features
84
+
85
+ - On-the-fly image resize and crop via URL query parameters
86
+ - Format conversion (JPEG, PNG, WebP, AVIF)
87
+ - Quality control for lossy formats
88
+ - Focal point-aware cropping
89
+ - Named transform presets
90
+ - Transformed image caching for subsequent requests
91
+ - Configurable Cache-Control headers
92
+ - S3-compatible cloud storage via `S3AssetStorageStrategy`
93
+ - Hashed asset naming for cache busting
94
+ - Sharp-based image processing
95
+ - Content-type detection with fallback
96
+
97
+ ## Admin UI
98
+
99
+ Server-only plugin. No Admin UI extensions.
100
+
101
+ ## API Extensions
102
+
103
+ No GraphQL API extensions. Assets are served via Express middleware at the configured route.
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./src/plugin";
2
+ export * from "./src/s3-asset-storage-strategy";
3
+ export * from "./src/sharp-asset-preview-strategy";
4
+ export * from "./src/types";
package/lib/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./src/plugin"), exports);
18
+ __exportStar(require("./src/s3-asset-storage-strategy"), exports);
19
+ __exportStar(require("./src/sharp-asset-preview-strategy"), exports);
20
+ __exportStar(require("./src/types"), exports);
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,+CAA6B;AAC7B,kEAAgD;AAChD,qEAAmD;AACnD,8CAA4B"}
@@ -0,0 +1,4 @@
1
+ import { Request } from "express";
2
+ import { AssetServerOptions, ImageTransformFormat } from "./types";
3
+ export declare function getAssetUrlPrefixFn(options: AssetServerOptions): ((request: Request, identifier: string) => string) | ((...args: any[]) => string);
4
+ export declare function getValidFormat(format?: unknown): ImageTransformFormat | undefined;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getValidFormat = exports.getAssetUrlPrefixFn = void 0;
4
+ const constants_1 = require("@deenruv/core/dist/common/constants");
5
+ function getAssetUrlPrefixFn(options) {
6
+ const { assetUrlPrefix, route } = options;
7
+ if (assetUrlPrefix == null) {
8
+ return (request, identifier) => {
9
+ var _a, _b;
10
+ const protocol = (_a = request.headers["x-forwarded-proto"]) !== null && _a !== void 0 ? _a : request.protocol;
11
+ return `${Array.isArray(protocol) ? protocol[0] : protocol}://${(_b = request.get("host")) !== null && _b !== void 0 ? _b : "could-not-determine-host"}/${route}/`;
12
+ };
13
+ }
14
+ if (typeof assetUrlPrefix === "string") {
15
+ return (...args) => assetUrlPrefix;
16
+ }
17
+ if (typeof assetUrlPrefix === "function") {
18
+ return (request, identifier) => {
19
+ const ctx = request[constants_1.REQUEST_CONTEXT_KEY];
20
+ return assetUrlPrefix(ctx, identifier);
21
+ };
22
+ }
23
+ throw new Error(`The assetUrlPrefix option was of an unexpected type: ${JSON.stringify(assetUrlPrefix)}`);
24
+ }
25
+ exports.getAssetUrlPrefixFn = getAssetUrlPrefixFn;
26
+ function getValidFormat(format) {
27
+ if (typeof format !== "string") {
28
+ return undefined;
29
+ }
30
+ switch (format) {
31
+ case "jpg":
32
+ case "jpeg":
33
+ case "png":
34
+ case "webp":
35
+ case "avif":
36
+ return format;
37
+ default:
38
+ return undefined;
39
+ }
40
+ }
41
+ exports.getValidFormat = getValidFormat;
42
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/common.ts"],"names":[],"mappings":";;;AAAA,mEAA0E;AAK1E,SAAgB,mBAAmB,CAAC,OAA2B;IAC7D,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1C,IAAI,cAAc,IAAI,IAAI,EAAE,CAAC;QAC3B,OAAO,CAAC,OAAgB,EAAE,UAAkB,EAAE,EAAE;;YAC9C,MAAM,QAAQ,GAAG,MAAA,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,mCAAI,OAAO,CAAC,QAAQ,CAAC;YAC1E,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,MACxD,MAAA,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,mCAAI,0BACzB,IAAI,KAAK,GAAG,CAAC;QACf,CAAC,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,cAAc,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,CAAC,GAAG,IAAW,EAAE,EAAE,CAAC,cAAc,CAAC;IAC5C,CAAC;IACD,IAAI,OAAO,cAAc,KAAK,UAAU,EAAE,CAAC;QACzC,OAAO,CAAC,OAAgB,EAAE,UAAkB,EAAE,EAAE;YAC9C,MAAM,GAAG,GAAI,OAAe,CAAC,+BAAmB,CAAC,CAAC;YAClD,OAAO,cAAc,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QACzC,CAAC,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,KAAK,CACb,wDAAwD,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC,EAAE,CACzF,CAAC;AACJ,CAAC;AAtBD,kDAsBC;AAED,SAAgB,cAAc,CAC5B,MAAgB;IAEhB,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ,KAAK,KAAK,CAAC;QACX,KAAK,MAAM,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAhBD,wCAgBC"}
@@ -0,0 +1,2 @@
1
+ export declare const loggerCtx = "AssetServerPlugin";
2
+ export declare const DEFAULT_CACHE_HEADER = "public, max-age=15552000";
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CACHE_HEADER = exports.loggerCtx = void 0;
4
+ exports.loggerCtx = "AssetServerPlugin";
5
+ exports.DEFAULT_CACHE_HEADER = "public, max-age=15552000";
6
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,SAAS,GAAG,mBAAmB,CAAC;AAChC,QAAA,oBAAoB,GAAG,0BAA0B,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { LocalAssetStorageStrategy } from "./local-asset-storage-strategy";
2
+ import { AssetServerOptions } from "./types";
3
+ /**
4
+ * By default the AssetServerPlugin will configure and use the LocalStorageStrategy to persist Assets.
5
+ */
6
+ export declare function defaultAssetStorageStrategyFactory(options: AssetServerOptions): LocalAssetStorageStrategy;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultAssetStorageStrategyFactory = void 0;
4
+ const common_1 = require("./common");
5
+ const local_asset_storage_strategy_1 = require("./local-asset-storage-strategy");
6
+ /**
7
+ * By default the AssetServerPlugin will configure and use the LocalStorageStrategy to persist Assets.
8
+ */
9
+ function defaultAssetStorageStrategyFactory(options) {
10
+ const { assetUrlPrefix, assetUploadDir, route } = options;
11
+ const prefixFn = (0, common_1.getAssetUrlPrefixFn)(options);
12
+ const toAbsoluteUrlFn = (request, identifier) => {
13
+ if (!identifier) {
14
+ return "";
15
+ }
16
+ const prefix = prefixFn(request, identifier);
17
+ return identifier.startsWith(prefix)
18
+ ? identifier
19
+ : `${prefix}${identifier}`;
20
+ };
21
+ return new local_asset_storage_strategy_1.LocalAssetStorageStrategy(assetUploadDir, toAbsoluteUrlFn);
22
+ }
23
+ exports.defaultAssetStorageStrategyFactory = defaultAssetStorageStrategyFactory;
24
+ //# sourceMappingURL=default-asset-storage-strategy-factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-asset-storage-strategy-factory.js","sourceRoot":"","sources":["../../src/default-asset-storage-strategy-factory.ts"],"names":[],"mappings":";;;AAEA,qCAA+C;AAC/C,iFAA2E;AAG3E;;GAEG;AACH,SAAgB,kCAAkC,CAChD,OAA2B;IAE3B,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,KAAK,EAAE,GAAG,OAAO,CAAC;IAC1D,MAAM,QAAQ,GAAG,IAAA,4BAAmB,EAAC,OAAO,CAAC,CAAC;IAC9C,MAAM,eAAe,GAAG,CAAC,OAAgB,EAAE,UAAkB,EAAU,EAAE;QACvE,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAC7C,OAAO,UAAU,CAAC,UAAU,CAAC,MAAM,CAAC;YAClC,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,GAAG,MAAM,GAAG,UAAU,EAAE,CAAC;IAC/B,CAAC,CAAC;IACF,OAAO,IAAI,wDAAyB,CAAC,cAAc,EAAE,eAAe,CAAC,CAAC;AACxE,CAAC;AAfD,gFAeC"}
Binary file
@@ -0,0 +1,21 @@
1
+ import { DefaultAssetNamingStrategy, RequestContext } from "@deenruv/core";
2
+ /**
3
+ * @description
4
+ * An extension of the {@link DefaultAssetNamingStrategy} which prefixes file names with
5
+ * the type (`'source'` or `'preview'`) as well as a 2-character sub-directory based on
6
+ * the md5 hash of the original file name.
7
+ *
8
+ * This is an implementation of the technique knows as "hashed directory" file storage,
9
+ * and the purpose is to reduce the number of files in a single directory, since a very large
10
+ * number of files can lead to performance issues when reading and writing to that directory.
11
+ *
12
+ * With this strategy, even with 200,000 total assets stored, each directory would
13
+ * only contain less than 800 files.
14
+ *
15
+ * @docsCategory core plugins/AssetServerPlugin
16
+ */
17
+ export declare class HashedAssetNamingStrategy extends DefaultAssetNamingStrategy {
18
+ generateSourceFileName(ctx: RequestContext, originalFileName: string, conflictFileName?: string): string;
19
+ generatePreviewFileName(ctx: RequestContext, originalFileName: string, conflictFileName?: string): string;
20
+ private getHashedDir;
21
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HashedAssetNamingStrategy = void 0;
7
+ const core_1 = require("@deenruv/core");
8
+ const crypto_1 = require("crypto");
9
+ const path_1 = __importDefault(require("path"));
10
+ /**
11
+ * @description
12
+ * An extension of the {@link DefaultAssetNamingStrategy} which prefixes file names with
13
+ * the type (`'source'` or `'preview'`) as well as a 2-character sub-directory based on
14
+ * the md5 hash of the original file name.
15
+ *
16
+ * This is an implementation of the technique knows as "hashed directory" file storage,
17
+ * and the purpose is to reduce the number of files in a single directory, since a very large
18
+ * number of files can lead to performance issues when reading and writing to that directory.
19
+ *
20
+ * With this strategy, even with 200,000 total assets stored, each directory would
21
+ * only contain less than 800 files.
22
+ *
23
+ * @docsCategory core plugins/AssetServerPlugin
24
+ */
25
+ class HashedAssetNamingStrategy extends core_1.DefaultAssetNamingStrategy {
26
+ generateSourceFileName(ctx, originalFileName, conflictFileName) {
27
+ const filename = super.generateSourceFileName(ctx, originalFileName, conflictFileName);
28
+ return path_1.default.join("source", this.getHashedDir(filename), filename);
29
+ }
30
+ generatePreviewFileName(ctx, originalFileName, conflictFileName) {
31
+ const filename = super.generatePreviewFileName(ctx, originalFileName, conflictFileName);
32
+ return path_1.default.join("preview", this.getHashedDir(filename), filename);
33
+ }
34
+ getHashedDir(filename) {
35
+ return (0, crypto_1.createHash)("md5").update(filename).digest("hex").slice(0, 2);
36
+ }
37
+ }
38
+ exports.HashedAssetNamingStrategy = HashedAssetNamingStrategy;
39
+ //# sourceMappingURL=hashed-asset-naming-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hashed-asset-naming-strategy.js","sourceRoot":"","sources":["../../src/hashed-asset-naming-strategy.ts"],"names":[],"mappings":";;;;;;AAAA,wCAA2E;AAC3E,mCAAoC;AACpC,gDAAwB;AAExB;;;;;;;;;;;;;;GAcG;AACH,MAAa,yBAA0B,SAAQ,iCAA0B;IACvE,sBAAsB,CACpB,GAAmB,EACnB,gBAAwB,EACxB,gBAAyB;QAEzB,MAAM,QAAQ,GAAG,KAAK,CAAC,sBAAsB,CAC3C,GAAG,EACH,gBAAgB,EAChB,gBAAgB,CACjB,CAAC;QACF,OAAO,cAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;IACpE,CAAC;IACD,uBAAuB,CACrB,GAAmB,EACnB,gBAAwB,EACxB,gBAAyB;QAEzB,MAAM,QAAQ,GAAG,KAAK,CAAC,uBAAuB,CAC5C,GAAG,EACH,gBAAgB,EAChB,gBAAgB,CACjB,CAAC;QACF,OAAO,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,CAAC;IACrE,CAAC;IAEO,YAAY,CAAC,QAAgB;QACnC,OAAO,IAAA,mBAAU,EAAC,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACtE,CAAC;CACF;AA7BD,8DA6BC"}
@@ -0,0 +1,28 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ /// <reference types="node" />
4
+ /// <reference types="node" />
5
+ import { AssetStorageStrategy } from "@deenruv/core";
6
+ import { Request } from "express";
7
+ import { ReadStream } from "fs";
8
+ import { Stream } from "stream";
9
+ /**
10
+ * @description
11
+ * A persistence strategy which saves files to the local file system.
12
+ *
13
+ * @docsCategory core plugins/AssetServerPlugin
14
+ */
15
+ export declare class LocalAssetStorageStrategy implements AssetStorageStrategy {
16
+ private readonly uploadPath;
17
+ private readonly toAbsoluteUrlFn?;
18
+ toAbsoluteUrl: ((reqest: Request, identifier: string) => string) | undefined;
19
+ constructor(uploadPath: string, toAbsoluteUrlFn?: ((reqest: Request, identifier: string) => string) | undefined);
20
+ writeFileFromStream(fileName: string, data: ReadStream): Promise<string>;
21
+ writeFileFromBuffer(fileName: string, data: Buffer): Promise<string>;
22
+ fileExists(fileName: string): Promise<boolean>;
23
+ readFileToBuffer(identifier: string): Promise<Buffer>;
24
+ readFileToStream(identifier: string): Promise<Stream>;
25
+ deleteFile(identifier: string): Promise<void>;
26
+ private filePathToIdentifier;
27
+ private identifierToFilePath;
28
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LocalAssetStorageStrategy = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ /**
10
+ * @description
11
+ * A persistence strategy which saves files to the local file system.
12
+ *
13
+ * @docsCategory core plugins/AssetServerPlugin
14
+ */
15
+ class LocalAssetStorageStrategy {
16
+ constructor(uploadPath, toAbsoluteUrlFn) {
17
+ this.uploadPath = uploadPath;
18
+ this.toAbsoluteUrlFn = toAbsoluteUrlFn;
19
+ fs_extra_1.default.ensureDirSync(this.uploadPath);
20
+ if (toAbsoluteUrlFn) {
21
+ this.toAbsoluteUrl = toAbsoluteUrlFn;
22
+ }
23
+ }
24
+ async writeFileFromStream(fileName, data) {
25
+ const filePath = path_1.default.join(this.uploadPath, fileName);
26
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
27
+ const writeStream = fs_extra_1.default.createWriteStream(filePath, "binary");
28
+ return new Promise((resolve, reject) => {
29
+ data.pipe(writeStream);
30
+ writeStream.on("close", () => resolve(this.filePathToIdentifier(filePath)));
31
+ writeStream.on("error", reject);
32
+ });
33
+ }
34
+ async writeFileFromBuffer(fileName, data) {
35
+ const filePath = path_1.default.join(this.uploadPath, fileName);
36
+ await fs_extra_1.default.ensureDir(path_1.default.dirname(filePath));
37
+ await fs_extra_1.default.writeFile(filePath, data, "binary");
38
+ return this.filePathToIdentifier(filePath);
39
+ }
40
+ fileExists(fileName) {
41
+ return new Promise((resolve) => {
42
+ fs_extra_1.default.access(this.identifierToFilePath(fileName), fs_extra_1.default.constants.F_OK, (err) => {
43
+ resolve(!err);
44
+ });
45
+ });
46
+ }
47
+ readFileToBuffer(identifier) {
48
+ return fs_extra_1.default.readFile(this.identifierToFilePath(identifier));
49
+ }
50
+ readFileToStream(identifier) {
51
+ const readStream = fs_extra_1.default.createReadStream(this.identifierToFilePath(identifier), "binary");
52
+ return Promise.resolve(readStream);
53
+ }
54
+ deleteFile(identifier) {
55
+ return fs_extra_1.default.unlink(this.identifierToFilePath(identifier));
56
+ }
57
+ filePathToIdentifier(filePath) {
58
+ const filePathDirname = path_1.default.dirname(filePath);
59
+ const deltaDirname = filePathDirname.replace(this.uploadPath, "");
60
+ const identifier = path_1.default.join(deltaDirname, path_1.default.basename(filePath));
61
+ return identifier.replace(/^[\\/]+/, "");
62
+ }
63
+ identifierToFilePath(identifier) {
64
+ return path_1.default.join(this.uploadPath, identifier);
65
+ }
66
+ }
67
+ exports.LocalAssetStorageStrategy = LocalAssetStorageStrategy;
68
+ //# sourceMappingURL=local-asset-storage-strategy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-asset-storage-strategy.js","sourceRoot":"","sources":["../../src/local-asset-storage-strategy.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,167 @@
1
+ import { MiddlewareConsumer, NestModule, OnApplicationBootstrap } from "@nestjs/common";
2
+ import { Type } from "@deenruv/common/lib/shared-types";
3
+ import { ProcessContext, RuntimeDeenruvConfig } from "@deenruv/core";
4
+ import { AssetServerOptions } from "./types";
5
+ /**
6
+ * @description
7
+ * The `AssetServerPlugin` serves assets (images and other files) from the local file system, and can also be configured to use
8
+ * other storage strategies (e.g. {@link S3AssetStorageStrategy}. It can also perform on-the-fly image transformations
9
+ * and caches the results for subsequent calls.
10
+ *
11
+ * ## Installation
12
+ *
13
+ * `yarn add \@deenruv/asset-server-plugin`
14
+ *
15
+ * or
16
+ *
17
+ * `npm install \@deenruv/asset-server-plugin`
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { AssetServerPlugin } from '\@deenruv/asset-server-plugin';
22
+ *
23
+ * const config: DeenruvConfig = {
24
+ * // Add an instance of the plugin to the plugins array
25
+ * plugins: [
26
+ * AssetServerPlugin.init({
27
+ * route: 'assets',
28
+ * assetUploadDir: path.join(__dirname, 'assets'),
29
+ * }),
30
+ * ],
31
+ * };
32
+ * ```
33
+ *
34
+ * The full configuration is documented at [AssetServerOptions](/reference/core-plugins/asset-server-plugin/asset-server-options)
35
+ *
36
+ * ## Image transformation
37
+ *
38
+ * Asset preview images can be transformed (resized & cropped) on the fly by appending query parameters to the url:
39
+ *
40
+ * `http://localhost:3000/assets/some-asset.jpg?w=500&h=300&mode=resize`
41
+ *
42
+ * The above URL will return `some-asset.jpg`, resized to fit in the bounds of a 500px x 300px rectangle.
43
+ *
44
+ * ### Preview mode
45
+ *
46
+ * The `mode` parameter can be either `crop` or `resize`. See the [ImageTransformMode](/reference/core-plugins/asset-server-plugin/image-transform-mode) docs for details.
47
+ *
48
+ * ### Focal point
49
+ *
50
+ * When cropping an image (`mode=crop`), Deenruv will attempt to keep the most "interesting" area of the image in the cropped frame. It does this
51
+ * by finding the area of the image with highest entropy (the busiest area of the image). However, sometimes this does not yield a satisfactory
52
+ * result - part or all of the main subject may still be cropped out.
53
+ *
54
+ * This is where specifying the focal point can help. The focal point of the image may be specified by passing the `fpx` and `fpy` query parameters.
55
+ * These are normalized coordinates (i.e. a number between 0 and 1), so the `fpx=0&fpy=0` corresponds to the top left of the image.
56
+ *
57
+ * For example, let's say there is a very wide landscape image which we want to crop to be square. The main subject is a house to the far left of the
58
+ * image. The following query would crop it to a square with the house centered:
59
+ *
60
+ * `http://localhost:3000/assets/landscape.jpg?w=150&h=150&mode=crop&fpx=0.2&fpy=0.7`
61
+ *
62
+ * ### Format
63
+ *
64
+ * Since v1.7.0, the image format can be specified by adding the `format` query parameter:
65
+ *
66
+ * `http://localhost:3000/assets/some-asset.jpg?format=webp`
67
+ *
68
+ * This means that, no matter the format of your original asset files, you can use more modern formats in your storefront if the browser
69
+ * supports them. Supported values for `format` are:
70
+ *
71
+ * * `jpeg` or `jpg`
72
+ * * `png`
73
+ * * `webp`
74
+ * * `avif`
75
+ *
76
+ * The `format` parameter can also be combined with presets (see below).
77
+ *
78
+ * ### Quality
79
+ *
80
+ * Since v2.2.0, the image quality can be specified by adding the `q` query parameter:
81
+ *
82
+ * `http://localhost:3000/assets/some-asset.jpg?q=75`
83
+ *
84
+ * This applies to the `jpg`, `webp` and `avif` formats. The default quality value for `jpg` and `webp` is 80, and for `avif` is 50.
85
+ *
86
+ * The `q` parameter can also be combined with presets (see below).
87
+ *
88
+ * ### Transform presets
89
+ *
90
+ * Presets can be defined which allow a single preset name to be used instead of specifying the width, height and mode. Presets are
91
+ * configured via the AssetServerOptions [presets property](/reference/core-plugins/asset-server-plugin/asset-server-options/#presets).
92
+ *
93
+ * For example, defining the following preset:
94
+ *
95
+ * ```ts
96
+ * AssetServerPlugin.init({
97
+ * // ...
98
+ * presets: [
99
+ * { name: 'my-preset', width: 85, height: 85, mode: 'crop' },
100
+ * ],
101
+ * }),
102
+ * ```
103
+ *
104
+ * means that a request to:
105
+ *
106
+ * `http://localhost:3000/assets/some-asset.jpg?preset=my-preset`
107
+ *
108
+ * is equivalent to:
109
+ *
110
+ * `http://localhost:3000/assets/some-asset.jpg?w=85&h=85&mode=crop`
111
+ *
112
+ * The AssetServerPlugin comes pre-configured with the following presets:
113
+ *
114
+ * name | width | height | mode
115
+ * -----|-------|--------|-----
116
+ * tiny | 50px | 50px | crop
117
+ * thumb | 150px | 150px | crop
118
+ * small | 300px | 300px | resize
119
+ * medium | 500px | 500px | resize
120
+ * large | 800px | 800px | resize
121
+ *
122
+ * ### Caching
123
+ * By default, the AssetServerPlugin will cache every transformed image, so that the transformation only needs to be performed a single time for
124
+ * a given configuration. Caching can be disabled per-request by setting the `?cache=false` query parameter.
125
+ *
126
+ * @docsCategory core plugins/AssetServerPlugin
127
+ */
128
+ export declare class AssetServerPlugin implements NestModule, OnApplicationBootstrap {
129
+ private processContext;
130
+ private static assetStorage;
131
+ private readonly cacheDir;
132
+ private presets;
133
+ private static options;
134
+ private cacheHeader;
135
+ /**
136
+ * @description
137
+ * Set the plugin options.
138
+ */
139
+ static init(options: AssetServerOptions): Type<AssetServerPlugin>;
140
+ /** @internal */
141
+ static configure(config: RuntimeDeenruvConfig): Promise<RuntimeDeenruvConfig>;
142
+ constructor(processContext: ProcessContext);
143
+ /** @internal */
144
+ onApplicationBootstrap(): void;
145
+ configure(consumer: MiddlewareConsumer): void;
146
+ /**
147
+ * Creates the image server instance
148
+ */
149
+ private createAssetServer;
150
+ /**
151
+ * Reads the file requested and send the response to the browser.
152
+ */
153
+ private sendAsset;
154
+ /**
155
+ * If an exception was thrown by the first handler, then it may be because a transformed image
156
+ * is being requested which does not yet exist. In this case, this handler will generate the
157
+ * transformed image, save it to cache, and serve the result as a response.
158
+ */
159
+ private generateTransformedImage;
160
+ private getFileNameFromRequest;
161
+ private md5;
162
+ private addSuffix;
163
+ /**
164
+ * Attempt to get the mime type from the file name.
165
+ */
166
+ private getMimeType;
167
+ }