mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
파일 페이지에서의 네트워크 호출 최적화
This commit is contained in:
@@ -1,42 +1,34 @@
|
||||
<script lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import { browser } from "$app/environment";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { requestFileThumbnailDownload } from "$lib/services/file";
|
||||
|
||||
interface Props {
|
||||
info: Writable<FileInfo | null>;
|
||||
onclick?: (file: FileInfo) => void;
|
||||
info: SummarizedFileInfo;
|
||||
onclick?: (file: SummarizedFileInfo) => void;
|
||||
}
|
||||
|
||||
let { info, onclick }: Props = $props();
|
||||
|
||||
let thumbnail: string | undefined = $state();
|
||||
|
||||
$effect(() => {
|
||||
if ($info) {
|
||||
requestFileThumbnailDownload($info.id, $info.dataKey)
|
||||
.then((thumbnailUrl) => {
|
||||
thumbnail = thumbnailUrl ?? undefined;
|
||||
})
|
||||
.catch(() => {
|
||||
// TODO: Error Handling
|
||||
thumbnail = undefined;
|
||||
});
|
||||
} else {
|
||||
thumbnail = undefined;
|
||||
}
|
||||
});
|
||||
let showThumbnail = $derived(
|
||||
browser && (info.contentType.startsWith("image/") || info.contentType.startsWith("video/")),
|
||||
);
|
||||
let thumbnailPromise = $derived(
|
||||
showThumbnail ? requestFileThumbnailDownload(info.id, info.dataKey?.key) : null,
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if $info}
|
||||
<button
|
||||
onclick={() => onclick?.($info)}
|
||||
class="aspect-square overflow-hidden rounded transition active:scale-95 active:brightness-90"
|
||||
>
|
||||
<button
|
||||
onclick={onclick && (() => setTimeout(() => onclick(info), 100))}
|
||||
class="aspect-square overflow-hidden rounded transition active:scale-95 active:brightness-90"
|
||||
>
|
||||
{#await thumbnailPromise}
|
||||
<div class="h-full w-full bg-gray-100"></div>
|
||||
{:then thumbnail}
|
||||
{#if thumbnail}
|
||||
<img src={thumbnail} alt={$info.name} class="h-full w-full object-cover" />
|
||||
<img src={thumbnail} alt={info.name} class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<div class="h-full w-full bg-gray-100"></div>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/await}
|
||||
</button>
|
||||
|
||||
@@ -1,98 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { get, type Writable } from "svelte/store";
|
||||
import { FileThumbnailButton, RowVirtualizer } from "$lib/components/atoms";
|
||||
import type { FileInfo } from "$lib/modules/filesystem";
|
||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem2.svelte";
|
||||
import { formatDate, formatDateSortable, SortBy, sortEntries } from "$lib/utils";
|
||||
|
||||
interface Props {
|
||||
files: Writable<FileInfo | null>[];
|
||||
onFileClick?: (file: FileInfo) => void;
|
||||
files: SummarizedFileInfo[];
|
||||
onFileClick?: (file: SummarizedFileInfo) => void;
|
||||
}
|
||||
|
||||
let { files, onFileClick }: Props = $props();
|
||||
|
||||
type FileEntry =
|
||||
| { date?: undefined; contentType?: undefined; info: Writable<FileInfo | null> }
|
||||
| { date: Date; contentType: string; info: Writable<FileInfo | null> };
|
||||
type Row =
|
||||
| { type: "header"; label: string }
|
||||
| { type: "items"; items: FileEntry[]; isLast: boolean };
|
||||
| { type: "items"; files: SummarizedFileInfo[]; isLast: boolean };
|
||||
|
||||
let filesWithDate: FileEntry[] = $state([]);
|
||||
let rows: Row[] = $state([]);
|
||||
let rows = $derived.by(() => {
|
||||
const groups = Map.groupBy(
|
||||
files.filter(
|
||||
(file) => file.contentType.startsWith("image/") || file.contentType.startsWith("video/"),
|
||||
),
|
||||
(file) => formatDateSortable(file.createdAt ?? file.lastModifiedAt),
|
||||
);
|
||||
return Array.from(groups.entries())
|
||||
.sort(([dateA], [dateB]) => dateB.localeCompare(dateA))
|
||||
.flatMap(([_, entries]) => {
|
||||
const sortedEntries = [...entries];
|
||||
sortEntries(sortedEntries, SortBy.DATE_DESC);
|
||||
|
||||
$effect(() => {
|
||||
filesWithDate = files.map((file) => {
|
||||
const info = get(file);
|
||||
if (info) {
|
||||
return {
|
||||
date: info.createdAt ?? info.lastModifiedAt,
|
||||
contentType: info.contentType,
|
||||
info: file,
|
||||
};
|
||||
} else {
|
||||
return { info: file };
|
||||
}
|
||||
});
|
||||
|
||||
const buildRows = () => {
|
||||
const map = new Map<string, FileEntry[]>();
|
||||
|
||||
for (const file of filesWithDate) {
|
||||
if (
|
||||
!file.date ||
|
||||
!(file.contentType.startsWith("image/") || file.contentType.startsWith("video/"))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const date = formatDateSortable(file.date);
|
||||
const entries = map.get(date) ?? [];
|
||||
entries.push(file);
|
||||
map.set(date, entries);
|
||||
}
|
||||
|
||||
const newRows: Row[] = [];
|
||||
const sortedDates = Array.from(map.keys()).sort((a, b) => b.localeCompare(a));
|
||||
for (const date of sortedDates) {
|
||||
const entries = map.get(date)!;
|
||||
sortEntries(entries, SortBy.DATE_DESC);
|
||||
|
||||
newRows.push({
|
||||
type: "header",
|
||||
label: formatDate(entries[0]!.date!),
|
||||
});
|
||||
|
||||
for (let i = 0; i < entries.length; i += 4) {
|
||||
newRows.push({
|
||||
type: "items",
|
||||
items: entries.slice(i, i + 4),
|
||||
isLast: i + 4 >= entries.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
rows = newRows;
|
||||
};
|
||||
return untrack(() => {
|
||||
buildRows();
|
||||
|
||||
const unsubscribes = filesWithDate.map((file) =>
|
||||
file.info.subscribe((value) => {
|
||||
const newDate = value?.createdAt ?? value?.lastModifiedAt;
|
||||
const newContentType = value?.contentType;
|
||||
if (file.date?.getTime() === newDate?.getTime() && file.contentType === newContentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
file.date = newDate;
|
||||
file.contentType = newContentType;
|
||||
buildRows();
|
||||
}),
|
||||
);
|
||||
return () => unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
});
|
||||
return [
|
||||
{
|
||||
type: "header",
|
||||
label: formatDate(sortedEntries[0]!.createdAt ?? sortedEntries[0]!.lastModifiedAt),
|
||||
},
|
||||
...Array.from({ length: Math.ceil(sortedEntries.length / 4) }, (_, i) => {
|
||||
const start = i * 4;
|
||||
const end = start + 4;
|
||||
return {
|
||||
type: "items" as const,
|
||||
files: sortedEntries.slice(start, end),
|
||||
isLast: end >= sortedEntries.length,
|
||||
};
|
||||
}),
|
||||
] satisfies Row[];
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -101,8 +51,8 @@
|
||||
itemHeight={(index) =>
|
||||
rows[index]!.type === "header"
|
||||
? 28
|
||||
: Math.ceil(rows[index]!.items.length / 4) * 181 +
|
||||
(Math.ceil(rows[index]!.items.length / 4) - 1) * 4 +
|
||||
: Math.ceil(rows[index]!.files.length / 4) * 181 +
|
||||
(Math.ceil(rows[index]!.files.length / 4) - 1) * 4 +
|
||||
16}
|
||||
class="flex flex-grow flex-col"
|
||||
>
|
||||
@@ -112,8 +62,8 @@
|
||||
<p class="pb-2 text-sm font-medium">{row.label}</p>
|
||||
{:else}
|
||||
<div class={["grid grid-cols-4 gap-x-1", row.isLast ? "pb-4" : "pb-1"]}>
|
||||
{#each row.items as { info }}
|
||||
<FileThumbnailButton {info} onclick={onFileClick} />
|
||||
{#each row.files as file}
|
||||
<FileThumbnailButton info={file} onclick={onFileClick} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -123,8 +73,6 @@
|
||||
<p class="text-gray-500">
|
||||
{#if files.length === 0}
|
||||
업로드된 파일이 없어요.
|
||||
{:else if filesWithDate.length === 0}
|
||||
파일 목록을 불러오고 있어요.
|
||||
{:else}
|
||||
사진 또는 동영상이 없어요.
|
||||
{/if}
|
||||
|
||||
97
src/lib/modules/file/download.svelte.ts
Normal file
97
src/lib/modules/file/download.svelte.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import axios from "axios";
|
||||
import { limitFunction } from "p-limit";
|
||||
import { decryptData } from "$lib/modules/crypto";
|
||||
|
||||
export interface FileDownloadState {
|
||||
id: number;
|
||||
status:
|
||||
| "download-pending"
|
||||
| "downloading"
|
||||
| "decryption-pending"
|
||||
| "decrypting"
|
||||
| "decrypted"
|
||||
| "canceled"
|
||||
| "error";
|
||||
progress?: number;
|
||||
rate?: number;
|
||||
estimated?: number;
|
||||
result?: ArrayBuffer;
|
||||
}
|
||||
|
||||
export type LiveFileDownloadState = FileDownloadState & {
|
||||
status: "download-pending" | "downloading" | "decryption-pending" | "decrypting";
|
||||
};
|
||||
|
||||
let downloadingFiles: FileDownloadState[] = $state([]);
|
||||
|
||||
export const isFileDownloading = (
|
||||
status: FileDownloadState["status"],
|
||||
): status is LiveFileDownloadState["status"] =>
|
||||
["download-pending", "downloading", "decryption-pending", "decrypting"].includes(status);
|
||||
|
||||
export const getFileDownloadState = (fileId: number) => {
|
||||
return downloadingFiles.find((file) => file.id === fileId && isFileDownloading(file.status));
|
||||
};
|
||||
|
||||
export const getDownloadingFiles = () => {
|
||||
return downloadingFiles.filter((file): file is LiveFileDownloadState =>
|
||||
isFileDownloading(file.status),
|
||||
);
|
||||
};
|
||||
|
||||
export const clearDownloadedFiles = () => {
|
||||
downloadingFiles = downloadingFiles.filter((file) => isFileDownloading(file.status));
|
||||
};
|
||||
|
||||
const requestFileDownload = limitFunction(
|
||||
async (state: FileDownloadState, id: number) => {
|
||||
state.status = "download-pending";
|
||||
|
||||
const res = await axios.get(`/api/file/${id}/download`, {
|
||||
responseType: "arraybuffer",
|
||||
onDownloadProgress: ({ progress, rate, estimated }) => {
|
||||
state.progress = progress;
|
||||
state.rate = rate;
|
||||
state.estimated = estimated;
|
||||
},
|
||||
});
|
||||
const fileEncrypted: ArrayBuffer = res.data;
|
||||
|
||||
state.status = "decryption-pending";
|
||||
return fileEncrypted;
|
||||
},
|
||||
{ concurrency: 1 },
|
||||
);
|
||||
|
||||
const decryptFile = limitFunction(
|
||||
async (
|
||||
state: FileDownloadState,
|
||||
fileEncrypted: ArrayBuffer,
|
||||
fileEncryptedIv: string,
|
||||
dataKey: CryptoKey,
|
||||
) => {
|
||||
state.status = "decrypting";
|
||||
|
||||
const fileBuffer = await decryptData(fileEncrypted, fileEncryptedIv, dataKey);
|
||||
|
||||
state.status = "decrypted";
|
||||
state.result = fileBuffer;
|
||||
return fileBuffer;
|
||||
},
|
||||
{ concurrency: 4 },
|
||||
);
|
||||
|
||||
export const downloadFile = async (id: number, fileEncryptedIv: string, dataKey: CryptoKey) => {
|
||||
downloadingFiles.push({
|
||||
id,
|
||||
status: "download-pending",
|
||||
});
|
||||
const state = downloadingFiles.at(-1)!;
|
||||
|
||||
try {
|
||||
return await decryptFile(state, await requestFileDownload(state, id), fileEncryptedIv, dataKey);
|
||||
} catch (e) {
|
||||
state.status = "error";
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import axios from "axios";
|
||||
import { limitFunction } from "p-limit";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { decryptData } from "$lib/modules/crypto";
|
||||
import { fileDownloadStatusStore, type FileDownloadStatus } from "$lib/stores";
|
||||
|
||||
const requestFileDownload = limitFunction(
|
||||
async (status: Writable<FileDownloadStatus>, id: number) => {
|
||||
status.update((value) => {
|
||||
value.status = "downloading";
|
||||
return value;
|
||||
});
|
||||
|
||||
const res = await axios.get(`/api/file/${id}/download`, {
|
||||
responseType: "arraybuffer",
|
||||
onDownloadProgress: ({ progress, rate, estimated }) => {
|
||||
status.update((value) => {
|
||||
value.progress = progress;
|
||||
value.rate = rate;
|
||||
value.estimated = estimated;
|
||||
return value;
|
||||
});
|
||||
},
|
||||
});
|
||||
const fileEncrypted: ArrayBuffer = res.data;
|
||||
|
||||
status.update((value) => {
|
||||
value.status = "decryption-pending";
|
||||
return value;
|
||||
});
|
||||
return fileEncrypted;
|
||||
},
|
||||
{ concurrency: 1 },
|
||||
);
|
||||
|
||||
const decryptFile = limitFunction(
|
||||
async (
|
||||
status: Writable<FileDownloadStatus>,
|
||||
fileEncrypted: ArrayBuffer,
|
||||
fileEncryptedIv: string,
|
||||
dataKey: CryptoKey,
|
||||
) => {
|
||||
status.update((value) => {
|
||||
value.status = "decrypting";
|
||||
return value;
|
||||
});
|
||||
|
||||
const fileBuffer = await decryptData(fileEncrypted, fileEncryptedIv, dataKey);
|
||||
|
||||
status.update((value) => {
|
||||
value.status = "decrypted";
|
||||
value.result = fileBuffer;
|
||||
return value;
|
||||
});
|
||||
return fileBuffer;
|
||||
},
|
||||
{ concurrency: 4 },
|
||||
);
|
||||
|
||||
export const downloadFile = async (id: number, fileEncryptedIv: string, dataKey: CryptoKey) => {
|
||||
const status = writable<FileDownloadStatus>({
|
||||
id,
|
||||
status: "download-pending",
|
||||
});
|
||||
fileDownloadStatusStore.update((value) => {
|
||||
value.push(status);
|
||||
return value;
|
||||
});
|
||||
|
||||
try {
|
||||
return await decryptFile(
|
||||
status,
|
||||
await requestFileDownload(status, id),
|
||||
fileEncryptedIv,
|
||||
dataKey,
|
||||
);
|
||||
} catch (e) {
|
||||
status.update((value) => {
|
||||
value.status = "error";
|
||||
return value;
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./cache";
|
||||
export * from "./download";
|
||||
export * from "./download.svelte";
|
||||
export * from "./upload.svelte";
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import {
|
||||
getFileInfo as getFileInfoFromIndexedDB,
|
||||
storeFileInfo,
|
||||
deleteFileInfo,
|
||||
} from "$lib/indexedDB";
|
||||
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
|
||||
import { trpc, isTRPCClientError } from "$trpc/client";
|
||||
|
||||
export interface FileInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey?: CryptoKey;
|
||||
dataKeyVersion?: Date;
|
||||
contentType: string;
|
||||
contentIv?: string;
|
||||
name: string;
|
||||
createdAt?: Date;
|
||||
lastModifiedAt: Date;
|
||||
categoryIds: number[];
|
||||
}
|
||||
|
||||
const fileInfoStore = new Map<number, Writable<FileInfo | null>>();
|
||||
|
||||
const fetchFileInfoFromIndexedDB = async (id: number, info: Writable<FileInfo | null>) => {
|
||||
if (get(info)) return;
|
||||
|
||||
const file = await getFileInfoFromIndexedDB(id);
|
||||
if (!file) return;
|
||||
|
||||
info.set(file);
|
||||
};
|
||||
|
||||
const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
|
||||
return new Date(parseInt(await decryptString(ciphertext, iv, dataKey), 10));
|
||||
};
|
||||
|
||||
const fetchFileInfoFromServer = async (
|
||||
id: number,
|
||||
info: Writable<FileInfo | null>,
|
||||
masterKey: CryptoKey,
|
||||
) => {
|
||||
let metadata;
|
||||
try {
|
||||
metadata = await trpc().file.get.query({ id });
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
|
||||
info.set(null);
|
||||
await deleteFileInfo(id);
|
||||
return;
|
||||
}
|
||||
throw new Error("Failed to fetch file information");
|
||||
}
|
||||
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
|
||||
|
||||
const name = await decryptString(metadata.name, metadata.nameIv, dataKey);
|
||||
const createdAt =
|
||||
metadata.createdAt && metadata.createdAtIv
|
||||
? await decryptDate(metadata.createdAt, metadata.createdAtIv, dataKey)
|
||||
: undefined;
|
||||
const lastModifiedAt = await decryptDate(
|
||||
metadata.lastModifiedAt,
|
||||
metadata.lastModifiedAtIv,
|
||||
dataKey,
|
||||
);
|
||||
|
||||
info.set({
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
dataKey,
|
||||
dataKeyVersion: new Date(metadata.dekVersion),
|
||||
contentType: metadata.contentType,
|
||||
contentIv: metadata.contentIv,
|
||||
name,
|
||||
createdAt,
|
||||
lastModifiedAt,
|
||||
categoryIds: metadata.categories,
|
||||
});
|
||||
await storeFileInfo({
|
||||
id,
|
||||
parentId: metadata.parent,
|
||||
name,
|
||||
contentType: metadata.contentType,
|
||||
createdAt,
|
||||
lastModifiedAt,
|
||||
categoryIds: metadata.categories,
|
||||
});
|
||||
};
|
||||
|
||||
const fetchFileInfo = async (id: number, info: Writable<FileInfo | null>, masterKey: CryptoKey) => {
|
||||
await fetchFileInfoFromIndexedDB(id, info);
|
||||
await fetchFileInfoFromServer(id, info, masterKey);
|
||||
};
|
||||
|
||||
export const getFileInfo = (fileId: number, masterKey: CryptoKey) => {
|
||||
// TODO: MEK rotation
|
||||
|
||||
let info = fileInfoStore.get(fileId);
|
||||
if (!info) {
|
||||
info = writable(null);
|
||||
fileInfoStore.set(fileId, info);
|
||||
}
|
||||
|
||||
fetchFileInfo(fileId, info, masterKey); // Intended
|
||||
return info;
|
||||
};
|
||||
@@ -41,12 +41,12 @@ interface RootDirectoryInfo {
|
||||
export type DirectoryInfo = LocalDirectoryInfo | RootDirectoryInfo;
|
||||
export type SubDirectoryInfo = Omit<LocalDirectoryInfo, "parentId" | "subDirectories" | "files">;
|
||||
|
||||
interface FileInfo {
|
||||
export interface FileInfo {
|
||||
id: number;
|
||||
parentId: DirectoryId;
|
||||
dataKey?: DataKey;
|
||||
contentType: string;
|
||||
contentIv: string | undefined;
|
||||
contentIv?: string;
|
||||
name: string;
|
||||
createdAt?: Date;
|
||||
lastModifiedAt: Date;
|
||||
@@ -81,6 +81,7 @@ export type SubCategoryInfo = Omit<
|
||||
>;
|
||||
|
||||
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) => {
|
||||
@@ -197,6 +198,100 @@ 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) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sql, type NotNull } from "kysely";
|
||||
import { sql } from "kysely";
|
||||
import pg from "pg";
|
||||
import { IntegrityError } from "./error";
|
||||
import db from "./kysely";
|
||||
@@ -486,10 +486,17 @@ export const addFileToCategory = async (fileId: number, categoryId: number) => {
|
||||
export const getAllFileCategories = async (fileId: number) => {
|
||||
const categories = await db
|
||||
.selectFrom("file_category")
|
||||
.select("category_id")
|
||||
.innerJoin("category", "file_category.category_id", "category.id")
|
||||
.selectAll("category")
|
||||
.where("file_id", "=", fileId)
|
||||
.execute();
|
||||
return categories.map(({ category_id }) => ({ id: category_id }));
|
||||
return categories.map((category) => ({
|
||||
id: category.id,
|
||||
mekVersion: category.master_encryption_key_version,
|
||||
encDek: category.encrypted_data_encryption_key,
|
||||
dekVersion: category.data_encryption_key_version,
|
||||
encName: category.encrypted_name,
|
||||
}));
|
||||
};
|
||||
|
||||
export const removeFileFromCategory = async (fileId: number, categoryId: number) => {
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
export interface FileDownloadStatus {
|
||||
id: number;
|
||||
status:
|
||||
| "download-pending"
|
||||
| "downloading"
|
||||
| "decryption-pending"
|
||||
| "decrypting"
|
||||
| "decrypted"
|
||||
| "canceled"
|
||||
| "error";
|
||||
progress?: number;
|
||||
rate?: number;
|
||||
estimated?: number;
|
||||
result?: ArrayBuffer;
|
||||
}
|
||||
|
||||
export const fileDownloadStatusStore = writable<Writable<FileDownloadStatus>[]>([]);
|
||||
|
||||
export const isFileDownloading = (
|
||||
status: FileDownloadStatus["status"],
|
||||
): status is "download-pending" | "downloading" | "decryption-pending" | "decrypting" => {
|
||||
return ["download-pending", "downloading", "decryption-pending", "decrypting"].includes(status);
|
||||
};
|
||||
@@ -1,2 +1 @@
|
||||
export * from "./file";
|
||||
export * from "./key";
|
||||
|
||||
Reference in New Issue
Block a user