feat: enhance file attachment features performance

This commit is contained in:
Bohdan Triapitsyn
2025-12-15 13:15:45 +02:00
parent b31559be67
commit e064bcf355
8 changed files with 629 additions and 158 deletions
+75 -10
View File
@@ -19,6 +19,32 @@ type FileStore = FileState & FileActions;
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024;
const guessMimeTypeFromName = (filename: string): string => {
const name = (filename || "").toLowerCase();
const ext = name.includes(".") ? name.split(".").pop() || "" : "";
switch (ext) {
case "png":
return "image/png";
case "jpg":
case "jpeg":
return "image/jpeg";
case "gif":
return "image/gif";
case "webp":
return "image/webp";
case "svg":
return "image/svg+xml";
case "bmp":
return "image/bmp";
case "ico":
return "image/x-icon";
case "pdf":
return "application/pdf";
default:
return "text/plain";
}
};
const guessMimeType = (file: File): string => {
if (file.type && file.type.trim().length > 0) {
return file.type;
@@ -65,6 +91,31 @@ const guessMimeType = (file: File): string => {
}
};
const base64ByteLength = (base64: string): number => {
const cleaned = base64.replace(/\s+/g, "");
if (!cleaned) {
return 0;
}
const padding = cleaned.endsWith("==") ? 2 : cleaned.endsWith("=") ? 1 : 0;
return Math.floor((cleaned.length * 3) / 4) - padding;
};
const base64EncodeBytes = (bytes: Uint8Array): string => {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let output = "";
for (let i = 0; i < bytes.length; i += 3) {
const a = bytes[i] ?? 0;
const b = bytes[i + 1];
const c = bytes[i + 2];
const triple = (a << 16) | ((b ?? 0) << 8) | (c ?? 0);
output += alphabet[(triple >> 18) & 63];
output += alphabet[(triple >> 12) & 63];
output += typeof b === "number" ? alphabet[(triple >> 6) & 63] : "=";
output += typeof c === "number" ? alphabet[triple & 63] : "=";
}
return output;
};
export const useFileStore = create<FileStore>()(
devtools(
@@ -153,6 +204,8 @@ export const useFileStore = create<FileStore>()(
}
let fileContent = content;
let encoding: "base64" | undefined;
let resolvedMimeType: string | undefined;
if (!fileContent) {
try {
@@ -171,6 +224,8 @@ export const useFileStore = create<FileStore>()(
if (response.data && "content" in response.data) {
fileContent = response.data.content;
encoding = response.data.encoding ?? undefined;
resolvedMimeType = response.data.mimeType ?? undefined;
} else {
fileContent = "";
}
@@ -181,26 +236,36 @@ export const useFileStore = create<FileStore>()(
}
}
const blob = new Blob([fileContent || ""], { type: "text/plain" });
const inferredMime = resolvedMimeType || guessMimeTypeFromName(name);
const safeMimeType = inferredMime && inferredMime.trim().length > 0 ? inferredMime : "application/octet-stream";
if (blob.size > MAX_ATTACHMENT_SIZE) {
const base64 = (() => {
if (encoding === "base64") {
return fileContent || "";
}
const encoder = new TextEncoder();
const data = encoder.encode(fileContent || "");
return base64EncodeBytes(data);
})();
const sizeBytes = encoding === "base64"
? base64ByteLength(base64)
: new TextEncoder().encode(fileContent || "").length;
if (sizeBytes > MAX_ATTACHMENT_SIZE) {
throw new Error(`File "${name}" is too large. Maximum size is 10MB.`);
}
const file = new File([blob], name, { type: "text/plain" });
const encoder = new TextEncoder();
const data = encoder.encode(fileContent || "");
const base64 = btoa(String.fromCharCode(...data));
const dataUrl = `data:text/plain;base64,${base64}`;
const file = new File([], name, { type: safeMimeType });
const dataUrl = `data:${safeMimeType};base64,${base64}`;
const attachedFile: AttachedFile = {
id: `server-file-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
file,
dataUrl,
mimeType: "text/plain",
mimeType: safeMimeType,
filename: name,
size: blob.size,
size: sizeBytes,
source: "server",
serverPath: path,
};