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,7 +1,7 @@
1
1
  {
2
2
  "name": "mktcms",
3
3
  "configKey": "mktcms",
4
- "version": "0.3.3",
4
+ "version": "0.3.5",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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 { data } = await useFetch("/api/admin/list", {
27
+ const data = await $fetch("/api/admin/list", {
29
28
  query: { path: props.path }
30
29
  });
31
- if (data.value) {
32
- files.value = data.value.files;
33
- dirs.value = data.value.dirs;
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 (!raw) {
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 (!file) {
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mktcms",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Simple CMS module for Nuxt",
5
5
  "repository": "mktcode/mktcms",
6
6
  "license": "MIT",