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">
|
<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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
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 { 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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user