mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
썸네일 로딩 로직 최적화
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { getFileThumbnail } from "$lib/modules/file";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
import { requestFileThumbnailDownload } from "$lib/services/file";
|
||||
|
||||
interface Props {
|
||||
info: SummarizedFileInfo;
|
||||
@@ -10,25 +9,16 @@
|
||||
|
||||
let { info, onclick }: Props = $props();
|
||||
|
||||
let showThumbnail = $derived(
|
||||
browser && (info.contentType.startsWith("image/") || info.contentType.startsWith("video/")),
|
||||
);
|
||||
let thumbnailPromise = $derived(
|
||||
showThumbnail ? requestFileThumbnailDownload(info.id, info.dataKey?.key) : null,
|
||||
);
|
||||
let thumbnail = $derived(getFileThumbnail(info));
|
||||
</script>
|
||||
|
||||
<button
|
||||
onclick={onclick && (() => setTimeout(() => onclick(info), 100))}
|
||||
class="aspect-square overflow-hidden rounded transition active:scale-95 active:brightness-90"
|
||||
>
|
||||
{#await thumbnailPromise}
|
||||
{#if $thumbnail}
|
||||
<img src={$thumbnail} alt={info.name} class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<div class="h-full w-full bg-gray-100"></div>
|
||||
{:then thumbnail}
|
||||
{#if thumbnail}
|
||||
<img src={thumbnail} alt={info.name} class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<div class="h-full w-full bg-gray-100"></div>
|
||||
{/if}
|
||||
{/await}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import { getFileThumbnail } from "$lib/modules/file";
|
||||
import type { CategoryFileInfo } from "$lib/modules/filesystem";
|
||||
import { requestFileThumbnailDownload } from "$lib/services/file";
|
||||
import type { SelectedFile } from "./service";
|
||||
|
||||
import IconClose from "~icons/material-symbols/close";
|
||||
@@ -16,12 +15,7 @@
|
||||
|
||||
let { info, onclick, onRemoveClick }: Props = $props();
|
||||
|
||||
let showThumbnail = $derived(
|
||||
browser && (info.contentType.startsWith("image/") || info.contentType.startsWith("video/")),
|
||||
);
|
||||
let thumbnailPromise = $derived(
|
||||
showThumbnail ? requestFileThumbnailDownload(info.id, info.dataKey?.key) : null,
|
||||
);
|
||||
let thumbnail = $derived(getFileThumbnail(info));
|
||||
</script>
|
||||
|
||||
<ActionEntryButton
|
||||
@@ -30,9 +24,5 @@
|
||||
actionButtonIcon={onRemoveClick && IconClose}
|
||||
onActionButtonClick={() => onRemoveClick?.(info)}
|
||||
>
|
||||
{#await thumbnailPromise}
|
||||
<DirectoryEntryLabel type="file" name={info.name} />
|
||||
{:then thumbnail}
|
||||
<DirectoryEntryLabel type="file" thumbnail={thumbnail ?? undefined} name={info.name} />
|
||||
{/await}
|
||||
<DirectoryEntryLabel type="file" thumbnail={$thumbnail} name={info.name} />
|
||||
</ActionEntryButton>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { LRUCache } from "lru-cache";
|
||||
import {
|
||||
getFileCacheIndex as getFileCacheIndexFromIndexedDB,
|
||||
storeFileCacheIndex,
|
||||
deleteFileCacheIndex,
|
||||
type FileCacheIndex,
|
||||
} from "$lib/indexedDB";
|
||||
import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs";
|
||||
import { getThumbnailUrl } from "$lib/modules/thumbnail";
|
||||
import { readFile, writeFile, deleteFile } from "$lib/modules/opfs";
|
||||
|
||||
const fileCacheIndex = new Map<number, FileCacheIndex>();
|
||||
const loadedThumbnails = new LRUCache<number, string>({ max: 100 });
|
||||
|
||||
export const prepareFileCache = async () => {
|
||||
for (const cache of await getFileCacheIndexFromIndexedDB()) {
|
||||
@@ -51,30 +48,3 @@ export const deleteFileCache = async (fileId: number) => {
|
||||
await deleteFile(`/cache/${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");
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./cache";
|
||||
export * from "./download.svelte";
|
||||
export * from "./thumbnail";
|
||||
export * from "./upload.svelte";
|
||||
|
||||
90
src/lib/modules/file/thumbnail.ts
Normal file
90
src/lib/modules/file/thumbnail.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { browser } from "$app/environment";
|
||||
import { decryptData } from "$lib/modules/crypto";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
import { readFile, writeFile, deleteFile, deleteDirectory } from "$lib/modules/opfs";
|
||||
import { getThumbnailUrl } from "$lib/modules/thumbnail";
|
||||
import { isTRPCClientError, trpc } from "$trpc/client";
|
||||
|
||||
const loadedThumbnails = new LRUCache<number, Writable<string>>({ max: 100 });
|
||||
const loadingThumbnails = new Map<number, Writable<string | undefined>>();
|
||||
|
||||
const fetchFromOpfs = async (fileId: number) => {
|
||||
const thumbnailBuffer = await readFile(`/thumbnail/file/${fileId}`);
|
||||
if (thumbnailBuffer) {
|
||||
return getThumbnailUrl(thumbnailBuffer);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFromServer = async (fileId: number, dataKey: CryptoKey) => {
|
||||
try {
|
||||
const [thumbnailEncrypted, { contentIv: thumbnailEncryptedIv }] = await Promise.all([
|
||||
fetch(`/api/file/${fileId}/thumbnail/download`),
|
||||
trpc().file.thumbnail.query({ id: fileId }),
|
||||
]);
|
||||
const thumbnailBuffer = await decryptData(
|
||||
await thumbnailEncrypted.arrayBuffer(),
|
||||
thumbnailEncryptedIv,
|
||||
dataKey,
|
||||
);
|
||||
|
||||
void writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer);
|
||||
return getThumbnailUrl(thumbnailBuffer);
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
return null;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileThumbnail = (file: SummarizedFileInfo) => {
|
||||
if (
|
||||
!browser ||
|
||||
!(file.contentType.startsWith("image/") || file.contentType.startsWith("video/"))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const thumbnail = loadedThumbnails.get(file.id);
|
||||
if (thumbnail) return thumbnail;
|
||||
|
||||
let loadingThumbnail = loadingThumbnails.get(file.id);
|
||||
if (loadingThumbnail) return loadingThumbnail;
|
||||
|
||||
loadingThumbnail = writable(undefined);
|
||||
loadingThumbnails.set(file.id, loadingThumbnail);
|
||||
|
||||
fetchFromOpfs(file.id)
|
||||
.then((thumbnail) => thumbnail ?? (file.dataKey && fetchFromServer(file.id, file.dataKey.key)))
|
||||
.then((thumbnail) => {
|
||||
if (thumbnail) {
|
||||
loadingThumbnail.set(thumbnail);
|
||||
loadedThumbnails.set(file.id, loadingThumbnail as Writable<string>);
|
||||
}
|
||||
loadingThumbnails.delete(file.id);
|
||||
});
|
||||
return loadingThumbnail;
|
||||
};
|
||||
|
||||
export const storeFileThumbnailCache = async (fileId: number, thumbnailBuffer: ArrayBuffer) => {
|
||||
await writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer);
|
||||
|
||||
const oldThumbnail = loadedThumbnails.get(fileId);
|
||||
if (oldThumbnail) {
|
||||
oldThumbnail.set(getThumbnailUrl(thumbnailBuffer));
|
||||
} else {
|
||||
loadedThumbnails.set(fileId, writable(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`);
|
||||
};
|
||||
@@ -1,15 +1,11 @@
|
||||
import { getAllFileInfos } from "$lib/indexedDB/filesystem";
|
||||
import { decryptData } from "$lib/modules/crypto";
|
||||
import {
|
||||
getFileCache,
|
||||
storeFileCache,
|
||||
deleteFileCache,
|
||||
getFileThumbnailCache,
|
||||
storeFileThumbnailCache,
|
||||
deleteFileThumbnailCache,
|
||||
downloadFile,
|
||||
deleteFileThumbnailCache,
|
||||
} from "$lib/modules/file";
|
||||
import { getThumbnailUrl } from "$lib/modules/thumbnail";
|
||||
import type { FileThumbnailUploadRequest } from "$lib/server/schemas";
|
||||
import { trpc } from "$trpc/client";
|
||||
|
||||
@@ -44,29 +40,6 @@ export const requestFileThumbnailUpload = async (
|
||||
return await fetch(`/api/file/${fileId}/thumbnail/upload`, { method: "POST", body: form });
|
||||
};
|
||||
|
||||
export const requestFileThumbnailDownload = async (fileId: number, dataKey?: CryptoKey) => {
|
||||
const cache = await getFileThumbnailCache(fileId);
|
||||
if (cache || !dataKey) return cache;
|
||||
|
||||
let thumbnailInfo;
|
||||
try {
|
||||
thumbnailInfo = await trpc().file.thumbnail.query({ id: fileId });
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return null;
|
||||
}
|
||||
const { contentIv: thumbnailEncryptedIv } = thumbnailInfo;
|
||||
|
||||
const res = await fetch(`/api/file/${fileId}/thumbnail/download`);
|
||||
if (!res.ok) return null;
|
||||
|
||||
const thumbnailEncrypted = await res.arrayBuffer();
|
||||
const thumbnailBuffer = await decryptData(thumbnailEncrypted, thumbnailEncryptedIv, dataKey);
|
||||
|
||||
storeFileThumbnailCache(fileId, thumbnailBuffer); // Intended
|
||||
return getThumbnailUrl(thumbnailBuffer);
|
||||
};
|
||||
|
||||
export const requestDeletedFilesCleanup = async () => {
|
||||
let liveFiles;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user