From e1262506c4168d9a9f90213cdf2e97e8581a6ae4 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 13 Jan 2025 09:10:56 +0900 Subject: [PATCH 01/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20DB=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/category.ts | 111 +++++++++++++++++++++++++++ src/lib/server/db/error.ts | 4 + src/lib/server/db/file.ts | 54 ++++++++++++- src/lib/server/db/schema/category.ts | 52 +++++++++++++ src/lib/server/db/schema/file.ts | 25 +++++- src/lib/server/db/schema/index.ts | 1 + 6 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 src/lib/server/db/category.ts create mode 100644 src/lib/server/db/schema/category.ts diff --git a/src/lib/server/db/category.ts b/src/lib/server/db/category.ts new file mode 100644 index 0000000..db543ab --- /dev/null +++ b/src/lib/server/db/category.ts @@ -0,0 +1,111 @@ +import { and, eq, isNull } from "drizzle-orm"; +import db from "./drizzle"; +import { IntegrityError } from "./error"; +import { category, categoryLog, mek } from "./schema"; + +type CategoryId = "root" | number; + +export interface NewCategoryParams { + parentId: "root" | number; + userId: number; + mekVersion: number; + encDek: string; + dekVersion: Date; + encName: string; + encNameIv: string; +} + +export const registerCategory = async (params: NewCategoryParams) => { + await db.transaction( + async (tx) => { + const meks = await tx + .select({ version: mek.version }) + .from(mek) + .where(and(eq(mek.userId, params.userId), eq(mek.state, "active"))) + .limit(1); + if (meks[0]?.version !== params.mekVersion) { + throw new IntegrityError("Inactive MEK version"); + } + + const newCategories = await tx + .insert(category) + .values({ + parentId: params.parentId === "root" ? null : params.parentId, + userId: params.userId, + mekVersion: params.mekVersion, + encDek: params.encDek, + dekVersion: params.dekVersion, + encName: { ciphertext: params.encName, iv: params.encNameIv }, + }) + .returning({ id: category.id }); + const { id: categoryId } = newCategories[0]!; + await tx.insert(categoryLog).values({ + categoryId, + timestamp: new Date(), + action: "create", + newName: { ciphertext: params.encName, iv: params.encNameIv }, + }); + }, + { behavior: "exclusive" }, + ); +}; + +export const getAllCategoriesByParent = async (userId: number, parentId: CategoryId) => { + return await db + .select() + .from(category) + .where( + and( + eq(category.userId, userId), + parentId === "root" ? isNull(category.parentId) : eq(category.parentId, parentId), + ), + ); +}; + +export const getCategory = async (userId: number, categoryId: number) => { + const res = await db + .select() + .from(category) + .where(and(eq(category.userId, userId), eq(category.id, categoryId))) + .limit(1); + return res[0] ?? null; +}; + +export const setCategoryEncName = async ( + userId: number, + categoryId: number, + dekVersion: Date, + encName: string, + encNameIv: string, +) => { + await db.transaction( + async (tx) => { + const categories = await tx + .select({ version: category.dekVersion }) + .from(category) + .where(and(eq(category.userId, userId), eq(category.id, categoryId))) + .limit(1); + if (!categories[0]) { + throw new IntegrityError("Category not found"); + } else if (categories[0].version.getTime() !== dekVersion.getTime()) { + throw new IntegrityError("Invalid DEK version"); + } + + await tx + .update(category) + .set({ encName: { ciphertext: encName, iv: encNameIv } }) + .where(and(eq(category.userId, userId), eq(category.id, categoryId))); + await tx.insert(categoryLog).values({ + categoryId, + timestamp: new Date(), + action: "rename", + newName: { ciphertext: encName, iv: encNameIv }, + }); + }, + { behavior: "exclusive" }, + ); +}; + +export const unregisterCategory = async (userId: number, categoryId: number) => { + await db.delete(category).where(and(eq(category.userId, userId), eq(category.id, categoryId))); +}; diff --git a/src/lib/server/db/error.ts b/src/lib/server/db/error.ts index 547cc6c..a145f14 100644 --- a/src/lib/server/db/error.ts +++ b/src/lib/server/db/error.ts @@ -1,4 +1,6 @@ type IntegrityErrorMessages = + // Category + | "Category not found" // Challenge | "Challenge already registered" // Client @@ -7,6 +9,8 @@ type IntegrityErrorMessages = // File | "Directory not found" | "File not found" + | "File not found in category" + | "File already added to category" | "Invalid DEK version" // HSK | "HSK already registered" diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index b99bd38..5d89306 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -1,7 +1,8 @@ +import { SqliteError } from "better-sqlite3"; import { and, eq, isNull } from "drizzle-orm"; import db from "./drizzle"; import { IntegrityError } from "./error"; -import { directory, directoryLog, file, fileLog, hsk, mek } from "./schema"; +import { directory, directoryLog, file, fileLog, fileCategory, hsk, mek } from "./schema"; type DirectoryId = "root" | number; @@ -237,6 +238,14 @@ export const getAllFilesByParent = async (userId: number, parentId: DirectoryId) ); }; +export const getAllFilesByCategory = async (userId: number, categoryId: number) => { + return await db + .select() + .from(file) + .innerJoin(fileCategory, eq(file.id, fileCategory.fileId)) + .where(and(eq(file.userId, userId), eq(fileCategory.categoryId, categoryId))); +}; + export const getAllFileIdsByContentHmac = async ( userId: number, hskVersion: number, @@ -308,3 +317,46 @@ export const unregisterFile = async (userId: number, fileId: number) => { } return files[0].path; }; + +export const addFileToCategory = async (fileId: number, categoryId: number) => { + await db.transaction( + async (tx) => { + try { + await tx.insert(fileCategory).values({ fileId, categoryId }); + await tx.insert(fileLog).values({ + fileId, + timestamp: new Date(), + action: "addToCategory", + categoryId, + }); + } catch (e) { + if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") { + throw new IntegrityError("File already added to category"); + } + throw e; + } + }, + { behavior: "exclusive" }, + ); +}; + +export const removeFileFromCategory = async (fileId: number, categoryId: number) => { + await db.transaction( + async (tx) => { + const res = await tx + .delete(fileCategory) + .where(and(eq(fileCategory.fileId, fileId), eq(fileCategory.categoryId, categoryId))); + if (res.changes === 0) { + throw new IntegrityError("File not found in category"); + } + + await tx.insert(fileLog).values({ + fileId, + timestamp: new Date(), + action: "removeFromCategory", + categoryId, + }); + }, + { behavior: "exclusive" }, + ); +}; diff --git a/src/lib/server/db/schema/category.ts b/src/lib/server/db/schema/category.ts new file mode 100644 index 0000000..f8e1621 --- /dev/null +++ b/src/lib/server/db/schema/category.ts @@ -0,0 +1,52 @@ +import { + sqliteTable, + text, + integer, + foreignKey, + type AnySQLiteColumn, +} from "drizzle-orm/sqlite-core"; +import { mek } from "./mek"; +import { user } from "./user"; + +const ciphertext = (name: string) => + text(name, { mode: "json" }).$type<{ + ciphertext: string; // Base64 + iv: string; // Base64 + }>(); + +export const category = sqliteTable( + "category", + { + id: integer("id").primaryKey({ autoIncrement: true }), + parentId: integer("parent_id").references((): AnySQLiteColumn => category.id, { + onDelete: "cascade", + }), + userId: integer("user_id") + .notNull() + .references(() => user.id), + mekVersion: integer("master_encryption_key_version").notNull(), + encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 + dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), + encName: ciphertext("encrypted_name").notNull(), + }, + (t) => ({ + ref1: foreignKey({ + columns: [t.parentId], + foreignColumns: [t.id], + }), + ref2: foreignKey({ + columns: [t.userId, t.mekVersion], + foreignColumns: [mek.userId, mek.version], + }), + }), +); + +export const categoryLog = sqliteTable("category_log", { + id: integer("id").primaryKey({ autoIncrement: true }), + categoryId: integer("category_id") + .notNull() + .references(() => category.id, { onDelete: "cascade" }), + timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), + action: text("action", { enum: ["create", "rename"] }).notNull(), + newName: ciphertext("new_name"), +}); diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts index 65c5471..9400ae9 100644 --- a/src/lib/server/db/schema/file.ts +++ b/src/lib/server/db/schema/file.ts @@ -1,4 +1,5 @@ -import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core"; +import { sqliteTable, text, integer, primaryKey, foreignKey } from "drizzle-orm/sqlite-core"; +import { category } from "./category"; import { hsk } from "./hsk"; import { mek } from "./mek"; import { user } from "./user"; @@ -82,6 +83,26 @@ export const fileLog = sqliteTable("file_log", { .notNull() .references(() => file.id, { onDelete: "cascade" }), timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), - action: text("action", { enum: ["create", "rename"] }).notNull(), + action: text("action", { + enum: ["create", "rename", "addToCategory", "removeFromCategory"], + }).notNull(), newName: ciphertext("new_name"), + categoryId: integer("category_id").references(() => category.id, { onDelete: "set null" }), }); + +export const fileCategory = sqliteTable( + "file_category", + { + fileId: integer("file_id") + .notNull() + .references(() => file.id, { onDelete: "cascade" }), + categoryId: integer("category_id") + .notNull() + .references(() => category.id, { onDelete: "cascade" }), + }, + (t) => ({ + pk: primaryKey({ + columns: [t.fileId, t.categoryId], + }), + }), +); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 40cb9be..13aff6b 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1,3 +1,4 @@ +export * from "./category"; export * from "./client"; export * from "./file"; export * from "./hsk"; From 2a2d01b50e2120d7c2e887ab8288171f07cdba95 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 10:57:32 +0900 Subject: [PATCH 02/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20DB=20=EC=8A=A4=ED=82=A4=EB=A7=88/=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=A5=BC=20Kysely=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/category.ts | 189 ++++++++++-------- src/lib/server/db/file.ts | 104 ++++++---- .../migrations/1737422340-AddFileCategory.ts | 65 ++++++ src/lib/server/db/migrations/index.ts | 2 + src/lib/server/db/schema/category.ts | 73 +++---- src/lib/server/db/schema/file.ts | 30 +-- src/lib/server/db/schema/index.ts | 1 + src/lib/server/db/schema/util.ts | 4 + 8 files changed, 281 insertions(+), 187 deletions(-) create mode 100644 src/lib/server/db/migrations/1737422340-AddFileCategory.ts create mode 100644 src/lib/server/db/schema/util.ts diff --git a/src/lib/server/db/category.ts b/src/lib/server/db/category.ts index db543ab..fb85322 100644 --- a/src/lib/server/db/category.ts +++ b/src/lib/server/db/category.ts @@ -1,111 +1,144 @@ -import { and, eq, isNull } from "drizzle-orm"; -import db from "./drizzle"; import { IntegrityError } from "./error"; -import { category, categoryLog, mek } from "./schema"; +import db from "./kysely"; +import type { Ciphertext } from "./schema"; type CategoryId = "root" | number; -export interface NewCategoryParams { - parentId: "root" | number; +interface Category { + id: number; + parentId: CategoryId; userId: number; mekVersion: number; encDek: string; dekVersion: Date; - encName: string; - encNameIv: string; + encName: Ciphertext; } -export const registerCategory = async (params: NewCategoryParams) => { - await db.transaction( - async (tx) => { - const meks = await tx - .select({ version: mek.version }) - .from(mek) - .where(and(eq(mek.userId, params.userId), eq(mek.state, "active"))) - .limit(1); - if (meks[0]?.version !== params.mekVersion) { - throw new IntegrityError("Inactive MEK version"); - } +export type NewCategory = Omit; - const newCategories = await tx - .insert(category) - .values({ - parentId: params.parentId === "root" ? null : params.parentId, - userId: params.userId, - mekVersion: params.mekVersion, - encDek: params.encDek, - dekVersion: params.dekVersion, - encName: { ciphertext: params.encName, iv: params.encNameIv }, - }) - .returning({ id: category.id }); - const { id: categoryId } = newCategories[0]!; - await tx.insert(categoryLog).values({ - categoryId, +export const registerCategory = async (params: NewCategory) => { + await db.transaction().execute(async (trx) => { + const mek = await trx + .selectFrom("master_encryption_key") + .select("version") + .where("user_id", "=", params.userId) + .where("state", "=", "active") + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (mek?.version !== params.mekVersion) { + throw new IntegrityError("Inactive MEK version"); + } + + const { categoryId } = await trx + .insertInto("category") + .values({ + parent_id: params.parentId !== "root" ? params.parentId : null, + user_id: params.userId, + master_encryption_key_version: params.mekVersion, + encrypted_data_encryption_key: params.encDek, + data_encryption_key_version: params.dekVersion, + encrypted_name: params.encName, + }) + .returning("id as categoryId") + .executeTakeFirstOrThrow(); + await trx + .insertInto("category_log") + .values({ + category_id: categoryId, timestamp: new Date(), action: "create", - newName: { ciphertext: params.encName, iv: params.encNameIv }, - }); - }, - { behavior: "exclusive" }, - ); + new_name: params.encName, + }) + .execute(); + }); }; export const getAllCategoriesByParent = async (userId: number, parentId: CategoryId) => { - return await db - .select() - .from(category) - .where( - and( - eq(category.userId, userId), - parentId === "root" ? isNull(category.parentId) : eq(category.parentId, parentId), - ), - ); + let query = db.selectFrom("category").selectAll().where("user_id", "=", userId); + query = + parentId === "root" + ? query.where("parent_id", "is", null) + : query.where("parent_id", "=", parentId); + const categories = await query.execute(); + return categories.map( + (category) => + ({ + id: category.id, + parentId: category.parent_id ?? "root", + userId: category.user_id, + mekVersion: category.master_encryption_key_version, + encDek: category.encrypted_data_encryption_key, + dekVersion: category.data_encryption_key_version, + encName: category.encrypted_name, + }) satisfies Category, + ); }; export const getCategory = async (userId: number, categoryId: number) => { - const res = await db - .select() - .from(category) - .where(and(eq(category.userId, userId), eq(category.id, categoryId))) - .limit(1); - return res[0] ?? null; + const category = await db + .selectFrom("category") + .selectAll() + .where("id", "=", categoryId) + .where("user_id", "=", userId) + .limit(1) + .executeTakeFirst(); + return category + ? ({ + id: category.id, + parentId: category.parent_id ?? "root", + userId: category.user_id, + mekVersion: category.master_encryption_key_version, + encDek: category.encrypted_data_encryption_key, + dekVersion: category.data_encryption_key_version, + encName: category.encrypted_name, + } satisfies Category) + : null; }; export const setCategoryEncName = async ( userId: number, categoryId: number, dekVersion: Date, - encName: string, - encNameIv: string, + encName: Ciphertext, ) => { - await db.transaction( - async (tx) => { - const categories = await tx - .select({ version: category.dekVersion }) - .from(category) - .where(and(eq(category.userId, userId), eq(category.id, categoryId))) - .limit(1); - if (!categories[0]) { - throw new IntegrityError("Category not found"); - } else if (categories[0].version.getTime() !== dekVersion.getTime()) { - throw new IntegrityError("Invalid DEK version"); - } + await db.transaction().execute(async (trx) => { + const category = await trx + .selectFrom("category") + .select("data_encryption_key_version") + .where("id", "=", categoryId) + .where("user_id", "=", userId) + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (!category) { + throw new IntegrityError("Category not found"); + } else if (category.data_encryption_key_version.getTime() !== dekVersion.getTime()) { + throw new IntegrityError("Invalid DEK version"); + } - await tx - .update(category) - .set({ encName: { ciphertext: encName, iv: encNameIv } }) - .where(and(eq(category.userId, userId), eq(category.id, categoryId))); - await tx.insert(categoryLog).values({ - categoryId, + await trx + .updateTable("category") + .set({ encrypted_name: encName }) + .where("id", "=", categoryId) + .where("user_id", "=", userId) + .execute(); + await trx + .insertInto("category_log") + .values({ + category_id: categoryId, timestamp: new Date(), action: "rename", - newName: { ciphertext: encName, iv: encNameIv }, - }); - }, - { behavior: "exclusive" }, - ); + new_name: encName, + }) + .execute(); + }); }; export const unregisterCategory = async (userId: number, categoryId: number) => { - await db.delete(category).where(and(eq(category.userId, userId), eq(category.id, categoryId))); + await db + .deleteFrom("category") + .where("id", "=", categoryId) + .where("user_id", "=", userId) + .execute(); }; diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index b372557..789c3b3 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -1,3 +1,4 @@ +import pg from "pg"; import { IntegrityError } from "./error"; import db from "./kysely"; import type { Ciphertext } from "./schema"; @@ -290,11 +291,33 @@ export const getAllFilesByParent = async (userId: number, parentId: DirectoryId) }; export const getAllFilesByCategory = async (userId: number, categoryId: number) => { - return await db - .select() - .from(file) - .innerJoin(fileCategory, eq(file.id, fileCategory.fileId)) - .where(and(eq(file.userId, userId), eq(fileCategory.categoryId, categoryId))); + const files = await db + .selectFrom("file") + .innerJoin("file_category", "file.id", "file_category.file_id") + .selectAll("file") + .where("user_id", "=", userId) + .where("category_id", "=", categoryId) + .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, + }) satisfies File, + ); }; export const getAllFileIdsByContentHmac = async ( @@ -394,44 +417,49 @@ export const unregisterFile = async (userId: number, fileId: number) => { }; export const addFileToCategory = async (fileId: number, categoryId: number) => { - await db.transaction( - async (tx) => { - try { - await tx.insert(fileCategory).values({ fileId, categoryId }); - await tx.insert(fileLog).values({ - fileId, + await db.transaction().execute(async (trx) => { + try { + await trx + .insertInto("file_category") + .values({ file_id: fileId, category_id: categoryId }) + .execute(); + await trx + .insertInto("file_log") + .values({ + file_id: fileId, timestamp: new Date(), - action: "addToCategory", - categoryId, - }); - } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") { - throw new IntegrityError("File already added to category"); - } - throw e; + action: "add-to-category", + category_id: categoryId, + }) + .execute(); + } catch (e) { + if (e instanceof pg.DatabaseError && e.code === "23505") { + throw new IntegrityError("File already added to category"); } - }, - { behavior: "exclusive" }, - ); + throw e; + } + }); }; export const removeFileFromCategory = async (fileId: number, categoryId: number) => { - await db.transaction( - async (tx) => { - const res = await tx - .delete(fileCategory) - .where(and(eq(fileCategory.fileId, fileId), eq(fileCategory.categoryId, categoryId))); - if (res.changes === 0) { - throw new IntegrityError("File not found in category"); - } + await db.transaction().execute(async (trx) => { + const res = await trx + .deleteFrom("file_category") + .where("file_id", "=", fileId) + .where("category_id", "=", categoryId) + .executeTakeFirst(); + if (res.numDeletedRows === 0n) { + throw new IntegrityError("File not found in category"); + } - await tx.insert(fileLog).values({ - fileId, + await trx + .insertInto("file_log") + .values({ + file_id: fileId, timestamp: new Date(), - action: "removeFromCategory", - categoryId, - }); - }, - { behavior: "exclusive" }, - ); + action: "remove-from-category", + category_id: categoryId, + }) + .execute(); + }); }; diff --git a/src/lib/server/db/migrations/1737422340-AddFileCategory.ts b/src/lib/server/db/migrations/1737422340-AddFileCategory.ts new file mode 100644 index 0000000..ff811e1 --- /dev/null +++ b/src/lib/server/db/migrations/1737422340-AddFileCategory.ts @@ -0,0 +1,65 @@ +import { Kysely } from "kysely"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const up = async (db: Kysely) => { + // category.ts + await db.schema + .createTable("category") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("parent_id", "integer", (col) => col.references("category.id").onDelete("cascade")) + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("master_encryption_key_version", "integer", (col) => col.notNull()) + .addColumn("encrypted_data_encryption_key", "text", (col) => col.unique().notNull()) + .addColumn("data_encryption_key_version", "timestamp(3)", (col) => col.notNull()) + .addColumn("encrypted_name", "json", (col) => col.notNull()) + .addForeignKeyConstraint( + "category_fk01", + ["user_id", "master_encryption_key_version"], + "master_encryption_key", + ["user_id", "version"], + ) + .execute(); + await db.schema + .createTable("category_log") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("category_id", "integer", (col) => + col.references("category.id").onDelete("cascade").notNull(), + ) + .addColumn("timestamp", "timestamp(3)", (col) => col.notNull()) + .addColumn("action", "text", (col) => col.notNull()) + .addColumn("new_name", "json") + .execute(); + + // file.ts + await db.schema + .alterTable("file_log") + .addColumn("category_id", "integer", (col) => + col.references("category.id").onDelete("set null"), + ) + .execute(); + await db.schema + .createTable("file_category") + .addColumn("file_id", "integer", (col) => + col.references("file.id").onDelete("cascade").notNull(), + ) + .addColumn("category_id", "integer", (col) => + col.references("category.id").onDelete("cascade").notNull(), + ) + .addPrimaryKeyConstraint("file_category_pk", ["file_id", "category_id"]) + .execute(); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const down = async (db: Kysely) => { + await db + .deleteFrom("file_log") + .where((eb) => + eb.or([eb("action", "=", "add-to-category"), eb("action", "=", "remove-from-category")]), + ) + .execute(); + + await db.schema.dropTable("file_category").execute(); + await db.schema.alterTable("file_log").dropColumn("category_id").execute(); + await db.schema.dropTable("category_log").execute(); + await db.schema.dropTable("category").execute(); +}; diff --git a/src/lib/server/db/migrations/index.ts b/src/lib/server/db/migrations/index.ts index 6caca84..aa6ee13 100644 --- a/src/lib/server/db/migrations/index.ts +++ b/src/lib/server/db/migrations/index.ts @@ -1,5 +1,7 @@ import * as Initial1737357000 from "./1737357000-Initial"; +import * as AddFileCategory1737422340 from "./1737422340-AddFileCategory"; export default { "1737357000-Initial": Initial1737357000, + "1737422340-AddFileCategory": AddFileCategory1737422340, }; diff --git a/src/lib/server/db/schema/category.ts b/src/lib/server/db/schema/category.ts index f8e1621..2304264 100644 --- a/src/lib/server/db/schema/category.ts +++ b/src/lib/server/db/schema/category.ts @@ -1,52 +1,27 @@ -import { - sqliteTable, - text, - integer, - foreignKey, - type AnySQLiteColumn, -} from "drizzle-orm/sqlite-core"; -import { mek } from "./mek"; -import { user } from "./user"; +import type { Generated } from "kysely"; +import type { Ciphertext } from "./util"; -const ciphertext = (name: string) => - text(name, { mode: "json" }).$type<{ - ciphertext: string; // Base64 - iv: string; // Base64 - }>(); +interface CategoryTable { + id: Generated; + parent_id: number | null; + user_id: number; + master_encryption_key_version: number; + encrypted_data_encryption_key: string; // Base64 + data_encryption_key_version: Date; + encrypted_name: Ciphertext; +} -export const category = sqliteTable( - "category", - { - id: integer("id").primaryKey({ autoIncrement: true }), - parentId: integer("parent_id").references((): AnySQLiteColumn => category.id, { - onDelete: "cascade", - }), - userId: integer("user_id") - .notNull() - .references(() => user.id), - mekVersion: integer("master_encryption_key_version").notNull(), - encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 - dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), - encName: ciphertext("encrypted_name").notNull(), - }, - (t) => ({ - ref1: foreignKey({ - columns: [t.parentId], - foreignColumns: [t.id], - }), - ref2: foreignKey({ - columns: [t.userId, t.mekVersion], - foreignColumns: [mek.userId, mek.version], - }), - }), -); +interface CategoryLogTable { + id: Generated; + category_id: number; + timestamp: Date; + action: "create" | "rename"; + new_name: Ciphertext | null; +} -export const categoryLog = sqliteTable("category_log", { - id: integer("id").primaryKey({ autoIncrement: true }), - categoryId: integer("category_id") - .notNull() - .references(() => category.id, { onDelete: "cascade" }), - timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), - action: text("action", { enum: ["create", "rename"] }).notNull(), - newName: ciphertext("new_name"), -}); +declare module "./index" { + interface Database { + category: CategoryTable; + category_log: CategoryLogTable; + } +} diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts index 2e13b4c..a1bf9bd 100644 --- a/src/lib/server/db/schema/file.ts +++ b/src/lib/server/db/schema/file.ts @@ -1,9 +1,5 @@ import type { ColumnType, Generated } from "kysely"; - -export type Ciphertext = { - ciphertext: string; // Base64 - iv: string; // Base64 -}; +import type { Ciphertext } from "./util"; interface DirectoryTable { id: Generated; @@ -45,26 +41,15 @@ interface FileLogTable { id: Generated; file_id: number; timestamp: ColumnType; - action: "create" | "rename"; + action: "create" | "rename" | "add-to-category" | "remove-from-category"; new_name: Ciphertext | null; + category_id: number | null; } -export const fileCategory = sqliteTable( - "file_category", - { - fileId: integer("file_id") - .notNull() - .references(() => file.id, { onDelete: "cascade" }), - categoryId: integer("category_id") - .notNull() - .references(() => category.id, { onDelete: "cascade" }), - }, - (t) => ({ - pk: primaryKey({ - columns: [t.fileId, t.categoryId], - }), - }), -); +interface FileCategoryTable { + file_id: number; + category_id: number; +} declare module "./index" { interface Database { @@ -72,5 +57,6 @@ declare module "./index" { directory_log: DirectoryLogTable; file: FileTable; file_log: FileLogTable; + file_category: FileCategoryTable; } } diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 00c72a7..d3dd9b1 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -5,6 +5,7 @@ export * from "./hsk"; export * from "./mek"; export * from "./session"; export * from "./user"; +export * from "./util"; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface Database {} diff --git a/src/lib/server/db/schema/util.ts b/src/lib/server/db/schema/util.ts new file mode 100644 index 0000000..d7f7350 --- /dev/null +++ b/src/lib/server/db/schema/util.ts @@ -0,0 +1,4 @@ +export type Ciphertext = { + ciphertext: string; // Base64 + iv: string; // Base64 +}; From f66421a5dcf3d766a91903fff329c865bc5c1150 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 11:10:16 +0900 Subject: [PATCH 03/20] =?UTF-8?q?@types/better-sqlite3=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .prettierignore | 3 --- package.json | 1 - pnpm-lock.yaml | 10 ---------- 3 files changed, 14 deletions(-) diff --git a/.prettierignore b/.prettierignore index 0f54b15..0c65201 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,8 +3,5 @@ package-lock.json pnpm-lock.yaml yarn.lock -# Output -/drizzle - # Documents *.md diff --git a/package.json b/package.json index 8185f79..93b2eed 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "@sveltejs/adapter-node": "^5.2.11", "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^4.0.4", - "@types/better-sqlite3": "^7.6.12", "@types/file-saver": "^2.0.7", "@types/ms": "^0.7.34", "@types/node-schedule": "^2.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5092a35..b8fafd8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,9 +48,6 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^4.0.4 version: 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) - '@types/better-sqlite3': - specifier: ^7.6.12 - version: 7.6.12 '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 @@ -717,9 +714,6 @@ packages: svelte: ^5.0.0-next.96 || ^5.0.0 vite: ^5.0.0 - '@types/better-sqlite3@7.6.12': - resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} - '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -2627,10 +2621,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@types/better-sqlite3@7.6.12': - dependencies: - '@types/node': 22.10.5 - '@types/cookie@0.6.0': {} '@types/estree@1.0.6': {} From 2993593770b7cb7ac7d82cd2ce76f1b4e892590e Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 14:35:34 +0900 Subject: [PATCH 04/20] =?UTF-8?q?/api/category/[id],=20/api/category/creat?= =?UTF-8?q?e=20Endpoint=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/category.ts | 2 +- src/lib/server/db/file.ts | 2 +- src/lib/server/schemas/category.ts | 28 ++++++++++++++ src/lib/server/schemas/directory.ts | 6 ++- src/lib/server/schemas/file.ts | 5 ++- src/lib/server/schemas/index.ts | 1 + src/lib/server/services/category.ts | 45 +++++++++++++++++++++++ src/lib/server/services/directory.ts | 3 +- src/routes/api/category/[id]/+server.ts | 33 +++++++++++++++++ src/routes/api/category/create/+server.ts | 23 ++++++++++++ 10 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 src/lib/server/schemas/category.ts create mode 100644 src/lib/server/services/category.ts create mode 100644 src/routes/api/category/[id]/+server.ts create mode 100644 src/routes/api/category/create/+server.ts diff --git a/src/lib/server/db/category.ts b/src/lib/server/db/category.ts index fb85322..d710b34 100644 --- a/src/lib/server/db/category.ts +++ b/src/lib/server/db/category.ts @@ -2,7 +2,7 @@ import { IntegrityError } from "./error"; import db from "./kysely"; import type { Ciphertext } from "./schema"; -type CategoryId = "root" | number; +export type CategoryId = "root" | number; interface Category { id: number; diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index 789c3b3..8b7819b 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -3,7 +3,7 @@ import { IntegrityError } from "./error"; import db from "./kysely"; import type { Ciphertext } from "./schema"; -type DirectoryId = "root" | number; +export type DirectoryId = "root" | number; interface Directory { id: number; diff --git a/src/lib/server/schemas/category.ts b/src/lib/server/schemas/category.ts new file mode 100644 index 0000000..13032b3 --- /dev/null +++ b/src/lib/server/schemas/category.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; + +export const categoryIdSchema = z.union([z.enum(["root"]), z.number().int().positive()]); + +export const categoryInfoResponse = z.object({ + metadata: z + .object({ + parent: categoryIdSchema, + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekVersion: z.string().datetime(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }) + .optional(), + subCategories: z.number().int().positive().array(), +}); +export type CategoryInfoResponse = z.infer; + +export const categoryCreateRequest = z.object({ + parent: categoryIdSchema, + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekVersion: z.string().datetime(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), +}); +export type CategoryCreateRequest = z.infer; diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index 15a5886..473d696 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -1,9 +1,11 @@ import { z } from "zod"; +export const directoryIdSchema = z.union([z.enum(["root"]), z.number().int().positive()]); + export const directoryInfoResponse = z.object({ metadata: z .object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), @@ -29,7 +31,7 @@ export const directoryRenameRequest = z.object({ export type DirectoryRenameRequest = z.infer; export const directoryCreateRequest = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 781baf2..8f552f7 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -1,8 +1,9 @@ import mime from "mime"; import { z } from "zod"; +import { directoryIdSchema } from "./directory"; export const fileInfoResponse = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), @@ -39,7 +40,7 @@ export const duplicateFileScanResponse = z.object({ export type DuplicateFileScanResponse = z.infer; export const fileUploadRequest = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index 6f8270b..1fed0d0 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -1,4 +1,5 @@ export * from "./auth"; +export * from "./category"; export * from "./client"; export * from "./directory"; export * from "./file"; diff --git a/src/lib/server/services/category.ts b/src/lib/server/services/category.ts new file mode 100644 index 0000000..5dd6c65 --- /dev/null +++ b/src/lib/server/services/category.ts @@ -0,0 +1,45 @@ +import { error } from "@sveltejs/kit"; +import { + registerCategory, + getAllCategoriesByParent, + getCategory, + type CategoryId, + type NewCategory, +} from "$lib/server/db/category"; +import { IntegrityError } from "$lib/server/db/error"; + +export const getCategoryInformation = async (userId: number, categoryId: CategoryId) => { + const category = categoryId !== "root" ? await getCategory(userId, categoryId) : undefined; + if (category === null) { + error(404, "Invalid category id"); + } + + const categories = await getAllCategoriesByParent(userId, categoryId); + return { + metadata: category && { + parentId: category.parentId ?? ("root" as const), + mekVersion: category.mekVersion, + encDek: category.encDek, + dekVersion: category.dekVersion, + encName: category.encName, + }, + categories: categories.map(({ id }) => id), + }; +}; + +export const createCategory = async (params: NewCategory) => { + const oneMinuteAgo = new Date(Date.now() - 60 * 1000); + const oneMinuteLater = new Date(Date.now() + 60 * 1000); + if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) { + error(400, "Invalid DEK version"); + } + + try { + await registerCategory(params); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Inactive MEK version") { + error(400, "Inactive MEK version"); + } + throw e; + } +}; diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts index be795b0..2525069 100644 --- a/src/lib/server/services/directory.ts +++ b/src/lib/server/services/directory.ts @@ -8,11 +8,12 @@ import { setDirectoryEncName, unregisterDirectory, getAllFilesByParent, + type DirectoryId, type NewDirectory, } from "$lib/server/db/file"; import type { Ciphertext } from "$lib/server/db/schema"; -export const getDirectoryInformation = async (userId: number, directoryId: "root" | number) => { +export const getDirectoryInformation = async (userId: number, directoryId: DirectoryId) => { const directory = directoryId !== "root" ? await getDirectory(userId, directoryId) : undefined; if (directory === null) { error(404, "Invalid directory id"); diff --git a/src/routes/api/category/[id]/+server.ts b/src/routes/api/category/[id]/+server.ts new file mode 100644 index 0000000..4a486fa --- /dev/null +++ b/src/routes/api/category/[id]/+server.ts @@ -0,0 +1,33 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryInfoResponse, type CategoryInfoResponse } from "$lib/server/schemas"; +import { getCategoryInformation } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); + + const zodRes = z + .object({ + id: z.union([z.enum(["root"]), z.coerce.number().int().positive()]), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + const { metadata, categories } = await getCategoryInformation(userId, id); + return json( + categoryInfoResponse.parse({ + metadata: metadata && { + parent: metadata.parentId, + mekVersion: metadata.mekVersion, + dek: metadata.encDek, + dekVersion: metadata.dekVersion.toISOString(), + name: metadata.encName.ciphertext, + nameIv: metadata.encName.iv, + }, + subCategories: categories, + } satisfies CategoryInfoResponse), + ); +}; diff --git a/src/routes/api/category/create/+server.ts b/src/routes/api/category/create/+server.ts new file mode 100644 index 0000000..216d850 --- /dev/null +++ b/src/routes/api/category/create/+server.ts @@ -0,0 +1,23 @@ +import { error, text } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryCreateRequest } from "$lib/server/schemas"; +import { createCategory } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const zodRes = categoryCreateRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { parent, mekVersion, dek, dekVersion, name, nameIv } = zodRes.data; + + await createCategory({ + userId, + parentId: parent, + mekVersion, + encDek: dek, + dekVersion: new Date(dekVersion), + encName: { ciphertext: name, iv: nameIv }, + }); + return text("Category created", { headers: { "Content-Type": "text/plain" } }); +}; From efe2782db05764a6be17915e0cbb57a2a449b2e5 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 16:07:23 +0900 Subject: [PATCH 05/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84=20(WiP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 아직은 하위 카테고리의 목록만 볼 수 있습니다. --- src/lib/components/TopBar.svelte | 8 +- src/lib/modules/filesystem.ts | 81 ++++++++++++++++++- src/routes/(main)/category/+page.svelte | 3 - .../(main)/category/[[id]]/+page.svelte | 61 ++++++++++++++ src/routes/(main)/category/[[id]]/+page.ts | 17 ++++ .../[[id]]/CreateCategoryModal.svelte | 30 +++++++ .../category/[[id]]/SubCategories.svelte | 41 ++++++++++ .../(main)/category/[[id]]/SubCategory.svelte | 61 ++++++++++++++ src/routes/(main)/category/[[id]]/service.ts | 28 +++++++ 9 files changed, 322 insertions(+), 8 deletions(-) delete mode 100644 src/routes/(main)/category/+page.svelte create mode 100644 src/routes/(main)/category/[[id]]/+page.svelte create mode 100644 src/routes/(main)/category/[[id]]/+page.ts create mode 100644 src/routes/(main)/category/[[id]]/CreateCategoryModal.svelte create mode 100644 src/routes/(main)/category/[[id]]/SubCategories.svelte create mode 100644 src/routes/(main)/category/[[id]]/SubCategory.svelte create mode 100644 src/routes/(main)/category/[[id]]/service.ts diff --git a/src/lib/components/TopBar.svelte b/src/lib/components/TopBar.svelte index 6691feb..9d1893f 100644 --- a/src/lib/components/TopBar.svelte +++ b/src/lib/components/TopBar.svelte @@ -7,16 +7,20 @@ children?: Snippet; onback?: () => void; title?: string; + xPadding?: boolean; } - let { children, onback, title }: Props = $props(); + let { children, onback, title, xPadding = false }: Props = $props(); const back = $derived(() => { setTimeout(onback || (() => history.back()), 100); }); -
+
diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index 1a1ff0f..54b5c10 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -12,7 +12,11 @@ import { type DirectoryId, } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas"; +import type { + CategoryInfoResponse, + DirectoryInfoResponse, + FileInfoResponse, +} from "$lib/server/schemas"; export type DirectoryInfo = | { @@ -43,8 +47,27 @@ export interface FileInfo { lastModifiedAt: Date; } +type CategoryId = "root" | number; + +export type CategoryInfo = + | { + id: "root"; + dataKey?: undefined; + dataKeyVersion?: undefined; + name?: undefined; + subCategoryIds: number[]; + } + | { + id: number; + dataKey?: CryptoKey; + dataKeyVersion?: Date; + name: string; + subCategoryIds: number[]; + }; + const directoryInfoStore = new Map>(); const fileInfoStore = new Map>(); +const categoryInfoStore = new Map>(); const fetchDirectoryInfoFromIndexedDB = async ( id: DirectoryId, @@ -124,7 +147,7 @@ export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => { directoryInfoStore.set(id, info); } - fetchDirectoryInfo(id, info, masterKey); + fetchDirectoryInfo(id, info, masterKey); // Intended return info; }; @@ -203,6 +226,58 @@ export const getFileInfo = (fileId: number, masterKey: CryptoKey) => { fileInfoStore.set(fileId, info); } - fetchFileInfo(fileId, info, masterKey); + fetchFileInfo(fileId, info, masterKey); // Intended + return info; +}; + +const fetchCategoryInfoFromServer = async ( + id: CategoryId, + info: Writable, + masterKey: CryptoKey, +) => { + const res = await callGetApi(`/api/category/${id}`); + if (res.status === 404) { + info.set(null); + return; + } else if (!res.ok) { + throw new Error("Failed to fetch category information"); + } + + const { metadata, subCategories }: CategoryInfoResponse = await res.json(); + + if (id === "root") { + info.set({ id, subCategoryIds: subCategories }); + } else { + const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); + const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); + + info.set({ + id, + dataKey, + dataKeyVersion: new Date(metadata!.dekVersion), + name, + subCategoryIds: subCategories, + }); + } +}; + +const fetchCategoryInfo = async ( + id: CategoryId, + info: Writable, + masterKey: CryptoKey, +) => { + await fetchCategoryInfoFromServer(id, info, masterKey); +}; + +export const getCategoryInfo = (categoryId: CategoryId, masterKey: CryptoKey) => { + // TODO: MEK rotation + + let info = categoryInfoStore.get(categoryId); + if (!info) { + info = writable(null); + categoryInfoStore.set(categoryId, info); + } + + fetchCategoryInfo(categoryId, info, masterKey); // Intended return info; }; diff --git a/src/routes/(main)/category/+page.svelte b/src/routes/(main)/category/+page.svelte deleted file mode 100644 index 73d68b7..0000000 --- a/src/routes/(main)/category/+page.svelte +++ /dev/null @@ -1,3 +0,0 @@ -
-

아직 개발 중이에요.

-
diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte new file mode 100644 index 0000000..91e027b --- /dev/null +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -0,0 +1,61 @@ + + + + 카테고리 + + +
+ {#if data.id !== "root"} + + {/if} + {#if $info} +
+
+ {#if data.id !== "root"} +

하위 카테고리

+ {/if} + {#key $info} + goto(`/category/${id}`)} + onCategoryCreateClick={() => { + isCreateCategoryModalOpen = true; + }} + /> + {/key} +
+ {#if data.id !== "root"} +
+

파일

+
+ {/if} +
+ {/if} +
+ + diff --git a/src/routes/(main)/category/[[id]]/+page.ts b/src/routes/(main)/category/[[id]]/+page.ts new file mode 100644 index 0000000..cfa37f8 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/+page.ts @@ -0,0 +1,17 @@ +import { error } from "@sveltejs/kit"; +import { z } from "zod"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ params }) => { + const zodRes = z + .object({ + id: z.coerce.number().int().positive().optional(), + }) + .safeParse(params); + if (!zodRes.success) error(404, "Not found"); + const { id } = zodRes.data; + + return { + id: id ? id : ("root" as const), + }; +}; diff --git a/src/routes/(main)/category/[[id]]/CreateCategoryModal.svelte b/src/routes/(main)/category/[[id]]/CreateCategoryModal.svelte new file mode 100644 index 0000000..37f868a --- /dev/null +++ b/src/routes/(main)/category/[[id]]/CreateCategoryModal.svelte @@ -0,0 +1,30 @@ + + + +

새 카테고리

+
+ +
+
+ + +
+
diff --git a/src/routes/(main)/category/[[id]]/SubCategories.svelte b/src/routes/(main)/category/[[id]]/SubCategories.svelte new file mode 100644 index 0000000..1f47fc8 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/SubCategories.svelte @@ -0,0 +1,41 @@ + + +
+ {#each subCategories as subCategory} + + {/each} + +
+ +

카테고리 추가하기

+
+
+
diff --git a/src/routes/(main)/category/[[id]]/SubCategory.svelte b/src/routes/(main)/category/[[id]]/SubCategory.svelte new file mode 100644 index 0000000..212b591 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/SubCategory.svelte @@ -0,0 +1,61 @@ + + +{#if $info} + + +
+
+
+ +
+

+ {$info.name} +

+ +
+
+{/if} + + diff --git a/src/routes/(main)/category/[[id]]/service.ts b/src/routes/(main)/category/[[id]]/service.ts new file mode 100644 index 0000000..a5d354a --- /dev/null +++ b/src/routes/(main)/category/[[id]]/service.ts @@ -0,0 +1,28 @@ +import { callPostApi } from "$lib/hooks"; +import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto"; +import type { CategoryCreateRequest } from "$lib/server/schemas"; +import type { MasterKey } from "$lib/stores"; + +export interface SelectedSubCategory { + id: number; + dataKey: CryptoKey; + dataKeyVersion: Date; + name: string; +} + +export const requestCategoryCreation = async ( + name: string, + parentId: "root" | number, + masterKey: MasterKey, +) => { + const { dataKey, dataKeyVersion } = await generateDataKey(); + const nameEncrypted = await encryptString(name, dataKey); + await callPostApi("/api/category/create", { + parent: parentId, + mekVersion: masterKey.version, + dek: await wrapDataKey(dataKey, masterKey.key), + dekVersion: dataKeyVersion.toISOString(), + name: nameEncrypted.ciphertext, + nameIv: nameEncrypted.iv, + }); +}; From 88d4757cf714958170ea8166068ab5f39431b26e Mon Sep 17 00:00:00 2001 From: static Date: Tue, 21 Jan 2025 17:32:08 +0900 Subject: [PATCH 06/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=B6=80=EB=B6=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/filesystem.ts | 13 +++- src/lib/server/schemas/category.ts | 10 +++ src/lib/server/services/category.ts | 30 +++++++++ .../(main)/category/[[id]]/+page.svelte | 6 +- src/routes/(main)/category/[[id]]/File.svelte | 61 +++++++++++++++++++ .../(main)/category/[[id]]/Files.svelte | 34 +++++++++++ src/routes/(main)/category/[[id]]/service.ts | 7 +++ .../api/category/[id]/file/add/+server.ts | 25 ++++++++ .../api/category/[id]/file/list/+server.ts | 17 ++++++ 9 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 src/routes/(main)/category/[[id]]/File.svelte create mode 100644 src/routes/(main)/category/[[id]]/Files.svelte create mode 100644 src/routes/api/category/[id]/file/add/+server.ts create mode 100644 src/routes/api/category/[id]/file/list/+server.ts diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index 54b5c10..cfafbbe 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -13,6 +13,7 @@ import { } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; import type { + CategoryFileListResponse, CategoryInfoResponse, DirectoryInfoResponse, FileInfoResponse, @@ -56,6 +57,7 @@ export type CategoryInfo = dataKeyVersion?: undefined; name?: undefined; subCategoryIds: number[]; + files?: undefined; } | { id: number; @@ -63,6 +65,7 @@ export type CategoryInfo = dataKeyVersion?: Date; name: string; subCategoryIds: number[]; + files: number[]; }; const directoryInfoStore = new Map>(); @@ -235,7 +238,7 @@ const fetchCategoryInfoFromServer = async ( info: Writable, masterKey: CryptoKey, ) => { - const res = await callGetApi(`/api/category/${id}`); + let res = await callGetApi(`/api/category/${id}`); if (res.status === 404) { info.set(null); return; @@ -251,12 +254,20 @@ const fetchCategoryInfoFromServer = async ( const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); + res = await callGetApi(`/api/category/${id}/file/list`); + if (!res.ok) { + throw new Error("Failed to fetch category files"); + } + + const { files }: CategoryFileListResponse = await res.json(); + info.set({ id, dataKey, dataKeyVersion: new Date(metadata!.dekVersion), name, subCategoryIds: subCategories, + files, }); } }; diff --git a/src/lib/server/schemas/category.ts b/src/lib/server/schemas/category.ts index 13032b3..a693bee 100644 --- a/src/lib/server/schemas/category.ts +++ b/src/lib/server/schemas/category.ts @@ -17,6 +17,16 @@ export const categoryInfoResponse = z.object({ }); export type CategoryInfoResponse = z.infer; +export const categoryFileAddRequest = z.object({ + file: z.number().int().positive(), +}); +export type CategoryFileAddRequest = z.infer; + +export const categoryFileListResponse = z.object({ + files: z.number().int().positive().array(), +}); +export type CategoryFileListResponse = z.infer; + export const categoryCreateRequest = z.object({ parent: categoryIdSchema, mekVersion: z.number().int().positive(), diff --git a/src/lib/server/services/category.ts b/src/lib/server/services/category.ts index 5dd6c65..e81e458 100644 --- a/src/lib/server/services/category.ts +++ b/src/lib/server/services/category.ts @@ -7,6 +7,7 @@ import { type NewCategory, } from "$lib/server/db/category"; import { IntegrityError } from "$lib/server/db/error"; +import { getAllFilesByCategory, getFile, addFileToCategory } from "$lib/server/db/file"; export const getCategoryInformation = async (userId: number, categoryId: CategoryId) => { const category = categoryId !== "root" ? await getCategory(userId, categoryId) : undefined; @@ -27,6 +28,35 @@ export const getCategoryInformation = async (userId: number, categoryId: Categor }; }; +export const addCategoryFile = async (userId: number, categoryId: number, fileId: number) => { + const category = await getCategory(userId, categoryId); + const file = await getFile(userId, fileId); + if (!category) { + error(404, "Invalid category id"); + } else if (!file) { + error(404, "Invalid file id"); + } + + try { + await addFileToCategory(fileId, categoryId); + } catch (e) { + if (e instanceof IntegrityError && e.message === "File already added to category") { + error(400, "File already added"); + } + throw e; + } +}; + +export const getCategoryFiles = async (userId: number, categoryId: number) => { + const category = await getCategory(userId, categoryId); + if (!category) { + error(404, "Invalid category id"); + } + + const files = await getAllFilesByCategory(userId, categoryId); + return { files: files.map(({ id }) => id) }; +}; + export const createCategory = async (params: NewCategory) => { const oneMinuteAgo = new Date(Date.now() - 60 * 1000); const oneMinuteLater = new Date(Date.now() + 60 * 1000); diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte index 91e027b..33ac0a7 100644 --- a/src/routes/(main)/category/[[id]]/+page.svelte +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -5,6 +5,7 @@ import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem"; import { masterKeyStore } from "$lib/stores"; import CreateCategoryModal from "./CreateCategoryModal.svelte"; + import Files from "./Files.svelte"; import SubCategories from "./SubCategories.svelte"; import { requestCategoryCreation } from "./service"; @@ -34,7 +35,7 @@ {/if} {#if $info} -
+
{#if data.id !== "root"}

하위 카테고리

@@ -52,6 +53,9 @@ {#if data.id !== "root"}

파일

+ {#key $info} + goto(`/file/${id}`)} /> + {/key}
{/if}
diff --git a/src/routes/(main)/category/[[id]]/File.svelte b/src/routes/(main)/category/[[id]]/File.svelte new file mode 100644 index 0000000..c3b9b97 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/File.svelte @@ -0,0 +1,61 @@ + + +{#if $info} + + +
+
+
+ +
+

+ {$info.name} +

+ +
+
+{/if} + + diff --git a/src/routes/(main)/category/[[id]]/Files.svelte b/src/routes/(main)/category/[[id]]/Files.svelte new file mode 100644 index 0000000..6679931 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/Files.svelte @@ -0,0 +1,34 @@ + + +
+ {#each files as file} + + {:else} +

이 카테고리에 추가된 파일이 없어요.

+ {/each} +
diff --git a/src/routes/(main)/category/[[id]]/service.ts b/src/routes/(main)/category/[[id]]/service.ts index a5d354a..6af11e3 100644 --- a/src/routes/(main)/category/[[id]]/service.ts +++ b/src/routes/(main)/category/[[id]]/service.ts @@ -10,6 +10,13 @@ export interface SelectedSubCategory { name: string; } +export interface SelectedFile { + id: number; + dataKey: CryptoKey; + dataKeyVersion: Date; + name: string; +} + export const requestCategoryCreation = async ( name: string, parentId: "root" | number, diff --git a/src/routes/api/category/[id]/file/add/+server.ts b/src/routes/api/category/[id]/file/add/+server.ts new file mode 100644 index 0000000..2eaf2f2 --- /dev/null +++ b/src/routes/api/category/[id]/file/add/+server.ts @@ -0,0 +1,25 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryFileAddRequest } from "$lib/server/schemas"; +import { addCategoryFile } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, params, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const paramsZodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = categoryFileAddRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { file } = bodyZodRes.data; + + await addCategoryFile(userId, id, file); + return text("File added", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/category/[id]/file/list/+server.ts b/src/routes/api/category/[id]/file/list/+server.ts new file mode 100644 index 0000000..e2aa7af --- /dev/null +++ b/src/routes/api/category/[id]/file/list/+server.ts @@ -0,0 +1,17 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryFileListResponse, type CategoryFileListResponse } from "$lib/server/schemas"; +import { getCategoryFiles } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); + + const zodRes = z.object({ id: z.coerce.number().int().positive() }).safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + const { files } = await getCategoryFiles(userId, id); + return json(categoryFileListResponse.parse({ files }) as CategoryFileListResponse); +}; From dbe2262d07c3aff9e685b1a9c04aae6ef989c8d3 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 11:28:13 +0900 Subject: [PATCH 07/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=9D=98=20=EC=A3=BC=EC=9A=94=20?= =?UTF-8?q?=EC=9A=94=EC=86=8C=EB=A5=BC=20=EB=B3=84=EB=8F=84=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- src/lib/organisms/Category/Category.svelte | 72 +++++++++++++++++++ .../organisms/Category}/File.svelte | 0 .../organisms/Category}/SubCategory.svelte | 0 src/lib/organisms/Category/index.ts | 2 + src/lib/organisms/Category/service.ts | 13 ++++ .../(main)/category/[[id]]/+page.svelte | 39 +++------- .../(main)/category/[[id]]/Files.svelte | 34 --------- .../category/[[id]]/SubCategories.svelte | 41 ----------- src/routes/(main)/category/[[id]]/service.ts | 14 ---- 10 files changed, 100 insertions(+), 118 deletions(-) create mode 100644 src/lib/organisms/Category/Category.svelte rename src/{routes/(main)/category/[[id]] => lib/organisms/Category}/File.svelte (100%) rename src/{routes/(main)/category/[[id]] => lib/organisms/Category}/SubCategory.svelte (100%) create mode 100644 src/lib/organisms/Category/index.ts create mode 100644 src/lib/organisms/Category/service.ts delete mode 100644 src/routes/(main)/category/[[id]]/Files.svelte delete mode 100644 src/routes/(main)/category/[[id]]/SubCategories.svelte diff --git a/package.json b/package.json index 93b2eed..1830e27 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,14 @@ "type": "module", "scripts": { "dev": "vite dev", - "dev:db": "docker compose -f docker-compose.dev.yaml -p arkvault-dev up -d", "build": "vite build", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", "lint": "prettier --check . && eslint .", + "db:up": "docker compose -f docker-compose.dev.yaml -p arkvault-dev up -d", + "db:down": "docker compose -f docker-compose.dev.yaml -p arkvault-dev down", "db:migrate": "kysely migrate" }, "devDependencies": { diff --git a/src/lib/organisms/Category/Category.svelte b/src/lib/organisms/Category/Category.svelte new file mode 100644 index 0000000..cfe1809 --- /dev/null +++ b/src/lib/organisms/Category/Category.svelte @@ -0,0 +1,72 @@ + + +
+
+ {#if info.id !== "root"} +

하위 카테고리

+ {/if} +
+ {#key info} + {#each subCategories as subCategory} + + {/each} + {/key} + +
+ +

카테고리 추가하기

+
+
+
+
+ {#if info.id !== "root"} +
+

파일

+
+ {#key info} + {#each files as file} + + {:else} +

이 카테고리에 추가된 파일이 없어요.

+ {/each} + {/key} +
+
+ {/if} +
diff --git a/src/routes/(main)/category/[[id]]/File.svelte b/src/lib/organisms/Category/File.svelte similarity index 100% rename from src/routes/(main)/category/[[id]]/File.svelte rename to src/lib/organisms/Category/File.svelte diff --git a/src/routes/(main)/category/[[id]]/SubCategory.svelte b/src/lib/organisms/Category/SubCategory.svelte similarity index 100% rename from src/routes/(main)/category/[[id]]/SubCategory.svelte rename to src/lib/organisms/Category/SubCategory.svelte diff --git a/src/lib/organisms/Category/index.ts b/src/lib/organisms/Category/index.ts new file mode 100644 index 0000000..51e0a58 --- /dev/null +++ b/src/lib/organisms/Category/index.ts @@ -0,0 +1,2 @@ +export { default } from "./Category.svelte"; +export * from "./service"; diff --git a/src/lib/organisms/Category/service.ts b/src/lib/organisms/Category/service.ts new file mode 100644 index 0000000..8499c88 --- /dev/null +++ b/src/lib/organisms/Category/service.ts @@ -0,0 +1,13 @@ +export interface SelectedSubCategory { + id: number; + dataKey: CryptoKey; + dataKeyVersion: Date; + name: string; +} + +export interface SelectedFile { + id: number; + dataKey: CryptoKey; + dataKeyVersion: Date; + name: string; +} diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte index 33ac0a7..aa551f1 100644 --- a/src/routes/(main)/category/[[id]]/+page.svelte +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -3,10 +3,9 @@ import { goto } from "$app/navigation"; import { TopBar } from "$lib/components"; import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem"; + import Category from "$lib/organisms/Category"; import { masterKeyStore } from "$lib/stores"; import CreateCategoryModal from "./CreateCategoryModal.svelte"; - import Files from "./Files.svelte"; - import SubCategories from "./SubCategories.svelte"; import { requestCategoryCreation } from "./service"; let { data } = $props(); @@ -34,32 +33,16 @@ {#if data.id !== "root"} {/if} - {#if $info} -
-
- {#if data.id !== "root"} -

하위 카테고리

- {/if} - {#key $info} - goto(`/category/${id}`)} - onCategoryCreateClick={() => { - isCreateCategoryModalOpen = true; - }} - /> - {/key} -
- {#if data.id !== "root"} -
-

파일

- {#key $info} - goto(`/file/${id}`)} /> - {/key} -
- {/if} -
- {/if} +
+ {#if $info} + goto(`/category/${id}`)} + onSubCategoryCreateClick={() => (isCreateCategoryModalOpen = true)} + onFileClick={({ id }) => goto(`/file/${id}`)} + /> + {/if} +
diff --git a/src/routes/(main)/category/[[id]]/Files.svelte b/src/routes/(main)/category/[[id]]/Files.svelte deleted file mode 100644 index 6679931..0000000 --- a/src/routes/(main)/category/[[id]]/Files.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -
- {#each files as file} - - {:else} -

이 카테고리에 추가된 파일이 없어요.

- {/each} -
diff --git a/src/routes/(main)/category/[[id]]/SubCategories.svelte b/src/routes/(main)/category/[[id]]/SubCategories.svelte deleted file mode 100644 index 1f47fc8..0000000 --- a/src/routes/(main)/category/[[id]]/SubCategories.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - -
- {#each subCategories as subCategory} - - {/each} - -
- -

카테고리 추가하기

-
-
-
diff --git a/src/routes/(main)/category/[[id]]/service.ts b/src/routes/(main)/category/[[id]]/service.ts index 6af11e3..c2018ed 100644 --- a/src/routes/(main)/category/[[id]]/service.ts +++ b/src/routes/(main)/category/[[id]]/service.ts @@ -3,20 +3,6 @@ import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto import type { CategoryCreateRequest } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; -export interface SelectedSubCategory { - id: number; - dataKey: CryptoKey; - dataKeyVersion: Date; - name: string; -} - -export interface SelectedFile { - id: number; - dataKey: CryptoKey; - dataKeyVersion: Date; - name: string; -} - export const requestCategoryCreation = async ( name: string, parentId: "root" | number, From a2402f37a07764c36fb44a5a4131faeaab60ecc3 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 13:22:16 +0900 Subject: [PATCH 08/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC?= =?UTF-8?q?=EC=97=90=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20BottomSheet=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(WiP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- pnpm-lock.yaml | 82 +++++++++---------- src/lib/components/BottomSheet.svelte | 2 +- .../molecules/Categories/Categories.svelte | 19 +++++ .../Categories/Category.svelte} | 4 +- src/lib/molecules/Categories/index.ts | 2 + src/lib/molecules/Categories/service.ts | 6 ++ src/lib/molecules/SubCategories.svelte | 57 +++++++++++++ src/lib/organisms/Category/Category.svelte | 24 ++---- src/lib/organisms/Category/service.ts | 7 -- .../(fullscreen)/file/[id]/+page.svelte | 15 +++- .../file/[id]/AddToCategoryBottomSheet.svelte | 42 ++++++++++ src/routes/(fullscreen)/file/[id]/service.ts | 9 ++ 13 files changed, 199 insertions(+), 72 deletions(-) create mode 100644 src/lib/molecules/Categories/Categories.svelte rename src/lib/{organisms/Category/SubCategory.svelte => molecules/Categories/Category.svelte} (93%) create mode 100644 src/lib/molecules/Categories/index.ts create mode 100644 src/lib/molecules/Categories/service.ts create mode 100644 src/lib/molecules/SubCategories.svelte create mode 100644 src/routes/(fullscreen)/file/[id]/AddToCategoryBottomSheet.svelte diff --git a/package.json b/package.json index 1830e27..9e31c9d 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.9", - "svelte": "^5.17.1", + "svelte": "^5.19.1", "svelte-check": "^4.1.3", "tailwindcss": "^3.4.17", "typescript": "^5.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8fafd8..9ed4442 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,13 +41,13 @@ importers: version: 1.2.12 '@sveltejs/adapter-node': specifier: ^5.2.11 - version: 5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))) + version: 5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))) '@sveltejs/kit': specifier: ^2.15.2 - version: 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + version: 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) '@sveltejs/vite-plugin-svelte': specifier: ^4.0.4 - version: 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + version: 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 @@ -77,7 +77,7 @@ importers: version: 9.1.0(eslint@9.17.0(jiti@2.4.2)) eslint-plugin-svelte: specifier: ^2.46.1 - version: 2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.17.1) + version: 2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.19.1) eslint-plugin-tailwindcss: specifier: ^3.17.5 version: 3.17.5(tailwindcss@3.4.17) @@ -107,16 +107,16 @@ importers: version: 3.4.2 prettier-plugin-svelte: specifier: ^3.3.2 - version: 3.3.2(prettier@3.4.2)(svelte@5.17.1) + version: 3.3.2(prettier@3.4.2)(svelte@5.19.1) prettier-plugin-tailwindcss: specifier: ^0.6.9 - version: 0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1))(prettier@3.4.2) + version: 0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1))(prettier@3.4.2) svelte: - specifier: ^5.17.1 - version: 5.17.1 + specifier: ^5.19.1 + version: 5.19.1 svelte-check: specifier: ^4.1.3 - version: 4.1.3(picomatch@4.0.2)(svelte@5.17.1)(typescript@5.7.3) + version: 4.1.3(picomatch@4.0.2)(svelte@5.19.1)(typescript@5.7.3) tailwindcss: specifier: ^3.4.17 version: 3.4.17 @@ -128,7 +128,7 @@ importers: version: 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) unplugin-icons: specifier: ^0.22.0 - version: 0.22.0(svelte@5.17.1) + version: 0.22.0(svelte@5.19.1) vite: specifier: ^5.4.11 version: 5.4.11(@types/node@22.10.5) @@ -1114,8 +1114,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@1.3.2: - resolution: {integrity: sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==} + esrap@1.4.3: + resolution: {integrity: sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -1992,8 +1992,8 @@ packages: svelte: optional: true - svelte@5.17.1: - resolution: {integrity: sha512-HitqD0XhU9OEytPuux/XYzxle4+7D8+fIb1tHbwMzOtBzDZZO+ESEuwMbahJ/3JoklfmRPB/Gzp74L87Qrxfpw==} + svelte@5.19.1: + resolution: {integrity: sha512-H/Vs2O51bwILZbaNUSdr4P1NbLpOGsxl4jJAjd88ELjzRgeRi1BHqexcVGannDr7D1pmTYWWajzHOM7bMbtB9Q==} engines: {node: '>=18'} tailwindcss@3.4.17: @@ -2573,17 +2573,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.30.1': optional: true - '@sveltejs/adapter-node@5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))': + '@sveltejs/adapter-node@5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))': dependencies: '@rollup/plugin-commonjs': 28.0.2(rollup@4.30.1) '@rollup/plugin-json': 6.1.0(rollup@4.30.1) '@rollup/plugin-node-resolve': 16.0.0(rollup@4.30.1) - '@sveltejs/kit': 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + '@sveltejs/kit': 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) rollup: 4.30.1 - '@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))': + '@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 @@ -2595,27 +2595,27 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.1 sirv: 3.0.0 - svelte: 5.17.1 + svelte: 5.19.1 tiny-glob: 0.2.9 vite: 5.4.11(@types/node@22.10.5) - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) debug: 4.4.0 - svelte: 5.17.1 + svelte: 5.19.1 vite: 5.4.11(@types/node@22.10.5) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))': + '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 - svelte: 5.17.1 + svelte: 5.19.1 vite: 5.4.11(@types/node@22.10.5) vitefu: 1.0.5(vite@5.4.11(@types/node@22.10.5)) transitivePeerDependencies: @@ -3001,7 +3001,7 @@ snapshots: dependencies: eslint: 9.17.0(jiti@2.4.2) - eslint-plugin-svelte@2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.17.1): + eslint-plugin-svelte@2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.19.1): dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2)) '@jridgewell/sourcemap-codec': 1.5.0 @@ -3014,9 +3014,9 @@ snapshots: postcss-safe-parser: 6.0.0(postcss@8.4.49) postcss-selector-parser: 6.1.2 semver: 7.6.3 - svelte-eslint-parser: 0.43.0(svelte@5.17.1) + svelte-eslint-parser: 0.43.0(svelte@5.19.1) optionalDependencies: - svelte: 5.17.1 + svelte: 5.19.1 transitivePeerDependencies: - ts-node @@ -3099,7 +3099,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@1.3.2: + esrap@1.4.3: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3683,16 +3683,16 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1): + prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1): dependencies: prettier: 3.4.2 - svelte: 5.17.1 + svelte: 5.19.1 - prettier-plugin-tailwindcss@0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1))(prettier@3.4.2): + prettier-plugin-tailwindcss@0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1))(prettier@3.4.2): dependencies: prettier: 3.4.2 optionalDependencies: - prettier-plugin-svelte: 3.3.2(prettier@3.4.2)(svelte@5.17.1) + prettier-plugin-svelte: 3.3.2(prettier@3.4.2)(svelte@5.19.1) prettier@3.4.2: {} @@ -3828,19 +3828,19 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.1.3(picomatch@4.0.2)(svelte@5.17.1)(typescript@5.7.3): + svelte-check@4.1.3(picomatch@4.0.2)(svelte@5.19.1)(typescript@5.7.3): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 4.0.3 fdir: 6.4.2(picomatch@4.0.2) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.17.1 + svelte: 5.19.1 typescript: 5.7.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@0.43.0(svelte@5.17.1): + svelte-eslint-parser@0.43.0(svelte@5.19.1): dependencies: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -3848,9 +3848,9 @@ snapshots: postcss: 8.4.49 postcss-scss: 4.0.9(postcss@8.4.49) optionalDependencies: - svelte: 5.17.1 + svelte: 5.19.1 - svelte@5.17.1: + svelte@5.19.1: dependencies: '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.5.0 @@ -3861,7 +3861,7 @@ snapshots: axobject-query: 4.1.0 clsx: 2.1.1 esm-env: 1.2.2 - esrap: 1.3.2 + esrap: 1.4.3 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.17 @@ -3957,7 +3957,7 @@ snapshots: undici-types@6.20.0: {} - unplugin-icons@0.22.0(svelte@5.17.1): + unplugin-icons@0.22.0(svelte@5.19.1): dependencies: '@antfu/install-pkg': 0.5.0 '@antfu/utils': 0.7.10 @@ -3967,7 +3967,7 @@ snapshots: local-pkg: 0.5.1 unplugin: 2.1.2 optionalDependencies: - svelte: 5.17.1 + svelte: 5.19.1 transitivePeerDependencies: - supports-color diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte index 3d3fbd6..a283957 100644 --- a/src/lib/components/BottomSheet.svelte +++ b/src/lib/components/BottomSheet.svelte @@ -28,7 +28,7 @@
e.stopPropagation()} - class="flex max-h-[70vh] min-h-[30vh] rounded-t-2xl bg-white px-4" + class="flex max-h-[70vh] min-h-[30vh] overflow-y-auto rounded-t-2xl bg-white px-4" transition:fly={{ y: 100, duration: 200 }} > {@render children?.()} diff --git a/src/lib/molecules/Categories/Categories.svelte b/src/lib/molecules/Categories/Categories.svelte new file mode 100644 index 0000000..2fdbfad --- /dev/null +++ b/src/lib/molecules/Categories/Categories.svelte @@ -0,0 +1,19 @@ + + +
+ {#each categories as category} + + {/each} +
diff --git a/src/lib/organisms/Category/SubCategory.svelte b/src/lib/molecules/Categories/Category.svelte similarity index 93% rename from src/lib/organisms/Category/SubCategory.svelte rename to src/lib/molecules/Categories/Category.svelte index 212b591..18d6a83 100644 --- a/src/lib/organisms/Category/SubCategory.svelte +++ b/src/lib/molecules/Categories/Category.svelte @@ -1,14 +1,14 @@ + +
+ {#snippet subCategoryCreate()} + +
+ +

카테고리 추가하기

+
+
+ {/snippet} + + {#if subCategoryCreatePosition === "top"} + {@render subCategoryCreate()} + {/if} + {#key info} + + {/key} + {#if subCategoryCreatePosition === "bottom"} + {@render subCategoryCreate()} + {/if} +
diff --git a/src/lib/organisms/Category/Category.svelte b/src/lib/organisms/Category/Category.svelte index cfe1809..b3cbd59 100644 --- a/src/lib/organisms/Category/Category.svelte +++ b/src/lib/organisms/Category/Category.svelte @@ -1,23 +1,21 @@ + + +
+ {#if $category} + + (category = getCategoryInfo(id, $masterKeyStore?.get(1)?.key!))} + subCategoryCreatePosition="top" + /> + {#if $category.id !== "root"} + + + + {/if} + {/if} +
+
diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index fcc5ce7..f48c16e 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,4 +1,6 @@ +import { callPostApi } from "$lib/hooks"; import { getFileCache, storeFileCache, downloadFile } from "$lib/modules/file"; +import type { CategoryFileAddRequest } from "$lib/server/schemas"; export const requestFileDownload = async ( fileId: number, @@ -12,3 +14,10 @@ export const requestFileDownload = async ( storeFileCache(fileId, fileBuffer); // Intended return fileBuffer; }; + +export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => { + const res = await callPostApi(`/api/category/${categoryId}/file/add`, { + file: fileId, + }); + return res.ok; +}; From 4c0d668cc1fcb8a11ce63364a547358e83fc0f24 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 13:50:36 +0900 Subject: [PATCH 09/20] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EB=B0=8F=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=EC=B6=94=EA=B0=80=20=EB=B2=84=ED=8A=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/filesystem.ts | 4 +- .../molecules/Categories/Categories.svelte | 12 +-- src/lib/server/db/file.ts | 9 +++ src/lib/server/schemas/file.ts | 1 + src/lib/server/services/file.ts | 3 + .../(fullscreen)/file/[id]/+page.svelte | 77 +++++++++++++------ src/routes/api/file/[id]/+server.ts | 2 + 7 files changed, 80 insertions(+), 28 deletions(-) diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index cfafbbe..f098456 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -46,6 +46,7 @@ export interface FileInfo { name: string; createdAt?: Date; lastModifiedAt: Date; + categoryIds: number[]; } type CategoryId = "root" | number; @@ -160,7 +161,7 @@ const fetchFileInfoFromIndexedDB = async (id: number, info: Writable { @@ -204,6 +205,7 @@ const fetchFileInfoFromServer = async ( name, createdAt, lastModifiedAt, + categoryIds: metadata.categories, }); await storeFileInfo({ id, diff --git a/src/lib/molecules/Categories/Categories.svelte b/src/lib/molecules/Categories/Categories.svelte index 2fdbfad..ef21e0e 100644 --- a/src/lib/molecules/Categories/Categories.svelte +++ b/src/lib/molecules/Categories/Categories.svelte @@ -12,8 +12,10 @@ let { categories, onCategoryClick }: Props = $props(); -
- {#each categories as category} - - {/each} -
+{#if categories.length > 0} +
+ {#each categories as category} + + {/each} +
+{/if} diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index 8b7819b..46c8d66 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -441,6 +441,15 @@ export const addFileToCategory = async (fileId: number, categoryId: number) => { }); }; +export const getAllFileCategories = async (fileId: number) => { + const categories = await db + .selectFrom("file_category") + .select("category_id") + .where("file_id", "=", fileId) + .execute(); + return categories.map(({ category_id }) => ({ id: category_id })); +}; + export const removeFileFromCategory = async (fileId: number, categoryId: number) => { await db.transaction().execute(async (trx) => { const res = await trx diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 8f552f7..ed0af94 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -18,6 +18,7 @@ export const fileInfoResponse = z.object({ createdAtIv: z.string().base64().nonempty().optional(), lastModifiedAt: z.string().base64().nonempty(), lastModifiedAtIv: z.string().base64().nonempty(), + categories: z.number().int().positive().array(), }); export type FileInfoResponse = z.infer; diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index 0f2d371..519bdfd 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -13,6 +13,7 @@ import { getFile, setFileEncName, unregisterFile, + getAllFileCategories, type NewFile, } from "$lib/server/db/file"; import type { Ciphertext } from "$lib/server/db/schema"; @@ -24,6 +25,7 @@ export const getFileInformation = async (userId: number, fileId: number) => { error(404, "Invalid file id"); } + const categories = await getAllFileCategories(fileId); return { parentId: file.parentId ?? ("root" as const), mekVersion: file.mekVersion, @@ -34,6 +36,7 @@ export const getFileInformation = async (userId: number, fileId: number) => { encName: file.encName, encCreatedAt: file.encCreatedAt, encLastModifiedAt: file.encLastModifiedAt, + categories: categories.map(({ id }) => id), }; }; diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index 2b27d89..89dc04d 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -2,18 +2,29 @@ import FileSaver from "file-saver"; import { untrack } from "svelte"; import { get, type Writable } from "svelte/store"; + import { goto } from "$app/navigation"; import { TopBar } from "$lib/components"; - import { getFileInfo, type FileInfo } from "$lib/modules/filesystem"; + import { EntryButton } from "$lib/components/buttons"; + import { + getFileInfo, + getCategoryInfo, + type FileInfo, + type CategoryInfo, + } from "$lib/modules/filesystem"; + import Categories from "$lib/molecules/Categories"; import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores"; import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte"; import DownloadStatus from "./DownloadStatus.svelte"; import { requestFileDownload, requestFileAdditionToCategory } from "./service"; + import IconAddCircle from "~icons/material-symbols/add-circle"; + let { data } = $props(); let info: Writable | undefined = $state(); + let categories: Writable[] = $state([]); - let isAddToCategoryBottomSheetOpen = $state(true); + let isAddToCategoryBottomSheetOpen = $state(false); const downloadStatus = $derived( $fileDownloadStatusStore.find((statusStore) => { @@ -50,6 +61,7 @@ const addToCategory = async (categoryId: number) => { await requestFileAdditionToCategory(data.id, categoryId); isAddToCategoryBottomSheetOpen = false; + info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME }; $effect(() => { @@ -58,6 +70,13 @@ viewerType = undefined; }); + $effect(() => { + categories = + $info?.categoryIds.map((id) => getCategoryInfo(id, $masterKeyStore?.get(1)?.key!)) ?? []; + + // TODO: Sorting + }); + $effect(() => { if ($info && $info.dataKey && $info.contentIv) { untrack(() => { @@ -89,28 +108,42 @@
- -
- {#snippet viewerLoading(message: string)} -
-

{message}

-
- {/snippet} +
+ + {#if $info && viewerType} +
+ {#snippet viewerLoading(message: string)} +

{message}

+ {/snippet} - {#if $info && viewerType === "image"} - {#if fileBlobUrl} - {$info.name} - {:else} - {@render viewerLoading("이미지를 불러오고 있어요.")} - {/if} - {:else if viewerType === "video"} - {#if fileBlobUrl} - - - {:else} - {@render viewerLoading("비디오를 불러오고 있어요.")} - {/if} + {#if viewerType === "image"} + {#if fileBlobUrl} + {$info.name} + {:else} + {@render viewerLoading("이미지를 불러오고 있어요.")} + {/if} + {:else if viewerType === "video"} + {#if fileBlobUrl} + + + {:else} + {@render viewerLoading("비디오를 불러오고 있어요.")} + {/if} + {/if} +
{/if} +
+

카테고리

+
+ goto(`/category/${id}`)} /> + (isAddToCategoryBottomSheetOpen = true)}> +
+ +

카테고리에 추가하기

+
+
+
+
diff --git a/src/routes/api/file/[id]/+server.ts b/src/routes/api/file/[id]/+server.ts index 892f62b..23e9385 100644 --- a/src/routes/api/file/[id]/+server.ts +++ b/src/routes/api/file/[id]/+server.ts @@ -26,6 +26,7 @@ export const GET: RequestHandler = async ({ locals, params }) => { encName, encCreatedAt, encLastModifiedAt, + categories, } = await getFileInformation(userId, id); return json( fileInfoResponse.parse({ @@ -41,6 +42,7 @@ export const GET: RequestHandler = async ({ locals, params }) => { createdAtIv: encCreatedAt?.iv, lastModifiedAt: encLastModifiedAt.ciphertext, lastModifiedAtIv: encLastModifiedAt.iv, + categories, } satisfies FileInfoResponse), ); }; From 368868910d808ba01421420ec4171d71fd96c81e Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 15:39:48 +0900 Subject: [PATCH 10/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C,=20=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../molecules/Categories/Categories.svelte | 13 +++- src/lib/molecules/Categories/Category.svelte | 30 ++++++---- src/lib/molecules/SubCategories.svelte | 14 ++++- src/lib/organisms/Category/Category.svelte | 30 ++++++---- .../organisms}/CreateCategoryModal.svelte | 0 src/lib/server/db/category.ts | 7 ++- src/lib/server/schemas/category.ts | 12 ++++ src/lib/server/services/category.ts | 60 ++++++++++++++++++- src/lib/services/category.ts | 21 +++++++ .../(fullscreen)/file/[id]/+page.svelte | 19 +++++- .../file/[id]/AddToCategoryBottomSheet.svelte | 22 ++++++- src/routes/(fullscreen)/file/[id]/service.ts | 12 +++- .../(main)/category/[[id]]/+page.svelte | 57 +++++++++++++++++- .../[[id]]/CategoryMenuBottomSheet.svelte | 57 ++++++++++++++++++ .../[[id]]/DeleteCategoryModal.svelte | 55 +++++++++++++++++ .../[[id]]/RenameCategoryModal.svelte | 47 +++++++++++++++ src/routes/(main)/category/[[id]]/service.ts | 35 +++++------ .../api/category/[id]/delete/+server.ts | 20 +++++++ .../api/category/[id]/file/remove/+server.ts | 25 ++++++++ .../api/category/[id]/rename/+server.ts | 25 ++++++++ 20 files changed, 507 insertions(+), 54 deletions(-) rename src/{routes/(main)/category/[[id]] => lib/organisms}/CreateCategoryModal.svelte (100%) create mode 100644 src/lib/services/category.ts create mode 100644 src/routes/(main)/category/[[id]]/CategoryMenuBottomSheet.svelte create mode 100644 src/routes/(main)/category/[[id]]/DeleteCategoryModal.svelte create mode 100644 src/routes/(main)/category/[[id]]/RenameCategoryModal.svelte create mode 100644 src/routes/api/category/[id]/delete/+server.ts create mode 100644 src/routes/api/category/[id]/file/remove/+server.ts create mode 100644 src/routes/api/category/[id]/rename/+server.ts diff --git a/src/lib/molecules/Categories/Categories.svelte b/src/lib/molecules/Categories/Categories.svelte index ef21e0e..0c07e2b 100644 --- a/src/lib/molecules/Categories/Categories.svelte +++ b/src/lib/molecules/Categories/Categories.svelte @@ -1,4 +1,6 @@ {#if categories.length > 0}
{#each categories as category} - + {/each}
{/if} diff --git a/src/lib/molecules/Categories/Category.svelte b/src/lib/molecules/Categories/Category.svelte index 18d6a83..ea2c392 100644 --- a/src/lib/molecules/Categories/Category.svelte +++ b/src/lib/molecules/Categories/Category.svelte @@ -1,17 +1,20 @@ @@ -40,13 +48,15 @@

{$info.name}

- + {#if MenuIcon && onMenuClick} + + {/if}
{/if} diff --git a/src/lib/molecules/SubCategories.svelte b/src/lib/molecules/SubCategories.svelte index 945e29c..7b08629 100644 --- a/src/lib/molecules/SubCategories.svelte +++ b/src/lib/molecules/SubCategories.svelte @@ -1,5 +1,6 @@ @@ -30,6 +43,7 @@ info={$category} onSubCategoryClick={({ id }) => (category = getCategoryInfo(id, $masterKeyStore?.get(1)?.key!))} + onSubCategoryCreateClick={() => (isCreateCategoryModalOpen = true)} subCategoryCreatePosition="top" /> {#if $category.id !== "root"} @@ -40,3 +54,5 @@ {/if}
+ + diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index f48c16e..e45a108 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,6 +1,8 @@ import { callPostApi } from "$lib/hooks"; import { getFileCache, storeFileCache, downloadFile } from "$lib/modules/file"; -import type { CategoryFileAddRequest } from "$lib/server/schemas"; +import type { CategoryFileAddRequest, CategoryFileRemoveRequest } from "$lib/server/schemas"; + +export { requestCategoryCreation } from "$lib/services/category"; export const requestFileDownload = async ( fileId: number, @@ -21,3 +23,11 @@ export const requestFileAdditionToCategory = async (fileId: number, categoryId: }); return res.ok; }; + +export const requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { + const res = await callPostApi( + `/api/category/${categoryId}/file/remove`, + { file: fileId }, + ); + return res.ok; +}; diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte index aa551f1..cbd0e2e 100644 --- a/src/routes/(main)/category/[[id]]/+page.svelte +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -3,16 +3,28 @@ import { goto } from "$app/navigation"; import { TopBar } from "$lib/components"; import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem"; + import type { SelectedCategory } from "$lib/molecules/Categories"; import Category from "$lib/organisms/Category"; + import CreateCategoryModal from "$lib/organisms/CreateCategoryModal.svelte"; import { masterKeyStore } from "$lib/stores"; - import CreateCategoryModal from "./CreateCategoryModal.svelte"; - import { requestCategoryCreation } from "./service"; + import CategoryMenuBottomSheet from "./CategoryMenuBottomSheet.svelte"; + import DeleteCategoryModal from "./DeleteCategoryModal.svelte"; + import RenameCategoryModal from "./RenameCategoryModal.svelte"; + import { + requestCategoryCreation, + requestCategoryRename, + requestCategoryDeletion, + } from "./service"; let { data } = $props(); let info: Writable | undefined = $state(); + let selectedSubCategory: SelectedCategory | undefined = $state(); let isCreateCategoryModalOpen = $state(false); + let isSubCategoryMenuBottomSheetOpen = $state(false); + let isRenameCategoryModalOpen = $state(false); + let isDeleteCategoryModalOpen = $state(false); const createCategory = async (name: string) => { await requestCategoryCreation(name, data.id, $masterKeyStore?.get(1)!); @@ -39,6 +51,10 @@ info={$info} onSubCategoryClick={({ id }) => goto(`/category/${id}`)} onSubCategoryCreateClick={() => (isCreateCategoryModalOpen = true)} + onSubCategoryMenuClick={(subCategory) => { + selectedSubCategory = subCategory; + isSubCategoryMenuBottomSheetOpen = true; + }} onFileClick={({ id }) => goto(`/file/${id}`)} /> {/if} @@ -46,3 +62,40 @@
+ + { + isSubCategoryMenuBottomSheetOpen = false; + isRenameCategoryModalOpen = true; + }} + onDeleteClick={() => { + isSubCategoryMenuBottomSheetOpen = false; + isDeleteCategoryModalOpen = true; + }} +/> + { + if (selectedSubCategory) { + await requestCategoryRename(selectedSubCategory, newName); + info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} +/> + { + if (selectedSubCategory) { + await requestCategoryDeletion(selectedSubCategory); + info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} +/> diff --git a/src/routes/(main)/category/[[id]]/CategoryMenuBottomSheet.svelte b/src/routes/(main)/category/[[id]]/CategoryMenuBottomSheet.svelte new file mode 100644 index 0000000..200501e --- /dev/null +++ b/src/routes/(main)/category/[[id]]/CategoryMenuBottomSheet.svelte @@ -0,0 +1,57 @@ + + + +
+ {#if selectedCategory} + {@const { name } = selectedCategory} +
+
+ +
+

+ {name} +

+
+
+ {/if} + +
+ +

이름 바꾸기

+
+
+ +
+ +

삭제하기

+
+
+
+
diff --git a/src/routes/(main)/category/[[id]]/DeleteCategoryModal.svelte b/src/routes/(main)/category/[[id]]/DeleteCategoryModal.svelte new file mode 100644 index 0000000..880cbda --- /dev/null +++ b/src/routes/(main)/category/[[id]]/DeleteCategoryModal.svelte @@ -0,0 +1,55 @@ + + + + {#if selectedCategory} + {@const { name } = selectedCategory} + {@const nameShort = name.length > 20 ? `${name.slice(0, 20)}...` : name} +
+
+

+ '{nameShort}' 카테고리를 삭제할까요? +

+

+ 모든 하위 카테고리도 함께 삭제돼요.
+ 하지만 카테고리에 추가된 파일들은 삭제되지 않아요. +

+
+
+ + +
+
+ {/if} +
diff --git a/src/routes/(main)/category/[[id]]/RenameCategoryModal.svelte b/src/routes/(main)/category/[[id]]/RenameCategoryModal.svelte new file mode 100644 index 0000000..dbb13c6 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/RenameCategoryModal.svelte @@ -0,0 +1,47 @@ + + + +

이름 바꾸기

+
+ +
+
+ + +
+
diff --git a/src/routes/(main)/category/[[id]]/service.ts b/src/routes/(main)/category/[[id]]/service.ts index c2018ed..8a5d9f8 100644 --- a/src/routes/(main)/category/[[id]]/service.ts +++ b/src/routes/(main)/category/[[id]]/service.ts @@ -1,21 +1,22 @@ import { callPostApi } from "$lib/hooks"; -import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto"; -import type { CategoryCreateRequest } from "$lib/server/schemas"; -import type { MasterKey } from "$lib/stores"; +import { encryptString } from "$lib/modules/crypto"; +import type { SelectedCategory } from "$lib/molecules/Categories"; +import type { CategoryRenameRequest } from "$lib/server/schemas"; -export const requestCategoryCreation = async ( - name: string, - parentId: "root" | number, - masterKey: MasterKey, -) => { - const { dataKey, dataKeyVersion } = await generateDataKey(); - const nameEncrypted = await encryptString(name, dataKey); - await callPostApi("/api/category/create", { - parent: parentId, - mekVersion: masterKey.version, - dek: await wrapDataKey(dataKey, masterKey.key), - dekVersion: dataKeyVersion.toISOString(), - name: nameEncrypted.ciphertext, - nameIv: nameEncrypted.iv, +export { requestCategoryCreation } from "$lib/services/category"; + +export const requestCategoryRename = async (category: SelectedCategory, newName: string) => { + const newNameEncrypted = await encryptString(newName, category.dataKey); + + const res = await callPostApi(`/api/category/${category.id}/rename`, { + dekVersion: category.dataKeyVersion.toISOString(), + name: newNameEncrypted.ciphertext, + nameIv: newNameEncrypted.iv, }); + return res.ok; +}; + +export const requestCategoryDeletion = async (category: SelectedCategory) => { + const res = await callPostApi(`/api/category/${category.id}/delete`); + return res.ok; }; diff --git a/src/routes/api/category/[id]/delete/+server.ts b/src/routes/api/category/[id]/delete/+server.ts new file mode 100644 index 0000000..cbbe356 --- /dev/null +++ b/src/routes/api/category/[id]/delete/+server.ts @@ -0,0 +1,20 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { deleteCategory } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); + + const zodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + await deleteCategory(userId, id); + return text("Category deleted", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/category/[id]/file/remove/+server.ts b/src/routes/api/category/[id]/file/remove/+server.ts new file mode 100644 index 0000000..6fdcccf --- /dev/null +++ b/src/routes/api/category/[id]/file/remove/+server.ts @@ -0,0 +1,25 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryFileRemoveRequest } from "$lib/server/schemas"; +import { removeCategoryFile } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, params, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const paramsZodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = categoryFileRemoveRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { file } = bodyZodRes.data; + + await removeCategoryFile(userId, id, file); + return text("File removed", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/category/[id]/rename/+server.ts b/src/routes/api/category/[id]/rename/+server.ts new file mode 100644 index 0000000..5351544 --- /dev/null +++ b/src/routes/api/category/[id]/rename/+server.ts @@ -0,0 +1,25 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryRenameRequest } from "$lib/server/schemas"; +import { renameCategory } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, params, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const paramsZodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = categoryRenameRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { dekVersion, name, nameIv } = bodyZodRes.data; + + await renameCategory(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv }); + return text("Category renamed", { headers: { "Content-Type": "text/plain" } }); +}; From 8f8bad6d10b20690a9aac4ed274ce10625e6e3fd Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 15:48:46 +0900 Subject: [PATCH 11/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=EB=B2=84=ED=8A=BC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/organisms/Category/Category.svelte | 4 +++- src/lib/organisms/Category/File.svelte | 24 ++++++++++++------- src/lib/services/category.ts | 10 +++++++- .../(fullscreen)/file/[id]/+page.svelte | 2 +- src/routes/(fullscreen)/file/[id]/service.ts | 12 ++-------- .../(main)/category/[[id]]/+page.svelte | 7 +++++- src/routes/(main)/category/[[id]]/service.ts | 2 +- 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/lib/organisms/Category/Category.svelte b/src/lib/organisms/Category/Category.svelte index 075a01a..d70a6d3 100644 --- a/src/lib/organisms/Category/Category.svelte +++ b/src/lib/organisms/Category/Category.svelte @@ -12,6 +12,7 @@ interface Props { info: CategoryInfo; onFileClick: (file: SelectedFile) => void; + onFileRemoveClick: (file: SelectedFile) => void; onSubCategoryClick: (subCategory: SelectedCategory) => void; onSubCategoryCreateClick: () => void; onSubCategoryMenuClick: (subCategory: SelectedCategory) => void; @@ -20,6 +21,7 @@ let { info, onFileClick, + onFileRemoveClick, onSubCategoryClick, onSubCategoryCreateClick, onSubCategoryMenuClick, @@ -53,7 +55,7 @@
{#key info} {#each files as file} - + {:else}

이 카테고리에 추가된 파일이 없어요.

{/each} diff --git a/src/lib/organisms/Category/File.svelte b/src/lib/organisms/Category/File.svelte index c3b9b97..f15f9b0 100644 --- a/src/lib/organisms/Category/File.svelte +++ b/src/lib/organisms/Category/File.svelte @@ -4,14 +4,15 @@ import type { SelectedFile } from "./service"; import IconDraft from "~icons/material-symbols/draft"; - import IconMoreVert from "~icons/material-symbols/more-vert"; + import IconClose from "~icons/material-symbols/close"; interface Props { info: Writable; onclick: (selectedFile: SelectedFile) => void; + onRemoveClick: (selectedFile: SelectedFile) => void; } - let { info, onclick }: Props = $props(); + let { info, onclick, onRemoveClick }: Props = $props(); const openFile = () => { const { id, dataKey, dataKeyVersion, name } = $info as FileInfo; @@ -22,10 +23,15 @@ }, 100); }; - const openMenu = (e: Event) => { + const removeFile = (e: Event) => { e.stopPropagation(); - // TODO + const { id, dataKey, dataKeyVersion, name } = $info as FileInfo; + if (!dataKey || !dataKeyVersion) return; // TODO: Error handling + + setTimeout(() => { + onRemoveClick({ id, dataKey, dataKeyVersion, name }); + }, 100); }; @@ -41,21 +47,21 @@ {$info.name}

{/if} diff --git a/src/lib/services/category.ts b/src/lib/services/category.ts index c2018ed..ab574f5 100644 --- a/src/lib/services/category.ts +++ b/src/lib/services/category.ts @@ -1,6 +1,6 @@ import { callPostApi } from "$lib/hooks"; import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto"; -import type { CategoryCreateRequest } from "$lib/server/schemas"; +import type { CategoryCreateRequest, CategoryFileRemoveRequest } from "$lib/server/schemas"; import type { MasterKey } from "$lib/stores"; export const requestCategoryCreation = async ( @@ -19,3 +19,11 @@ export const requestCategoryCreation = async ( nameIv: nameEncrypted.iv, }); }; + +export const requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { + const res = await callPostApi( + `/api/category/${categoryId}/file/remove`, + { file: fileId }, + ); + return res.ok; +}; diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index 7097078..902d09e 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -16,9 +16,9 @@ import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte"; import DownloadStatus from "./DownloadStatus.svelte"; import { + requestFileRemovalFromCategory, requestFileDownload, requestFileAdditionToCategory, - requestFileRemovalFromCategory, } from "./service"; import IconClose from "~icons/material-symbols/close"; diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index e45a108..43f0134 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,8 +1,8 @@ import { callPostApi } from "$lib/hooks"; import { getFileCache, storeFileCache, downloadFile } from "$lib/modules/file"; -import type { CategoryFileAddRequest, CategoryFileRemoveRequest } from "$lib/server/schemas"; +import type { CategoryFileAddRequest } from "$lib/server/schemas"; -export { requestCategoryCreation } from "$lib/services/category"; +export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category"; export const requestFileDownload = async ( fileId: number, @@ -23,11 +23,3 @@ export const requestFileAdditionToCategory = async (fileId: number, categoryId: }); return res.ok; }; - -export const requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { - const res = await callPostApi( - `/api/category/${categoryId}/file/remove`, - { file: fileId }, - ); - return res.ok; -}; diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte index cbd0e2e..163b890 100644 --- a/src/routes/(main)/category/[[id]]/+page.svelte +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -12,6 +12,7 @@ import RenameCategoryModal from "./RenameCategoryModal.svelte"; import { requestCategoryCreation, + requestFileRemovalFromCategory, requestCategoryRename, requestCategoryDeletion, } from "./service"; @@ -49,13 +50,17 @@ {#if $info} goto(`/file/${id}`)} + onFileRemoveClick={({ id }) => { + requestFileRemovalFromCategory(id, data.id as number); + info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + }} onSubCategoryClick={({ id }) => goto(`/category/${id}`)} onSubCategoryCreateClick={() => (isCreateCategoryModalOpen = true)} onSubCategoryMenuClick={(subCategory) => { selectedSubCategory = subCategory; isSubCategoryMenuBottomSheetOpen = true; }} - onFileClick={({ id }) => goto(`/file/${id}`)} /> {/if} diff --git a/src/routes/(main)/category/[[id]]/service.ts b/src/routes/(main)/category/[[id]]/service.ts index 8a5d9f8..a4ebe57 100644 --- a/src/routes/(main)/category/[[id]]/service.ts +++ b/src/routes/(main)/category/[[id]]/service.ts @@ -3,7 +3,7 @@ import { encryptString } from "$lib/modules/crypto"; import type { SelectedCategory } from "$lib/molecules/Categories"; import type { CategoryRenameRequest } from "$lib/server/schemas"; -export { requestCategoryCreation } from "$lib/services/category"; +export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category"; export const requestCategoryRename = async (category: SelectedCategory, newName: string) => { const newNameEncrypted = await encryptString(newName, category.dataKey); From f34764ffe02edd03d70967b29aa232bdc834dd69 Mon Sep 17 00:00:00 2001 From: static Date: Wed, 22 Jan 2025 22:24:44 +0900 Subject: [PATCH 12/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=97=90=EC=84=9C=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EC=9E=AC=EA=B7=80=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=91=9C=EC=8B=9C=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/inputs/CheckBox.svelte | 23 ++++++++ src/lib/components/inputs/index.ts | 1 + src/lib/modules/filesystem.ts | 6 +- src/lib/organisms/Category/Category.svelte | 15 ++++- src/lib/server/db/file.ts | 56 ++++++++++--------- src/lib/server/schemas/category.ts | 7 ++- src/lib/server/services/category.ts | 6 +- .../(main)/category/[[id]]/+page.svelte | 3 + .../api/category/[id]/file/list/+server.ts | 22 ++++++-- 9 files changed, 98 insertions(+), 41 deletions(-) create mode 100644 src/lib/components/inputs/CheckBox.svelte diff --git a/src/lib/components/inputs/CheckBox.svelte b/src/lib/components/inputs/CheckBox.svelte new file mode 100644 index 0000000..813a7e0 --- /dev/null +++ b/src/lib/components/inputs/CheckBox.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/inputs/index.ts b/src/lib/components/inputs/index.ts index c2c534d..47cb929 100644 --- a/src/lib/components/inputs/index.ts +++ b/src/lib/components/inputs/index.ts @@ -1 +1,2 @@ +export { default as CheckBox } from "./CheckBox.svelte"; export { default as TextInput } from "./TextInput.svelte"; diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index f098456..0e786ce 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -66,7 +66,7 @@ export type CategoryInfo = dataKeyVersion?: Date; name: string; subCategoryIds: number[]; - files: number[]; + files: { id: number; isRecursive: boolean }[]; }; const directoryInfoStore = new Map>(); @@ -256,7 +256,7 @@ const fetchCategoryInfoFromServer = async ( const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); - res = await callGetApi(`/api/category/${id}/file/list`); + res = await callGetApi(`/api/category/${id}/file/list?recursive=true`); if (!res.ok) { throw new Error("Failed to fetch category files"); } @@ -269,7 +269,7 @@ const fetchCategoryInfoFromServer = async ( dataKeyVersion: new Date(metadata!.dekVersion), name, subCategoryIds: subCategories, - files, + files: files.map(({ file, isRecursive }) => ({ id: file, isRecursive })), }); } }; diff --git a/src/lib/organisms/Category/Category.svelte b/src/lib/organisms/Category/Category.svelte index d70a6d3..563293d 100644 --- a/src/lib/organisms/Category/Category.svelte +++ b/src/lib/organisms/Category/Category.svelte @@ -1,5 +1,6 @@ @@ -46,13 +46,15 @@

{$info.name}

- + {#if onRemoveClick} + + {/if} {/if} From 606609d468ae67e9b3fbff79b7efa0d36b034b7e Mon Sep 17 00:00:00 2001 From: static Date: Thu, 23 Jan 2025 00:28:30 +0900 Subject: [PATCH 16/20] =?UTF-8?q?=EC=B9=B4=ED=85=8C=EA=B3=A0=EB=A6=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=9D=B4=EB=A6=84=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/util.ts | 29 ++++++++++++ .../molecules/Categories/Categories.svelte | 45 ++++++++++++++++--- src/lib/organisms/Category/Category.svelte | 38 +++++++++++++--- .../DirectoryEntries/DirectoryEntries.svelte | 2 +- .../[[id]]/DirectoryEntries/index.ts | 1 - .../[[id]]/DirectoryEntries/service.ts | 31 ------------- 6 files changed, 100 insertions(+), 46 deletions(-) delete mode 100644 src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts diff --git a/src/lib/modules/util.ts b/src/lib/modules/util.ts index 67e1b3b..0048e9e 100644 --- a/src/lib/modules/util.ts +++ b/src/lib/modules/util.ts @@ -27,3 +27,32 @@ export const formatNetworkSpeed = (speed: number) => { if (speed < 1000 * 1000 * 1000) return `${(speed / 1000 / 1000).toFixed(1)} Mbps`; return `${(speed / 1000 / 1000 / 1000).toFixed(1)} Gbps`; }; + +export enum SortBy { + NAME_ASC, + NAME_DESC, +} + +type SortFunc = (a?: string, b?: string) => number; + +const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); + +const sortByNameAsc: SortFunc = (a, b) => { + if (a && b) return collator.compare(a, b); + if (a) return -1; + if (b) return 1; + return 0; +}; + +const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b); + +export const sortEntries = (entries: T[], sortBy: SortBy) => { + let sortFunc: SortFunc; + if (sortBy === SortBy.NAME_ASC) { + sortFunc = sortByNameAsc; + } else { + sortFunc = sortByNameDesc; + } + + entries.sort((a, b) => sortFunc(a.name, b.name)); +}; diff --git a/src/lib/molecules/Categories/Categories.svelte b/src/lib/molecules/Categories/Categories.svelte index 0c07e2b..a11313e 100644 --- a/src/lib/molecules/Categories/Categories.svelte +++ b/src/lib/molecules/Categories/Categories.svelte @@ -1,8 +1,9 @@ -{#if categories.length > 0} +{#if categoriesWithName.length > 0}
- {#each categories as category} + {#each categoriesWithName as { info }} - import type { Writable } from "svelte/store"; + import { untrack } from "svelte"; + import { get, type Writable } from "svelte/store"; import { CheckBox } from "$lib/components/inputs"; import { getFileInfo, type FileInfo, type CategoryInfo } from "$lib/modules/filesystem"; + import { SortBy, sortEntries } from "$lib/modules/util"; import type { SelectedCategory } from "$lib/molecules/Categories"; import SubCategories from "$lib/molecules/SubCategories.svelte"; import { masterKeyStore } from "$lib/stores"; @@ -17,6 +19,7 @@ onSubCategoryClick: (subCategory: SelectedCategory) => void; onSubCategoryCreateClick: () => void; onSubCategoryMenuClick: (subCategory: SelectedCategory) => void; + sortBy?: SortBy; isFileRecursive: boolean; } @@ -27,21 +30,42 @@ onSubCategoryClick, onSubCategoryCreateClick, onSubCategoryMenuClick, + sortBy = SortBy.NAME_ASC, isFileRecursive = $bindable(), }: Props = $props(); - let files: { info: Writable; isRecursive: boolean }[] = $state([]); + let files: { name?: string; info: Writable; isRecursive: boolean }[] = $state( + [], + ); $effect(() => { files = info.files ?.filter(({ isRecursive }) => isFileRecursive || !isRecursive) - .map(({ id, isRecursive }) => ({ - info: getFileInfo(id, $masterKeyStore?.get(1)?.key!), - isRecursive, - })) ?? []; + .map(({ id, isRecursive }) => { + const info = getFileInfo(id, $masterKeyStore?.get(1)?.key!); + return { + name: get(info)?.name, + info, + isRecursive, + }; + }) ?? []; - // TODO: Sorting + const sort = () => { + sortEntries(files, sortBy); + }; + return untrack(() => { + sort(); + + const unsubscribes = files.map((file) => + file.info.subscribe((value) => { + if (file.name === value?.name) return; + file.name = value?.name; + sort(); + }), + ); + return () => unsubscribes.forEach((unsubscribe) => unsubscribe()); + }); }); diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte index e482e38..baa9760 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte @@ -7,6 +7,7 @@ type DirectoryInfo, type FileInfo, } from "$lib/modules/filesystem"; + import { SortBy, sortEntries } from "$lib/modules/util"; import { fileUploadStatusStore, isFileUploading, @@ -15,7 +16,6 @@ } from "$lib/stores"; import File from "./File.svelte"; import SubDirectory from "./SubDirectory.svelte"; - import { SortBy, sortEntries } from "./service"; import UploadingFile from "./UploadingFile.svelte"; import type { SelectedDirectoryEntry } from "../service"; diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts b/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts index 72ab278..075644e 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts @@ -1,2 +1 @@ export { default } from "./DirectoryEntries.svelte"; -export * from "./service"; diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts b/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts deleted file mode 100644 index b797727..0000000 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts +++ /dev/null @@ -1,31 +0,0 @@ -export enum SortBy { - NAME_ASC, - NAME_DESC, -} - -type SortFunc = (a?: string, b?: string) => number; - -const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); - -const sortByNameAsc: SortFunc = (a, b) => { - if (a && b) return collator.compare(a, b); - if (a) return -1; - if (b) return 1; - return 0; -}; - -const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b); - -export const sortEntries = ( - entries: T[], - sortBy: SortBy = SortBy.NAME_ASC, -) => { - let sortFunc: SortFunc; - if (sortBy === SortBy.NAME_ASC) { - sortFunc = sortByNameAsc; - } else { - sortFunc = sortByNameDesc; - } - - entries.sort((a, b) => sortFunc(a.name, b.name)); -}; From b8b87877d2a3dc5ccf2e44a0bbe9b3e8be857756 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 23 Jan 2025 00:32:44 +0900 Subject: [PATCH 17/20] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=EC=84=9C=20=EB=B7=B0=EC=96=B4=20=EB=A1=9C?= =?UTF-8?q?=EB=94=A9=20=EB=A9=94=EC=84=B8=EC=A7=80=EB=A5=BC=20=EB=8D=94=20?= =?UTF-8?q?=EB=B9=A0=EB=A5=B4=EA=B2=8C=20=ED=91=9C=EC=8B=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(fullscreen)/file/[id]/+page.svelte | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/routes/(fullscreen)/file/[id]/+page.svelte b/src/routes/(fullscreen)/file/[id]/+page.svelte index 902d09e..b86cc52 100644 --- a/src/routes/(fullscreen)/file/[id]/+page.svelte +++ b/src/routes/(fullscreen)/file/[id]/+page.svelte @@ -42,14 +42,7 @@ let viewerType: "image" | "video" | undefined = $state(); let fileBlobUrl: string | undefined = $state(); - const updateViewer = async (info: FileInfo, buffer: ArrayBuffer) => { - const contentType = info.contentType; - if (contentType.startsWith("image")) { - viewerType = "image"; - } else if (contentType.startsWith("video")) { - viewerType = "video"; - } - + const updateViewer = async (buffer: ArrayBuffer, contentType: string) => { const fileBlob = new Blob([buffer], { type: contentType }); if (contentType === "image/heic") { const { default: heic2any } = await import("heic2any"); @@ -89,11 +82,18 @@ $effect(() => { if ($info && $info.dataKey && $info.contentIv) { + const contentType = $info.contentType; + if (contentType.startsWith("image")) { + viewerType = "image"; + } else if (contentType.startsWith("video")) { + viewerType = "video"; + } + untrack(() => { if (!downloadStatus && !isDownloadRequested) { isDownloadRequested = true; requestFileDownload(data.id, $info.contentIv!, $info.dataKey!).then(async (buffer) => { - const blob = await updateViewer($info, buffer); + const blob = await updateViewer(buffer, contentType); if (!viewerType) { FileSaver.saveAs(blob, $info.name); } @@ -105,7 +105,9 @@ $effect(() => { if ($info && $downloadStatus?.status === "decrypted") { - untrack(() => !isDownloadRequested && updateViewer($info, $downloadStatus.result!)); + untrack( + () => !isDownloadRequested && updateViewer($downloadStatus.result!, $info.contentType), + ); } }); From ca67f5a81c48bc2d143905e45eb72eb26298be4f Mon Sep 17 00:00:00 2001 From: static Date: Thu, 23 Jan 2025 12:57:37 +0900 Subject: [PATCH 18/20] =?UTF-8?q?/api/category/[id]/file/list=20Endpoint?= =?UTF-8?q?=EC=97=90=EC=84=9C,=20recursive=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EC=9D=98=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20false=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=B4?= =?UTF-8?q?=EB=8F=84=20=EC=9E=AC=EA=B7=80=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=B2=80=EC=83=89=EB=90=98=EB=8D=98=20=EB=B2=84=EA=B7=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/filesystem.ts | 2 +- src/lib/server/db/file.ts | 4 ++-- src/lib/server/services/category.ts | 4 ++-- src/routes/api/category/[id]/file/list/+server.ts | 13 +++++++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index dc1208e..ce2c35a 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -281,7 +281,7 @@ const fetchCategoryInfoFromServer = async ( const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); - res = await callGetApi(`/api/category/${id}/file/list?recursive=true`); + res = await callGetApi(`/api/category/${id}/file/list?recurse=true`); if (!res.ok) { throw new Error("Failed to fetch category files"); } diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index 6a53c5b..219dc42 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -294,7 +294,7 @@ export const getAllFilesByParent = async (userId: number, parentId: DirectoryId) export const getAllFilesByCategory = async ( userId: number, categoryId: number, - recursive: boolean, + recurse: boolean, ) => { const files = await db .withRecursive("cte", (db) => @@ -304,7 +304,7 @@ export const getAllFilesByCategory = async ( .select(["id", "parent_id", "user_id", "file_category.file_id"]) .select(sql`0`.as("depth")) .where("id", "=", categoryId) - .$if(recursive, (qb) => + .$if(recurse, (qb) => qb.unionAll((db) => db .selectFrom("category") diff --git a/src/lib/server/services/category.ts b/src/lib/server/services/category.ts index 0d03696..cb3db7a 100644 --- a/src/lib/server/services/category.ts +++ b/src/lib/server/services/category.ts @@ -66,13 +66,13 @@ export const addCategoryFile = async (userId: number, categoryId: number, fileId } }; -export const getCategoryFiles = async (userId: number, categoryId: number, recursive: boolean) => { +export const getCategoryFiles = async (userId: number, categoryId: number, recurse: boolean) => { const category = await getCategory(userId, categoryId); if (!category) { error(404, "Invalid category id"); } - const files = await getAllFilesByCategory(userId, categoryId, recursive); + const files = await getAllFilesByCategory(userId, categoryId, recurse); return { files }; }; diff --git a/src/routes/api/category/[id]/file/list/+server.ts b/src/routes/api/category/[id]/file/list/+server.ts index db55ba5..5d80474 100644 --- a/src/routes/api/category/[id]/file/list/+server.ts +++ b/src/routes/api/category/[id]/file/list/+server.ts @@ -13,12 +13,17 @@ export const GET: RequestHandler = async ({ locals, url, params }) => { const { id } = paramsZodRes.data; const queryZodRes = z - .object({ recursive: z.coerce.boolean().nullable() }) - .safeParse({ recursive: url.searchParams.get("recursive") }); + .object({ + recurse: z + .enum(["true", "false"]) + .transform((value) => value === "true") + .nullable(), + }) + .safeParse({ recurse: url.searchParams.get("recurse") }); if (!queryZodRes.success) error(400, "Invalid query parameters"); - const { recursive } = queryZodRes.data; + const { recurse } = queryZodRes.data; - const { files } = await getCategoryFiles(userId, id, recursive ?? false); + const { files } = await getCategoryFiles(userId, id, recurse ?? false); return json( categoryFileListResponse.parse({ files: files.map(({ id, isRecursive }) => ({ file: id, isRecursive })), From fd10f13a4d0e7a2c5891189e493a13c95c2a08d1 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 23 Jan 2025 13:21:34 +0900 Subject: [PATCH 19/20] =?UTF-8?q?=EC=82=AC=EC=86=8C=ED=95=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/inputs/CheckBox.svelte | 2 +- src/lib/modules/filesystem.ts | 2 +- src/lib/molecules/SubCategories.svelte | 2 -- src/routes/(fullscreen)/file/[id]/+page.svelte | 2 -- .../(main)/category/[[id]]/DeleteCategoryModal.svelte | 9 +-------- .../directory/[[id]]/DeleteDirectoryEntryModal.svelte | 9 +-------- src/routes/api/category/[id]/file/list/+server.ts | 8 ++++++-- 7 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/lib/components/inputs/CheckBox.svelte b/src/lib/components/inputs/CheckBox.svelte index 813a7e0..6875040 100644 --- a/src/lib/components/inputs/CheckBox.svelte +++ b/src/lib/components/inputs/CheckBox.svelte @@ -12,7 +12,7 @@ let { children, checked = $bindable(false) }: Props = $props(); -
- +
diff --git a/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte b/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte index 07fb6dd..16dce02 100644 --- a/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte +++ b/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte @@ -48,14 +48,7 @@

- +
diff --git a/src/routes/api/category/[id]/file/list/+server.ts b/src/routes/api/category/[id]/file/list/+server.ts index 5d80474..c93c963 100644 --- a/src/routes/api/category/[id]/file/list/+server.ts +++ b/src/routes/api/category/[id]/file/list/+server.ts @@ -8,7 +8,11 @@ import type { RequestHandler } from "./$types"; export const GET: RequestHandler = async ({ locals, url, params }) => { const { userId } = await authorize(locals, "activeClient"); - const paramsZodRes = z.object({ id: z.coerce.number().int().positive() }).safeParse(params); + const paramsZodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); if (!paramsZodRes.success) error(400, "Invalid path parameters"); const { id } = paramsZodRes.data; @@ -27,6 +31,6 @@ export const GET: RequestHandler = async ({ locals, url, params }) => { return json( categoryFileListResponse.parse({ files: files.map(({ id, isRecursive }) => ({ file: id, isRecursive })), - }) as CategoryFileListResponse, + }) satisfies CategoryFileListResponse, ); }; From 3ee98166c6d8efb87b6d1c10161dd9512f531800 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 23 Jan 2025 13:30:50 +0900 Subject: [PATCH 20/20] =?UTF-8?q?=EC=A2=85=EC=A2=85=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=EC=97=90=EC=84=9C=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=9D=84=20=EC=82=AD=EC=A0=9C=ED=96=88=EC=9D=8C=EC=97=90?= =?UTF-8?q?=EB=8F=84=20=EC=82=AD=EC=A0=9C=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EC=9D=80=20=EA=B2=83=EC=9C=BC=EB=A1=9C=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=90=98=EB=8D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/(main)/category/[[id]]/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte index d83ded2..587390e 100644 --- a/src/routes/(main)/category/[[id]]/+page.svelte +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -54,8 +54,8 @@ bind:isFileRecursive info={$info} onFileClick={({ id }) => goto(`/file/${id}`)} - onFileRemoveClick={({ id }) => { - requestFileRemovalFromCategory(id, data.id as number); + onFileRemoveClick={async ({ id }) => { + await requestFileRemovalFromCategory(id, data.id as number); info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME }} onSubCategoryClick={({ id }) => goto(`/category/${id}`)}