mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 08:06:56 +00:00
즐겨찾기 기능 구현
This commit is contained in:
@@ -8,10 +8,14 @@ type IntegrityErrorMessages =
|
||||
| "User client already exists"
|
||||
// File
|
||||
| "Directory not found"
|
||||
| "Directory already favorited"
|
||||
| "Directory not favorited"
|
||||
| "File not found"
|
||||
| "File is not legacy"
|
||||
| "File not found in category"
|
||||
| "File already added to category"
|
||||
| "File already favorited"
|
||||
| "File not favorited"
|
||||
| "Invalid DEK version"
|
||||
// HSK
|
||||
| "HSK already registered"
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Directory {
|
||||
encDek: string;
|
||||
dekVersion: Date;
|
||||
encName: Ciphertext;
|
||||
isFavorite: boolean;
|
||||
}
|
||||
|
||||
interface File {
|
||||
@@ -31,6 +32,7 @@ interface File {
|
||||
encName: Ciphertext;
|
||||
encCreatedAt: Ciphertext | null;
|
||||
encLastModifiedAt: Ciphertext;
|
||||
isFavorite: boolean;
|
||||
}
|
||||
|
||||
interface FileCategory {
|
||||
@@ -42,7 +44,7 @@ interface FileCategory {
|
||||
encName: Ciphertext;
|
||||
}
|
||||
|
||||
export const registerDirectory = async (params: Omit<Directory, "id">) => {
|
||||
export const registerDirectory = async (params: Omit<Directory, "id" | "isFavorite">) => {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
const mek = await trx
|
||||
.selectFrom("master_encryption_key")
|
||||
@@ -97,6 +99,7 @@ export const getAllDirectoriesByParent = async (userId: number, parentId: Direct
|
||||
encDek: directory.encrypted_data_encryption_key,
|
||||
dekVersion: directory.data_encryption_key_version,
|
||||
encName: directory.encrypted_name,
|
||||
isFavorite: directory.is_favorite,
|
||||
}) satisfies Directory,
|
||||
);
|
||||
};
|
||||
@@ -130,6 +133,7 @@ export const getAllRecursiveDirectoriesByParent = async (userId: number, parentI
|
||||
encDek: directory.encrypted_data_encryption_key,
|
||||
dekVersion: directory.data_encryption_key_version,
|
||||
encName: directory.encrypted_name,
|
||||
isFavorite: directory.is_favorite,
|
||||
}) satisfies Directory,
|
||||
);
|
||||
};
|
||||
@@ -151,6 +155,7 @@ export const getDirectory = async (userId: number, directoryId: number) => {
|
||||
encDek: directory.encrypted_data_encryption_key,
|
||||
dekVersion: directory.data_encryption_key_version,
|
||||
encName: directory.encrypted_name,
|
||||
isFavorite: directory.is_favorite,
|
||||
} satisfies Directory)
|
||||
: null;
|
||||
};
|
||||
@@ -243,7 +248,7 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
|
||||
});
|
||||
};
|
||||
|
||||
export const registerFile = async (trx: typeof db, params: Omit<File, "id">) => {
|
||||
export const registerFile = async (trx: typeof db, params: Omit<File, "id" | "isFavorite">) => {
|
||||
if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) {
|
||||
throw new Error("Invalid arguments");
|
||||
}
|
||||
@@ -305,6 +310,7 @@ export const getAllFilesByParent = async (userId: number, parentId: DirectoryId)
|
||||
encName: file.encrypted_name,
|
||||
encCreatedAt: file.encrypted_created_at,
|
||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||
isFavorite: file.is_favorite,
|
||||
}) satisfies File,
|
||||
);
|
||||
};
|
||||
@@ -357,6 +363,7 @@ export const getAllFilesByCategory = async (
|
||||
encName: file.encrypted_name,
|
||||
encCreatedAt: file.encrypted_created_at,
|
||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||
isFavorite: file.is_favorite,
|
||||
isRecursive: file.depth > 0,
|
||||
}) satisfies File & { isRecursive: boolean },
|
||||
);
|
||||
@@ -393,6 +400,7 @@ export const getLegacyFiles = async (userId: number, limit: number = 100) => {
|
||||
encName: file.encrypted_name,
|
||||
encCreatedAt: file.encrypted_created_at,
|
||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||
isFavorite: file.is_favorite,
|
||||
}) satisfies File,
|
||||
);
|
||||
};
|
||||
@@ -436,6 +444,7 @@ export const getFilesWithoutThumbnail = async (userId: number, limit: number = 1
|
||||
encName: file.encrypted_name,
|
||||
encCreatedAt: file.encrypted_created_at,
|
||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||
isFavorite: file.is_favorite,
|
||||
}) satisfies File,
|
||||
);
|
||||
};
|
||||
@@ -480,6 +489,7 @@ export const getFile = async (userId: number, fileId: number) => {
|
||||
encName: file.encrypted_name,
|
||||
encCreatedAt: file.encrypted_created_at,
|
||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||
isFavorite: file.is_favorite,
|
||||
} satisfies File)
|
||||
: null;
|
||||
};
|
||||
@@ -518,6 +528,7 @@ export const getFilesWithCategories = async (userId: number, fileIds: number[])
|
||||
encName: file.encrypted_name,
|
||||
encCreatedAt: file.encrypted_created_at,
|
||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||
isFavorite: file.is_favorite,
|
||||
categories: file.categories.map((category) => ({
|
||||
id: category.id,
|
||||
parentId: category.parent_id ?? "root",
|
||||
@@ -803,3 +814,127 @@ export const removeFileFromCategory = async (fileId: number, categoryId: number)
|
||||
.execute();
|
||||
});
|
||||
};
|
||||
|
||||
export const setFileFavorite = async (userId: number, fileId: number, isFavorite: boolean) => {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
const file = await trx
|
||||
.selectFrom("file")
|
||||
.select("is_favorite")
|
||||
.where("id", "=", fileId)
|
||||
.where("user_id", "=", userId)
|
||||
.limit(1)
|
||||
.forUpdate()
|
||||
.executeTakeFirst();
|
||||
if (!file) {
|
||||
throw new IntegrityError("File not found");
|
||||
} else if (file.is_favorite === isFavorite) {
|
||||
throw new IntegrityError(isFavorite ? "File already favorited" : "File not favorited");
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("file")
|
||||
.set({ is_favorite: isFavorite })
|
||||
.where("id", "=", fileId)
|
||||
.where("user_id", "=", userId)
|
||||
.execute();
|
||||
await trx
|
||||
.insertInto("file_log")
|
||||
.values({
|
||||
file_id: fileId,
|
||||
timestamp: new Date(),
|
||||
action: isFavorite ? "add-to-favorites" : "remove-from-favorites",
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
};
|
||||
|
||||
export const setDirectoryFavorite = async (
|
||||
userId: number,
|
||||
directoryId: number,
|
||||
isFavorite: boolean,
|
||||
) => {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
const directory = await trx
|
||||
.selectFrom("directory")
|
||||
.select("is_favorite")
|
||||
.where("id", "=", directoryId)
|
||||
.where("user_id", "=", userId)
|
||||
.limit(1)
|
||||
.forUpdate()
|
||||
.executeTakeFirst();
|
||||
if (!directory) {
|
||||
throw new IntegrityError("Directory not found");
|
||||
} else if (directory.is_favorite === isFavorite) {
|
||||
throw new IntegrityError(
|
||||
isFavorite ? "Directory already favorited" : "Directory not favorited",
|
||||
);
|
||||
}
|
||||
|
||||
await trx
|
||||
.updateTable("directory")
|
||||
.set({ is_favorite: isFavorite })
|
||||
.where("id", "=", directoryId)
|
||||
.where("user_id", "=", userId)
|
||||
.execute();
|
||||
await trx
|
||||
.insertInto("directory_log")
|
||||
.values({
|
||||
directory_id: directoryId,
|
||||
timestamp: new Date(),
|
||||
action: isFavorite ? "add-to-favorites" : "remove-from-favorites",
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllFavoriteFiles = async (userId: number) => {
|
||||
const files = await db
|
||||
.selectFrom("file")
|
||||
.selectAll()
|
||||
.where("user_id", "=", userId)
|
||||
.where("is_favorite", "=", true)
|
||||
.execute();
|
||||
return files.map(
|
||||
(file) =>
|
||||
({
|
||||
id: file.id,
|
||||
parentId: file.parent_id ?? "root",
|
||||
userId: file.user_id,
|
||||
path: file.path,
|
||||
mekVersion: file.master_encryption_key_version,
|
||||
encDek: file.encrypted_data_encryption_key,
|
||||
dekVersion: file.data_encryption_key_version,
|
||||
hskVersion: file.hmac_secret_key_version,
|
||||
contentHmac: file.content_hmac,
|
||||
contentType: file.content_type,
|
||||
encContentIv: file.encrypted_content_iv,
|
||||
encContentHash: file.encrypted_content_hash,
|
||||
encName: file.encrypted_name,
|
||||
encCreatedAt: file.encrypted_created_at,
|
||||
encLastModifiedAt: file.encrypted_last_modified_at,
|
||||
isFavorite: file.is_favorite,
|
||||
}) satisfies File,
|
||||
);
|
||||
};
|
||||
|
||||
export const getAllFavoriteDirectories = async (userId: number) => {
|
||||
const directories = await db
|
||||
.selectFrom("directory")
|
||||
.selectAll()
|
||||
.where("user_id", "=", userId)
|
||||
.where("is_favorite", "=", true)
|
||||
.execute();
|
||||
return directories.map(
|
||||
(directory) =>
|
||||
({
|
||||
id: directory.id,
|
||||
parentId: directory.parent_id ?? "root",
|
||||
userId: directory.user_id,
|
||||
mekVersion: directory.master_encryption_key_version,
|
||||
encDek: directory.encrypted_data_encryption_key,
|
||||
dekVersion: directory.data_encryption_key_version,
|
||||
encName: directory.encrypted_name,
|
||||
isFavorite: directory.is_favorite,
|
||||
}) satisfies Directory,
|
||||
);
|
||||
};
|
||||
|
||||
29
src/lib/server/db/migrations/1768643000-AddFavorites.ts
Normal file
29
src/lib/server/db/migrations/1768643000-AddFavorites.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Kysely, sql } from "kysely";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const up = async (db: Kysely<any>) => {
|
||||
// file.ts
|
||||
await db.schema
|
||||
.alterTable("directory")
|
||||
.addColumn("is_favorite", "boolean", (col) => col.notNull().defaultTo(false))
|
||||
.execute();
|
||||
await db.schema
|
||||
.alterTable("file")
|
||||
.addColumn("is_favorite", "boolean", (col) => col.notNull().defaultTo(false))
|
||||
.execute();
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const down = async (db: Kysely<any>) => {
|
||||
await db
|
||||
.deleteFrom("file_log")
|
||||
.where("action", "in", ["add-to-favorites", "remove-from-favorites"])
|
||||
.execute();
|
||||
await db
|
||||
.deleteFrom("directory_log")
|
||||
.where("action", "in", ["add-to-favorites", "remove-from-favorites"])
|
||||
.execute();
|
||||
|
||||
await db.schema.alterTable("file").dropColumn("is_favorite").execute();
|
||||
await db.schema.alterTable("directory").dropColumn("is_favorite").execute();
|
||||
};
|
||||
@@ -2,10 +2,12 @@ import * as Initial1737357000 from "./1737357000-Initial";
|
||||
import * as AddFileCategory1737422340 from "./1737422340-AddFileCategory";
|
||||
import * as AddThumbnail1738409340 from "./1738409340-AddThumbnail";
|
||||
import * as AddChunkedUpload1768062380 from "./1768062380-AddChunkedUpload";
|
||||
import * as AddFavorites1768643000 from "./1768643000-AddFavorites";
|
||||
|
||||
export default {
|
||||
"1737357000-Initial": Initial1737357000,
|
||||
"1737422340-AddFileCategory": AddFileCategory1737422340,
|
||||
"1738409340-AddThumbnail": AddThumbnail1738409340,
|
||||
"1768062380-AddChunkedUpload": AddChunkedUpload1768062380,
|
||||
"1768643000-AddFavorites": AddFavorites1768643000,
|
||||
};
|
||||
|
||||
@@ -9,13 +9,14 @@ interface DirectoryTable {
|
||||
encrypted_data_encryption_key: string; // Base64
|
||||
data_encryption_key_version: Date;
|
||||
encrypted_name: Ciphertext;
|
||||
is_favorite: Generated<boolean>;
|
||||
}
|
||||
|
||||
interface DirectoryLogTable {
|
||||
id: Generated<number>;
|
||||
directory_id: number;
|
||||
timestamp: ColumnType<Date, Date, never>;
|
||||
action: "create" | "rename";
|
||||
action: "create" | "rename" | "add-to-favorites" | "remove-from-favorites";
|
||||
new_name: Ciphertext | null;
|
||||
}
|
||||
|
||||
@@ -35,13 +36,21 @@ interface FileTable {
|
||||
encrypted_name: Ciphertext;
|
||||
encrypted_created_at: Ciphertext | null;
|
||||
encrypted_last_modified_at: Ciphertext;
|
||||
is_favorite: Generated<boolean>;
|
||||
}
|
||||
|
||||
interface FileLogTable {
|
||||
id: Generated<number>;
|
||||
file_id: number;
|
||||
timestamp: ColumnType<Date, Date, never>;
|
||||
action: "create" | "rename" | "migrate" | "add-to-category" | "remove-from-category";
|
||||
action:
|
||||
| "create"
|
||||
| "rename"
|
||||
| "migrate"
|
||||
| "add-to-category"
|
||||
| "remove-from-category"
|
||||
| "add-to-favorites"
|
||||
| "remove-from-favorites";
|
||||
new_name: Ciphertext | null;
|
||||
category_id: number | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user