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 @@