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}
+
+
+
+{/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}
+
+
+
+{/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)}
-
- {/snippet}
+
+
+ {#if $info && viewerType}
+
+ {#snippet viewerLoading(message: string)}
+
{message}
+ {/snippet}
- {#if $info && viewerType === "image"}
- {#if fileBlobUrl}
-

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

+ {: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}
+
+
+ {/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}`)}