mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-03 23:56:53 +00:00
즐겨찾기 기능 구현
This commit is contained in:
@@ -39,6 +39,7 @@ const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>({
|
|||||||
directory.subDirectories.map(async (directory) => ({
|
directory.subDirectories.map(async (directory) => ({
|
||||||
id: directory.id,
|
id: directory.id,
|
||||||
parentId: id,
|
parentId: id,
|
||||||
|
isFavorite: directory.isFavorite,
|
||||||
...(await decryptDirectoryMetadata(directory, masterKey)),
|
...(await decryptDirectoryMetadata(directory, masterKey)),
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
@@ -47,6 +48,7 @@ const cache = new FilesystemCache<DirectoryId, MaybeDirectoryInfo>({
|
|||||||
id: file.id,
|
id: file.id,
|
||||||
parentId: id,
|
parentId: id,
|
||||||
contentType: file.contentType,
|
contentType: file.contentType,
|
||||||
|
isFavorite: file.isFavorite,
|
||||||
...(await decryptFileMetadata(file, masterKey)),
|
...(await decryptFileMetadata(file, masterKey)),
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface LocalDirectoryInfo {
|
|||||||
parentId: DirectoryId;
|
parentId: DirectoryId;
|
||||||
dataKey?: DataKey;
|
dataKey?: DataKey;
|
||||||
name: string;
|
name: string;
|
||||||
|
isFavorite?: boolean;
|
||||||
subDirectories: SubDirectoryInfo[];
|
subDirectories: SubDirectoryInfo[];
|
||||||
files: SummarizedFileInfo[];
|
files: SummarizedFileInfo[];
|
||||||
}
|
}
|
||||||
@@ -36,6 +37,7 @@ export interface FileInfo {
|
|||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
lastModifiedAt: Date;
|
lastModifiedAt: Date;
|
||||||
categories: FileCategoryInfo[];
|
categories: FileCategoryInfo[];
|
||||||
|
isFavorite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MaybeFileInfo =
|
export type MaybeFileInfo =
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ type IntegrityErrorMessages =
|
|||||||
| "User client already exists"
|
| "User client already exists"
|
||||||
// File
|
// File
|
||||||
| "Directory not found"
|
| "Directory not found"
|
||||||
|
| "Directory already favorited"
|
||||||
|
| "Directory not favorited"
|
||||||
| "File not found"
|
| "File not found"
|
||||||
| "File is not legacy"
|
| "File is not legacy"
|
||||||
| "File not found in category"
|
| "File not found in category"
|
||||||
| "File already added to category"
|
| "File already added to category"
|
||||||
|
| "File already favorited"
|
||||||
|
| "File not favorited"
|
||||||
| "Invalid DEK version"
|
| "Invalid DEK version"
|
||||||
// HSK
|
// HSK
|
||||||
| "HSK already registered"
|
| "HSK already registered"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface Directory {
|
|||||||
encDek: string;
|
encDek: string;
|
||||||
dekVersion: Date;
|
dekVersion: Date;
|
||||||
encName: Ciphertext;
|
encName: Ciphertext;
|
||||||
|
isFavorite: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface File {
|
interface File {
|
||||||
@@ -31,6 +32,7 @@ interface File {
|
|||||||
encName: Ciphertext;
|
encName: Ciphertext;
|
||||||
encCreatedAt: Ciphertext | null;
|
encCreatedAt: Ciphertext | null;
|
||||||
encLastModifiedAt: Ciphertext;
|
encLastModifiedAt: Ciphertext;
|
||||||
|
isFavorite: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileCategory {
|
interface FileCategory {
|
||||||
@@ -42,7 +44,7 @@ interface FileCategory {
|
|||||||
encName: Ciphertext;
|
encName: Ciphertext;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const registerDirectory = async (params: Omit<Directory, "id">) => {
|
export const registerDirectory = async (params: Omit<Directory, "id" | "isFavorite">) => {
|
||||||
await db.transaction().execute(async (trx) => {
|
await db.transaction().execute(async (trx) => {
|
||||||
const mek = await trx
|
const mek = await trx
|
||||||
.selectFrom("master_encryption_key")
|
.selectFrom("master_encryption_key")
|
||||||
@@ -97,6 +99,7 @@ export const getAllDirectoriesByParent = async (userId: number, parentId: Direct
|
|||||||
encDek: directory.encrypted_data_encryption_key,
|
encDek: directory.encrypted_data_encryption_key,
|
||||||
dekVersion: directory.data_encryption_key_version,
|
dekVersion: directory.data_encryption_key_version,
|
||||||
encName: directory.encrypted_name,
|
encName: directory.encrypted_name,
|
||||||
|
isFavorite: directory.is_favorite,
|
||||||
}) satisfies Directory,
|
}) satisfies Directory,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -130,6 +133,7 @@ export const getAllRecursiveDirectoriesByParent = async (userId: number, parentI
|
|||||||
encDek: directory.encrypted_data_encryption_key,
|
encDek: directory.encrypted_data_encryption_key,
|
||||||
dekVersion: directory.data_encryption_key_version,
|
dekVersion: directory.data_encryption_key_version,
|
||||||
encName: directory.encrypted_name,
|
encName: directory.encrypted_name,
|
||||||
|
isFavorite: directory.is_favorite,
|
||||||
}) satisfies Directory,
|
}) satisfies Directory,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -151,6 +155,7 @@ export const getDirectory = async (userId: number, directoryId: number) => {
|
|||||||
encDek: directory.encrypted_data_encryption_key,
|
encDek: directory.encrypted_data_encryption_key,
|
||||||
dekVersion: directory.data_encryption_key_version,
|
dekVersion: directory.data_encryption_key_version,
|
||||||
encName: directory.encrypted_name,
|
encName: directory.encrypted_name,
|
||||||
|
isFavorite: directory.is_favorite,
|
||||||
} satisfies Directory)
|
} satisfies Directory)
|
||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
@@ -243,7 +248,7 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const registerFile = async (trx: typeof db, params: Omit<File, "id">) => {
|
export const registerFile = async (trx: typeof db, params: Omit<File, "id" | "isFavorite">) => {
|
||||||
if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) {
|
if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) {
|
||||||
throw new Error("Invalid arguments");
|
throw new Error("Invalid arguments");
|
||||||
}
|
}
|
||||||
@@ -305,6 +310,7 @@ export const getAllFilesByParent = async (userId: number, parentId: DirectoryId)
|
|||||||
encName: file.encrypted_name,
|
encName: file.encrypted_name,
|
||||||
encCreatedAt: file.encrypted_created_at,
|
encCreatedAt: file.encrypted_created_at,
|
||||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||||
|
isFavorite: file.is_favorite,
|
||||||
}) satisfies File,
|
}) satisfies File,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -357,6 +363,7 @@ export const getAllFilesByCategory = async (
|
|||||||
encName: file.encrypted_name,
|
encName: file.encrypted_name,
|
||||||
encCreatedAt: file.encrypted_created_at,
|
encCreatedAt: file.encrypted_created_at,
|
||||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||||
|
isFavorite: file.is_favorite,
|
||||||
isRecursive: file.depth > 0,
|
isRecursive: file.depth > 0,
|
||||||
}) satisfies File & { isRecursive: boolean },
|
}) satisfies File & { isRecursive: boolean },
|
||||||
);
|
);
|
||||||
@@ -393,6 +400,7 @@ export const getLegacyFiles = async (userId: number, limit: number = 100) => {
|
|||||||
encName: file.encrypted_name,
|
encName: file.encrypted_name,
|
||||||
encCreatedAt: file.encrypted_created_at,
|
encCreatedAt: file.encrypted_created_at,
|
||||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||||
|
isFavorite: file.is_favorite,
|
||||||
}) satisfies File,
|
}) satisfies File,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -436,6 +444,7 @@ export const getFilesWithoutThumbnail = async (userId: number, limit: number = 1
|
|||||||
encName: file.encrypted_name,
|
encName: file.encrypted_name,
|
||||||
encCreatedAt: file.encrypted_created_at,
|
encCreatedAt: file.encrypted_created_at,
|
||||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||||
|
isFavorite: file.is_favorite,
|
||||||
}) satisfies File,
|
}) satisfies File,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -480,6 +489,7 @@ export const getFile = async (userId: number, fileId: number) => {
|
|||||||
encName: file.encrypted_name,
|
encName: file.encrypted_name,
|
||||||
encCreatedAt: file.encrypted_created_at,
|
encCreatedAt: file.encrypted_created_at,
|
||||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||||
|
isFavorite: file.is_favorite,
|
||||||
} satisfies File)
|
} satisfies File)
|
||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
@@ -518,6 +528,7 @@ export const getFilesWithCategories = async (userId: number, fileIds: number[])
|
|||||||
encName: file.encrypted_name,
|
encName: file.encrypted_name,
|
||||||
encCreatedAt: file.encrypted_created_at,
|
encCreatedAt: file.encrypted_created_at,
|
||||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||||
|
isFavorite: file.is_favorite,
|
||||||
categories: file.categories.map((category) => ({
|
categories: file.categories.map((category) => ({
|
||||||
id: category.id,
|
id: category.id,
|
||||||
parentId: category.parent_id ?? "root",
|
parentId: category.parent_id ?? "root",
|
||||||
@@ -803,3 +814,127 @@ export const removeFileFromCategory = async (fileId: number, categoryId: number)
|
|||||||
.execute();
|
.execute();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setFileFavorite = async (userId: number, fileId: number, isFavorite: boolean) => {
|
||||||
|
await db.transaction().execute(async (trx) => {
|
||||||
|
const file = await trx
|
||||||
|
.selectFrom("file")
|
||||||
|
.select("is_favorite")
|
||||||
|
.where("id", "=", fileId)
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.limit(1)
|
||||||
|
.forUpdate()
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (!file) {
|
||||||
|
throw new IntegrityError("File not found");
|
||||||
|
} else if (file.is_favorite === isFavorite) {
|
||||||
|
throw new IntegrityError(isFavorite ? "File already favorited" : "File not favorited");
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.updateTable("file")
|
||||||
|
.set({ is_favorite: isFavorite })
|
||||||
|
.where("id", "=", fileId)
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.execute();
|
||||||
|
await trx
|
||||||
|
.insertInto("file_log")
|
||||||
|
.values({
|
||||||
|
file_id: fileId,
|
||||||
|
timestamp: new Date(),
|
||||||
|
action: isFavorite ? "add-to-favorites" : "remove-from-favorites",
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setDirectoryFavorite = async (
|
||||||
|
userId: number,
|
||||||
|
directoryId: number,
|
||||||
|
isFavorite: boolean,
|
||||||
|
) => {
|
||||||
|
await db.transaction().execute(async (trx) => {
|
||||||
|
const directory = await trx
|
||||||
|
.selectFrom("directory")
|
||||||
|
.select("is_favorite")
|
||||||
|
.where("id", "=", directoryId)
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.limit(1)
|
||||||
|
.forUpdate()
|
||||||
|
.executeTakeFirst();
|
||||||
|
if (!directory) {
|
||||||
|
throw new IntegrityError("Directory not found");
|
||||||
|
} else if (directory.is_favorite === isFavorite) {
|
||||||
|
throw new IntegrityError(
|
||||||
|
isFavorite ? "Directory already favorited" : "Directory not favorited",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.updateTable("directory")
|
||||||
|
.set({ is_favorite: isFavorite })
|
||||||
|
.where("id", "=", directoryId)
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.execute();
|
||||||
|
await trx
|
||||||
|
.insertInto("directory_log")
|
||||||
|
.values({
|
||||||
|
directory_id: directoryId,
|
||||||
|
timestamp: new Date(),
|
||||||
|
action: isFavorite ? "add-to-favorites" : "remove-from-favorites",
|
||||||
|
})
|
||||||
|
.execute();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllFavoriteFiles = async (userId: number) => {
|
||||||
|
const files = await db
|
||||||
|
.selectFrom("file")
|
||||||
|
.selectAll()
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.where("is_favorite", "=", true)
|
||||||
|
.execute();
|
||||||
|
return files.map(
|
||||||
|
(file) =>
|
||||||
|
({
|
||||||
|
id: file.id,
|
||||||
|
parentId: file.parent_id ?? "root",
|
||||||
|
userId: file.user_id,
|
||||||
|
path: file.path,
|
||||||
|
mekVersion: file.master_encryption_key_version,
|
||||||
|
encDek: file.encrypted_data_encryption_key,
|
||||||
|
dekVersion: file.data_encryption_key_version,
|
||||||
|
hskVersion: file.hmac_secret_key_version,
|
||||||
|
contentHmac: file.content_hmac,
|
||||||
|
contentType: file.content_type,
|
||||||
|
encContentIv: file.encrypted_content_iv,
|
||||||
|
encContentHash: file.encrypted_content_hash,
|
||||||
|
encName: file.encrypted_name,
|
||||||
|
encCreatedAt: file.encrypted_created_at,
|
||||||
|
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||||
|
isFavorite: file.is_favorite,
|
||||||
|
}) satisfies File,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllFavoriteDirectories = async (userId: number) => {
|
||||||
|
const directories = await db
|
||||||
|
.selectFrom("directory")
|
||||||
|
.selectAll()
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.where("is_favorite", "=", true)
|
||||||
|
.execute();
|
||||||
|
return directories.map(
|
||||||
|
(directory) =>
|
||||||
|
({
|
||||||
|
id: directory.id,
|
||||||
|
parentId: directory.parent_id ?? "root",
|
||||||
|
userId: directory.user_id,
|
||||||
|
mekVersion: directory.master_encryption_key_version,
|
||||||
|
encDek: directory.encrypted_data_encryption_key,
|
||||||
|
dekVersion: directory.data_encryption_key_version,
|
||||||
|
encName: directory.encrypted_name,
|
||||||
|
isFavorite: directory.is_favorite,
|
||||||
|
}) satisfies Directory,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
29
src/lib/server/db/migrations/1768643000-AddFavorites.ts
Normal file
29
src/lib/server/db/migrations/1768643000-AddFavorites.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Kysely, sql } from "kysely";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const up = async (db: Kysely<any>) => {
|
||||||
|
// file.ts
|
||||||
|
await db.schema
|
||||||
|
.alterTable("directory")
|
||||||
|
.addColumn("is_favorite", "boolean", (col) => col.notNull().defaultTo(false))
|
||||||
|
.execute();
|
||||||
|
await db.schema
|
||||||
|
.alterTable("file")
|
||||||
|
.addColumn("is_favorite", "boolean", (col) => col.notNull().defaultTo(false))
|
||||||
|
.execute();
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const down = async (db: Kysely<any>) => {
|
||||||
|
await db
|
||||||
|
.deleteFrom("file_log")
|
||||||
|
.where("action", "in", ["add-to-favorites", "remove-from-favorites"])
|
||||||
|
.execute();
|
||||||
|
await db
|
||||||
|
.deleteFrom("directory_log")
|
||||||
|
.where("action", "in", ["add-to-favorites", "remove-from-favorites"])
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
await db.schema.alterTable("file").dropColumn("is_favorite").execute();
|
||||||
|
await db.schema.alterTable("directory").dropColumn("is_favorite").execute();
|
||||||
|
};
|
||||||
@@ -2,10 +2,12 @@ import * as Initial1737357000 from "./1737357000-Initial";
|
|||||||
import * as AddFileCategory1737422340 from "./1737422340-AddFileCategory";
|
import * as AddFileCategory1737422340 from "./1737422340-AddFileCategory";
|
||||||
import * as AddThumbnail1738409340 from "./1738409340-AddThumbnail";
|
import * as AddThumbnail1738409340 from "./1738409340-AddThumbnail";
|
||||||
import * as AddChunkedUpload1768062380 from "./1768062380-AddChunkedUpload";
|
import * as AddChunkedUpload1768062380 from "./1768062380-AddChunkedUpload";
|
||||||
|
import * as AddFavorites1768643000 from "./1768643000-AddFavorites";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
"1737357000-Initial": Initial1737357000,
|
"1737357000-Initial": Initial1737357000,
|
||||||
"1737422340-AddFileCategory": AddFileCategory1737422340,
|
"1737422340-AddFileCategory": AddFileCategory1737422340,
|
||||||
"1738409340-AddThumbnail": AddThumbnail1738409340,
|
"1738409340-AddThumbnail": AddThumbnail1738409340,
|
||||||
"1768062380-AddChunkedUpload": AddChunkedUpload1768062380,
|
"1768062380-AddChunkedUpload": AddChunkedUpload1768062380,
|
||||||
|
"1768643000-AddFavorites": AddFavorites1768643000,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ interface DirectoryTable {
|
|||||||
encrypted_data_encryption_key: string; // Base64
|
encrypted_data_encryption_key: string; // Base64
|
||||||
data_encryption_key_version: Date;
|
data_encryption_key_version: Date;
|
||||||
encrypted_name: Ciphertext;
|
encrypted_name: Ciphertext;
|
||||||
|
is_favorite: Generated<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DirectoryLogTable {
|
interface DirectoryLogTable {
|
||||||
id: Generated<number>;
|
id: Generated<number>;
|
||||||
directory_id: number;
|
directory_id: number;
|
||||||
timestamp: ColumnType<Date, Date, never>;
|
timestamp: ColumnType<Date, Date, never>;
|
||||||
action: "create" | "rename";
|
action: "create" | "rename" | "add-to-favorites" | "remove-from-favorites";
|
||||||
new_name: Ciphertext | null;
|
new_name: Ciphertext | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,13 +36,21 @@ interface FileTable {
|
|||||||
encrypted_name: Ciphertext;
|
encrypted_name: Ciphertext;
|
||||||
encrypted_created_at: Ciphertext | null;
|
encrypted_created_at: Ciphertext | null;
|
||||||
encrypted_last_modified_at: Ciphertext;
|
encrypted_last_modified_at: Ciphertext;
|
||||||
|
is_favorite: Generated<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileLogTable {
|
interface FileLogTable {
|
||||||
id: Generated<number>;
|
id: Generated<number>;
|
||||||
file_id: number;
|
file_id: number;
|
||||||
timestamp: ColumnType<Date, Date, never>;
|
timestamp: ColumnType<Date, Date, never>;
|
||||||
action: "create" | "rename" | "migrate" | "add-to-category" | "remove-from-category";
|
action:
|
||||||
|
| "create"
|
||||||
|
| "rename"
|
||||||
|
| "migrate"
|
||||||
|
| "add-to-category"
|
||||||
|
| "remove-from-category"
|
||||||
|
| "add-to-favorites"
|
||||||
|
| "remove-from-favorites";
|
||||||
new_name: Ciphertext | null;
|
new_name: Ciphertext | null;
|
||||||
category_id: number | null;
|
category_id: number | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<TopBarMenu
|
<TopBarMenu
|
||||||
bind:isOpen={isMenuOpen}
|
bind:isOpen={isMenuOpen}
|
||||||
directoryId={["category", "gallery", "search"].includes(
|
directoryId={["category", "gallery", "search", "favorite"].includes(
|
||||||
page.url.searchParams.get("from") ?? "",
|
page.url.searchParams.get("from") ?? "",
|
||||||
)
|
)
|
||||||
? info?.parentId
|
? info?.parentId
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
const pages = [
|
const pages = [
|
||||||
{ path: "/home", label: "홈", icon: IconHome },
|
{ path: "/home", label: "홈", icon: IconHome },
|
||||||
{ path: "/directory", label: "파일", icon: IconFolder },
|
{ path: "/directory", label: "파일", icon: IconFolder },
|
||||||
{ path: "/favorite", label: "즐겨찾기", icon: IconFavorite },
|
{ path: "/favorites", label: "즐겨찾기", icon: IconFavorite },
|
||||||
{ path: "/category", label: "카테고리", icon: IconCategory },
|
{ path: "/category", label: "카테고리", icon: IconCategory },
|
||||||
{ path: "/menu", label: "전체", icon: IconMenu },
|
{ path: "/menu", label: "전체", icon: IconMenu },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
requestFileUpload,
|
requestFileUpload,
|
||||||
requestEntryRename,
|
requestEntryRename,
|
||||||
requestEntryDeletion,
|
requestEntryDeletion,
|
||||||
|
requestFavoriteToggle,
|
||||||
} from "./service.svelte";
|
} from "./service.svelte";
|
||||||
|
|
||||||
import IconSearch from "~icons/material-symbols/search";
|
import IconSearch from "~icons/material-symbols/search";
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
let isEntryDeleteModalOpen = $state(false);
|
let isEntryDeleteModalOpen = $state(false);
|
||||||
|
|
||||||
let showParentEntry = $derived(
|
let showParentEntry = $derived(
|
||||||
["file", "search"].includes(page.url.searchParams.get("from") ?? ""),
|
["file", "search", "favorite"].includes(page.url.searchParams.get("from") ?? ""),
|
||||||
);
|
);
|
||||||
let showBackButton = $derived(data.id !== "root" || showParentEntry);
|
let showBackButton = $derived(data.id !== "root" || showParentEntry);
|
||||||
|
|
||||||
@@ -194,6 +195,12 @@
|
|||||||
isEntryMenuBottomSheetOpen = false;
|
isEntryMenuBottomSheetOpen = false;
|
||||||
isEntryDeleteModalOpen = true;
|
isEntryDeleteModalOpen = true;
|
||||||
}}
|
}}
|
||||||
|
onFavoriteClick={async () => {
|
||||||
|
if (await requestFavoriteToggle(context.selectedEntry!)) {
|
||||||
|
isEntryMenuBottomSheetOpen = false;
|
||||||
|
void getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<EntryRenameModal
|
<EntryRenameModal
|
||||||
bind:isOpen={isEntryRenameModalOpen}
|
bind:isOpen={isEntryRenameModalOpen}
|
||||||
|
|||||||
@@ -19,7 +19,13 @@
|
|||||||
let thumbnail = $derived(getFileThumbnail(info));
|
let thumbnail = $derived(getFileThumbnail(info));
|
||||||
|
|
||||||
const action = (callback: typeof onclick) => {
|
const action = (callback: typeof onclick) => {
|
||||||
callback({ type: "file", id: info.id, dataKey: info.dataKey, name: info.name });
|
callback({
|
||||||
|
type: "file",
|
||||||
|
id: info.id,
|
||||||
|
dataKey: info.dataKey,
|
||||||
|
name: info.name,
|
||||||
|
isFavorite: info.isFavorite ?? false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,13 @@
|
|||||||
let { info, onclick, onOpenMenuClick }: Props = $props();
|
let { info, onclick, onOpenMenuClick }: Props = $props();
|
||||||
|
|
||||||
const action = (callback: typeof onclick) => {
|
const action = (callback: typeof onclick) => {
|
||||||
callback({ type: "directory", id: info.id, dataKey: info.dataKey, name: info.name });
|
callback({
|
||||||
|
type: "directory",
|
||||||
|
id: info.id,
|
||||||
|
dataKey: info.dataKey,
|
||||||
|
name: info.name,
|
||||||
|
isFavorite: info.isFavorite ?? false,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -3,24 +3,35 @@
|
|||||||
import { DirectoryEntryLabel, IconEntryButton } from "$lib/components/molecules";
|
import { DirectoryEntryLabel, IconEntryButton } from "$lib/components/molecules";
|
||||||
import { useContext } from "./service.svelte";
|
import { useContext } from "./service.svelte";
|
||||||
|
|
||||||
|
import IconFavorite from "~icons/material-symbols/favorite";
|
||||||
|
import IconFavoriteBorder from "~icons/material-symbols/favorite-outline";
|
||||||
import IconEdit from "~icons/material-symbols/edit";
|
import IconEdit from "~icons/material-symbols/edit";
|
||||||
import IconDelete from "~icons/material-symbols/delete";
|
import IconDelete from "~icons/material-symbols/delete";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onDeleteClick: () => void;
|
onDeleteClick: () => void;
|
||||||
|
onFavoriteClick: () => void;
|
||||||
onRenameClick: () => void;
|
onRenameClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { isOpen = $bindable(), onDeleteClick, onRenameClick }: Props = $props();
|
let { isOpen = $bindable(), onDeleteClick, onFavoriteClick, onRenameClick }: Props = $props();
|
||||||
let context = useContext();
|
let context = useContext();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if context.selectedEntry}
|
{#if context.selectedEntry}
|
||||||
{@const { name, type } = context.selectedEntry}
|
{@const { name, type, isFavorite } = context.selectedEntry}
|
||||||
<BottomSheet bind:isOpen class="p-4">
|
<BottomSheet bind:isOpen class="p-4">
|
||||||
<DirectoryEntryLabel {type} {name} class="h-12 p-2" textClass="!font-semibold" />
|
<DirectoryEntryLabel {type} {name} class="h-12 p-2" textClass="!font-semibold" />
|
||||||
<div class="my-2 h-px w-full bg-gray-200"></div>
|
<div class="my-2 h-px w-full bg-gray-200"></div>
|
||||||
|
<IconEntryButton
|
||||||
|
icon={isFavorite ? IconFavorite : IconFavoriteBorder}
|
||||||
|
onclick={onFavoriteClick}
|
||||||
|
class="h-12 w-full"
|
||||||
|
iconClass={isFavorite ? "text-red-500" : ""}
|
||||||
|
>
|
||||||
|
{isFavorite ? "즐겨찾기에서 해제하기" : "즐겨찾기에 추가하기"}
|
||||||
|
</IconEntryButton>
|
||||||
<IconEntryButton icon={IconEdit} onclick={onRenameClick} class="h-12 w-full">
|
<IconEntryButton icon={IconEdit} onclick={onRenameClick} class="h-12 w-full">
|
||||||
이름 바꾸기
|
이름 바꾸기
|
||||||
</IconEntryButton>
|
</IconEntryButton>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export interface SelectedEntry {
|
|||||||
id: number;
|
id: number;
|
||||||
dataKey: DataKey | undefined;
|
dataKey: DataKey | undefined;
|
||||||
name: string;
|
name: string;
|
||||||
|
isFavorite: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createContext = () => {
|
export const createContext = () => {
|
||||||
@@ -149,3 +150,25 @@ export const requestEntryDeletion = async (entry: SelectedEntry) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const requestFavoriteToggle = async (entry: SelectedEntry) => {
|
||||||
|
try {
|
||||||
|
if (entry.type === "directory") {
|
||||||
|
if (entry.isFavorite) {
|
||||||
|
await trpc().favorites.removeDirectory.mutate({ id: entry.id });
|
||||||
|
} else {
|
||||||
|
await trpc().favorites.addDirectory.mutate({ id: entry.id });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (entry.isFavorite) {
|
||||||
|
await trpc().favorites.removeFile.mutate({ id: entry.id });
|
||||||
|
} else {
|
||||||
|
await trpc().favorites.addFile.mutate({ id: entry.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// TODO: Error Handling
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<div class="flex h-full items-center justify-center p-4">
|
|
||||||
<p class="text-gray-500">아직 개발 중이에요.</p>
|
|
||||||
</div>
|
|
||||||
7
src/routes/(main)/favorites/+page.server.ts
Normal file
7
src/routes/(main)/favorites/+page.server.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createCaller } from "$trpc/router.server";
|
||||||
|
import type { PageServerLoad } from "./$types";
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async (event) => {
|
||||||
|
const favorites = await createCaller(event).favorites.get();
|
||||||
|
return { favorites };
|
||||||
|
};
|
||||||
84
src/routes/(main)/favorites/+page.svelte
Normal file
84
src/routes/(main)/favorites/+page.svelte
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import { RowVirtualizer } from "$lib/components/atoms";
|
||||||
|
import { masterKeyStore } from "$lib/stores";
|
||||||
|
import Directory from "./Directory.svelte";
|
||||||
|
import File from "./File.svelte";
|
||||||
|
import { requestFavoriteEntries, requestRemoveFavorite, type FavoriteEntry } from "./service";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
|
let entries: FavoriteEntry[] = $state([]);
|
||||||
|
let isLoading = $state(true);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const masterKey = $masterKeyStore?.get(1)?.key;
|
||||||
|
if (masterKey) {
|
||||||
|
entries = await requestFavoriteEntries(data.favorites, masterKey);
|
||||||
|
}
|
||||||
|
isLoading = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleRemove = async (entry: FavoriteEntry) => {
|
||||||
|
if (await requestRemoveFavorite(entry.type, entry.details.id)) {
|
||||||
|
entries = entries.filter(
|
||||||
|
(e) => !(e.type === entry.type && e.details.id === entry.details.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (entry: FavoriteEntry) => {
|
||||||
|
goto(
|
||||||
|
entry.type === "file"
|
||||||
|
? `/file/${entry.details.id}?from=favorite`
|
||||||
|
: `/directory/${entry.details.id}?from=favorite`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>즐겨찾기</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="flex h-full flex-col p-4">
|
||||||
|
{#if isLoading}
|
||||||
|
<div class="flex flex-grow items-center justify-center">
|
||||||
|
<p class="text-gray-500">
|
||||||
|
{#if data.favorites.files.length === 0 && data.favorites.directories.length === 0}
|
||||||
|
즐겨찾기한 항목이 없어요.
|
||||||
|
{:else}
|
||||||
|
로딩 중...
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{:else if entries.length === 0}
|
||||||
|
<div class="flex flex-grow items-center justify-center">
|
||||||
|
<p class="text-gray-500">즐겨찾기한 항목이 없어요.</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<RowVirtualizer
|
||||||
|
count={entries.length}
|
||||||
|
getItemKey={(index) => `${entries[index]!.type}-${entries[index]!.details.id}`}
|
||||||
|
estimateItemHeight={() => 56}
|
||||||
|
itemGap={4}
|
||||||
|
>
|
||||||
|
{#snippet item(index)}
|
||||||
|
{@const entry = entries[index]!}
|
||||||
|
{#if entry.type === "directory"}
|
||||||
|
<Directory
|
||||||
|
info={entry.details}
|
||||||
|
onclick={() => handleClick(entry)}
|
||||||
|
onRemoveClick={() => handleRemove(entry)}
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<File
|
||||||
|
info={entry.details}
|
||||||
|
onclick={() => handleClick(entry)}
|
||||||
|
onRemoveClick={() => handleRemove(entry)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</RowVirtualizer>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
24
src/routes/(main)/favorites/Directory.svelte
Normal file
24
src/routes/(main)/favorites/Directory.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ActionEntryButton } from "$lib/components/atoms";
|
||||||
|
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||||
|
import type { SubDirectoryInfo } from "$lib/modules/filesystem";
|
||||||
|
|
||||||
|
import IconClose from "~icons/material-symbols/close";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
info: SubDirectoryInfo;
|
||||||
|
onclick: () => void;
|
||||||
|
onRemoveClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { info, onclick, onRemoveClick }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionEntryButton
|
||||||
|
class="h-14"
|
||||||
|
{onclick}
|
||||||
|
actionButtonIcon={IconClose}
|
||||||
|
onActionButtonClick={onRemoveClick}
|
||||||
|
>
|
||||||
|
<DirectoryEntryLabel type="directory" name={info.name} />
|
||||||
|
</ActionEntryButton>
|
||||||
27
src/routes/(main)/favorites/File.svelte
Normal file
27
src/routes/(main)/favorites/File.svelte
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ActionEntryButton } from "$lib/components/atoms";
|
||||||
|
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||||
|
import { getFileThumbnail } from "$lib/modules/file";
|
||||||
|
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||||
|
|
||||||
|
import IconClose from "~icons/material-symbols/close";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
info: SummarizedFileInfo;
|
||||||
|
onclick: () => void;
|
||||||
|
onRemoveClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { info, onclick, onRemoveClick }: Props = $props();
|
||||||
|
|
||||||
|
let thumbnail = $derived(getFileThumbnail(info));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ActionEntryButton
|
||||||
|
class="h-14"
|
||||||
|
{onclick}
|
||||||
|
actionButtonIcon={IconClose}
|
||||||
|
onActionButtonClick={onRemoveClick}
|
||||||
|
>
|
||||||
|
<DirectoryEntryLabel type="file" thumbnail={$thumbnail} name={info.name} />
|
||||||
|
</ActionEntryButton>
|
||||||
86
src/routes/(main)/favorites/service.ts
Normal file
86
src/routes/(main)/favorites/service.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
decryptDirectoryMetadata,
|
||||||
|
decryptFileMetadata,
|
||||||
|
getFileInfo,
|
||||||
|
type SummarizedFileInfo,
|
||||||
|
type SubDirectoryInfo,
|
||||||
|
} from "$lib/modules/filesystem";
|
||||||
|
import { HybridPromise, sortEntries } from "$lib/utils";
|
||||||
|
import { trpc } from "$trpc/client";
|
||||||
|
import type { RouterOutputs } from "$trpc/router.server";
|
||||||
|
|
||||||
|
export type FavoriteEntry =
|
||||||
|
| { type: "directory"; name: string; details: SubDirectoryInfo }
|
||||||
|
| { type: "file"; name: string; details: SummarizedFileInfo };
|
||||||
|
|
||||||
|
export const requestFavoriteEntries = async (
|
||||||
|
favorites: RouterOutputs["favorites"]["get"],
|
||||||
|
masterKey: CryptoKey,
|
||||||
|
): Promise<FavoriteEntry[]> => {
|
||||||
|
const directories: FavoriteEntry[] = await Promise.all(
|
||||||
|
favorites.directories.map(async (dir) => {
|
||||||
|
const metadata = await decryptDirectoryMetadata(dir, masterKey);
|
||||||
|
return {
|
||||||
|
type: "directory" as const,
|
||||||
|
name: metadata.name,
|
||||||
|
details: {
|
||||||
|
id: dir.id,
|
||||||
|
parentId: dir.parent,
|
||||||
|
isFavorite: true,
|
||||||
|
dataKey: metadata.dataKey,
|
||||||
|
name: metadata.name,
|
||||||
|
} as SubDirectoryInfo,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileResults = await Promise.all(
|
||||||
|
favorites.files.map(async (file) => {
|
||||||
|
const result = await HybridPromise.resolve(
|
||||||
|
getFileInfo(file.id, masterKey, {
|
||||||
|
async fetchFromServer(id, cachedInfo) {
|
||||||
|
const metadata = await decryptFileMetadata(file, masterKey);
|
||||||
|
return {
|
||||||
|
categories: [],
|
||||||
|
...cachedInfo,
|
||||||
|
id: id as number,
|
||||||
|
exists: true,
|
||||||
|
parentId: file.parent,
|
||||||
|
contentType: file.contentType,
|
||||||
|
isFavorite: true,
|
||||||
|
...metadata,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (result?.exists) {
|
||||||
|
return {
|
||||||
|
type: "file" as const,
|
||||||
|
name: result.name,
|
||||||
|
details: result as SummarizedFileInfo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const files = fileResults.filter(
|
||||||
|
(f): f is { type: "file"; name: string; details: SummarizedFileInfo } => f !== null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [...sortEntries(directories), ...sortEntries(files)];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requestRemoveFavorite = async (type: "file" | "directory", id: number) => {
|
||||||
|
try {
|
||||||
|
if (type === "directory") {
|
||||||
|
await trpc().favorites.removeDirectory.mutate({ id });
|
||||||
|
} else {
|
||||||
|
await trpc().favorites.removeFile.mutate({ id });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
// TODO: Error Handling
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
categoryRouter,
|
categoryRouter,
|
||||||
clientRouter,
|
clientRouter,
|
||||||
directoryRouter,
|
directoryRouter,
|
||||||
|
favoritesRouter,
|
||||||
fileRouter,
|
fileRouter,
|
||||||
hskRouter,
|
hskRouter,
|
||||||
mekRouter,
|
mekRouter,
|
||||||
@@ -19,6 +20,7 @@ export const appRouter = router({
|
|||||||
category: categoryRouter,
|
category: categoryRouter,
|
||||||
client: clientRouter,
|
client: clientRouter,
|
||||||
directory: directoryRouter,
|
directory: directoryRouter,
|
||||||
|
favorites: favoritesRouter,
|
||||||
file: fileRouter,
|
file: fileRouter,
|
||||||
hsk: hskRouter,
|
hsk: hskRouter,
|
||||||
mek: mekRouter,
|
mek: mekRouter,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const directoryRouter = router({
|
|||||||
dekVersion: directory.dekVersion,
|
dekVersion: directory.dekVersion,
|
||||||
name: directory.encName.ciphertext,
|
name: directory.encName.ciphertext,
|
||||||
nameIv: directory.encName.iv,
|
nameIv: directory.encName.iv,
|
||||||
|
isFavorite: directory.isFavorite,
|
||||||
},
|
},
|
||||||
subDirectories: directories.map((directory) => ({
|
subDirectories: directories.map((directory) => ({
|
||||||
id: directory.id,
|
id: directory.id,
|
||||||
@@ -39,6 +40,7 @@ const directoryRouter = router({
|
|||||||
dekVersion: directory.dekVersion,
|
dekVersion: directory.dekVersion,
|
||||||
name: directory.encName.ciphertext,
|
name: directory.encName.ciphertext,
|
||||||
nameIv: directory.encName.iv,
|
nameIv: directory.encName.iv,
|
||||||
|
isFavorite: directory.isFavorite,
|
||||||
})),
|
})),
|
||||||
files: files.map((file) => ({
|
files: files.map((file) => ({
|
||||||
id: file.id,
|
id: file.id,
|
||||||
@@ -52,6 +54,7 @@ const directoryRouter = router({
|
|||||||
createdAtIv: file.encCreatedAt?.iv,
|
createdAtIv: file.encCreatedAt?.iv,
|
||||||
lastModifiedAt: file.encLastModifiedAt.ciphertext,
|
lastModifiedAt: file.encLastModifiedAt.ciphertext,
|
||||||
lastModifiedAtIv: file.encLastModifiedAt.iv,
|
lastModifiedAtIv: file.encLastModifiedAt.iv,
|
||||||
|
isFavorite: file.isFavorite,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
124
src/trpc/routers/favorites.ts
Normal file
124
src/trpc/routers/favorites.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { FileRepo, IntegrityError } from "$lib/server/db";
|
||||||
|
import { router, roleProcedure } from "../init.server";
|
||||||
|
|
||||||
|
const favoritesRouter = router({
|
||||||
|
get: roleProcedure["activeClient"].query(async ({ ctx }) => {
|
||||||
|
const [files, directories] = await Promise.all([
|
||||||
|
FileRepo.getAllFavoriteFiles(ctx.session.userId),
|
||||||
|
FileRepo.getAllFavoriteDirectories(ctx.session.userId),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
files: files.map((file) => ({
|
||||||
|
id: file.id,
|
||||||
|
parent: file.parentId,
|
||||||
|
mekVersion: file.mekVersion,
|
||||||
|
dek: file.encDek,
|
||||||
|
dekVersion: file.dekVersion,
|
||||||
|
contentType: file.contentType,
|
||||||
|
name: file.encName.ciphertext,
|
||||||
|
nameIv: file.encName.iv,
|
||||||
|
createdAt: file.encCreatedAt?.ciphertext,
|
||||||
|
createdAtIv: file.encCreatedAt?.iv,
|
||||||
|
lastModifiedAt: file.encLastModifiedAt.ciphertext,
|
||||||
|
lastModifiedAtIv: file.encLastModifiedAt.iv,
|
||||||
|
})),
|
||||||
|
directories: directories.map((directory) => ({
|
||||||
|
id: directory.id,
|
||||||
|
parent: directory.parentId,
|
||||||
|
mekVersion: directory.mekVersion,
|
||||||
|
dek: directory.encDek,
|
||||||
|
dekVersion: directory.dekVersion,
|
||||||
|
name: directory.encName.ciphertext,
|
||||||
|
nameIv: directory.encName.iv,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
addFile: roleProcedure["activeClient"]
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
id: z.int().positive(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
try {
|
||||||
|
await FileRepo.setFileFavorite(ctx.session.userId, input.id, true);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof IntegrityError) {
|
||||||
|
if (e.message === "File not found") {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
|
||||||
|
} else if (e.message === "File already favorited") {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeFile: roleProcedure["activeClient"]
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
id: z.int().positive(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
try {
|
||||||
|
await FileRepo.setFileFavorite(ctx.session.userId, input.id, false);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof IntegrityError) {
|
||||||
|
if (e.message === "File not found") {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid file id" });
|
||||||
|
} else if (e.message === "File not favorited") {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
addDirectory: roleProcedure["activeClient"]
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
id: z.int().positive(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
try {
|
||||||
|
await FileRepo.setDirectoryFavorite(ctx.session.userId, input.id, true);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof IntegrityError) {
|
||||||
|
if (e.message === "Directory not found") {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" });
|
||||||
|
} else if (e.message === "Directory already favorited") {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
removeDirectory: roleProcedure["activeClient"]
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
id: z.int().positive(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
try {
|
||||||
|
await FileRepo.setDirectoryFavorite(ctx.session.userId, input.id, false);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof IntegrityError) {
|
||||||
|
if (e.message === "Directory not found") {
|
||||||
|
throw new TRPCError({ code: "NOT_FOUND", message: "Invalid directory id" });
|
||||||
|
} else if (e.message === "Directory not favorited") {
|
||||||
|
throw new TRPCError({ code: "BAD_REQUEST", message: e.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default favoritesRouter;
|
||||||
@@ -2,6 +2,7 @@ export { default as authRouter } from "./auth";
|
|||||||
export { default as categoryRouter } from "./category";
|
export { default as categoryRouter } from "./category";
|
||||||
export { default as clientRouter } from "./client";
|
export { default as clientRouter } from "./client";
|
||||||
export { default as directoryRouter } from "./directory";
|
export { default as directoryRouter } from "./directory";
|
||||||
|
export { default as favoritesRouter } from "./favorites";
|
||||||
export { default as fileRouter } from "./file";
|
export { default as fileRouter } from "./file";
|
||||||
export { default as hskRouter } from "./hsk";
|
export { default as hskRouter } from "./hsk";
|
||||||
export { default as mekRouter } from "./mek";
|
export { default as mekRouter } from "./mek";
|
||||||
|
|||||||
Reference in New Issue
Block a user