From 5b8621e524bbc3ca3b3be14f5225c80a5df482f7 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 12 Oct 2026 08:51:07 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=AC=EB=8B=A4=EC=9D=B4=EB=A0=89=ED=8A=B8?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EC=B2=98=EB=A6=AC,=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=95=EB=B3=B4=20=EB=B3=80=ED=99=98,=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=91=9C=EC=8B=9C=20UI=20=EB=93=B1=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=EB=90=9C=20=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=ED=95=98=EA=B3=A0=20=EB=AA=A8=EB=93=88=20?= =?UTF-8?q?=EA=B0=84=20=EC=88=9C=ED=99=98=20=EC=9D=98=EC=A1=B4=EC=84=B1=20?= =?UTF-8?q?=EB=B0=8F=20=EA=B3=84=EC=B8=B5=20=EC=9C=84=EB=B0=98=20=ED=95=B4?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/hooks.server.ts | 13 +- src/lib/client/media/heic.ts | 5 + src/lib/client/media/thumbnail.ts | 11 +- src/lib/client/service-worker.ts | 4 +- src/lib/client/transfer/upload.svelte.ts | 38 ++-- src/lib/features/favorites/index.ts | 16 +- src/lib/features/filesystem/category.ts | 39 ++-- .../components/FileThumbnailButton.svelte | 3 +- src/lib/features/filesystem/directory.ts | 72 ++------ src/lib/features/filesystem/dto.ts | 46 +++++ src/lib/features/filesystem/file.ts | 169 ++++++------------ src/lib/features/filesystem/types.ts | 7 + src/lib/features/search/index.ts | 7 +- .../components/TransferProgress.svelte | 12 ++ .../components/UploadStatusText.svelte | 26 +++ src/lib/features/transfer/components/index.ts | 2 + src/lib/server/auth.ts | 26 ++- src/lib/server/db/error.ts | 2 +- src/lib/server/db/repositories/session.ts | 7 +- src/lib/server/errors.ts | 4 + src/lib/server/http.ts | 4 +- src/lib/server/middlewares/authenticate.ts | 7 +- src/lib/server/services/auth.ts | 25 ++- src/lib/server/services/client.ts | 4 + src/lib/server/services/key.ts | 7 +- src/lib/server/services/user.ts | 4 +- src/lib/shared/crypto/aes.ts | 16 +- src/lib/shared/crypto/rsa.ts | 19 +- src/lib/shared/format/chunk.ts | 4 +- src/lib/shared/redirect.ts | 12 ++ src/lib/shared/service-worker.ts | 3 + src/lib/ui/buttons/ActionEntryButton.svelte | 11 +- src/lib/ui/buttons/Button.svelte | 3 +- src/lib/ui/buttons/EntryButton.svelte | 6 +- src/lib/ui/buttons/FloatingButton.svelte | 5 +- src/lib/ui/buttons/IconEntryButton.svelte | 3 +- src/lib/ui/buttons/TextButton.svelte | 3 +- src/lib/ui/index.ts | 1 + src/lib/ui/inputs/Chip.svelte | 7 +- src/lib/ui/labels/CategoryLabel.svelte | 2 +- src/lib/ui/labels/DirectoryEntryLabel.svelte | 2 +- src/lib/ui/layout/TitledDiv.svelte | 2 +- src/lib/ui/overlays/ActionModal.svelte | 3 +- src/lib/ui/overlays/BottomSheet.svelte | 2 +- src/lib/ui/overlays/Modal.svelte | 2 +- src/lib/ui/overlays/RenameModal.svelte | 2 +- src/lib/ui/overlays/TextInputModal.svelte | 3 +- src/lib/ui/press.ts | 6 + src/params.ts | 11 +- .../(fullscreen)/auth/login/+page.svelte | 10 +- src/routes/(fullscreen)/auth/login/+page.ts | 4 +- .../(fullscreen)/client/pending/+page.ts | 4 +- .../file/{[id] => [id=int]}/+page.svelte | 6 +- .../(fullscreen)/file/[id=int]/+page.ts | 5 + .../AddToCategoryBottomSheet.svelte | 0 .../{[id] => [id=int]}/DownloadStatus.svelte | 5 +- .../file/{[id] => [id=int]}/TopBarMenu.svelte | 0 src/routes/(fullscreen)/file/[id]/+page.ts | 15 -- .../(fullscreen)/file/downloads/File.svelte | 6 +- .../(fullscreen)/file/uploads/File.svelte | 19 +- .../(fullscreen)/key/export/+page.svelte | 3 +- .../(fullscreen)/key/generate/+page.svelte | 3 +- src/routes/(fullscreen)/key/generate/+page.ts | 4 +- .../(fullscreen)/settings/cache/File.svelte | 3 +- .../{[[id]] => [[id=int]]}/+page.svelte | 0 .../(main)/category/[[id=int]]/+page.ts | 5 + .../CategoryDeleteModal.svelte | 0 .../CategoryMenuBottomSheet.svelte | 0 .../CategoryRenameModal.svelte | 0 .../{[[id]] => [[id=int]]}/service.svelte.ts | 0 src/routes/(main)/category/[[id]]/+page.ts | 17 -- .../{[[id]] => [[id=int]]}/+page.svelte | 18 +- .../(main)/directory/[[id=int]]/+page.ts | 5 + .../DirectoryCreateModal.svelte | 0 .../DuplicateFileModal.svelte | 0 .../EntryCreateBottomSheet.svelte | 0 .../EntryDeleteModal.svelte | 0 .../EntryMenuBottomSheet.svelte | 0 .../EntryRenameModal.svelte | 0 .../[[id=int]]/TransferStatusCard.svelte | 29 +++ .../directory-entries/DirectoryEntries.svelte | 2 +- .../directory-entries/UploadingFile.svelte | 15 +- .../directory-entries/index.ts | 0 .../{[[id]] => [[id=int]]}/service.svelte.ts | 0 src/routes/(main)/directory/[[id]]/+page.ts | 17 -- .../[[id]]/DownloadStatusCard.svelte | 23 --- .../directory/[[id]]/UploadStatusCard.svelte | 23 --- src/routes/(main)/home/+page.svelte | 8 +- src/routes/(main)/home/service.ts | 14 -- src/routes/+layout.ts | 7 +- .../download/+server.ts | 11 +- src/service-worker/stores.ts | 6 +- src/trpc/client.ts | 3 +- src/trpc/dto.ts | 8 +- src/trpc/init.server.ts | 12 +- src/trpc/schemas.ts | 2 +- 96 files changed, 452 insertions(+), 548 deletions(-) create mode 100644 src/lib/client/media/heic.ts create mode 100644 src/lib/features/filesystem/dto.ts create mode 100644 src/lib/features/transfer/components/TransferProgress.svelte create mode 100644 src/lib/features/transfer/components/UploadStatusText.svelte create mode 100644 src/lib/features/transfer/components/index.ts create mode 100644 src/lib/shared/redirect.ts create mode 100644 src/lib/ui/press.ts rename src/routes/(fullscreen)/file/{[id] => [id=int]}/+page.svelte (97%) create mode 100644 src/routes/(fullscreen)/file/[id=int]/+page.ts rename src/routes/(fullscreen)/file/{[id] => [id=int]}/AddToCategoryBottomSheet.svelte (100%) rename src/routes/(fullscreen)/file/{[id] => [id=int]}/DownloadStatus.svelte (70%) rename src/routes/(fullscreen)/file/{[id] => [id=int]}/TopBarMenu.svelte (100%) delete mode 100644 src/routes/(fullscreen)/file/[id]/+page.ts rename src/routes/(main)/category/{[[id]] => [[id=int]]}/+page.svelte (100%) create mode 100644 src/routes/(main)/category/[[id=int]]/+page.ts rename src/routes/(main)/category/{[[id]] => [[id=int]]}/CategoryDeleteModal.svelte (100%) rename src/routes/(main)/category/{[[id]] => [[id=int]]}/CategoryMenuBottomSheet.svelte (100%) rename src/routes/(main)/category/{[[id]] => [[id=int]]}/CategoryRenameModal.svelte (100%) rename src/routes/(main)/category/{[[id]] => [[id=int]]}/service.svelte.ts (100%) delete mode 100644 src/routes/(main)/category/[[id]]/+page.ts rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/+page.svelte (91%) create mode 100644 src/routes/(main)/directory/[[id=int]]/+page.ts rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/DirectoryCreateModal.svelte (100%) rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/DuplicateFileModal.svelte (100%) rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/EntryCreateBottomSheet.svelte (100%) rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/EntryDeleteModal.svelte (100%) rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/EntryMenuBottomSheet.svelte (100%) rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/EntryRenameModal.svelte (100%) create mode 100644 src/routes/(main)/directory/[[id=int]]/TransferStatusCard.svelte rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/directory-entries/DirectoryEntries.svelte (97%) rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/directory-entries/UploadingFile.svelte (53%) rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/directory-entries/index.ts (100%) rename src/routes/(main)/directory/{[[id]] => [[id=int]]}/service.svelte.ts (100%) delete mode 100644 src/routes/(main)/directory/[[id]]/+page.ts delete mode 100644 src/routes/(main)/directory/[[id]]/DownloadStatusCard.svelte delete mode 100644 src/routes/(main)/directory/[[id]]/UploadStatusCard.svelte delete mode 100644 src/routes/(main)/home/service.ts rename src/routes/api/file/{[id] => [id=int]}/[[thumbnail=thumbnail]]/download/+server.ts (84%) diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 0d3e02f..37d56d3 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,9 +1,13 @@ import { sequence, type ServerInit } from "@sveltejs/kit/hooks"; import schedule from "node-schedule"; -import { ClientRepo, SessionRepo } from "#lib/server/db/index.ts"; import { migrateDB } from "#lib/server/db/kysely.ts"; import { authenticate, setAgentInfo } from "#lib/server/middlewares/index.ts"; -import { FileDeletionService, UploadService } from "#lib/server/services/index.ts"; +import { + AuthService, + ClientService, + FileDeletionService, + UploadService, +} from "#lib/server/services/index.ts"; export const init: ServerInit = async () => { await migrateDB(); @@ -11,9 +15,8 @@ export const init: ServerInit = async () => { schedule.scheduleJob("0 * * * *", async () => { // Failures are logged and retried at the next run, so they don't crash the server const results = await Promise.allSettled([ - ClientRepo.cleanupExpiredUserClientChallenges(), - SessionRepo.cleanupExpiredSessions(), - SessionRepo.cleanupExpiredSessionUpgradeChallenges(), + AuthService.cleanupExpiredSessions(), + ClientService.cleanupExpiredChallenges(), UploadService.cleanupExpiredUploadSessions(), ]); results.forEach((result) => { diff --git a/src/lib/client/media/heic.ts b/src/lib/client/media/heic.ts new file mode 100644 index 0000000..4201dd5 --- /dev/null +++ b/src/lib/client/media/heic.ts @@ -0,0 +1,5 @@ +// Chromium can't decode HEIC images, so they are converted to be displayed +export const convertHeic = async (blob: Blob, toType: "image/png" | "image/jpeg") => { + const { default: heic2any } = await import("heic2any"); + return (await heic2any({ blob, toType })) as Blob; +}; diff --git a/src/lib/client/media/thumbnail.ts b/src/lib/client/media/thumbnail.ts index 07d3ce8..14ed9f8 100644 --- a/src/lib/client/media/thumbnail.ts +++ b/src/lib/client/media/thumbnail.ts @@ -1,3 +1,5 @@ +import { convertHeic } from "./heic.ts"; + const scaleSize = (width: number, height: number, targetSize: number) => { if (width <= targetSize || height <= targetSize) { return { width, height }; @@ -111,11 +113,6 @@ const generateVideoThumbnail = (videoUrl: string, time = 0) => { }); }; -const convertHeicToPng = async (blob: Blob) => { - const { default: heic2any } = await import("heic2any"); - return (await heic2any({ blob, toType: "image/png" })) as Blob; -}; - // Generates a thumbnail of the image or video at the URL. Returns `null` if not supported. export const generateThumbnailFromUrl = async (url: string, contentType: string) => { try { @@ -125,8 +122,8 @@ export const generateThumbnailFromUrl = async (url: string, contentType: string) } catch { if (contentType !== "image/heic") return null; - // Chromium can't decode HEIC images - const png = URL.createObjectURL(await convertHeicToPng(await (await fetch(url)).blob())); + const blob = await (await fetch(url)).blob(); + const png = URL.createObjectURL(await convertHeic(blob, "image/png")); try { return await generateImageThumbnail(png); } finally { diff --git a/src/lib/client/service-worker.ts b/src/lib/client/service-worker.ts index c3063ee..094a8ad 100644 --- a/src/lib/client/service-worker.ts +++ b/src/lib/client/service-worker.ts @@ -1,6 +1,8 @@ +import { LRUCache } from "lru-cache"; import { handleDownloadProgress } from "#lib/client/transfer/download.svelte.ts"; import { DECRYPTED_FILE_URL_PREFIX, + MAX_PREPARED_FILES, type FileMetadata, type ServiceWorkerMessage, type ServiceWorkerResponse, @@ -10,7 +12,7 @@ const PREPARE_TIMEOUT_MS = 5000; // The service worker may be terminated at any time, losing its in-memory metadata. // Keep a copy here so that it can be re-requested on demand. -const preparedFiles = new Map(); +const preparedFiles = new LRUCache({ max: MAX_PREPARED_FILES }); let isListening = false; export const listenForServiceWorkerMessages = () => { diff --git a/src/lib/client/transfer/upload.svelte.ts b/src/lib/client/transfer/upload.svelte.ts index 4d51983..50d8c86 100644 --- a/src/lib/client/transfer/upload.svelte.ts +++ b/src/lib/client/transfer/upload.svelte.ts @@ -20,43 +20,43 @@ import { getChunkCount } from "#lib/shared/format/index.ts"; import { Scheduler } from "#lib/utils/index.ts"; import { trpc } from "#trpc/client.ts"; +const LIVE_STATUSES = [ + "queued", + "encryption-pending", + "encrypting", + "upload-pending", + "uploading", +] as const; + +type LiveStatus = (typeof LIVE_STATUSES)[number]; + export interface FileUploadState { id: string; name: string; parentId: DirectoryId; - status: - | "queued" - | "encryption-pending" - | "encrypting" - | "upload-pending" - | "uploading" - | "uploaded" - | "canceled" - | "error"; + status: LiveStatus | "uploaded" | "canceled" | "error"; progress?: number; rate?: number; estimated?: number; } -export type LiveFileUploadState = FileUploadState & { - status: "queued" | "encryption-pending" | "encrypting" | "upload-pending" | "uploading"; -}; +export type LiveFileUploadState = FileUploadState & { status: LiveStatus }; const scheduler = new Scheduler<{ fileId: number; thumbnailBuffer?: ArrayBuffer } | undefined>(); let uploadingFiles: FileUploadState[] = $state([]); -const isFileUploading = (status: FileUploadState["status"]) => - ["queued", "encryption-pending", "encrypting", "upload-pending", "uploading"].includes(status); +const isFileUploading = (file: FileUploadState): file is LiveFileUploadState => { + return (LIVE_STATUSES as readonly string[]).includes(file.status); +}; export const getUploadingFiles = (parentId?: DirectoryId) => { - return uploadingFiles.filter( - (file) => - (parentId === undefined || file.parentId === parentId) && isFileUploading(file.status), - ); + return uploadingFiles + .filter(isFileUploading) + .filter((file) => parentId === undefined || file.parentId === parentId); }; export const clearUploadedFiles = () => { - uploadingFiles = uploadingFiles.filter((file) => isFileUploading(file.status)); + uploadingFiles = uploadingFiles.filter(isFileUploading); }; const requestDuplicateFileScan = limitFunction( diff --git a/src/lib/features/favorites/index.ts b/src/lib/features/favorites/index.ts index 3f1fafc..da0efa6 100644 --- a/src/lib/features/favorites/index.ts +++ b/src/lib/features/favorites/index.ts @@ -1,6 +1,8 @@ import { getDirectoryInfo, getFileInfo, + isExistingFileInfo, + isLocalDirectoryInfo, setDirectoryFavorite, setFileFavorite, type LocalDirectoryInfo, @@ -29,18 +31,16 @@ export const getFavoriteEntries = async () => { ]); return [ ...sortEntries( - directories.map((directory): FavoriteEntry => ({ + directories.filter(isLocalDirectoryInfo).map((directory): FavoriteEntry => ({ type: "directory", - name: directory.name!, - details: directory as LocalDirectoryInfo, + name: directory.name, + details: directory, })), ), ...sortEntries( - files.map((file): FavoriteEntry => ({ - type: "file", - name: file.name!, - details: file as SummarizedFileInfo, - })), + files + .filter(isExistingFileInfo) + .map((file): FavoriteEntry => ({ type: "file", name: file.name, details: file })), ), ]; }; diff --git a/src/lib/features/filesystem/category.ts b/src/lib/features/filesystem/category.ts index f4ec339..a98b2ad 100644 --- a/src/lib/features/filesystem/category.ts +++ b/src/lib/features/filesystem/category.ts @@ -2,12 +2,8 @@ import * as IndexedDB from "#lib/client/storage/indexed-db/index.ts"; import { isTRPCClientError, trpc } from "#trpc/client.ts"; import { FilesystemCache } from "./cache.svelte.ts"; import { invalidateFile } from "./file.ts"; -import { - decryptCategoryMetadata, - decryptFileMetadata, - encryptName, - generateEntryKeys, -} from "./metadata.ts"; +import { toCategoryInfo, toFileInfo } from "./dto.ts"; +import { encryptName, generateEntryKeys } from "./metadata.ts"; import { useResource } from "./resource.svelte.ts"; import type { CategoryInfo, DataKey, LocalCategoryInfo, MaybeCategoryInfo } from "./types.ts"; @@ -60,38 +56,25 @@ const cache = new FilesystemCache({ try { const category = await trpc.category.get.query({ id, recurse: true }); const [subCategories, files, metadata] = await Promise.all([ + Promise.all(category.subCategories.map(toCategoryInfo)), Promise.all( - category.subCategories.map(async (category) => ({ - id: category.id, - parentId: id, - ...(await decryptCategoryMetadata(category)), + (category.files ?? []).map(async (file) => ({ + ...(await toFileInfo(file)), + isRecursive: file.isRecursive, })), ), - category.files && - Promise.all( - category.files.map(async (file) => ({ - id: file.id, - parentId: file.parent, - contentType: file.contentType, - isFavorite: file.isFavorite, - isRecursive: file.isRecursive, - ...(await decryptFileMetadata(file)), - })), - ), - category.metadata && decryptCategoryMetadata(category.metadata), + category.metadata && toCategoryInfo(category.metadata), ]); return storeToIndexedDB( - id !== "root" + metadata ? { - id, - parentId: category.metadata!.parent, + ...metadata, subCategories, - files: files!, + files, isFileRecursive: cachedInfo?.isFileRecursive ?? false, - ...metadata!, } - : { id, subCategories }, + : { id: "root", subCategories }, ); } catch (e) { if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") { diff --git a/src/lib/features/filesystem/components/FileThumbnailButton.svelte b/src/lib/features/filesystem/components/FileThumbnailButton.svelte index ca19e4c..0e94419 100644 --- a/src/lib/features/filesystem/components/FileThumbnailButton.svelte +++ b/src/lib/features/filesystem/components/FileThumbnailButton.svelte @@ -1,6 +1,7 @@ +{/if} diff --git a/src/routes/(main)/directory/[[id]]/directory-entries/DirectoryEntries.svelte b/src/routes/(main)/directory/[[id=int]]/directory-entries/DirectoryEntries.svelte similarity index 97% rename from src/routes/(main)/directory/[[id]]/directory-entries/DirectoryEntries.svelte rename to src/routes/(main)/directory/[[id=int]]/directory-entries/DirectoryEntries.svelte index 7094bb5..a9a785f 100644 --- a/src/routes/(main)/directory/[[id]]/directory-entries/DirectoryEntries.svelte +++ b/src/routes/(main)/directory/[[id=int]]/directory-entries/DirectoryEntries.svelte @@ -49,7 +49,7 @@ ...sortEntries(info.subDirectories.map(toEntry("directory"))), ...sortEntries([ ...info.files.map(toEntry("file")), - ...(getUploadingFiles(info.id) as LiveFileUploadState[]).map(toEntry("uploading-file")), + ...getUploadingFiles(info.id).map(toEntry("uploading-file")), ]), ]); diff --git a/src/routes/(main)/directory/[[id]]/directory-entries/UploadingFile.svelte b/src/routes/(main)/directory/[[id=int]]/directory-entries/UploadingFile.svelte similarity index 53% rename from src/routes/(main)/directory/[[id]]/directory-entries/UploadingFile.svelte rename to src/routes/(main)/directory/[[id=int]]/directory-entries/UploadingFile.svelte index b57403e..79b699c 100644 --- a/src/routes/(main)/directory/[[id]]/directory-entries/UploadingFile.svelte +++ b/src/routes/(main)/directory/[[id=int]]/directory-entries/UploadingFile.svelte @@ -1,6 +1,6 @@ - -{#if downloadingFiles.length > 0} - -{/if} diff --git a/src/routes/(main)/directory/[[id]]/UploadStatusCard.svelte b/src/routes/(main)/directory/[[id]]/UploadStatusCard.svelte deleted file mode 100644 index b356e77..0000000 --- a/src/routes/(main)/directory/[[id]]/UploadStatusCard.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -{#if uploadingFiles.length > 0} - -{/if} diff --git a/src/routes/(main)/home/+page.svelte b/src/routes/(main)/home/+page.svelte index 66cc8ef..c1a68d5 100644 --- a/src/routes/(main)/home/+page.svelte +++ b/src/routes/(main)/home/+page.svelte @@ -2,14 +2,16 @@ import { onMount } from "svelte"; import { goto } from "$app/navigation"; import { FileThumbnailButton } from "#lib/features/filesystem/components/index.ts"; - import { type SummarizedFileInfo } from "#lib/features/filesystem/index.ts"; + import { + getRecentMediaFileInfos, + type SummarizedFileInfo, + } from "#lib/features/filesystem/index.ts"; import { EntryButton } from "#lib/ui/index.ts"; - import { requestFreshMediaFilesRetrieval } from "./service.ts"; let mediaFiles: SummarizedFileInfo[] = $state([]); onMount(async () => { - mediaFiles = await requestFreshMediaFilesRetrieval(); + mediaFiles = await getRecentMediaFileInfos(8); }); diff --git a/src/routes/(main)/home/service.ts b/src/routes/(main)/home/service.ts deleted file mode 100644 index 6673323..0000000 --- a/src/routes/(main)/home/service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { getAllFileInfos } from "#lib/client/storage/indexed-db/index.ts"; - -export const requestFreshMediaFilesRetrieval = async (limit = 8) => { - const files = await getAllFileInfos(); - files.sort( - (a, b) => - (b.createdAt ?? b.lastModifiedAt).getTime() - (a.createdAt ?? a.lastModifiedAt).getTime(), - ); - return files - .filter( - ({ contentType }) => contentType.startsWith("image/") || contentType.startsWith("video/"), - ) - .slice(0, limit); -}; diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 7ed68b6..f3d8e93 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,5 +1,6 @@ import { redirect } from "@sveltejs/kit"; import { keyring } from "#lib/client/keys/index.ts"; +import { withRedirectPath } from "#lib/shared/redirect.ts"; import type { LayoutLoad } from "./$types"; export const ssr = false; @@ -9,10 +10,10 @@ const KEYLESS_PATHS = ["/auth/login", "/key", "/client/pending"]; export const load: LayoutLoad = ({ url }) => { if (KEYLESS_PATHS.some((path) => url.pathname.startsWith(path))) return; - const redirectQuery = `?redirect=${encodeURIComponent(url.pathname + url.search)}`; + const redirectPath = url.pathname + url.search; if (!keyring.clientKeys) { - redirect(307, "/key/generate" + redirectQuery); + redirect(307, withRedirectPath("/key/generate", redirectPath)); } else if (!keyring.hasMasterKeys) { - redirect(307, "/client/pending" + redirectQuery); + redirect(307, withRedirectPath("/client/pending", redirectPath)); } }; diff --git a/src/routes/api/file/[id]/[[thumbnail=thumbnail]]/download/+server.ts b/src/routes/api/file/[id=int]/[[thumbnail=thumbnail]]/download/+server.ts similarity index 84% rename from src/routes/api/file/[id]/[[thumbnail=thumbnail]]/download/+server.ts rename to src/routes/api/file/[id=int]/[[thumbnail=thumbnail]]/download/+server.ts index b5a1f2c..b4179d7 100644 --- a/src/routes/api/file/[id]/[[thumbnail=thumbnail]]/download/+server.ts +++ b/src/routes/api/file/[id=int]/[[thumbnail=thumbnail]]/download/+server.ts @@ -1,5 +1,3 @@ -import { error } from "@sveltejs/kit"; -import { z } from "zod"; import { authorize } from "#lib/server/auth.ts"; import { handleAppErrors } from "#lib/server/http.ts"; import { FileService } from "#lib/server/services/index.ts"; @@ -8,14 +6,7 @@ import type { RequestEvent, RequestHandler } from "./$types"; const downloadHandler = async ({ locals, params, request }: RequestEvent) => { 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 { id } = params; const getStream = params.thumbnail ? FileService.getFileThumbnailStream diff --git a/src/service-worker/stores.ts b/src/service-worker/stores.ts index 13e8e10..8ffa226 100644 --- a/src/service-worker/stores.ts +++ b/src/service-worker/stores.ts @@ -1,6 +1,4 @@ -import type { FileMetadata } from "#lib/shared/service-worker.ts"; - -const MAX_FILE_METADATA_ENTRIES = 32; +import { MAX_PREPARED_FILES, type FileMetadata } from "#lib/shared/service-worker.ts"; const fileMetadata = new Map(); @@ -18,7 +16,7 @@ export const fileMetadataStore = { fileMetadata.delete(fileId); fileMetadata.set(fileId, metadata); - while (fileMetadata.size > MAX_FILE_METADATA_ENTRIES) { + while (fileMetadata.size > MAX_PREPARED_FILES) { fileMetadata.delete(fileMetadata.keys().next().value!); } }, diff --git a/src/trpc/client.ts b/src/trpc/client.ts index 03dc738..c8837fa 100644 --- a/src/trpc/client.ts +++ b/src/trpc/client.ts @@ -3,6 +3,7 @@ import { observable } from "@trpc/server/observable"; import superjson from "superjson"; import { goto } from "$app/navigation"; import { navigating } from "$app/state"; +import { withRedirectPath } from "#lib/shared/redirect.ts"; import type { AppRouter } from "./router.server.ts"; // Procedures that report invalid credentials as UNAUTHORIZED, not an expired session @@ -21,7 +22,7 @@ const sessionExpiryLink: TRPCLink = () => { !CREDENTIAL_PROCEDURES.has(op.path) && url.pathname !== "/auth/login" ) { - void goto(`/auth/login?redirect=${encodeURIComponent(url.pathname + url.search)}`); + void goto(withRedirectPath("/auth/login", url.pathname + url.search)); } observer.error(err); }, diff --git a/src/trpc/dto.ts b/src/trpc/dto.ts index 84e278c..33eb55f 100644 --- a/src/trpc/dto.ts +++ b/src/trpc/dto.ts @@ -1,6 +1,8 @@ -import type { Category } from "#lib/server/db/repositories/category.ts"; -import type { Directory } from "#lib/server/db/repositories/directory.ts"; -import type { File } from "#lib/server/db/repositories/file.ts"; +import type { CategoryRepo, DirectoryRepo, FileRepo } from "#lib/server/db/index.ts"; + +type Category = CategoryRepo.Category; +type Directory = DirectoryRepo.Directory; +type File = FileRepo.File; // Shapes of filesystem entries exposed by the API diff --git a/src/trpc/init.server.ts b/src/trpc/init.server.ts index f9294bd..fd849b3 100644 --- a/src/trpc/init.server.ts +++ b/src/trpc/init.server.ts @@ -30,15 +30,9 @@ export const publicProcedure = t.procedure.use(async ({ next }) => { return result; }); -const authedProcedure = publicProcedure.use(async ({ ctx, next }) => { - if (!ctx.locals.session) { - throw new TRPCError({ code: "UNAUTHORIZED" }); - } - return next(); -}); - +// `authorize` also rejects unauthenticated requests const sessionProcedure = (requiredPermission: "any" | "notClient") => - authedProcedure.use(async ({ ctx, next }) => { + publicProcedure.use(async ({ ctx, next }) => { const session: Session = await authorize(ctx.locals, requiredPermission); return next({ ctx: { session } }); }); @@ -46,7 +40,7 @@ const sessionProcedure = (requiredPermission: "any" | "notClient") => const clientSessionProcedure = ( requiredPermission: "anyClient" | "pendingClient" | "activeClient", ) => - authedProcedure.use(async ({ ctx, next }) => { + publicProcedure.use(async ({ ctx, next }) => { const session: ClientSession = await authorize(ctx.locals, requiredPermission); return next({ ctx: { session } }); }); diff --git a/src/trpc/schemas.ts b/src/trpc/schemas.ts index 5d6f3c2..3236f63 100644 --- a/src/trpc/schemas.ts +++ b/src/trpc/schemas.ts @@ -2,7 +2,7 @@ import { z } from "zod"; export const IdSchema = z.int().positive(); export const DirectoryIdSchema = z.union([z.literal("root"), IdSchema]); -export const CategoryIdSchema = z.union([z.literal("root"), IdSchema]); +export const CategoryIdSchema = DirectoryIdSchema; export const Base64Schema = z.base64().nonempty(); export const EncryptedNameSchema = {