썸네일을 메모리와 OPFS에 캐시하도록 개선

This commit is contained in:
static
2025-07-06 05:36:05 +09:00
parent 3a637b14b4
commit 781642fed6
11 changed files with 88 additions and 32 deletions

View File

@@ -10,7 +10,7 @@
name: string;
subtext?: string;
textClass?: ClassValue;
thumbnail?: ArrayBuffer;
thumbnail?: string;
type: "directory" | "file";
}
@@ -22,19 +22,12 @@
thumbnail,
type,
}: Props = $props();
let thumbnailUrl: string | undefined = $state();
$effect(() => {
thumbnailUrl = thumbnail && URL.createObjectURL(new Blob([thumbnail]));
return () => thumbnailUrl && URL.revokeObjectURL(thumbnailUrl);
});
</script>
{#snippet iconSnippet()}
<div class="flex h-10 w-10 items-center justify-center overflow-y-hidden text-xl">
{#if thumbnailUrl}
<img src={thumbnailUrl} alt={name} loading="lazy" />
{#if thumbnail}
<img src={thumbnail} alt={name} loading="lazy" />
{:else if type === "directory"}
<IconFolder />
{:else}

View File

@@ -1,3 +1,4 @@
export * from "./cache";
export * from "./download";
export * from "./thumbnail";
export * from "./upload";

View File

@@ -0,0 +1,29 @@
import { LRUCache } from "lru-cache";
import { readFile, writeFile, deleteFile } from "$lib/modules/opfs";
import { getThumbnailUrl } from "$lib/modules/thumbnail";
const loadedThumbnails = new LRUCache<number, string>({ max: 100 });
export const getFileThumbnail = async (fileId: number) => {
const thumbnail = loadedThumbnails.get(fileId);
if (thumbnail) {
return thumbnail;
}
const thumbnailBuffer = await readFile(`/thumbnails/${fileId}`);
if (!thumbnailBuffer) return null;
const thumbnailUrl = getThumbnailUrl(thumbnailBuffer);
loadedThumbnails.set(fileId, thumbnailUrl);
return thumbnailUrl;
};
export const storeFileThumbnail = async (fileId: number, thumbnailBuffer: ArrayBuffer) => {
await writeFile(`/thumbnails/${fileId}`, thumbnailBuffer);
loadedThumbnails.set(fileId, getThumbnailUrl(thumbnailBuffer));
};
export const deleteFileThumbnail = async (fileId: number) => {
loadedThumbnails.delete(fileId);
await deleteFile(`/thumbnails/${fileId}`);
};

View File

@@ -130,9 +130,8 @@ const encryptFile = limitFunction(
const lastModifiedAtEncrypted = await encryptString(file.lastModified.toString(), dataKey);
const thumbnail = await generateThumbnail(file, fileType);
const thumbnailEncrypted = thumbnail
? await encryptData(await thumbnail.arrayBuffer(), dataKey)
: null;
const thumbnailBuffer = await thumbnail?.arrayBuffer();
const thumbnailEncrypted = thumbnailBuffer ? await encryptData(thumbnailBuffer, dataKey) : null;
status.update((value) => {
value.status = "upload-pending";
@@ -148,7 +147,8 @@ const encryptFile = limitFunction(
nameEncrypted,
createdAtEncrypted,
lastModifiedAtEncrypted,
thumbnailEncrypted,
thumbnail: thumbnail &&
thumbnailEncrypted && { plaintext: thumbnailBuffer, ...thumbnailEncrypted },
};
},
{ concurrency: 4 },
@@ -198,7 +198,9 @@ export const uploadFile = async (
hmacSecret: HmacSecret,
masterKey: MasterKey,
onDuplicate: () => Promise<boolean>,
): Promise<{ fileId: number; fileBuffer: ArrayBuffer } | undefined> => {
): Promise<
{ fileId: number; fileBuffer: ArrayBuffer; thumbnailBuffer?: ArrayBuffer } | undefined
> => {
const status = writable<FileUploadStatus>({
name: file.name,
parentId,
@@ -236,7 +238,7 @@ export const uploadFile = async (
nameEncrypted,
createdAtEncrypted,
lastModifiedAtEncrypted,
thumbnailEncrypted,
thumbnail,
} = await encryptFile(status, file, fileBuffer, masterKey);
const form = new FormData();
@@ -263,20 +265,20 @@ export const uploadFile = async (
form.set("checksum", fileEncryptedHash);
let thumbnailForm = null;
if (thumbnailEncrypted) {
if (thumbnail) {
thumbnailForm = new FormData();
thumbnailForm.set(
"metadata",
JSON.stringify({
dekVersion: dataKeyVersion.toISOString(),
contentIv: thumbnailEncrypted.iv,
contentIv: thumbnail.iv,
} satisfies FileThumbnailUploadRequest),
);
thumbnailForm.set("content", new Blob([thumbnailEncrypted.ciphertext]));
thumbnailForm.set("content", new Blob([thumbnail.ciphertext]));
}
const { fileId } = await requestFileUpload(status, form, thumbnailForm);
return { fileId, fileBuffer };
return { fileId, fileBuffer, thumbnailBuffer: thumbnail?.plaintext };
} catch (e) {
status.update((value) => {
value.status = "error";

View File

@@ -1,3 +1,5 @@
import { encodeToBase64 } from "$lib/modules/crypto";
const scaleSize = (width: number, height: number, targetSize: number) => {
if (width <= targetSize || height <= targetSize) {
return { width, height };
@@ -74,3 +76,7 @@ export const generateVideoThumbnail = (videoUrl: string, time = 0) => {
video.src = videoUrl;
});
};
export const getThumbnailUrl = (thumbnailBuffer: ArrayBuffer) => {
return `data:image/webp;base64,${encodeToBase64(thumbnailBuffer)}`;
};