Compare commits

...
Author SHA1 Message Date
Graham Steffaniakandcoderabbitai[bot] 28f067f808 Leftover folio docs fix (#3347)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-11-06 13:47:39 -06:00
5 changed files with 161 additions and 56 deletions

No files matched your search

+1
View File
@@ -325,6 +325,7 @@ func handleWithStaticData(w http.ResponseWriter, r *http.Request, d *requestCont
"playwrightTest": settings.Env.IsPlaywright,
"officeProduct": officeProductForSPA(),
"officeExtensions": wopiExtensionsForSPA(),
"textPreviewMimeTypes": iteminfo.TextPreviewMimeTypes(),
}
cspNonce, err := utils.CSPNonce()
+80 -50
View File
@@ -271,57 +271,57 @@ var compressedFile = map[string]bool{
// Raw camera image: extension -> MIME type (single source for IsRawImage + type detection)
var rawImageMimeType = map[string]string{
".cr2": "image/x-canon-cr2",
".cr3": "image/x-canon-cr3",
".nef": "image/x-nikon-nef",
".nrw": "image/x-nikon-nef",
".arw": "image/x-sony-arw",
".srf": "image/x-sony-arw",
".sr2": "image/x-sony-arw",
".orf": "image/x-olympus-orf",
".rw2": "image/x-panasonic-rw2",
".raw": "image/x-panasonic-raw",
".dng": "image/x-adobe-dng",
".raf": "image/x-fuji-raf",
".pef": "image/x-pentax-pef",
".ptx": "image/x-pentax-pef",
".rwl": "image/x-leica-rwl",
".3fr": "image/x-hasselblad-3fr",
".fff": "image/x-hasselblad-fff",
".erf": "image/x-epson-erf",
".mrw": "image/x-minolta-mrw",
".dcr": "image/x-kodak-dcr",
".kdc": "image/x-kodak-dcr",
".dc2": "image/x-kodak-dc2",
".x3f": "image/x-sigma-x3f",
".iiq": "image/x-phaseone-iiq",
".nkc": "image/x-kodak-nkc",
".r3d": "image/x-red-r3d",
".cr2": "image/x-canon-cr2",
".cr3": "image/x-canon-cr3",
".nef": "image/x-nikon-nef",
".nrw": "image/x-nikon-nef",
".arw": "image/x-sony-arw",
".srf": "image/x-sony-arw",
".sr2": "image/x-sony-arw",
".orf": "image/x-olympus-orf",
".rw2": "image/x-panasonic-rw2",
".raw": "image/x-panasonic-raw",
".dng": "image/x-adobe-dng",
".raf": "image/x-fuji-raf",
".pef": "image/x-pentax-pef",
".ptx": "image/x-pentax-pef",
".rwl": "image/x-leica-rwl",
".3fr": "image/x-hasselblad-3fr",
".fff": "image/x-hasselblad-fff",
".erf": "image/x-epson-erf",
".mrw": "image/x-minolta-mrw",
".dcr": "image/x-kodak-dcr",
".kdc": "image/x-kodak-dcr",
".dc2": "image/x-kodak-dc2",
".x3f": "image/x-sigma-x3f",
".iiq": "image/x-phaseone-iiq",
".nkc": "image/x-kodak-nkc",
".r3d": "image/x-red-r3d",
}
// 3D model extension -> MIME type (single source for Is3DModel + type detection)
var model3DMimeType = map[string]string{
".gltf": "model/gltf+json",
".glb": "model/gltf-binary",
".obj": "model/obj",
".fbx": "model/fbx",
".stl": "model/stl",
".ply": "model/ply",
".dae": "model/vnd.collada+xml",
".3mf": "model/3mf",
".3ds": "model/3ds",
".usdz": "model/vnd.usdz+zip",
".usd": "model/vnd.usd+zip",
".usda": "model/vnd.usd+zip",
".usdc": "model/vnd.usd+zip",
".amf": "model/x-amf",
".vrml": "model/vrml",
".wrl": "model/vrml",
".vtk": "model/vtk",
".vtp": "model/vtk",
".pcd": "model/pcd",
".xyz": "model/xyz",
".vox": "model/vox",
".gltf": "model/gltf+json",
".glb": "model/gltf-binary",
".obj": "model/obj",
".fbx": "model/fbx",
".stl": "model/stl",
".ply": "model/ply",
".dae": "model/vnd.collada+xml",
".3mf": "model/3mf",
".3ds": "model/3ds",
".usdz": "model/vnd.usdz+zip",
".usd": "model/vnd.usd+zip",
".usda": "model/vnd.usd+zip",
".usdc": "model/vnd.usd+zip",
".amf": "model/x-amf",
".vrml": "model/vrml",
".wrl": "model/vrml",
".vtk": "model/vtk",
".vtp": "model/vtk",
".pcd": "model/pcd",
".xyz": "model/xyz",
".vox": "model/vox",
}
var audioMetadataTypes = map[string]bool{
@@ -560,10 +560,40 @@ func TextPreviewKind(ext string) string {
return textPreviewKinds[ext]
}
// textPreviewMimeTypes are non-text/* mimetypes that textTypes extensions resolve to
// (see the custom table in backend/internal/adapters/fs/files/mime.go). The frontend
// receives this list through globalVars (backend/internal/web/static.go), so it is
// the single source of truth.
// Do not add mimetypes shared with binaries (e.g. application/x-msdownload for .exe).
var textPreviewMimeTypes = map[string]bool{
"application/json": true,
"application/yaml": true,
"application/x-yaml": true,
"application/sql": true,
"application/x-sql": true,
"application/x-sh": true,
"application/x-php": true,
"application/x-powershell": true,
"application/x-tex": true,
"application/toml": true,
"application/x-ruby": true,
}
// IsTextPreviewable reports whether an item uses the text preview endpoint. The decision
// is the item's mimetype: every text/* type is previewable, and everything else is not.
// is the item's mimetype: every text/* type and the textPreviewMimeTypes allowlist.
func IsTextPreviewable(mimetype string) bool {
return strings.HasPrefix(mimetype, "text/")
return strings.HasPrefix(mimetype, "text/") || textPreviewMimeTypes[mimetype]
}
// TextPreviewMimeTypes returns the non-text/* allowlist in sorted order, for the
// frontend's globalVars. Sorting keeps the served value stable across restarts.
func TextPreviewMimeTypes() []string {
types := make([]string, 0, len(textPreviewMimeTypes))
for mt := range textPreviewMimeTypes {
types = append(types, mt)
}
sort.Strings(types)
return types
}
// TextPreviewRenderKind returns the renderer kind for a text-previewable file: the
@@ -573,4 +603,4 @@ func TextPreviewRenderKind(name string) string {
return kind
}
return "plain"
}
}
@@ -1,6 +1,7 @@
package iteminfo
import (
"sort"
"testing"
"github.com/stretchr/testify/require"
@@ -24,7 +25,11 @@ func TestIsTextPreviewable(t *testing.T) {
for _, mt := range []string{"text/plain", "text/markdown", "text/lyrics", "text/x-go", "text/html"} {
require.True(t, IsTextPreviewable(mt), mt)
}
for _, mt := range []string{"", "image/svg+xml", "application/pdf", "application/json", "application/x-sqlite3", "application/postscript", "application/backup", "application/wasm"} {
for _, mt := range []string{"application/json", "application/yaml", "application/sql", "application/x-sh", "application/x-php", "application/x-powershell", "application/x-tex"} {
require.True(t, IsTextPreviewable(mt), mt)
}
// Mimetypes shared with binaries (.exe, .bat via application/x-msdownload) must stay non-previewable.
for _, mt := range []string{"", "image/svg+xml", "application/pdf", "application/x-sqlite3", "application/postscript", "application/backup", "application/wasm", "application/x-msdownload", "application/x-msdos-program", "application/octet-stream"} {
require.False(t, IsTextPreviewable(mt), mt)
}
}
@@ -34,3 +39,13 @@ func TestTextPreviewRenderKind(t *testing.T) {
require.Equal(t, "plain", TextPreviewRenderKind("50. Holy Water.lrc"))
require.Equal(t, "plain", TextPreviewRenderKind("cert.pem"))
}
func TestTextPreviewMimeTypesIsSortedAndMatchesAllowlist(t *testing.T) {
types := TextPreviewMimeTypes()
require.NotEmpty(t, types)
require.True(t, sort.StringsAreSorted(types), "list must be sorted for a stable globalVars value")
for _, mt := range types {
require.True(t, IsTextPreviewable(mt), mt)
}
require.Contains(t, types, "application/json")
}
+43 -1
View File
@@ -1,4 +1,29 @@
import { afterEach, describe, expect, it, vi } from "vitest";
// The backend serves the text allowlist through globalVars (backend/internal/web/static.go).
const { globalsMock } = vi.hoisted(() => ({
globalsMock: {
textPreviewMimeTypes: [
"application/json",
"application/yaml",
"application/x-yaml",
"application/sql",
"application/x-sql",
"application/x-sh",
"application/x-php",
"application/x-powershell",
"application/x-tex",
"application/toml",
"application/x-ruby",
] as string[],
},
}));
vi.mock("@/utils/constants.js", async (importOriginal) => ({
...(await importOriginal<object>()),
globalVars: globalsMock,
}));
import {
clearTextPreviewCache,
fetchTextPreview,
@@ -40,8 +65,25 @@ describe("isTextPreviewableType", () => {
}
});
it("is true for text-extension mimetypes that are not text/*", () => {
for (const mt of ["application/json", "application/yaml", "application/sql", "application/x-sh", "application/x-php", "application/x-powershell", "application/x-tex"]) {
expect(isTextPreviewableType(mt), mt).toBe(true);
}
});
it("follows the allowlist served in globalVars", () => {
const original = globalsMock.textPreviewMimeTypes;
try {
globalsMock.textPreviewMimeTypes = ["application/x-custom"];
expect(isTextPreviewableType("application/x-custom")).toBe(true);
expect(isTextPreviewableType("application/json")).toBe(false);
} finally {
globalsMock.textPreviewMimeTypes = original;
}
});
it("is false for non-text mimetypes and missing types", () => {
for (const mt of ["application/x-sqlite3", "application/postscript", "application/wasm", "application/backup", "image/svg+xml", "image/png", "directory", "", null, undefined]) {
for (const mt of ["application/x-sqlite3", "application/postscript", "application/wasm", "application/backup", "application/x-msdownload", "application/octet-stream", "image/svg+xml", "image/png", "directory", "", null, undefined]) {
expect(isTextPreviewableType(mt), String(mt)).toBe(false);
}
});
+21 -4
View File
@@ -1,3 +1,4 @@
import { globalVars } from "@/utils/constants.js";
import { state } from "@/store/index.ts";
import type { TextPreviewPayload } from "@/utils/textPreviewRender.ts";
@@ -26,13 +27,29 @@ export function languageFromPath(path: string | null | undefined): string | null
return name.slice(dot + 1).toLowerCase();
}
// Non-text/* mimetypes (JSON, YAML, SQL, shell, ...) served by the backend in globalVars.
// The backend is the single source of truth; the Set is rebuilt only when the served array changes.
let allowlistSource: string[] | undefined;
let allowlistSet = new Set<string>();
function textPreviewMimeTypes(): Set<string> {
const source = globalVars?.textPreviewMimeTypes as string[] | undefined;
if (source !== allowlistSource) {
allowlistSource = source;
allowlistSet = new Set(source ?? []);
}
return allowlistSet;
}
/**
* True when an item's mimetype is text/*. The backend uses the same rule, so an item the
* listing marks as text is the one the text preview endpoint renders. Binary types such as
* sqlite, postscript, wasm and backup are not text/* and never reach the endpoint.
* True when an item's mimetype is text/* or on the backend's text allowlist. The backend uses
* the same rule, so an item the listing marks as text is the one the text preview endpoint
* renders. Binary types such as sqlite, postscript, wasm and backup never reach the endpoint.
*/
export function isTextPreviewableType(mimetype: string | null | undefined): boolean {
return (mimetype ?? "").startsWith("text/");
// split always yields at least one element; the ?? "" satisfies noUncheckedIndexedAccess.
const base = (mimetype ?? "").split(";")[0] ?? "";
return base.startsWith("text/") || textPreviewMimeTypes().has(base);
}
// Text fetches are queued so a folder full of text files does not fire dozens of