diff --git a/src/lib/modules/filesystem/category.ts b/src/lib/modules/filesystem/category.ts index 041dbb1..16285ff 100644 --- a/src/lib/modules/filesystem/category.ts +++ b/src/lib/modules/filesystem/category.ts @@ -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({ diff --git a/src/lib/modules/filesystem/common.ts b/src/lib/modules/filesystem/common.ts deleted file mode 100644 index dc3cc04..0000000 --- a/src/lib/modules/filesystem/common.ts +++ /dev/null @@ -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; diff --git a/src/lib/modules/filesystem/directory.ts b/src/lib/modules/filesystem/directory.ts index 5626e31..e42b1aa 100644 --- a/src/lib/modules/filesystem/directory.ts +++ b/src/lib/modules/filesystem/directory.ts @@ -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({ @@ -106,8 +105,30 @@ export const getDirectoryInfo = ( id: DirectoryId, masterKey: CryptoKey, options?: { - fetchFromServer?: FilesystemCacheOptions["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, + }); + }), + }); }; diff --git a/src/lib/modules/filesystem/file.ts b/src/lib/modules/filesystem/file.ts index 3298f31..fdd2c8a 100644 --- a/src/lib/modules/filesystem/file.ts +++ b/src/lib/modules/filesystem/file.ts @@ -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({ @@ -175,9 +174,38 @@ const bulkStoreToIndexedDB = (infos: FileInfo[]) => { export const getFileInfo = ( id: number, masterKey: CryptoKey, - options?: { fetchFromServer?: FilesystemCacheOptions["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) => { diff --git a/src/lib/modules/filesystem/index.ts b/src/lib/modules/filesystem/index.ts index 2d99afd..cb9e0f4 100644 --- a/src/lib/modules/filesystem/index.ts +++ b/src/lib/modules/filesystem/index.ts @@ -1,5 +1,4 @@ export * from "./category"; -export * from "./common"; export * from "./directory"; export * from "./file"; export * from "./types"; diff --git a/src/lib/modules/filesystem/FilesystemCache.svelte.ts b/src/lib/modules/filesystem/internal.svelte.ts similarity index 67% rename from src/lib/modules/filesystem/FilesystemCache.svelte.ts rename to src/lib/modules/filesystem/internal.svelte.ts index c84757d..30ba83b 100644 --- a/src/lib/modules/filesystem/FilesystemCache.svelte.ts +++ b/src/lib/modules/filesystem/internal.svelte.ts @@ -1,6 +1,7 @@ import { untrack } from "svelte"; +import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -export interface FilesystemCacheOptions { +interface FilesystemCacheOptions { fetchFromIndexedDB: (key: K) => Promise; fetchFromServer: (key: K, cachedValue: V | undefined, masterKey: CryptoKey) => Promise; bulkFetchFromIndexedDB?: (keys: Set) => Promise>; @@ -18,7 +19,7 @@ export class FilesystemCache { get( key: K, masterKey: CryptoKey, - options?: { fetchFromServer?: FilesystemCacheOptions["fetchFromServer"] }, + options?: { fetchFromServer?: (cachedValue: V | undefined) => Promise }, ) { return untrack(() => { let state = this.map.get(key); @@ -42,8 +43,10 @@ export class FilesystemCache { 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 { }); } } + +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; diff --git a/src/routes/(fullscreen)/search/service.ts b/src/routes/(fullscreen)/search/service.ts index 3f250dc..d9c1e84 100644 --- a/src/routes/(fullscreen)/search/service.ts +++ b/src/routes/(fullscreen)/search/service.ts @@ -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; diff --git a/src/routes/(fullscreen)/settings/migration/service.svelte.ts b/src/routes/(fullscreen)/settings/migration/service.svelte.ts index ae75463..cc4bc7f 100644 --- a/src/routes/(fullscreen)/settings/migration/service.svelte.ts +++ b/src/routes/(fullscreen)/settings/migration/service.svelte.ts @@ -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; }; diff --git a/src/routes/(fullscreen)/settings/thumbnail/service.ts b/src/routes/(fullscreen)/settings/thumbnail/service.ts index e8dbf0c..138ccf1 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/service.ts +++ b/src/routes/(fullscreen)/settings/thumbnail/service.ts @@ -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; }; diff --git a/src/routes/(main)/favorites/service.ts b/src/routes/(main)/favorites/service.ts index 7717d1c..d02b85e 100644 --- a/src/routes/(main)/favorites/service.ts +++ b/src/routes/(main)/favorites/service.ts @@ -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 => { - 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 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, - }; - }, +) => { + const [directories, files] = await HybridPromise.all([ + HybridPromise.all( + favorites.directories.map((directory) => + getDirectoryInfo(directory.id, masterKey, { + serverResponse: { ...directory, isFavorite: true }, }), - ); - if (result?.exists) { - return { - type: "file" as const, - name: result.name, - details: result as SummarizedFileInfo, - }; - } - return null; - }), - ); - - const files = fileResults.filter( - (f): f is { type: "file"; name: string; details: SummarizedFileInfo } => f !== null, - ); - - return [...sortEntries(directories), ...sortEntries(files)]; + ), + ), + 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, + }), + ), + ), + ...sortEntries( + files.map( + (file): FavoriteEntry => ({ + type: "file", + name: file.name!, + details: file as SummarizedFileInfo, + }), + ), + ), + ]; }; export const requestRemoveFavorite = async (type: "file" | "directory", id: number) => {