mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-02-04 16:16:55 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66c3f2df71 | ||
|
|
385404ece2 | ||
|
|
9635d2a51b | ||
|
|
3b0cfd5a92 | ||
|
|
2f6d35c335 |
@@ -13,6 +13,7 @@ node_modules
|
|||||||
/library
|
/library
|
||||||
/thumbnails
|
/thumbnails
|
||||||
/uploads
|
/uploads
|
||||||
|
/log
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@ node_modules
|
|||||||
/library
|
/library
|
||||||
/thumbnails
|
/thumbnails
|
||||||
/uploads
|
/uploads
|
||||||
|
/log
|
||||||
|
|
||||||
# OS
|
# OS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ services:
|
|||||||
- ./data/library:/app/data/library
|
- ./data/library:/app/data/library
|
||||||
- ./data/thumbnails:/app/data/thumbnails
|
- ./data/thumbnails:/app/data/thumbnails
|
||||||
- ./data/uploads:/app/data/uploads
|
- ./data/uploads:/app/data/uploads
|
||||||
|
- ./data/log:/app/data/log
|
||||||
environment:
|
environment:
|
||||||
# ArkVault
|
# ArkVault
|
||||||
- DATABASE_HOST=database
|
- DATABASE_HOST=database
|
||||||
@@ -22,6 +23,7 @@ services:
|
|||||||
- LIBRARY_PATH=/app/data/library
|
- LIBRARY_PATH=/app/data/library
|
||||||
- THUMBNAILS_PATH=/app/data/thumbnails
|
- THUMBNAILS_PATH=/app/data/thumbnails
|
||||||
- UPLOADS_PATH=/app/data/uploads
|
- UPLOADS_PATH=/app/data/uploads
|
||||||
|
- LOG_DIR=/app/data/log
|
||||||
# SvelteKit
|
# SvelteKit
|
||||||
- ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For}
|
- ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For}
|
||||||
- XFF_DEPTH=${TRUST_PROXY:-}
|
- XFF_DEPTH=${TRUST_PROXY:-}
|
||||||
|
|||||||
@@ -4,3 +4,6 @@ export const ENCRYPTION_OVERHEAD = AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE;
|
|||||||
|
|
||||||
export const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MiB
|
export const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MiB
|
||||||
export const ENCRYPTED_CHUNK_SIZE = CHUNK_SIZE + ENCRYPTION_OVERHEAD;
|
export const ENCRYPTED_CHUNK_SIZE = CHUNK_SIZE + ENCRYPTION_OVERHEAD;
|
||||||
|
|
||||||
|
export const MAX_FILE_SIZE = 512 * 1024 * 1024; // 512 MiB
|
||||||
|
export const MAX_CHUNKS = Math.ceil(MAX_FILE_SIZE / CHUNK_SIZE); // 128 chunks
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export class FilesystemCache<K, V extends object> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(state.value
|
(state.value
|
||||||
? Promise.resolve(state.value)
|
? Promise.resolve($state.snapshot(state.value) as V)
|
||||||
: this.options.fetchFromIndexedDB(key).then((loadedInfo) => {
|
: this.options.fetchFromIndexedDB(key).then((loadedInfo) => {
|
||||||
if (loadedInfo) {
|
if (loadedInfo) {
|
||||||
state.value = loadedInfo;
|
state.value = loadedInfo;
|
||||||
|
|||||||
@@ -74,28 +74,6 @@ export const getAllDirectoriesByParent = async (userId: number, parentId: Direct
|
|||||||
return directories.map(toDirectory);
|
return directories.map(toDirectory);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllRecursiveDirectoriesByParent = async (userId: number, parentId: DirectoryId) => {
|
|
||||||
const directories = await db
|
|
||||||
.withRecursive("directory_tree", (db) =>
|
|
||||||
db
|
|
||||||
.selectFrom("directory")
|
|
||||||
.selectAll()
|
|
||||||
.$if(parentId === "root", (qb) => qb.where("parent_id", "is", null))
|
|
||||||
.$if(parentId !== "root", (qb) => qb.where("parent_id", "=", parentId as number))
|
|
||||||
.where("user_id", "=", userId)
|
|
||||||
.unionAll((db) =>
|
|
||||||
db
|
|
||||||
.selectFrom("directory")
|
|
||||||
.innerJoin("directory_tree", "directory.parent_id", "directory_tree.id")
|
|
||||||
.selectAll("directory"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.selectFrom("directory_tree")
|
|
||||||
.selectAll()
|
|
||||||
.execute();
|
|
||||||
return directories.map(toDirectory);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllFavoriteDirectories = async (userId: number) => {
|
export const getAllFavoriteDirectories = async (userId: number) => {
|
||||||
const directories = await db
|
const directories = await db
|
||||||
.selectFrom("directory")
|
.selectFrom("directory")
|
||||||
@@ -117,6 +95,61 @@ export const getDirectory = async (userId: number, directoryId: number) => {
|
|||||||
return directory ? toDirectory(directory) : null;
|
return directory ? toDirectory(directory) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const searchDirectories = async (
|
||||||
|
userId: number,
|
||||||
|
filters: {
|
||||||
|
parentId: DirectoryId;
|
||||||
|
inFavorites: boolean;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const directories = await db
|
||||||
|
.withRecursive("directory_tree", (db) =>
|
||||||
|
db
|
||||||
|
.selectFrom("directory")
|
||||||
|
.select("id")
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.$if(filters.parentId === "root", (qb) => qb.where((eb) => eb.lit(false))) // directory_tree will be empty if parentId is "root"
|
||||||
|
.$if(filters.parentId !== "root", (qb) => qb.where("id", "=", filters.parentId as number))
|
||||||
|
.unionAll(
|
||||||
|
db
|
||||||
|
.selectFrom("directory as d")
|
||||||
|
.innerJoin("directory_tree as dt", "d.parent_id", "dt.id")
|
||||||
|
.select("d.id"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.withRecursive("favorite_directory_tree", (db) =>
|
||||||
|
db
|
||||||
|
.selectFrom("directory")
|
||||||
|
.select("id")
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.$if(!filters.inFavorites, (qb) => qb.where((eb) => eb.lit(false))) // favorite_directory_tree will be empty if inFavorites is false
|
||||||
|
.$if(filters.inFavorites, (qb) => qb.where("is_favorite", "=", true))
|
||||||
|
.unionAll((db) =>
|
||||||
|
db
|
||||||
|
.selectFrom("directory as d")
|
||||||
|
.innerJoin("favorite_directory_tree as dt", "d.parent_id", "dt.id")
|
||||||
|
.select("d.id"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.selectFrom("directory")
|
||||||
|
.selectAll()
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.$if(filters.parentId !== "root", (qb) =>
|
||||||
|
qb.where((eb) =>
|
||||||
|
eb.exists(eb.selectFrom("directory_tree as dt").whereRef("dt.id", "=", "parent_id")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$if(filters.inFavorites, (qb) =>
|
||||||
|
qb.where((eb) =>
|
||||||
|
eb.exists(
|
||||||
|
eb.selectFrom("favorite_directory_tree as dt").whereRef("dt.id", "=", "directory.id"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.execute();
|
||||||
|
return directories.map(toDirectory);
|
||||||
|
};
|
||||||
|
|
||||||
export const setDirectoryEncName = async (
|
export const setDirectoryEncName = async (
|
||||||
userId: number,
|
userId: number,
|
||||||
directoryId: number,
|
directoryId: number,
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export const searchFiles = async (
|
|||||||
userId: number,
|
userId: number,
|
||||||
filters: {
|
filters: {
|
||||||
parentId: DirectoryId;
|
parentId: DirectoryId;
|
||||||
|
inFavorites: boolean;
|
||||||
includeCategoryIds: number[];
|
includeCategoryIds: number[];
|
||||||
excludeCategoryIds: number[];
|
excludeCategoryIds: number[];
|
||||||
},
|
},
|
||||||
@@ -258,7 +259,7 @@ export const searchFiles = async (
|
|||||||
.selectFrom("directory")
|
.selectFrom("directory")
|
||||||
.select("id")
|
.select("id")
|
||||||
.where("user_id", "=", userId)
|
.where("user_id", "=", userId)
|
||||||
.where((eb) => eb.val(filters.parentId !== "root")) // directory_tree will be empty if parentId is "root"
|
.$if(filters.parentId === "root", (qb) => qb.where((eb) => eb.lit(false))) // directory_tree will be empty if parentId is "root"
|
||||||
.$if(filters.parentId !== "root", (qb) => qb.where("id", "=", filters.parentId as number))
|
.$if(filters.parentId !== "root", (qb) => qb.where("id", "=", filters.parentId as number))
|
||||||
.unionAll(
|
.unionAll(
|
||||||
db
|
db
|
||||||
@@ -267,6 +268,20 @@ export const searchFiles = async (
|
|||||||
.select("d.id"),
|
.select("d.id"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.withRecursive("favorite_directory_tree", (db) =>
|
||||||
|
db
|
||||||
|
.selectFrom("directory")
|
||||||
|
.select("id")
|
||||||
|
.where("user_id", "=", userId)
|
||||||
|
.$if(!filters.inFavorites, (qb) => qb.where((eb) => eb.lit(false))) // favorite_directory_tree will be empty if inFavorites is false
|
||||||
|
.$if(filters.inFavorites, (qb) => qb.where("is_favorite", "=", true))
|
||||||
|
.unionAll((db) =>
|
||||||
|
db
|
||||||
|
.selectFrom("directory as d")
|
||||||
|
.innerJoin("favorite_directory_tree as dt", "d.parent_id", "dt.id")
|
||||||
|
.select("d.id"),
|
||||||
|
),
|
||||||
|
)
|
||||||
.withRecursive("include_category_tree", (db) =>
|
.withRecursive("include_category_tree", (db) =>
|
||||||
db
|
db
|
||||||
.selectFrom("category")
|
.selectFrom("category")
|
||||||
@@ -295,18 +310,30 @@ export const searchFiles = async (
|
|||||||
)
|
)
|
||||||
.selectFrom("file")
|
.selectFrom("file")
|
||||||
.selectAll("file")
|
.selectAll("file")
|
||||||
.$if(filters.parentId === "root", (qb) => qb.where("user_id", "=", userId)) // directory_tree isn't used if parentId is "root"
|
.where("user_id", "=", userId)
|
||||||
.$if(filters.parentId !== "root", (qb) =>
|
.$if(filters.parentId !== "root", (qb) =>
|
||||||
qb.where("parent_id", "in", (eb) => eb.selectFrom("directory_tree").select("id")),
|
qb.where((eb) =>
|
||||||
|
eb.exists(eb.selectFrom("directory_tree as dt").whereRef("dt.id", "=", "file.parent_id")),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.where((eb) =>
|
.$if(filters.inFavorites, (qb) =>
|
||||||
|
qb.where((eb) =>
|
||||||
|
eb.or([
|
||||||
|
eb("is_favorite", "=", true),
|
||||||
|
eb.exists(
|
||||||
|
eb.selectFrom("favorite_directory_tree as dt").whereRef("dt.id", "=", "file.parent_id"),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.$if(filters.excludeCategoryIds.length > 0, (qb) =>
|
||||||
|
qb.where((eb) =>
|
||||||
eb.not(
|
eb.not(
|
||||||
eb.exists(
|
eb.exists(
|
||||||
eb
|
eb
|
||||||
.selectFrom("file_category")
|
.selectFrom("file_category")
|
||||||
.whereRef("file_id", "=", "file.id")
|
.innerJoin("exclude_category_tree", "category_id", "exclude_category_tree.id")
|
||||||
.where("category_id", "in", (eb) =>
|
.whereRef("file_id", "=", "file.id"),
|
||||||
eb.selectFrom("exclude_category_tree").select("id"),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
37
src/lib/server/modules/logger.ts
Normal file
37
src/lib/server/modules/logger.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { appendFileSync, existsSync, mkdirSync } from "fs";
|
||||||
|
import { env } from "$env/dynamic/private";
|
||||||
|
|
||||||
|
const LOG_DIR = env.LOG_DIR || "log";
|
||||||
|
|
||||||
|
const getLogFilePath = () => {
|
||||||
|
const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
|
||||||
|
return `${LOG_DIR}/arkvault-${date}.log`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureLogDir = () => {
|
||||||
|
if (!existsSync(LOG_DIR)) {
|
||||||
|
mkdirSync(LOG_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLogLine = (type: string, data: Record<string, unknown>) => {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
return JSON.stringify({ timestamp, type, ...data });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const demoLogger = {
|
||||||
|
log: (type: string, data: Record<string, unknown>) => {
|
||||||
|
const line = formatLogLine(type, data);
|
||||||
|
|
||||||
|
// Output to stdout
|
||||||
|
console.log(line);
|
||||||
|
|
||||||
|
// Output to file
|
||||||
|
try {
|
||||||
|
ensureLogDir();
|
||||||
|
appendFileSync(getLogFilePath(), line + "\n", { encoding: "utf-8" });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to write to log file:", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let email = $state("");
|
let email = $state("arkvault-demo@minchan.me");
|
||||||
let password = $state("");
|
let password = $state("arkvault-demo");
|
||||||
|
|
||||||
let isForceLoginModalOpen = $state(false);
|
let isForceLoginModalOpen = $state(false);
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
|
|
||||||
{#if isOpen && (directoryId || downloadUrl || fileBlob)}
|
{#if isOpen && (directoryId || downloadUrl || fileBlob)}
|
||||||
<div
|
<div
|
||||||
class="absolute right-2 top-full z-20 min-w-40 space-y-1 rounded-lg bg-white px-1 py-2 shadow-2xl"
|
class="absolute right-2 top-full z-20 min-w-44 space-y-1 rounded-lg bg-white px-1 py-2 shadow-2xl"
|
||||||
transition:fly={{ y: -8, duration: 200 }}
|
transition:fly={{ y: -8, duration: 200 }}
|
||||||
>
|
>
|
||||||
<p class="px-3 pt-2 text-sm font-semibold text-gray-600">더보기</p>
|
<p class="px-3 pt-2 text-sm font-semibold text-gray-600">더보기</p>
|
||||||
@@ -57,7 +57,9 @@
|
|||||||
onclick: () => void,
|
onclick: () => void,
|
||||||
)}
|
)}
|
||||||
<button {onclick} class="rounded-xl active:bg-gray-100">
|
<button {onclick} class="rounded-xl active:bg-gray-100">
|
||||||
<div class="flex items-center gap-x-3 px-3 py-2 text-gray-700 transition active:scale-95">
|
<div
|
||||||
|
class="flex items-center gap-x-3 px-3 py-2 text-lg text-gray-700 transition active:scale-95"
|
||||||
|
>
|
||||||
<Icon />
|
<Icon />
|
||||||
<p class="font-medium">{text}</p>
|
<p class="font-medium">{text}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
includeImages: false,
|
includeImages: false,
|
||||||
includeVideos: false,
|
includeVideos: false,
|
||||||
includeDirectories: false,
|
includeDirectories: false,
|
||||||
searchInFavorites: false,
|
|
||||||
searchInDirectory: false,
|
searchInDirectory: false,
|
||||||
|
searchInFavorites: false,
|
||||||
categories: [],
|
categories: [],
|
||||||
});
|
});
|
||||||
let hasCategoryFilter = $derived(filters.categories.length > 0);
|
let hasCategoryFilter = $derived(filters.categories.length > 0);
|
||||||
@@ -47,7 +47,6 @@
|
|||||||
filters.includeImages ||
|
filters.includeImages ||
|
||||||
filters.includeVideos ||
|
filters.includeVideos ||
|
||||||
filters.includeDirectories ||
|
filters.includeDirectories ||
|
||||||
filters.searchInFavorites ||
|
|
||||||
filters.name.trim().length > 0,
|
filters.name.trim().length > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -84,9 +83,7 @@
|
|||||||
|
|
||||||
return sortEntries(
|
return sortEntries(
|
||||||
[...directories, ...files].filter(
|
[...directories, ...files].filter(
|
||||||
(entry) =>
|
(entry) => !nameFilter || searchString(entry.name, nameFilter),
|
||||||
(!nameFilter || searchString(entry.name, nameFilter)) &&
|
|
||||||
(!filters.searchInFavorites || entry.isFavorite),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -145,6 +142,7 @@
|
|||||||
// Svelte sucks
|
// Svelte sucks
|
||||||
hasAnyFilter;
|
hasAnyFilter;
|
||||||
filters.searchInDirectory;
|
filters.searchInDirectory;
|
||||||
|
filters.searchInFavorites;
|
||||||
filters.categories.length;
|
filters.categories.length;
|
||||||
|
|
||||||
if (untrack(() => isRestoredFromSnapshot)) {
|
if (untrack(() => isRestoredFromSnapshot)) {
|
||||||
@@ -156,6 +154,7 @@
|
|||||||
requestSearch(
|
requestSearch(
|
||||||
{
|
{
|
||||||
ancestorId: filters.searchInDirectory ? data.directoryId! : "root",
|
ancestorId: filters.searchInDirectory ? data.directoryId! : "root",
|
||||||
|
inFavorites: filters.searchInFavorites,
|
||||||
categories: filters.categories,
|
categories: filters.categories,
|
||||||
},
|
},
|
||||||
$masterKeyStore?.get(1)?.key!,
|
$masterKeyStore?.get(1)?.key!,
|
||||||
@@ -216,7 +215,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if hasAnyFilter}
|
{#if hasAnyFilter}
|
||||||
<div class="flex flex-grow flex-col space-y-2 bg-white p-4">
|
<div class="flex flex-grow flex-col space-y-2 bg-white p-4">
|
||||||
<p class="text-lg font-bold text-gray-800">검색 결과</p>
|
<p class="text-lg font-bold text-gray-800">
|
||||||
|
검색 결과
|
||||||
|
{#if result.length > 0}
|
||||||
|
{" "}({result.length}개)
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
{#if result.length > 0}
|
{#if result.length > 0}
|
||||||
<RowVirtualizer
|
<RowVirtualizer
|
||||||
count={result.length}
|
count={result.length}
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ClassValue } from "svelte/elements";
|
|
||||||
|
|
||||||
import IconArrowBack from "~icons/material-symbols/arrow-back";
|
import IconArrowBack from "~icons/material-symbols/arrow-back";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: ClassValue;
|
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { class: className, value = $bindable() }: Props = $props();
|
let { value = $bindable() }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={["sticky top-0 z-10 flex items-center gap-x-2 px-2 py-3 backdrop-blur-2xl", className]}>
|
<div class={"sticky top-0 z-10 flex items-center gap-x-2 px-2 py-3 backdrop-blur-2xl"}>
|
||||||
<button
|
<button
|
||||||
onclick={() => history.back()}
|
onclick={() => history.back()}
|
||||||
class="w-[2.3rem] flex-shrink-0 rounded-full p-1 active:bg-black active:bg-opacity-[0.04]"
|
class="w-[2.3rem] flex-shrink-0 rounded-full p-1 active:bg-black active:bg-opacity-[0.04]"
|
||||||
@@ -22,6 +19,6 @@
|
|||||||
bind:value
|
bind:value
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="검색"
|
placeholder="검색"
|
||||||
class="mr-1 h-[2.3rem] flex-grow rounded-lg bg-gray-100 px-3 py-1.5 placeholder-gray-600 focus:outline-none"
|
class="mr-1 h-[2.3rem] min-w-0 flex-grow rounded-lg bg-gray-100 px-3 py-1.5 placeholder-gray-600 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { trpc } from "$trpc/client";
|
|||||||
|
|
||||||
export interface SearchFilter {
|
export interface SearchFilter {
|
||||||
ancestorId: DirectoryId;
|
ancestorId: DirectoryId;
|
||||||
|
inFavorites: boolean;
|
||||||
categories: { info: LocalCategoryInfo; type: "include" | "exclude" }[];
|
categories: { info: LocalCategoryInfo; type: "include" | "exclude" }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export interface SearchResult {
|
|||||||
export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey) => {
|
export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey) => {
|
||||||
const { directories: directoriesRaw, files: filesRaw } = await trpc().search.search.query({
|
const { directories: directoriesRaw, files: filesRaw } = await trpc().search.search.query({
|
||||||
ancestor: filter.ancestorId,
|
ancestor: filter.ancestorId,
|
||||||
|
inFavorites: filter.inFavorites,
|
||||||
includeCategories: filter.categories
|
includeCategories: filter.categories
|
||||||
.filter(({ type }) => type === "include")
|
.filter(({ type }) => type === "include")
|
||||||
.map(({ info }) => info.id),
|
.map(({ info }) => info.id),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||||
import { getFileThumbnail } from "$lib/modules/file";
|
import { getFileThumbnail } from "$lib/modules/file";
|
||||||
import type { CategoryFileInfo } from "$lib/modules/filesystem";
|
import type { CategoryFileInfo } from "$lib/modules/filesystem";
|
||||||
|
import { formatDateTime } from "$lib/utils";
|
||||||
import type { SelectedFile } from "./service.svelte";
|
import type { SelectedFile } from "./service.svelte";
|
||||||
|
|
||||||
import IconClose from "~icons/material-symbols/close";
|
import IconClose from "~icons/material-symbols/close";
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ActionEntryButton
|
<ActionEntryButton
|
||||||
class="h-12"
|
class="h-14"
|
||||||
onclick={() => onclick(info)}
|
onclick={() => onclick(info)}
|
||||||
actionButtonIcon={onRemoveClick && IconClose}
|
actionButtonIcon={onRemoveClick && IconClose}
|
||||||
onActionButtonClick={() => onRemoveClick?.(info)}
|
onActionButtonClick={() => onRemoveClick?.(info)}
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
type="file"
|
type="file"
|
||||||
thumbnail={$thumbnail}
|
thumbnail={$thumbnail}
|
||||||
name={info.name}
|
name={info.name}
|
||||||
|
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
|
||||||
isFavorite={info.isFavorite}
|
isFavorite={info.isFavorite}
|
||||||
/>
|
/>
|
||||||
</ActionEntryButton>
|
</ActionEntryButton>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>파일</title>
|
<title>내 파일</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<input bind:this={fileInput} onchange={uploadFile} type="file" multiple class="hidden" />
|
<input bind:this={fileInput} onchange={uploadFile} type="file" multiple class="hidden" />
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
<title>즐겨찾기</title>
|
<title>즐겨찾기</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
<TopBar title="즐겨찾기" showBackButton={false}>
|
<TopBar title="즐겨찾기" showBackButton={false}>
|
||||||
<button
|
<button
|
||||||
onclick={() => goto("/search?from=favorites")}
|
onclick={() => goto("/search?from=favorites")}
|
||||||
@@ -52,22 +53,8 @@
|
|||||||
<IconSearch />
|
<IconSearch />
|
||||||
</button>
|
</button>
|
||||||
</TopBar>
|
</TopBar>
|
||||||
<div class="flex h-full flex-col p-4 !pt-0">
|
<div class="flex flex-grow flex-col px-4 pb-4">
|
||||||
{#if isLoading}
|
{#if entries.length > 0}
|
||||||
<div class="flex flex-grow items-center justify-center">
|
|
||||||
<p class="text-gray-500">
|
|
||||||
{#if data.favorites.files.length === 0 && data.favorites.directories.length === 0}
|
|
||||||
즐겨찾기한 항목이 없어요.
|
|
||||||
{:else}
|
|
||||||
즐겨찾기 목록을 불러오고 있어요.
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{:else if entries.length === 0}
|
|
||||||
<div class="flex flex-grow items-center justify-center">
|
|
||||||
<p class="text-gray-500">즐겨찾기한 항목이 없어요.</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<RowVirtualizer
|
<RowVirtualizer
|
||||||
count={entries.length}
|
count={entries.length}
|
||||||
getItemKey={(index) => `${entries[index]!.type}-${entries[index]!.details.id}`}
|
getItemKey={(index) => `${entries[index]!.type}-${entries[index]!.details.id}`}
|
||||||
@@ -91,5 +78,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</RowVirtualizer>
|
</RowVirtualizer>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-grow items-center justify-center">
|
||||||
|
<p class="text-gray-500">
|
||||||
|
{#if isLoading}
|
||||||
|
즐겨찾기 목록을 불러오고 있어요.
|
||||||
|
{:else}
|
||||||
|
즐겨찾기한 항목이 없어요.
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
import { DirectoryEntryLabel } from "$lib/components/molecules";
|
||||||
import { getFileThumbnail } from "$lib/modules/file";
|
import { getFileThumbnail } from "$lib/modules/file";
|
||||||
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
|
||||||
|
import { formatDateTime } from "$lib/utils";
|
||||||
|
|
||||||
import IconClose from "~icons/material-symbols/close";
|
import IconClose from "~icons/material-symbols/close";
|
||||||
|
|
||||||
@@ -23,5 +24,11 @@
|
|||||||
actionButtonIcon={IconClose}
|
actionButtonIcon={IconClose}
|
||||||
onActionButtonClick={onRemoveClick}
|
onActionButtonClick={onRemoveClick}
|
||||||
>
|
>
|
||||||
<DirectoryEntryLabel type="file" thumbnail={$thumbnail} name={info.name} isFavorite />
|
<DirectoryEntryLabel
|
||||||
|
type="file"
|
||||||
|
thumbnail={$thumbnail}
|
||||||
|
name={info.name}
|
||||||
|
subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)}
|
||||||
|
isFavorite
|
||||||
|
/>
|
||||||
</ActionEntryButton>
|
</ActionEntryButton>
|
||||||
|
|||||||
@@ -52,13 +52,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<p class="font-semibold">보안</p>
|
<p class="font-semibold">보안</p>
|
||||||
<MenuEntryButton
|
|
||||||
onclick={() => goto("/auth/changePassword")}
|
|
||||||
icon={IconPassword}
|
|
||||||
iconColor="text-blue-500"
|
|
||||||
>
|
|
||||||
비밀번호 바꾸기
|
|
||||||
</MenuEntryButton>
|
|
||||||
<MenuEntryButton onclick={logout} icon={IconLogout} iconColor="text-red-500">
|
<MenuEntryButton onclick={logout} icon={IconLogout} iconColor="text-red-500">
|
||||||
로그아웃
|
로그아웃
|
||||||
</MenuEntryButton>
|
</MenuEntryButton>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ClientRepo, SessionRepo, UserRepo, IntegrityError } from "$lib/server/d
|
|||||||
import env from "$lib/server/loadenv";
|
import env from "$lib/server/loadenv";
|
||||||
import { cookieOptions } from "$lib/server/modules/auth";
|
import { cookieOptions } from "$lib/server/modules/auth";
|
||||||
import { generateChallenge, verifySignature, issueSessionId } from "$lib/server/modules/crypto";
|
import { generateChallenge, verifySignature, issueSessionId } from "$lib/server/modules/crypto";
|
||||||
|
import { demoLogger } from "$lib/server/modules/logger";
|
||||||
import { router, publicProcedure, roleProcedure } from "../init.server";
|
import { router, publicProcedure, roleProcedure } from "../init.server";
|
||||||
|
|
||||||
const authRouter = router({
|
const authRouter = router({
|
||||||
@@ -24,6 +25,10 @@ const authRouter = router({
|
|||||||
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
|
const { sessionId, sessionIdSigned } = await issueSessionId(32, env.session.secret);
|
||||||
await SessionRepo.createSession(user.id, sessionId, ctx.locals.ip, ctx.locals.userAgent);
|
await SessionRepo.createSession(user.id, sessionId, ctx.locals.ip, ctx.locals.userAgent);
|
||||||
ctx.cookies.set("sessionId", sessionIdSigned, cookieOptions);
|
ctx.cookies.set("sessionId", sessionIdSigned, cookieOptions);
|
||||||
|
|
||||||
|
if (input.email === "arkvault-demo@minchan.me") {
|
||||||
|
demoLogger.log("demo:login", { ip: ctx.locals.ip, sessionId });
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
logout: roleProcedure["any"].mutation(async ({ ctx }) => {
|
logout: roleProcedure["any"].mutation(async ({ ctx }) => {
|
||||||
@@ -38,22 +43,8 @@ const authRouter = router({
|
|||||||
newPassword: z.string().nonempty(),
|
newPassword: z.string().nonempty(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(() => {
|
||||||
if (input.oldPassword === input.newPassword) {
|
throw new TRPCError({ code: "NOT_IMPLEMENTED" });
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Same passwords" });
|
|
||||||
} else if (input.newPassword.length < 8) {
|
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "Too short password" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await UserRepo.getUser(ctx.session.userId);
|
|
||||||
if (!user) {
|
|
||||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid session id" });
|
|
||||||
} else if (!(await argon2.verify(user.password, input.oldPassword))) {
|
|
||||||
throw new TRPCError({ code: "FORBIDDEN", message: "Invalid password" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await UserRepo.setUserPassword(ctx.session.userId, await argon2.hash(input.newPassword));
|
|
||||||
await SessionRepo.deleteAllOtherSessions(ctx.session.userId, ctx.session.sessionId);
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
upgrade: roleProcedure["notClient"]
|
upgrade: roleProcedure["notClient"]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|||||||
import { DirectoryIdSchema } from "$lib/schemas";
|
import { DirectoryIdSchema } from "$lib/schemas";
|
||||||
import { DirectoryRepo, FileRepo, IntegrityError } from "$lib/server/db";
|
import { DirectoryRepo, FileRepo, IntegrityError } from "$lib/server/db";
|
||||||
import { safeUnlink } from "$lib/server/modules/filesystem";
|
import { safeUnlink } from "$lib/server/modules/filesystem";
|
||||||
|
import { demoLogger } from "$lib/server/modules/logger";
|
||||||
import { router, roleProcedure } from "../init.server";
|
import { router, roleProcedure } from "../init.server";
|
||||||
|
|
||||||
const directoryRouter = router({
|
const directoryRouter = router({
|
||||||
@@ -134,6 +135,7 @@ const directoryRouter = router({
|
|||||||
const files = await DirectoryRepo.unregisterDirectory(ctx.session.userId, input.id);
|
const files = await DirectoryRepo.unregisterDirectory(ctx.session.userId, input.id);
|
||||||
return {
|
return {
|
||||||
deletedFiles: files.map((file) => {
|
deletedFiles: files.map((file) => {
|
||||||
|
demoLogger.log("file:delete", { ip: ctx.locals.ip, fileId: file.id, recursive: true });
|
||||||
safeUnlink(file.path); // Intended
|
safeUnlink(file.path); // Intended
|
||||||
safeUnlink(file.thumbnailPath); // Intended
|
safeUnlink(file.thumbnailPath); // Intended
|
||||||
return file.id;
|
return file.id;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { TRPCError } from "@trpc/server";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { FileRepo, MediaRepo, IntegrityError } from "$lib/server/db";
|
import { FileRepo, MediaRepo, IntegrityError } from "$lib/server/db";
|
||||||
import { safeUnlink } from "$lib/server/modules/filesystem";
|
import { safeUnlink } from "$lib/server/modules/filesystem";
|
||||||
|
import { demoLogger } from "$lib/server/modules/logger";
|
||||||
import { router, roleProcedure } from "../init.server";
|
import { router, roleProcedure } from "../init.server";
|
||||||
|
|
||||||
const fileRouter = router({
|
const fileRouter = router({
|
||||||
@@ -174,6 +175,7 @@ const fileRouter = router({
|
|||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
const { path, thumbnailPath } = await FileRepo.unregisterFile(ctx.session.userId, input.id);
|
const { path, thumbnailPath } = await FileRepo.unregisterFile(ctx.session.userId, input.id);
|
||||||
|
demoLogger.log("file:delete", { ip: ctx.locals.ip, fileId: input.id });
|
||||||
safeUnlink(path); // Intended
|
safeUnlink(path); // Intended
|
||||||
safeUnlink(thumbnailPath); // Intended
|
safeUnlink(thumbnailPath); // Intended
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const searchRouter = router({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
ancestor: DirectoryIdSchema.default("root"),
|
ancestor: DirectoryIdSchema.default("root"),
|
||||||
|
inFavorites: z.boolean().default(false),
|
||||||
includeCategories: z.number().positive().array().default([]),
|
includeCategories: z.number().positive().array().default([]),
|
||||||
excludeCategories: z.number().positive().array().default([]),
|
excludeCategories: z.number().positive().array().default([]),
|
||||||
}),
|
}),
|
||||||
@@ -15,10 +16,14 @@ const searchRouter = router({
|
|||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const [directories, files] = await Promise.all([
|
const [directories, files] = await Promise.all([
|
||||||
input.includeCategories.length === 0 && input.excludeCategories.length === 0
|
input.includeCategories.length === 0 && input.excludeCategories.length === 0
|
||||||
? DirectoryRepo.getAllRecursiveDirectoriesByParent(ctx.session.userId, input.ancestor)
|
? DirectoryRepo.searchDirectories(ctx.session.userId, {
|
||||||
|
parentId: input.ancestor,
|
||||||
|
inFavorites: input.inFavorites,
|
||||||
|
})
|
||||||
: [],
|
: [],
|
||||||
FileRepo.searchFiles(ctx.session.userId, {
|
FileRepo.searchFiles(ctx.session.userId, {
|
||||||
parentId: input.ancestor,
|
parentId: input.ancestor,
|
||||||
|
inFavorites: input.inFavorites,
|
||||||
includeCategoryIds: input.includeCategories,
|
includeCategoryIds: input.includeCategories,
|
||||||
excludeCategoryIds: input.excludeCategories,
|
excludeCategoryIds: input.excludeCategories,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import mime from "mime";
|
|||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { MAX_CHUNKS } from "$lib/constants";
|
||||||
import { DirectoryIdSchema } from "$lib/schemas";
|
import { DirectoryIdSchema } from "$lib/schemas";
|
||||||
import { FileRepo, MediaRepo, UploadRepo, IntegrityError } from "$lib/server/db";
|
import { FileRepo, MediaRepo, UploadRepo, IntegrityError } from "$lib/server/db";
|
||||||
import db from "$lib/server/db/kysely";
|
import db from "$lib/server/db/kysely";
|
||||||
import env from "$lib/server/loadenv";
|
import env from "$lib/server/loadenv";
|
||||||
import { safeRecursiveRm, safeUnlink } from "$lib/server/modules/filesystem";
|
import { safeRecursiveRm, safeUnlink } from "$lib/server/modules/filesystem";
|
||||||
|
import { demoLogger } from "$lib/server/modules/logger";
|
||||||
import { router, roleProcedure } from "../init.server";
|
import { router, roleProcedure } from "../init.server";
|
||||||
|
|
||||||
const UPLOADS_EXPIRES = 24 * 3600 * 1000; // 24 hours
|
const UPLOADS_EXPIRES = 24 * 3600 * 1000; // 24 hours
|
||||||
@@ -28,7 +30,7 @@ const uploadRouter = router({
|
|||||||
startFileUpload: roleProcedure["activeClient"]
|
startFileUpload: roleProcedure["activeClient"]
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
chunks: z.int().positive(),
|
chunks: z.int().positive().max(MAX_CHUNKS),
|
||||||
parent: DirectoryIdSchema,
|
parent: DirectoryIdSchema,
|
||||||
mekVersion: z.int().positive(),
|
mekVersion: z.int().positive(),
|
||||||
dek: z.base64().nonempty(),
|
dek: z.base64().nonempty(),
|
||||||
@@ -76,6 +78,7 @@ const uploadRouter = router({
|
|||||||
: null,
|
: null,
|
||||||
encLastModifiedAt: { ciphertext: input.lastModifiedAt, iv: input.lastModifiedAtIv },
|
encLastModifiedAt: { ciphertext: input.lastModifiedAt, iv: input.lastModifiedAtIv },
|
||||||
});
|
});
|
||||||
|
demoLogger.log("upload:start", { ip: ctx.locals.ip, uploadId: id });
|
||||||
return { uploadId: id };
|
return { uploadId: id };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await safeRecursiveRm(path);
|
await safeRecursiveRm(path);
|
||||||
@@ -153,6 +156,7 @@ const uploadRouter = router({
|
|||||||
});
|
});
|
||||||
|
|
||||||
await safeRecursiveRm(session.path);
|
await safeRecursiveRm(session.path);
|
||||||
|
demoLogger.log("upload:complete", { ip: ctx.locals.ip, uploadId, fileId });
|
||||||
return { file: fileId };
|
return { file: fileId };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await safeUnlink(filePath);
|
await safeUnlink(filePath);
|
||||||
@@ -183,6 +187,7 @@ const uploadRouter = router({
|
|||||||
fileId: input.file,
|
fileId: input.file,
|
||||||
dekVersion: input.dekVersion,
|
dekVersion: input.dekVersion,
|
||||||
});
|
});
|
||||||
|
demoLogger.log("thumbnail:start", { ip: ctx.locals.ip, uploadId: id });
|
||||||
return { uploadId: id };
|
return { uploadId: id };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await safeRecursiveRm(path);
|
await safeRecursiveRm(path);
|
||||||
@@ -238,6 +243,11 @@ const uploadRouter = router({
|
|||||||
await UploadRepo.deleteUploadSession(trx, uploadId);
|
await UploadRepo.deleteUploadSession(trx, uploadId);
|
||||||
return oldPath;
|
return oldPath;
|
||||||
});
|
});
|
||||||
|
demoLogger.log("thumbnail:complete", {
|
||||||
|
ip: ctx.locals.ip,
|
||||||
|
uploadId,
|
||||||
|
fileId: session.fileId,
|
||||||
|
});
|
||||||
await Promise.all([safeUnlink(oldThumbnailPath), safeRecursiveRm(session.path)]);
|
await Promise.all([safeUnlink(oldThumbnailPath), safeRecursiveRm(session.path)]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await safeUnlink(thumbnailPath);
|
await safeUnlink(thumbnailPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user