프론트엔드 파일시스템 모듈 리팩토링

This commit is contained in:
static
2025-12-31 00:43:12 +09:00
parent e4413ddbf6
commit 26323c2d4d
30 changed files with 421 additions and 459 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { browser } from "$app/environment"; 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"; import { requestFileThumbnailDownload } from "$lib/services/file";
interface Props { interface Props {

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Component } from "svelte"; import type { Component } from "svelte";
import type { SvelteHTMLElements } from "svelte/elements"; 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 { SortBy, sortEntries } from "$lib/utils";
import Category from "./Category.svelte"; import Category from "./Category.svelte";
import type { SelectedCategory } from "./service"; import type { SelectedCategory } from "./service";

View File

@@ -3,7 +3,7 @@
import type { SvelteHTMLElements } from "svelte/elements"; import type { SvelteHTMLElements } from "svelte/elements";
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { CategoryLabel } from "$lib/components/molecules"; 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"; import type { SelectedCategory } from "./service";
interface Props { interface Props {

View File

@@ -2,7 +2,7 @@
import type { Component } from "svelte"; import type { Component } from "svelte";
import type { ClassValue, SvelteHTMLElements } from "svelte/elements"; import type { ClassValue, SvelteHTMLElements } from "svelte/elements";
import { Categories, IconEntryButton, type SelectedCategory } from "$lib/components/molecules"; 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"; import IconAddCircle from "~icons/material-symbols/add-circle";

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { CheckBox, RowVirtualizer } from "$lib/components/atoms"; import { CheckBox, RowVirtualizer } from "$lib/components/atoms";
import { SubCategories, type SelectedCategory } from "$lib/components/molecules"; 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 { sortEntries } from "$lib/utils";
import File from "./File.svelte"; import File from "./File.svelte";
import type { SelectedFile } from "./service"; import type { SelectedFile } from "./service";

View File

@@ -2,7 +2,7 @@
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; 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 { requestFileThumbnailDownload } from "$lib/services/file";
import type { SelectedFile } from "./service"; import type { SelectedFile } from "./service";

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { FileThumbnailButton, RowVirtualizer } from "$lib/components/atoms"; 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"; import { formatDate, formatDateSortable, SortBy, sortEntries } from "$lib/utils";
interface Props { interface Props {

View 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);
}
});
};

View 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,
),
);
};

View 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,
),
);
};

View File

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

View 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;

View 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"
>;

View File

@@ -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");
}
};

View File

@@ -5,7 +5,7 @@
import { page } from "$app/state"; import { page } from "$app/state";
import { FullscreenDiv } from "$lib/components/atoms"; import { FullscreenDiv } from "$lib/components/atoms";
import { Categories, IconEntryButton, TopBar } from "$lib/components/molecules"; 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 { captureVideoThumbnail } from "$lib/modules/thumbnail";
import { getFileDownloadState } from "$lib/modules/file"; import { getFileDownloadState } from "$lib/modules/file";
import { masterKeyStore } from "$lib/stores"; import { masterKeyStore } from "$lib/stores";

View File

@@ -2,7 +2,7 @@
import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "$lib/components/atoms"; import { BottomDiv, BottomSheet, Button, FullscreenDiv } from "$lib/components/atoms";
import { SubCategories } from "$lib/components/molecules"; import { SubCategories } from "$lib/components/molecules";
import { CategoryCreateModal } from "$lib/components/organisms"; 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 { masterKeyStore } from "$lib/stores";
import { requestCategoryCreation } from "./service"; import { requestCategoryCreation } from "./service";

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { FullscreenDiv } from "$lib/components/atoms"; import { FullscreenDiv } from "$lib/components/atoms";
import { TopBar } from "$lib/components/molecules"; 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 { getDownloadingFiles, clearDownloadedFiles } from "$lib/modules/file";
import { masterKeyStore } from "$lib/stores"; import { masterKeyStore } from "$lib/stores";
import File from "./File.svelte"; import File from "./File.svelte";

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { FileDownloadState } from "$lib/modules/file"; 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 { formatNetworkSpeed } from "$lib/utils";
import IconCloud from "~icons/material-symbols/cloud"; import IconCloud from "~icons/material-symbols/cloud";

View File

@@ -4,7 +4,7 @@
import { FullscreenDiv } from "$lib/components/atoms"; import { FullscreenDiv } from "$lib/components/atoms";
import { TopBar } from "$lib/components/molecules"; import { TopBar } from "$lib/components/molecules";
import { Gallery } from "$lib/components/organisms"; 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"; import { masterKeyStore } from "$lib/stores";
let { data } = $props(); let { data } = $props();

View File

@@ -4,7 +4,7 @@
import { TopBar } from "$lib/components/molecules"; import { TopBar } from "$lib/components/molecules";
import type { FileCacheIndex } from "$lib/indexedDB"; import type { FileCacheIndex } from "$lib/indexedDB";
import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file"; 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 { masterKeyStore } from "$lib/stores";
import { formatFileSize } from "$lib/utils"; import { formatFileSize } from "$lib/utils";
import File from "./File.svelte"; import File from "./File.svelte";

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { FileCacheIndex } from "$lib/indexedDB"; 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 { formatDate, formatFileSize } from "$lib/utils";
import IconDraft from "~icons/material-symbols/draft"; import IconDraft from "~icons/material-symbols/draft";

View File

@@ -5,7 +5,7 @@
import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms"; import { BottomDiv, Button, FullscreenDiv } from "$lib/components/atoms";
import { IconEntryButton, TopBar } from "$lib/components/molecules"; import { IconEntryButton, TopBar } from "$lib/components/molecules";
import { deleteAllFileThumbnailCaches } from "$lib/modules/file"; 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 { masterKeyStore } from "$lib/stores";
import File from "./File.svelte"; import File from "./File.svelte";
import { import {

View File

@@ -13,7 +13,7 @@
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; 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 { formatDateTime } from "$lib/utils";
import type { GenerationStatus } from "./service.svelte"; import type { GenerationStatus } from "./service.svelte";

View File

@@ -2,7 +2,7 @@ import { limitFunction } from "p-limit";
import { get, writable, type Writable } from "svelte/store"; import { get, writable, type Writable } from "svelte/store";
import { encryptData } from "$lib/modules/crypto"; import { encryptData } from "$lib/modules/crypto";
import { storeFileThumbnailCache } from "$lib/modules/file"; 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 { generateThumbnail as doGenerateThumbnail } from "$lib/modules/thumbnail";
import { requestFileDownload, requestFileThumbnailUpload } from "$lib/services/file"; import { requestFileDownload, requestFileThumbnailUpload } from "$lib/services/file";

View File

@@ -2,7 +2,7 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { TopBar } from "$lib/components/molecules"; import { TopBar } from "$lib/components/molecules";
import { Category, CategoryCreateModal } from "$lib/components/organisms"; 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 { masterKeyStore } from "$lib/stores";
import CategoryDeleteModal from "./CategoryDeleteModal.svelte"; import CategoryDeleteModal from "./CategoryDeleteModal.svelte";
import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte"; import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte";

View File

@@ -4,7 +4,7 @@
import { page } from "$app/state"; import { page } from "$app/state";
import { FloatingButton } from "$lib/components/atoms"; import { FloatingButton } from "$lib/components/atoms";
import { TopBar } from "$lib/components/molecules"; 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 { masterKeyStore, hmacSecretStore } from "$lib/stores";
import DirectoryCreateModal from "./DirectoryCreateModal.svelte"; import DirectoryCreateModal from "./DirectoryCreateModal.svelte";
import DirectoryEntries from "./DirectoryEntries"; import DirectoryEntries from "./DirectoryEntries";

View File

@@ -2,7 +2,7 @@
import { ActionEntryButton, RowVirtualizer } from "$lib/components/atoms"; import { ActionEntryButton, RowVirtualizer } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; import { DirectoryEntryLabel } from "$lib/components/molecules";
import { getUploadingFiles, type LiveFileUploadState } from "$lib/modules/file"; 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 { sortEntries } from "$lib/utils";
import File from "./File.svelte"; import File from "./File.svelte";
import SubDirectory from "./SubDirectory.svelte"; import SubDirectory from "./SubDirectory.svelte";

View File

@@ -2,7 +2,7 @@
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; 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 { requestFileThumbnailDownload } from "$lib/services/file";
import { formatDateTime } from "$lib/utils"; import { formatDateTime } from "$lib/utils";
import type { SelectedEntry } from "../service.svelte"; import type { SelectedEntry } from "../service.svelte";

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { ActionEntryButton } from "$lib/components/atoms"; import { ActionEntryButton } from "$lib/components/atoms";
import { DirectoryEntryLabel } from "$lib/components/molecules"; 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 type { SelectedEntry } from "../service.svelte";
import IconMoreVert from "~icons/material-symbols/more-vert"; import IconMoreVert from "~icons/material-symbols/more-vert";

View File

@@ -2,7 +2,7 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { EntryButton, FileThumbnailButton } from "$lib/components/atoms"; 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 { masterKeyStore } from "$lib/stores";
import { requestFreshMediaFilesRetrieval } from "./service"; import { requestFreshMediaFilesRetrieval } from "./service";