파일 및 디렉터리 메타데이터 복호화 로직 리팩토링

This commit is contained in:
static
2026-01-18 13:01:44 +09:00
parent 4797ccfd23
commit 63163d6279
10 changed files with 156 additions and 210 deletions

View File

@@ -1,7 +1,6 @@
import * as IndexedDB from "$lib/indexedDB";
import { trpc, isTRPCClientError } from "$trpc/client";
import { decryptFileMetadata, decryptCategoryMetadata } from "./common";
import { FilesystemCache } from "./FilesystemCache.svelte";
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
import type { CategoryInfo, MaybeCategoryInfo } from "./types";
const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo>({

View File

@@ -1,50 +0,0 @@
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
export const decryptDirectoryMetadata = async (
metadata: { dek: string; dekVersion: Date; name: string; nameIv: string },
masterKey: CryptoKey,
) => {
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
const name = await decryptString(metadata.name, metadata.nameIv, dataKey);
return {
dataKey: { key: dataKey, version: metadata.dekVersion },
name,
};
};
const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10));
};
export const decryptFileMetadata = async (
metadata: {
dek: string;
dekVersion: Date;
name: string;
nameIv: string;
createdAt?: string;
createdAtIv?: string;
lastModifiedAt: string;
lastModifiedAtIv: string;
},
masterKey: CryptoKey,
) => {
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
const [name, createdAt, lastModifiedAt] = await Promise.all([
decryptString(metadata.name, metadata.nameIv, dataKey),
metadata.createdAt
? decryptDate(metadata.createdAt, metadata.createdAtIv!, dataKey)
: undefined,
decryptDate(metadata.lastModifiedAt, metadata.lastModifiedAtIv, dataKey),
]);
return {
dataKey: { key: dataKey, version: metadata.dekVersion },
name,
createdAt,
lastModifiedAt,
};
};
export const decryptCategoryMetadata = decryptDirectoryMetadata;

View File

@@ -1,7 +1,6 @@
import * as IndexedDB from "$lib/indexedDB";
import { trpc, isTRPCClientError } from "$trpc/client";
import { decryptDirectoryMetadata, decryptFileMetadata } from "./common";
import { FilesystemCache, type FilesystemCacheOptions } from "./FilesystemCache.svelte";
import { FilesystemCache, decryptDirectoryMetadata, decryptFileMetadata } from "./internal.svelte";
import type { DirectoryInfo, MaybeDirectoryInfo } from "./types";
const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>({
@@ -106,8 +105,30 @@ export const getDirectoryInfo = (
id: DirectoryId,
masterKey: CryptoKey,
options?: {
fetchFromServer?: FilesystemCacheOptions<DirectoryId, MaybeDirectoryInfo>["fetchFromServer"];
serverResponse?: {
parent: DirectoryId;
dek: string;
dekVersion: Date;
name: string;
nameIv: string;
isFavorite: boolean;
};
},
) => {
return cache.get(id, masterKey, options);
return cache.get(id, masterKey, {
fetchFromServer:
options?.serverResponse &&
(async (cachedValue) => {
const metadata = await decryptDirectoryMetadata(options!.serverResponse!, masterKey);
return storeToIndexedDB({
subDirectories: [],
files: [],
...cachedValue,
id: id as number,
parentId: options!.serverResponse!.parent,
isFavorite: options!.serverResponse!.isFavorite,
...metadata,
});
}),
});
};

View File

@@ -1,7 +1,6 @@
import * as IndexedDB from "$lib/indexedDB";
import { trpc, isTRPCClientError } from "$trpc/client";
import { decryptFileMetadata, decryptCategoryMetadata } from "./common";
import { FilesystemCache, type FilesystemCacheOptions } from "./FilesystemCache.svelte";
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
import type { FileInfo, MaybeFileInfo } from "./types";
const cache = new FilesystemCache<number, MaybeFileInfo>({
@@ -175,9 +174,38 @@ const bulkStoreToIndexedDB = (infos: FileInfo[]) => {
export const getFileInfo = (
id: number,
masterKey: CryptoKey,
options?: { fetchFromServer?: FilesystemCacheOptions<number, MaybeFileInfo>["fetchFromServer"] },
options?: {
serverResponse?: {
parent: DirectoryId;
dek: string;
dekVersion: Date;
contentType: string;
name: string;
nameIv: string;
createdAt?: string;
createdAtIv?: string;
lastModifiedAt: string;
lastModifiedAtIv: string;
isFavorite: boolean;
};
},
) => {
return cache.get(id, masterKey, options);
return cache.get(id, masterKey, {
fetchFromServer:
options?.serverResponse &&
(async (cachedValue) => {
const metadata = await decryptFileMetadata(options!.serverResponse!, masterKey);
return storeToIndexedDB({
categories: [],
...cachedValue,
id,
parentId: options!.serverResponse!.parent,
contentType: options!.serverResponse!.contentType,
isFavorite: options!.serverResponse!.isFavorite,
...metadata,
});
}),
});
};
export const bulkGetFileInfo = (ids: number[], masterKey: CryptoKey) => {

View File

@@ -1,5 +1,4 @@
export * from "./category";
export * from "./common";
export * from "./directory";
export * from "./file";
export * from "./types";

View File

@@ -1,6 +1,7 @@
import { untrack } from "svelte";
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
export interface FilesystemCacheOptions<K, V> {
interface FilesystemCacheOptions<K, V> {
fetchFromIndexedDB: (key: K) => Promise<V | undefined>;
fetchFromServer: (key: K, cachedValue: V | undefined, masterKey: CryptoKey) => Promise<V>;
bulkFetchFromIndexedDB?: (keys: Set<K>) => Promise<Map<K, V>>;
@@ -18,7 +19,7 @@ export class FilesystemCache<K, V extends object> {
get(
key: K,
masterKey: CryptoKey,
options?: { fetchFromServer?: FilesystemCacheOptions<K, V>["fetchFromServer"] },
options?: { fetchFromServer?: (cachedValue: V | undefined) => Promise<V> },
) {
return untrack(() => {
let state = this.map.get(key);
@@ -42,8 +43,10 @@ export class FilesystemCache<K, V extends object> {
return loadedInfo;
})
)
.then((cachedInfo) =>
(options?.fetchFromServer ?? this.options.fetchFromServer)(key, cachedInfo, masterKey),
.then(
(cachedInfo) =>
options?.fetchFromServer?.(cachedInfo) ??
this.options.fetchFromServer(key, cachedInfo, masterKey),
)
.then((loadedInfo) => {
if (state.value) {
@@ -126,3 +129,52 @@ export class FilesystemCache<K, V extends object> {
});
}
}
export const decryptDirectoryMetadata = async (
metadata: { dek: string; dekVersion: Date; name: string; nameIv: string },
masterKey: CryptoKey,
) => {
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
const name = await decryptString(metadata.name, metadata.nameIv, dataKey);
return {
dataKey: { key: dataKey, version: metadata.dekVersion },
name,
};
};
const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10));
};
export const decryptFileMetadata = async (
metadata: {
dek: string;
dekVersion: Date;
name: string;
nameIv: string;
createdAt?: string;
createdAtIv?: string;
lastModifiedAt: string;
lastModifiedAtIv: string;
},
masterKey: CryptoKey,
) => {
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
const [name, createdAt, lastModifiedAt] = await Promise.all([
decryptString(metadata.name, metadata.nameIv, dataKey),
metadata.createdAt
? decryptDate(metadata.createdAt, metadata.createdAtIv!, dataKey)
: undefined,
decryptDate(metadata.lastModifiedAt, metadata.lastModifiedAtIv, dataKey),
]);
return {
dataKey: { key: dataKey, version: metadata.dekVersion },
name,
createdAt,
lastModifiedAt,
};
};
export const decryptCategoryMetadata = decryptDirectoryMetadata;

View File

@@ -1,6 +1,4 @@
import {
decryptDirectoryMetadata,
decryptFileMetadata,
getDirectoryInfo,
getFileInfo,
type LocalDirectoryInfo,
@@ -30,49 +28,14 @@ export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey)
.filter(({ type }) => type === "exclude")
.map(({ info }) => info.id),
});
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,
isFavorite: !!directory.isFavorite,
};
},
}),
),
getDirectoryInfo(directory.id, masterKey, { serverResponse: directory }),
),
),
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,
isFavorite: !!file.isFavorite,
...metadata,
};
},
}),
),
),
filesRaw.map((file) => getFileInfo(file.id, masterKey, { serverResponse: file })),
),
]);
return { directories, files } as SearchResult;

View File

@@ -1,12 +1,7 @@
import { limitFunction } from "p-limit";
import { SvelteMap } from "svelte/reactivity";
import { CHUNK_SIZE } from "$lib/constants";
import {
decryptFileMetadata,
getFileInfo,
type FileInfo,
type MaybeFileInfo,
} from "$lib/modules/filesystem";
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
import { uploadBlob } from "$lib/modules/upload";
import { requestFileDownload } from "$lib/services/file";
import { HybridPromise, Scheduler } from "$lib/utils";
@@ -35,26 +30,7 @@ export const requestLegacyFiles = async (
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,
isFavorite: file.isFavorite,
...metadata,
};
},
}),
),
),
filesRaw.map((file) => getFileInfo(file.id, masterKey, { serverResponse: file })),
);
return files;
};

View File

@@ -1,12 +1,7 @@
import { limitFunction } from "p-limit";
import { SvelteMap } from "svelte/reactivity";
import { storeFileThumbnailCache } from "$lib/modules/file";
import {
decryptFileMetadata,
getFileInfo,
type FileInfo,
type MaybeFileInfo,
} from "$lib/modules/filesystem";
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
import { generateThumbnail } from "$lib/modules/thumbnail";
import { requestFileDownload, requestFileThumbnailUpload } from "$lib/services/file";
import { HybridPromise, Scheduler } from "$lib/utils";
@@ -40,26 +35,7 @@ export const requestMissingThumbnailFiles = async (
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,
isFavorite: file.isFavorite,
...metadata,
};
},
}),
),
),
filesRaw.map((file) => getFileInfo(file.id, masterKey, { serverResponse: file })),
);
return files;
};

View File

@@ -1,9 +1,9 @@
import {
decryptDirectoryMetadata,
decryptFileMetadata,
getDirectoryInfo,
getFileInfo,
type SummarizedFileInfo,
type SubDirectoryInfo,
type LocalDirectoryInfo,
} from "$lib/modules/filesystem";
import { HybridPromise, sortEntries } from "$lib/utils";
import { trpc } from "$trpc/client";
@@ -16,59 +16,41 @@ export type FavoriteEntry =
export const requestFavoriteEntries = async (
favorites: RouterOutputs["favorites"]["get"],
masterKey: CryptoKey,
): Promise<FavoriteEntry[]> => {
const directories: FavoriteEntry[] = await Promise.all(
favorites.directories.map(async (dir) => {
const metadata = await decryptDirectoryMetadata(dir, masterKey);
return {
type: "directory" as const,
name: metadata.name,
details: {
id: dir.id,
parentId: dir.parent,
isFavorite: true,
dataKey: metadata.dataKey,
name: metadata.name,
} as SubDirectoryInfo,
};
) => {
const [directories, files] = await HybridPromise.all([
HybridPromise.all(
favorites.directories.map((directory) =>
getDirectoryInfo(directory.id, masterKey, {
serverResponse: { ...directory, isFavorite: true },
}),
);
const fileResults = await Promise.all(
favorites.files.map(async (file) => {
const result = await HybridPromise.resolve(
getFileInfo(file.id, masterKey, {
async fetchFromServer(id, cachedInfo) {
const metadata = await decryptFileMetadata(file, masterKey);
return {
categories: [],
...cachedInfo,
id: id as number,
exists: true,
parentId: file.parent,
contentType: file.contentType,
isFavorite: true,
...metadata,
};
},
),
),
HybridPromise.all(
favorites.files.map((file) =>
getFileInfo(file.id, masterKey, { serverResponse: { ...file, isFavorite: true } }),
),
),
]);
return [
...sortEntries(
directories.map(
(directory): FavoriteEntry => ({
type: "directory",
name: directory.name!,
details: directory as LocalDirectoryInfo,
}),
);
if (result?.exists) {
return {
type: "file" as const,
name: result.name,
details: result as SummarizedFileInfo,
};
}
return null;
),
),
...sortEntries(
files.map(
(file): FavoriteEntry => ({
type: "file",
name: file.name!,
details: file as SummarizedFileInfo,
}),
);
const files = fileResults.filter(
(f): f is { type: "file"; name: string; details: SummarizedFileInfo } => f !== null,
);
return [...sortEntries(directories), ...sortEntries(files)];
),
),
];
};
export const requestRemoveFavorite = async (type: "file" | "directory", id: number) => {