mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-11-04 08:10:28 +09:00
서버, 클라이언트, 서비스 워커 공용 코드를 lib/shared로 분리하고 브라우저 전용 모듈을 lib/client로 재구성
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6a6aa1765b
commit
aee87fd9b6
82 files changed
+326
-221
No files matched your search
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
import type { ClientInit } from "@sveltejs/kit/hooks";
|
import type { ClientInit } from "@sveltejs/kit/hooks";
|
||||||
|
import { prepareFileCache } from "#lib/client/cache/index.js";
|
||||||
|
import { listenForMetadataRequests } from "#lib/client/serviceWorker.js";
|
||||||
import {
|
import {
|
||||||
cleanupDanglingInfos,
|
cleanupDanglingInfos,
|
||||||
getClientKey,
|
getClientKey,
|
||||||
getMasterKeys,
|
getMasterKeys,
|
||||||
getHmacSecrets,
|
getHmacSecrets,
|
||||||
} from "#lib/indexedDB/index.js";
|
} from "#lib/client/storage/indexedDB/index.js";
|
||||||
import { prepareFileCache } from "#lib/modules/file/index.js";
|
|
||||||
import { listenForMetadataRequests } from "#lib/serviceWorker/index.js";
|
|
||||||
import { clientKeyStore, masterKeyStore, hmacSecretStore } from "#lib/stores/index.js";
|
import { clientKeyStore, masterKeyStore, hmacSecretStore } from "#lib/stores/index.js";
|
||||||
|
|
||||||
const requestPersistentStorage = async () => {
|
const requestPersistentStorage = async () => {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import {
|
|||||||
storeFileCacheIndex,
|
storeFileCacheIndex,
|
||||||
deleteFileCacheIndex,
|
deleteFileCacheIndex,
|
||||||
type FileCacheIndex,
|
type FileCacheIndex,
|
||||||
} from "#lib/indexedDB/index.js";
|
} from "#lib/client/storage/indexedDB/index.js";
|
||||||
import { readFile, writeFile, deleteFile } from "#lib/modules/opfs.js";
|
import { readFile, writeFile, deleteFile } from "#lib/client/storage/opfs.js";
|
||||||
|
|
||||||
const fileCacheIndex = new Map<number, FileCacheIndex>();
|
const fileCacheIndex = new Map<number, FileCacheIndex>();
|
||||||
|
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./fileCache";
|
||||||
|
export * from "./thumbnailCache";
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { LRUCache } from "lru-cache";
|
import { LRUCache } from "lru-cache";
|
||||||
import { writable, type Writable } from "svelte/store";
|
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 type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { readFile, writeFile, deleteFile, deleteDirectory } from "#lib/modules/opfs.js";
|
import { decryptChunk } from "#lib/shared/crypto/index.js";
|
||||||
import { getThumbnailUrl } from "#lib/modules/thumbnail.js";
|
|
||||||
|
|
||||||
const loadedThumbnails = new LRUCache<number, Writable<string>>({ max: 100 });
|
const loadedThumbnails = new LRUCache<number, Writable<string>>({ max: 100 });
|
||||||
const loadingThumbnails = new Map<number, Writable<string | undefined>>();
|
const loadingThumbnails = new Map<number, Writable<string | undefined>>();
|
||||||
@@ -1,23 +1,7 @@
|
|||||||
import HmacWorker from "#workers/hmac.js?worker";
|
|
||||||
import type { ComputeMessage, ResultMessage } from "#workers/hmac.js";
|
import type { ComputeMessage, ResultMessage } from "#workers/hmac.js";
|
||||||
|
import HmacWorker from "#workers/hmac.js?worker";
|
||||||
|
|
||||||
export const digestMessage = async (message: BufferSource) => {
|
// Computes HMAC of large blobs incrementally in a worker, which WebCrypto doesn't support
|
||||||
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"],
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const signMessageHmac = async (message: Blob, hmacSecret: CryptoKey) => {
|
export const signMessageHmac = async (message: Blob, hmacSecret: CryptoKey) => {
|
||||||
const stream = message.stream();
|
const stream = message.stream();
|
||||||
const hmacSecretRaw = new Uint8Array(await crypto.subtle.exportKey("raw", hmacSecret));
|
const hmacSecretRaw = new Uint8Array(await crypto.subtle.exportKey("raw", hmacSecret));
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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) => {
|
const scaleSize = (width: number, height: number, targetSize: number) => {
|
||||||
if (width <= targetSize || height <= targetSize) {
|
if (width <= targetSize || height <= targetSize) {
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
import { DECRYPTED_FILE_URL_PREFIX } from "#lib/constants/index.js";
|
import { DECRYPTED_FILE_URL_PREFIX } from "#lib/shared/serviceWorker.js";
|
||||||
import type { FileMetadata, ServiceWorkerMessage, ServiceWorkerResponse } from "./types";
|
import type {
|
||||||
|
FileMetadata,
|
||||||
|
ServiceWorkerMessage,
|
||||||
|
ServiceWorkerResponse,
|
||||||
|
} from "#lib/shared/serviceWorker.js";
|
||||||
|
|
||||||
const PREPARE_TIMEOUT_MS = 5000;
|
const PREPARE_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,9 +1,9 @@
|
|||||||
import { sha256 } from "@noble/hashes/sha2.js";
|
import { sha256 } from "@noble/hashes/sha2.js";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import pLimit from "p-limit";
|
import pLimit from "p-limit";
|
||||||
import { ENCRYPTION_OVERHEAD, CHUNK_SIZE } from "#lib/constants/index.js";
|
import { encodeToBase64, encryptChunk } from "#lib/shared/crypto/index.js";
|
||||||
import { encodeToBase64, encryptChunk } from "#lib/modules/crypto/index.js";
|
import { CHUNK_SIZE, getChunkCount, getEncryptedSize } from "#lib/shared/format/index.js";
|
||||||
import { BoundedQueue } from "#lib/utils/index.js";
|
import { BoundedQueue } from "#lib/utils/concurrency/index.js";
|
||||||
|
|
||||||
interface UploadStats {
|
interface UploadStats {
|
||||||
progress: number;
|
progress: number;
|
||||||
@@ -98,8 +98,8 @@ export const uploadBlob = async (
|
|||||||
const networkConcurrency = options?.concurrency ?? 4;
|
const networkConcurrency = options?.concurrency ?? 4;
|
||||||
const maxQueueSize = 8;
|
const maxQueueSize = 8;
|
||||||
|
|
||||||
const totalChunks = Math.ceil(blob.size / CHUNK_SIZE);
|
const totalChunks = getChunkCount(blob.size);
|
||||||
const totalBytes = blob.size + totalChunks * ENCRYPTION_OVERHEAD;
|
const totalBytes = getEncryptedSize(blob.size);
|
||||||
|
|
||||||
const uploadedByChunk = new Array<number>(totalChunks).fill(0);
|
const uploadedByChunk = new Array<number>(totalChunks).fill(0);
|
||||||
const speedMeter = createSpeedMeter(3000, 200);
|
const speedMeter = createSpeedMeter(3000, 200);
|
||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { limitFunction } from "p-limit";
|
import { limitFunction } from "p-limit";
|
||||||
import { ENCRYPTED_CHUNK_SIZE } from "#lib/constants/index.js";
|
import { decryptChunk, concatenateBuffers } from "#lib/shared/crypto/index.js";
|
||||||
import { decryptChunk, concatenateBuffers } from "#lib/modules/crypto/index.js";
|
import { ENCRYPTED_CHUNK_SIZE } from "#lib/shared/format/index.js";
|
||||||
|
|
||||||
export interface FileDownloadState {
|
export interface FileDownloadState {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
export * from "./cache";
|
|
||||||
export * from "./download.svelte";
|
export * from "./download.svelte";
|
||||||
export * from "./thumbnail";
|
|
||||||
export * from "./upload.svelte";
|
export * from "./upload.svelte";
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import ExifReader from "exifreader";
|
|
||||||
import { limitFunction } from "p-limit";
|
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 {
|
import {
|
||||||
encodeToBase64,
|
encodeToBase64,
|
||||||
generateDataKey,
|
generateDataKey,
|
||||||
wrapDataKey,
|
wrapDataKey,
|
||||||
encryptString,
|
encryptString,
|
||||||
} from "#lib/modules/crypto/index.js";
|
} from "#lib/shared/crypto/index.js";
|
||||||
import { signMessageHmac } from "#lib/modules/crypto/index.js";
|
import { getChunkCount } from "#lib/shared/format/index.js";
|
||||||
import { generateThumbnail } from "#lib/modules/thumbnail.js";
|
|
||||||
import { uploadBlob } from "#lib/modules/upload.js";
|
|
||||||
import type { MasterKey, HmacSecret } from "#lib/stores/index.js";
|
import type { MasterKey, HmacSecret } from "#lib/stores/index.js";
|
||||||
import { Scheduler } from "#lib/utils/index.js";
|
import { Scheduler } from "#lib/utils/index.js";
|
||||||
import { trpc } from "#trpc/client.js";
|
import { trpc } from "#trpc/client.js";
|
||||||
@@ -84,32 +84,6 @@ const getFileType = (file: File) => {
|
|||||||
throw new Error("Unknown file type");
|
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 {
|
interface FileMetadata {
|
||||||
parentId: "root" | number;
|
parentId: "root" | number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -140,7 +114,7 @@ const requestFileMetadataEncryption = limitFunction(
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const { uploadId } = await trpc.upload.startFileUpload.mutate({
|
const { uploadId } = await trpc.upload.startFileUpload.mutate({
|
||||||
chunks: Math.ceil(file.size / CHUNK_SIZE),
|
chunks: getChunkCount(file.size),
|
||||||
parent: fileMetadata.parentId,
|
parent: fileMetadata.parentId,
|
||||||
mekVersion: masterKey.version,
|
mekVersion: masterKey.version,
|
||||||
dek: dataKeyWrapped,
|
dek: dataKeyWrapped,
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getFileThumbnail } from "#lib/modules/file/index.js";
|
import { getFileThumbnail } from "#lib/client/cache/index.js";
|
||||||
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
|
|
||||||
import IconFavorite from "~icons/material-symbols/favorite";
|
import IconFavorite from "~icons/material-symbols/favorite";
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./serviceWorker";
|
|
||||||
export * from "./upload";
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const DECRYPTED_FILE_URL_PREFIX = "/_internal/decryptedFile/";
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export const AES_GCM_IV_SIZE = 12;
|
|
||||||
export const AES_GCM_TAG_SIZE = 16;
|
|
||||||
export const ENCRYPTION_OVERHEAD = AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE;
|
|
||||||
|
|
||||||
export const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MiB
|
|
||||||
export const ENCRYPTED_CHUNK_SIZE = CHUNK_SIZE + ENCRYPTION_OVERHEAD;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as IndexedDB from "#lib/indexedDB/index.js";
|
import * as IndexedDB from "#lib/client/storage/indexedDB/index.js";
|
||||||
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
||||||
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
||||||
import type { CategoryInfo, MaybeCategoryInfo } from "./types";
|
import type { CategoryInfo, MaybeCategoryInfo } from "./types";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as IndexedDB from "#lib/indexedDB/index.js";
|
import * as IndexedDB from "#lib/client/storage/indexedDB/index.js";
|
||||||
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
||||||
import { FilesystemCache, decryptDirectoryMetadata, decryptFileMetadata } from "./internal.svelte";
|
import { FilesystemCache, decryptDirectoryMetadata, decryptFileMetadata } from "./internal.svelte";
|
||||||
import type { DirectoryInfo, MaybeDirectoryInfo } from "./types";
|
import type { DirectoryInfo, MaybeDirectoryInfo } from "./types";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import * as IndexedDB from "#lib/indexedDB/index.js";
|
import * as IndexedDB from "#lib/client/storage/indexedDB/index.js";
|
||||||
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
||||||
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
import { FilesystemCache, decryptFileMetadata, decryptCategoryMetadata } from "./internal.svelte";
|
||||||
import type { FileInfo, MaybeFileInfo } from "./types";
|
import type { FileInfo, MaybeFileInfo } from "./types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { untrack } from "svelte";
|
import { untrack } from "svelte";
|
||||||
import { unwrapDataKey, decryptString } from "#lib/modules/crypto/index.js";
|
import { unwrapDataKey, decryptString } from "#lib/shared/crypto/index.js";
|
||||||
|
|
||||||
interface FilesystemCacheOptions<K, V> {
|
interface FilesystemCacheOptions<K, V> {
|
||||||
fetchFromIndexedDB: (key: K) => Promise<V | undefined>;
|
fetchFromIndexedDB: (key: K) => Promise<V | undefined>;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
export const parseRangeHeader = (value: string | null) => {
|
|
||||||
if (!value) return undefined;
|
|
||||||
|
|
||||||
const firstRange = value.split(",")[0]!.trim();
|
|
||||||
const parts = firstRange.replace(/bytes=/, "").split("-");
|
|
||||||
return {
|
|
||||||
start: parts[0] ? parseInt(parts[0], 10) : undefined,
|
|
||||||
end: parts[1] ? parseInt(parts[1], 10) : undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getContentRangeHeader = (range?: { start: number; end: number; total: number }) => {
|
|
||||||
return range && { "Content-Range": `bytes ${range.start}-${range.end}/${range.total}` };
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { storeClientKey } from "#lib/indexedDB/index.js";
|
import { storeClientKey } from "#lib/client/storage/indexedDB/index.js";
|
||||||
import type { ClientKeys } from "#lib/stores/index.js";
|
import type { ClientKeys } from "#lib/stores/index.js";
|
||||||
|
|
||||||
const SerializedClientKeysSchema = z.intersection(
|
const SerializedClientKeysSchema = z.intersection(
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { FileRepo, MediaRepo } from "#lib/server/db/index.js";
|
|||||||
import type { Ciphertext } from "#lib/server/db/schema/index.js";
|
import type { Ciphertext } from "#lib/server/db/schema/index.js";
|
||||||
import { AppError } from "#lib/server/errors.js";
|
import { AppError } from "#lib/server/errors.js";
|
||||||
import { createEncContentStream } from "#lib/server/storage/index.js";
|
import { createEncContentStream } from "#lib/server/storage/index.js";
|
||||||
|
import type { RangeRequest } from "#lib/shared/http.js";
|
||||||
import { schedulePendingFileDeletions } from "./fileDeletion";
|
import { schedulePendingFileDeletions } from "./fileDeletion";
|
||||||
|
|
||||||
type Range = { start?: number; end?: number };
|
|
||||||
|
|
||||||
export const getFile = async (userId: number, fileId: number) => {
|
export const getFile = async (userId: number, fileId: number) => {
|
||||||
const file = await FileRepo.getFile(userId, fileId);
|
const file = await FileRepo.getFile(userId, fileId);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -68,7 +67,7 @@ export const getFileThumbnail = async (userId: number, fileId: number) => {
|
|||||||
|
|
||||||
const decodeIv = (iv: string | null) => (iv ? Buffer.from(iv, "base64") : undefined);
|
const decodeIv = (iv: string | null) => (iv ? Buffer.from(iv, "base64") : undefined);
|
||||||
|
|
||||||
export const getFileStream = async (userId: number, fileId: number, range?: Range) => {
|
export const getFileStream = async (userId: number, fileId: number, range?: RangeRequest) => {
|
||||||
const file = await FileRepo.getFile(userId, fileId);
|
const file = await FileRepo.getFile(userId, fileId);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
throw new AppError("NOT_FOUND", "File not found");
|
throw new AppError("NOT_FOUND", "File not found");
|
||||||
@@ -76,7 +75,11 @@ export const getFileStream = async (userId: number, fileId: number, range?: Rang
|
|||||||
return await createEncContentStream(file.path, decodeIv(file.encContentIv), range);
|
return await createEncContentStream(file.path, decodeIv(file.encContentIv), range);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFileThumbnailStream = async (userId: number, fileId: number, range?: Range) => {
|
export const getFileThumbnailStream = async (
|
||||||
|
userId: number,
|
||||||
|
fileId: number,
|
||||||
|
range?: RangeRequest,
|
||||||
|
) => {
|
||||||
const thumbnail = await getFileThumbnail(userId, fileId);
|
const thumbnail = await getFileThumbnail(userId, fileId);
|
||||||
return await createEncContentStream(thumbnail.path, decodeIv(thumbnail.encContentIv), range);
|
return await createEncContentStream(thumbnail.path, decodeIv(thumbnail.encContentIv), range);
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { open } from "fs/promises";
|
import { open } from "fs/promises";
|
||||||
import type { Readable } from "stream";
|
import type { Readable } from "stream";
|
||||||
import { ENCRYPTION_OVERHEAD, ENCRYPTED_CHUNK_SIZE } from "#lib/constants/index.js";
|
|
||||||
import { AppError } from "#lib/server/errors.js";
|
import { AppError } from "#lib/server/errors.js";
|
||||||
|
import { ENCRYPTION_OVERHEAD, ENCRYPTED_CHUNK_SIZE } from "#lib/shared/format/index.js";
|
||||||
|
|
||||||
const invalidChunkSize = () => new AppError("BAD_REQUEST", "Invalid chunk size");
|
const invalidChunkSize = () => new AppError("BAD_REQUEST", "Invalid chunk size");
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,22 @@ import { createReadStream } from "fs";
|
|||||||
import { stat } from "fs/promises";
|
import { stat } from "fs/promises";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
import { AppError } from "#lib/server/errors.js";
|
import { AppError } from "#lib/server/errors.js";
|
||||||
|
import { resolveRange, type RangeRequest } from "#lib/shared/http.js";
|
||||||
|
|
||||||
export const createEncContentStream = async (
|
export const createEncContentStream = async (
|
||||||
path: string,
|
path: string,
|
||||||
iv?: Buffer,
|
iv?: Buffer,
|
||||||
range?: { start?: number; end?: number },
|
rangeRequest?: RangeRequest,
|
||||||
) => {
|
) => {
|
||||||
const { size: fileSize } = await stat(path);
|
const { size: fileSize } = await stat(path);
|
||||||
const ivSize = iv?.byteLength ?? 0;
|
const ivSize = iv?.byteLength ?? 0;
|
||||||
const totalSize = fileSize + ivSize;
|
|
||||||
|
|
||||||
const start = range?.start ?? 0;
|
const range = resolveRange(rangeRequest, fileSize + ivSize);
|
||||||
const end = range?.end ?? totalSize - 1;
|
if (!range) {
|
||||||
if (start > end || start < 0 || end >= totalSize) {
|
|
||||||
throw new AppError("RANGE_NOT_SATISFIABLE", "Invalid range");
|
throw new AppError("RANGE_NOT_SATISFIABLE", "Invalid range");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { start, end } = range;
|
||||||
return {
|
return {
|
||||||
encContentStream: Readable.toWeb(
|
encContentStream: Readable.toWeb(
|
||||||
Readable.from(
|
Readable.from(
|
||||||
@@ -34,7 +34,7 @@ export const createEncContentStream = async (
|
|||||||
})(),
|
})(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
range: { start, end, total: totalSize },
|
range,
|
||||||
isRangeRequest: !!range,
|
isRangeRequest: !!rangeRequest,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./client";
|
|
||||||
export * from "./types";
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { encodeToBase64, decryptChallenge, signMessageRSA } from "#lib/modules/crypto/index.js";
|
import { encodeToBase64, decryptChallenge, signMessageRSA } from "#lib/shared/crypto/index.js";
|
||||||
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
||||||
|
|
||||||
export const requestSessionUpgrade = async (
|
export const requestSessionUpgrade = async (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { generateDataKey, wrapDataKey, encryptString } from "#lib/modules/crypto/index.js";
|
import { generateDataKey, wrapDataKey, encryptString } from "#lib/shared/crypto/index.js";
|
||||||
import type { MasterKey } from "#lib/stores/index.js";
|
import type { MasterKey } from "#lib/stores/index.js";
|
||||||
import { trpc } from "#trpc/client.js";
|
import { trpc } from "#trpc/client.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { getAllFileInfos } from "#lib/indexedDB/filesystem.js";
|
|
||||||
import {
|
import {
|
||||||
getFileCache,
|
getFileCache,
|
||||||
storeFileCache,
|
storeFileCache,
|
||||||
deleteFileCache,
|
deleteFileCache,
|
||||||
downloadFile,
|
|
||||||
deleteFileThumbnailCache,
|
deleteFileThumbnailCache,
|
||||||
} from "#lib/modules/file/index.js";
|
} from "#lib/client/cache/index.js";
|
||||||
import { uploadBlob } from "#lib/modules/upload.js";
|
import { getAllFileInfos } from "#lib/client/storage/indexedDB/filesystem.js";
|
||||||
|
import { uploadBlob } from "#lib/client/transfer/chunkedUpload.js";
|
||||||
|
import { downloadFile } from "#lib/client/transfer/index.js";
|
||||||
import { trpc } from "#trpc/client.js";
|
import { trpc } from "#trpc/client.js";
|
||||||
|
|
||||||
export const requestFileDownload = async (
|
export const requestFileDownload = async (
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { storeMasterKeys } from "#lib/indexedDB/index.js";
|
import { storeMasterKeys } from "#lib/client/storage/indexedDB/index.js";
|
||||||
|
import { requestSessionUpgrade } from "#lib/services/auth.js";
|
||||||
import {
|
import {
|
||||||
encodeToBase64,
|
encodeToBase64,
|
||||||
exportRSAKeyToBase64,
|
exportRSAKeyToBase64,
|
||||||
@@ -7,8 +8,7 @@ import {
|
|||||||
unwrapMasterKey,
|
unwrapMasterKey,
|
||||||
signMasterKeyWrapped,
|
signMasterKeyWrapped,
|
||||||
verifyMasterKeyWrapped,
|
verifyMasterKeyWrapped,
|
||||||
} from "#lib/modules/crypto/index.js";
|
} from "#lib/shared/crypto/index.js";
|
||||||
import { requestSessionUpgrade } from "#lib/services/auth.js";
|
|
||||||
import { masterKeyStore, type ClientKeys } from "#lib/stores/index.js";
|
import { masterKeyStore, type ClientKeys } from "#lib/stores/index.js";
|
||||||
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
import { trpc, isTRPCClientError } from "#trpc/client.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { AES_GCM_IV_SIZE } from "#lib/constants/index.js";
|
import { AES_GCM_IV_SIZE } from "#lib/shared/format/index.js";
|
||||||
import {
|
import {
|
||||||
encodeString,
|
encodeString,
|
||||||
decodeString,
|
decodeString,
|
||||||
encodeToBase64,
|
encodeToBase64,
|
||||||
decodeFromBase64,
|
decodeFromBase64,
|
||||||
concatenateBuffers,
|
concatenateBuffers,
|
||||||
} from "./utils";
|
} from "./encoding";
|
||||||
|
|
||||||
export const generateMasterKey = async () => {
|
export const generateMasterKey = async () => {
|
||||||
return {
|
return {
|
||||||
File renamed without changes.
@@ -1,4 +1,4 @@
|
|||||||
export * from "./aes";
|
export * from "./aes";
|
||||||
|
export * from "./encoding";
|
||||||
export * from "./rsa";
|
export * from "./rsa";
|
||||||
export * from "./sha";
|
export * from "./sha";
|
||||||
export * from "./utils";
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { encodeString, encodeToBase64, decodeFromBase64 } from "./utils";
|
import { encodeString, encodeToBase64, decodeFromBase64 } from "./encoding";
|
||||||
|
|
||||||
export const generateEncryptionKeyPair = async () => {
|
export const generateEncryptionKeyPair = async () => {
|
||||||
const keyPair = await crypto.subtle.generateKey(
|
const keyPair = await crypto.subtle.generateKey(
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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"],
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,7 +1,34 @@
|
|||||||
import { ENCRYPTION_OVERHEAD, CHUNK_SIZE, ENCRYPTED_CHUNK_SIZE } from "../constants";
|
// Files are encrypted in chunks of `CHUNK_SIZE` bytes, each of which is stored as
|
||||||
|
// `IV || ciphertext || tag`. Legacy files are encrypted as a single chunk.
|
||||||
|
|
||||||
export * from "../../lib/modules/crypto";
|
export const AES_GCM_IV_SIZE = 12;
|
||||||
|
export const AES_GCM_TAG_SIZE = 16;
|
||||||
|
export const ENCRYPTION_OVERHEAD = AES_GCM_IV_SIZE + AES_GCM_TAG_SIZE;
|
||||||
|
|
||||||
|
export const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MiB
|
||||||
|
export const ENCRYPTED_CHUNK_SIZE = CHUNK_SIZE + ENCRYPTION_OVERHEAD;
|
||||||
|
|
||||||
|
export const getChunkCount = (size: number) => {
|
||||||
|
return Math.ceil(size / CHUNK_SIZE);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEncryptedSize = (size: number) => {
|
||||||
|
return size + getChunkCount(size) * ENCRYPTION_OVERHEAD;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDecryptedSize = (encryptedSize: number, isLegacy: boolean) => {
|
||||||
|
if (isLegacy) {
|
||||||
|
return encryptedSize - ENCRYPTION_OVERHEAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullChunks = Math.floor(encryptedSize / ENCRYPTED_CHUNK_SIZE);
|
||||||
|
const lastChunkEncSize = encryptedSize % ENCRYPTED_CHUNK_SIZE;
|
||||||
|
return (
|
||||||
|
fullChunks * CHUNK_SIZE + (lastChunkEncSize > 0 ? lastChunkEncSize - ENCRYPTION_OVERHEAD : 0)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Maps a range of the plaintext to the range of the encrypted chunks that contain it
|
||||||
export const getEncryptedRange = (
|
export const getEncryptedRange = (
|
||||||
start: number,
|
start: number,
|
||||||
end: number,
|
end: number,
|
||||||
@@ -26,15 +53,3 @@ export const getEncryptedRange = (
|
|||||||
end: Math.min((lastChunkIndex + 1) * ENCRYPTED_CHUNK_SIZE - 1, totalEncryptedSize - 1),
|
end: Math.min((lastChunkIndex + 1) * ENCRYPTED_CHUNK_SIZE - 1, totalEncryptedSize - 1),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDecryptedSize = (encryptedSize: number, isLegacy: boolean) => {
|
|
||||||
if (isLegacy) {
|
|
||||||
return encryptedSize - ENCRYPTION_OVERHEAD;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fullChunks = Math.floor(encryptedSize / ENCRYPTED_CHUNK_SIZE);
|
|
||||||
const lastChunkEncSize = encryptedSize % ENCRYPTED_CHUNK_SIZE;
|
|
||||||
return (
|
|
||||||
fullChunks * CHUNK_SIZE + (lastChunkEncSize > 0 ? lastChunkEncSize - ENCRYPTION_OVERHEAD : 0)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./chunk";
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export type RangeRequest = { start: number; end?: number } | { suffixLength: number };
|
||||||
|
|
||||||
|
export interface ByteRange {
|
||||||
|
start: number;
|
||||||
|
end: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the first range is used if multiple ranges are requested
|
||||||
|
export const parseRangeHeader = (value: string | null): RangeRequest | undefined => {
|
||||||
|
const match = value?.match(/^bytes=\s*(\d*)-(\d*)/);
|
||||||
|
if (!match) return undefined;
|
||||||
|
|
||||||
|
const [, start, end] = match;
|
||||||
|
if (start) {
|
||||||
|
return { start: parseInt(start, 10), end: end ? parseInt(end, 10) : undefined };
|
||||||
|
} else if (end) {
|
||||||
|
return { suffixLength: parseInt(end, 10) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Returns `null` if the range is not satisfiable
|
||||||
|
export const resolveRange = (range: RangeRequest | undefined, total: number): ByteRange | null => {
|
||||||
|
if (!range) {
|
||||||
|
return { start: 0, end: total - 1, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = "suffixLength" in range ? Math.max(0, total - range.suffixLength) : range.start;
|
||||||
|
const end = "suffixLength" in range ? total - 1 : Math.min(range.end ?? total - 1, total - 1);
|
||||||
|
return start <= end ? { start, end, total } : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getContentRangeHeader = (range: ByteRange) => {
|
||||||
|
return { "Content-Range": `bytes ${range.start}-${range.end}/${range.total}` };
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
export const DECRYPTED_FILE_URL_PREFIX = "/_internal/decryptedFile/";
|
||||||
|
|
||||||
export interface FileMetadata {
|
export interface FileMetadata {
|
||||||
isLegacy: boolean;
|
isLegacy: boolean;
|
||||||
dataKey: CryptoKey;
|
dataKey: CryptoKey;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { concatenateBuffers, exportRSAKey, digestMessage } from "#lib/modules/crypto/index.js";
|
import { concatenateBuffers, exportRSAKey, digestMessage } from "#lib/shared/crypto/index.js";
|
||||||
|
|
||||||
export { requestMasterKeyDownload } from "#lib/services/key.js";
|
export { requestMasterKeyDownload } from "#lib/services/key.js";
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
import { FullscreenDiv } from "#lib/components/atoms/index.js";
|
import { FullscreenDiv } from "#lib/components/atoms/index.js";
|
||||||
import { Categories, IconEntryButton, TopBar } from "#lib/components/molecules/index.js";
|
import { Categories, IconEntryButton, TopBar } from "#lib/components/molecules/index.js";
|
||||||
import { getFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
import { getFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { captureVideoThumbnail } from "#lib/modules/thumbnail.js";
|
import { captureVideoThumbnail } from "#lib/client/media/thumbnail.js";
|
||||||
import { getFileDownloadState } from "#lib/modules/file/index.js";
|
import { getFileDownloadState } from "#lib/client/transfer/index.js";
|
||||||
import { masterKeyStore } from "#lib/stores/index.js";
|
import { masterKeyStore } from "#lib/stores/index.js";
|
||||||
import { HybridPromise } from "#lib/utils/index.js";
|
import { HybridPromise } from "#lib/utils/index.js";
|
||||||
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
|
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { isFileDownloading, type FileDownloadState } from "#lib/modules/file/index.js";
|
import { isFileDownloading, type FileDownloadState } from "#lib/client/transfer/index.js";
|
||||||
import { formatNetworkSpeed } from "#lib/utils/index.js";
|
import { formatNetworkSpeed } from "#lib/utils/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { storeFileThumbnailCache } from "#lib/modules/file/index.js";
|
import { storeFileThumbnailCache } from "#lib/client/cache/index.js";
|
||||||
import { prepareFileDecryption, getDecryptedFileUrl } from "#lib/serviceWorker/index.js";
|
import { prepareFileDecryption, getDecryptedFileUrl } from "#lib/client/serviceWorker.js";
|
||||||
import { requestFileThumbnailUpload } from "#lib/services/file.js";
|
import { requestFileThumbnailUpload } from "#lib/services/file.js";
|
||||||
import { trpc } from "#trpc/client.js";
|
import { trpc } from "#trpc/client.js";
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
getDownloadingFiles,
|
getDownloadingFiles,
|
||||||
clearDownloadedFiles,
|
clearDownloadedFiles,
|
||||||
type FileDownloadState,
|
type FileDownloadState,
|
||||||
} from "#lib/modules/file/index.js";
|
} from "#lib/client/transfer/index.js";
|
||||||
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { masterKeyStore } from "#lib/stores/index.js";
|
import { masterKeyStore } from "#lib/stores/index.js";
|
||||||
import File from "./File.svelte";
|
import File from "./File.svelte";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FileDownloadState } from "#lib/modules/file/index.js";
|
import type { FileDownloadState } from "#lib/client/transfer/index.js";
|
||||||
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { formatNetworkSpeed } from "#lib/utils/index.js";
|
import { formatNetworkSpeed } from "#lib/utils/index.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { FullscreenDiv } from "#lib/components/atoms/index.js";
|
import { FullscreenDiv } from "#lib/components/atoms/index.js";
|
||||||
import { TopBar } from "#lib/components/molecules/index.js";
|
import { TopBar } from "#lib/components/molecules/index.js";
|
||||||
import { getUploadingFiles, clearUploadedFiles } from "#lib/modules/file/index.js";
|
import { getUploadingFiles, clearUploadedFiles } from "#lib/client/transfer/index.js";
|
||||||
import File from "./File.svelte";
|
import File from "./File.svelte";
|
||||||
|
|
||||||
const uploadingFiles = getUploadingFiles();
|
const uploadingFiles = getUploadingFiles();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FileUploadState } from "#lib/modules/file/index.js";
|
import type { FileUploadState } from "#lib/client/transfer/index.js";
|
||||||
import { formatNetworkSpeed } from "#lib/utils/index.js";
|
import { formatNetworkSpeed } from "#lib/utils/index.js";
|
||||||
|
|
||||||
import IconPending from "~icons/material-symbols/pending";
|
import IconPending from "~icons/material-symbols/pending";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { deserializeClientKeys } from "#lib/modules/key.js";
|
||||||
import {
|
import {
|
||||||
generateEncryptionKeyPair,
|
generateEncryptionKeyPair,
|
||||||
generateSigningKeyPair,
|
generateSigningKeyPair,
|
||||||
@@ -10,8 +11,7 @@ import {
|
|||||||
makeAESKeyNonextractable,
|
makeAESKeyNonextractable,
|
||||||
wrapHmacSecret,
|
wrapHmacSecret,
|
||||||
generateHmacSecret,
|
generateHmacSecret,
|
||||||
} from "#lib/modules/crypto/index.js";
|
} from "#lib/shared/crypto/index.js";
|
||||||
import { deserializeClientKeys } from "#lib/modules/key.js";
|
|
||||||
import { clientKeyStore } from "#lib/stores/index.js";
|
import { clientKeyStore } from "#lib/stores/index.js";
|
||||||
|
|
||||||
export { requestLogout } from "#lib/services/auth.js";
|
export { requestLogout } from "#lib/services/auth.js";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
||||||
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
||||||
import { getFileThumbnail } from "#lib/modules/file/index.js";
|
import { getFileThumbnail } from "#lib/client/cache/index.js";
|
||||||
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { formatDateTime } from "#lib/utils/index.js";
|
import { formatDateTime } from "#lib/utils/index.js";
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -2,11 +2,11 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { FullscreenDiv } from "#lib/components/atoms/index.js";
|
import { FullscreenDiv } from "#lib/components/atoms/index.js";
|
||||||
import { TopBar } from "#lib/components/molecules/index.js";
|
import { TopBar } from "#lib/components/molecules/index.js";
|
||||||
import type { FileCacheIndex } from "#lib/indexedDB/index.js";
|
import type { FileCacheIndex } from "#lib/client/storage/indexedDB/index.js";
|
||||||
import {
|
import {
|
||||||
getFileCacheIndex,
|
getFileCacheIndex,
|
||||||
deleteFileCache as doDeleteFileCache,
|
deleteFileCache as doDeleteFileCache,
|
||||||
} from "#lib/modules/file/index.js";
|
} from "#lib/client/cache/index.js";
|
||||||
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
import { bulkGetFileInfo, type MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { masterKeyStore } from "#lib/stores/index.js";
|
import { masterKeyStore } from "#lib/stores/index.js";
|
||||||
import { formatFileSize } from "#lib/utils/index.js";
|
import { formatFileSize } from "#lib/utils/index.js";
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { FileCacheIndex } from "#lib/indexedDB/index.js";
|
import type { FileCacheIndex } from "#lib/client/storage/indexedDB/index.js";
|
||||||
import type { MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
import type { MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { formatDate, formatFileSize } from "#lib/utils/index.js";
|
import { formatDate, formatFileSize } from "#lib/utils/index.js";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { goto } from "$app/navigation";
|
import { goto } from "$app/navigation";
|
||||||
import { BottomDiv, Button, FullscreenDiv } from "#lib/components/atoms/index.js";
|
import { BottomDiv, Button, FullscreenDiv } from "#lib/components/atoms/index.js";
|
||||||
import { IconEntryButton, TopBar } from "#lib/components/molecules/index.js";
|
import { IconEntryButton, TopBar } from "#lib/components/molecules/index.js";
|
||||||
import { deleteAllFileThumbnailCaches } from "#lib/modules/file/index.js";
|
import { deleteAllFileThumbnailCaches } from "#lib/client/cache/index.js";
|
||||||
import type { MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
import type { MaybeFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { masterKeyStore } from "#lib/stores/index.js";
|
import { masterKeyStore } from "#lib/stores/index.js";
|
||||||
import { sortEntries } from "#lib/utils/index.js";
|
import { sortEntries } from "#lib/utils/index.js";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { limitFunction } from "p-limit";
|
import { limitFunction } from "p-limit";
|
||||||
import { SvelteMap } from "svelte/reactivity";
|
import { SvelteMap } from "svelte/reactivity";
|
||||||
import { storeFileThumbnailCache } from "#lib/modules/file/index.js";
|
import { storeFileThumbnailCache } from "#lib/client/cache/index.js";
|
||||||
|
import { generateThumbnail } from "#lib/client/media/thumbnail.js";
|
||||||
import { getFileInfo, type FileInfo } from "#lib/modules/filesystem/index.js";
|
import { getFileInfo, type FileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { generateThumbnail } from "#lib/modules/thumbnail.js";
|
|
||||||
import { requestFileDownload, requestFileThumbnailUpload } from "#lib/services/file.js";
|
import { requestFileDownload, requestFileThumbnailUpload } from "#lib/services/file.js";
|
||||||
import { HybridPromise, Scheduler } from "#lib/utils/index.js";
|
import { HybridPromise, Scheduler } from "#lib/utils/index.js";
|
||||||
import type { RouterOutputs } from "#trpc/router.server.js";
|
import type { RouterOutputs } from "#trpc/router.server.js";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { CheckBox, RowVirtualizer } from "#lib/components/atoms/index.js";
|
import { CheckBox, RowVirtualizer } from "#lib/components/atoms/index.js";
|
||||||
import { SubCategories, TopBar } from "#lib/components/molecules/index.js";
|
import { SubCategories, TopBar } from "#lib/components/molecules/index.js";
|
||||||
import { CategoryCreateModal } from "#lib/components/organisms/index.js";
|
import { CategoryCreateModal } from "#lib/components/organisms/index.js";
|
||||||
import { updateCategoryInfo } from "#lib/indexedDB/index.js";
|
import { updateCategoryInfo } from "#lib/client/storage/indexedDB/index.js";
|
||||||
import { getCategoryInfo, type MaybeCategoryInfo } from "#lib/modules/filesystem/index.js";
|
import { getCategoryInfo, type MaybeCategoryInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { masterKeyStore } from "#lib/stores/index.js";
|
import { masterKeyStore } from "#lib/stores/index.js";
|
||||||
import { HybridPromise, sortEntries } from "#lib/utils/index.js";
|
import { HybridPromise, sortEntries } from "#lib/utils/index.js";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
||||||
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
||||||
import { getFileThumbnail } from "#lib/modules/file/index.js";
|
import { getFileThumbnail } from "#lib/client/cache/index.js";
|
||||||
import type { CategoryFileInfo } from "#lib/modules/filesystem/index.js";
|
import type { CategoryFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { formatDateTime } from "#lib/utils/index.js";
|
import { formatDateTime } from "#lib/utils/index.js";
|
||||||
import type { SelectedFile } from "./service.svelte";
|
import type { SelectedFile } from "./service.svelte";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getContext, setContext } from "svelte";
|
import { getContext, setContext } from "svelte";
|
||||||
import { encryptString } from "#lib/modules/crypto/index.js";
|
|
||||||
import type { SelectedCategory } from "#lib/components/molecules/index.js";
|
import type { SelectedCategory } from "#lib/components/molecules/index.js";
|
||||||
|
import { encryptString } from "#lib/shared/crypto/index.js";
|
||||||
import { trpc } from "#trpc/client.js";
|
import { trpc } from "#trpc/client.js";
|
||||||
|
|
||||||
export { requestCategoryCreation, requestFileRemovalFromCategory } from "#lib/services/category.js";
|
export { requestCategoryCreation, requestFileRemovalFromCategory } from "#lib/services/category.js";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionEntryButton, RowVirtualizer } from "#lib/components/atoms/index.js";
|
import { ActionEntryButton, RowVirtualizer } from "#lib/components/atoms/index.js";
|
||||||
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
||||||
import { getUploadingFiles, type LiveFileUploadState } from "#lib/modules/file/index.js";
|
import { getUploadingFiles, type LiveFileUploadState } from "#lib/client/transfer/index.js";
|
||||||
import type { DirectoryInfo } from "#lib/modules/filesystem/index.js";
|
import type { DirectoryInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { sortEntries } from "#lib/utils/index.js";
|
import { sortEntries } from "#lib/utils/index.js";
|
||||||
import File from "./File.svelte";
|
import File from "./File.svelte";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
||||||
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
||||||
import { getFileThumbnail } from "#lib/modules/file/index.js";
|
import { getFileThumbnail } from "#lib/client/cache/index.js";
|
||||||
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { formatDateTime } from "#lib/utils/index.js";
|
import { formatDateTime } from "#lib/utils/index.js";
|
||||||
import type { SelectedEntry } from "../service.svelte";
|
import type { SelectedEntry } from "../service.svelte";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { LiveFileUploadState } from "#lib/modules/file/index.js";
|
import type { LiveFileUploadState } from "#lib/client/transfer/index.js";
|
||||||
import { formatNetworkSpeed } from "#lib/utils/index.js";
|
import { formatNetworkSpeed } from "#lib/utils/index.js";
|
||||||
|
|
||||||
import IconDraft from "~icons/material-symbols/draft";
|
import IconDraft from "~icons/material-symbols/draft";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getDownloadingFiles } from "#lib/modules/file/index.js";
|
import { getDownloadingFiles } from "#lib/client/transfer/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onclick: () => void;
|
onclick: () => void;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getUploadingFiles } from "#lib/modules/file/index.js";
|
import { getUploadingFiles } from "#lib/client/transfer/index.js";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onclick: () => void;
|
onclick: () => void;
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { getContext, setContext } from "svelte";
|
import { getContext, setContext } from "svelte";
|
||||||
import { storeHmacSecrets } from "#lib/indexedDB/index.js";
|
|
||||||
import {
|
|
||||||
generateDataKey,
|
|
||||||
wrapDataKey,
|
|
||||||
unwrapHmacSecret,
|
|
||||||
encryptString,
|
|
||||||
} from "#lib/modules/crypto/index.js";
|
|
||||||
import {
|
import {
|
||||||
storeFileCache,
|
storeFileCache,
|
||||||
deleteFileCache,
|
deleteFileCache,
|
||||||
storeFileThumbnailCache,
|
storeFileThumbnailCache,
|
||||||
deleteFileThumbnailCache,
|
deleteFileThumbnailCache,
|
||||||
uploadFile,
|
} from "#lib/client/cache/index.js";
|
||||||
} from "#lib/modules/file/index.js";
|
import { storeHmacSecrets } from "#lib/client/storage/indexedDB/index.js";
|
||||||
|
import { uploadFile } from "#lib/client/transfer/index.js";
|
||||||
import type { DataKey } from "#lib/modules/filesystem/index.js";
|
import type { DataKey } from "#lib/modules/filesystem/index.js";
|
||||||
|
import {
|
||||||
|
generateDataKey,
|
||||||
|
wrapDataKey,
|
||||||
|
unwrapHmacSecret,
|
||||||
|
encryptString,
|
||||||
|
} from "#lib/shared/crypto/index.js";
|
||||||
import { hmacSecretStore, type MasterKey, type HmacSecret } from "#lib/stores/index.js";
|
import { hmacSecretStore, type MasterKey, type HmacSecret } from "#lib/stores/index.js";
|
||||||
import { trpc } from "#trpc/client.js";
|
import { trpc } from "#trpc/client.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
import { ActionEntryButton } from "#lib/components/atoms/index.js";
|
||||||
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
import { DirectoryEntryLabel } from "#lib/components/molecules/index.js";
|
||||||
import { getFileThumbnail } from "#lib/modules/file/index.js";
|
import { getFileThumbnail } from "#lib/client/cache/index.js";
|
||||||
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
||||||
import { formatDateTime } from "#lib/utils/index.js";
|
import { formatDateTime } from "#lib/utils/index.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAllFileInfos } from "#lib/indexedDB/index.js";
|
import { getAllFileInfos } from "#lib/client/storage/indexedDB/index.js";
|
||||||
|
|
||||||
export const requestFreshMediaFilesRetrieval = async (limit = 8) => {
|
export const requestFreshMediaFilesRetrieval = async (limit = 8) => {
|
||||||
const files = await getAllFileInfos();
|
const files = await getAllFileInfos();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getDownloadingFiles, getUploadingFiles } from "#lib/modules/file/index.js";
|
import { getDownloadingFiles, getUploadingFiles } from "#lib/client/transfer/index.js";
|
||||||
import "../app.css";
|
import "../app.css";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { parseRangeHeader, getContentRangeHeader } from "#lib/modules/http.js";
|
|
||||||
import { authorize } from "#lib/server/auth.js";
|
import { authorize } from "#lib/server/auth.js";
|
||||||
import { handleAppErrors } from "#lib/server/http.js";
|
import { handleAppErrors } from "#lib/server/http.js";
|
||||||
import { FileService } from "#lib/server/services/index.js";
|
import { FileService } from "#lib/server/services/index.js";
|
||||||
|
import { parseRangeHeader, getContentRangeHeader } from "#lib/shared/http.js";
|
||||||
import type { RequestEvent, RequestHandler } from "./$types";
|
import type { RequestEvent, RequestHandler } from "./$types";
|
||||||
|
|
||||||
const downloadHandler = async ({ locals, params, request }: RequestEvent) => {
|
const downloadHandler = async ({ locals, params, request }: RequestEvent) => {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "../lib/constants";
|
|
||||||
@@ -1,14 +1,25 @@
|
|||||||
import { DECRYPTED_FILE_URL_PREFIX, CHUNK_SIZE, ENCRYPTED_CHUNK_SIZE } from "../constants";
|
import { getFile } from "#lib/client/storage/opfs.js";
|
||||||
import { decryptChunk, getEncryptedRange, getDecryptedSize } from "../modules/crypto";
|
import { decryptChunk } from "#lib/shared/crypto/index.js";
|
||||||
import { parseRangeHeader, getContentRangeHeader } from "../modules/http";
|
import {
|
||||||
|
CHUNK_SIZE,
|
||||||
|
ENCRYPTED_CHUNK_SIZE,
|
||||||
|
getDecryptedSize,
|
||||||
|
getEncryptedRange,
|
||||||
|
} from "#lib/shared/format/index.js";
|
||||||
|
import {
|
||||||
|
getContentRangeHeader,
|
||||||
|
parseRangeHeader,
|
||||||
|
resolveRange,
|
||||||
|
type ByteRange,
|
||||||
|
type RangeRequest,
|
||||||
|
} from "#lib/shared/http.js";
|
||||||
|
import { DECRYPTED_FILE_URL_PREFIX, type FileMetadata } from "#lib/shared/serviceWorker.js";
|
||||||
import { getFileMetadata } from "../modules/metadata";
|
import { getFileMetadata } from "../modules/metadata";
|
||||||
import { getFile } from "../modules/opfs";
|
|
||||||
import type { FileMetadata } from "../types";
|
|
||||||
|
|
||||||
const createResponse = (
|
const createResponse = (
|
||||||
stream: ReadableStream<Uint8Array>,
|
stream: ReadableStream<Uint8Array>,
|
||||||
isRangeRequest: boolean,
|
isRangeRequest: boolean,
|
||||||
range: { start: number; end: number; total: number },
|
range: ByteRange,
|
||||||
contentType?: string,
|
contentType?: string,
|
||||||
downloadFilename?: string,
|
downloadFilename?: string,
|
||||||
) => {
|
) => {
|
||||||
@@ -33,22 +44,28 @@ const createResponse = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createRangeNotSatisfiableResponse = (total: number) => {
|
||||||
|
return new Response("Invalid range", {
|
||||||
|
status: 416,
|
||||||
|
headers: { "Content-Range": `bytes */${total}` },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const streamFromOpfs = async (
|
const streamFromOpfs = async (
|
||||||
file: File,
|
file: File,
|
||||||
metadata?: FileMetadata,
|
metadata?: FileMetadata,
|
||||||
range?: { start?: number; end?: number },
|
rangeRequest?: RangeRequest,
|
||||||
downloadFilename?: string,
|
downloadFilename?: string,
|
||||||
) => {
|
) => {
|
||||||
const start = range?.start ?? 0;
|
const range = resolveRange(rangeRequest, file.size);
|
||||||
const end = range?.end ?? file.size - 1;
|
if (!range) {
|
||||||
if (start > end || start < 0 || end >= file.size) {
|
return createRangeNotSatisfiableResponse(file.size);
|
||||||
return new Response("Invalid range", { status: 416 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return createResponse(
|
return createResponse(
|
||||||
file.slice(start, end + 1).stream(),
|
file.slice(range.start, range.end + 1).stream(),
|
||||||
!!range,
|
!!rangeRequest,
|
||||||
{ start, end, total: file.size },
|
range,
|
||||||
metadata?.contentType,
|
metadata?.contentType,
|
||||||
downloadFilename,
|
downloadFilename,
|
||||||
);
|
);
|
||||||
@@ -57,16 +74,16 @@ const streamFromOpfs = async (
|
|||||||
const streamFromServer = async (
|
const streamFromServer = async (
|
||||||
id: number,
|
id: number,
|
||||||
metadata: FileMetadata,
|
metadata: FileMetadata,
|
||||||
range?: { start?: number; end?: number },
|
rangeRequest?: RangeRequest,
|
||||||
downloadFilename?: string,
|
downloadFilename?: string,
|
||||||
) => {
|
) => {
|
||||||
const totalSize = getDecryptedSize(metadata.encContentSize, metadata.isLegacy);
|
const totalSize = getDecryptedSize(metadata.encContentSize, metadata.isLegacy);
|
||||||
const start = range?.start ?? 0;
|
const range = resolveRange(rangeRequest, totalSize);
|
||||||
const end = range?.end ?? totalSize - 1;
|
if (!range) {
|
||||||
if (start > end || start < 0 || end >= totalSize) {
|
return createRangeNotSatisfiableResponse(totalSize);
|
||||||
return new Response("Invalid range", { status: 416 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { start, end } = range;
|
||||||
const encryptedRange = getEncryptedRange(start, end, metadata.encContentSize, metadata.isLegacy);
|
const encryptedRange = getEncryptedRange(start, end, metadata.encContentSize, metadata.isLegacy);
|
||||||
const apiResponse = await fetch(`/api/file/${id}/download`, {
|
const apiResponse = await fetch(`/api/file/${id}/download`, {
|
||||||
headers: { Range: `bytes=${encryptedRange.start}-${encryptedRange.end}` },
|
headers: { Range: `bytes=${encryptedRange.start}-${encryptedRange.end}` },
|
||||||
@@ -85,9 +102,10 @@ const streamFromServer = async (
|
|||||||
controller.close();
|
controller.close();
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
!!range,
|
!!rangeRequest,
|
||||||
{ start, end, total: totalSize },
|
range,
|
||||||
metadata.contentType,
|
metadata.contentType,
|
||||||
|
downloadFilename,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,8 +142,8 @@ const streamFromServer = async (
|
|||||||
|
|
||||||
return createResponse(
|
return createResponse(
|
||||||
apiResponse.body.pipeThrough(decryptingStream),
|
apiResponse.body.pipeThrough(decryptingStream),
|
||||||
!!range,
|
!!rangeRequest,
|
||||||
{ start, end, total: totalSize },
|
range,
|
||||||
metadata.contentType,
|
metadata.contentType,
|
||||||
downloadFilename,
|
downloadFilename,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { self } from "$app/service-worker";
|
import { self } from "$app/service-worker";
|
||||||
import { DECRYPTED_FILE_URL_PREFIX } from "./constants";
|
import {
|
||||||
|
DECRYPTED_FILE_URL_PREFIX,
|
||||||
|
type ServiceWorkerMessage,
|
||||||
|
type ServiceWorkerResponse,
|
||||||
|
} from "#lib/shared/serviceWorker.js";
|
||||||
import { decryptFile } from "./handlers";
|
import { decryptFile } from "./handlers";
|
||||||
import { fileMetadataStore } from "./stores";
|
import { fileMetadataStore } from "./stores";
|
||||||
import type { ServiceWorkerMessage, ServiceWorkerResponse } from "./types";
|
|
||||||
|
|
||||||
self.addEventListener("message", (event) => {
|
self.addEventListener("message", (event) => {
|
||||||
const message: ServiceWorkerMessage = event.data;
|
const message: ServiceWorkerMessage = event.data;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "../../lib/modules/http";
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { self } from "$app/service-worker";
|
import { self } from "$app/service-worker";
|
||||||
|
import type { FileMetadata, ServiceWorkerResponse } from "#lib/shared/serviceWorker.js";
|
||||||
import { fileMetadataStore } from "../stores";
|
import { fileMetadataStore } from "../stores";
|
||||||
import type { FileMetadata, ServiceWorkerResponse } from "../types";
|
|
||||||
|
|
||||||
const METADATA_REQUEST_TIMEOUT_MS = 3000;
|
const METADATA_REQUEST_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "../../lib/modules/opfs";
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FileMetadata } from "./types";
|
import type { FileMetadata } from "#lib/shared/serviceWorker.js";
|
||||||
|
|
||||||
const MAX_FILE_METADATA_ENTRIES = 32;
|
const MAX_FILE_METADATA_ENTRIES = 32;
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export * from "../lib/serviceWorker/types";
|
|
||||||
@@ -39,6 +39,25 @@ describe("createEncContentStream", () => {
|
|||||||
expect(await read(encContentStream)).toBe("2345");
|
expect(await read(encContentStream)).toBe("2345");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("serves suffix ranges", async () => {
|
||||||
|
const path = await createFile("0123456789");
|
||||||
|
|
||||||
|
const { encContentStream, range } = await createEncContentStream(path, undefined, {
|
||||||
|
suffixLength: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(range).toEqual({ start: 7, end: 9, total: 10 });
|
||||||
|
expect(await read(encContentStream)).toBe("789");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unsatisfiable ranges", async () => {
|
||||||
|
const path = await createFile("0123456789");
|
||||||
|
|
||||||
|
await expect(createEncContentStream(path, undefined, { start: 10 })).rejects.toMatchObject({
|
||||||
|
code: "RANGE_NOT_SATISFIABLE",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("prepends the legacy IV to the content", async () => {
|
it("prepends the legacy IV to the content", async () => {
|
||||||
const path = await createFile("6789");
|
const path = await createFile("6789");
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
encryptChunk,
|
encryptChunk,
|
||||||
encryptString,
|
encryptString,
|
||||||
generateDataKey,
|
generateDataKey,
|
||||||
} from "#lib/modules/crypto/index.js";
|
} from "#lib/shared/crypto/index.js";
|
||||||
|
|
||||||
describe("base64", () => {
|
describe("base64", () => {
|
||||||
it("round-trips large buffers", () => {
|
it("round-trips large buffers", () => {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { CHUNK_SIZE, ENCRYPTED_CHUNK_SIZE, ENCRYPTION_OVERHEAD } from "#lib/constants/index.js";
|
import {
|
||||||
import { getContentRangeHeader, parseRangeHeader } from "#lib/modules/http.js";
|
CHUNK_SIZE,
|
||||||
import { getDecryptedSize, getEncryptedRange } from "../../src/service-worker/modules/crypto";
|
ENCRYPTED_CHUNK_SIZE,
|
||||||
|
ENCRYPTION_OVERHEAD,
|
||||||
|
getDecryptedSize,
|
||||||
|
getEncryptedRange,
|
||||||
|
getEncryptedSize,
|
||||||
|
} from "#lib/shared/format/index.js";
|
||||||
|
import { getContentRangeHeader, parseRangeHeader, resolveRange } from "#lib/shared/http.js";
|
||||||
|
|
||||||
describe("parseRangeHeader", () => {
|
describe("parseRangeHeader", () => {
|
||||||
it("parses byte ranges", () => {
|
it("parses byte ranges", () => {
|
||||||
@@ -9,6 +15,33 @@ describe("parseRangeHeader", () => {
|
|||||||
expect(parseRangeHeader("bytes=0-")).toEqual({ start: 0, end: undefined });
|
expect(parseRangeHeader("bytes=0-")).toEqual({ start: 0, end: undefined });
|
||||||
expect(parseRangeHeader("bytes=10-20")).toEqual({ start: 10, end: 20 });
|
expect(parseRangeHeader("bytes=10-20")).toEqual({ start: 10, end: 20 });
|
||||||
expect(parseRangeHeader("bytes=10-20, 30-40")).toEqual({ start: 10, end: 20 });
|
expect(parseRangeHeader("bytes=10-20, 30-40")).toEqual({ start: 10, end: 20 });
|
||||||
|
expect(parseRangeHeader("bytes=-500")).toEqual({ suffixLength: 500 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores invalid ranges", () => {
|
||||||
|
expect(parseRangeHeader("bytes=-")).toBeUndefined();
|
||||||
|
expect(parseRangeHeader("items=0-1")).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("resolveRange", () => {
|
||||||
|
it("resolves the whole content without a range", () => {
|
||||||
|
expect(resolveRange(undefined, 100)).toEqual({ start: 0, end: 99, total: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clamps the end to the content size", () => {
|
||||||
|
expect(resolveRange({ start: 10, end: 1000 }, 100)).toEqual({ start: 10, end: 99, total: 100 });
|
||||||
|
expect(resolveRange({ start: 10 }, 100)).toEqual({ start: 10, end: 99, total: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resolves suffix ranges", () => {
|
||||||
|
expect(resolveRange({ suffixLength: 10 }, 100)).toEqual({ start: 90, end: 99, total: 100 });
|
||||||
|
expect(resolveRange({ suffixLength: 1000 }, 100)).toEqual({ start: 0, end: 99, total: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects unsatisfiable ranges", () => {
|
||||||
|
expect(resolveRange({ start: 100 }, 100)).toBeNull();
|
||||||
|
expect(resolveRange({ start: 20, end: 10 }, 100)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("formats Content-Range", () => {
|
it("formats Content-Range", () => {
|
||||||
@@ -19,13 +52,13 @@ describe("parseRangeHeader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("encrypted chunk layout", () => {
|
describe("encrypted chunk layout", () => {
|
||||||
const encryptedSizeOf = (size: number) =>
|
|
||||||
size + Math.ceil(size / CHUNK_SIZE) * ENCRYPTION_OVERHEAD;
|
|
||||||
|
|
||||||
it.each([1, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE + 1, 3 * CHUNK_SIZE + 12345])(
|
it.each([1, CHUNK_SIZE - 1, CHUNK_SIZE, CHUNK_SIZE + 1, 3 * CHUNK_SIZE + 12345])(
|
||||||
"computes the decrypted size of %i bytes",
|
"computes the decrypted size of %i bytes",
|
||||||
(size) => {
|
(size) => {
|
||||||
expect(getDecryptedSize(encryptedSizeOf(size), false)).toBe(size);
|
expect(getEncryptedSize(size)).toBe(
|
||||||
|
size + Math.ceil(size / CHUNK_SIZE) * ENCRYPTION_OVERHEAD,
|
||||||
|
);
|
||||||
|
expect(getDecryptedSize(getEncryptedSize(size), false)).toBe(size);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -35,7 +68,7 @@ describe("encrypted chunk layout", () => {
|
|||||||
|
|
||||||
it("maps a plaintext range to whole encrypted chunks", () => {
|
it("maps a plaintext range to whole encrypted chunks", () => {
|
||||||
const totalSize = 3 * CHUNK_SIZE + 100;
|
const totalSize = 3 * CHUNK_SIZE + 100;
|
||||||
const totalEncryptedSize = encryptedSizeOf(totalSize);
|
const totalEncryptedSize = getEncryptedSize(totalSize);
|
||||||
|
|
||||||
expect(getEncryptedRange(0, 10, totalEncryptedSize, false)).toEqual({
|
expect(getEncryptedRange(0, 10, totalEncryptedSize, false)).toEqual({
|
||||||
firstChunkIndex: 0,
|
firstChunkIndex: 0,
|
||||||
|
|||||||
Reference in new issue
Block a user