@webiny/api-file-manager-server 6.6.0-alpha.0 → 6.6.0-alpha.1

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.
@@ -0,0 +1,33 @@
1
+ import { IdentityContext } from "@webiny/api-core/features/security/IdentityContext/index.js";
2
+ import { GetFileUseCase } from "@webiny/api-file-manager/features/file/GetFile/index.js";
3
+ import type { IAssetTypeHandler } from "@webiny/api-file-manager/features/assetDelivery/abstractions/AssetType.js";
4
+ import type { Asset } from "@webiny/api-file-manager/delivery/AssetDelivery/Asset.js";
5
+ import type { AssetRequest } from "@webiny/api-file-manager/delivery/AssetDelivery/AssetRequest.js";
6
+ import type { ILocalAssetDeliveryConfig } from "../assetDelivery/abstractions.js";
7
+ /**
8
+ * Defers loading `sharp` until an image is actually transformed. Server counterpart of
9
+ * `api-file-manager-s3`'s `LazySharpTransform`.
10
+ *
11
+ * The dynamic import is deliberate — it keeps `sharp` out of the main bundle (note the
12
+ * `webpackChunkName`). That import is async and DI resolution is not, which is why this used to be
13
+ * a per-request `RequestContextInitializer` that imported the module and then registered the real
14
+ * handler. `IAssetTypeHandler` has a single async method, so the import can be awaited there
15
+ * instead.
16
+ *
17
+ * The constructed handler IS memoized: the initializer registered it `.inSingletonScope()`, and
18
+ * nothing below caches the instance (Node caches the module, not the object built from it).
19
+ */
20
+ declare class LazyLocalSharpTransform implements IAssetTypeHandler {
21
+ private readonly storagePath;
22
+ private readonly config;
23
+ private readonly identityContext;
24
+ private readonly getFile;
25
+ private handler;
26
+ constructor(storagePath: string, config: ILocalAssetDeliveryConfig, identityContext: IdentityContext.Interface, getFile: GetFileUseCase.Interface);
27
+ private resolveHandler;
28
+ handle(assetRequest: AssetRequest, asset: Asset): Promise<Asset>;
29
+ }
30
+ export declare const LazyLocalSharpTransformImpl: typeof LazyLocalSharpTransform & {
31
+ __abstraction: import("@webiny/di").Abstraction<IAssetTypeHandler>;
32
+ };
33
+ export {};
@@ -0,0 +1,32 @@
1
+ import { IdentityContext } from "@webiny/api-core/features/security/IdentityContext/index.js";
2
+ import { GetFileUseCase } from "@webiny/api-file-manager/features/file/GetFile/index.js";
3
+ import { ImageAssetTypeHandler } from "@webiny/api-file-manager/features/assetDelivery/assetTypes/image/index.js";
4
+ import { LocalAssetDeliveryConfig, LocalStoragePath } from "./abstractions.js";
5
+ class LazyLocalSharpTransform {
6
+ constructor(storagePath, config, identityContext, getFile){
7
+ this.storagePath = storagePath;
8
+ this.config = config;
9
+ this.identityContext = identityContext;
10
+ this.getFile = getFile;
11
+ this.handler = null;
12
+ }
13
+ resolveHandler() {
14
+ if (!this.handler) this.handler = import("./LocalSharpTransform.js").then(({ LocalSharpTransform })=>new LocalSharpTransform(this.storagePath, this.config, this.identityContext, this.getFile));
15
+ return this.handler;
16
+ }
17
+ async handle(assetRequest, asset) {
18
+ return (await this.resolveHandler()).handle(assetRequest, asset);
19
+ }
20
+ }
21
+ const LazyLocalSharpTransformImpl = ImageAssetTypeHandler.createImplementation({
22
+ implementation: LazyLocalSharpTransform,
23
+ dependencies: [
24
+ LocalStoragePath,
25
+ LocalAssetDeliveryConfig,
26
+ IdentityContext,
27
+ GetFileUseCase
28
+ ]
29
+ });
30
+ export { LazyLocalSharpTransformImpl };
31
+
32
+ //# sourceMappingURL=LazyLocalSharpTransform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assetDelivery/LazyLocalSharpTransform.js","sources":["../../src/assetDelivery/LazyLocalSharpTransform.ts"],"sourcesContent":["import { IdentityContext } from \"@webiny/api-core/features/security/IdentityContext/index.js\";\nimport { GetFileUseCase } from \"@webiny/api-file-manager/features/file/GetFile/index.js\";\nimport { ImageAssetTypeHandler } from \"@webiny/api-file-manager/features/assetDelivery/assetTypes/image/index.js\";\nimport type { IAssetTypeHandler } from \"@webiny/api-file-manager/features/assetDelivery/abstractions/AssetType.js\";\nimport type { Asset } from \"@webiny/api-file-manager/delivery/AssetDelivery/Asset.js\";\nimport type { AssetRequest } from \"@webiny/api-file-manager/delivery/AssetDelivery/AssetRequest.js\";\nimport { LocalAssetDeliveryConfig, LocalStoragePath } from \"~/assetDelivery/abstractions.js\";\nimport type { ILocalAssetDeliveryConfig } from \"~/assetDelivery/abstractions.js\";\n\n/**\n * Defers loading `sharp` until an image is actually transformed. Server counterpart of\n * `api-file-manager-s3`'s `LazySharpTransform`.\n *\n * The dynamic import is deliberate — it keeps `sharp` out of the main bundle (note the\n * `webpackChunkName`). That import is async and DI resolution is not, which is why this used to be\n * a per-request `RequestContextInitializer` that imported the module and then registered the real\n * handler. `IAssetTypeHandler` has a single async method, so the import can be awaited there\n * instead.\n *\n * The constructed handler IS memoized: the initializer registered it `.inSingletonScope()`, and\n * nothing below caches the instance (Node caches the module, not the object built from it).\n */\nclass LazyLocalSharpTransform implements IAssetTypeHandler {\n private handler: Promise<IAssetTypeHandler> | null = null;\n\n constructor(\n private readonly storagePath: string,\n private readonly config: ILocalAssetDeliveryConfig,\n private readonly identityContext: IdentityContext.Interface,\n private readonly getFile: GetFileUseCase.Interface\n ) {}\n\n private resolveHandler(): Promise<IAssetTypeHandler> {\n if (!this.handler) {\n this.handler = import(\n /* webpackChunkName: \"localAssetDelivery\" */ \"./LocalSharpTransform.js\"\n ).then(\n ({ LocalSharpTransform }) =>\n new LocalSharpTransform(\n this.storagePath,\n this.config,\n this.identityContext,\n this.getFile\n )\n );\n }\n return this.handler;\n }\n\n async handle(assetRequest: AssetRequest, asset: Asset): Promise<Asset> {\n return (await this.resolveHandler()).handle(assetRequest, asset);\n }\n}\n\nexport const LazyLocalSharpTransformImpl = ImageAssetTypeHandler.createImplementation({\n implementation: LazyLocalSharpTransform,\n dependencies: [LocalStoragePath, LocalAssetDeliveryConfig, IdentityContext, GetFileUseCase]\n});\n"],"names":["LazyLocalSharpTransform","storagePath","config","identityContext","getFile","LocalSharpTransform","assetRequest","asset","LazyLocalSharpTransformImpl","ImageAssetTypeHandler","LocalStoragePath","LocalAssetDeliveryConfig","IdentityContext","GetFileUseCase"],"mappings":";;;;AAsBA,MAAMA;IAGF,YACqBC,WAAmB,EACnBC,MAAiC,EACjCC,eAA0C,EAC1CC,OAAiC,CACpD;aAJmBH,WAAW,GAAXA;aACAC,MAAM,GAANA;aACAC,eAAe,GAAfA;aACAC,OAAO,GAAPA;aANb,OAAO,GAAsC;IAOlD;IAEK,iBAA6C;QACjD,IAAI,CAAC,IAAI,CAAC,OAAO,EACb,IAAI,CAAC,OAAO,GAAG,MAAM,CAAN,4BAEb,IAAI,CACF,CAAC,EAAEC,mBAAmB,EAAE,GACpB,IAAIA,oBACA,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,MAAM,EACX,IAAI,CAAC,eAAe,EACpB,IAAI,CAAC,OAAO;QAI5B,OAAO,IAAI,CAAC,OAAO;IACvB;IAEA,MAAM,OAAOC,YAA0B,EAAEC,KAAY,EAAkB;QACnE,OAAQ,OAAM,IAAI,CAAC,cAAc,EAAC,EAAG,MAAM,CAACD,cAAcC;IAC9D;AACJ;AAEO,MAAMC,8BAA8BC,sBAAsB,oBAAoB,CAAC;IAClF,gBAAgBT;IAChB,cAAc;QAACU;QAAkBC;QAA0BC;QAAiBC;KAAe;AAC/F"}
@@ -1,18 +1,24 @@
1
- import { AssetTransformationStrategy as AssetTransformationStrategyAbstraction } from "@webiny/api-file-manager/exports/api/file-manager/assetDelivery.js";
2
- import { FileManagerServerConfig } from "../features/FileManagerServerConfig/abstractions.js";
1
+ import { IdentityContext } from "@webiny/api-core/features/security/IdentityContext/index.js";
2
+ import { GetFileUseCase } from "@webiny/api-file-manager/features/file/GetFile/index.js";
3
+ import type { IAssetTypeHandler } from "@webiny/api-file-manager/features/assetDelivery/abstractions/AssetType.js";
4
+ import type { Asset } from "@webiny/api-file-manager/delivery/AssetDelivery/Asset.js";
5
+ import type { AssetRequest } from "@webiny/api-file-manager/delivery/AssetDelivery/AssetRequest.js";
3
6
  import type { ILocalAssetDeliveryConfig } from "../assetDelivery/abstractions.js";
4
- declare class AssetTransformationStrategyImpl implements AssetTransformationStrategyAbstraction.Interface {
7
+ export declare class LocalSharpTransform implements IAssetTypeHandler {
5
8
  private readonly storagePath;
6
9
  private readonly imageResizeWidths;
7
- constructor(serverConfig: FileManagerServerConfig.Interface, config: ILocalAssetDeliveryConfig);
8
- transform(assetRequest: AssetTransformationStrategyAbstraction.AssetRequest, asset: AssetTransformationStrategyAbstraction.Asset): Promise<AssetTransformationStrategyAbstraction.Asset>;
10
+ private readonly imageQuality;
11
+ private readonly identityContext;
12
+ private readonly getFile;
13
+ private transformer?;
14
+ constructor(storagePath: string, config: ILocalAssetDeliveryConfig, identityContext: IdentityContext.Interface, getFile: GetFileUseCase.Interface);
15
+ private getTransformer;
16
+ handle(assetRequest: AssetRequest, asset: Asset): Promise<Asset>;
17
+ private loadAssetCrop;
9
18
  private transformAsset;
10
19
  private optimizeAsset;
11
20
  private isAssetAnimated;
12
- private optimizePng;
13
- private optimizeJpeg;
14
21
  }
15
- export declare const LocalSharpTransform: typeof AssetTransformationStrategyImpl & {
16
- __abstraction: import("@webiny/di").Abstraction<import("@webiny/api-file-manager/delivery").AssetTransformationStrategy>;
22
+ export declare const LocalSharpTransformImpl: typeof LocalSharpTransform & {
23
+ __abstraction: import("@webiny/di").Abstraction<IAssetTypeHandler>;
17
24
  };
18
- export {};
@@ -1,78 +1,105 @@
1
- import sharp from "sharp";
2
1
  import promises from "node:fs/promises";
3
2
  import node_path from "node:path";
4
- import { AssetTransformationStrategy } from "@webiny/api-file-manager/exports/api/file-manager/assetDelivery.js";
5
- import { LocalAssetDeliveryConfig } from "./abstractions.js";
6
- import { FileManagerServerConfig } from "../features/FileManagerServerConfig/abstractions.js";
7
- import * as __rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114 from "@webiny/api-file-manager/features/assetDelivery/transformation/index.js";
8
- class AssetTransformationStrategyImpl {
9
- constructor(serverConfig, config){
10
- this.storagePath = serverConfig.storagePath;
3
+ import { IdentityContext } from "@webiny/api-core/features/security/IdentityContext/index.js";
4
+ import { GetFileUseCase } from "@webiny/api-file-manager/features/file/GetFile/index.js";
5
+ import { CallableContentsReader, DEFAULT_IMAGE_QUALITY, contentTypeForFormat } from "@webiny/api-file-manager/features/assetDelivery/transformation/index.js";
6
+ import { normalizeImageOptions } from "@webiny/api-file-manager/features/assetDelivery/assetTypes/image/normalizeImageOptions.js";
7
+ import { AssetKeyGenerator, ImageAssetTypeHandler } from "@webiny/api-file-manager/features/assetDelivery/assetTypes/image/index.js";
8
+ import { LocalAssetDeliveryConfig, LocalStoragePath } from "./abstractions.js";
9
+ const hasTransform = (options)=>void 0 !== options.width || void 0 !== options.format || void 0 !== options.quality;
10
+ class LocalSharpTransform {
11
+ constructor(storagePath, config, identityContext, getFile){
12
+ this.storagePath = storagePath;
11
13
  this.imageResizeWidths = config.imageResizeWidths;
14
+ this.imageQuality = {
15
+ ...DEFAULT_IMAGE_QUALITY,
16
+ ...config.imageQuality ?? {}
17
+ };
18
+ this.identityContext = identityContext;
19
+ this.getFile = getFile;
12
20
  }
13
- async transform(assetRequest, asset) {
14
- if (!__rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114.SUPPORTED_TRANSFORMABLE_IMAGES.includes(asset.getExtension())) {
15
- console.log(`Transformations/optimizations of ${asset.getContentType()} assets are not supported. Skipping.`);
16
- return asset;
21
+ async getTransformer() {
22
+ if (!this.transformer) {
23
+ const { SharpTransformer } = await import("@webiny/api-file-manager/features/assetDelivery/assetTypes/image/SharpTransformer.js");
24
+ this.transformer = new SharpTransformer();
17
25
  }
18
- const { original, ...options } = assetRequest.getOptions();
26
+ return this.transformer;
27
+ }
28
+ async handle(assetRequest, asset) {
29
+ const rawQuery = assetRequest.getOptions();
30
+ const acceptHeader = assetRequest.getContext().accept;
31
+ const { original, crop, focal, aspectRatio, ...options } = normalizeImageOptions(rawQuery, acceptHeader);
32
+ const assetCrop = await this.loadAssetCrop(asset.getId());
33
+ const framing = {
34
+ crop: crop ?? assetCrop,
35
+ focal,
36
+ aspectRatio
37
+ };
19
38
  const transformedAsset = asset.clone();
20
- if (Object.keys(options).length > 0) return this.transformAsset(transformedAsset, options);
21
- return this.optimizeAsset(transformedAsset);
39
+ if (hasTransform(options)) return this.transformAsset(transformedAsset, options, framing);
40
+ return this.optimizeAsset(transformedAsset, framing);
22
41
  }
23
- async transformAsset(asset, options) {
24
- if (options.width) {
25
- const assetKey = __rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114.AssetKeyGenerator.create(asset);
26
- const transformedAssetKey = assetKey.getTransformedImageKey(options);
27
- const transformedFilePath = node_path.join(this.storagePath, transformedAssetKey);
28
- try {
29
- const buffer = await promises.readFile(transformedFilePath);
30
- const newAsset = asset.withProps({
31
- size: buffer.length
32
- });
33
- newAsset.setContentsReader(__rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114.CallableContentsReader.create(()=>buffer));
34
- console.log("Return a previously transformed asset", {
35
- key: transformedAssetKey,
36
- size: newAsset.getSize()
37
- });
38
- return newAsset;
39
- } catch {
40
- const optimizedImage = await this.optimizeAsset(asset);
41
- const widths = __rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114.WidthCollection.create(this.imageResizeWidths);
42
- const width = widths.getClosestOrMax(options.width);
43
- console.log(`Resize the asset (width: ${width})`);
44
- const buffer = await optimizedImage.getContents();
45
- const transformedBuffer = await sharp(buffer, {
46
- animated: this.isAssetAnimated(asset)
47
- }).withMetadata().resize({
48
- width,
49
- withoutEnlargement: true
50
- }).toBuffer();
51
- const newAsset = asset.withProps({
52
- size: transformedBuffer.length
53
- });
54
- newAsset.setContentsReader(__rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114.CallableContentsReader.create(()=>transformedBuffer));
55
- await promises.mkdir(node_path.dirname(transformedFilePath), {
56
- recursive: true
57
- });
58
- await promises.writeFile(transformedFilePath, await newAsset.getContents());
59
- console.log("Return the resized asset", {
60
- key: transformedAssetKey,
61
- size: newAsset.getSize()
62
- });
63
- return newAsset;
64
- }
42
+ async loadAssetCrop(fileId) {
43
+ const result = await this.identityContext.withoutAuthorization(()=>this.getFile.execute(fileId));
44
+ if (result.isFail()) return;
45
+ const imageEdit = result.value.metadata?.imageEdit;
46
+ return imageEdit?.crop;
47
+ }
48
+ async transformAsset(asset, options, framing) {
49
+ const assetKey = AssetKeyGenerator.create(asset, framing);
50
+ const transformedAssetKey = assetKey.getTransformedImageKey(options);
51
+ const transformedFilePath = node_path.join(this.storagePath, transformedAssetKey);
52
+ const contentType = options.format ? contentTypeForFormat(options.format) : asset.getContentType();
53
+ try {
54
+ const buffer = await promises.readFile(transformedFilePath);
55
+ const newAsset = asset.withProps({
56
+ size: buffer.length,
57
+ contentType
58
+ });
59
+ newAsset.setContentsReader(CallableContentsReader.create(()=>buffer));
60
+ console.log("Return a previously transformed asset", {
61
+ key: transformedAssetKey,
62
+ size: newAsset.getSize()
63
+ });
64
+ return newAsset;
65
+ } catch {
66
+ const optimizedImage = await this.optimizeAsset(asset, framing);
67
+ console.log("Transform the asset", options);
68
+ const baseBuffer = await optimizedImage.getContents();
69
+ const transformer = await this.getTransformer();
70
+ const { buffer: transformedBuffer, contentType: outputContentType } = await transformer.transformBuffer({
71
+ buffer: baseBuffer,
72
+ animated: this.isAssetAnimated(asset),
73
+ sourceContentType: asset.getContentType(),
74
+ widths: this.imageResizeWidths,
75
+ options,
76
+ qualityDefaults: this.imageQuality
77
+ });
78
+ const newAsset = asset.withProps({
79
+ size: transformedBuffer.length,
80
+ contentType: outputContentType
81
+ });
82
+ newAsset.setContentsReader(CallableContentsReader.create(()=>transformedBuffer));
83
+ await promises.mkdir(node_path.dirname(transformedFilePath), {
84
+ recursive: true
85
+ });
86
+ await promises.writeFile(transformedFilePath, await newAsset.getContents());
87
+ console.log("Return the transformed asset", {
88
+ key: transformedAssetKey,
89
+ size: newAsset.getSize(),
90
+ contentType: newAsset.getContentType()
91
+ });
92
+ return newAsset;
65
93
  }
66
- return asset;
67
94
  }
68
- async optimizeAsset(asset) {
95
+ async optimizeAsset(asset, framing) {
69
96
  console.log("Optimize asset", {
70
97
  id: asset.getId(),
71
98
  key: asset.getKey(),
72
99
  size: asset.getSize(),
73
100
  type: asset.getContentType()
74
101
  });
75
- const assetKey = __rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114.AssetKeyGenerator.create(asset);
102
+ const assetKey = AssetKeyGenerator.create(asset, framing);
76
103
  const optimizedAssetKey = assetKey.getOptimizedImageKey();
77
104
  const optimizedFilePath = node_path.join(this.storagePath, optimizedAssetKey);
78
105
  try {
@@ -81,27 +108,31 @@ class AssetTransformationStrategyImpl {
81
108
  const newAsset = asset.withProps({
82
109
  size: buffer.length
83
110
  });
84
- newAsset.setContentsReader(__rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114.CallableContentsReader.create(()=>buffer));
111
+ newAsset.setContentsReader(CallableContentsReader.create(()=>buffer));
85
112
  return newAsset;
86
113
  } catch {
87
114
  console.log("Create an optimized version of the original asset", asset.getKey());
88
- const buffer = await asset.getContents();
89
- const optimizationMap = {
90
- "image/png": (buffer)=>this.optimizePng(buffer),
91
- "image/jpeg": (buffer)=>this.optimizeJpeg(buffer),
92
- "image/jpg": (buffer)=>this.optimizeJpeg(buffer)
93
- };
94
- const optimization = optimizationMap[asset.getContentType()];
95
- if (!optimization) {
96
- console.log(`No optimizations defined for ${asset.getContentType()}`);
115
+ let buffer = await asset.getContents();
116
+ const transformer = await this.getTransformer();
117
+ const framed = await transformer.extractFramedRegion(buffer, framing);
118
+ const cropped = framed !== buffer;
119
+ buffer = framed;
120
+ const contentType = asset.getContentType();
121
+ const canOptimize = "image/png" === contentType || "image/jpeg" === contentType || "image/jpg" === contentType;
122
+ if (!canOptimize && !cropped) {
123
+ console.log(`No optimizations defined for ${contentType}`);
97
124
  return asset;
98
125
  }
99
- const optimizedBuffer = await optimization(buffer).toBuffer();
100
- console.log("Optimized asset size", optimizedBuffer.length);
126
+ let finalBuffer = buffer;
127
+ if (canOptimize) {
128
+ const pipeline = "image/png" === contentType ? await transformer.optimizePng(buffer) : await transformer.optimizeJpeg(buffer);
129
+ finalBuffer = await pipeline.toBuffer();
130
+ }
131
+ console.log("Optimized asset size", finalBuffer.length);
101
132
  const newAsset = asset.withProps({
102
- size: optimizedBuffer.length
133
+ size: finalBuffer.length
103
134
  });
104
- newAsset.setContentsReader(__rspack_external__webiny_api_file_manager_features_assetDelivery_transformation_index_js_08a35114.CallableContentsReader.create(()=>optimizedBuffer));
135
+ newAsset.setContentsReader(CallableContentsReader.create(()=>finalBuffer));
105
136
  await promises.mkdir(node_path.dirname(optimizedFilePath), {
106
137
  recursive: true
107
138
  });
@@ -115,34 +146,16 @@ class AssetTransformationStrategyImpl {
115
146
  "webp"
116
147
  ].includes(asset.getExtension());
117
148
  }
118
- optimizePng(buffer) {
119
- return sharp(buffer).resize({
120
- width: 2560,
121
- withoutEnlargement: true,
122
- fit: "inside"
123
- }).png({
124
- compressionLevel: 9,
125
- adaptiveFiltering: true,
126
- force: true
127
- }).withMetadata();
128
- }
129
- optimizeJpeg(buffer) {
130
- return sharp(buffer).resize({
131
- width: 2560,
132
- withoutEnlargement: true,
133
- fit: "inside"
134
- }).withMetadata().toFormat("jpeg", {
135
- quality: 90
136
- });
137
- }
138
149
  }
139
- const LocalSharpTransform = AssetTransformationStrategy.createImplementation({
140
- implementation: AssetTransformationStrategyImpl,
150
+ const LocalSharpTransformImpl = ImageAssetTypeHandler.createImplementation({
151
+ implementation: LocalSharpTransform,
141
152
  dependencies: [
142
- FileManagerServerConfig,
143
- LocalAssetDeliveryConfig
153
+ LocalStoragePath,
154
+ LocalAssetDeliveryConfig,
155
+ IdentityContext,
156
+ GetFileUseCase
144
157
  ]
145
158
  });
146
- export { LocalSharpTransform };
159
+ export { LocalSharpTransform, LocalSharpTransformImpl };
147
160
 
148
161
  //# sourceMappingURL=LocalSharpTransform.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assetDelivery/LocalSharpTransform.js","sources":["../../src/assetDelivery/LocalSharpTransform.ts"],"sourcesContent":["import sharp from \"sharp\";\nimport type { Sharp } from \"sharp\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { AssetRequestOptions } from \"@webiny/api-file-manager/exports/api/file-manager/assetDelivery.js\";\nimport { AssetTransformationStrategy as AssetTransformationStrategyAbstraction } from \"@webiny/api-file-manager/exports/api/file-manager/assetDelivery.js\";\nimport { WidthCollection } from \"@webiny/api-file-manager/features/assetDelivery/transformation/index.js\";\nimport * as utils from \"@webiny/api-file-manager/features/assetDelivery/transformation/index.js\";\nimport { CallableContentsReader } from \"@webiny/api-file-manager/features/assetDelivery/transformation/index.js\";\nimport { AssetKeyGenerator } from \"@webiny/api-file-manager/features/assetDelivery/transformation/index.js\";\nimport { LocalAssetDeliveryConfig } from \"~/assetDelivery/abstractions.js\";\nimport { FileManagerServerConfig } from \"~/features/FileManagerServerConfig/abstractions.js\";\nimport type { ILocalAssetDeliveryConfig } from \"~/assetDelivery/abstractions.js\";\n\nclass AssetTransformationStrategyImpl implements AssetTransformationStrategyAbstraction.Interface {\n private readonly storagePath: string;\n private readonly imageResizeWidths: number[];\n\n constructor(\n serverConfig: FileManagerServerConfig.Interface,\n config: ILocalAssetDeliveryConfig\n ) {\n this.storagePath = serverConfig.storagePath;\n this.imageResizeWidths = config.imageResizeWidths;\n }\n\n async transform(\n assetRequest: AssetTransformationStrategyAbstraction.AssetRequest,\n asset: AssetTransformationStrategyAbstraction.Asset\n ): Promise<AssetTransformationStrategyAbstraction.Asset> {\n if (!utils.SUPPORTED_TRANSFORMABLE_IMAGES.includes(asset.getExtension())) {\n console.log(\n `Transformations/optimizations of ${asset.getContentType()} assets are not supported. Skipping.`\n );\n return asset;\n }\n\n // oxlint-disable-next-line typescript/no-unused-vars\n const { original, ...options } = assetRequest.getOptions();\n\n const transformedAsset = asset.clone();\n\n if (Object.keys(options).length > 0) {\n return this.transformAsset(transformedAsset, options);\n }\n\n return this.optimizeAsset(transformedAsset);\n }\n\n private async transformAsset(\n asset: AssetTransformationStrategyAbstraction.Asset,\n options: Omit<AssetRequestOptions, \"original\">\n ) {\n if (options.width) {\n const assetKey = AssetKeyGenerator.create(asset);\n const transformedAssetKey = assetKey.getTransformedImageKey(options);\n const transformedFilePath = path.join(this.storagePath, transformedAssetKey);\n\n try {\n const buffer = await fs.readFile(transformedFilePath);\n\n const newAsset = asset.withProps({ size: buffer.length });\n newAsset.setContentsReader(CallableContentsReader.create(() => buffer));\n\n console.log(`Return a previously transformed asset`, {\n key: transformedAssetKey,\n size: newAsset.getSize()\n });\n\n return newAsset;\n } catch {\n const optimizedImage = await this.optimizeAsset(asset);\n\n const widths = WidthCollection.create(this.imageResizeWidths);\n const width = widths.getClosestOrMax(options.width);\n\n console.log(`Resize the asset (width: ${width})`);\n const buffer = await optimizedImage.getContents();\n const transformedBuffer = await sharp(buffer, {\n animated: this.isAssetAnimated(asset)\n })\n .withMetadata()\n .resize({ width, withoutEnlargement: true })\n .toBuffer();\n\n const newAsset = asset.withProps({ size: transformedBuffer.length });\n newAsset.setContentsReader(CallableContentsReader.create(() => transformedBuffer));\n\n await fs.mkdir(path.dirname(transformedFilePath), { recursive: true });\n await fs.writeFile(transformedFilePath, await newAsset.getContents());\n\n console.log(`Return the resized asset`, {\n key: transformedAssetKey,\n size: newAsset.getSize()\n });\n\n return newAsset;\n }\n }\n\n return asset;\n }\n\n private async optimizeAsset(asset: AssetTransformationStrategyAbstraction.Asset) {\n console.log(\"Optimize asset\", {\n id: asset.getId(),\n key: asset.getKey(),\n size: asset.getSize(),\n type: asset.getContentType()\n });\n\n const assetKey = AssetKeyGenerator.create(asset);\n const optimizedAssetKey = assetKey.getOptimizedImageKey();\n const optimizedFilePath = path.join(this.storagePath, optimizedAssetKey);\n\n try {\n const buffer = await fs.readFile(optimizedFilePath);\n\n console.log(\"Return a previously optimized asset\", optimizedAssetKey);\n\n const newAsset = asset.withProps({ size: buffer.length });\n newAsset.setContentsReader(CallableContentsReader.create(() => buffer));\n\n return newAsset;\n } catch {\n console.log(\"Create an optimized version of the original asset\", asset.getKey());\n const buffer = await asset.getContents();\n\n const optimizationMap: Record<string, ((buffer: Buffer) => Sharp) | undefined> = {\n \"image/png\": (buffer: Buffer) => this.optimizePng(buffer),\n \"image/jpeg\": (buffer: Buffer) => this.optimizeJpeg(buffer),\n \"image/jpg\": (buffer: Buffer) => this.optimizeJpeg(buffer)\n };\n\n const optimization = optimizationMap[asset.getContentType()];\n\n if (!optimization) {\n console.log(`No optimizations defined for ${asset.getContentType()}`);\n return asset;\n }\n\n const optimizedBuffer = await optimization(buffer).toBuffer();\n\n console.log(\"Optimized asset size\", optimizedBuffer.length);\n\n const newAsset = asset.withProps({ size: optimizedBuffer.length });\n newAsset.setContentsReader(CallableContentsReader.create(() => optimizedBuffer));\n\n await fs.mkdir(path.dirname(optimizedFilePath), { recursive: true });\n await fs.writeFile(optimizedFilePath, await newAsset.getContents());\n\n return newAsset;\n }\n }\n\n private isAssetAnimated(asset: AssetTransformationStrategyAbstraction.Asset) {\n return [\"gif\", \"webp\"].includes(asset.getExtension());\n }\n\n private optimizePng(buffer: Buffer) {\n return sharp(buffer)\n .resize({ width: 2560, withoutEnlargement: true, fit: \"inside\" })\n .png({ compressionLevel: 9, adaptiveFiltering: true, force: true })\n .withMetadata();\n }\n\n private optimizeJpeg(buffer: Buffer) {\n return sharp(buffer)\n .resize({ width: 2560, withoutEnlargement: true, fit: \"inside\" })\n .withMetadata()\n .toFormat(\"jpeg\", { quality: 90 });\n }\n}\n\nexport const LocalSharpTransform = AssetTransformationStrategyAbstraction.createImplementation({\n implementation: AssetTransformationStrategyImpl,\n dependencies: [FileManagerServerConfig, LocalAssetDeliveryConfig]\n});\n"],"names":["AssetTransformationStrategyImpl","serverConfig","config","assetRequest","asset","utils","console","original","options","transformedAsset","Object","assetKey","AssetKeyGenerator","transformedAssetKey","transformedFilePath","path","buffer","fs","newAsset","CallableContentsReader","optimizedImage","widths","WidthCollection","width","transformedBuffer","sharp","optimizedAssetKey","optimizedFilePath","optimizationMap","optimization","optimizedBuffer","LocalSharpTransform","AssetTransformationStrategyAbstraction","FileManagerServerConfig","LocalAssetDeliveryConfig"],"mappings":"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"}
1
+ {"version":3,"file":"assetDelivery/LocalSharpTransform.js","sources":["../../src/assetDelivery/LocalSharpTransform.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { IdentityContext } from \"@webiny/api-core/features/security/IdentityContext/index.js\";\nimport { GetFileUseCase } from \"@webiny/api-file-manager/features/file/GetFile/index.js\";\nimport {\n contentTypeForFormat,\n DEFAULT_IMAGE_QUALITY,\n type ImageFormat\n} from \"@webiny/api-file-manager/features/assetDelivery/transformation/index.js\";\nimport type {\n AssetImageEdit,\n Framing,\n ImageRequestOptions\n} from \"@webiny/api-file-manager/features/assetDelivery/assetTypes/image/imageTypes.js\";\nimport { normalizeImageOptions } from \"@webiny/api-file-manager/features/assetDelivery/assetTypes/image/normalizeImageOptions.js\";\nimport { CallableContentsReader } from \"@webiny/api-file-manager/features/assetDelivery/transformation/index.js\";\nimport { AssetKeyGenerator } from \"@webiny/api-file-manager/features/assetDelivery/assetTypes/image/index.js\";\nimport { ImageAssetTypeHandler } from \"@webiny/api-file-manager/features/assetDelivery/assetTypes/image/index.js\";\nimport type { IAssetTypeHandler } from \"@webiny/api-file-manager/features/assetDelivery/abstractions/AssetType.js\";\nimport type { Asset } from \"@webiny/api-file-manager/delivery/AssetDelivery/Asset.js\";\nimport type { AssetRequest } from \"@webiny/api-file-manager/delivery/AssetDelivery/AssetRequest.js\";\nimport type { SharpTransformer } from \"@webiny/api-file-manager/features/assetDelivery/assetTypes/image/SharpTransformer.js\";\nimport { LocalStoragePath } from \"~/assetDelivery/abstractions.js\";\nimport { LocalAssetDeliveryConfig } from \"~/assetDelivery/abstractions.js\";\nimport type { ILocalAssetDeliveryConfig } from \"~/assetDelivery/abstractions.js\";\n\ntype TransformOptions = Omit<ImageRequestOptions, \"original\" | \"crop\" | \"focal\" | \"aspectRatio\">;\n\nconst hasTransform = (options: TransformOptions): boolean => {\n return (\n options.width !== undefined || options.format !== undefined || options.quality !== undefined\n );\n};\n\nexport class LocalSharpTransform implements IAssetTypeHandler {\n private readonly storagePath: string;\n private readonly imageResizeWidths: number[];\n private readonly imageQuality: Record<ImageFormat, number>;\n private readonly identityContext: IdentityContext.Interface;\n private readonly getFile: GetFileUseCase.Interface;\n private transformer?: SharpTransformer;\n\n constructor(\n storagePath: string,\n config: ILocalAssetDeliveryConfig,\n identityContext: IdentityContext.Interface,\n getFile: GetFileUseCase.Interface\n ) {\n this.storagePath = storagePath;\n this.imageResizeWidths = config.imageResizeWidths;\n this.imageQuality = { ...DEFAULT_IMAGE_QUALITY, ...(config.imageQuality ?? {}) };\n this.identityContext = identityContext;\n this.getFile = getFile;\n }\n\n private async getTransformer(): Promise<SharpTransformer> {\n if (!this.transformer) {\n const { SharpTransformer } =\n await import(\"@webiny/api-file-manager/features/assetDelivery/assetTypes/image/SharpTransformer.js\");\n this.transformer = new SharpTransformer();\n }\n return this.transformer;\n }\n\n async handle(assetRequest: AssetRequest, asset: Asset): Promise<Asset> {\n const rawQuery = assetRequest.getOptions() as Record<string, any>;\n const acceptHeader = assetRequest.getContext<{ accept?: string }>().accept;\n // oxlint-disable-next-line typescript/no-unused-vars\n const { original, crop, focal, aspectRatio, ...options } = normalizeImageOptions(\n rawQuery,\n acceptHeader\n );\n\n const assetCrop = await this.loadAssetCrop(asset.getId());\n const framing: Framing = {\n crop: crop ?? assetCrop,\n focal,\n aspectRatio\n };\n\n const transformedAsset = asset.clone();\n\n if (hasTransform(options)) {\n return this.transformAsset(transformedAsset, options, framing);\n }\n\n return this.optimizeAsset(transformedAsset, framing);\n }\n\n private async loadAssetCrop(fileId: string) {\n const result = await this.identityContext.withoutAuthorization(() =>\n this.getFile.execute(fileId)\n );\n\n if (result.isFail()) {\n return undefined;\n }\n\n const imageEdit = result.value.metadata?.imageEdit as AssetImageEdit | undefined;\n return imageEdit?.crop;\n }\n\n private async transformAsset(asset: Asset, options: TransformOptions, framing: Framing) {\n const assetKey = AssetKeyGenerator.create(asset, framing);\n const transformedAssetKey = assetKey.getTransformedImageKey(options);\n const transformedFilePath = path.join(this.storagePath, transformedAssetKey);\n\n const contentType = options.format\n ? contentTypeForFormat(options.format)\n : asset.getContentType();\n\n try {\n const buffer = await fs.readFile(transformedFilePath);\n\n const newAsset = asset.withProps({ size: buffer.length, contentType });\n newAsset.setContentsReader(CallableContentsReader.create(() => buffer));\n\n console.log(`Return a previously transformed asset`, {\n key: transformedAssetKey,\n size: newAsset.getSize()\n });\n\n return newAsset;\n } catch {\n const optimizedImage = await this.optimizeAsset(asset, framing);\n\n console.log(`Transform the asset`, options);\n const baseBuffer = await optimizedImage.getContents();\n const transformer = await this.getTransformer();\n const { buffer: transformedBuffer, contentType: outputContentType } =\n await transformer.transformBuffer({\n buffer: baseBuffer,\n animated: this.isAssetAnimated(asset),\n sourceContentType: asset.getContentType(),\n widths: this.imageResizeWidths,\n options,\n qualityDefaults: this.imageQuality\n });\n\n const newAsset = asset.withProps({\n size: transformedBuffer.length,\n contentType: outputContentType\n });\n newAsset.setContentsReader(CallableContentsReader.create(() => transformedBuffer));\n\n await fs.mkdir(path.dirname(transformedFilePath), { recursive: true });\n await fs.writeFile(transformedFilePath, await newAsset.getContents());\n\n console.log(`Return the transformed asset`, {\n key: transformedAssetKey,\n size: newAsset.getSize(),\n contentType: newAsset.getContentType()\n });\n\n return newAsset;\n }\n }\n\n private async optimizeAsset(asset: Asset, framing: Framing) {\n console.log(\"Optimize asset\", {\n id: asset.getId(),\n key: asset.getKey(),\n size: asset.getSize(),\n type: asset.getContentType()\n });\n\n const assetKey = AssetKeyGenerator.create(asset, framing);\n const optimizedAssetKey = assetKey.getOptimizedImageKey();\n const optimizedFilePath = path.join(this.storagePath, optimizedAssetKey);\n\n try {\n const buffer = await fs.readFile(optimizedFilePath);\n\n console.log(\"Return a previously optimized asset\", optimizedAssetKey);\n\n const newAsset = asset.withProps({ size: buffer.length });\n newAsset.setContentsReader(CallableContentsReader.create(() => buffer));\n\n return newAsset;\n } catch {\n console.log(\"Create an optimized version of the original asset\", asset.getKey());\n let buffer = await asset.getContents();\n\n const transformer = await this.getTransformer();\n const framed = await transformer.extractFramedRegion(buffer, framing);\n const cropped = framed !== buffer;\n buffer = framed;\n\n const contentType = asset.getContentType();\n const canOptimize =\n contentType === \"image/png\" ||\n contentType === \"image/jpeg\" ||\n contentType === \"image/jpg\";\n\n if (!canOptimize && !cropped) {\n console.log(`No optimizations defined for ${contentType}`);\n return asset;\n }\n\n let finalBuffer = buffer;\n if (canOptimize) {\n const pipeline =\n contentType === \"image/png\"\n ? await transformer.optimizePng(buffer)\n : await transformer.optimizeJpeg(buffer);\n finalBuffer = await pipeline.toBuffer();\n }\n\n console.log(\"Optimized asset size\", finalBuffer.length);\n\n const newAsset = asset.withProps({ size: finalBuffer.length });\n newAsset.setContentsReader(CallableContentsReader.create(() => finalBuffer));\n\n await fs.mkdir(path.dirname(optimizedFilePath), { recursive: true });\n await fs.writeFile(optimizedFilePath, await newAsset.getContents());\n\n return newAsset;\n }\n }\n\n private isAssetAnimated(asset: Asset) {\n return [\"gif\", \"webp\"].includes(asset.getExtension());\n }\n}\n\nexport const LocalSharpTransformImpl = ImageAssetTypeHandler.createImplementation({\n implementation: LocalSharpTransform,\n dependencies: [LocalStoragePath, LocalAssetDeliveryConfig, IdentityContext, GetFileUseCase]\n});\n"],"names":["hasTransform","options","undefined","LocalSharpTransform","storagePath","config","identityContext","getFile","DEFAULT_IMAGE_QUALITY","SharpTransformer","assetRequest","asset","rawQuery","acceptHeader","original","crop","focal","aspectRatio","normalizeImageOptions","assetCrop","framing","transformedAsset","fileId","result","imageEdit","assetKey","AssetKeyGenerator","transformedAssetKey","transformedFilePath","path","contentType","contentTypeForFormat","buffer","fs","newAsset","CallableContentsReader","console","optimizedImage","baseBuffer","transformer","transformedBuffer","outputContentType","optimizedAssetKey","optimizedFilePath","framed","cropped","canOptimize","finalBuffer","pipeline","LocalSharpTransformImpl","ImageAssetTypeHandler","LocalStoragePath","LocalAssetDeliveryConfig","IdentityContext","GetFileUseCase"],"mappings":"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"}
@@ -1,5 +1,8 @@
1
+ import type { ImageFormat } from "@webiny/api-file-manager/features/assetDelivery/transformation/index.js";
1
2
  export interface ILocalAssetDeliveryConfig {
2
3
  imageResizeWidths: number[];
4
+ imageQuality?: Partial<Record<ImageFormat, number>>;
3
5
  assetStreamingMaxSize: number;
4
6
  }
5
7
  export declare const LocalAssetDeliveryConfig: import("@webiny/di").Abstraction<ILocalAssetDeliveryConfig>;
8
+ export declare const LocalStoragePath: import("@webiny/di").Abstraction<string>;
@@ -1,5 +1,6 @@
1
1
  import { createAbstraction } from "@webiny/feature/api";
2
2
  const LocalAssetDeliveryConfig = createAbstraction("AssetDelivery/LocalConfig");
3
- export { LocalAssetDeliveryConfig };
3
+ const LocalStoragePath = createAbstraction("AssetDelivery/LocalStoragePath");
4
+ export { LocalAssetDeliveryConfig, LocalStoragePath };
4
5
 
5
6
  //# sourceMappingURL=abstractions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assetDelivery/abstractions.js","sources":["../../src/assetDelivery/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\n\nexport interface ILocalAssetDeliveryConfig {\n imageResizeWidths: number[];\n assetStreamingMaxSize: number;\n}\n\nexport const LocalAssetDeliveryConfig = createAbstraction<ILocalAssetDeliveryConfig>(\n \"AssetDelivery/LocalConfig\"\n);\n"],"names":["LocalAssetDeliveryConfig","createAbstraction"],"mappings":";AAOO,MAAMA,2BAA2BC,kBACpC"}
1
+ {"version":3,"file":"assetDelivery/abstractions.js","sources":["../../src/assetDelivery/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\nimport type { ImageFormat } from \"@webiny/api-file-manager/features/assetDelivery/transformation/index.js\";\n\nexport interface ILocalAssetDeliveryConfig {\n imageResizeWidths: number[];\n imageQuality?: Partial<Record<ImageFormat, number>>;\n assetStreamingMaxSize: number;\n}\n\nexport const LocalAssetDeliveryConfig = createAbstraction<ILocalAssetDeliveryConfig>(\n \"AssetDelivery/LocalConfig\"\n);\n\nexport const LocalStoragePath = createAbstraction<string>(\"AssetDelivery/LocalStoragePath\");\n"],"names":["LocalAssetDeliveryConfig","createAbstraction","LocalStoragePath"],"mappings":";AASO,MAAMA,2BAA2BC,kBACpC;AAGG,MAAMC,mBAAmBD,kBAA0B"}
@@ -1,11 +1,12 @@
1
1
  import { createFeature } from "@webiny/feature/api";
2
- import { LocalAssetDeliveryConfig } from "./abstractions.js";
2
+ import { LocalAssetDeliveryConfig, LocalStoragePath } from "./abstractions.js";
3
3
  import { LocalAssetResolver } from "./LocalAssetResolver.js";
4
4
  import { LocalOutputStrategy } from "./LocalOutputStrategy.js";
5
- import { LocalSharpTransform } from "./LocalSharpTransform.js";
5
+ import { LazyLocalSharpTransformImpl } from "./LazyLocalSharpTransform.js";
6
6
  const createLocalAssetDeliveryFeature = (params = {})=>createFeature({
7
7
  name: "AssetDelivery/Local",
8
8
  register (container) {
9
+ container.registerInstance(LocalStoragePath, process.env.WEBINY_LOCAL_STORAGE_PATH);
9
10
  container.registerInstance(LocalAssetDeliveryConfig, {
10
11
  imageResizeWidths: params.imageResizeWidths ?? [
11
12
  128,
@@ -19,11 +20,12 @@ const createLocalAssetDeliveryFeature = (params = {})=>createFeature({
19
20
  2048,
20
21
  3840
21
22
  ],
23
+ imageQuality: params.imageQuality ?? {},
22
24
  assetStreamingMaxSize: params.assetStreamingMaxSize ?? 4718592
23
25
  });
24
26
  container.register(LocalAssetResolver);
25
27
  container.register(LocalOutputStrategy);
26
- container.register(LocalSharpTransform);
28
+ if ("asset-delivery" === process.env.WEBINY_FUNCTION_TYPE) container.register(LazyLocalSharpTransformImpl).inSingletonScope();
27
29
  }
28
30
  });
29
31
  export { createLocalAssetDeliveryFeature };
@@ -1 +1 @@
1
- {"version":3,"file":"assetDelivery/feature.js","sources":["../../src/assetDelivery/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { LocalAssetDeliveryConfig } from \"./abstractions.js\";\nimport type { AssetDeliveryParams } from \"./types.js\";\nimport { LocalAssetResolver } from \"./LocalAssetResolver.js\";\nimport { LocalOutputStrategy } from \"./LocalOutputStrategy.js\";\nimport { LocalSharpTransform } from \"./LocalSharpTransform.js\";\n\nexport const createLocalAssetDeliveryFeature = (params: AssetDeliveryParams = {}) => {\n return createFeature({\n name: \"AssetDelivery/Local\",\n register(container) {\n container.registerInstance(LocalAssetDeliveryConfig, {\n imageResizeWidths: params.imageResizeWidths ?? [\n 128, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840\n ],\n assetStreamingMaxSize: params.assetStreamingMaxSize ?? 4718592\n });\n\n container.register(LocalAssetResolver);\n container.register(LocalOutputStrategy);\n container.register(LocalSharpTransform);\n }\n });\n};\n"],"names":["createLocalAssetDeliveryFeature","params","createFeature","container","LocalAssetDeliveryConfig","LocalAssetResolver","LocalOutputStrategy","LocalSharpTransform"],"mappings":";;;;;AAOO,MAAMA,kCAAkC,CAACC,SAA8B,CAAC,CAAC,GACrEC,cAAc;QACjB,MAAM;QACN,UAASC,SAAS;YACdA,UAAU,gBAAgB,CAACC,0BAA0B;gBACjD,mBAAmBH,OAAO,iBAAiB,IAAI;oBAC3C;oBAAK;oBAAK;oBAAK;oBAAK;oBAAK;oBAAM;oBAAM;oBAAM;oBAAM;iBACpD;gBACD,uBAAuBA,OAAO,qBAAqB,IAAI;YAC3D;YAEAE,UAAU,QAAQ,CAACE;YACnBF,UAAU,QAAQ,CAACG;YACnBH,UAAU,QAAQ,CAACI;QACvB;IACJ"}
1
+ {"version":3,"file":"assetDelivery/feature.js","sources":["../../src/assetDelivery/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { LocalStoragePath } from \"./abstractions.js\";\nimport { LocalAssetDeliveryConfig } from \"./abstractions.js\";\nimport type { AssetDeliveryParams } from \"./types.js\";\nimport { LocalAssetResolver } from \"./LocalAssetResolver.js\";\nimport { LocalOutputStrategy } from \"./LocalOutputStrategy.js\";\nimport { LazyLocalSharpTransformImpl } from \"./LazyLocalSharpTransform.js\";\n\nexport const createLocalAssetDeliveryFeature = (params: AssetDeliveryParams = {}) => {\n return createFeature({\n name: \"AssetDelivery/Local\",\n register(container) {\n container.registerInstance(\n LocalStoragePath,\n process.env.WEBINY_LOCAL_STORAGE_PATH as string\n );\n container.registerInstance(LocalAssetDeliveryConfig, {\n imageResizeWidths: params.imageResizeWidths ?? [\n 128, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840\n ],\n imageQuality: params.imageQuality ?? {},\n assetStreamingMaxSize: params.assetStreamingMaxSize ?? 4718592\n });\n\n container.register(LocalAssetResolver);\n container.register(LocalOutputStrategy);\n\n if (process.env.WEBINY_FUNCTION_TYPE === \"asset-delivery\") {\n // Registered eagerly; `sharp` is still loaded lazily, inside the handler.\n container.register(LazyLocalSharpTransformImpl).inSingletonScope();\n }\n }\n });\n};\n"],"names":["createLocalAssetDeliveryFeature","params","createFeature","container","LocalStoragePath","process","LocalAssetDeliveryConfig","LocalAssetResolver","LocalOutputStrategy","LazyLocalSharpTransformImpl"],"mappings":";;;;;AAQO,MAAMA,kCAAkC,CAACC,SAA8B,CAAC,CAAC,GACrEC,cAAc;QACjB,MAAM;QACN,UAASC,SAAS;YACdA,UAAU,gBAAgB,CACtBC,kBACAC,QAAQ,GAAG,CAAC,yBAAyB;YAEzCF,UAAU,gBAAgB,CAACG,0BAA0B;gBACjD,mBAAmBL,OAAO,iBAAiB,IAAI;oBAC3C;oBAAK;oBAAK;oBAAK;oBAAK;oBAAK;oBAAM;oBAAM;oBAAM;oBAAM;iBACpD;gBACD,cAAcA,OAAO,YAAY,IAAI,CAAC;gBACtC,uBAAuBA,OAAO,qBAAqB,IAAI;YAC3D;YAEAE,UAAU,QAAQ,CAACI;YACnBJ,UAAU,QAAQ,CAACK;YAEnB,IAAIH,AAAqC,qBAArCA,QAAQ,GAAG,CAAC,oBAAoB,EAEhCF,UAAU,QAAQ,CAACM,6BAA6B,gBAAgB;QAExE;IACJ"}
@@ -1,5 +1,8 @@
1
+ import type { ImageFormat } from "@webiny/api-file-manager/features/assetDelivery/transformation/index.js";
1
2
  export type AssetDeliveryParams = {
2
3
  imageResizeWidths?: number[];
4
+ /** Per-format encoder quality (1-100). Merged over the built-in defaults. */
5
+ imageQuality?: Partial<Record<ImageFormat, number>>;
3
6
  presignedUrlTtl?: number;
4
7
  assetStreamingMaxSize?: number;
5
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webiny/api-file-manager-server",
3
- "version": "6.6.0-alpha.0",
3
+ "version": "6.6.0-alpha.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -14,25 +14,25 @@
14
14
  "author": "Webiny Ltd",
15
15
  "license": "MIT",
16
16
  "dependencies": {
17
- "@webiny/api-core": "6.6.0-alpha.0",
18
- "@webiny/api-file-manager": "6.6.0-alpha.0",
19
- "@webiny/api-graphql": "6.6.0-alpha.0",
20
- "@webiny/background-tasks": "6.6.0-alpha.0",
21
- "@webiny/event-handler-core": "6.6.0-alpha.0",
22
- "@webiny/feature": "6.6.0-alpha.0",
23
- "@webiny/handler": "6.6.0-alpha.0",
24
- "@webiny/plugins": "6.6.0-alpha.0",
25
- "@webiny/utils": "6.6.0-alpha.0",
26
- "@webiny/validation": "6.6.0-alpha.0",
27
- "exifreader": "4.41.3",
17
+ "@webiny/api-core": "6.6.0-alpha.1",
18
+ "@webiny/api-file-manager": "6.6.0-alpha.1",
19
+ "@webiny/api-graphql": "6.6.0-alpha.1",
20
+ "@webiny/background-tasks": "6.6.0-alpha.1",
21
+ "@webiny/event-handler-core": "6.6.0-alpha.1",
22
+ "@webiny/feature": "6.6.0-alpha.1",
23
+ "@webiny/handler": "6.6.0-alpha.1",
24
+ "@webiny/plugins": "6.6.0-alpha.1",
25
+ "@webiny/utils": "6.6.0-alpha.1",
26
+ "@webiny/validation": "6.6.0-alpha.1",
27
+ "exifreader": "4.43.0",
28
28
  "p-map": "7.0.6",
29
29
  "sharp": "0.35.3"
30
30
  },
31
31
  "devDependencies": {
32
- "@webiny/build-tools": "6.6.0-alpha.0",
32
+ "@webiny/build-tools": "6.6.0-alpha.1",
33
33
  "rimraf": "6.1.3",
34
34
  "typescript": "7.0.2",
35
- "vitest": "4.1.10"
35
+ "vitest": "4.1.11"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
@@ -1,14 +1,23 @@
1
- import { HttpRoute } from "@webiny/event-handler-core";
2
- import type { IHttpRequest, IHttpResponse } from "@webiny/event-handler-core";
1
+ import { HttpRouteDefinition, HttpRouteHandler } from "@webiny/event-handler-core";
3
2
  import { FileManagerServerConfig } from "../../features/FileManagerServerConfig/abstractions.js";
4
- declare class UploadPartRouteImpl implements HttpRoute.Interface {
3
+ declare class UploadPartRouteImpl implements HttpRouteHandler.Interface {
5
4
  private readonly config;
6
- readonly method = "PUT";
7
- readonly path = "/webiny-file-upload/parts";
8
5
  constructor(config: FileManagerServerConfig.Interface);
9
- handle(request: IHttpRequest): Promise<IHttpResponse>;
6
+ handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response): Promise<import("@webiny/event-handler-core").IHttpResponseBuilder>;
10
7
  }
11
8
  export declare const UploadPartRoute: typeof UploadPartRouteImpl & {
12
9
  __abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRoute>;
13
10
  };
11
+ declare class UploadPartRouteDefinitionImpl implements HttpRouteDefinition.Interface {
12
+ readonly name = "upload-part";
13
+ readonly method = "PUT";
14
+ readonly path = "/webiny-file-upload/parts";
15
+ readonly handler: typeof UploadPartRouteImpl & {
16
+ __abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRoute>;
17
+ };
18
+ }
19
+ /** What the router matches on. Zero dependencies, so building it costs nothing. */
20
+ export declare const UploadPartRouteDefinition: typeof UploadPartRouteDefinitionImpl & {
21
+ __abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRouteDefinition>;
22
+ };
14
23
  export {};
@@ -1,44 +1,42 @@
1
1
  import node_path from "node:path";
2
2
  import { createHash } from "node:crypto";
3
3
  import { mkdir, writeFile } from "node:fs/promises";
4
- import { HttpRoute } from "@webiny/event-handler-core";
4
+ import { HttpRouteDefinition, HttpRouteHandler } from "@webiny/event-handler-core";
5
5
  import { verifyUploadToken } from "../../utils/uploadToken.js";
6
6
  import { FileManagerServerConfig } from "../../features/FileManagerServerConfig/abstractions.js";
7
- import { isPathContained, json, toBuffer } from "../utils.js";
7
+ import { isPathContained, toBuffer } from "../utils.js";
8
8
  class UploadPartRouteImpl {
9
9
  constructor(config){
10
10
  this.config = config;
11
- this.method = "PUT";
12
- this.path = "/webiny-file-upload/parts";
13
11
  }
14
- async handle(request) {
12
+ async handle(request, response) {
15
13
  const storagePath = this.config.storagePath;
16
14
  const secret = this.config.uploadSecret;
17
15
  const query = request.query ?? {};
18
16
  const uploadId = query["uploadId"];
19
17
  const partNumberStr = query["partNumber"];
20
18
  const token = query["token"];
21
- if (!uploadId || !partNumberStr || !token) return json(400, {
19
+ if (!uploadId || !partNumberStr || !token) return response.status(400).json({
22
20
  error: "Missing uploadId, partNumber, or token."
23
21
  });
24
22
  const partNumber = parseInt(partNumberStr, 10);
25
- if (isNaN(partNumber) || partNumber < 1) return json(400, {
23
+ if (isNaN(partNumber) || partNumber < 1) return response.status(400).json({
26
24
  error: "Invalid partNumber."
27
25
  });
28
26
  let payload;
29
27
  try {
30
28
  payload = verifyUploadToken(token, secret);
31
29
  } catch (err) {
32
- return json(400, {
30
+ return response.status(400).json({
33
31
  error: err instanceof Error ? err.message : "Invalid token."
34
32
  });
35
33
  }
36
34
  const expectedKey = `tenants/${payload.tenantId}/multipart/${uploadId}/part-${partNumber}`;
37
- if (payload.key !== expectedKey) return json(400, {
35
+ if (payload.key !== expectedKey) return response.status(400).json({
38
36
  error: "Token key mismatch."
39
37
  });
40
38
  const destPath = node_path.join(storagePath, expectedKey);
41
- if (!isPathContained(destPath, storagePath)) return json(400, {
39
+ if (!isPathContained(destPath, storagePath)) return response.status(400).json({
42
40
  error: "Invalid path."
43
41
  });
44
42
  await mkdir(node_path.dirname(destPath), {
@@ -47,20 +45,27 @@ class UploadPartRouteImpl {
47
45
  const body = toBuffer(request.body);
48
46
  await writeFile(destPath, body);
49
47
  const etag = createHash("md5").update(body).digest("hex");
50
- return {
51
- statusCode: 200,
52
- headers: {
53
- ETag: etag
54
- }
55
- };
48
+ return response.header("etag", etag);
56
49
  }
57
50
  }
58
- const UploadPartRoute = HttpRoute.createImplementation({
51
+ const UploadPartRoute = HttpRouteHandler.createImplementation({
59
52
  implementation: UploadPartRouteImpl,
60
53
  dependencies: [
61
54
  FileManagerServerConfig
62
55
  ]
63
56
  });
64
- export { UploadPartRoute };
57
+ class UploadPartRouteDefinitionImpl {
58
+ constructor(){
59
+ this.name = "upload-part";
60
+ this.method = "PUT";
61
+ this.path = "/webiny-file-upload/parts";
62
+ this.handler = UploadPartRoute;
63
+ }
64
+ }
65
+ const UploadPartRouteDefinition = HttpRouteDefinition.createImplementation({
66
+ implementation: UploadPartRouteDefinitionImpl,
67
+ dependencies: []
68
+ });
69
+ export { UploadPartRoute, UploadPartRouteDefinition };
65
70
 
66
71
  //# sourceMappingURL=UploadPartRoute.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes/UploadPartRoute/UploadPartRoute.js","sources":["../../../src/routes/UploadPartRoute/UploadPartRoute.ts"],"sourcesContent":["import path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { HttpRoute } from \"@webiny/event-handler-core\";\nimport type { IHttpRequest, IHttpResponse } from \"@webiny/event-handler-core\";\nimport { verifyUploadToken } from \"~/utils/uploadToken.js\";\nimport { FileManagerServerConfig } from \"~/features/FileManagerServerConfig/abstractions.js\";\nimport { isPathContained, json, toBuffer } from \"../utils.js\";\n\nclass UploadPartRouteImpl implements HttpRoute.Interface {\n public readonly method = \"PUT\";\n public readonly path = \"/webiny-file-upload/parts\";\n\n public constructor(private readonly config: FileManagerServerConfig.Interface) {}\n\n public async handle(request: IHttpRequest): Promise<IHttpResponse> {\n const storagePath = this.config.storagePath;\n const secret = this.config.uploadSecret;\n\n const query = request.query ?? {};\n const uploadId = query[\"uploadId\"];\n const partNumberStr = query[\"partNumber\"];\n const token = query[\"token\"];\n\n if (!uploadId || !partNumberStr || !token) {\n return json(400, { error: \"Missing uploadId, partNumber, or token.\" });\n }\n\n const partNumber = parseInt(partNumberStr, 10);\n if (isNaN(partNumber) || partNumber < 1) {\n return json(400, { error: \"Invalid partNumber.\" });\n }\n\n let payload;\n try {\n payload = verifyUploadToken(token, secret);\n } catch (err) {\n return json(400, { error: err instanceof Error ? err.message : \"Invalid token.\" });\n }\n\n const expectedKey = `tenants/${payload.tenantId}/multipart/${uploadId}/part-${partNumber}`;\n if (payload.key !== expectedKey) {\n return json(400, { error: \"Token key mismatch.\" });\n }\n\n const destPath = path.join(storagePath, expectedKey);\n if (!isPathContained(destPath, storagePath)) {\n return json(400, { error: \"Invalid path.\" });\n }\n\n await mkdir(path.dirname(destPath), { recursive: true });\n\n const body = toBuffer(request.body);\n await writeFile(destPath, body);\n\n const etag = createHash(\"md5\").update(body).digest(\"hex\");\n\n return { statusCode: 200, headers: { ETag: etag } };\n }\n}\n\nexport const UploadPartRoute = HttpRoute.createImplementation({\n implementation: UploadPartRouteImpl,\n dependencies: [FileManagerServerConfig]\n});\n"],"names":["UploadPartRouteImpl","config","request","storagePath","secret","query","uploadId","partNumberStr","token","json","partNumber","parseInt","isNaN","payload","verifyUploadToken","err","Error","expectedKey","destPath","path","isPathContained","mkdir","body","toBuffer","writeFile","etag","createHash","UploadPartRoute","HttpRoute","FileManagerServerConfig"],"mappings":";;;;;;;AASA,MAAMA;IAIF,YAAoCC,MAAyC,CAAE;aAA3CA,MAAM,GAANA;aAHpB,MAAM,GAAG;aACT,IAAI,GAAG;IAEyD;IAEhF,MAAa,OAAOC,OAAqB,EAA0B;QAC/D,MAAMC,cAAc,IAAI,CAAC,MAAM,CAAC,WAAW;QAC3C,MAAMC,SAAS,IAAI,CAAC,MAAM,CAAC,YAAY;QAEvC,MAAMC,QAAQH,QAAQ,KAAK,IAAI,CAAC;QAChC,MAAMI,WAAWD,KAAK,CAAC,WAAW;QAClC,MAAME,gBAAgBF,KAAK,CAAC,aAAa;QACzC,MAAMG,QAAQH,KAAK,CAAC,QAAQ;QAE5B,IAAI,CAACC,YAAY,CAACC,iBAAiB,CAACC,OAChC,OAAOC,KAAK,KAAK;YAAE,OAAO;QAA0C;QAGxE,MAAMC,aAAaC,SAASJ,eAAe;QAC3C,IAAIK,MAAMF,eAAeA,aAAa,GAClC,OAAOD,KAAK,KAAK;YAAE,OAAO;QAAsB;QAGpD,IAAII;QACJ,IAAI;YACAA,UAAUC,kBAAkBN,OAAOJ;QACvC,EAAE,OAAOW,KAAK;YACV,OAAON,KAAK,KAAK;gBAAE,OAAOM,eAAeC,QAAQD,IAAI,OAAO,GAAG;YAAiB;QACpF;QAEA,MAAME,cAAc,CAAC,QAAQ,EAAEJ,QAAQ,QAAQ,CAAC,WAAW,EAAEP,SAAS,MAAM,EAAEI,YAAY;QAC1F,IAAIG,QAAQ,GAAG,KAAKI,aAChB,OAAOR,KAAK,KAAK;YAAE,OAAO;QAAsB;QAGpD,MAAMS,WAAWC,UAAAA,IAAS,CAAChB,aAAac;QACxC,IAAI,CAACG,gBAAgBF,UAAUf,cAC3B,OAAOM,KAAK,KAAK;YAAE,OAAO;QAAgB;QAG9C,MAAMY,MAAMF,UAAAA,OAAY,CAACD,WAAW;YAAE,WAAW;QAAK;QAEtD,MAAMI,OAAOC,SAASrB,QAAQ,IAAI;QAClC,MAAMsB,UAAUN,UAAUI;QAE1B,MAAMG,OAAOC,WAAW,OAAO,MAAM,CAACJ,MAAM,MAAM,CAAC;QAEnD,OAAO;YAAE,YAAY;YAAK,SAAS;gBAAE,MAAMG;YAAK;QAAE;IACtD;AACJ;AAEO,MAAME,kBAAkBC,UAAU,oBAAoB,CAAC;IAC1D,gBAAgB5B;IAChB,cAAc;QAAC6B;KAAwB;AAC3C"}
1
+ {"version":3,"file":"routes/UploadPartRoute/UploadPartRoute.js","sources":["../../../src/routes/UploadPartRoute/UploadPartRoute.ts"],"sourcesContent":["import path from \"node:path\";\nimport { createHash } from \"node:crypto\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { HttpRouteDefinition, HttpRouteHandler } from \"@webiny/event-handler-core\";\nimport { verifyUploadToken } from \"~/utils/uploadToken.js\";\nimport { FileManagerServerConfig } from \"~/features/FileManagerServerConfig/abstractions.js\";\nimport { isPathContained, toBuffer } from \"../utils.js\";\n\nclass UploadPartRouteImpl implements HttpRouteHandler.Interface {\n public constructor(private readonly config: FileManagerServerConfig.Interface) {}\n\n public async handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) {\n const storagePath = this.config.storagePath;\n const secret = this.config.uploadSecret;\n\n const query = request.query ?? {};\n const uploadId = query[\"uploadId\"];\n const partNumberStr = query[\"partNumber\"];\n const token = query[\"token\"];\n\n if (!uploadId || !partNumberStr || !token) {\n return response.status(400).json({ error: \"Missing uploadId, partNumber, or token.\" });\n }\n\n const partNumber = parseInt(partNumberStr, 10);\n if (isNaN(partNumber) || partNumber < 1) {\n return response.status(400).json({ error: \"Invalid partNumber.\" });\n }\n\n let payload;\n try {\n payload = verifyUploadToken(token, secret);\n } catch (err) {\n return response\n .status(400)\n .json({ error: err instanceof Error ? err.message : \"Invalid token.\" });\n }\n\n const expectedKey = `tenants/${payload.tenantId}/multipart/${uploadId}/part-${partNumber}`;\n if (payload.key !== expectedKey) {\n return response.status(400).json({ error: \"Token key mismatch.\" });\n }\n\n const destPath = path.join(storagePath, expectedKey);\n if (!isPathContained(destPath, storagePath)) {\n return response.status(400).json({ error: \"Invalid path.\" });\n }\n\n await mkdir(path.dirname(destPath), { recursive: true });\n\n const body = toBuffer(request.body);\n await writeFile(destPath, body);\n\n const etag = createHash(\"md5\").update(body).digest(\"hex\");\n\n return response.header(\"etag\", etag);\n }\n}\n\nexport const UploadPartRoute = HttpRouteHandler.createImplementation({\n implementation: UploadPartRouteImpl,\n dependencies: [FileManagerServerConfig]\n});\n\nclass UploadPartRouteDefinitionImpl implements HttpRouteDefinition.Interface {\n readonly name = \"upload-part\";\n readonly method = \"PUT\";\n readonly path = \"/webiny-file-upload/parts\";\n readonly handler = UploadPartRoute;\n}\n\n/** What the router matches on. Zero dependencies, so building it costs nothing. */\nexport const UploadPartRouteDefinition = HttpRouteDefinition.createImplementation({\n implementation: UploadPartRouteDefinitionImpl,\n dependencies: []\n});\n"],"names":["UploadPartRouteImpl","config","request","response","storagePath","secret","query","uploadId","partNumberStr","token","partNumber","parseInt","isNaN","payload","verifyUploadToken","err","Error","expectedKey","destPath","path","isPathContained","mkdir","body","toBuffer","writeFile","etag","createHash","UploadPartRoute","HttpRouteHandler","FileManagerServerConfig","UploadPartRouteDefinitionImpl","UploadPartRouteDefinition","HttpRouteDefinition"],"mappings":";;;;;;;AAQA,MAAMA;IACF,YAAoCC,MAAyC,CAAE;aAA3CA,MAAM,GAANA;IAA4C;IAEhF,MAAa,OAAOC,OAAiC,EAAEC,QAAmC,EAAE;QACxF,MAAMC,cAAc,IAAI,CAAC,MAAM,CAAC,WAAW;QAC3C,MAAMC,SAAS,IAAI,CAAC,MAAM,CAAC,YAAY;QAEvC,MAAMC,QAAQJ,QAAQ,KAAK,IAAI,CAAC;QAChC,MAAMK,WAAWD,KAAK,CAAC,WAAW;QAClC,MAAME,gBAAgBF,KAAK,CAAC,aAAa;QACzC,MAAMG,QAAQH,KAAK,CAAC,QAAQ;QAE5B,IAAI,CAACC,YAAY,CAACC,iBAAiB,CAACC,OAChC,OAAON,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAA0C;QAGxF,MAAMO,aAAaC,SAASH,eAAe;QAC3C,IAAII,MAAMF,eAAeA,aAAa,GAClC,OAAOP,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAsB;QAGpE,IAAIU;QACJ,IAAI;YACAA,UAAUC,kBAAkBL,OAAOJ;QACvC,EAAE,OAAOU,KAAK;YACV,OAAOZ,SACF,MAAM,CAAC,KACP,IAAI,CAAC;gBAAE,OAAOY,eAAeC,QAAQD,IAAI,OAAO,GAAG;YAAiB;QAC7E;QAEA,MAAME,cAAc,CAAC,QAAQ,EAAEJ,QAAQ,QAAQ,CAAC,WAAW,EAAEN,SAAS,MAAM,EAAEG,YAAY;QAC1F,IAAIG,QAAQ,GAAG,KAAKI,aAChB,OAAOd,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAsB;QAGpE,MAAMe,WAAWC,UAAAA,IAAS,CAACf,aAAaa;QACxC,IAAI,CAACG,gBAAgBF,UAAUd,cAC3B,OAAOD,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAgB;QAG9D,MAAMkB,MAAMF,UAAAA,OAAY,CAACD,WAAW;YAAE,WAAW;QAAK;QAEtD,MAAMI,OAAOC,SAASrB,QAAQ,IAAI;QAClC,MAAMsB,UAAUN,UAAUI;QAE1B,MAAMG,OAAOC,WAAW,OAAO,MAAM,CAACJ,MAAM,MAAM,CAAC;QAEnD,OAAOnB,SAAS,MAAM,CAAC,QAAQsB;IACnC;AACJ;AAEO,MAAME,kBAAkBC,iBAAiB,oBAAoB,CAAC;IACjE,gBAAgB5B;IAChB,cAAc;QAAC6B;KAAwB;AAC3C;AAEA,MAAMC;;aACO,IAAI,GAAG;aACP,MAAM,GAAG;aACT,IAAI,GAAG;aACP,OAAO,GAAGH;;AACvB;AAGO,MAAMI,4BAA4BC,oBAAoB,oBAAoB,CAAC;IAC9E,gBAAgBF;IAChB,cAAc,EAAE;AACpB"}
@@ -1,9 +1,9 @@
1
1
  import { createFeature } from "@webiny/feature/api";
2
- import { UploadPartRoute } from "./UploadPartRoute.js";
2
+ import { UploadPartRouteDefinition } from "./UploadPartRoute.js";
3
3
  const UploadPartRouteFeature = createFeature({
4
4
  name: "FileManagerServer/UploadPartRoute",
5
5
  register (container) {
6
- container.register(UploadPartRoute);
6
+ container.register(UploadPartRouteDefinition);
7
7
  }
8
8
  });
9
9
  export { UploadPartRouteFeature };
@@ -1 +1 @@
1
- {"version":3,"file":"routes/UploadPartRoute/feature.js","sources":["../../../src/routes/UploadPartRoute/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { UploadPartRoute } from \"./UploadPartRoute.js\";\n\nexport const UploadPartRouteFeature = createFeature({\n name: \"FileManagerServer/UploadPartRoute\",\n register(container) {\n container.register(UploadPartRoute);\n }\n});\n"],"names":["UploadPartRouteFeature","createFeature","container","UploadPartRoute"],"mappings":";;AAGO,MAAMA,yBAAyBC,cAAc;IAChD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;IACvB;AACJ"}
1
+ {"version":3,"file":"routes/UploadPartRoute/feature.js","sources":["../../../src/routes/UploadPartRoute/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { UploadPartRouteDefinition } from \"./UploadPartRoute.js\";\n\nexport const UploadPartRouteFeature = createFeature({\n name: \"FileManagerServer/UploadPartRoute\",\n register(container) {\n container.register(UploadPartRouteDefinition);\n }\n});\n"],"names":["UploadPartRouteFeature","createFeature","container","UploadPartRouteDefinition"],"mappings":";;AAGO,MAAMA,yBAAyBC,cAAc;IAChD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;IACvB;AACJ"}
@@ -1,14 +1,23 @@
1
- import { HttpRoute } from "@webiny/event-handler-core";
2
- import type { IHttpRequest, IHttpResponse } from "@webiny/event-handler-core";
1
+ import { HttpRouteDefinition, HttpRouteHandler } from "@webiny/event-handler-core";
3
2
  import { FileManagerServerConfig } from "../../features/FileManagerServerConfig/abstractions.js";
4
- declare class UploadSingleFileRouteImpl implements HttpRoute.Interface {
3
+ declare class UploadSingleFileRouteImpl implements HttpRouteHandler.Interface {
5
4
  private readonly config;
6
- readonly method = "POST";
7
- readonly path = "/webiny-file-upload";
8
5
  constructor(config: FileManagerServerConfig.Interface);
9
- handle(request: IHttpRequest): Promise<IHttpResponse>;
6
+ handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response): Promise<import("@webiny/event-handler-core").IHttpResponseBuilder>;
10
7
  }
11
8
  export declare const UploadSingleFileRoute: typeof UploadSingleFileRouteImpl & {
12
9
  __abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRoute>;
13
10
  };
11
+ declare class UploadSingleFileRouteDefinitionImpl implements HttpRouteDefinition.Interface {
12
+ readonly name = "upload-single-file";
13
+ readonly method = "POST";
14
+ readonly path = "/webiny-file-upload";
15
+ readonly handler: typeof UploadSingleFileRouteImpl & {
16
+ __abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRoute>;
17
+ };
18
+ }
19
+ /** What the router matches on. Zero dependencies, so building it costs nothing. */
20
+ export declare const UploadSingleFileRouteDefinition: typeof UploadSingleFileRouteDefinitionImpl & {
21
+ __abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core").IHttpRouteDefinition>;
22
+ };
14
23
  export {};
@@ -1,70 +1,78 @@
1
1
  import node_path from "node:path";
2
2
  import { mkdir, writeFile } from "node:fs/promises";
3
- import { HttpRoute } from "@webiny/event-handler-core";
3
+ import { HttpRouteDefinition, HttpRouteHandler } from "@webiny/event-handler-core";
4
4
  import { verifyUploadToken } from "../../utils/uploadToken.js";
5
5
  import { FileManagerServerConfig } from "../../features/FileManagerServerConfig/abstractions.js";
6
- import { getBoundary, isPathContained, json, parseMultipart, toBuffer } from "../utils.js";
6
+ import { getBoundary, isPathContained, parseMultipart, toBuffer } from "../utils.js";
7
7
  class UploadSingleFileRouteImpl {
8
8
  constructor(config){
9
9
  this.config = config;
10
- this.method = "POST";
11
- this.path = "/webiny-file-upload";
12
10
  }
13
- async handle(request) {
11
+ async handle(request, response) {
14
12
  const storagePath = this.config.storagePath;
15
13
  const secret = this.config.uploadSecret;
16
14
  const contentType = request.headers["content-type"] ?? request.headers["Content-Type"] ?? "";
17
15
  const boundary = getBoundary(contentType);
18
- if (!boundary) return json(400, {
16
+ if (!boundary) return response.status(400).json({
19
17
  error: "Expected multipart/form-data with a boundary."
20
18
  });
21
19
  const { fields, files } = parseMultipart(toBuffer(request.body), boundary);
22
20
  const key = fields.find((field)=>"key" === field.name)?.value;
23
21
  const token = fields.find((field)=>"token" === field.name)?.value;
24
22
  const file = files[0];
25
- if (!key || !token) return json(400, {
23
+ if (!key || !token) return response.status(400).json({
26
24
  error: "Missing key or token."
27
25
  });
28
- if (!file) return json(400, {
26
+ if (!file) return response.status(400).json({
29
27
  error: "No file in request."
30
28
  });
31
29
  let payload;
32
30
  try {
33
31
  payload = verifyUploadToken(token, secret);
34
32
  } catch (err) {
35
- return json(400, {
33
+ return response.status(400).json({
36
34
  error: err instanceof Error ? err.message : "Invalid token."
37
35
  });
38
36
  }
39
- if (payload.key !== key) return json(400, {
37
+ if (payload.key !== key) return response.status(400).json({
40
38
  error: "Token key mismatch."
41
39
  });
42
40
  const fileSize = file.data.length;
43
- if (payload.uploadMaxFileSize > 0 && fileSize > payload.uploadMaxFileSize) return json(400, {
41
+ if (payload.uploadMaxFileSize > 0 && fileSize > payload.uploadMaxFileSize) return response.status(400).json({
44
42
  error: "File exceeds maximum allowed size."
45
43
  });
46
- if (payload.uploadMinFileSize > 0 && fileSize < payload.uploadMinFileSize) return json(400, {
44
+ if (payload.uploadMinFileSize > 0 && fileSize < payload.uploadMinFileSize) return response.status(400).json({
47
45
  error: "File is below minimum allowed size."
48
46
  });
49
47
  const destPath = node_path.join(storagePath, key);
50
- if (!isPathContained(destPath, storagePath)) return json(400, {
48
+ if (!isPathContained(destPath, storagePath)) return response.status(400).json({
51
49
  error: "Invalid path."
52
50
  });
53
51
  await mkdir(node_path.dirname(destPath), {
54
52
  recursive: true
55
53
  });
56
54
  await writeFile(destPath, file.data);
57
- return {
58
- statusCode: 204
59
- };
55
+ return response.status(204);
60
56
  }
61
57
  }
62
- const UploadSingleFileRoute = HttpRoute.createImplementation({
58
+ const UploadSingleFileRoute = HttpRouteHandler.createImplementation({
63
59
  implementation: UploadSingleFileRouteImpl,
64
60
  dependencies: [
65
61
  FileManagerServerConfig
66
62
  ]
67
63
  });
68
- export { UploadSingleFileRoute };
64
+ class UploadSingleFileRouteDefinitionImpl {
65
+ constructor(){
66
+ this.name = "upload-single-file";
67
+ this.method = "POST";
68
+ this.path = "/webiny-file-upload";
69
+ this.handler = UploadSingleFileRoute;
70
+ }
71
+ }
72
+ const UploadSingleFileRouteDefinition = HttpRouteDefinition.createImplementation({
73
+ implementation: UploadSingleFileRouteDefinitionImpl,
74
+ dependencies: []
75
+ });
76
+ export { UploadSingleFileRoute, UploadSingleFileRouteDefinition };
69
77
 
70
78
  //# sourceMappingURL=UploadSingleFileRoute.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes/UploadSingleFileRoute/UploadSingleFileRoute.js","sources":["../../../src/routes/UploadSingleFileRoute/UploadSingleFileRoute.ts"],"sourcesContent":["import path from \"node:path\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { HttpRoute } from \"@webiny/event-handler-core\";\nimport type { IHttpRequest, IHttpResponse } from \"@webiny/event-handler-core\";\nimport { verifyUploadToken } from \"~/utils/uploadToken.js\";\nimport { FileManagerServerConfig } from \"~/features/FileManagerServerConfig/abstractions.js\";\nimport { isPathContained, json, toBuffer, parseMultipart, getBoundary } from \"../utils.js\";\n\nclass UploadSingleFileRouteImpl implements HttpRoute.Interface {\n public readonly method = \"POST\";\n public readonly path = \"/webiny-file-upload\";\n\n public constructor(private readonly config: FileManagerServerConfig.Interface) {}\n\n public async handle(request: IHttpRequest): Promise<IHttpResponse> {\n const storagePath = this.config.storagePath;\n const secret = this.config.uploadSecret;\n\n const contentType =\n request.headers[\"content-type\"] ?? request.headers[\"Content-Type\"] ?? \"\";\n const boundary = getBoundary(contentType);\n if (!boundary) {\n return json(400, { error: \"Expected multipart/form-data with a boundary.\" });\n }\n\n const { fields, files } = parseMultipart(toBuffer(request.body), boundary);\n const key = fields.find(field => field.name === \"key\")?.value;\n const token = fields.find(field => field.name === \"token\")?.value;\n const file = files[0];\n\n if (!key || !token) {\n return json(400, { error: \"Missing key or token.\" });\n }\n\n if (!file) {\n return json(400, { error: \"No file in request.\" });\n }\n\n let payload;\n try {\n payload = verifyUploadToken(token, secret);\n } catch (err) {\n return json(400, { error: err instanceof Error ? err.message : \"Invalid token.\" });\n }\n\n if (payload.key !== key) {\n return json(400, { error: \"Token key mismatch.\" });\n }\n\n const fileSize = file.data.length;\n if (payload.uploadMaxFileSize > 0 && fileSize > payload.uploadMaxFileSize) {\n return json(400, { error: \"File exceeds maximum allowed size.\" });\n }\n\n if (payload.uploadMinFileSize > 0 && fileSize < payload.uploadMinFileSize) {\n return json(400, { error: \"File is below minimum allowed size.\" });\n }\n\n const destPath = path.join(storagePath, key);\n if (!isPathContained(destPath, storagePath)) {\n return json(400, { error: \"Invalid path.\" });\n }\n\n await mkdir(path.dirname(destPath), { recursive: true });\n await writeFile(destPath, file.data);\n\n return { statusCode: 204 };\n }\n}\n\nexport const UploadSingleFileRoute = HttpRoute.createImplementation({\n implementation: UploadSingleFileRouteImpl,\n dependencies: [FileManagerServerConfig]\n});\n"],"names":["UploadSingleFileRouteImpl","config","request","storagePath","secret","contentType","boundary","getBoundary","json","fields","files","parseMultipart","toBuffer","key","field","token","file","payload","verifyUploadToken","err","Error","fileSize","destPath","path","isPathContained","mkdir","writeFile","UploadSingleFileRoute","HttpRoute","FileManagerServerConfig"],"mappings":";;;;;;AAQA,MAAMA;IAIF,YAAoCC,MAAyC,CAAE;aAA3CA,MAAM,GAANA;aAHpB,MAAM,GAAG;aACT,IAAI,GAAG;IAEyD;IAEhF,MAAa,OAAOC,OAAqB,EAA0B;QAC/D,MAAMC,cAAc,IAAI,CAAC,MAAM,CAAC,WAAW;QAC3C,MAAMC,SAAS,IAAI,CAAC,MAAM,CAAC,YAAY;QAEvC,MAAMC,cACFH,QAAQ,OAAO,CAAC,eAAe,IAAIA,QAAQ,OAAO,CAAC,eAAe,IAAI;QAC1E,MAAMI,WAAWC,YAAYF;QAC7B,IAAI,CAACC,UACD,OAAOE,KAAK,KAAK;YAAE,OAAO;QAAgD;QAG9E,MAAM,EAAEC,MAAM,EAAEC,KAAK,EAAE,GAAGC,eAAeC,SAASV,QAAQ,IAAI,GAAGI;QACjE,MAAMO,MAAMJ,OAAO,IAAI,CAACK,CAAAA,QAASA,AAAe,UAAfA,MAAM,IAAI,GAAa;QACxD,MAAMC,QAAQN,OAAO,IAAI,CAACK,CAAAA,QAASA,AAAe,YAAfA,MAAM,IAAI,GAAe;QAC5D,MAAME,OAAON,KAAK,CAAC,EAAE;QAErB,IAAI,CAACG,OAAO,CAACE,OACT,OAAOP,KAAK,KAAK;YAAE,OAAO;QAAwB;QAGtD,IAAI,CAACQ,MACD,OAAOR,KAAK,KAAK;YAAE,OAAO;QAAsB;QAGpD,IAAIS;QACJ,IAAI;YACAA,UAAUC,kBAAkBH,OAAOX;QACvC,EAAE,OAAOe,KAAK;YACV,OAAOX,KAAK,KAAK;gBAAE,OAAOW,eAAeC,QAAQD,IAAI,OAAO,GAAG;YAAiB;QACpF;QAEA,IAAIF,QAAQ,GAAG,KAAKJ,KAChB,OAAOL,KAAK,KAAK;YAAE,OAAO;QAAsB;QAGpD,MAAMa,WAAWL,KAAK,IAAI,CAAC,MAAM;QACjC,IAAIC,QAAQ,iBAAiB,GAAG,KAAKI,WAAWJ,QAAQ,iBAAiB,EACrE,OAAOT,KAAK,KAAK;YAAE,OAAO;QAAqC;QAGnE,IAAIS,QAAQ,iBAAiB,GAAG,KAAKI,WAAWJ,QAAQ,iBAAiB,EACrE,OAAOT,KAAK,KAAK;YAAE,OAAO;QAAsC;QAGpE,MAAMc,WAAWC,UAAAA,IAAS,CAACpB,aAAaU;QACxC,IAAI,CAACW,gBAAgBF,UAAUnB,cAC3B,OAAOK,KAAK,KAAK;YAAE,OAAO;QAAgB;QAG9C,MAAMiB,MAAMF,UAAAA,OAAY,CAACD,WAAW;YAAE,WAAW;QAAK;QACtD,MAAMI,UAAUJ,UAAUN,KAAK,IAAI;QAEnC,OAAO;YAAE,YAAY;QAAI;IAC7B;AACJ;AAEO,MAAMW,wBAAwBC,UAAU,oBAAoB,CAAC;IAChE,gBAAgB5B;IAChB,cAAc;QAAC6B;KAAwB;AAC3C"}
1
+ {"version":3,"file":"routes/UploadSingleFileRoute/UploadSingleFileRoute.js","sources":["../../../src/routes/UploadSingleFileRoute/UploadSingleFileRoute.ts"],"sourcesContent":["import path from \"node:path\";\nimport { mkdir, writeFile } from \"node:fs/promises\";\nimport { HttpRouteDefinition, HttpRouteHandler } from \"@webiny/event-handler-core\";\nimport { verifyUploadToken } from \"~/utils/uploadToken.js\";\nimport { FileManagerServerConfig } from \"~/features/FileManagerServerConfig/abstractions.js\";\nimport { isPathContained, toBuffer, parseMultipart, getBoundary } from \"../utils.js\";\n\nclass UploadSingleFileRouteImpl implements HttpRouteHandler.Interface {\n public constructor(private readonly config: FileManagerServerConfig.Interface) {}\n\n public async handle(request: HttpRouteHandler.Request, response: HttpRouteHandler.Response) {\n const storagePath = this.config.storagePath;\n const secret = this.config.uploadSecret;\n\n const contentType =\n request.headers[\"content-type\"] ?? request.headers[\"Content-Type\"] ?? \"\";\n const boundary = getBoundary(contentType);\n if (!boundary) {\n return response\n .status(400)\n .json({ error: \"Expected multipart/form-data with a boundary.\" });\n }\n\n const { fields, files } = parseMultipart(toBuffer(request.body), boundary);\n const key = fields.find(field => field.name === \"key\")?.value;\n const token = fields.find(field => field.name === \"token\")?.value;\n const file = files[0];\n\n if (!key || !token) {\n return response.status(400).json({ error: \"Missing key or token.\" });\n }\n\n if (!file) {\n return response.status(400).json({ error: \"No file in request.\" });\n }\n\n let payload;\n try {\n payload = verifyUploadToken(token, secret);\n } catch (err) {\n return response\n .status(400)\n .json({ error: err instanceof Error ? err.message : \"Invalid token.\" });\n }\n\n if (payload.key !== key) {\n return response.status(400).json({ error: \"Token key mismatch.\" });\n }\n\n const fileSize = file.data.length;\n if (payload.uploadMaxFileSize > 0 && fileSize > payload.uploadMaxFileSize) {\n return response.status(400).json({ error: \"File exceeds maximum allowed size.\" });\n }\n\n if (payload.uploadMinFileSize > 0 && fileSize < payload.uploadMinFileSize) {\n return response.status(400).json({ error: \"File is below minimum allowed size.\" });\n }\n\n const destPath = path.join(storagePath, key);\n if (!isPathContained(destPath, storagePath)) {\n return response.status(400).json({ error: \"Invalid path.\" });\n }\n\n await mkdir(path.dirname(destPath), { recursive: true });\n await writeFile(destPath, file.data);\n\n return response.status(204);\n }\n}\n\nexport const UploadSingleFileRoute = HttpRouteHandler.createImplementation({\n implementation: UploadSingleFileRouteImpl,\n dependencies: [FileManagerServerConfig]\n});\n\nclass UploadSingleFileRouteDefinitionImpl implements HttpRouteDefinition.Interface {\n readonly name = \"upload-single-file\";\n readonly method = \"POST\";\n readonly path = \"/webiny-file-upload\";\n readonly handler = UploadSingleFileRoute;\n}\n\n/** What the router matches on. Zero dependencies, so building it costs nothing. */\nexport const UploadSingleFileRouteDefinition = HttpRouteDefinition.createImplementation({\n implementation: UploadSingleFileRouteDefinitionImpl,\n dependencies: []\n});\n"],"names":["UploadSingleFileRouteImpl","config","request","response","storagePath","secret","contentType","boundary","getBoundary","fields","files","parseMultipart","toBuffer","key","field","token","file","payload","verifyUploadToken","err","Error","fileSize","destPath","path","isPathContained","mkdir","writeFile","UploadSingleFileRoute","HttpRouteHandler","FileManagerServerConfig","UploadSingleFileRouteDefinitionImpl","UploadSingleFileRouteDefinition","HttpRouteDefinition"],"mappings":";;;;;;AAOA,MAAMA;IACF,YAAoCC,MAAyC,CAAE;aAA3CA,MAAM,GAANA;IAA4C;IAEhF,MAAa,OAAOC,OAAiC,EAAEC,QAAmC,EAAE;QACxF,MAAMC,cAAc,IAAI,CAAC,MAAM,CAAC,WAAW;QAC3C,MAAMC,SAAS,IAAI,CAAC,MAAM,CAAC,YAAY;QAEvC,MAAMC,cACFJ,QAAQ,OAAO,CAAC,eAAe,IAAIA,QAAQ,OAAO,CAAC,eAAe,IAAI;QAC1E,MAAMK,WAAWC,YAAYF;QAC7B,IAAI,CAACC,UACD,OAAOJ,SACF,MAAM,CAAC,KACP,IAAI,CAAC;YAAE,OAAO;QAAgD;QAGvE,MAAM,EAAEM,MAAM,EAAEC,KAAK,EAAE,GAAGC,eAAeC,SAASV,QAAQ,IAAI,GAAGK;QACjE,MAAMM,MAAMJ,OAAO,IAAI,CAACK,CAAAA,QAASA,AAAe,UAAfA,MAAM,IAAI,GAAa;QACxD,MAAMC,QAAQN,OAAO,IAAI,CAACK,CAAAA,QAASA,AAAe,YAAfA,MAAM,IAAI,GAAe;QAC5D,MAAME,OAAON,KAAK,CAAC,EAAE;QAErB,IAAI,CAACG,OAAO,CAACE,OACT,OAAOZ,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAwB;QAGtE,IAAI,CAACa,MACD,OAAOb,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAsB;QAGpE,IAAIc;QACJ,IAAI;YACAA,UAAUC,kBAAkBH,OAAOV;QACvC,EAAE,OAAOc,KAAK;YACV,OAAOhB,SACF,MAAM,CAAC,KACP,IAAI,CAAC;gBAAE,OAAOgB,eAAeC,QAAQD,IAAI,OAAO,GAAG;YAAiB;QAC7E;QAEA,IAAIF,QAAQ,GAAG,KAAKJ,KAChB,OAAOV,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAsB;QAGpE,MAAMkB,WAAWL,KAAK,IAAI,CAAC,MAAM;QACjC,IAAIC,QAAQ,iBAAiB,GAAG,KAAKI,WAAWJ,QAAQ,iBAAiB,EACrE,OAAOd,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAqC;QAGnF,IAAIc,QAAQ,iBAAiB,GAAG,KAAKI,WAAWJ,QAAQ,iBAAiB,EACrE,OAAOd,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAsC;QAGpF,MAAMmB,WAAWC,UAAAA,IAAS,CAACnB,aAAaS;QACxC,IAAI,CAACW,gBAAgBF,UAAUlB,cAC3B,OAAOD,SAAS,MAAM,CAAC,KAAK,IAAI,CAAC;YAAE,OAAO;QAAgB;QAG9D,MAAMsB,MAAMF,UAAAA,OAAY,CAACD,WAAW;YAAE,WAAW;QAAK;QACtD,MAAMI,UAAUJ,UAAUN,KAAK,IAAI;QAEnC,OAAOb,SAAS,MAAM,CAAC;IAC3B;AACJ;AAEO,MAAMwB,wBAAwBC,iBAAiB,oBAAoB,CAAC;IACvE,gBAAgB5B;IAChB,cAAc;QAAC6B;KAAwB;AAC3C;AAEA,MAAMC;;aACO,IAAI,GAAG;aACP,MAAM,GAAG;aACT,IAAI,GAAG;aACP,OAAO,GAAGH;;AACvB;AAGO,MAAMI,kCAAkCC,oBAAoB,oBAAoB,CAAC;IACpF,gBAAgBF;IAChB,cAAc,EAAE;AACpB"}
@@ -1,9 +1,9 @@
1
1
  import { createFeature } from "@webiny/feature/api";
2
- import { UploadSingleFileRoute } from "./UploadSingleFileRoute.js";
2
+ import { UploadSingleFileRouteDefinition } from "./UploadSingleFileRoute.js";
3
3
  const UploadSingleFileRouteFeature = createFeature({
4
4
  name: "FileManagerServer/UploadSingleFileRoute",
5
5
  register (container) {
6
- container.register(UploadSingleFileRoute);
6
+ container.register(UploadSingleFileRouteDefinition);
7
7
  }
8
8
  });
9
9
  export { UploadSingleFileRouteFeature };
@@ -1 +1 @@
1
- {"version":3,"file":"routes/UploadSingleFileRoute/feature.js","sources":["../../../src/routes/UploadSingleFileRoute/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { UploadSingleFileRoute } from \"./UploadSingleFileRoute.js\";\n\nexport const UploadSingleFileRouteFeature = createFeature({\n name: \"FileManagerServer/UploadSingleFileRoute\",\n register(container) {\n container.register(UploadSingleFileRoute);\n }\n});\n"],"names":["UploadSingleFileRouteFeature","createFeature","container","UploadSingleFileRoute"],"mappings":";;AAGO,MAAMA,+BAA+BC,cAAc;IACtD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;IACvB;AACJ"}
1
+ {"version":3,"file":"routes/UploadSingleFileRoute/feature.js","sources":["../../../src/routes/UploadSingleFileRoute/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { UploadSingleFileRouteDefinition } from \"./UploadSingleFileRoute.js\";\n\nexport const UploadSingleFileRouteFeature = createFeature({\n name: \"FileManagerServer/UploadSingleFileRoute\",\n register(container) {\n container.register(UploadSingleFileRouteDefinition);\n }\n});\n"],"names":["UploadSingleFileRouteFeature","createFeature","container","UploadSingleFileRouteDefinition"],"mappings":";;AAGO,MAAMA,+BAA+BC,cAAc;IACtD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;IACvB;AACJ"}
package/routes/utils.d.ts CHANGED
@@ -1,6 +1,4 @@
1
- import type { IHttpResponse } from "@webiny/event-handler-core";
2
1
  export declare const isPathContained: (filePath: string, storagePath: string) => boolean;
3
- export declare const json: (statusCode: number, data: unknown) => IHttpResponse;
4
2
  export declare const toBuffer: (body: unknown) => Buffer;
5
3
  interface MultipartField {
6
4
  name: string;
package/routes/utils.js CHANGED
@@ -4,13 +4,6 @@ const isPathContained = (filePath, storagePath)=>{
4
4
  const root = node_path.resolve(storagePath);
5
5
  return resolved === root || resolved.startsWith(root + node_path.sep);
6
6
  };
7
- const json = (statusCode, data)=>({
8
- statusCode,
9
- headers: {
10
- "content-type": "application/json"
11
- },
12
- body: JSON.stringify(data)
13
- });
14
7
  const toBuffer = (body)=>{
15
8
  if (Buffer.isBuffer(body)) return body;
16
9
  if (body instanceof Uint8Array) return Buffer.from(body);
@@ -57,6 +50,6 @@ const getBoundary = (contentType)=>{
57
50
  const value = match?.[1] ?? match?.[2];
58
51
  return value?.trim();
59
52
  };
60
- export { getBoundary, isPathContained, json, parseMultipart, toBuffer };
53
+ export { getBoundary, isPathContained, parseMultipart, toBuffer };
61
54
 
62
55
  //# sourceMappingURL=utils.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routes/utils.js","sources":["../../src/routes/utils.ts"],"sourcesContent":["import path from \"node:path\";\nimport type { IHttpResponse } from \"@webiny/event-handler-core\";\n\nexport const isPathContained = (filePath: string, storagePath: string): boolean => {\n const resolved = path.resolve(filePath);\n const root = path.resolve(storagePath);\n return resolved === root || resolved.startsWith(root + path.sep);\n};\n\nexport const json = (statusCode: number, data: unknown): IHttpResponse => ({\n statusCode,\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(data)\n});\n\n/* Normalise a transport request body into a Buffer. */\nexport const toBuffer = (body: unknown): Buffer => {\n if (Buffer.isBuffer(body)) {\n return body;\n }\n if (body instanceof Uint8Array) {\n return Buffer.from(body);\n }\n if (typeof body === \"string\") {\n return Buffer.from(body, \"base64\");\n }\n return Buffer.alloc(0);\n};\n\ninterface MultipartField {\n name: string;\n value: string;\n}\n\ninterface MultipartFile {\n name: string;\n filename: string;\n data: Buffer;\n}\n\nexport interface MultipartResult {\n fields: MultipartField[];\n files: MultipartFile[];\n}\n\n/* Minimal multipart/form-data parser operating on the raw body buffer. */\nexport const parseMultipart = (buffer: Buffer, boundary: string): MultipartResult => {\n const fields: MultipartField[] = [];\n const files: MultipartFile[] = [];\n const separator = Buffer.from(`--${boundary}`);\n\n let idx = buffer.indexOf(separator);\n while (idx !== -1) {\n const next = buffer.indexOf(separator, idx + separator.length);\n if (next === -1) {\n break;\n }\n\n let part = buffer.subarray(idx + separator.length, next);\n if (part.subarray(0, 2).toString(\"latin1\") === \"\\r\\n\") {\n part = part.subarray(2);\n }\n if (part.subarray(-2).toString(\"latin1\") === \"\\r\\n\") {\n part = part.subarray(0, -2);\n }\n\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd !== -1) {\n const rawHeaders = part.subarray(0, headerEnd).toString(\"utf8\");\n const content = part.subarray(headerEnd + 4);\n\n const disposition = /content-disposition:[^\\r\\n]*/i.exec(rawHeaders)?.[0] ?? \"\";\n const name = /name=\"([^\"]*)\"/i.exec(disposition)?.[1];\n const filename = /filename=\"([^\"]*)\"/i.exec(disposition)?.[1];\n\n if (name !== undefined) {\n if (filename !== undefined) {\n files.push({ name, filename, data: content });\n } else {\n fields.push({ name, value: content.toString(\"utf8\") });\n }\n }\n }\n\n idx = next;\n }\n\n return { fields, files };\n};\n\nexport const getBoundary = (contentType: string): string | undefined => {\n const match = /boundary=(?:\"([^\"]+)\"|([^;]+))/i.exec(contentType);\n const value = match?.[1] ?? match?.[2];\n return value?.trim();\n};\n"],"names":["isPathContained","filePath","storagePath","resolved","path","root","json","statusCode","data","JSON","toBuffer","body","Buffer","Uint8Array","parseMultipart","buffer","boundary","fields","files","separator","idx","next","part","headerEnd","rawHeaders","content","disposition","name","filename","undefined","getBoundary","contentType","match","value"],"mappings":";AAGO,MAAMA,kBAAkB,CAACC,UAAkBC;IAC9C,MAAMC,WAAWC,UAAAA,OAAY,CAACH;IAC9B,MAAMI,OAAOD,UAAAA,OAAY,CAACF;IAC1B,OAAOC,aAAaE,QAAQF,SAAS,UAAU,CAACE,OAAOD,UAAAA,GAAQ;AACnE;AAEO,MAAME,OAAO,CAACC,YAAoBC,OAAkC;QACvED;QACA,SAAS;YAAE,gBAAgB;QAAmB;QAC9C,MAAME,KAAK,SAAS,CAACD;IACzB;AAGO,MAAME,WAAW,CAACC;IACrB,IAAIC,OAAO,QAAQ,CAACD,OAChB,OAAOA;IAEX,IAAIA,gBAAgBE,YAChB,OAAOD,OAAO,IAAI,CAACD;IAEvB,IAAI,AAAgB,YAAhB,OAAOA,MACP,OAAOC,OAAO,IAAI,CAACD,MAAM;IAE7B,OAAOC,OAAO,KAAK,CAAC;AACxB;AAmBO,MAAME,iBAAiB,CAACC,QAAgBC;IAC3C,MAAMC,SAA2B,EAAE;IACnC,MAAMC,QAAyB,EAAE;IACjC,MAAMC,YAAYP,OAAO,IAAI,CAAC,CAAC,EAAE,EAAEI,UAAU;IAE7C,IAAII,MAAML,OAAO,OAAO,CAACI;IACzB,MAAOC,AAAQ,OAARA,IAAY;QACf,MAAMC,OAAON,OAAO,OAAO,CAACI,WAAWC,MAAMD,UAAU,MAAM;QAC7D,IAAIE,AAAS,OAATA,MACA;QAGJ,IAAIC,OAAOP,OAAO,QAAQ,CAACK,MAAMD,UAAU,MAAM,EAAEE;QACnD,IAAIC,AAA2C,WAA3CA,KAAK,QAAQ,CAAC,GAAG,GAAG,QAAQ,CAAC,WAC7BA,OAAOA,KAAK,QAAQ,CAAC;QAEzB,IAAIA,AAAyC,WAAzCA,KAAK,QAAQ,CAAC,IAAI,QAAQ,CAAC,WAC3BA,OAAOA,KAAK,QAAQ,CAAC,GAAG;QAG5B,MAAMC,YAAYD,KAAK,OAAO,CAAC;QAC/B,IAAIC,AAAc,OAAdA,WAAkB;YAClB,MAAMC,aAAaF,KAAK,QAAQ,CAAC,GAAGC,WAAW,QAAQ,CAAC;YACxD,MAAME,UAAUH,KAAK,QAAQ,CAACC,YAAY;YAE1C,MAAMG,cAAc,gCAAgC,IAAI,CAACF,aAAa,CAAC,EAAE,IAAI;YAC7E,MAAMG,OAAO,kBAAkB,IAAI,CAACD,cAAc,CAAC,EAAE;YACrD,MAAME,WAAW,sBAAsB,IAAI,CAACF,cAAc,CAAC,EAAE;YAE7D,IAAIC,AAASE,WAATF,MACA,IAAIC,AAAaC,WAAbD,UACAV,MAAM,IAAI,CAAC;gBAAES;gBAAMC;gBAAU,MAAMH;YAAQ;iBAE3CR,OAAO,IAAI,CAAC;gBAAEU;gBAAM,OAAOF,QAAQ,QAAQ,CAAC;YAAQ;QAGhE;QAEAL,MAAMC;IACV;IAEA,OAAO;QAAEJ;QAAQC;IAAM;AAC3B;AAEO,MAAMY,cAAc,CAACC;IACxB,MAAMC,QAAQ,kCAAkC,IAAI,CAACD;IACrD,MAAME,QAAQD,OAAO,CAAC,EAAE,IAAIA,OAAO,CAAC,EAAE;IACtC,OAAOC,OAAO;AAClB"}
1
+ {"version":3,"file":"routes/utils.js","sources":["../../src/routes/utils.ts"],"sourcesContent":["import path from \"node:path\";\n\nexport const isPathContained = (filePath: string, storagePath: string): boolean => {\n const resolved = path.resolve(filePath);\n const root = path.resolve(storagePath);\n return resolved === root || resolved.startsWith(root + path.sep);\n};\n\n/* Normalise a transport request body into a Buffer. */\nexport const toBuffer = (body: unknown): Buffer => {\n if (Buffer.isBuffer(body)) {\n return body;\n }\n if (body instanceof Uint8Array) {\n return Buffer.from(body);\n }\n if (typeof body === \"string\") {\n return Buffer.from(body, \"base64\");\n }\n return Buffer.alloc(0);\n};\n\ninterface MultipartField {\n name: string;\n value: string;\n}\n\ninterface MultipartFile {\n name: string;\n filename: string;\n data: Buffer;\n}\n\nexport interface MultipartResult {\n fields: MultipartField[];\n files: MultipartFile[];\n}\n\n/* Minimal multipart/form-data parser operating on the raw body buffer. */\nexport const parseMultipart = (buffer: Buffer, boundary: string): MultipartResult => {\n const fields: MultipartField[] = [];\n const files: MultipartFile[] = [];\n const separator = Buffer.from(`--${boundary}`);\n\n let idx = buffer.indexOf(separator);\n while (idx !== -1) {\n const next = buffer.indexOf(separator, idx + separator.length);\n if (next === -1) {\n break;\n }\n\n let part = buffer.subarray(idx + separator.length, next);\n if (part.subarray(0, 2).toString(\"latin1\") === \"\\r\\n\") {\n part = part.subarray(2);\n }\n if (part.subarray(-2).toString(\"latin1\") === \"\\r\\n\") {\n part = part.subarray(0, -2);\n }\n\n const headerEnd = part.indexOf(\"\\r\\n\\r\\n\");\n if (headerEnd !== -1) {\n const rawHeaders = part.subarray(0, headerEnd).toString(\"utf8\");\n const content = part.subarray(headerEnd + 4);\n\n const disposition = /content-disposition:[^\\r\\n]*/i.exec(rawHeaders)?.[0] ?? \"\";\n const name = /name=\"([^\"]*)\"/i.exec(disposition)?.[1];\n const filename = /filename=\"([^\"]*)\"/i.exec(disposition)?.[1];\n\n if (name !== undefined) {\n if (filename !== undefined) {\n files.push({ name, filename, data: content });\n } else {\n fields.push({ name, value: content.toString(\"utf8\") });\n }\n }\n }\n\n idx = next;\n }\n\n return { fields, files };\n};\n\nexport const getBoundary = (contentType: string): string | undefined => {\n const match = /boundary=(?:\"([^\"]+)\"|([^;]+))/i.exec(contentType);\n const value = match?.[1] ?? match?.[2];\n return value?.trim();\n};\n"],"names":["isPathContained","filePath","storagePath","resolved","path","root","toBuffer","body","Buffer","Uint8Array","parseMultipart","buffer","boundary","fields","files","separator","idx","next","part","headerEnd","rawHeaders","content","disposition","name","filename","undefined","getBoundary","contentType","match","value"],"mappings":";AAEO,MAAMA,kBAAkB,CAACC,UAAkBC;IAC9C,MAAMC,WAAWC,UAAAA,OAAY,CAACH;IAC9B,MAAMI,OAAOD,UAAAA,OAAY,CAACF;IAC1B,OAAOC,aAAaE,QAAQF,SAAS,UAAU,CAACE,OAAOD,UAAAA,GAAQ;AACnE;AAGO,MAAME,WAAW,CAACC;IACrB,IAAIC,OAAO,QAAQ,CAACD,OAChB,OAAOA;IAEX,IAAIA,gBAAgBE,YAChB,OAAOD,OAAO,IAAI,CAACD;IAEvB,IAAI,AAAgB,YAAhB,OAAOA,MACP,OAAOC,OAAO,IAAI,CAACD,MAAM;IAE7B,OAAOC,OAAO,KAAK,CAAC;AACxB;AAmBO,MAAME,iBAAiB,CAACC,QAAgBC;IAC3C,MAAMC,SAA2B,EAAE;IACnC,MAAMC,QAAyB,EAAE;IACjC,MAAMC,YAAYP,OAAO,IAAI,CAAC,CAAC,EAAE,EAAEI,UAAU;IAE7C,IAAII,MAAML,OAAO,OAAO,CAACI;IACzB,MAAOC,AAAQ,OAARA,IAAY;QACf,MAAMC,OAAON,OAAO,OAAO,CAACI,WAAWC,MAAMD,UAAU,MAAM;QAC7D,IAAIE,AAAS,OAATA,MACA;QAGJ,IAAIC,OAAOP,OAAO,QAAQ,CAACK,MAAMD,UAAU,MAAM,EAAEE;QACnD,IAAIC,AAA2C,WAA3CA,KAAK,QAAQ,CAAC,GAAG,GAAG,QAAQ,CAAC,WAC7BA,OAAOA,KAAK,QAAQ,CAAC;QAEzB,IAAIA,AAAyC,WAAzCA,KAAK,QAAQ,CAAC,IAAI,QAAQ,CAAC,WAC3BA,OAAOA,KAAK,QAAQ,CAAC,GAAG;QAG5B,MAAMC,YAAYD,KAAK,OAAO,CAAC;QAC/B,IAAIC,AAAc,OAAdA,WAAkB;YAClB,MAAMC,aAAaF,KAAK,QAAQ,CAAC,GAAGC,WAAW,QAAQ,CAAC;YACxD,MAAME,UAAUH,KAAK,QAAQ,CAACC,YAAY;YAE1C,MAAMG,cAAc,gCAAgC,IAAI,CAACF,aAAa,CAAC,EAAE,IAAI;YAC7E,MAAMG,OAAO,kBAAkB,IAAI,CAACD,cAAc,CAAC,EAAE;YACrD,MAAME,WAAW,sBAAsB,IAAI,CAACF,cAAc,CAAC,EAAE;YAE7D,IAAIC,AAASE,WAATF,MACA,IAAIC,AAAaC,WAAbD,UACAV,MAAM,IAAI,CAAC;gBAAES;gBAAMC;gBAAU,MAAMH;YAAQ;iBAE3CR,OAAO,IAAI,CAAC;gBAAEU;gBAAM,OAAOF,QAAQ,QAAQ,CAAC;YAAQ;QAGhE;QAEAL,MAAMC;IACV;IAEA,OAAO;QAAEJ;QAAQC;IAAM;AAC3B;AAEO,MAAMY,cAAc,CAACC;IACxB,MAAMC,QAAQ,kCAAkC,IAAI,CAACD;IACrD,MAAME,QAAQD,OAAO,CAAC,EAAE,IAAIA,OAAO,CAAC,EAAE;IACtC,OAAOC,OAAO;AAClB"}