파일 썸네일이 캐시되는 OPFS의 경로 변경

This commit is contained in:
static
2025-07-07 18:29:04 +09:00
parent d3de06a7f9
commit 40a87aa81f
10 changed files with 56 additions and 58 deletions

View File

@@ -2,7 +2,6 @@
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; import { DirectoryEntryLabel } from "$lib/components/molecules";
import { getFileThumbnail } from "$lib/modules/file";
import type { FileInfo } from "$lib/modules/filesystem"; import type { FileInfo } from "$lib/modules/filesystem";
import { requestFileThumbnailDownload, type SelectedFile } from "./service"; import { requestFileThumbnailDownload, type SelectedFile } from "./service";
@@ -34,10 +33,7 @@
$effect(() => { $effect(() => {
if ($info?.dataKey) { if ($info?.dataKey) {
getFileThumbnail($info.id) requestFileThumbnailDownload($info.id, $info.dataKey)
.then(
(thumbnailUrl) => thumbnailUrl || requestFileThumbnailDownload($info.id, $info.dataKey!),
)
.then((thumbnailUrl) => { .then((thumbnailUrl) => {
thumbnail = thumbnailUrl ?? undefined; thumbnail = thumbnailUrl ?? undefined;
}) })

View File

@@ -1,12 +1,15 @@
import { LRUCache } from "lru-cache";
import { import {
getFileCacheIndex as getFileCacheIndexFromIndexedDB, getFileCacheIndex as getFileCacheIndexFromIndexedDB,
storeFileCacheIndex, storeFileCacheIndex,
deleteFileCacheIndex, deleteFileCacheIndex,
type FileCacheIndex, type FileCacheIndex,
} from "$lib/indexedDB"; } from "$lib/indexedDB";
import { readFile, writeFile, deleteFile } from "$lib/modules/opfs"; import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs";
import { getThumbnailUrl } from "$lib/modules/thumbnail";
const fileCacheIndex = new Map<number, FileCacheIndex>(); const fileCacheIndex = new Map<number, FileCacheIndex>();
const loadedThumbnails = new LRUCache<number, string>({ max: 100 });
export const prepareFileCache = async () => { export const prepareFileCache = async () => {
for (const cache of await getFileCacheIndexFromIndexedDB()) { for (const cache of await getFileCacheIndexFromIndexedDB()) {
@@ -48,3 +51,32 @@ export const deleteFileCache = async (fileId: number) => {
await deleteFile(`/cache/${fileId}`); await deleteFile(`/cache/${fileId}`);
await deleteFileCacheIndex(fileId); await deleteFileCacheIndex(fileId);
}; };
export const getFileThumbnailCache = async (fileId: number) => {
const thumbnail = loadedThumbnails.get(fileId);
if (thumbnail) {
return thumbnail;
}
const thumbnailBuffer = await readFile(`/thumbnail/file/${fileId}`);
if (!thumbnailBuffer) return null;
const thumbnailUrl = getThumbnailUrl(thumbnailBuffer);
loadedThumbnails.set(fileId, thumbnailUrl);
return thumbnailUrl;
};
export const storeFileThumbnailCache = async (fileId: number, thumbnailBuffer: ArrayBuffer) => {
await writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer);
loadedThumbnails.set(fileId, getThumbnailUrl(thumbnailBuffer));
};
export const deleteFileThumbnailCache = async (fileId: number) => {
loadedThumbnails.delete(fileId);
await deleteFile(`/thumbnail/file/${fileId}`);
};
export const deleteAllFileThumbnailCaches = async () => {
loadedThumbnails.clear();
await deleteDirectory("/thumbnail/file");
};

View File

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

View File

@@ -1,34 +0,0 @@
import { LRUCache } from "lru-cache";
import { readFile, writeFile, deleteFile, deleteDirectory } 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}`);
};
export const deleteAllFileThumbnails = async () => {
loadedThumbnails.clear();
await deleteDirectory("/thumbnails");
};

View File

@@ -1,6 +1,12 @@
import { callGetApi } from "$lib/hooks"; import { callGetApi } from "$lib/hooks";
import { decryptData } from "$lib/modules/crypto"; import { decryptData } from "$lib/modules/crypto";
import { getFileCache, storeFileCache, downloadFile, storeFileThumbnail } from "$lib/modules/file"; import {
getFileCache,
storeFileCache,
getFileThumbnailCache,
storeFileThumbnailCache,
downloadFile,
} from "$lib/modules/file";
import { getThumbnailUrl } from "$lib/modules/thumbnail"; import { getThumbnailUrl } from "$lib/modules/thumbnail";
import type { FileThumbnailInfoResponse } from "$lib/server/schemas"; import type { FileThumbnailInfoResponse } from "$lib/server/schemas";
@@ -18,6 +24,9 @@ export const requestFileDownload = async (
}; };
export const requestFileThumbnailDownload = async (fileId: number, dataKey: CryptoKey) => { export const requestFileThumbnailDownload = async (fileId: number, dataKey: CryptoKey) => {
const cache = await getFileThumbnailCache(fileId);
if (cache) return cache;
let res = await callGetApi(`/api/file/${fileId}/thumbnail`); let res = await callGetApi(`/api/file/${fileId}/thumbnail`);
if (!res.ok) return null; if (!res.ok) return null;
@@ -29,6 +38,6 @@ export const requestFileThumbnailDownload = async (fileId: number, dataKey: Cryp
const thumbnailEncrypted = await res.arrayBuffer(); const thumbnailEncrypted = await res.arrayBuffer();
const thumbnail = await decryptData(thumbnailEncrypted, thumbnailEncryptedIv, dataKey); const thumbnail = await decryptData(thumbnailEncrypted, thumbnailEncryptedIv, dataKey);
storeFileThumbnail(fileId, thumbnail); // Intended storeFileThumbnailCache(fileId, thumbnail); // Intended
return getThumbnailUrl(thumbnail); return getThumbnailUrl(thumbnail);
}; };

View File

@@ -4,7 +4,7 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms"; import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms";
import { IconEntryButton, TopBar } from "$lib/components/molecules"; import { IconEntryButton, TopBar } from "$lib/components/molecules";
import { deleteAllFileThumbnails } from "$lib/modules/file"; import { deleteAllFileThumbnailCaches } from "$lib/modules/file";
import { getFileInfo } from "$lib/modules/filesystem"; import { getFileInfo } from "$lib/modules/filesystem";
import { masterKeyStore } from "$lib/stores"; import { masterKeyStore } from "$lib/stores";
import File from "./File.svelte"; import File from "./File.svelte";
@@ -44,7 +44,7 @@
<FullscreenDiv class="bg-gray-100 !px-0"> <FullscreenDiv class="bg-gray-100 !px-0">
<div class="flex flex-grow flex-col space-y-4"> <div class="flex flex-grow flex-col space-y-4">
<div class="flex-shrink-0 bg-white p-4 !pt-0"> <div class="flex-shrink-0 bg-white p-4 !pt-0">
<IconEntryButton icon={IconDelete} onclick={deleteAllFileThumbnails} class="w-full"> <IconEntryButton icon={IconDelete} onclick={deleteAllFileThumbnailCaches} class="w-full">
저장된 썸네일 모두 삭제하기 저장된 썸네일 모두 삭제하기
</IconEntryButton> </IconEntryButton>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import { callPostApi } from "$lib/hooks"; import { callPostApi } from "$lib/hooks";
import type { MissingThumbnailFileScanResponse } from "$lib/server/schemas/file"; import type { MissingThumbnailFileScanResponse } from "$lib/server/schemas";
import type { PageLoad } from "./$types"; import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch }) => { export const load: PageLoad = async ({ fetch }) => {

View File

@@ -1,7 +1,7 @@
import { limitFunction } from "p-limit"; import { limitFunction } from "p-limit";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { encryptData } from "$lib/modules/crypto"; import { encryptData } from "$lib/modules/crypto";
import { storeFileThumbnail } from "$lib/modules/file"; import { storeFileThumbnailCache } from "$lib/modules/file";
import type { FileInfo } from "$lib/modules/filesystem"; import type { FileInfo } from "$lib/modules/filesystem";
import { generateImageThumbnail, generateVideoThumbnail } from "$lib/modules/thumbnail"; import { generateImageThumbnail, generateVideoThumbnail } from "$lib/modules/thumbnail";
import type { FileThumbnailUploadRequest } from "$lib/server/schemas"; import type { FileThumbnailUploadRequest } from "$lib/server/schemas";
@@ -104,7 +104,7 @@ const requestThumbnailUpload = limitFunction(
workingFiles.delete(fileId); workingFiles.delete(fileId);
persistentStates.files = persistentStates.files.filter(({ id }) => id != fileId); persistentStates.files = persistentStates.files.filter(({ id }) => id != fileId);
storeFileThumbnail(fileId, thumbnail.plaintext); // Intended storeFileThumbnailCache(fileId, thumbnail.plaintext); // Intended
return true; return true;
}, },
{ concurrency: 4 }, { concurrency: 4 },

View File

@@ -2,7 +2,6 @@
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; import { DirectoryEntryLabel } from "$lib/components/molecules";
import { getFileThumbnail } from "$lib/modules/file";
import type { FileInfo } from "$lib/modules/filesystem"; import type { FileInfo } from "$lib/modules/filesystem";
import { formatDateTime } from "$lib/modules/util"; import { formatDateTime } from "$lib/modules/util";
import { requestFileThumbnailDownload } from "./service"; import { requestFileThumbnailDownload } from "./service";
@@ -36,10 +35,7 @@
$effect(() => { $effect(() => {
if ($info?.dataKey) { if ($info?.dataKey) {
getFileThumbnail($info.id) requestFileThumbnailDownload($info.id, $info.dataKey)
.then(
(thumbnailUrl) => thumbnailUrl || requestFileThumbnailDownload($info.id, $info.dataKey!),
)
.then((thumbnailUrl) => { .then((thumbnailUrl) => {
thumbnail = thumbnailUrl ?? undefined; thumbnail = thumbnailUrl ?? undefined;
}) })

View File

@@ -5,8 +5,8 @@ import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$
import { import {
storeFileCache, storeFileCache,
deleteFileCache, deleteFileCache,
storeFileThumbnail, storeFileThumbnailCache,
deleteFileThumbnail, deleteFileThumbnailCache,
uploadFile, uploadFile,
} from "$lib/modules/file"; } from "$lib/modules/file";
import type { import type {
@@ -88,7 +88,7 @@ export const requestFileUpload = async (
storeFileCache(res.fileId, res.fileBuffer); // Intended storeFileCache(res.fileId, res.fileBuffer); // Intended
if (res.thumbnailBuffer) { if (res.thumbnailBuffer) {
storeFileThumbnail(res.fileId, res.thumbnailBuffer); // Intended storeFileThumbnailCache(res.fileId, res.thumbnailBuffer); // Intended
} }
return true; return true;
@@ -121,11 +121,11 @@ export const requestEntryDeletion = async (entry: SelectedEntry) => {
if (entry.type === "directory") { if (entry.type === "directory") {
const { deletedFiles }: DirectoryDeleteResponse = await res.json(); const { deletedFiles }: DirectoryDeleteResponse = await res.json();
await Promise.all( await Promise.all(
deletedFiles.flatMap((fileId) => [deleteFileCache(fileId), deleteFileThumbnail(fileId)]), deletedFiles.flatMap((fileId) => [deleteFileCache(fileId), deleteFileThumbnailCache(fileId)]),
); );
return true; return true;
} else { } else {
await Promise.all([deleteFileCache(entry.id), deleteFileThumbnail(entry.id)]); await Promise.all([deleteFileCache(entry.id), deleteFileThumbnailCache(entry.id)]);
return true; return true;
} }
}; };