From aee87fd9b64709c202057f4696db2dd0c60746df Mon Sep 17 00:00:00 2001 From: static Date: Mon, 12 Oct 2026 00:04:56 +0900 Subject: [PATCH] =?UTF-8?q?=EC=84=9C=EB=B2=84,=20=ED=81=B4=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EC=96=B8=ED=8A=B8,=20=EC=84=9C=EB=B9=84=EC=8A=A4=20?= =?UTF-8?q?=EC=9B=8C=EC=BB=A4=20=EA=B3=B5=EC=9A=A9=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20lib/shared=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EA=B3=A0=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20=EC=A0=84?= =?UTF-8?q?=EC=9A=A9=20=EB=AA=A8=EB=93=88=EC=9D=84=20lib/client=EB=A1=9C?= =?UTF-8?q?=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- src/hooks.client.ts | 6 +- .../cache.ts => client/cache/fileCache.ts} | 4 +- src/lib/client/cache/index.ts | 2 + .../cache/thumbnailCache.ts} | 6 +- .../crypto/sha.ts => client/crypto/hmac.ts} | 20 +----- src/lib/client/media/exif.ts | 27 ++++++++ .../{modules => client/media}/thumbnail.ts | 2 +- .../client.ts => client/serviceWorker.ts} | 8 ++- .../storage}/indexedDB/cacheIndex.ts | 0 .../storage}/indexedDB/filesystem.ts | 0 .../{ => client/storage}/indexedDB/index.ts | 0 .../storage}/indexedDB/keyStore.ts | 0 src/lib/{modules => client/storage}/opfs.ts | 0 .../transfer/chunkedUpload.ts} | 10 +-- .../transfer}/download.svelte.ts | 4 +- .../file => client/transfer}/index.ts | 2 - .../file => client/transfer}/upload.svelte.ts | 40 ++---------- .../atoms/buttons/FileThumbnailButton.svelte | 2 +- src/lib/constants/index.ts | 2 - src/lib/constants/serviceWorker.ts | 1 - src/lib/constants/upload.ts | 6 -- src/lib/modules/filesystem/category.ts | 2 +- src/lib/modules/filesystem/directory.ts | 2 +- src/lib/modules/filesystem/file.ts | 2 +- src/lib/modules/filesystem/internal.svelte.ts | 2 +- src/lib/modules/http.ts | 14 ---- src/lib/modules/key.ts | 2 +- src/lib/server/services/file.ts | 11 ++-- src/lib/server/storage/chunks.ts | 2 +- src/lib/server/storage/streams.ts | 14 ++-- src/lib/serviceWorker/index.ts | 2 - src/lib/services/auth.ts | 2 +- src/lib/services/category.ts | 2 +- src/lib/services/file.ts | 8 +-- src/lib/services/key.ts | 6 +- src/lib/{modules => shared}/crypto/aes.ts | 4 +- .../utils.ts => shared/crypto/encoding.ts} | 0 src/lib/{modules => shared}/crypto/index.ts | 2 +- src/lib/{modules => shared}/crypto/rsa.ts | 2 +- src/lib/shared/crypto/sha.ts | 16 +++++ .../crypto.ts => lib/shared/format/chunk.ts} | 43 +++++++++---- src/lib/shared/format/index.ts | 1 + src/lib/shared/http.ts | 35 ++++++++++ .../types.ts => shared/serviceWorker.ts} | 2 + .../(fullscreen)/client/pending/service.ts | 2 +- .../(fullscreen)/file/[id]/+page.svelte | 4 +- .../file/[id]/DownloadStatus.svelte | 2 +- src/routes/(fullscreen)/file/[id]/service.ts | 4 +- .../(fullscreen)/file/downloads/+page.svelte | 2 +- .../(fullscreen)/file/downloads/File.svelte | 2 +- .../(fullscreen)/file/uploads/+page.svelte | 2 +- .../(fullscreen)/file/uploads/File.svelte | 2 +- .../(fullscreen)/key/generate/service.ts | 4 +- src/routes/(fullscreen)/search/File.svelte | 2 +- .../(fullscreen)/settings/cache/+page.svelte | 4 +- .../(fullscreen)/settings/cache/File.svelte | 2 +- .../settings/thumbnail/+page.svelte | 2 +- .../settings/thumbnail/service.ts | 4 +- .../(main)/category/[[id]]/+page.svelte | 2 +- src/routes/(main)/category/[[id]]/File.svelte | 2 +- .../(main)/category/[[id]]/service.svelte.ts | 2 +- .../DirectoryEntries/DirectoryEntries.svelte | 2 +- .../[[id]]/DirectoryEntries/File.svelte | 2 +- .../DirectoryEntries/UploadingFile.svelte | 2 +- .../[[id]]/DownloadStatusCard.svelte | 2 +- .../directory/[[id]]/UploadStatusCard.svelte | 2 +- .../(main)/directory/[[id]]/service.svelte.ts | 18 +++--- src/routes/(main)/favorites/File.svelte | 2 +- src/routes/(main)/home/service.ts | 2 +- src/routes/+layout.svelte | 2 +- .../download/+server.ts | 2 +- src/service-worker/constants.ts | 1 - src/service-worker/handlers/decryptFile.ts | 64 ++++++++++++------- src/service-worker/index.ts | 7 +- src/service-worker/modules/http.ts | 1 - src/service-worker/modules/metadata.ts | 2 +- src/service-worker/modules/opfs.ts | 1 - src/service-worker/stores.ts | 2 +- src/service-worker/types.ts | 1 - tests/server/download.test.ts | 19 ++++++ tests/unit/crypto.test.ts | 2 +- tests/unit/range.test.ts | 49 +++++++++++--- 82 files changed, 326 insertions(+), 221 deletions(-) rename src/lib/{modules/file/cache.ts => client/cache/fileCache.ts} (91%) create mode 100644 src/lib/client/cache/index.ts rename src/lib/{modules/file/thumbnail.ts => client/cache/thumbnailCache.ts} (94%) rename src/lib/{modules/crypto/sha.ts => client/crypto/hmac.ts} (67%) create mode 100644 src/lib/client/media/exif.ts rename src/lib/{modules => client/media}/thumbnail.ts (98%) rename src/lib/{serviceWorker/client.ts => client/serviceWorker.ts} (90%) rename src/lib/{ => client/storage}/indexedDB/cacheIndex.ts (100%) rename src/lib/{ => client/storage}/indexedDB/filesystem.ts (100%) rename src/lib/{ => client/storage}/indexedDB/index.ts (100%) rename src/lib/{ => client/storage}/indexedDB/keyStore.ts (100%) rename src/lib/{modules => client/storage}/opfs.ts (100%) rename src/lib/{modules/upload.ts => client/transfer/chunkedUpload.ts} (93%) rename src/lib/{modules/file => client/transfer}/download.svelte.ts (95%) rename src/lib/{modules/file => client/transfer}/index.ts (55%) rename src/lib/{modules/file => client/transfer}/upload.svelte.ts (81%) delete mode 100644 src/lib/constants/index.ts delete mode 100644 src/lib/constants/serviceWorker.ts delete mode 100644 src/lib/constants/upload.ts delete mode 100644 src/lib/modules/http.ts delete mode 100644 src/lib/serviceWorker/index.ts rename src/lib/{modules => shared}/crypto/aes.ts (97%) rename src/lib/{modules/crypto/utils.ts => shared/crypto/encoding.ts} (100%) rename src/lib/{modules => shared}/crypto/index.ts (71%) rename src/lib/{modules => shared}/crypto/rsa.ts (99%) create mode 100644 src/lib/shared/crypto/sha.ts rename src/{service-worker/modules/crypto.ts => lib/shared/format/chunk.ts} (58%) create mode 100644 src/lib/shared/format/index.ts create mode 100644 src/lib/shared/http.ts rename src/lib/{serviceWorker/types.ts => shared/serviceWorker.ts} (90%) delete mode 100644 src/service-worker/constants.ts delete mode 100644 src/service-worker/modules/http.ts delete mode 100644 src/service-worker/modules/opfs.ts delete mode 100644 src/service-worker/types.ts diff --git a/src/hooks.client.ts b/src/hooks.client.ts index dcd160a..645c890 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,12 +1,12 @@ import type { ClientInit } from "@sveltejs/kit/hooks"; +import { prepareFileCache } from "#lib/client/cache/index.js"; +import { listenForMetadataRequests } from "#lib/client/serviceWorker.js"; import { cleanupDanglingInfos, getClientKey, getMasterKeys, getHmacSecrets, -} from "#lib/indexedDB/index.js"; -import { prepareFileCache } from "#lib/modules/file/index.js"; -import { listenForMetadataRequests } from "#lib/serviceWorker/index.js"; +} from "#lib/client/storage/indexedDB/index.js"; import { clientKeyStore, masterKeyStore, hmacSecretStore } from "#lib/stores/index.js"; const requestPersistentStorage = async () => { diff --git a/src/lib/modules/file/cache.ts b/src/lib/client/cache/fileCache.ts similarity index 91% rename from src/lib/modules/file/cache.ts rename to src/lib/client/cache/fileCache.ts index 921a770..487baa9 100644 --- a/src/lib/modules/file/cache.ts +++ b/src/lib/client/cache/fileCache.ts @@ -3,8 +3,8 @@ import { storeFileCacheIndex, deleteFileCacheIndex, type FileCacheIndex, -} from "#lib/indexedDB/index.js"; -import { readFile, writeFile, deleteFile } from "#lib/modules/opfs.js"; +} from "#lib/client/storage/indexedDB/index.js"; +import { readFile, writeFile, deleteFile } from "#lib/client/storage/opfs.js"; const fileCacheIndex = new Map(); diff --git a/src/lib/client/cache/index.ts b/src/lib/client/cache/index.ts new file mode 100644 index 0000000..66c72a3 --- /dev/null +++ b/src/lib/client/cache/index.ts @@ -0,0 +1,2 @@ +export * from "./fileCache"; +export * from "./thumbnailCache"; diff --git a/src/lib/modules/file/thumbnail.ts b/src/lib/client/cache/thumbnailCache.ts similarity index 94% rename from src/lib/modules/file/thumbnail.ts rename to src/lib/client/cache/thumbnailCache.ts index ae13ba6..ebdfcd6 100644 --- a/src/lib/modules/file/thumbnail.ts +++ b/src/lib/client/cache/thumbnailCache.ts @@ -1,9 +1,9 @@ import { LRUCache } from "lru-cache"; import { writable, type Writable } from "svelte/store"; -import { decryptChunk } from "#lib/modules/crypto/index.js"; +import { getThumbnailUrl } from "#lib/client/media/thumbnail.js"; +import { readFile, writeFile, deleteFile, deleteDirectory } from "#lib/client/storage/opfs.js"; import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js"; -import { readFile, writeFile, deleteFile, deleteDirectory } from "#lib/modules/opfs.js"; -import { getThumbnailUrl } from "#lib/modules/thumbnail.js"; +import { decryptChunk } from "#lib/shared/crypto/index.js"; const loadedThumbnails = new LRUCache>({ max: 100 }); const loadingThumbnails = new Map>(); diff --git a/src/lib/modules/crypto/sha.ts b/src/lib/client/crypto/hmac.ts similarity index 67% rename from src/lib/modules/crypto/sha.ts rename to src/lib/client/crypto/hmac.ts index aa31280..6df84ef 100644 --- a/src/lib/modules/crypto/sha.ts +++ b/src/lib/client/crypto/hmac.ts @@ -1,23 +1,7 @@ -import HmacWorker from "#workers/hmac.js?worker"; import type { ComputeMessage, ResultMessage } from "#workers/hmac.js"; +import HmacWorker from "#workers/hmac.js?worker"; -export const digestMessage = async (message: BufferSource) => { - return await crypto.subtle.digest("SHA-256", message); -}; - -export const generateHmacSecret = async () => { - return { - hmacSecret: await crypto.subtle.generateKey( - { - name: "HMAC", - hash: "SHA-256", - } satisfies HmacKeyGenParams, - true, - ["sign", "verify"], - ), - }; -}; - +// Computes HMAC of large blobs incrementally in a worker, which WebCrypto doesn't support export const signMessageHmac = async (message: Blob, hmacSecret: CryptoKey) => { const stream = message.stream(); const hmacSecretRaw = new Uint8Array(await crypto.subtle.exportKey("raw", hmacSecret)); diff --git a/src/lib/client/media/exif.ts b/src/lib/client/media/exif.ts new file mode 100644 index 0000000..f6b363f --- /dev/null +++ b/src/lib/client/media/exif.ts @@ -0,0 +1,27 @@ +import ExifReader from "exifreader"; + +export const extractExifDateTime = (fileBuffer: ArrayBuffer) => { + const exif = ExifReader.load(fileBuffer); + const dateTimeOriginal = exif["DateTimeOriginal"]?.description; + const offsetTimeOriginal = exif["OffsetTimeOriginal"]?.description; + if (!dateTimeOriginal) return undefined; + + const [date, time] = dateTimeOriginal.split(" "); + if (!date || !time) return undefined; + + const [year, month, day] = date.split(":").map(Number); + const [hour, minute, second] = time.split(":").map(Number); + if (!year || !month || !day || !hour || !minute || !second) return undefined; + + if (!offsetTimeOriginal) { + // No timezone information.. Assume local timezone + return new Date(year, month - 1, day, hour, minute, second); + } + + const offsetSign = offsetTimeOriginal[0] === "+" ? 1 : -1; + const [offsetHour, offsetMinute] = offsetTimeOriginal.slice(1).split(":").map(Number); + + const utcDate = Date.UTC(year, month - 1, day, hour, minute, second); + const offsetMs = offsetSign * ((offsetHour ?? 0) * 60 + (offsetMinute ?? 0)) * 60 * 1000; + return new Date(utcDate - offsetMs); +}; diff --git a/src/lib/modules/thumbnail.ts b/src/lib/client/media/thumbnail.ts similarity index 98% rename from src/lib/modules/thumbnail.ts rename to src/lib/client/media/thumbnail.ts index 6c7d6eb..09cd116 100644 --- a/src/lib/modules/thumbnail.ts +++ b/src/lib/client/media/thumbnail.ts @@ -1,4 +1,4 @@ -import { encodeToBase64 } from "#lib/modules/crypto/index.js"; +import { encodeToBase64 } from "#lib/shared/crypto/index.js"; const scaleSize = (width: number, height: number, targetSize: number) => { if (width <= targetSize || height <= targetSize) { diff --git a/src/lib/serviceWorker/client.ts b/src/lib/client/serviceWorker.ts similarity index 90% rename from src/lib/serviceWorker/client.ts rename to src/lib/client/serviceWorker.ts index 9627d4e..e4b86b6 100644 --- a/src/lib/serviceWorker/client.ts +++ b/src/lib/client/serviceWorker.ts @@ -1,5 +1,9 @@ -import { DECRYPTED_FILE_URL_PREFIX } from "#lib/constants/index.js"; -import type { FileMetadata, ServiceWorkerMessage, ServiceWorkerResponse } from "./types"; +import { DECRYPTED_FILE_URL_PREFIX } from "#lib/shared/serviceWorker.js"; +import type { + FileMetadata, + ServiceWorkerMessage, + ServiceWorkerResponse, +} from "#lib/shared/serviceWorker.js"; const PREPARE_TIMEOUT_MS = 5000; diff --git a/src/lib/indexedDB/cacheIndex.ts b/src/lib/client/storage/indexedDB/cacheIndex.ts similarity index 100% rename from src/lib/indexedDB/cacheIndex.ts rename to src/lib/client/storage/indexedDB/cacheIndex.ts diff --git a/src/lib/indexedDB/filesystem.ts b/src/lib/client/storage/indexedDB/filesystem.ts similarity index 100% rename from src/lib/indexedDB/filesystem.ts rename to src/lib/client/storage/indexedDB/filesystem.ts diff --git a/src/lib/indexedDB/index.ts b/src/lib/client/storage/indexedDB/index.ts similarity index 100% rename from src/lib/indexedDB/index.ts rename to src/lib/client/storage/indexedDB/index.ts diff --git a/src/lib/indexedDB/keyStore.ts b/src/lib/client/storage/indexedDB/keyStore.ts similarity index 100% rename from src/lib/indexedDB/keyStore.ts rename to src/lib/client/storage/indexedDB/keyStore.ts diff --git a/src/lib/modules/opfs.ts b/src/lib/client/storage/opfs.ts similarity index 100% rename from src/lib/modules/opfs.ts rename to src/lib/client/storage/opfs.ts diff --git a/src/lib/modules/upload.ts b/src/lib/client/transfer/chunkedUpload.ts similarity index 93% rename from src/lib/modules/upload.ts rename to src/lib/client/transfer/chunkedUpload.ts index 9bd5e05..8e96360 100644 --- a/src/lib/modules/upload.ts +++ b/src/lib/client/transfer/chunkedUpload.ts @@ -1,9 +1,9 @@ import { sha256 } from "@noble/hashes/sha2.js"; import axios from "axios"; import pLimit from "p-limit"; -import { ENCRYPTION_OVERHEAD, CHUNK_SIZE } from "#lib/constants/index.js"; -import { encodeToBase64, encryptChunk } from "#lib/modules/crypto/index.js"; -import { BoundedQueue } from "#lib/utils/index.js"; +import { encodeToBase64, encryptChunk } from "#lib/shared/crypto/index.js"; +import { CHUNK_SIZE, getChunkCount, getEncryptedSize } from "#lib/shared/format/index.js"; +import { BoundedQueue } from "#lib/utils/concurrency/index.js"; interface UploadStats { progress: number; @@ -98,8 +98,8 @@ export const uploadBlob = async ( const networkConcurrency = options?.concurrency ?? 4; const maxQueueSize = 8; - const totalChunks = Math.ceil(blob.size / CHUNK_SIZE); - const totalBytes = blob.size + totalChunks * ENCRYPTION_OVERHEAD; + const totalChunks = getChunkCount(blob.size); + const totalBytes = getEncryptedSize(blob.size); const uploadedByChunk = new Array(totalChunks).fill(0); const speedMeter = createSpeedMeter(3000, 200); diff --git a/src/lib/modules/file/download.svelte.ts b/src/lib/client/transfer/download.svelte.ts similarity index 95% rename from src/lib/modules/file/download.svelte.ts rename to src/lib/client/transfer/download.svelte.ts index 2158647..f5fe935 100644 --- a/src/lib/modules/file/download.svelte.ts +++ b/src/lib/client/transfer/download.svelte.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { limitFunction } from "p-limit"; -import { ENCRYPTED_CHUNK_SIZE } from "#lib/constants/index.js"; -import { decryptChunk, concatenateBuffers } from "#lib/modules/crypto/index.js"; +import { decryptChunk, concatenateBuffers } from "#lib/shared/crypto/index.js"; +import { ENCRYPTED_CHUNK_SIZE } from "#lib/shared/format/index.js"; export interface FileDownloadState { id: number; diff --git a/src/lib/modules/file/index.ts b/src/lib/client/transfer/index.ts similarity index 55% rename from src/lib/modules/file/index.ts rename to src/lib/client/transfer/index.ts index 9e9ce0c..a862c01 100644 --- a/src/lib/modules/file/index.ts +++ b/src/lib/client/transfer/index.ts @@ -1,4 +1,2 @@ -export * from "./cache"; export * from "./download.svelte"; -export * from "./thumbnail"; export * from "./upload.svelte"; diff --git a/src/lib/modules/file/upload.svelte.ts b/src/lib/client/transfer/upload.svelte.ts similarity index 81% rename from src/lib/modules/file/upload.svelte.ts rename to src/lib/client/transfer/upload.svelte.ts index c514bff..3c23ca8 100644 --- a/src/lib/modules/file/upload.svelte.ts +++ b/src/lib/client/transfer/upload.svelte.ts @@ -1,15 +1,15 @@ -import ExifReader from "exifreader"; import { limitFunction } from "p-limit"; -import { CHUNK_SIZE } from "#lib/constants/index.js"; +import { signMessageHmac } from "#lib/client/crypto/hmac.js"; +import { extractExifDateTime } from "#lib/client/media/exif.js"; +import { generateThumbnail } from "#lib/client/media/thumbnail.js"; +import { uploadBlob } from "#lib/client/transfer/chunkedUpload.js"; import { encodeToBase64, generateDataKey, wrapDataKey, encryptString, -} from "#lib/modules/crypto/index.js"; -import { signMessageHmac } from "#lib/modules/crypto/index.js"; -import { generateThumbnail } from "#lib/modules/thumbnail.js"; -import { uploadBlob } from "#lib/modules/upload.js"; +} from "#lib/shared/crypto/index.js"; +import { getChunkCount } from "#lib/shared/format/index.js"; import type { MasterKey, HmacSecret } from "#lib/stores/index.js"; import { Scheduler } from "#lib/utils/index.js"; import { trpc } from "#trpc/client.js"; @@ -84,32 +84,6 @@ const getFileType = (file: File) => { throw new Error("Unknown file type"); }; -const extractExifDateTime = (fileBuffer: ArrayBuffer) => { - const exif = ExifReader.load(fileBuffer); - const dateTimeOriginal = exif["DateTimeOriginal"]?.description; - const offsetTimeOriginal = exif["OffsetTimeOriginal"]?.description; - if (!dateTimeOriginal) return undefined; - - const [date, time] = dateTimeOriginal.split(" "); - if (!date || !time) return undefined; - - const [year, month, day] = date.split(":").map(Number); - const [hour, minute, second] = time.split(":").map(Number); - if (!year || !month || !day || !hour || !minute || !second) return undefined; - - if (!offsetTimeOriginal) { - // No timezone information.. Assume local timezone - return new Date(year, month - 1, day, hour, minute, second); - } - - const offsetSign = offsetTimeOriginal[0] === "+" ? 1 : -1; - const [offsetHour, offsetMinute] = offsetTimeOriginal.slice(1).split(":").map(Number); - - const utcDate = Date.UTC(year, month - 1, day, hour, minute, second); - const offsetMs = offsetSign * ((offsetHour ?? 0) * 60 + (offsetMinute ?? 0)) * 60 * 1000; - return new Date(utcDate - offsetMs); -}; - interface FileMetadata { parentId: "root" | number; name: string; @@ -140,7 +114,7 @@ const requestFileMetadataEncryption = limitFunction( ]); const { uploadId } = await trpc.upload.startFileUpload.mutate({ - chunks: Math.ceil(file.size / CHUNK_SIZE), + chunks: getChunkCount(file.size), parent: fileMetadata.parentId, mekVersion: masterKey.version, dek: dataKeyWrapped, diff --git a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte index 6b9d2a9..d543446 100644 --- a/src/lib/components/atoms/buttons/FileThumbnailButton.svelte +++ b/src/lib/components/atoms/buttons/FileThumbnailButton.svelte @@ -1,5 +1,5 @@