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

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 * as IndexedDB from "$lib/indexedDB";
import { trpc, isTRPCClientError } from "$trpc/client"; import { trpc, isTRPCClientError } from "$trpc/client";
import { decryptFileMetadata, decryptCategoryMetadata } from "./common"; import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
import { FilesystemCache } from "./FilesystemCache.svelte";
import type { CategoryInfo, MaybeCategoryInfo } from "./types"; import type { CategoryInfo, MaybeCategoryInfo } from "./types";
const cache = new FilesystemCache<CategoryId, MaybeCategoryInfo>({ 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 * as IndexedDB from "$lib/indexedDB";
import { trpc, isTRPCClientError } from "$trpc/client"; import { trpc, isTRPCClientError } from "$trpc/client";
import { decryptDirectoryMetadata, decryptFileMetadata } from "./common"; import { FilesystemCache, decryptDirectoryMetadata, decryptFileMetadata } from "./internal.svelte";
import { FilesystemCache, type FilesystemCacheOptions } from "./FilesystemCache.svelte";
import type { DirectoryInfo, MaybeDirectoryInfo } from "./types"; import type { DirectoryInfo, MaybeDirectoryInfo } from "./types";
const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>({ const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>({
@@ -106,8 +105,30 @@ export const getDirectoryInfo = (
id: DirectoryId, id: DirectoryId,
masterKey: CryptoKey, masterKey: CryptoKey,
options?: { 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 * as IndexedDB from "$lib/indexedDB";
import { trpc, isTRPCClientError } from "$trpc/client"; import { trpc, isTRPCClientError } from "$trpc/client";
import { decryptFileMetadata, decryptCategoryMetadata } from "./common"; import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
import { FilesystemCache, type FilesystemCacheOptions } from "./FilesystemCache.svelte";
import type { FileInfo, MaybeFileInfo } from "./types"; import type { FileInfo, MaybeFileInfo } from "./types";
const cache = new FilesystemCache<number, MaybeFileInfo>({ const cache = new FilesystemCache<number, MaybeFileInfo>({
@@ -175,9 +174,38 @@ const bulkStoreToIndexedDB = (infos: FileInfo[]) => {
export const getFileInfo = ( export const getFileInfo = (
id: number, id: number,
masterKey: CryptoKey, 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) => { export const bulkGetFileInfo = (ids: number[], masterKey: CryptoKey) => {

View File

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

View File

@@ -1,6 +1,7 @@
import { untrack } from "svelte"; 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>; fetchFromIndexedDB: (key: K) => Promise<V | undefined>;
fetchFromServer: (key: K, cachedValue: V | undefined, masterKey: CryptoKey) => Promise<V>; fetchFromServer: (key: K, cachedValue: V | undefined, masterKey: CryptoKey) => Promise<V>;
bulkFetchFromIndexedDB?: (keys: Set<K>) => Promise<Map<K, V>>; bulkFetchFromIndexedDB?: (keys: Set<K>) => Promise<Map<K, V>>;
@@ -18,7 +19,7 @@ export class FilesystemCache<K, V extends object> {
get( get(
key: K, key: K,
masterKey: CryptoKey, masterKey: CryptoKey,
options?: { fetchFromServer?: FilesystemCacheOptions<K, V>["fetchFromServer"] }, options?: { fetchFromServer?: (cachedValue: V | undefined) => Promise<V> },
) { ) {
return untrack(() => { return untrack(() => {
let state = this.map.get(key); let state = this.map.get(key);
@@ -42,8 +43,10 @@ export class FilesystemCache<K, V extends object> {
return loadedInfo; return loadedInfo;
}) })
) )
.then((cachedInfo) => .then(
(options?.fetchFromServer ?? this.options.fetchFromServer)(key, cachedInfo, masterKey), (cachedInfo) =>
options?.fetchFromServer?.(cachedInfo) ??
this.options.fetchFromServer(key, cachedInfo, masterKey),
) )
.then((loadedInfo) => { .then((loadedInfo) => {
if (state.value) { 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 { import {
decryptDirectoryMetadata,
decryptFileMetadata,
getDirectoryInfo, getDirectoryInfo,
getFileInfo, getFileInfo,
type LocalDirectoryInfo, type LocalDirectoryInfo,
@@ -30,49 +28,14 @@ export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey)
.filter(({ type }) => type === "exclude") .filter(({ type }) => type === "exclude")
.map(({ info }) => info.id), .map(({ info }) => info.id),
}); });
const [directories, files] = await HybridPromise.all([ const [directories, files] = await HybridPromise.all([
HybridPromise.all( HybridPromise.all(
directoriesRaw.map((directory) => directoriesRaw.map((directory) =>
HybridPromise.resolve( getDirectoryInfo(directory.id, masterKey, { serverResponse: directory }),
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,
};
},
}),
),
), ),
), ),
HybridPromise.all( HybridPromise.all(
filesRaw.map((file) => filesRaw.map((file) => getFileInfo(file.id, masterKey, { serverResponse: 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,
};
},
}),
),
),
), ),
]); ]);
return { directories, files } as SearchResult; return { directories, files } as SearchResult;

View File

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

View File

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

View File

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