Skip to content

Commit bc771b0

Browse files
committed
fix(api-file-manager): use precalculated asset size and add logging
1 parent 85beabc commit bc771b0

File tree

7 files changed

+94
-30
lines changed

7 files changed

+94
-30
lines changed

packages/api-file-manager-s3/src/assetDelivery/assetDeliveryConfig.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SharpTransform } from "~/assetDelivery/s3/SharpTransform";
1010
export type AssetDeliveryParams = Parameters<typeof createBaseAssetDelivery>[0] & {
1111
imageResizeWidths?: number[];
1212
presignedUrlTtl?: number;
13+
assetStreamingMaxSize?: number;
1314
};
1415

1516
export const assetDeliveryConfig = (params: AssetDeliveryParams) => {
@@ -19,6 +20,11 @@ export const assetDeliveryConfig = (params: AssetDeliveryParams) => {
1920
const {
2021
presignedUrlTtl = 900,
2122
imageResizeWidths = [100, 300, 500, 750, 1000, 1500, 2500],
23+
/**
24+
* Even though Lambda's response payload limit is 6,291,556 bytes, we leave some room for the response envelope.
25+
* We had situations where a 4.7MB file would cause the payload to go over the limit, so let's be on the safe side.
26+
*/
27+
assetStreamingMaxSize = 4718592,
2228
...baseParams
2329
} = params;
2430

@@ -35,7 +41,7 @@ export const assetDeliveryConfig = (params: AssetDeliveryParams) => {
3541
});
3642

3743
config.decorateAssetOutputStrategy(() => {
38-
return new S3OutputStrategy(s3, bucket, presignedUrlTtl);
44+
return new S3OutputStrategy(s3, bucket, presignedUrlTtl, assetStreamingMaxSize);
3945
});
4046

4147
config.decorateAssetTransformationStrategy(() => {

packages/api-file-manager-s3/src/assetDelivery/s3/S3OutputStrategy.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import { GetObjectCommand, getSignedUrl, S3 } from "@webiny/aws-sdk/client-s3";
33
import { S3RedirectAssetReply } from "~/assetDelivery/s3/S3RedirectAssetReply";
44
import { S3StreamAssetReply } from "~/assetDelivery/s3/S3StreamAssetReply";
55

6-
const MAX_RETURN_CONTENT_LENGTH = 4915200; // ~4.8 MB
7-
86
/**
97
* This strategy outputs an asset taking into account the size of the asset contents.
108
* If the asset is larger than 5MB, a presigned URL will be generated, and a redirect will happen.
@@ -13,21 +11,30 @@ export class S3OutputStrategy implements AssetOutputStrategy {
1311
private readonly s3: S3;
1412
private readonly bucket: string;
1513
private readonly presignedUrlTtl: number;
14+
private readonly assetStreamingMaxSize: number;
1615

17-
constructor(s3: S3, bucket: string, presignedUrlTtl: number) {
16+
constructor(s3: S3, bucket: string, presignedUrlTtl: number, assetStreamingMaxSize: number) {
17+
this.assetStreamingMaxSize = assetStreamingMaxSize;
1818
this.presignedUrlTtl = presignedUrlTtl;
1919
this.s3 = s3;
2020
this.bucket = bucket;
2121
}
2222

2323
async output(asset: Asset): Promise<AssetReply> {
24-
if ((await asset.getSize()) > MAX_RETURN_CONTENT_LENGTH) {
24+
if (asset.getSize() > this.assetStreamingMaxSize) {
25+
console.log(
26+
`Asset size is greater than ${this.assetStreamingMaxSize}; redirecting to a presigned S3 URL.`
27+
);
28+
2529
return new S3RedirectAssetReply(
2630
await this.getPresignedUrl(asset),
2731
this.presignedUrlTtl
2832
);
2933
}
3034

35+
console.log(
36+
`Asset size is smaller than ${this.assetStreamingMaxSize}; streaming directly from Lambda function.`
37+
);
3138
return new S3StreamAssetReply(asset);
3239
}
3340

packages/api-file-manager-s3/src/assetDelivery/s3/SharpTransform.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export class SharpTransform implements AssetTransformationStrategy {
2626

2727
async transform(assetRequest: AssetRequest, asset: Asset): Promise<Asset> {
2828
if (!utils.SUPPORTED_TRANSFORMABLE_IMAGES.includes(asset.getExtension())) {
29+
console.log(
30+
`Transformations/optimizations of ${asset.getContentType()} assets are not supported. Skipping.`
31+
);
2932
return asset;
3033
}
3134

@@ -45,6 +48,7 @@ export class SharpTransform implements AssetTransformationStrategy {
4548
}
4649

4750
private async transformAsset(asset: Asset, options: Omit<AssetRequestOptions, "original">) {
51+
console.log("Transform asset", options);
4852
if (options.width) {
4953
const { s3, bucket } = this.params;
5054

@@ -63,7 +67,15 @@ export class SharpTransform implements AssetTransformationStrategy {
6367

6468
const buffer = Buffer.from(await Body.transformToByteArray());
6569

66-
asset.setContentsReader(new CallableContentsReader(() => buffer));
70+
const newAsset = asset.withProps({ size: buffer.length });
71+
newAsset.setContentsReader(new CallableContentsReader(() => buffer));
72+
73+
console.log(`Return a previously transformed asset`, {
74+
key: transformedAssetKey,
75+
size: newAsset.getSize()
76+
});
77+
78+
return newAsset;
6779
} catch (e) {
6880
const optimizedImage = await this.optimizeAsset(asset);
6981

@@ -73,22 +85,33 @@ export class SharpTransform implements AssetTransformationStrategy {
7385
/**
7486
* `width` is the only transformation we currently support.
7587
*/
88+
console.log(`Resize the asset (width: ${width})`);
7689
const buffer = await optimizedImage.getContents();
77-
const transformedBuffer = sharp(buffer, { animated: this.isAssetAnimated(asset) })
90+
const transformedBuffer = await sharp(buffer, {
91+
animated: this.isAssetAnimated(asset)
92+
})
7893
.resize({ width })
7994
.toBuffer();
8095

8196
/**
8297
* Transformations are applied to the optimized image.
8398
*/
84-
asset.setContentsReader(new CallableContentsReader(() => transformedBuffer));
99+
const newAsset = asset.withProps({ size: transformedBuffer.length });
100+
newAsset.setContentsReader(new CallableContentsReader(() => transformedBuffer));
85101

86102
await s3.putObject({
87103
Bucket: bucket,
88104
Key: transformedAssetKey,
89-
ContentType: asset.getContentType(),
90-
Body: await asset.getContents()
105+
ContentType: newAsset.getContentType(),
106+
Body: await newAsset.getContents()
91107
});
108+
109+
console.log(`Return the resized asset`, {
110+
key: transformedAssetKey,
111+
size: newAsset.getSize()
112+
});
113+
114+
return newAsset;
92115
}
93116
}
94117

@@ -98,6 +121,13 @@ export class SharpTransform implements AssetTransformationStrategy {
98121
private async optimizeAsset(asset: Asset) {
99122
const { s3, bucket } = this.params;
100123

124+
console.log("Optimize asset", {
125+
id: asset.getId(),
126+
key: asset.getKey(),
127+
size: asset.getSize(),
128+
type: asset.getContentType()
129+
});
130+
101131
const assetKey = new AssetKeyGenerator(asset);
102132
const optimizedAssetKey = assetKey.getOptimizedImageKey();
103133

@@ -111,10 +141,16 @@ export class SharpTransform implements AssetTransformationStrategy {
111141
throw new Error(`Missing image body!`);
112142
}
113143

144+
console.log("Return a previously optimized asset", optimizedAssetKey);
145+
114146
const buffer = Buffer.from(await Body.transformToByteArray());
115147

116-
asset.setContentsReader(new CallableContentsReader(() => buffer));
148+
const newAsset = asset.withProps({ size: buffer.length });
149+
newAsset.setContentsReader(new CallableContentsReader(() => buffer));
150+
151+
return newAsset;
117152
} catch (e) {
153+
console.log("Create an optimized version of the original asset", asset.getKey());
118154
// If not found, create an optimized version of the original asset.
119155
const buffer = await asset.getContents();
120156

@@ -127,23 +163,26 @@ export class SharpTransform implements AssetTransformationStrategy {
127163
const optimization = optimizationMap[asset.getContentType()];
128164

129165
if (!optimization) {
130-
console.log(`no optimizations defined for ${asset.getContentType()}`);
166+
console.log(`No optimizations defined for ${asset.getContentType()}`);
131167
return asset;
132168
}
133169

134-
const optimizedBuffer = optimization(buffer).toBuffer();
170+
const optimizedBuffer = await optimization(buffer).toBuffer();
171+
172+
console.log("Optimized asset size", optimizedBuffer.length);
135173

136-
asset.setContentsReader(new CallableContentsReader(() => optimizedBuffer));
174+
const newAsset = asset.withProps({ size: optimizedBuffer.length });
175+
newAsset.setContentsReader(new CallableContentsReader(() => optimizedBuffer));
137176

138177
await s3.putObject({
139178
Bucket: bucket,
140179
Key: optimizedAssetKey,
141-
ContentType: asset.getContentType(),
142-
Body: await asset.getContents()
180+
ContentType: newAsset.getContentType(),
181+
Body: await newAsset.getContents()
143182
});
144-
}
145183

146-
return asset;
184+
return newAsset;
185+
}
147186
}
148187

149188
private isAssetAnimated(asset: Asset) {
@@ -160,6 +199,7 @@ export class SharpTransform implements AssetTransformationStrategy {
160199
private optimizeJpeg(buffer: Buffer) {
161200
return sharp(buffer)
162201
.resize({ width: 2560, withoutEnlargement: true, fit: "inside" })
202+
.withMetadata()
163203
.toFormat("jpeg", { quality: 90 });
164204
}
165205
}

packages/api-file-manager/src/delivery/AssetDelivery/Asset.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ export class Asset {
2121
}
2222

2323
clone() {
24-
const clonedAsset = new Asset(structuredClone(this.props));
25-
clonedAsset.outputStrategy = this.outputStrategy;
26-
clonedAsset.contentsReader = this.contentsReader;
27-
return clonedAsset;
24+
return this.withProps(structuredClone(this.props));
25+
}
26+
27+
withProps(props: Partial<AssetData>) {
28+
const newAsset = new Asset({ ...this.props, ...props });
29+
newAsset.contentsReader = this.contentsReader;
30+
newAsset.outputStrategy = this.outputStrategy;
31+
return newAsset;
2832
}
2933

3034
getId() {
@@ -39,9 +43,8 @@ export class Asset {
3943
getKey() {
4044
return this.props.key;
4145
}
42-
async getSize() {
43-
const buffer = await this.getContents();
44-
return buffer.length;
46+
getSize() {
47+
return this.props.size;
4548
}
4649
getContentType() {
4750
return this.props.contentType;

packages/api-file-manager/src/delivery/AssetDelivery/FilesAssetRequestResolver.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Request } from "@webiny/handler/types";
22
import { AssetRequestResolver } from "./abstractions/AssetRequestResolver";
3-
import { AssetRequest } from "./AssetRequest";
3+
import { AssetRequest, AssetRequestOptions } from "./AssetRequest";
44

55
export class FilesAssetRequestResolver implements AssetRequestResolver {
66
async resolve(request: Request): Promise<AssetRequest | undefined> {
@@ -15,15 +15,21 @@ export class FilesAssetRequestResolver implements AssetRequestResolver {
1515
// Example: { '*': '/files/65722cb5c7824a0008d05963/image-48.jpg' },
1616
const path = params["*"];
1717

18+
const options: AssetRequestOptions = {
19+
...query,
20+
original: "original" in query
21+
};
22+
23+
if (query.width) {
24+
options.width = parseInt(query.width);
25+
}
26+
1827
return new AssetRequest({
1928
key: decodeURI(path).replace("/files/", ""),
2029
context: {
2130
url: request.url
2231
},
23-
options: {
24-
...query,
25-
width: query.width ? parseInt(query.width) : undefined
26-
}
32+
options
2733
});
2834
}
2935
}

packages/api-file-manager/src/delivery/AssetDelivery/transformation/TransformationAssetProcessor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class TransformationAssetProcessor implements AssetProcessor {
1212

1313
// If the `original` image was requested, we skip all transformations.
1414
if (original) {
15+
console.log("Skip transformations; original asset was requested.");
1516
return asset;
1617
}
1718

packages/api-file-manager/src/delivery/setupAssetDelivery.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export const setupAssetDelivery = (params: AssetDeliveryParams) => {
146146
);
147147

148148
// Get reply object (runs the output strategy under the hood).
149+
console.log(`Output asset (size: ${processedAsset.getSize()} bytes).`);
149150
return outputAsset(reply, processedAsset);
150151
},
151152
{ override: true }

0 commit comments

Comments
 (0)