@devultur/self-host 1.3.2 → 1.4.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.
- package/dist/index.d.ts +3 -0
- package/dist/index.js +117 -81
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -681,6 +681,9 @@ declare const ObjectMeta: {
|
|
|
681
681
|
readonly SHA256_VERIFIED: "sha256Verified";
|
|
682
682
|
/** Epoch ms. From then on the gate responds 410 (and an external sweeper may delete it). */
|
|
683
683
|
readonly EXPIRES_AT: "expiresAt";
|
|
684
|
+
/** An image's intrinsic size (px), read from its header bytes on a single PUT. */
|
|
685
|
+
readonly IMAGE_WIDTH: "imageWidth";
|
|
686
|
+
readonly IMAGE_HEIGHT: "imageHeight";
|
|
684
687
|
};
|
|
685
688
|
|
|
686
689
|
/** The upload an upload URL (`/v1/upload/<key>?u=<id>`) points to, as plugins see it. */
|
package/dist/index.js
CHANGED
|
@@ -870,6 +870,95 @@ import {
|
|
|
870
870
|
SHA256_VERIFIED_HEADER
|
|
871
871
|
} from "@devultur/core";
|
|
872
872
|
|
|
873
|
+
// src/routes/upload-image-size.ts
|
|
874
|
+
import { IMAGE_HEIGHT_HEADER, IMAGE_WIDTH_HEADER } from "@devultur/core";
|
|
875
|
+
|
|
876
|
+
// src/image/dimensions.ts
|
|
877
|
+
function readImageDimensions(bytes, contentType) {
|
|
878
|
+
if (contentType === "image/jpeg") return readJpeg(bytes);
|
|
879
|
+
if (contentType === "image/png") return readPng(bytes);
|
|
880
|
+
if (contentType === "image/webp") return readWebp(bytes);
|
|
881
|
+
if (contentType === "image/avif") return readAvif(bytes);
|
|
882
|
+
if (contentType === "image/gif") return readGif(bytes);
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
var uint16BE = (bytes, offset) => bytes[offset] << 8 | bytes[offset + 1];
|
|
886
|
+
var uint32BE = (bytes, offset) => (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
|
|
887
|
+
var uint16LE = (bytes, offset) => bytes[offset] | bytes[offset + 1] << 8;
|
|
888
|
+
var uint24LE = (bytes, offset) => bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16;
|
|
889
|
+
var isStartOfFrame = (marker) => marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
|
|
890
|
+
function readJpeg(bytes) {
|
|
891
|
+
if (bytes[0] !== 255 || bytes[1] !== 216) return null;
|
|
892
|
+
let offset = 2;
|
|
893
|
+
while (offset + 9 < bytes.length) {
|
|
894
|
+
if (bytes[offset] !== 255) return null;
|
|
895
|
+
const marker = bytes[offset + 1];
|
|
896
|
+
if (isStartOfFrame(marker)) {
|
|
897
|
+
return { height: uint16BE(bytes, offset + 5), width: uint16BE(bytes, offset + 7) };
|
|
898
|
+
}
|
|
899
|
+
offset += 2 + uint16BE(bytes, offset + 2);
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
function readPng(bytes) {
|
|
904
|
+
const isPng2 = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
905
|
+
if (!isPng2 || bytes.length < 24) return null;
|
|
906
|
+
return { width: uint32BE(bytes, 16), height: uint32BE(bytes, 20) };
|
|
907
|
+
}
|
|
908
|
+
function readWebp(bytes) {
|
|
909
|
+
if (bytes.length < 30) return null;
|
|
910
|
+
const chunk = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
|
|
911
|
+
if (chunk === "VP8X") return { width: uint24LE(bytes, 24) + 1, height: uint24LE(bytes, 27) + 1 };
|
|
912
|
+
if (chunk === "VP8 ") return { width: uint16LE(bytes, 26) & 16383, height: uint16LE(bytes, 28) & 16383 };
|
|
913
|
+
if (chunk === "VP8L") {
|
|
914
|
+
const bits = bytes[21] | bytes[22] << 8 | bytes[23] << 16 | bytes[24] << 24;
|
|
915
|
+
return { width: (bits & 16383) + 1, height: (bits >> 14 & 16383) + 1 };
|
|
916
|
+
}
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
function readAvif(bytes) {
|
|
920
|
+
const limit = Math.min(bytes.length - 20, 64 * 1024);
|
|
921
|
+
for (let offset = 4; offset < limit; offset += 1) {
|
|
922
|
+
const isIspe = bytes[offset] === 105 && bytes[offset + 1] === 115 && bytes[offset + 2] === 112 && bytes[offset + 3] === 101;
|
|
923
|
+
if (isIspe) return { width: uint32BE(bytes, offset + 8), height: uint32BE(bytes, offset + 12) };
|
|
924
|
+
}
|
|
925
|
+
return null;
|
|
926
|
+
}
|
|
927
|
+
function readGif(bytes) {
|
|
928
|
+
if (bytes.length < 10) return null;
|
|
929
|
+
const signature = String.fromCharCode(...bytes.subarray(0, 6));
|
|
930
|
+
if (signature !== "GIF87a" && signature !== "GIF89a") return null;
|
|
931
|
+
return { width: uint16LE(bytes, 6), height: uint16LE(bytes, 8) };
|
|
932
|
+
}
|
|
933
|
+
var fourCC = (bytes, offset) => String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
|
|
934
|
+
function isAnimatedImage(bytes, contentType) {
|
|
935
|
+
if (contentType === "image/gif") return true;
|
|
936
|
+
if (contentType === "image/webp")
|
|
937
|
+
return bytes.length > 20 && fourCC(bytes, 12) === "VP8X" && (bytes[20] & 2) !== 0;
|
|
938
|
+
if (contentType === "image/png") return hasPngAnimationControl(bytes);
|
|
939
|
+
if (contentType === "image/avif") return hasAvifSequenceBrand(bytes);
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
function hasPngAnimationControl(bytes) {
|
|
943
|
+
let offset = 8;
|
|
944
|
+
while (offset + 8 <= bytes.length) {
|
|
945
|
+
const chunk = fourCC(bytes, offset + 4);
|
|
946
|
+
if (chunk === "acTL") return true;
|
|
947
|
+
if (chunk === "IDAT") return false;
|
|
948
|
+
offset += 12 + uint32BE(bytes, offset);
|
|
949
|
+
}
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
function hasAvifSequenceBrand(bytes) {
|
|
953
|
+
if (bytes.length < 16 || fourCC(bytes, 4) !== "ftyp") return false;
|
|
954
|
+
const boxEnd = Math.min(uint32BE(bytes, 0), bytes.length);
|
|
955
|
+
for (let offset = 8; offset + 4 <= boxEnd; offset += 4) {
|
|
956
|
+
if (offset === 12) continue;
|
|
957
|
+
if (fourCC(bytes, offset) === "avis") return true;
|
|
958
|
+
}
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
|
|
873
962
|
// src/routes/upload-integrity.ts
|
|
874
963
|
import { normalizeSha256, SHA256_HEADER, sha256Hex } from "@devultur/core";
|
|
875
964
|
var ObjectMeta = {
|
|
@@ -877,12 +966,17 @@ var ObjectMeta = {
|
|
|
877
966
|
/** `"true"` = the worker hashed the bytes; `"false"` = sha256 declared by the client (multipart). */
|
|
878
967
|
SHA256_VERIFIED: "sha256Verified",
|
|
879
968
|
/** Epoch ms. From then on the gate responds 410 (and an external sweeper may delete it). */
|
|
880
|
-
EXPIRES_AT: "expiresAt"
|
|
969
|
+
EXPIRES_AT: "expiresAt",
|
|
970
|
+
/** An image's intrinsic size (px), read from its header bytes on a single PUT. */
|
|
971
|
+
IMAGE_WIDTH: "imageWidth",
|
|
972
|
+
IMAGE_HEIGHT: "imageHeight"
|
|
881
973
|
};
|
|
882
974
|
var WORKER_METADATA_KEYS = /* @__PURE__ */ new Set([
|
|
883
975
|
"security",
|
|
884
976
|
"enc",
|
|
885
977
|
ObjectMeta.EXPIRES_AT.toLowerCase(),
|
|
978
|
+
ObjectMeta.IMAGE_WIDTH.toLowerCase(),
|
|
979
|
+
ObjectMeta.IMAGE_HEIGHT.toLowerCase(),
|
|
886
980
|
"derivedfrom",
|
|
887
981
|
LOCK_METADATA_KEY
|
|
888
982
|
]);
|
|
@@ -959,6 +1053,25 @@ async function discardStaleVariants(bucket, pending) {
|
|
|
959
1053
|
await deletePrefix(bucket, variantPrefix(pending.storageKey));
|
|
960
1054
|
}
|
|
961
1055
|
|
|
1056
|
+
// src/routes/upload-image-size.ts
|
|
1057
|
+
var MAX_IMAGE_SIDE = 1e5;
|
|
1058
|
+
function isImageSide(value) {
|
|
1059
|
+
return Number.isInteger(value) && value > 0 && value <= MAX_IMAGE_SIDE;
|
|
1060
|
+
}
|
|
1061
|
+
function imageSizeMetadata(contentType, body) {
|
|
1062
|
+
if (!contentType.startsWith("image/")) return {};
|
|
1063
|
+
const dimensions = readImageDimensions(new Uint8Array(body), contentType);
|
|
1064
|
+
if (!dimensions || !isImageSide(dimensions.width) || !isImageSide(dimensions.height)) return {};
|
|
1065
|
+
return { [ObjectMeta.IMAGE_WIDTH]: String(dimensions.width), [ObjectMeta.IMAGE_HEIGHT]: String(dimensions.height) };
|
|
1066
|
+
}
|
|
1067
|
+
function applyImageSizeHeaders(headers, customMetadata) {
|
|
1068
|
+
const width = customMetadata?.[ObjectMeta.IMAGE_WIDTH];
|
|
1069
|
+
const height = customMetadata?.[ObjectMeta.IMAGE_HEIGHT];
|
|
1070
|
+
if (!width || !height) return;
|
|
1071
|
+
headers.set(IMAGE_WIDTH_HEADER, width);
|
|
1072
|
+
headers.set(IMAGE_HEIGHT_HEADER, height);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
962
1075
|
// src/routes/object-response.ts
|
|
963
1076
|
function objectExpiresAt(customMetadata) {
|
|
964
1077
|
const expiresAt = Number(customMetadata?.[ObjectMeta.EXPIRES_AT]);
|
|
@@ -1023,6 +1136,7 @@ async function streamObject(input) {
|
|
|
1023
1136
|
headers.set("ETag", object.httpEtag);
|
|
1024
1137
|
headers.set("Accept-Ranges", "bytes");
|
|
1025
1138
|
applyIntegrityHeaders(headers, object.customMetadata);
|
|
1139
|
+
applyImageSizeHeaders(headers, object.customMetadata);
|
|
1026
1140
|
const enc = decodeEncryptionMeta2(object.customMetadata?.enc);
|
|
1027
1141
|
const loaded = enc ? await loadStorageKey(input.keysBucket, input.fullKey, enc.keyId) : null;
|
|
1028
1142
|
if (enc && !loaded) {
|
|
@@ -2565,85 +2679,6 @@ function hasTransformQuery(url) {
|
|
|
2565
2679
|
return TRANSFORM_QUERY_PARAMS.some((param) => url.searchParams.has(param));
|
|
2566
2680
|
}
|
|
2567
2681
|
|
|
2568
|
-
// src/image/dimensions.ts
|
|
2569
|
-
function readImageDimensions(bytes, contentType) {
|
|
2570
|
-
if (contentType === "image/jpeg") return readJpeg(bytes);
|
|
2571
|
-
if (contentType === "image/png") return readPng(bytes);
|
|
2572
|
-
if (contentType === "image/webp") return readWebp(bytes);
|
|
2573
|
-
if (contentType === "image/avif") return readAvif(bytes);
|
|
2574
|
-
return null;
|
|
2575
|
-
}
|
|
2576
|
-
var uint16BE = (bytes, offset) => bytes[offset] << 8 | bytes[offset + 1];
|
|
2577
|
-
var uint32BE = (bytes, offset) => (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0;
|
|
2578
|
-
var uint16LE = (bytes, offset) => bytes[offset] | bytes[offset + 1] << 8;
|
|
2579
|
-
var uint24LE = (bytes, offset) => bytes[offset] | bytes[offset + 1] << 8 | bytes[offset + 2] << 16;
|
|
2580
|
-
var isStartOfFrame = (marker) => marker >= 192 && marker <= 207 && marker !== 196 && marker !== 200 && marker !== 204;
|
|
2581
|
-
function readJpeg(bytes) {
|
|
2582
|
-
if (bytes[0] !== 255 || bytes[1] !== 216) return null;
|
|
2583
|
-
let offset = 2;
|
|
2584
|
-
while (offset + 9 < bytes.length) {
|
|
2585
|
-
if (bytes[offset] !== 255) return null;
|
|
2586
|
-
const marker = bytes[offset + 1];
|
|
2587
|
-
if (isStartOfFrame(marker)) {
|
|
2588
|
-
return { height: uint16BE(bytes, offset + 5), width: uint16BE(bytes, offset + 7) };
|
|
2589
|
-
}
|
|
2590
|
-
offset += 2 + uint16BE(bytes, offset + 2);
|
|
2591
|
-
}
|
|
2592
|
-
return null;
|
|
2593
|
-
}
|
|
2594
|
-
function readPng(bytes) {
|
|
2595
|
-
const isPng2 = bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
|
|
2596
|
-
if (!isPng2 || bytes.length < 24) return null;
|
|
2597
|
-
return { width: uint32BE(bytes, 16), height: uint32BE(bytes, 20) };
|
|
2598
|
-
}
|
|
2599
|
-
function readWebp(bytes) {
|
|
2600
|
-
if (bytes.length < 30) return null;
|
|
2601
|
-
const chunk = String.fromCharCode(bytes[12], bytes[13], bytes[14], bytes[15]);
|
|
2602
|
-
if (chunk === "VP8X") return { width: uint24LE(bytes, 24) + 1, height: uint24LE(bytes, 27) + 1 };
|
|
2603
|
-
if (chunk === "VP8 ") return { width: uint16LE(bytes, 26) & 16383, height: uint16LE(bytes, 28) & 16383 };
|
|
2604
|
-
if (chunk === "VP8L") {
|
|
2605
|
-
const bits = bytes[21] | bytes[22] << 8 | bytes[23] << 16 | bytes[24] << 24;
|
|
2606
|
-
return { width: (bits & 16383) + 1, height: (bits >> 14 & 16383) + 1 };
|
|
2607
|
-
}
|
|
2608
|
-
return null;
|
|
2609
|
-
}
|
|
2610
|
-
function readAvif(bytes) {
|
|
2611
|
-
const limit = Math.min(bytes.length - 20, 64 * 1024);
|
|
2612
|
-
for (let offset = 4; offset < limit; offset += 1) {
|
|
2613
|
-
const isIspe = bytes[offset] === 105 && bytes[offset + 1] === 115 && bytes[offset + 2] === 112 && bytes[offset + 3] === 101;
|
|
2614
|
-
if (isIspe) return { width: uint32BE(bytes, offset + 8), height: uint32BE(bytes, offset + 12) };
|
|
2615
|
-
}
|
|
2616
|
-
return null;
|
|
2617
|
-
}
|
|
2618
|
-
var fourCC = (bytes, offset) => String.fromCharCode(bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3]);
|
|
2619
|
-
function isAnimatedImage(bytes, contentType) {
|
|
2620
|
-
if (contentType === "image/gif") return true;
|
|
2621
|
-
if (contentType === "image/webp")
|
|
2622
|
-
return bytes.length > 20 && fourCC(bytes, 12) === "VP8X" && (bytes[20] & 2) !== 0;
|
|
2623
|
-
if (contentType === "image/png") return hasPngAnimationControl(bytes);
|
|
2624
|
-
if (contentType === "image/avif") return hasAvifSequenceBrand(bytes);
|
|
2625
|
-
return false;
|
|
2626
|
-
}
|
|
2627
|
-
function hasPngAnimationControl(bytes) {
|
|
2628
|
-
let offset = 8;
|
|
2629
|
-
while (offset + 8 <= bytes.length) {
|
|
2630
|
-
const chunk = fourCC(bytes, offset + 4);
|
|
2631
|
-
if (chunk === "acTL") return true;
|
|
2632
|
-
if (chunk === "IDAT") return false;
|
|
2633
|
-
offset += 12 + uint32BE(bytes, offset);
|
|
2634
|
-
}
|
|
2635
|
-
return false;
|
|
2636
|
-
}
|
|
2637
|
-
function hasAvifSequenceBrand(bytes) {
|
|
2638
|
-
if (bytes.length < 16 || fourCC(bytes, 4) !== "ftyp") return false;
|
|
2639
|
-
const boxEnd = Math.min(uint32BE(bytes, 0), bytes.length);
|
|
2640
|
-
for (let offset = 8; offset + 4 <= boxEnd; offset += 4) {
|
|
2641
|
-
if (offset === 12) continue;
|
|
2642
|
-
if (fourCC(bytes, offset) === "avis") return true;
|
|
2643
|
-
}
|
|
2644
|
-
return false;
|
|
2645
|
-
}
|
|
2646
|
-
|
|
2647
2682
|
// src/image/storage.ts
|
|
2648
2683
|
import { decodeEncryptionMeta as decodeEncryptionMeta6, decryptChunk as decryptChunk2 } from "@devultur/core";
|
|
2649
2684
|
var HEADER_PROBE_BYTES = 256 * 1024;
|
|
@@ -5278,7 +5313,8 @@ upload.put("/:key{.+}", async (context) => {
|
|
|
5278
5313
|
...allowedUploadMetadata(pending?.uploadMetadata ?? getRequestState(context).config?.uploadMetadata),
|
|
5279
5314
|
// A transcode output (no pending) expires with its video's raw.
|
|
5280
5315
|
...integrityMetadata(digest.sha256, true, pending?.expiresAt ?? await inheritedExpiresAt(context, key)),
|
|
5281
|
-
...lockMetadata(uploadLockOf(context, key, pending))
|
|
5316
|
+
...lockMetadata(uploadLockOf(context, key, pending)),
|
|
5317
|
+
...imageSizeMetadata(contentType, body)
|
|
5282
5318
|
};
|
|
5283
5319
|
if (resourceSecurity) customMetadata.security = JSON.stringify(resourceSecurity);
|
|
5284
5320
|
const write = await prepareUploadWrite(context, pending?.key ?? key, pending, finalStorageKey);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devultur/self-host",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Self-hosted media Worker for Cloudflare: R2 uploads, edge-cached delivery, image transforms, HLS transcode and captions",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"@jsquash/resize": "2.1.1",
|
|
47
47
|
"@jsquash/webp": "1.5.0",
|
|
48
48
|
"hono": "^4.7.0",
|
|
49
|
-
"@devultur/core": "1.
|
|
49
|
+
"@devultur/core": "1.4.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@cf-wasm/photon": "0.4.0",
|