mktcms 0.3.3 → 0.3.5
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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref } from "vue";
|
|
3
|
-
import { useFetch } from "#app";
|
|
4
3
|
import FileIcon from "./fileIcon.vue";
|
|
5
4
|
import FileButtons from "./fileButtons.vue";
|
|
6
5
|
const props = defineProps({
|
|
@@ -25,12 +24,12 @@ async function toggleExpand() {
|
|
|
25
24
|
isExpanded.value = !isExpanded.value;
|
|
26
25
|
if (isExpanded.value && files.value.length === 0 && dirs.value.length === 0 && !isLoading.value) {
|
|
27
26
|
isLoading.value = true;
|
|
28
|
-
const
|
|
27
|
+
const data = await $fetch("/api/admin/list", {
|
|
29
28
|
query: { path: props.path }
|
|
30
29
|
});
|
|
31
|
-
if (data
|
|
32
|
-
files.value = data.
|
|
33
|
-
dirs.value = data.
|
|
30
|
+
if (data) {
|
|
31
|
+
files.value = data.files;
|
|
32
|
+
dirs.value = data.dirs;
|
|
34
33
|
}
|
|
35
34
|
isLoading.value = false;
|
|
36
35
|
}
|
|
@@ -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
|
});
|