Files
arkvault/src/lib/modules/filesystem/directory.ts

126 lines
3.6 KiB
TypeScript

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 { MaybeDirectoryInfo } from "./types";
const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>();
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,
exists: true as const,
subDirectories,
files,
};
} else if (directory) {
return {
id,
exists: true as const,
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 });
void IndexedDB.deleteDanglingDirectoryInfos(id, new Set(subDirectoriesRaw.map(({ id }) => id)));
void IndexedDB.deleteDanglingFileInfos(id, new Set(filesRaw.map(({ id }) => id)));
const existingFiles = await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id));
const [subDirectories, files, decryptedMetadata] = await Promise.all([
Promise.all(
subDirectoriesRaw.map(async (directory) => {
const decrypted = await decryptDirectoryMetadata(directory, masterKey);
await IndexedDB.storeDirectoryInfo({
id: directory.id,
parentId: id,
name: decrypted.name,
});
return {
id: directory.id,
...decrypted,
};
}),
),
Promise.all(
filesRaw.map(async (file, index) => {
const decrypted = await decryptFileMetadata(file, masterKey);
await IndexedDB.storeFileInfo({
id: file.id,
parentId: id,
contentType: file.contentType,
name: decrypted.name,
createdAt: decrypted.createdAt,
lastModifiedAt: decrypted.lastModifiedAt,
categoryIds: existingFiles[index]?.categoryIds ?? [],
});
return {
id: file.id,
contentType: file.contentType,
...decrypted,
};
}),
),
metadata ? decryptDirectoryMetadata(metadata, masterKey) : undefined,
]);
if (id !== "root" && metadata && decryptedMetadata) {
await IndexedDB.storeDirectoryInfo({
id,
parentId: metadata.parent,
name: decryptedMetadata.name,
});
}
if (id === "root") {
return {
id,
exists: true as const,
subDirectories,
files,
};
} else {
return {
id,
exists: true as const,
parentId: metadata!.parent,
subDirectories,
files,
...decryptedMetadata!,
};
}
} catch (e) {
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
await IndexedDB.deleteDirectoryInfo(id as number);
return { id, exists: false as const };
}
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,
),
);
};