mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
프론트엔드 파일시스템 모듈 리팩토링
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
import { requestFileThumbnailDownload } from "$lib/services/file";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from "svelte";
|
||||
import type { SvelteHTMLElements } from "svelte/elements";
|
||||
import type { SubCategoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { SubCategoryInfo } from "$lib/modules/filesystem";
|
||||
import { SortBy, sortEntries } from "$lib/utils";
|
||||
import Category from "./Category.svelte";
|
||||
import type { SelectedCategory } from "./service";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { SvelteHTMLElements } from "svelte/elements";
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { CategoryLabel } from "$lib/components/molecules";
|
||||
import type { SubCategoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { SubCategoryInfo } from "$lib/modules/filesystem";
|
||||
import type { SelectedCategory } from "./service";
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { Component } from "svelte";
|
||||
import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
|
||||
import { Categories, IconEntryButton, type SelectedCategory } from "$lib/components/molecules";
|
||||
import type { CategoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { CategoryInfo } from "$lib/modules/filesystem";
|
||||
|
||||
import IconAddCircle from "~icons/material-symbols/add-circle";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { CheckBox, RowVirtualizer } from "$lib/components/atoms";
|
||||
import { SubCategories, type SelectedCategory } from "$lib/components/molecules";
|
||||
import type { CategoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { CategoryInfo } from "$lib/modules/filesystem";
|
||||
import { sortEntries } from "$lib/utils";
|
||||
import File from "./File.svelte";
|
||||
import type { SelectedFile } from "./service";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import type { CategoryFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { CategoryFileInfo } from "$lib/modules/filesystem";
|
||||
import { requestFileThumbnailDownload } from "$lib/services/file";
|
||||
import type { SelectedFile } from "./service";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { FileThumbnailButton, RowVirtualizer } from "$lib/components/atoms";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
import { formatDate, formatDateSortable, SortBy, sortEntries } from "$lib/utils";
|
||||
|
||||
interface Props {
|
||||
|
||||
104
src/lib/modules/filesystem/category.ts
Normal file
104
src/lib/modules/filesystem/category.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import * as IndexedDB from "$lib/indexedDB";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
||||
import type { CategoryInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<CategoryId, CategoryInfo, Partial<CategoryInfo>>();
|
||||
|
||||
const fetchFromIndexedDB = async (id: CategoryId) => {
|
||||
const [category, subCategories] = await Promise.all([
|
||||
id !== "root" ? IndexedDB.getCategoryInfo(id) : undefined,
|
||||
IndexedDB.getCategoryInfos(id),
|
||||
]);
|
||||
const files = category
|
||||
? await Promise.all(
|
||||
category.files.map(async (file) => {
|
||||
const fileInfo = await IndexedDB.getFileInfo(file.id);
|
||||
return fileInfo
|
||||
? {
|
||||
id: file.id,
|
||||
contentType: fileInfo.contentType,
|
||||
name: fileInfo.name,
|
||||
createdAt: fileInfo.createdAt,
|
||||
lastModifiedAt: fileInfo.lastModifiedAt,
|
||||
isRecursive: file.isRecursive,
|
||||
}
|
||||
: undefined;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subCategories };
|
||||
} else if (category) {
|
||||
return {
|
||||
id,
|
||||
name: category.name,
|
||||
subCategories,
|
||||
files: files!.filter((file) => !!file),
|
||||
isFileRecursive: category.isFileRecursive,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFromServer = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
try {
|
||||
const {
|
||||
metadata,
|
||||
subCategories: subCategoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().category.get.query({ id });
|
||||
const [subCategories, files] = await Promise.all([
|
||||
Promise.all(
|
||||
subCategoriesRaw.map(async (category) => ({
|
||||
id: category.id,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
filesRaw
|
||||
? Promise.all(
|
||||
filesRaw.map(async (file) => ({
|
||||
id: file.id,
|
||||
contentType: file.contentType,
|
||||
isRecursive: file.isRecursive,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
)
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subCategories };
|
||||
} else {
|
||||
return {
|
||||
id,
|
||||
subCategories,
|
||||
files,
|
||||
...(await decryptCategoryMetadata(metadata!, masterKey)),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
cache.delete(id);
|
||||
await IndexedDB.deleteCategoryInfo(id as number);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCategoryInfo = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, async (isInitial, resolve) => {
|
||||
if (isInitial) {
|
||||
const info = await fetchFromIndexedDB(id);
|
||||
if (info) {
|
||||
resolve(info);
|
||||
}
|
||||
}
|
||||
|
||||
const info = await fetchFromServer(id, masterKey);
|
||||
if (info) {
|
||||
resolve(info);
|
||||
}
|
||||
});
|
||||
};
|
||||
74
src/lib/modules/filesystem/directory.ts
Normal file
74
src/lib/modules/filesystem/directory.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import * as IndexedDB from "$lib/indexedDB";
|
||||
import { monotonicResolve } from "$lib/utils";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
import { FilesystemCache, decryptDirectoryMetadata, decryptFileMetadata } from "./internal.svelte";
|
||||
import type { DirectoryInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<DirectoryId, DirectoryInfo>();
|
||||
|
||||
const fetchFromIndexedDB = async (id: DirectoryId) => {
|
||||
const [directory, subDirectories, files] = await Promise.all([
|
||||
id !== "root" ? IndexedDB.getDirectoryInfo(id) : undefined,
|
||||
IndexedDB.getDirectoryInfos(id),
|
||||
IndexedDB.getFileInfos(id),
|
||||
]);
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subDirectories, files };
|
||||
} else if (directory) {
|
||||
return { id, parentId: directory.parentId, name: directory.name, subDirectories, files };
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFromServer = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
try {
|
||||
const {
|
||||
metadata,
|
||||
subDirectories: subDirectoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().directory.get.query({ id });
|
||||
const [subDirectories, files] = await Promise.all([
|
||||
Promise.all(
|
||||
subDirectoriesRaw.map(async (directory) => ({
|
||||
id: directory.id,
|
||||
...(await decryptDirectoryMetadata(directory, masterKey)),
|
||||
})),
|
||||
),
|
||||
Promise.all(
|
||||
filesRaw.map(async (file) => ({
|
||||
id: file.id,
|
||||
contentType: file.contentType,
|
||||
...(await decryptFileMetadata(file, masterKey)),
|
||||
})),
|
||||
),
|
||||
]);
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subDirectories, files };
|
||||
} else {
|
||||
return {
|
||||
id,
|
||||
parentId: metadata!.parent,
|
||||
subDirectories,
|
||||
files,
|
||||
...(await decryptDirectoryMetadata(metadata!, masterKey)),
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
cache.delete(id);
|
||||
await IndexedDB.deleteDirectoryInfo(id as number);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, (isInitial, resolve) =>
|
||||
monotonicResolve(
|
||||
[isInitial && fetchFromIndexedDB(id), fetchFromServer(id, masterKey)],
|
||||
resolve,
|
||||
),
|
||||
);
|
||||
};
|
||||
70
src/lib/modules/filesystem/file.ts
Normal file
70
src/lib/modules/filesystem/file.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as IndexedDB from "$lib/indexedDB";
|
||||
import { monotonicResolve } from "$lib/utils";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
||||
import type { FileInfo } from "./types";
|
||||
|
||||
const cache = new FilesystemCache<number, FileInfo>();
|
||||
|
||||
const fetchFromIndexedDB = async (id: number) => {
|
||||
const file = await IndexedDB.getFileInfo(id);
|
||||
const categories = file
|
||||
? await Promise.all(
|
||||
file.categoryIds.map(async (categoryId) => {
|
||||
const category = await IndexedDB.getCategoryInfo(categoryId);
|
||||
return category ? { id: category.id, name: category.name } : undefined;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (file) {
|
||||
return {
|
||||
id,
|
||||
parentId: file.parentId,
|
||||
contentType: file.contentType,
|
||||
name: file.name,
|
||||
createdAt: file.createdAt,
|
||||
lastModifiedAt: file.lastModifiedAt,
|
||||
categories: categories!.filter((category) => !!category),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFromServer = async (id: number, masterKey: CryptoKey) => {
|
||||
try {
|
||||
const { categories: categoriesRaw, ...metadata } = await trpc().file.get.query({ id });
|
||||
const [categories] = await Promise.all([
|
||||
Promise.all(
|
||||
categoriesRaw.map(async (category) => ({
|
||||
id: category.id,
|
||||
...(await decryptCategoryMetadata(category, masterKey)),
|
||||
})),
|
||||
),
|
||||
]);
|
||||
|
||||
return {
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
contentType: metadata.contentType,
|
||||
contentIv: metadata.contentIv,
|
||||
categories,
|
||||
...(await decryptFileMetadata(metadata, masterKey)),
|
||||
};
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
cache.delete(id);
|
||||
await IndexedDB.deleteFileInfo(id);
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const getFileInfo = async (id: number, masterKey: CryptoKey) => {
|
||||
return await cache.get(id, (isInitial, resolve) =>
|
||||
monotonicResolve(
|
||||
[isInitial && fetchFromIndexedDB(id), fetchFromServer(id, masterKey)],
|
||||
resolve,
|
||||
),
|
||||
);
|
||||
};
|
||||
4
src/lib/modules/filesystem/index.ts
Normal file
4
src/lib/modules/filesystem/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./category";
|
||||
export * from "./directory";
|
||||
export * from "./file";
|
||||
export * from "./types";
|
||||
84
src/lib/modules/filesystem/internal.svelte.ts
Normal file
84
src/lib/modules/filesystem/internal.svelte.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
|
||||
|
||||
export class FilesystemCache<K, V extends RV, RV = V> {
|
||||
private map = new Map<K, V | Promise<V>>();
|
||||
|
||||
get(key: K, loader: (isInitial: boolean, resolve: (value: RV) => void) => void) {
|
||||
const info = this.map.get(key);
|
||||
if (info instanceof Promise) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<V>();
|
||||
if (!info) {
|
||||
this.map.set(key, promise);
|
||||
}
|
||||
|
||||
loader(!info, (loadedInfo) => {
|
||||
let info = this.map.get(key)!;
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(loadedInfo);
|
||||
this.map.set(key, state as V);
|
||||
resolve(state as V);
|
||||
} else {
|
||||
Object.assign(info, loadedInfo);
|
||||
resolve(info);
|
||||
}
|
||||
});
|
||||
|
||||
return info ?? promise;
|
||||
}
|
||||
|
||||
delete(key: K) {
|
||||
this.map.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
62
src/lib/modules/filesystem/types.ts
Normal file
62
src/lib/modules/filesystem/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
type DataKey = { key: CryptoKey; version: Date };
|
||||
|
||||
interface LocalDirectoryInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey?: DataKey;
|
||||
name: string;
|
||||
subDirectories: SubDirectoryInfo[];
|
||||
files: SummarizedFileInfo[];
|
||||
}
|
||||
|
||||
interface RootDirectoryInfo {
|
||||
id: "root";
|
||||
parentId?: undefined;
|
||||
dataKey?: undefined;
|
||||
dataKeyVersion?: undefined;
|
||||
name?: undefined;
|
||||
subDirectories: SubDirectoryInfo[];
|
||||
files: SummarizedFileInfo[];
|
||||
}
|
||||
|
||||
export type DirectoryInfo = LocalDirectoryInfo | RootDirectoryInfo;
|
||||
export type SubDirectoryInfo = Omit<LocalDirectoryInfo, "parentId" | "subDirectories" | "files">;
|
||||
|
||||
export interface FileInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey?: DataKey;
|
||||
contentType: string;
|
||||
contentIv?: string;
|
||||
name: string;
|
||||
createdAt?: Date;
|
||||
lastModifiedAt: Date;
|
||||
categories: { id: number; name: string }[];
|
||||
}
|
||||
|
||||
export type SummarizedFileInfo = Omit<FileInfo, "parentId" | "contentIv" | "categories">;
|
||||
export type CategoryFileInfo = SummarizedFileInfo & { isRecursive: boolean };
|
||||
|
||||
interface LocalCategoryInfo {
|
||||
id: number;
|
||||
dataKey?: DataKey | undefined;
|
||||
name: string;
|
||||
subCategories: SubCategoryInfo[];
|
||||
files: CategoryFileInfo[];
|
||||
isFileRecursive: boolean;
|
||||
}
|
||||
|
||||
interface RootCategoryInfo {
|
||||
id: "root";
|
||||
dataKey?: undefined;
|
||||
name?: undefined;
|
||||
subCategories: SubCategoryInfo[];
|
||||
files?: undefined;
|
||||
isFileRecursive?: undefined;
|
||||
}
|
||||
|
||||
export type CategoryInfo = LocalCategoryInfo | RootCategoryInfo;
|
||||
export type SubCategoryInfo = Omit<
|
||||
LocalCategoryInfo,
|
||||
"subCategories" | "files" | "isFileRecursive"
|
||||
>;
|
||||
@@ -1,436 +0,0 @@
|
||||
import {
|
||||
getDirectoryInfos as getDirectoryInfosFromIndexedDB,
|
||||
getDirectoryInfo as getDirectoryInfoFromIndexedDB,
|
||||
storeDirectoryInfo,
|
||||
deleteDirectoryInfo,
|
||||
getFileInfos as getFileInfosFromIndexedDB,
|
||||
getFileInfo as getFileInfoFromIndexedDB,
|
||||
storeFileInfo,
|
||||
deleteFileInfo,
|
||||
getCategoryInfos as getCategoryInfosFromIndexedDB,
|
||||
getCategoryInfo as getCategoryInfoFromIndexedDB,
|
||||
storeCategoryInfo,
|
||||
updateCategoryInfo as updateCategoryInfoInIndexedDB,
|
||||
deleteCategoryInfo,
|
||||
} from "$lib/indexedDB";
|
||||
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
|
||||
import { monotonicResolve } from "$lib/utils";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
|
||||
type DataKey = { key: CryptoKey; version: Date };
|
||||
|
||||
interface LocalDirectoryInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey?: DataKey;
|
||||
name: string;
|
||||
subDirectories: SubDirectoryInfo[];
|
||||
files: SummarizedFileInfo[];
|
||||
}
|
||||
|
||||
interface RootDirectoryInfo {
|
||||
id: "root";
|
||||
parentId?: undefined;
|
||||
dataKey?: undefined;
|
||||
dataKeyVersion?: undefined;
|
||||
name?: undefined;
|
||||
subDirectories: SubDirectoryInfo[];
|
||||
files: SummarizedFileInfo[];
|
||||
}
|
||||
|
||||
export type DirectoryInfo = LocalDirectoryInfo | RootDirectoryInfo;
|
||||
export type SubDirectoryInfo = Omit<LocalDirectoryInfo, "parentId" | "subDirectories" | "files">;
|
||||
|
||||
export interface FileInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey?: DataKey;
|
||||
contentType: string;
|
||||
contentIv?: string;
|
||||
name: string;
|
||||
createdAt?: Date;
|
||||
lastModifiedAt: Date;
|
||||
categories: { id: number; name: string }[];
|
||||
}
|
||||
|
||||
export type SummarizedFileInfo = Omit<FileInfo, "parentId" | "contentIv" | "categories">;
|
||||
export type CategoryFileInfo = SummarizedFileInfo & { isRecursive: boolean };
|
||||
|
||||
interface LocalCategoryInfo {
|
||||
id: number;
|
||||
dataKey?: DataKey | undefined;
|
||||
name: string;
|
||||
subCategories: SubCategoryInfo[];
|
||||
files: CategoryFileInfo[];
|
||||
isFileRecursive: boolean;
|
||||
}
|
||||
|
||||
interface RootCategoryInfo {
|
||||
id: "root";
|
||||
dataKey?: undefined;
|
||||
name?: undefined;
|
||||
subCategories: SubCategoryInfo[];
|
||||
files?: undefined;
|
||||
isFileRecursive?: undefined;
|
||||
}
|
||||
|
||||
export type CategoryInfo = LocalCategoryInfo | RootCategoryInfo;
|
||||
export type SubCategoryInfo = Omit<
|
||||
LocalCategoryInfo,
|
||||
"subCategories" | "files" | "isFileRecursive"
|
||||
>;
|
||||
|
||||
const directoryInfoCache = new Map<DirectoryId, DirectoryInfo | Promise<DirectoryInfo>>();
|
||||
const fileInfoCache = new Map<number, FileInfo | Promise<FileInfo>>();
|
||||
const categoryInfoCache = new Map<CategoryId, CategoryInfo | Promise<CategoryInfo>>();
|
||||
|
||||
export const getDirectoryInfo = async (id: DirectoryId, masterKey: CryptoKey) => {
|
||||
const info = directoryInfoCache.get(id);
|
||||
if (info instanceof Promise) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<DirectoryInfo>();
|
||||
if (!info) {
|
||||
directoryInfoCache.set(id, promise);
|
||||
}
|
||||
|
||||
monotonicResolve(
|
||||
[!info && fetchDirectoryInfoFromIndexedDB(id), fetchDirectoryInfoFromServer(id, masterKey)],
|
||||
(directoryInfo) => {
|
||||
let info = directoryInfoCache.get(id);
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(directoryInfo);
|
||||
directoryInfoCache.set(id, state);
|
||||
resolve(state);
|
||||
} else {
|
||||
Object.assign(info!, directoryInfo);
|
||||
resolve(info!);
|
||||
}
|
||||
},
|
||||
);
|
||||
return info ?? promise;
|
||||
};
|
||||
|
||||
const fetchDirectoryInfoFromIndexedDB = async (
|
||||
id: DirectoryId,
|
||||
): Promise<DirectoryInfo | undefined> => {
|
||||
const [directory, subDirectories, files] = await Promise.all([
|
||||
id !== "root" ? getDirectoryInfoFromIndexedDB(id) : undefined,
|
||||
getDirectoryInfosFromIndexedDB(id),
|
||||
getFileInfosFromIndexedDB(id),
|
||||
]);
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subDirectories, files };
|
||||
} else if (directory) {
|
||||
return { id, parentId: directory.parentId, name: directory.name, subDirectories, files };
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDirectoryInfoFromServer = async (
|
||||
id: DirectoryId,
|
||||
masterKey: CryptoKey,
|
||||
): Promise<DirectoryInfo | undefined> => {
|
||||
try {
|
||||
const {
|
||||
metadata,
|
||||
subDirectories: subDirectoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().directory.get.query({ id });
|
||||
const [subDirectories, files] = await Promise.all([
|
||||
Promise.all(
|
||||
subDirectoriesRaw.map(async (directory) => {
|
||||
const { dataKey } = await unwrapDataKey(directory.dek, masterKey);
|
||||
const name = await decryptString(directory.name, directory.nameIv, dataKey);
|
||||
return {
|
||||
id: directory.id,
|
||||
dataKey: { key: dataKey, version: directory.dekVersion },
|
||||
name,
|
||||
};
|
||||
}),
|
||||
),
|
||||
Promise.all(
|
||||
filesRaw.map(async (file) => {
|
||||
const { dataKey } = await unwrapDataKey(file.dek, masterKey);
|
||||
const [name, createdAt, lastModifiedAt] = await Promise.all([
|
||||
decryptString(file.name, file.nameIv, dataKey),
|
||||
file.createdAt ? decryptDate(file.createdAt, file.createdAtIv!, dataKey) : undefined,
|
||||
decryptDate(file.lastModifiedAt, file.lastModifiedAtIv, dataKey),
|
||||
]);
|
||||
return {
|
||||
id: file.id,
|
||||
dataKey: { key: dataKey, version: file.dekVersion },
|
||||
contentType: file.contentType,
|
||||
name,
|
||||
createdAt,
|
||||
lastModifiedAt,
|
||||
};
|
||||
}),
|
||||
),
|
||||
]);
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subDirectories, files };
|
||||
} else {
|
||||
const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey);
|
||||
const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey);
|
||||
return {
|
||||
id,
|
||||
parentId: metadata!.parent,
|
||||
dataKey: { key: dataKey, version: metadata!.dekVersion },
|
||||
name,
|
||||
subDirectories,
|
||||
files,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
directoryInfoCache.delete(id);
|
||||
await deleteDirectoryInfo(id as number);
|
||||
return;
|
||||
}
|
||||
throw new Error("Failed to fetch directory information");
|
||||
}
|
||||
};
|
||||
|
||||
const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
|
||||
return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10));
|
||||
};
|
||||
|
||||
export const getFileInfo = async (id: number, masterKey: CryptoKey) => {
|
||||
const info = fileInfoCache.get(id);
|
||||
if (info instanceof Promise) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<FileInfo>();
|
||||
if (!info) {
|
||||
fileInfoCache.set(id, promise);
|
||||
}
|
||||
|
||||
monotonicResolve(
|
||||
[!info && fetchFileInfoFromIndexedDB(id), fetchFileInfoFromServer(id, masterKey)],
|
||||
(fileInfo) => {
|
||||
let info = fileInfoCache.get(id);
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(fileInfo);
|
||||
fileInfoCache.set(id, state);
|
||||
resolve(state);
|
||||
} else {
|
||||
Object.assign(info!, fileInfo);
|
||||
resolve(info!);
|
||||
}
|
||||
},
|
||||
);
|
||||
return info ?? promise;
|
||||
};
|
||||
|
||||
const fetchFileInfoFromIndexedDB = async (id: number): Promise<FileInfo | undefined> => {
|
||||
const file = await getFileInfoFromIndexedDB(id);
|
||||
const categories = await Promise.all(
|
||||
file?.categoryIds.map(async (categoryId) => {
|
||||
const categoryInfo = await getCategoryInfoFromIndexedDB(categoryId);
|
||||
return categoryInfo ? { id: categoryId, name: categoryInfo.name } : undefined;
|
||||
}) ?? [],
|
||||
);
|
||||
|
||||
if (file) {
|
||||
return {
|
||||
id,
|
||||
parentId: file.parentId,
|
||||
contentType: file.contentType,
|
||||
name: file.name,
|
||||
createdAt: file.createdAt,
|
||||
lastModifiedAt: file.lastModifiedAt,
|
||||
categories: categories.filter((category) => !!category),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFileInfoFromServer = async (
|
||||
id: number,
|
||||
masterKey: CryptoKey,
|
||||
): Promise<FileInfo | undefined> => {
|
||||
try {
|
||||
const { categories: categoriesRaw, ...metadata } = await trpc().file.get.query({ id });
|
||||
const categories = await Promise.all(
|
||||
categoriesRaw.map(async (category) => {
|
||||
const { dataKey } = await unwrapDataKey(category.dek, masterKey);
|
||||
const name = await decryptString(category.name, category.nameIv, dataKey);
|
||||
return { id: category.id, name };
|
||||
}),
|
||||
);
|
||||
|
||||
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 {
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
dataKey: { key: dataKey, version: new Date(metadata.dekVersion) },
|
||||
contentType: metadata.contentType,
|
||||
contentIv: metadata.contentIv,
|
||||
name,
|
||||
createdAt,
|
||||
lastModifiedAt,
|
||||
categories,
|
||||
};
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
fileInfoCache.delete(id);
|
||||
await deleteFileInfo(id);
|
||||
return;
|
||||
}
|
||||
throw new Error("Failed to fetch file information");
|
||||
}
|
||||
};
|
||||
|
||||
export const getCategoryInfo = async (id: CategoryId, masterKey: CryptoKey) => {
|
||||
const info = categoryInfoCache.get(id);
|
||||
if (info instanceof Promise) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const { promise, resolve } = Promise.withResolvers<CategoryInfo>();
|
||||
if (!info) {
|
||||
categoryInfoCache.set(id, promise);
|
||||
const categoryInfo = await fetchCategoryInfoFromIndexedDB(id);
|
||||
if (categoryInfo) {
|
||||
const state = $state(categoryInfo);
|
||||
categoryInfoCache.set(id, state);
|
||||
resolve(state);
|
||||
}
|
||||
}
|
||||
|
||||
fetchCategoryInfoFromServer(id, masterKey).then((categoryInfo) => {
|
||||
if (!categoryInfo) return;
|
||||
|
||||
let info = categoryInfoCache.get(id);
|
||||
if (info instanceof Promise) {
|
||||
const state = $state(categoryInfo);
|
||||
categoryInfoCache.set(id, state);
|
||||
resolve(state);
|
||||
} else {
|
||||
Object.assign(info!, categoryInfo);
|
||||
resolve(info!);
|
||||
}
|
||||
});
|
||||
|
||||
return info ?? promise;
|
||||
};
|
||||
|
||||
const fetchCategoryInfoFromIndexedDB = async (
|
||||
id: CategoryId,
|
||||
): Promise<CategoryInfo | undefined> => {
|
||||
const [category, subCategories] = await Promise.all([
|
||||
id !== "root" ? getCategoryInfoFromIndexedDB(id) : undefined,
|
||||
getCategoryInfosFromIndexedDB(id),
|
||||
]);
|
||||
const files = category
|
||||
? await Promise.all(
|
||||
category.files.map(async (file) => {
|
||||
const fileInfo = await getFileInfoFromIndexedDB(file.id);
|
||||
return fileInfo
|
||||
? {
|
||||
id: file.id,
|
||||
contentType: fileInfo.contentType,
|
||||
name: fileInfo.name,
|
||||
createdAt: fileInfo.createdAt,
|
||||
lastModifiedAt: fileInfo.lastModifiedAt,
|
||||
isRecursive: file.isRecursive,
|
||||
}
|
||||
: undefined;
|
||||
}),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subCategories };
|
||||
} else if (category) {
|
||||
return {
|
||||
id,
|
||||
name: category.name,
|
||||
subCategories,
|
||||
files: files!.filter((file) => !!file),
|
||||
isFileRecursive: category.isFileRecursive,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const fetchCategoryInfoFromServer = async (
|
||||
id: CategoryId,
|
||||
masterKey: CryptoKey,
|
||||
): Promise<CategoryInfo | undefined> => {
|
||||
try {
|
||||
const {
|
||||
metadata,
|
||||
subCategories: subCategoriesRaw,
|
||||
files: filesRaw,
|
||||
} = await trpc().category.get.query({ id, recurse: true });
|
||||
const [subCategories, files] = await Promise.all([
|
||||
Promise.all(
|
||||
subCategoriesRaw.map(async (category) => {
|
||||
const { dataKey } = await unwrapDataKey(category.dek, masterKey);
|
||||
const name = await decryptString(category.name, category.nameIv, dataKey);
|
||||
return {
|
||||
id: category.id,
|
||||
dataKey: { key: dataKey, version: category.dekVersion },
|
||||
name,
|
||||
};
|
||||
}),
|
||||
),
|
||||
id !== "root"
|
||||
? Promise.all(
|
||||
filesRaw!.map(async (file) => {
|
||||
const { dataKey } = await unwrapDataKey(file.dek, masterKey);
|
||||
const [name, createdAt, lastModifiedAt] = await Promise.all([
|
||||
decryptString(file.name, file.nameIv, dataKey),
|
||||
file.createdAt
|
||||
? decryptDate(file.createdAt, file.createdAtIv!, dataKey)
|
||||
: undefined,
|
||||
decryptDate(file.lastModifiedAt, file.lastModifiedAtIv, dataKey),
|
||||
]);
|
||||
return {
|
||||
id: file.id,
|
||||
dataKey: { key: dataKey, version: file.dekVersion },
|
||||
contentType: file.contentType,
|
||||
name,
|
||||
createdAt,
|
||||
lastModifiedAt,
|
||||
isRecursive: file.isRecursive,
|
||||
};
|
||||
}),
|
||||
)
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
if (id === "root") {
|
||||
return { id, subCategories };
|
||||
} else {
|
||||
const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey);
|
||||
const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey);
|
||||
return {
|
||||
id,
|
||||
dataKey: { key: dataKey, version: metadata!.dekVersion },
|
||||
name,
|
||||
subCategories,
|
||||
files: files!,
|
||||
isFileRecursive: false,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
categoryInfoCache.delete(id);
|
||||
await deleteCategoryInfo(id as number);
|
||||
return;
|
||||
}
|
||||
throw new Error("Failed to fetch category information");
|
||||
}
|
||||
};
|
||||
@@ -5,7 +5,7 @@
|
||||
import { page } from "$app/state";
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { Categories, IconEntryButton, TopBar } from "$lib/components/molecules";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { captureVideoThumbnail } from "$lib/modules/thumbnail";
|
||||
import { getFileDownloadState } from "$lib/modules/file";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "$lib/components/atoms";
|
||||
import { SubCategories } from "$lib/components/molecules";
|
||||
import { CategoryCreateModal } from "$lib/components/organisms";
|
||||
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import { requestCategoryCreation } from "./service";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { getFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getFileInfo } from "$lib/modules/filesystem";
|
||||
import { getDownloadingFiles, clearDownloadedFiles } from "$lib/modules/file";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import File from "./File.svelte";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { FileDownloadState } from "$lib/modules/file";
|
||||
import type { FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import { formatNetworkSpeed } from "$lib/utils";
|
||||
|
||||
import IconCloud from "~icons/material-symbols/cloud";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { FullscreenDiv } from "$lib/components/atoms";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { Gallery } from "$lib/components/organisms";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import type { FileCacheIndex } from "$lib/indexedDB";
|
||||
import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import { formatFileSize } from "$lib/utils";
|
||||
import File from "./File.svelte";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { FileCacheIndex } from "$lib/indexedDB";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
import { formatDate, formatFileSize } from "$lib/utils";
|
||||
|
||||
import IconDraft from "~icons/material-symbols/draft";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms";
|
||||
import { IconEntryButton, TopBar } from "$lib/components/molecules";
|
||||
import { deleteAllFileThumbnailCaches } from "$lib/modules/file";
|
||||
import { getFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getFileInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import File from "./File.svelte";
|
||||
import {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import type { Writable } from "svelte/store";
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import type { FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import { formatDateTime } from "$lib/utils";
|
||||
import type { GenerationStatus } from "./service.svelte";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { limitFunction } from "p-limit";
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { encryptData } from "$lib/modules/crypto";
|
||||
import { storeFileThumbnailCache } from "$lib/modules/file";
|
||||
import type { FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import { generateThumbnail as doGenerateThumbnail } from "$lib/modules/thumbnail";
|
||||
import { requestFileDownload, requestFileThumbnailUpload } from "$lib/services/file";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { goto } from "$app/navigation";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { Category, CategoryCreateModal } from "$lib/components/organisms";
|
||||
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
|
||||
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { page } from "$app/state";
|
||||
import { FloatingButton } from "$lib/components/atoms";
|
||||
import { TopBar } from "$lib/components/molecules";
|
||||
import { getDirectoryInfo, type DirectoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getDirectoryInfo, type DirectoryInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore, hmacSecretStore } from "$lib/stores";
|
||||
import DirectoryCreateModal from "./DirectoryCreateModal.svelte";
|
||||
import DirectoryEntries from "./DirectoryEntries";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ActionEntryButton, RowVirtualizer } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import { getUploadingFiles, type LiveFileUploadState } from "$lib/modules/file";
|
||||
import type { DirectoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { DirectoryInfo } from "$lib/modules/filesystem";
|
||||
import { sortEntries } from "$lib/utils";
|
||||
import File from "./File.svelte";
|
||||
import SubDirectory from "./SubDirectory.svelte";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { browser } from "$app/environment";
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||
import { requestFileThumbnailDownload } from "$lib/services/file";
|
||||
import { formatDateTime } from "$lib/utils";
|
||||
import type { SelectedEntry } from "../service.svelte";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { ActionEntryButton } from "$lib/components/atoms";
|
||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||
import type { SubDirectoryInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import type { SubDirectoryInfo } from "$lib/modules/filesystem";
|
||||
import type { SelectedEntry } from "../service.svelte";
|
||||
|
||||
import IconMoreVert from "~icons/material-symbols/more-vert";
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMount } from "svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { EntryButton, FileThumbnailButton } from "$lib/components/atoms";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
import { requestFreshMediaFilesRetrieval } from "./service";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user