썸네일이 누락된 파일 조회 및 레거시 파일 조회 네트워크 호출 최적화

This commit is contained in:
static
2026-01-15 20:33:27 +09:00
parent ebcdbd2d83
commit fe83a71a1f
16 changed files with 367 additions and 174 deletions

View File

@@ -58,15 +58,25 @@
filters.includeImages || filters.includeVideos || filters.includeDirectories;
const directories =
!hasTypeFilter || filters.includeDirectories ? serverResult.directories : [];
!hasTypeFilter || filters.includeDirectories
? serverResult.directories.map((directory) => ({
type: "directory" as const,
...directory,
}))
: [];
const files =
!hasTypeFilter || filters.includeImages || filters.includeVideos
? serverResult.files.filter(
({ contentType }) =>
!hasTypeFilter ||
(filters.includeImages && contentType.startsWith("image/")) ||
(filters.includeVideos && contentType.startsWith("video/")),
)
? serverResult.files
.filter(
({ contentType }) =>
!hasTypeFilter ||
(filters.includeImages && contentType.startsWith("image/")) ||
(filters.includeVideos && contentType.startsWith("video/")),
)
.map((file) => ({
type: "file" as const,
...file,
}))
: [];
return sortEntries(

View File

@@ -1,8 +1,13 @@
import type { DataKey, LocalCategoryInfo } from "$lib/modules/filesystem";
import {
decryptDirectoryMetadata,
decryptFileMetadata,
} from "$lib/modules/filesystem/internal.svelte";
getDirectoryInfo,
getFileInfo,
type LocalDirectoryInfo,
type FileInfo,
type LocalCategoryInfo,
} from "$lib/modules/filesystem";
import { HybridPromise } from "$lib/utils";
import { trpc } from "$trpc/client";
export interface SearchFilter {
@@ -10,28 +15,9 @@ export interface SearchFilter {
categories: { info: LocalCategoryInfo; type: "include" | "exclude" }[];
}
interface SearchedDirectory {
type: "directory";
id: number;
parentId: DirectoryId;
dataKey?: DataKey;
name: string;
}
interface SearchedFile {
type: "file";
id: number;
parentId: DirectoryId;
dataKey?: DataKey;
contentType: string;
name: string;
createdAt?: Date;
lastModifiedAt: Date;
}
export interface SearchResult {
directories: SearchedDirectory[];
files: SearchedFile[];
directories: LocalDirectoryInfo[];
files: FileInfo[];
}
export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey) => {
@@ -45,51 +31,47 @@ export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey)
.map(({ info }) => info.id),
});
// TODO: FIXME
const [directories, files] = await Promise.all([
Promise.all(
directoriesRaw.map(async (dir) => {
const metadata = await decryptDirectoryMetadata(
{ dek: dir.dek, dekVersion: dir.dekVersion, name: dir.name, nameIv: dir.nameIv },
masterKey,
);
return {
type: "directory" as const,
id: dir.id,
parentId: dir.parent,
dataKey: metadata.dataKey,
name: metadata.name,
};
}),
const [directories, files] = await HybridPromise.all([
HybridPromise.all(
directoriesRaw.map((directory) =>
HybridPromise.resolve(
getDirectoryInfo(directory.id, masterKey, {
async fetchFromServer(id, cachedInfo, masterKey) {
const metadata = await decryptDirectoryMetadata(directory, masterKey);
return {
subDirectories: [],
files: [],
...cachedInfo,
id: id as number,
exists: true,
parentId: directory.parent,
...metadata,
};
},
}),
),
),
),
Promise.all(
filesRaw.map(async (file) => {
const metadata = await decryptFileMetadata(
{
dek: file.dek,
dekVersion: file.dekVersion,
name: file.name,
nameIv: file.nameIv,
createdAt: file.createdAt,
createdAtIv: file.createdAtIv,
lastModifiedAt: file.lastModifiedAt,
lastModifiedAtIv: file.lastModifiedAtIv,
},
masterKey,
);
return {
type: "file" as const,
id: file.id,
parentId: file.parent,
dataKey: metadata.dataKey,
contentType: file.contentType,
name: metadata.name,
createdAt: metadata.createdAt,
lastModifiedAt: metadata.lastModifiedAt,
};
}),
HybridPromise.all(
filesRaw.map((file) =>
HybridPromise.resolve(
getFileInfo(file.id, masterKey, {
async fetchFromServer(id, cachedInfo, masterKey) {
const metadata = await decryptFileMetadata(file, masterKey);
return {
categories: [],
...cachedInfo,
id: id as number,
exists: true,
parentId: file.parent,
contentType: file.contentType,
...metadata,
};
},
}),
),
),
),
]);
return { directories, files } satisfies SearchResult;
return { directories, files } as SearchResult;
};

View File

@@ -3,11 +3,16 @@
import { goto } from "$app/navigation";
import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms";
import { TopBar } from "$lib/components/molecules";
import { bulkGetFileInfo, type MaybeFileInfo } from "$lib/modules/filesystem";
import type { MaybeFileInfo } from "$lib/modules/filesystem";
import { masterKeyStore } from "$lib/stores";
import { sortEntries } from "$lib/utils";
import File from "./File.svelte";
import { getMigrationState, clearMigrationStates, requestFileMigration } from "./service.svelte";
import {
getMigrationState,
clearMigrationStates,
requestLegacyFiles,
requestFileMigration,
} from "./service.svelte";
let { data } = $props();
@@ -30,9 +35,7 @@
};
onMount(async () => {
fileInfos = sortEntries(
Array.from((await bulkGetFileInfo(data.files, $masterKeyStore?.get(1)?.key!)).values()),
);
fileInfos = sortEntries(await requestLegacyFiles(data.files, $masterKeyStore?.get(1)?.key!));
});
$effect(() => clearMigrationStates);

View File

@@ -1,11 +1,17 @@
import { limitFunction } from "p-limit";
import { SvelteMap } from "svelte/reactivity";
import { CHUNK_SIZE } from "$lib/constants";
import type { FileInfo } from "$lib/modules/filesystem";
import {
decryptFileMetadata,
getFileInfo,
type FileInfo,
type MaybeFileInfo,
} from "$lib/modules/filesystem";
import { uploadBlob } from "$lib/modules/upload";
import { requestFileDownload } from "$lib/services/file";
import { Scheduler } from "$lib/utils";
import { HybridPromise, Scheduler } from "$lib/utils";
import { trpc } from "$trpc/client";
import type { RouterOutputs } from "$trpc/router.server";
export type MigrationStatus =
| "queued"
@@ -24,6 +30,35 @@ export interface MigrationState {
const scheduler = new Scheduler();
const states = new SvelteMap<number, MigrationState>();
export const requestLegacyFiles = async (
filesRaw: RouterOutputs["file"]["listLegacy"],
masterKey: CryptoKey,
) => {
const files = await HybridPromise.all(
filesRaw.map((file) =>
HybridPromise.resolve(
getFileInfo(file.id, masterKey, {
async fetchFromServer(id, cachedInfo, masterKey) {
const metadata = await decryptFileMetadata(file, masterKey);
return {
categories: [],
...cachedInfo,
id: id as number,
exists: true,
isLegacy: file.isLegacy,
parentId: file.parent,
contentType: file.contentType,
...metadata,
};
},
}),
),
),
);
return files as MaybeFileInfo[];
};
const createState = (status: MigrationStatus): MigrationState => {
const state = $state({ status });
return state;

View File

@@ -4,13 +4,14 @@
import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms";
import { IconEntryButton, TopBar } from "$lib/components/molecules";
import { deleteAllFileThumbnailCaches } from "$lib/modules/file";
import { bulkGetFileInfo, type MaybeFileInfo } from "$lib/modules/filesystem";
import type { MaybeFileInfo } from "$lib/modules/filesystem";
import { masterKeyStore } from "$lib/stores";
import { sortEntries } from "$lib/utils";
import File from "./File.svelte";
import {
getThumbnailGenerationStatus,
clearThumbnailGenerationStatuses,
requestMissingThumbnailFiles,
requestThumbnailGeneration,
type GenerationStatus,
} from "./service";
@@ -42,7 +43,7 @@
onMount(async () => {
fileInfos = sortEntries(
Array.from((await bulkGetFileInfo(data.files, $masterKeyStore?.get(1)?.key!)).values()),
await requestMissingThumbnailFiles(data.files, $masterKeyStore?.get(1)?.key!),
);
});

View File

@@ -1,10 +1,16 @@
import { limitFunction } from "p-limit";
import { SvelteMap } from "svelte/reactivity";
import { storeFileThumbnailCache } from "$lib/modules/file";
import type { FileInfo } from "$lib/modules/filesystem";
import {
decryptFileMetadata,
getFileInfo,
type FileInfo,
type MaybeFileInfo,
} from "$lib/modules/filesystem";
import { generateThumbnail } from "$lib/modules/thumbnail";
import { requestFileDownload, requestFileThumbnailUpload } from "$lib/services/file";
import { Scheduler } from "$lib/utils";
import { HybridPromise, Scheduler } from "$lib/utils";
import type { RouterOutputs } from "$trpc/router.server";
export type GenerationStatus =
| "queued"
@@ -29,6 +35,35 @@ export const clearThumbnailGenerationStatuses = () => {
}
};
export const requestMissingThumbnailFiles = async (
filesRaw: RouterOutputs["file"]["listWithoutThumbnail"],
masterKey: CryptoKey,
) => {
const files = await HybridPromise.all(
filesRaw.map((file) =>
HybridPromise.resolve(
getFileInfo(file.id, masterKey, {
async fetchFromServer(id, cachedInfo, masterKey) {
const metadata = await decryptFileMetadata(file, masterKey);
return {
categories: [],
...cachedInfo,
id: id as number,
exists: true,
isLegacy: file.isLegacy,
parentId: file.parent,
contentType: file.contentType,
...metadata,
};
},
}),
),
),
);
return files as MaybeFileInfo[];
};
const requestThumbnailUpload = limitFunction(
async (fileInfo: FileInfo, fileBuffer: ArrayBuffer) => {
statuses.set(fileInfo.id, "generating");