mktcms 0.3.4 → 0.3.6
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/module.json
CHANGED
|
@@ -109,14 +109,15 @@ const mode = ref("preview");
|
|
|
109
109
|
</div>
|
|
110
110
|
|
|
111
111
|
<div
|
|
112
|
-
id="mktcms-page"
|
|
113
112
|
class="flex-1 min-h-0 overflow-auto border border-gray-200 rounded-sm p-4 lg:block lg:h-full"
|
|
114
113
|
:class="mode === 'preview' ? 'block' : 'hidden'"
|
|
115
114
|
>
|
|
116
|
-
<
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
<div id="mktcms-page">
|
|
116
|
+
<MDC
|
|
117
|
+
:value="debouncedMarkdown"
|
|
118
|
+
class="markdown-content"
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
120
121
|
</div>
|
|
121
122
|
</div>
|
|
122
123
|
</div>
|
|
@@ -1,10 +1,115 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { createError, defineEventHandler, getValidatedRouterParams, send } from "h3";
|
|
3
4
|
import { useStorage } from "nitropack/runtime";
|
|
4
5
|
import { toNodeBuffer } from "../../utils/toNodeBuffer.js";
|
|
5
6
|
import { parsedFile } from "../../utils/parsedFile.js";
|
|
6
7
|
import { normalizeContentKey } from "../../utils/contentKey.js";
|
|
7
8
|
import { isCsvPath, isImagePath, isJsonPath, isMarkdownPath, isPdfPath, toFileExtension } from "../../../shared/contentFiles.js";
|
|
9
|
+
const CACHE_CONTROL_CONTENT = "public, max-age=86400, stale-while-revalidate=604800";
|
|
10
|
+
function getMetaRecord(meta) {
|
|
11
|
+
if (!meta || typeof meta !== "object") {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
return meta;
|
|
15
|
+
}
|
|
16
|
+
function getLastModifiedMs(meta) {
|
|
17
|
+
const metaRecord = getMetaRecord(meta);
|
|
18
|
+
const mtimeCandidate = metaRecord?.mtime ?? metaRecord?.updatedAt;
|
|
19
|
+
if (mtimeCandidate instanceof Date) {
|
|
20
|
+
return mtimeCandidate.getTime();
|
|
21
|
+
}
|
|
22
|
+
if (typeof mtimeCandidate === "number" && Number.isFinite(mtimeCandidate)) {
|
|
23
|
+
return mtimeCandidate;
|
|
24
|
+
}
|
|
25
|
+
if (typeof mtimeCandidate === "string") {
|
|
26
|
+
const parsed = Date.parse(mtimeCandidate);
|
|
27
|
+
if (!Number.isNaN(parsed)) {
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
function getSizeBytes(meta, fallbackContent, fallbackRawSize) {
|
|
34
|
+
if (typeof fallbackRawSize === "number" && Number.isFinite(fallbackRawSize)) {
|
|
35
|
+
return fallbackRawSize;
|
|
36
|
+
}
|
|
37
|
+
const metaSize = getMetaRecord(meta)?.size;
|
|
38
|
+
if (typeof metaSize === "number" && Number.isFinite(metaSize)) {
|
|
39
|
+
return metaSize;
|
|
40
|
+
}
|
|
41
|
+
if (typeof fallbackContent === "string") {
|
|
42
|
+
return Buffer.byteLength(fallbackContent);
|
|
43
|
+
}
|
|
44
|
+
if (fallbackContent !== void 0) {
|
|
45
|
+
return Buffer.byteLength(JSON.stringify(fallbackContent));
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
function getContentFingerprint(content) {
|
|
50
|
+
if (content === void 0 || content === null) {
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
if (Buffer.isBuffer(content)) {
|
|
54
|
+
return createHash("sha1").update(content).digest("base64url").slice(0, 12);
|
|
55
|
+
}
|
|
56
|
+
if (typeof content === "string") {
|
|
57
|
+
return createHash("sha1").update(content).digest("base64url").slice(0, 12);
|
|
58
|
+
}
|
|
59
|
+
return createHash("sha1").update(JSON.stringify(content)).digest("base64url").slice(0, 12);
|
|
60
|
+
}
|
|
61
|
+
function buildWeakEtag(lastModifiedMs, sizeBytes, fingerprint) {
|
|
62
|
+
const etagVersion = Number.isFinite(lastModifiedMs) ? Math.floor(lastModifiedMs) : 0;
|
|
63
|
+
const base = `${etagVersion.toString(36)}-${Math.floor(sizeBytes).toString(36)}`;
|
|
64
|
+
return `W/"${fingerprint ? `${base}-${fingerprint}` : base}"`;
|
|
65
|
+
}
|
|
66
|
+
function toHeaderString(value) {
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(value)) {
|
|
71
|
+
return value.join(",");
|
|
72
|
+
}
|
|
73
|
+
return void 0;
|
|
74
|
+
}
|
|
75
|
+
function ifNoneMatchMatches(headerValue, etag) {
|
|
76
|
+
if (!headerValue) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (headerValue.trim() === "*") {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const normalizedEtag = etag.replace(/^W\//, "").trim();
|
|
83
|
+
return headerValue.split(",").map((token) => token.trim().replace(/^W\//, "").trim()).includes(normalizedEtag);
|
|
84
|
+
}
|
|
85
|
+
function ifModifiedSinceMatches(headerValue, lastModifiedMs) {
|
|
86
|
+
if (!headerValue || !Number.isFinite(lastModifiedMs)) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const ifModifiedSinceMs = Date.parse(headerValue);
|
|
90
|
+
if (Number.isNaN(ifModifiedSinceMs)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const lastModifiedSeconds = Math.floor(lastModifiedMs / 1e3);
|
|
94
|
+
const ifModifiedSinceSeconds = Math.floor(ifModifiedSinceMs / 1e3);
|
|
95
|
+
return lastModifiedSeconds <= ifModifiedSinceSeconds;
|
|
96
|
+
}
|
|
97
|
+
function setCachingHeaders(event, etag, lastModifiedMs) {
|
|
98
|
+
event.node.res.setHeader("Cache-Control", CACHE_CONTROL_CONTENT);
|
|
99
|
+
event.node.res.setHeader("ETag", etag);
|
|
100
|
+
if (Number.isFinite(lastModifiedMs)) {
|
|
101
|
+
const lastModifiedSeconds = Math.floor(lastModifiedMs / 1e3);
|
|
102
|
+
event.node.res.setHeader("Last-Modified", new Date(lastModifiedSeconds * 1e3).toUTCString());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function isNotModified(event, etag, lastModifiedMs) {
|
|
106
|
+
const ifNoneMatch = toHeaderString(event.node.req.headers["if-none-match"]);
|
|
107
|
+
if (ifNoneMatch) {
|
|
108
|
+
return ifNoneMatchMatches(ifNoneMatch, etag);
|
|
109
|
+
}
|
|
110
|
+
const ifModifiedSince = toHeaderString(event.node.req.headers["if-modified-since"]);
|
|
111
|
+
return ifModifiedSinceMatches(ifModifiedSince, lastModifiedMs);
|
|
112
|
+
}
|
|
8
113
|
function getFileType(path) {
|
|
9
114
|
const isImage = isImagePath(path);
|
|
10
115
|
const isPdf = isPdfPath(path);
|
|
@@ -19,6 +124,8 @@ function getContentType(path) {
|
|
|
19
124
|
const ext = toFileExtension(path).slice(1);
|
|
20
125
|
if (ext === "jpg") {
|
|
21
126
|
return "image/jpeg";
|
|
127
|
+
} else if (ext === "svg") {
|
|
128
|
+
return "image/svg+xml";
|
|
22
129
|
} else {
|
|
23
130
|
return "image/" + ext;
|
|
24
131
|
}
|
|
@@ -39,24 +146,46 @@ export default defineEventHandler(async (event) => {
|
|
|
39
146
|
const { isImage, isPdf } = getFileType(contentKey);
|
|
40
147
|
event.node.res.setHeader("Content-Type", getContentType(contentKey));
|
|
41
148
|
const storage = useStorage("content");
|
|
149
|
+
const meta = await storage.getMeta(contentKey);
|
|
150
|
+
const lastModifiedMs = getLastModifiedMs(meta);
|
|
42
151
|
if (isImage || isPdf) {
|
|
43
152
|
const raw = await storage.getItemRaw(contentKey);
|
|
44
|
-
if (
|
|
153
|
+
if (raw == null) {
|
|
45
154
|
throw createError({
|
|
46
155
|
statusCode: 404,
|
|
47
156
|
statusMessage: "File not found"
|
|
48
157
|
});
|
|
49
158
|
}
|
|
50
159
|
const body = toNodeBuffer(raw);
|
|
160
|
+
const etag2 = buildWeakEtag(
|
|
161
|
+
lastModifiedMs,
|
|
162
|
+
getSizeBytes(meta, void 0, body.byteLength),
|
|
163
|
+
Number.isFinite(lastModifiedMs) ? void 0 : getContentFingerprint(body)
|
|
164
|
+
);
|
|
165
|
+
setCachingHeaders(event, etag2, lastModifiedMs);
|
|
166
|
+
if (isNotModified(event, etag2, lastModifiedMs)) {
|
|
167
|
+
event.node.res.statusCode = 304;
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
51
170
|
event.node.res.setHeader("Content-Length", String(body.byteLength));
|
|
52
171
|
return send(event, body);
|
|
53
172
|
}
|
|
54
173
|
const file = await storage.getItem(contentKey);
|
|
55
|
-
if (
|
|
174
|
+
if (file == null) {
|
|
56
175
|
throw createError({
|
|
57
176
|
statusCode: 404,
|
|
58
177
|
statusMessage: "File not found"
|
|
59
178
|
});
|
|
60
179
|
}
|
|
180
|
+
const etag = buildWeakEtag(
|
|
181
|
+
lastModifiedMs,
|
|
182
|
+
getSizeBytes(meta, file),
|
|
183
|
+
Number.isFinite(lastModifiedMs) ? void 0 : getContentFingerprint(file)
|
|
184
|
+
);
|
|
185
|
+
setCachingHeaders(event, etag, lastModifiedMs);
|
|
186
|
+
if (isNotModified(event, etag, lastModifiedMs)) {
|
|
187
|
+
event.node.res.statusCode = 304;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
61
190
|
return parsedFile(contentKey, file);
|
|
62
191
|
});
|