mirror of
https://github.com/gtsteffaniak/filebrowser.git
synced 2026-11-06 17:42:42 -05:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fd07f4ac1d | ||
|
|
da548cb7b3 | ||
|
|
8cbca1ab3a | ||
|
|
e517ad2777 | ||
|
|
74f2951688 | ||
|
|
943a915cf3 | ||
|
|
efe5379f3f | ||
|
|
0d2451e009 | ||
|
|
20af71da61 | ||
|
|
7f88f829ef | ||
|
|
fafb755576 | ||
|
|
3e05196256 | ||
|
|
4db06c9ab1 | ||
|
|
0d30433739 | ||
|
|
b45e3498b8 |
No files matched your search
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user