@aglyn/tenant-data-admin 1.0.0-beta.221 → 1.0.0-beta.222
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.
- package/package.json +7 -7
- package/src/lib/server/api-keys.js +3 -3
- package/src/lib/server/api-keys.js.map +1 -1
- package/src/lib/server/api-v1-resources.d.ts +44 -3
- package/src/lib/server/api-v1-resources.js +82 -5
- package/src/lib/server/api-v1-resources.js.map +1 -1
- package/src/lib/server/collection-live-pages.d.ts +2 -2
- package/src/lib/server/collection-live-pages.js +1 -1
- package/src/lib/server/collection-live-pages.js.map +1 -1
- package/src/lib/server/email-delivery-log.d.ts +17 -73
- package/src/lib/server/email-delivery-log.js +44 -222
- package/src/lib/server/email-delivery-log.js.map +1 -1
- package/src/lib/server/email-flow-gate.js +27 -38
- package/src/lib/server/email-flow-gate.js.map +1 -1
- package/src/lib/server/erase-person.d.ts +34 -54
- package/src/lib/server/erase-person.js +44 -239
- package/src/lib/server/erase-person.js.map +1 -1
- package/src/lib/server/host-visitor-records.d.ts +12 -4
- package/src/lib/server/host-visitor-records.js +20 -10
- package/src/lib/server/host-visitor-records.js.map +1 -1
- package/src/lib/server/hosted-page-review.js +46 -33
- package/src/lib/server/hosted-page-review.js.map +1 -1
- package/src/lib/server/link-reputation-review.d.ts +6 -2
- package/src/lib/server/link-reputation-review.js +61 -20
- package/src/lib/server/link-reputation-review.js.map +1 -1
- package/src/lib/server/live-page-drops.d.ts +67 -0
- package/src/lib/server/live-page-drops.js +57 -0
- package/src/lib/server/live-page-drops.js.map +1 -0
- package/src/lib/server/media-bandwidth-cap.d.ts +78 -0
- package/src/lib/server/media-bandwidth-cap.js +180 -0
- package/src/lib/server/media-bandwidth-cap.js.map +1 -0
- package/src/lib/server/media-cdn-icon.d.ts +77 -0
- package/src/lib/server/media-cdn-icon.js +140 -0
- package/src/lib/server/media-cdn-icon.js.map +1 -0
- package/src/lib/server/media-delivery-regeneration.d.ts +132 -0
- package/src/lib/server/media-delivery-regeneration.js +216 -0
- package/src/lib/server/media-delivery-regeneration.js.map +1 -0
- package/src/lib/server/media-delivery.d.ts +2 -0
- package/src/lib/server/media-delivery.js +2 -1
- package/src/lib/server/media-delivery.js.map +1 -1
- package/src/lib/server/media-serve-count.d.ts +50 -0
- package/src/lib/server/media-serve-count.js +62 -0
- package/src/lib/server/media-serve-count.js.map +1 -0
- package/src/lib/server/media-signing.d.ts +13 -2
- package/src/lib/server/media-signing.js +12 -8
- package/src/lib/server/media-signing.js.map +1 -1
- package/src/lib/server/media-tombstone.js +7 -5
- package/src/lib/server/media-tombstone.js.map +1 -1
- package/src/lib/server/media-variants.d.ts +151 -20
- package/src/lib/server/media-variants.js +204 -27
- package/src/lib/server/media-variants.js.map +1 -1
- package/src/lib/server/organizations.d.ts +102 -2
- package/src/lib/server/organizations.js +388 -58
- package/src/lib/server/organizations.js.map +1 -1
- package/src/lib/server/outbound-send-review.js +5 -4
- package/src/lib/server/outbound-send-review.js.map +1 -1
- package/src/lib/server/paged-sweeps.d.ts +51 -0
- package/src/lib/server/paged-sweeps.js +79 -0
- package/src/lib/server/paged-sweeps.js.map +1 -0
- package/src/lib/server/platform-marketing-consent.d.ts +0 -49
- package/src/lib/server/platform-marketing-consent.js +27 -11
- package/src/lib/server/platform-marketing-consent.js.map +1 -1
- package/src/lib/server/serve-media-cdn.d.ts +69 -1
- package/src/lib/server/serve-media-cdn.js +424 -70
- package/src/lib/server/serve-media-cdn.js.map +1 -1
- package/src/lib/server/site-icon-render.d.ts +68 -0
- package/src/lib/server/site-icon-render.js +157 -0
- package/src/lib/server/site-icon-render.js.map +1 -0
- package/src/lib/server/staff-seat.d.ts +50 -0
- package/src/lib/server/staff-seat.js +118 -0
- package/src/lib/server/staff-seat.js.map +1 -0
- package/src/lib/server/transfer-export.d.ts +46 -0
- package/src/lib/server/transfer-export.js +221 -0
- package/src/lib/server/transfer-export.js.map +1 -0
- package/src/lib/server/transfer-jobs.d.ts +385 -0
- package/src/lib/server/transfer-jobs.js +2368 -0
- package/src/lib/server/transfer-jobs.js.map +1 -0
- package/src/lib/server/transfer-packages.d.ts +127 -0
- package/src/lib/server/transfer-packages.js +1036 -0
- package/src/lib/server/transfer-packages.js.map +1 -0
- package/src/lib/server/upgrade-proposal.d.ts +97 -0
- package/src/lib/server/upgrade-proposal.js +312 -0
- package/src/lib/server/upgrade-proposal.js.map +1 -0
- package/src/lib/server/upsert-contact.d.ts +17 -8
- package/src/lib/server/upsert-contact.js +82 -16
- package/src/lib/server/upsert-contact.js.map +1 -1
- package/src/lib/server/web-risk.d.ts +48 -9
- package/src/lib/server/web-risk.js +281 -143
- package/src/lib/server/web-risk.js.map +1 -1
- package/src/tenancy.d.ts +3 -1
- package/src/tenancy.js +5 -1
- package/src/tenancy.js.map +1 -1
- package/src/lib/server/campaign-attribution-store.d.ts +0 -55
- package/src/lib/server/campaign-attribution-store.js +0 -95
- package/src/lib/server/campaign-attribution-store.js.map +0 -1
- package/src/lib/server/campaign-conversion-attribution.d.ts +0 -283
- package/src/lib/server/campaign-conversion-attribution.js +0 -450
- package/src/lib/server/campaign-conversion-attribution.js.map +0 -1
- package/src/lib/server/dataset-live-pages.d.ts +0 -179
- package/src/lib/server/dataset-live-pages.js +0 -421
- package/src/lib/server/dataset-live-pages.js.map +0 -1
- package/src/lib/server/email-revenue-attribution.d.ts +0 -185
- package/src/lib/server/email-revenue-attribution.js +0 -321
- package/src/lib/server/email-revenue-attribution.js.map +0 -1
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
import type { Sharp, SharpOptions } from 'sharp';
|
|
17
18
|
/**
|
|
18
19
|
* WebP variant generation for a media asset (AGL-175), and the record of
|
|
19
20
|
* whether it worked (AGL-1468).
|
|
@@ -50,28 +51,104 @@
|
|
|
50
51
|
* A partial run reports both: the widths that landed AND the error, because
|
|
51
52
|
* the widths that landed are real files that the CDN route can serve.
|
|
52
53
|
*/
|
|
53
|
-
/** The shape of `sharp`'s default export
|
|
54
|
-
type SharpFactory = (input
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
/** The shape of `sharp`'s default export. */
|
|
55
|
+
type SharpFactory = (input?: Buffer, options?: SharpOptions) => Sharp;
|
|
56
|
+
/**
|
|
57
|
+
* What a media document records about its delivery copies' display half
|
|
58
|
+
* (AGL-3486). Present only when a display object was written; its absence on
|
|
59
|
+
* a document at the current encoder generation means the original is clean
|
|
60
|
+
* enough to serve as it is.
|
|
61
|
+
*/
|
|
62
|
+
export interface MediaDisplayCopy {
|
|
63
|
+
/** Always the original's own type — the display copy never changes format. */
|
|
64
|
+
contentType: string;
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
sizeBytes: number;
|
|
68
|
+
}
|
|
66
69
|
export interface MediaVariantOutcome {
|
|
67
70
|
/** Widths actually written. Safe to store — every entry is a real object. */
|
|
68
71
|
variants: number[];
|
|
72
|
+
/**
|
|
73
|
+
* The display copy written for this asset, when one was needed and made.
|
|
74
|
+
* Only ever produced when the caller asked for one (`display: true`).
|
|
75
|
+
*/
|
|
76
|
+
display?: MediaDisplayCopy;
|
|
69
77
|
/**
|
|
70
78
|
* Present ONLY when generation was attempted and did not complete.
|
|
71
79
|
* `undefined` means nothing went wrong, which includes "nothing to do".
|
|
72
80
|
*/
|
|
73
81
|
error?: string;
|
|
74
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Writes one derived object. `contentType` is the type the bytes ARE: WebP
|
|
85
|
+
* for a variant, the original's own type for a display copy.
|
|
86
|
+
*/
|
|
87
|
+
export type SaveMediaDerivedObject = (path: string, bytes: Buffer, contentType: string) => Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* The WebP encoder settings for every `?w=` variant (AGL-3486).
|
|
90
|
+
*
|
|
91
|
+
* Quality 72 is where a photo stops showing a difference to the eye at the
|
|
92
|
+
* size it is painted; 80, the setting since AGL-175, spent 15-25% more bytes
|
|
93
|
+
* on detail nobody sees. Effort 5 of 6: measured on the four images the issue
|
|
94
|
+
* names, effort 6 saved another 1-2% for twice the encode time, and the
|
|
95
|
+
* encode runs inside an upload request or after a CDN response.
|
|
96
|
+
*
|
|
97
|
+
* Changing either value changes the bytes every asset should hold, so it
|
|
98
|
+
* bumps `MEDIA_VARIANT_ENCODER_VERSION` — that is what reaches the existing
|
|
99
|
+
* corpus.
|
|
100
|
+
*/
|
|
101
|
+
export declare const MEDIA_VARIANT_WEBP_OPTIONS: Readonly<{
|
|
102
|
+
quality: 72;
|
|
103
|
+
effort: 5;
|
|
104
|
+
}>;
|
|
105
|
+
/**
|
|
106
|
+
* The quality the display copy is re-encoded at (AGL-3486). Higher than a
|
|
107
|
+
* variant's because the display copy is what a link preview, an email client
|
|
108
|
+
* and a CSS background receive at full size — it stands in for the original.
|
|
109
|
+
*/
|
|
110
|
+
export declare const MEDIA_DISPLAY_QUALITY = 82;
|
|
111
|
+
/**
|
|
112
|
+
* Formats that get a display copy. JPEG, PNG and WebP are the formats a
|
|
113
|
+
* camera, a screenshot tool and a design export produce, and the ones `sharp`
|
|
114
|
+
* re-encodes losslessly enough to stand in for the original. Not GIF, whose
|
|
115
|
+
* animation the encoder would flatten, and not SVG or ICO, which have no
|
|
116
|
+
* variants at all.
|
|
117
|
+
*/
|
|
118
|
+
export declare const MEDIA_DISPLAY_TYPES: ReadonlySet<string>;
|
|
119
|
+
/**
|
|
120
|
+
* Encode one `?w=` variant: auto-oriented, downscaled, metadata stripped.
|
|
121
|
+
*
|
|
122
|
+
* `rotate()` with no angle applies the EXIF orientation, which a WebP cannot
|
|
123
|
+
* carry — before AGL-3486 a portrait phone photo's variants came out lying on
|
|
124
|
+
* their side. `sharp` writes no EXIF, XMP or IPTC unless asked to keep it, so
|
|
125
|
+
* a photo's GPS position never reaches a variant.
|
|
126
|
+
*/
|
|
127
|
+
export declare function encodeMediaVariant(sharp: SharpFactory, source: Buffer, width: number): Promise<Buffer>;
|
|
128
|
+
/**
|
|
129
|
+
* The display copy of an image, or null when the original can be served as
|
|
130
|
+
* it is (AGL-3486).
|
|
131
|
+
*
|
|
132
|
+
* The bare URL is what a link preview, an email client, a CSS background and
|
|
133
|
+
* every pasted Copy URL fetch, and before this it served the upload byte for
|
|
134
|
+
* byte: a 6 MB, 6000-pixel phone photo with the GPS position it was taken at.
|
|
135
|
+
* A display copy is made when the original has any of three properties:
|
|
136
|
+
*
|
|
137
|
+
* - **larger than {@link MEDIA_DELIVERY_MAX_EDGE} on its long edge** —
|
|
138
|
+
* downscaled to fit, since nothing on a page paints more;
|
|
139
|
+
* - **EXIF, XMP or IPTC metadata** — stripped. This is the privacy half, and
|
|
140
|
+
* it is kept even when the re-encode is not smaller: a location is not
|
|
141
|
+
* something to trade for bytes;
|
|
142
|
+
* - **an EXIF orientation** — applied, so the pixels are upright without it.
|
|
143
|
+
*
|
|
144
|
+
* Same format as the original, so nothing that could read the original loses
|
|
145
|
+
* the ability to read the copy. Null for an animated image, whose frames the
|
|
146
|
+
* encoder would drop, and for a format outside {@link MEDIA_DISPLAY_TYPES}.
|
|
147
|
+
*/
|
|
148
|
+
export declare function encodeMediaDisplay(sharp: SharpFactory, source: Buffer, contentType: string): Promise<{
|
|
149
|
+
bytes: Buffer;
|
|
150
|
+
copy: MediaDisplayCopy;
|
|
151
|
+
} | null>;
|
|
75
152
|
/**
|
|
76
153
|
* `image/*` types that produce no variants, for two different reasons.
|
|
77
154
|
*
|
|
@@ -85,9 +162,10 @@ export interface MediaVariantOutcome {
|
|
|
85
162
|
* outcome, and it has to read as one or the fault counter measures the favicon
|
|
86
163
|
* instead of a fault (AGL-3121).
|
|
87
164
|
*
|
|
88
|
-
* Exported because
|
|
89
|
-
*
|
|
90
|
-
* and not the
|
|
165
|
+
* Exported because lazy regeneration (`media-delivery-regeneration.ts`) has to
|
|
166
|
+
* skip exactly this set. The backfill script it replaced kept a second copy of
|
|
167
|
+
* the SVG half, which is how ICO came to be missing from one and not the
|
|
168
|
+
* other.
|
|
91
169
|
*/
|
|
92
170
|
export declare const MEDIA_TYPES_WITHOUT_VARIANTS: ReadonlySet<string>;
|
|
93
171
|
/**
|
|
@@ -143,12 +221,25 @@ export declare function loadSharp(): Promise<SharpFactory>;
|
|
|
143
221
|
*/
|
|
144
222
|
export declare function classifyLoadFailure(error: unknown): string;
|
|
145
223
|
/**
|
|
146
|
-
* Generate the
|
|
224
|
+
* Generate the delivery copies for one asset and report the outcome: its
|
|
225
|
+
* WebP variants, and with `display: true` its display copy (AGL-3486).
|
|
147
226
|
*
|
|
148
227
|
* `saveVariant` is a callback rather than a `Bucket` so this stays free of a
|
|
149
228
|
* storage dependency and so a spec can assert on the BYTES it produces —
|
|
150
229
|
* which is the only assertion that distinguishes a working variant from the
|
|
151
230
|
* original served back under a `?w=` that selects nothing.
|
|
231
|
+
*
|
|
232
|
+
* ## A variant is never larger than what it replaces
|
|
233
|
+
*
|
|
234
|
+
* A width the asset has no variant for is answered with the display copy, or
|
|
235
|
+
* the original when there is none — so a variant is only worth storing when
|
|
236
|
+
* it is SMALLER than that answer. Measured on a customer's 1280px JPEGs before
|
|
237
|
+
* AGL-3486: the source-width WebP at `?w=1280` was 384 KB against a 305 KB
|
|
238
|
+
* original, so the variant made every retina visit 26% heavier. A width whose
|
|
239
|
+
* encode comes out no smaller is now skipped and the smaller bytes serve it.
|
|
240
|
+
*
|
|
241
|
+
* The display copy is made FIRST for that reason: it is what the comparison
|
|
242
|
+
* is against.
|
|
152
243
|
*/
|
|
153
244
|
export declare function generateMediaVariants(options: {
|
|
154
245
|
buffer: Buffer;
|
|
@@ -156,8 +247,46 @@ export declare function generateMediaVariants(options: {
|
|
|
156
247
|
sourceWidth?: number | null;
|
|
157
248
|
/** Object path of the ORIGINAL; variants are `${objectPath}__w{n}.webp`. */
|
|
158
249
|
objectPath: string;
|
|
159
|
-
saveVariant:
|
|
250
|
+
saveVariant: SaveMediaDerivedObject;
|
|
251
|
+
/** Also make the display copy. Asked for by an asset, never by a poster. */
|
|
252
|
+
display?: boolean;
|
|
160
253
|
}): Promise<MediaVariantOutcome>;
|
|
254
|
+
/**
|
|
255
|
+
* The media document fields a generation outcome writes (AGL-3486), shared by
|
|
256
|
+
* every route that generates so none of them can forget the encoder
|
|
257
|
+
* generation — the field lazy regeneration keys on.
|
|
258
|
+
*
|
|
259
|
+
* `display` is written as `null` rather than left out when there is none, so
|
|
260
|
+
* a replace whose new bytes need no display copy clears the previous one's
|
|
261
|
+
* record instead of pointing at an object it just deleted. The caller decides
|
|
262
|
+
* `variantsError`, because the routes record it with a counter beside it.
|
|
263
|
+
*/
|
|
264
|
+
export declare function mediaVariantDocFields(outcome: Pick<MediaVariantOutcome, 'variants' | 'display'>): {
|
|
265
|
+
variants: number[];
|
|
266
|
+
variantEncoderVersion: number;
|
|
267
|
+
display: MediaDisplayCopy | null;
|
|
268
|
+
};
|
|
269
|
+
/**
|
|
270
|
+
* Every derived object a media document names, given its original's path
|
|
271
|
+
* (AGL-3486): the `?w=` variants, the display copy, a video's poster and the
|
|
272
|
+
* poster's widths, and its renditions.
|
|
273
|
+
*
|
|
274
|
+
* The one list every path that moves, replaces or deletes an asset reads, so
|
|
275
|
+
* a new kind of derived object is added in one place. Before it, each of the
|
|
276
|
+
* three kept its own: a replace dropped all four kinds, while a folder move
|
|
277
|
+
* and a delete knew only the variants — so a moved film lost its poster and
|
|
278
|
+
* renditions to the old prefix, and a deleted one left them in the bucket.
|
|
279
|
+
*/
|
|
280
|
+
export declare function mediaDerivedObjectPaths(objectPath: string,
|
|
281
|
+
/** The media document's fields, or its snapshot. */
|
|
282
|
+
document: {
|
|
283
|
+
variants?: unknown;
|
|
284
|
+
display?: unknown;
|
|
285
|
+
poster?: unknown;
|
|
286
|
+
videoRenditions?: unknown;
|
|
287
|
+
} | {
|
|
288
|
+
get(field: string): unknown;
|
|
289
|
+
}): string[];
|
|
161
290
|
/**
|
|
162
291
|
* The largest object this will pull back OUT of storage in order to generate.
|
|
163
292
|
*
|
|
@@ -210,8 +339,10 @@ export declare function generateStoredMediaVariants(options: {
|
|
|
210
339
|
objectPath: string;
|
|
211
340
|
/** Fetches the whole object. Only ever called when there is work to do. */
|
|
212
341
|
readSource: () => Promise<Buffer>;
|
|
213
|
-
saveVariant:
|
|
342
|
+
saveVariant: SaveMediaDerivedObject;
|
|
214
343
|
maxSourceBytes?: number;
|
|
344
|
+
/** Also make the display copy (see {@link generateMediaVariants}). */
|
|
345
|
+
display?: boolean;
|
|
215
346
|
}): Promise<MediaVariantOutcome>;
|
|
216
347
|
/**
|
|
217
348
|
* Can this deployment produce a variant at all?
|
|
@@ -303,7 +434,7 @@ export declare function generateMediaPoster(options: {
|
|
|
303
434
|
buffer: Buffer;
|
|
304
435
|
/** Object path of the ORIGINAL video, without the poster suffix. */
|
|
305
436
|
objectPath: string;
|
|
306
|
-
saveVariant:
|
|
437
|
+
saveVariant: SaveMediaDerivedObject;
|
|
307
438
|
maxWidth?: number;
|
|
308
439
|
}): Promise<MediaPosterOutcome>;
|
|
309
440
|
export {};
|
|
@@ -14,8 +14,120 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
18
|
-
|
|
17
|
+
*/ import { readImageDimensions } from "@aglyn/aglyn/server";
|
|
18
|
+
// By path: the media-ref grammar is a leaf, and the routes that spread this
|
|
19
|
+
// module's helpers into a document reach it the same way.
|
|
20
|
+
import { MEDIA_CDN_VARIANT_WIDTHS, MEDIA_DELIVERY_MAX_EDGE, MEDIA_POSTER_OBJECT_SUFFIX, MEDIA_VARIANT_ENCODER_VERSION, mediaDisplayObjectPath, mediaPosterObjectPath, mediaRenditionObjectPath, parseMediaRenditions } from "@aglyn/aglyn/app-utils/media-ref";
|
|
21
|
+
/**
|
|
22
|
+
* The WebP encoder settings for every `?w=` variant (AGL-3486).
|
|
23
|
+
*
|
|
24
|
+
* Quality 72 is where a photo stops showing a difference to the eye at the
|
|
25
|
+
* size it is painted; 80, the setting since AGL-175, spent 15-25% more bytes
|
|
26
|
+
* on detail nobody sees. Effort 5 of 6: measured on the four images the issue
|
|
27
|
+
* names, effort 6 saved another 1-2% for twice the encode time, and the
|
|
28
|
+
* encode runs inside an upload request or after a CDN response.
|
|
29
|
+
*
|
|
30
|
+
* Changing either value changes the bytes every asset should hold, so it
|
|
31
|
+
* bumps `MEDIA_VARIANT_ENCODER_VERSION` — that is what reaches the existing
|
|
32
|
+
* corpus.
|
|
33
|
+
*/ export const MEDIA_VARIANT_WEBP_OPTIONS = Object.freeze({
|
|
34
|
+
quality: 72,
|
|
35
|
+
effort: 5
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* The quality the display copy is re-encoded at (AGL-3486). Higher than a
|
|
39
|
+
* variant's because the display copy is what a link preview, an email client
|
|
40
|
+
* and a CSS background receive at full size — it stands in for the original.
|
|
41
|
+
*/ export const MEDIA_DISPLAY_QUALITY = 82;
|
|
42
|
+
/**
|
|
43
|
+
* Formats that get a display copy. JPEG, PNG and WebP are the formats a
|
|
44
|
+
* camera, a screenshot tool and a design export produce, and the ones `sharp`
|
|
45
|
+
* re-encodes losslessly enough to stand in for the original. Not GIF, whose
|
|
46
|
+
* animation the encoder would flatten, and not SVG or ICO, which have no
|
|
47
|
+
* variants at all.
|
|
48
|
+
*/ export const MEDIA_DISPLAY_TYPES = new Set([
|
|
49
|
+
'image/jpeg',
|
|
50
|
+
'image/jpg',
|
|
51
|
+
'image/png',
|
|
52
|
+
'image/webp'
|
|
53
|
+
]);
|
|
54
|
+
/**
|
|
55
|
+
* Encode one `?w=` variant: auto-oriented, downscaled, metadata stripped.
|
|
56
|
+
*
|
|
57
|
+
* `rotate()` with no angle applies the EXIF orientation, which a WebP cannot
|
|
58
|
+
* carry — before AGL-3486 a portrait phone photo's variants came out lying on
|
|
59
|
+
* their side. `sharp` writes no EXIF, XMP or IPTC unless asked to keep it, so
|
|
60
|
+
* a photo's GPS position never reaches a variant.
|
|
61
|
+
*/ export async function encodeMediaVariant(sharp, source, width) {
|
|
62
|
+
return sharp(source).rotate().resize({
|
|
63
|
+
width,
|
|
64
|
+
withoutEnlargement: true
|
|
65
|
+
}).webp(MEDIA_VARIANT_WEBP_OPTIONS).toBuffer();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The display copy of an image, or null when the original can be served as
|
|
69
|
+
* it is (AGL-3486).
|
|
70
|
+
*
|
|
71
|
+
* The bare URL is what a link preview, an email client, a CSS background and
|
|
72
|
+
* every pasted Copy URL fetch, and before this it served the upload byte for
|
|
73
|
+
* byte: a 6 MB, 6000-pixel phone photo with the GPS position it was taken at.
|
|
74
|
+
* A display copy is made when the original has any of three properties:
|
|
75
|
+
*
|
|
76
|
+
* - **larger than {@link MEDIA_DELIVERY_MAX_EDGE} on its long edge** —
|
|
77
|
+
* downscaled to fit, since nothing on a page paints more;
|
|
78
|
+
* - **EXIF, XMP or IPTC metadata** — stripped. This is the privacy half, and
|
|
79
|
+
* it is kept even when the re-encode is not smaller: a location is not
|
|
80
|
+
* something to trade for bytes;
|
|
81
|
+
* - **an EXIF orientation** — applied, so the pixels are upright without it.
|
|
82
|
+
*
|
|
83
|
+
* Same format as the original, so nothing that could read the original loses
|
|
84
|
+
* the ability to read the copy. Null for an animated image, whose frames the
|
|
85
|
+
* encoder would drop, and for a format outside {@link MEDIA_DISPLAY_TYPES}.
|
|
86
|
+
*/ export async function encodeMediaDisplay(sharp, source, contentType) {
|
|
87
|
+
var _metadata_pages, _metadata_width, _metadata_height, _metadata_orientation;
|
|
88
|
+
if (!MEDIA_DISPLAY_TYPES.has(contentType)) return null;
|
|
89
|
+
const metadata = await sharp(source).metadata();
|
|
90
|
+
if (((_metadata_pages = metadata.pages) != null ? _metadata_pages : 1) > 1) return null;
|
|
91
|
+
const width = (_metadata_width = metadata.width) != null ? _metadata_width : 0;
|
|
92
|
+
const height = (_metadata_height = metadata.height) != null ? _metadata_height : 0;
|
|
93
|
+
const oversize = Math.max(width, height) > MEDIA_DELIVERY_MAX_EDGE;
|
|
94
|
+
const oriented = ((_metadata_orientation = metadata.orientation) != null ? _metadata_orientation : 1) > 1;
|
|
95
|
+
const carriesMetadata = Boolean(metadata.exif || metadata.xmp || metadata.iptc);
|
|
96
|
+
if (!oversize && !oriented && !carriesMetadata) return null;
|
|
97
|
+
let pipeline = sharp(source).rotate();
|
|
98
|
+
if (oversize) {
|
|
99
|
+
pipeline = pipeline.resize({
|
|
100
|
+
width: MEDIA_DELIVERY_MAX_EDGE,
|
|
101
|
+
height: MEDIA_DELIVERY_MAX_EDGE,
|
|
102
|
+
fit: 'inside',
|
|
103
|
+
withoutEnlargement: true
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
pipeline = contentType === 'image/png' ? pipeline.png({
|
|
107
|
+
compressionLevel: 9,
|
|
108
|
+
// A palette PNG re-encoded as truecolor can triple in size.
|
|
109
|
+
palette: Boolean(metadata.isPalette)
|
|
110
|
+
}) : contentType === 'image/webp' ? pipeline.webp(_extends({}, MEDIA_VARIANT_WEBP_OPTIONS, {
|
|
111
|
+
quality: MEDIA_DISPLAY_QUALITY
|
|
112
|
+
})) : pipeline.jpeg({
|
|
113
|
+
quality: MEDIA_DISPLAY_QUALITY,
|
|
114
|
+
mozjpeg: true
|
|
115
|
+
});
|
|
116
|
+
const { data, info } = await pipeline.toBuffer({
|
|
117
|
+
resolveWithObject: true
|
|
118
|
+
});
|
|
119
|
+
// Only the privacy reason justifies a copy that is not smaller.
|
|
120
|
+
if (!carriesMetadata && data.length >= source.length) return null;
|
|
121
|
+
return {
|
|
122
|
+
bytes: data,
|
|
123
|
+
copy: {
|
|
124
|
+
contentType,
|
|
125
|
+
width: info.width,
|
|
126
|
+
height: info.height,
|
|
127
|
+
sizeBytes: data.length
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
19
131
|
/**
|
|
20
132
|
* Formats a WebP copy at the SOURCE'S OWN width beats.
|
|
21
133
|
*
|
|
@@ -49,9 +161,10 @@ import { MEDIA_CDN_VARIANT_WIDTHS } from "./serve-media-cdn.js";
|
|
|
49
161
|
* outcome, and it has to read as one or the fault counter measures the favicon
|
|
50
162
|
* instead of a fault (AGL-3121).
|
|
51
163
|
*
|
|
52
|
-
* Exported because
|
|
53
|
-
*
|
|
54
|
-
* and not the
|
|
164
|
+
* Exported because lazy regeneration (`media-delivery-regeneration.ts`) has to
|
|
165
|
+
* skip exactly this set. The backfill script it replaced kept a second copy of
|
|
166
|
+
* the SVG half, which is how ICO came to be missing from one and not the
|
|
167
|
+
* other.
|
|
55
168
|
*/ export const MEDIA_TYPES_WITHOUT_VARIANTS = new Set([
|
|
56
169
|
'image/svg+xml',
|
|
57
170
|
'image/x-icon',
|
|
@@ -143,42 +256,110 @@ import { MEDIA_CDN_VARIANT_WIDTHS } from "./serve-media-cdn.js";
|
|
|
143
256
|
return text.slice(0, 300);
|
|
144
257
|
}
|
|
145
258
|
/**
|
|
146
|
-
* Generate the
|
|
259
|
+
* Generate the delivery copies for one asset and report the outcome: its
|
|
260
|
+
* WebP variants, and with `display: true` its display copy (AGL-3486).
|
|
147
261
|
*
|
|
148
262
|
* `saveVariant` is a callback rather than a `Bucket` so this stays free of a
|
|
149
263
|
* storage dependency and so a spec can assert on the BYTES it produces —
|
|
150
264
|
* which is the only assertion that distinguishes a working variant from the
|
|
151
265
|
* original served back under a `?w=` that selects nothing.
|
|
266
|
+
*
|
|
267
|
+
* ## A variant is never larger than what it replaces
|
|
268
|
+
*
|
|
269
|
+
* A width the asset has no variant for is answered with the display copy, or
|
|
270
|
+
* the original when there is none — so a variant is only worth storing when
|
|
271
|
+
* it is SMALLER than that answer. Measured on a customer's 1280px JPEGs before
|
|
272
|
+
* AGL-3486: the source-width WebP at `?w=1280` was 384 KB against a 305 KB
|
|
273
|
+
* original, so the variant made every retina visit 26% heavier. A width whose
|
|
274
|
+
* encode comes out no smaller is now skipped and the smaller bytes serve it.
|
|
275
|
+
*
|
|
276
|
+
* The display copy is made FIRST for that reason: it is what the comparison
|
|
277
|
+
* is against.
|
|
152
278
|
*/ export async function generateMediaVariants(options) {
|
|
153
279
|
const widths = mediaVariantWidthsFor(options);
|
|
154
|
-
|
|
280
|
+
const wantsDisplay = options.display === true && MEDIA_DISPLAY_TYPES.has(options.contentType);
|
|
281
|
+
if (!widths.length && !wantsDisplay) return {
|
|
155
282
|
variants: []
|
|
156
283
|
};
|
|
157
284
|
const variants = [];
|
|
285
|
+
let display;
|
|
158
286
|
try {
|
|
159
287
|
const sharp = await loadSharp();
|
|
288
|
+
let fallbackBytes = options.buffer.length;
|
|
289
|
+
if (wantsDisplay) {
|
|
290
|
+
const made = await encodeMediaDisplay(sharp, options.buffer, options.contentType);
|
|
291
|
+
if (made) {
|
|
292
|
+
await options.saveVariant(mediaDisplayObjectPath(options.objectPath), made.bytes, options.contentType);
|
|
293
|
+
display = made.copy;
|
|
294
|
+
fallbackBytes = made.bytes.length;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
160
297
|
for (const width of widths){
|
|
161
|
-
const webp = await sharp
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}).webp({
|
|
165
|
-
quality: 80
|
|
166
|
-
}).toBuffer();
|
|
167
|
-
await options.saveVariant(`${options.objectPath}__w${width}.webp`, webp);
|
|
298
|
+
const webp = await encodeMediaVariant(sharp, options.buffer, width);
|
|
299
|
+
if (webp.length >= fallbackBytes) continue;
|
|
300
|
+
await options.saveVariant(`${options.objectPath}__w${width}.webp`, webp, 'image/webp');
|
|
168
301
|
variants.push(width);
|
|
169
302
|
}
|
|
170
303
|
} catch (error) {
|
|
171
304
|
// Still never fails the upload — but no longer only whispers.
|
|
172
305
|
console.error('media variant generation failed', options.objectPath, error);
|
|
173
|
-
return {
|
|
174
|
-
variants
|
|
306
|
+
return _extends({
|
|
307
|
+
variants
|
|
308
|
+
}, display ? {
|
|
309
|
+
display
|
|
310
|
+
} : {}, {
|
|
175
311
|
error: describe(error)
|
|
176
|
-
};
|
|
312
|
+
});
|
|
177
313
|
}
|
|
178
|
-
return {
|
|
314
|
+
return _extends({
|
|
179
315
|
variants
|
|
316
|
+
}, display ? {
|
|
317
|
+
display
|
|
318
|
+
} : {});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* The media document fields a generation outcome writes (AGL-3486), shared by
|
|
322
|
+
* every route that generates so none of them can forget the encoder
|
|
323
|
+
* generation — the field lazy regeneration keys on.
|
|
324
|
+
*
|
|
325
|
+
* `display` is written as `null` rather than left out when there is none, so
|
|
326
|
+
* a replace whose new bytes need no display copy clears the previous one's
|
|
327
|
+
* record instead of pointing at an object it just deleted. The caller decides
|
|
328
|
+
* `variantsError`, because the routes record it with a counter beside it.
|
|
329
|
+
*/ export function mediaVariantDocFields(outcome) {
|
|
330
|
+
var _outcome_display;
|
|
331
|
+
return {
|
|
332
|
+
variants: outcome.variants,
|
|
333
|
+
variantEncoderVersion: MEDIA_VARIANT_ENCODER_VERSION,
|
|
334
|
+
display: (_outcome_display = outcome.display) != null ? _outcome_display : null
|
|
180
335
|
};
|
|
181
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* Every derived object a media document names, given its original's path
|
|
339
|
+
* (AGL-3486): the `?w=` variants, the display copy, a video's poster and the
|
|
340
|
+
* poster's widths, and its renditions.
|
|
341
|
+
*
|
|
342
|
+
* The one list every path that moves, replaces or deletes an asset reads, so
|
|
343
|
+
* a new kind of derived object is added in one place. Before it, each of the
|
|
344
|
+
* three kept its own: a replace dropped all four kinds, while a folder move
|
|
345
|
+
* and a delete knew only the variants — so a moved film lost its poster and
|
|
346
|
+
* renditions to the old prefix, and a deleted one left them in the bucket.
|
|
347
|
+
*/ export function mediaDerivedObjectPaths(objectPath, /** The media document's fields, or its snapshot. */ document) {
|
|
348
|
+
const read = (field)=>'get' in document && typeof document.get === 'function' ? document.get(field) : document[field];
|
|
349
|
+
const widths = (value)=>Array.isArray(value) ? value.filter((width)=>Number.isInteger(width) && width > 0) : [];
|
|
350
|
+
const poster = read('poster');
|
|
351
|
+
return [
|
|
352
|
+
...widths(read('variants')).map((width)=>`${objectPath}__w${width}.webp`),
|
|
353
|
+
...read('display') ? [
|
|
354
|
+
mediaDisplayObjectPath(objectPath)
|
|
355
|
+
] : [],
|
|
356
|
+
...poster ? [
|
|
357
|
+
mediaPosterObjectPath(objectPath)
|
|
358
|
+
] : [],
|
|
359
|
+
...widths(poster == null ? void 0 : poster.variants).map((width)=>`${objectPath}${MEDIA_POSTER_OBJECT_SUFFIX}__w${width}.webp`),
|
|
360
|
+
...parseMediaRenditions(read('videoRenditions')).map((rendition)=>mediaRenditionObjectPath(objectPath, rendition))
|
|
361
|
+
];
|
|
362
|
+
}
|
|
182
363
|
/**
|
|
183
364
|
* The largest object this will pull back OUT of storage in order to generate.
|
|
184
365
|
*
|
|
@@ -224,7 +405,8 @@ import { MEDIA_CDN_VARIANT_WIDTHS } from "./serve-media-cdn.js";
|
|
|
224
405
|
*/ export async function generateStoredMediaVariants(options) {
|
|
225
406
|
var _options_maxSourceBytes;
|
|
226
407
|
const widths = mediaVariantWidthsFor(options);
|
|
227
|
-
|
|
408
|
+
const wantsDisplay = options.display === true && MEDIA_DISPLAY_TYPES.has(options.contentType);
|
|
409
|
+
if (!widths.length && !wantsDisplay) return {
|
|
228
410
|
variants: []
|
|
229
411
|
};
|
|
230
412
|
const maxSourceBytes = (_options_maxSourceBytes = options.maxSourceBytes) != null ? _options_maxSourceBytes : MEDIA_VARIANT_SOURCE_MAX_BYTES;
|
|
@@ -288,12 +470,7 @@ import { MEDIA_CDN_VARIANT_WIDTHS } from "./serve-media-cdn.js";
|
|
|
288
470
|
channels: 3
|
|
289
471
|
}
|
|
290
472
|
}).png().toBuffer();
|
|
291
|
-
const resized = await sharp
|
|
292
|
-
width: 320,
|
|
293
|
-
withoutEnlargement: true
|
|
294
|
-
}).webp({
|
|
295
|
-
quality: 80
|
|
296
|
-
}).toBuffer();
|
|
473
|
+
const resized = await encodeMediaVariant(sharp, source, 320);
|
|
297
474
|
// `RIFF....WEBP` — a WebP, not the PNG handed back.
|
|
298
475
|
const isWebp = resized.length > 12 && resized.toString('ascii', 0, 4) === 'RIFF' && resized.toString('ascii', 8, 12) === 'WEBP';
|
|
299
476
|
if (!isWebp) return {
|
|
@@ -333,7 +510,7 @@ import { MEDIA_CDN_VARIANT_WIDTHS } from "./serve-media-cdn.js";
|
|
|
333
510
|
* the largest realistic player box is the 1920 the variant widths already
|
|
334
511
|
* top out at.
|
|
335
512
|
*/ export const MEDIA_POSTER_MAX_WIDTH = 1920;
|
|
336
|
-
/** WebP quality for the poster
|
|
513
|
+
/** WebP quality for the poster — the variants' own since AGL-3486. */ const POSTER_QUALITY = MEDIA_VARIANT_WEBP_OPTIONS.quality;
|
|
337
514
|
/**
|
|
338
515
|
* Turn a client-supplied still into the asset's poster (AGL-2742).
|
|
339
516
|
*
|
|
@@ -397,7 +574,7 @@ import { MEDIA_CDN_VARIANT_WIDTHS } from "./serve-media-cdn.js";
|
|
|
397
574
|
};
|
|
398
575
|
}
|
|
399
576
|
try {
|
|
400
|
-
await options.saveVariant(posterPath, webp);
|
|
577
|
+
await options.saveVariant(posterPath, webp, 'image/webp');
|
|
401
578
|
} catch (error) {
|
|
402
579
|
console.error('media poster save failed', posterPath, error);
|
|
403
580
|
return {
|