feat: Refactor server-side document, audiobook, and authentication logic, introduce new client-side APIs and caching, and update related tests.

This commit is contained in:
Richard R 2026-02-17 19:30:12 -07:00
parent 8e1b6b4009
commit c2188b476f
110 changed files with 302 additions and 548 deletions

View file

@ -15,7 +15,7 @@ import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu';
import { AudiobookExportModal } from '@/components/AudiobookExportModal';
import type { TTSAudiobookChapter } from '@/types/tts';
import type { AudiobookGenerationSettings } from '@/types/client';
import { resolveDocumentId } from '@/lib/dexie';
import { resolveDocumentId } from '@/lib/client/dexie';
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';

View file

@ -11,7 +11,7 @@ import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton';
import { Header } from '@/components/Header';
import { useTTS } from "@/contexts/TTSContext";
import TTSPlayer from '@/components/player/TTSPlayer';
import { resolveDocumentId } from '@/lib/dexie';
import { resolveDocumentId } from '@/lib/client/dexie';
import { DocumentHeaderMenu } from '@/components/documents/DocumentHeaderMenu';
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';

View file

@ -4,7 +4,7 @@ import { Toaster } from 'react-hot-toast';
import { Providers } from '@/app/providers';
import ClaimDataPopup from '@/components/auth/ClaimDataModal';
import { getAuthBaseUrl, isAnonymousAuthSessionsEnabled, isAuthEnabled, isGithubAuthEnabled } from '@/lib/server/auth-config';
import { getAuthBaseUrl, isAnonymousAuthSessionsEnabled, isAuthEnabled, isGithubAuthEnabled } from '@/lib/server/auth/config';
export const dynamic = 'force-dynamic';

View file

@ -15,7 +15,7 @@ import type { TTSAudiobookChapter } from '@/types/tts';
import type { AudiobookGenerationSettings } from '@/types/client';
import TTSPlayer from '@/components/player/TTSPlayer';
import { RateLimitPauseButton } from '@/components/player/RateLimitPauseButton';
import { resolveDocumentId } from '@/lib/dexie';
import { resolveDocumentId } from '@/lib/client/dexie';
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';

View file

@ -4,7 +4,7 @@ import { useState, useEffect, Suspense } from 'react';
import { Button, Input } from '@headlessui/react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { getAuthClient } from '@/lib/auth-client';
import { getAuthClient } from '@/lib/client/auth-client';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { showPrivacyModal } from '@/components/PrivacyModal';
import { GithubIcon } from '@/components/icons/Icons';

View file

@ -4,7 +4,7 @@ import { useState, useEffect } from 'react';
import { Button, Input } from '@headlessui/react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { getAuthClient } from '@/lib/auth-client';
import { getAuthClient } from '@/lib/client/auth-client';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { showPrivacyModal } from '@/components/PrivacyModal';
import { LoadingSpinner } from '@/components/Spinner';

View file

@ -3,8 +3,8 @@ import Link from 'next/link';
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { auth } from '@/lib/server/auth';
import { isAuthEnabled } from '@/lib/server/auth-config';
import { auth } from '@/lib/server/auth/auth';
import { isAuthEnabled } from '@/lib/server/auth/config';
export const metadata: Metadata = {
title: 'Read and Listen to Documents',

View file

@ -1,7 +1,7 @@
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
import { auth } from '@/lib/server/auth';
import { isAuthEnabled } from '@/lib/server/auth-config';
import { auth } from '@/lib/server/auth/auth';
import { isAuthEnabled } from '@/lib/server/auth/config';
export async function DELETE() {
if (!isAuthEnabled() || !auth) {

View file

@ -7,24 +7,24 @@ import { randomUUID } from 'crypto';
import { and, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { audiobooks, audiobookChapters } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { requireAuthContext } from '@/lib/server/auth/auth';
import {
deleteAudiobookObject,
getAudiobookObjectBuffer,
isMissingBlobError,
listAudiobookObjects,
putAudiobookObject,
} from '@/lib/server/audiobooks-blobstore';
} from '@/lib/server/audiobooks/blobstore';
import {
decodeChapterFileName,
encodeChapterFileName,
encodeChapterTitleTag,
ffprobeAudio,
} from '@/lib/server/audiobook';
import { isS3Configured } from '@/lib/server/s3';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobook-scope';
import { getFFmpegPath } from '@/lib/server/ffmpeg-bin';
} from '@/lib/server/audiobooks/chapters';
import { isS3Configured } from '@/lib/server/storage/s3';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope';
import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin';
import type { AudiobookGenerationSettings } from '@/types/client';
import type { TTSAudioBytes, TTSAudiobookFormat } from '@/types/tts';

View file

@ -6,7 +6,7 @@ import { join } from 'path';
import { and, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { audiobooks, audiobookChapters } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { requireAuthContext } from '@/lib/server/auth/auth';
import {
audiobookPrefix,
deleteAudiobookObject,
@ -14,16 +14,16 @@ import {
getAudiobookObjectBuffer,
listAudiobookObjects,
putAudiobookObject,
} from '@/lib/server/audiobooks-blobstore';
} from '@/lib/server/audiobooks/blobstore';
import {
decodeChapterFileName,
escapeFFMetadata,
ffprobeAudio,
} from '@/lib/server/audiobook';
import { isS3Configured } from '@/lib/server/s3';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { getFFmpegPath } from '@/lib/server/ffmpeg-bin';
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobook-scope';
} from '@/lib/server/audiobooks/chapters';
import { isS3Configured } from '@/lib/server/storage/s3';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin';
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope';
import type { TTSAudiobookFormat } from '@/types/tts';
export const dynamic = 'force-dynamic';

View file

@ -2,13 +2,13 @@ import { NextRequest, NextResponse } from 'next/server';
import { and, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { audiobooks, audiobookChapters } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { getAudiobookObjectBuffer, isMissingBlobError, listAudiobookObjects } from '@/lib/server/audiobooks-blobstore';
import { decodeChapterFileName } from '@/lib/server/audiobook';
import { pruneAudiobookChaptersNotOnDisk } from '@/lib/server/audiobook-prune';
import { isS3Configured } from '@/lib/server/s3';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobook-scope';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { getAudiobookObjectBuffer, isMissingBlobError, listAudiobookObjects } from '@/lib/server/audiobooks/blobstore';
import { decodeChapterFileName } from '@/lib/server/audiobooks/chapters';
import { pruneAudiobookChaptersNotOnDisk } from '@/lib/server/audiobooks/prune';
import { isS3Configured } from '@/lib/server/storage/s3';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { buildAllowedAudiobookUserIds, pickAudiobookOwner } from '@/lib/server/audiobooks/user-scope';
import type { AudiobookGenerationSettings } from '@/types/client';
import type { TTSAudiobookChapter, TTSAudiobookFormat } from '@/types/tts';

View file

@ -1,4 +1,4 @@
import { auth } from "@/lib/server/auth"; // path to your auth file
import { auth } from "@/lib/server/auth/auth"; // path to your auth file
import { toNextJsHandler } from "better-auth/next-js";
const handlers = auth

View file

@ -2,11 +2,11 @@ import { NextRequest, NextResponse } from 'next/server';
import { and, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { documents } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { contentTypeForName } from '@/lib/server/library';
import { getDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents-blobstore';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { isS3Configured } from '@/lib/server/s3';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { contentTypeForName } from '@/lib/server/storage/library-mount';
import { getDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { isS3Configured } from '@/lib/server/storage/s3';
export const dynamic = 'force-dynamic';

View file

@ -2,10 +2,10 @@ import { NextRequest, NextResponse } from 'next/server';
import { and, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { documents } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { isValidDocumentId, presignGet } from '@/lib/server/documents-blobstore';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { isS3Configured } from '@/lib/server/s3';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { isValidDocumentId, presignGet } from '@/lib/server/documents/blobstore';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { isS3Configured } from '@/lib/server/storage/s3';
export const dynamic = 'force-dynamic';

View file

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { presignDocumentPreviewGet } from '@/lib/server/document-previews-blobstore';
import { ensureDocumentPreview } from '@/lib/server/document-previews';
import { presignDocumentPreviewGet } from '@/lib/server/documents/previews-blobstore';
import { ensureDocumentPreview } from '@/lib/server/documents/previews';
import { validatePreviewRequest } from '../utils';
export const dynamic = 'force-dynamic';

View file

@ -2,19 +2,24 @@ import { NextRequest, NextResponse } from 'next/server';
import { and, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { documents } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { isValidDocumentId } from '@/lib/server/documents-blobstore';
import { requireAuthContext } from '@/lib/server/auth/auth';
import {
getDocumentRange,
isMissingBlobError as isMissingDocumentBlobError,
isValidDocumentId,
} from '@/lib/server/documents/blobstore';
import {
getDocumentPreviewBuffer,
isMissingBlobError,
} from '@/lib/server/document-previews-blobstore';
isMissingBlobError as isMissingPreviewBlobError,
} from '@/lib/server/documents/previews-blobstore';
import {
ensureDocumentPreview,
enqueueDocumentPreview,
isPreviewableDocumentType,
} from '@/lib/server/document-previews';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { isS3Configured } from '@/lib/server/s3';
} from '@/lib/server/documents/previews';
import { extractRawTextSnippet } from '@/lib/server/documents/text-snippets';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { isS3Configured } from '@/lib/server/storage/s3';
export const dynamic = 'force-dynamic';
@ -34,6 +39,11 @@ function streamBuffer(buffer: Buffer): ReadableStream<Uint8Array> {
});
}
function clampInt(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min;
return Math.max(min, Math.min(max, Math.trunc(value)));
}
export async function GET(req: NextRequest) {
try {
if (!isS3Configured()) return s3NotConfiguredResponse();
@ -48,6 +58,7 @@ export async function GET(req: NextRequest) {
const url = new URL(req.url);
const id = (url.searchParams.get('id') || '').trim().toLowerCase();
const snippetRequested = (url.searchParams.get('snippet') || '').trim() === '1';
const presignUrl = `/api/documents/blob/preview/presign?id=${encodeURIComponent(id)}`;
const fallbackUrl = `/api/documents/blob/preview/fallback?id=${encodeURIComponent(id)}`;
if (!isValidDocumentId(id)) {
@ -74,6 +85,28 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
if (snippetRequested) {
const maxChars = clampInt(Number.parseInt(url.searchParams.get('maxChars') || '1600', 10), 100, 8000);
const maxBytes = clampInt(Number.parseInt(url.searchParams.get('maxBytes') || '131072', 10), 4096, 1024 * 1024);
try {
const head = await getDocumentRange(id, 0, maxBytes - 1, testNamespace);
const decoded = new TextDecoder().decode(new Uint8Array(head));
const snippet = extractRawTextSnippet(decoded, maxChars);
return NextResponse.json(
{ snippet },
{ headers: { 'Cache-Control': 'no-store' } },
);
} catch (error) {
if (isMissingDocumentBlobError(error)) {
await db
.delete(documents)
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)));
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
throw error;
}
}
if (!isPreviewableDocumentType(doc.type)) {
return NextResponse.json({ error: `Preview not supported for type ${doc.type}` }, { status: 415 });
}
@ -112,7 +145,7 @@ export async function GET(req: NextRequest) {
},
});
} catch (error) {
if (isMissingBlobError(error)) {
if (isMissingPreviewBlobError(error)) {
await enqueueDocumentPreview(
{
id: doc.id,

View file

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { presignDocumentPreviewGet } from '@/lib/server/document-previews-blobstore';
import { ensureDocumentPreview } from '@/lib/server/document-previews';
import { presignDocumentPreviewGet } from '@/lib/server/documents/previews-blobstore';
import { ensureDocumentPreview } from '@/lib/server/documents/previews';
import { validatePreviewRequest } from '../utils';
export const dynamic = 'force-dynamic';

View file

@ -2,11 +2,11 @@ import { NextRequest, NextResponse } from 'next/server';
import { and, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { documents } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { isValidDocumentId } from '@/lib/server/documents-blobstore';
import { isPreviewableDocumentType } from '@/lib/server/document-previews';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { isS3Configured } from '@/lib/server/s3';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { isValidDocumentId } from '@/lib/server/documents/blobstore';
import { isPreviewableDocumentType } from '@/lib/server/documents/previews';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { isS3Configured } from '@/lib/server/storage/s3';
export function s3NotConfiguredResponse(): NextResponse {
return NextResponse.json(

View file

@ -1,129 +0,0 @@
import { NextRequest, NextResponse } from 'next/server';
import { and, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { documents } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { contentTypeForName } from '@/lib/server/library';
import { extractRawTextSnippet } from '@/lib/text-snippets';
import {
getDocumentBlob,
getDocumentRange,
isMissingBlobError,
isValidDocumentId,
presignGet,
} from '@/lib/server/documents-blobstore';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { isS3Configured } from '@/lib/server/s3';
export const dynamic = 'force-dynamic';
function clampInt(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min;
return Math.max(min, Math.min(max, Math.trunc(value)));
}
function s3NotConfiguredResponse(): NextResponse {
return NextResponse.json(
{ error: 'Documents storage is not configured. Set S3_* environment variables.' },
{ status: 503 },
);
}
export async function GET(req: NextRequest) {
try {
if (!isS3Configured()) return s3NotConfiguredResponse();
const ctxOrRes = await requireAuthContext(req);
if (ctxOrRes instanceof Response) return ctxOrRes;
const testNamespace = getOpenReaderTestNamespace(req.headers);
const unclaimedUserId = getUnclaimedUserIdForNamespace(testNamespace);
const storageUserId = ctxOrRes.userId ?? unclaimedUserId;
const allowedUserIds = ctxOrRes.authEnabled ? [storageUserId, unclaimedUserId] : [unclaimedUserId];
const url = new URL(req.url);
const id = (url.searchParams.get('id') || '').trim().toLowerCase();
const format = (url.searchParams.get('format') || '').toLowerCase().trim();
const prefersProxy = (url.searchParams.get('proxy') || '').trim() === '1';
if (!isValidDocumentId(id)) {
return NextResponse.json({ error: 'Invalid id' }, { status: 400 });
}
const rows = (await db
.select({ id: documents.id, userId: documents.userId, name: documents.name, filePath: documents.filePath })
.from(documents)
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)))) as Array<{
id: string;
userId: string;
name: string;
filePath: string;
}>;
const doc = rows.find((row) => row.userId === storageUserId) ?? rows[0];
if (!doc) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
const filename = doc.name || `${id}.bin`;
const responseType = contentTypeForName(filename);
if (format === 'snippet') {
const maxChars = clampInt(Number.parseInt(url.searchParams.get('maxChars') || '1600', 10), 100, 8000);
const maxBytes = clampInt(Number.parseInt(url.searchParams.get('maxBytes') || '131072', 10), 4096, 1024 * 1024);
try {
const head = await getDocumentRange(id, 0, maxBytes - 1, testNamespace);
const decoded = new TextDecoder().decode(new Uint8Array(head));
const snippet = extractRawTextSnippet(decoded, maxChars);
return NextResponse.json(
{ snippet },
{ headers: { 'Cache-Control': 'no-store' } },
);
} catch (error) {
if (isMissingBlobError(error)) {
await db
.delete(documents)
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)));
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
throw error;
}
}
try {
if (!prefersProxy) {
const directUrl = await presignGet(id, testNamespace).catch(() => null);
if (directUrl) {
return NextResponse.redirect(directUrl, {
status: 307,
headers: { 'Cache-Control': 'no-store' },
});
}
}
const content = await getDocumentBlob(id, testNamespace);
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(new Uint8Array(content));
controller.close();
},
});
return new NextResponse(stream, {
headers: {
'Content-Type': responseType,
'Cache-Control': 'no-store',
},
});
} catch (error) {
if (isMissingBlobError(error)) {
await db
.delete(documents)
.where(and(eq(documents.id, id), inArray(documents.userId, allowedUserIds)));
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
throw error;
}
} catch (error) {
console.error('Error loading document content:', error);
return NextResponse.json({ error: 'Failed to load document content' }, { status: 500 });
}
}

View file

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAuthContext } from '@/lib/server/auth';
import { isValidDocumentId, putDocumentBlob } from '@/lib/server/documents-blobstore';
import { isS3Configured } from '@/lib/server/s3';
import { getOpenReaderTestNamespace } from '@/lib/server/test-namespace';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { isValidDocumentId, putDocumentBlob } from '@/lib/server/documents/blobstore';
import { isS3Configured } from '@/lib/server/storage/s3';
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
export const dynamic = 'force-dynamic';

View file

@ -1,8 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { requireAuthContext } from '@/lib/server/auth';
import { isValidDocumentId, presignPut } from '@/lib/server/documents-blobstore';
import { getOpenReaderTestNamespace } from '@/lib/server/test-namespace';
import { isS3Configured } from '@/lib/server/s3';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { isValidDocumentId, presignPut } from '@/lib/server/documents/blobstore';
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
import { isS3Configured } from '@/lib/server/storage/s3';
export const dynamic = 'force-dynamic';

View file

@ -5,14 +5,14 @@ import path from 'path';
import { existsSync } from 'fs';
import { randomUUID, createHash } from 'crypto';
import { pathToFileURL } from 'url';
import { requireAuthContext } from '@/lib/server/auth';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { db } from '@/db';
import { documents } from '@/db/schema';
import { safeDocumentName } from '@/lib/server/documents-utils';
import { enqueueDocumentPreview } from '@/lib/server/document-previews';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { isS3Configured } from '@/lib/server/s3';
import { putDocumentBlob } from '@/lib/server/documents-blobstore';
import { safeDocumentName } from '@/lib/server/documents/utils';
import { enqueueDocumentPreview } from '@/lib/server/documents/previews';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { isS3Configured } from '@/lib/server/storage/s3';
import { putDocumentBlob } from '@/lib/server/documents/blobstore';
const DOCSTORE_DIR = path.join(process.cwd(), 'docstore');
const TEMP_DIR = path.join(DOCSTORE_DIR, 'tmp');

View file

@ -1,8 +1,8 @@
import { readFile, stat } from 'fs/promises';
import path from 'path';
import { NextRequest, NextResponse } from 'next/server';
import { contentTypeForName, decodeLibraryId, isPathWithinRoot, parseLibraryRoots } from '@/lib/server/library';
import { auth } from '@/lib/server/auth';
import { contentTypeForName, decodeLibraryId, isPathWithinRoot, parseLibraryRoots } from '@/lib/server/storage/library-mount';
import { auth } from '@/lib/server/auth/auth';
export const dynamic = 'force-dynamic';

View file

@ -2,9 +2,9 @@ import type { Dirent } from 'fs';
import { readdir, stat } from 'fs/promises';
import path from 'path';
import { NextRequest, NextResponse } from 'next/server';
import { parseLibraryRoots } from '@/lib/server/library';
import { parseLibraryRoots } from '@/lib/server/storage/library-mount';
import type { DocumentType } from '@/types/documents';
import { auth } from '@/lib/server/auth';
import { auth } from '@/lib/server/auth/auth';
export const dynamic = 'force-dynamic';

View file

@ -2,16 +2,16 @@ import { NextRequest, NextResponse } from 'next/server';
import { and, count, eq, inArray } from 'drizzle-orm';
import { db } from '@/db';
import { documents } from '@/db/schema';
import { requireAuthContext } from '@/lib/server/auth';
import { safeDocumentName, toDocumentTypeFromName } from '@/lib/server/documents-utils';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { safeDocumentName, toDocumentTypeFromName } from '@/lib/server/documents/utils';
import {
cleanupDocumentPreviewArtifacts,
deleteDocumentPreviewRows,
enqueueDocumentPreview,
} from '@/lib/server/document-previews';
import { deleteDocumentBlob, headDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents-blobstore';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { isS3Configured } from '@/lib/server/s3';
} from '@/lib/server/documents/previews';
import { deleteDocumentBlob, headDocumentBlob, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
import { isS3Configured } from '@/lib/server/storage/s3';
import type { BaseDocument, DocumentType } from '@/types/documents';
export const dynamic = 'force-dynamic';

View file

@ -1,10 +1,10 @@
import { NextResponse, type NextRequest } from 'next/server';
import { auth } from '@/lib/server/auth';
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limiter';
import { auth } from '@/lib/server/auth/auth';
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limit/rate-limiter';
import { headers } from 'next/headers';
import { isAuthEnabled } from '@/lib/server/auth-config';
import { getClientIp } from '@/lib/server/request-ip';
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/device-id';
import { isAuthEnabled } from '@/lib/server/auth/config';
import { getClientIp } from '@/lib/server/rate-limit/request-ip';
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/rate-limit/device-id';
export const dynamic = 'force-dynamic';

View file

@ -1,17 +1,17 @@
import { NextRequest, NextResponse } from 'next/server';
import OpenAI from 'openai';
import { SpeechCreateParams } from 'openai/resources/audio/speech.mjs';
import { isKokoroModel } from '@/lib/kokoro';
import { isKokoroModel } from '@/lib/shared/kokoro';
import { LRUCache } from 'lru-cache';
import { createHash } from 'crypto';
import type { TTSRequestPayload } from '@/types/client';
import type { TTSError, TTSAudioBuffer } from '@/types/tts';
import { headers } from 'next/headers';
import { auth } from '@/lib/server/auth';
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limiter';
import { isAuthEnabled } from '@/lib/server/auth-config';
import { getClientIp } from '@/lib/server/request-ip';
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/device-id';
import { auth } from '@/lib/server/auth/auth';
import { rateLimiter, RATE_LIMITS, isTtsRateLimitEnabled } from '@/lib/server/rate-limit/rate-limiter';
import { isAuthEnabled } from '@/lib/server/auth/config';
import { getClientIp } from '@/lib/server/rate-limit/request-ip';
import { getOrCreateDeviceId, setDeviceIdCookie } from '@/lib/server/rate-limit/device-id';
function attachDeviceIdCookie(response: NextResponse, deviceId: string | null, didCreate: boolean) {
if (didCreate && deviceId) {

View file

@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from 'next/server';
import { isKokoroModel } from '@/lib/kokoro';
import { auth } from '@/lib/server/auth';
import { isKokoroModel } from '@/lib/shared/kokoro';
import { auth } from '@/lib/server/auth/auth';
const OPENAI_VOICES = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer'];
const GPT4O_MINI_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];

View file

@ -1,10 +1,10 @@
import { NextRequest, NextResponse } from 'next/server';
import { claimAnonymousData } from '@/lib/server/claim-data';
import { auth } from '@/lib/server/auth';
import { claimAnonymousData } from '@/lib/server/user/claim-data';
import { auth } from '@/lib/server/auth/auth';
import { db } from '@/db';
import { audiobooks, documents, userDocumentProgress, userPreferences } from '@/db/schema';
import { count, eq, ne } from 'drizzle-orm';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
async function checkClaimMigrationReadiness(): Promise<NextResponse | null> {
const [legacyRows] = await db

View file

@ -1,15 +1,15 @@
import { NextRequest, NextResponse } from 'next/server';
import { PassThrough, Readable } from 'stream';
import { auth } from '@/lib/server/auth';
import { auth } from '@/lib/server/auth/auth';
import { db } from '@/db';
import { documents, audiobooks, audiobookChapters, userPreferences, userDocumentProgress, userTtsChars } from '@/db/schema';
import { and, desc, eq, inArray } from 'drizzle-orm';
import archiver from 'archiver';
import { appendUserExportArchive } from '@/lib/server/user-export';
import { getDocumentBlobStream } from '@/lib/server/documents-blobstore';
import { getAudiobookObjectStream, listAudiobookObjects } from '@/lib/server/audiobooks-blobstore';
import { isS3Configured } from '@/lib/server/s3';
import { getOpenReaderTestNamespace } from '@/lib/server/test-namespace';
import { appendUserExportArchive } from '@/lib/server/user/data-export';
import { getDocumentBlobStream } from '@/lib/server/documents/blobstore';
import { getAudiobookObjectStream, listAudiobookObjects } from '@/lib/server/audiobooks/blobstore';
import { isS3Configured } from '@/lib/server/storage/s3';
import { getOpenReaderTestNamespace } from '@/lib/server/testing/test-namespace';
export const dynamic = 'force-dynamic';

View file

@ -3,7 +3,7 @@ import { eq } from 'drizzle-orm';
import { db } from '@/db';
import { userPreferences } from '@/db/schema';
import { SYNCED_PREFERENCE_KEYS, type SyncedPreferencesPatch } from '@/types/user-state';
import { resolveUserStateScope } from '@/lib/server/user-state-scope';
import { resolveUserStateScope } from '@/lib/server/user/resolve-state-scope';
export const dynamic = 'force-dynamic';

View file

@ -3,8 +3,8 @@ import { and, eq, sql } from 'drizzle-orm';
import { db } from '@/db';
import { userDocumentProgress } from '@/db/schema';
import type { ReaderType } from '@/types/user-state';
import { isValidDocumentId } from '@/lib/server/documents-blobstore';
import { resolveUserStateScope } from '@/lib/server/user-state-scope';
import { isValidDocumentId } from '@/lib/server/documents/blobstore';
import { resolveUserStateScope } from '@/lib/server/user/resolve-state-scope';
export const dynamic = 'force-dynamic';

View file

@ -5,9 +5,9 @@ import { tmpdir } from 'os';
import { join } from 'path';
import { spawn } from 'child_process';
import type { TTSSentenceAlignment, TTSAudioBytes, TTSAudioBuffer } from '@/types/tts';
import { preprocessSentenceForAudio } from '@/lib/nlp';
import { auth } from '@/lib/server/auth';
import { getFFmpegPath } from '@/lib/server/ffmpeg-bin';
import { preprocessSentenceForAudio } from '@/lib/shared/nlp';
import { auth } from '@/lib/server/auth/auth';
import { getFFmpegPath } from '@/lib/server/audiobooks/ffmpeg-bin';
export const runtime = 'nodejs';

View file

@ -18,7 +18,7 @@ import {
deleteAudiobook,
downloadAudiobookChapter,
downloadAudiobook
} from '@/lib/client';
} from '@/lib/client/api/audiobooks';
import type { AudiobookGenerationSettings } from '@/types/client';
interface AudiobookExportModalProps {
isOpen: boolean;

View file

@ -2,7 +2,7 @@
import { useEffect, useState } from 'react';
import { Analytics } from '@vercel/analytics/next';
import { CONSENT_CHANGED_EVENT, disableAnalytics, getConsentState } from '@/lib/analytics';
import { CONSENT_CHANGED_EVENT, disableAnalytics, getConsentState } from '@/lib/client/analytics';
export function ConsentAwareAnalytics() {
const [enabled, setEnabled] = useState(false);

View file

@ -2,7 +2,7 @@
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { getConsentState, setConsentState } from '@/lib/analytics';
import { getConsentState, setConsentState } from '@/lib/client/analytics';
import { Transition } from '@headlessui/react';
export function CookieConsentBanner() {

View file

@ -9,7 +9,7 @@ import {
TransitionChild,
Button,
} from '@headlessui/react';
import { updateAppConfig, getAppConfig } from '@/lib/dexie';
import { updateAppConfig, getAppConfig } from '@/lib/client/dexie';
interface PrivacyModalProps {
onAccept?: () => void;

View file

@ -23,7 +23,7 @@ import Link from 'next/link';
import { useTheme } from '@/contexts/ThemeContext';
import { useConfig } from '@/contexts/ConfigContext';
import { ChevronUpDownIcon, CheckIcon, SettingsIcon } from '@/components/icons/Icons';
import { getAppConfig, getFirstVisit, setFirstVisit } from '@/lib/dexie';
import { getAppConfig, getFirstVisit, setFirstVisit } from '@/lib/client/dexie';
import { useDocuments } from '@/contexts/DocumentContext';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { ProgressPopup } from '@/components/ProgressPopup';
@ -31,14 +31,14 @@ import { useTimeEstimation } from '@/hooks/useTimeEstimation';
import { THEMES } from '@/contexts/ThemeContext';
import { DocumentSelectionModal } from '@/components/documents/DocumentSelectionModal';
import { BaseDocument } from '@/types/documents';
import { getAuthClient } from '@/lib/auth-client';
import { getAuthClient } from '@/lib/client/auth-client';
import { useAuthSession } from '@/hooks/useAuthSession';
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
import { useRouter } from 'next/navigation';
import { showPrivacyModal } from '@/components/PrivacyModal';
import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client-documents';
import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/document-cache';
import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/document-preview-cache';
import { deleteDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents';
import { cacheStoredDocumentFromBytes, clearDocumentCache } from '@/lib/client/cache/documents';
import { clearAllDocumentPreviewCaches, clearInMemoryDocumentPreviewCache } from '@/lib/client/cache/previews';
const enableDestructiveDelete = process.env.NEXT_PUBLIC_ENABLE_DESTRUCTIVE_DELETE_ACTIONS !== 'false';
const showAllDeepInfra = process.env.NEXT_PUBLIC_SHOW_ALL_DEEPINFRA_MODELS !== 'false';

View file

@ -5,7 +5,7 @@ import type { BetterFetchError } from 'better-auth/react';
import { usePathname, useRouter } from 'next/navigation';
import { useAuthConfig, useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useAuthSession } from '@/hooks/useAuthSession';
import { getAuthClient } from '@/lib/auth-client';
import { getAuthClient } from '@/lib/client/auth-client';
import { LoadingSpinner } from '@/components/Spinner';
function sleep(ms: number) {

View file

@ -4,7 +4,7 @@ import { Button } from '@headlessui/react';
import Link from 'next/link';
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
import { useAuthSession } from '@/hooks/useAuthSession';
import { getAuthClient } from '@/lib/auth-client';
import { getAuthClient } from '@/lib/client/auth-client';
import { useRouter } from 'next/navigation';
export function UserMenu({ className = '' }: { className?: string }) {

View file

@ -5,7 +5,7 @@ import { useDocuments } from '@/contexts/DocumentContext';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { DocumentType, DocumentListDocument, Folder, DocumentListState, SortBy, SortDirection } from '@/types/documents';
import { getDocumentListState, saveDocumentListState } from '@/lib/dexie';
import { getDocumentListState, saveDocumentListState } from '@/lib/client/dexie';
import { ConfirmDialog } from '@/components/ConfirmDialog';
import { DocumentListItem } from '@/components/doclist/DocumentListItem';
import { DocumentFolder } from '@/components/doclist/DocumentFolder';

View file

@ -5,13 +5,13 @@ import {
documentPreviewFallbackUrl,
getDocumentContentSnippet,
getDocumentPreviewStatus,
} from '@/lib/client-documents';
} from '@/lib/client/api/documents';
import {
getInMemoryDocumentPreviewUrl,
getPersistedDocumentPreviewUrl,
primeDocumentPreviewCache,
setInMemoryDocumentPreviewUrl,
} from '@/lib/document-preview-cache';
} from '@/lib/client/cache/previews';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';

View file

@ -2,11 +2,11 @@
import { Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, Button } from '@headlessui/react';
import { getAppConfig, getAllEpubDocuments, getAllHtmlDocuments, getAllPdfDocuments, updateAppConfig } from '@/lib/dexie';
import { listDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client-documents';
import { getAppConfig, getAllEpubDocuments, getAllHtmlDocuments, getAllPdfDocuments, updateAppConfig } from '@/lib/client/dexie';
import { listDocuments, mimeTypeForDoc, uploadDocuments } from '@/lib/client/api/documents';
import { useDocuments } from '@/contexts/DocumentContext';
import type { BaseDocument } from '@/types/documents';
import { cacheStoredDocumentFromBytes } from '@/lib/document-cache';
import { cacheStoredDocumentFromBytes } from '@/lib/client/cache/documents';
export function DexieMigrationModal() {
const { refreshDocuments } = useDocuments();

View file

@ -4,7 +4,7 @@ import { useState, useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { UploadIcon } from '@/components/icons/Icons';
import { useDocuments } from '@/contexts/DocumentContext';
import { uploadDocxAsPdf } from '@/lib/client-documents';
import { uploadDocxAsPdf } from '@/lib/client/api/documents';
const enableDocx = process.env.NEXT_PUBLIC_ENABLE_DOCX_CONVERSION !== 'false';

View file

@ -8,7 +8,7 @@ import {
} from '@headlessui/react';
import { ChevronUpDownIcon, AudioWaveIcon } from '@/components/icons/Icons';
import { useEffect, useMemo, useState } from 'react';
import { buildKokoroVoiceString, getMaxVoicesForProvider, isKokoroModel, parseKokoroVoiceNames } from '@/lib/kokoro';
import { buildKokoroVoiceString, getMaxVoicesForProvider, isKokoroModel, parseKokoroVoiceNames } from '@/lib/shared/kokoro';
export function VoicesControlBase({
availableVoices,

View file

@ -2,9 +2,9 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, ReactNode } from 'react';
import { useLiveQuery } from 'dexie-react-hooks';
import { db, initDB, migrateLegacyDexieDocumentIdsToSha, updateAppConfig } from '@/lib/dexie';
import { db, initDB, migrateLegacyDexieDocumentIdsToSha, updateAppConfig } from '@/lib/client/dexie';
import { APP_CONFIG_DEFAULTS, type ViewType, type SavedVoices, type AppConfigValues, type AppConfigRow } from '@/types/config';
import { scheduleUserPreferencesSync, cancelPendingPreferenceSync, getUserPreferences, putUserPreferences } from '@/lib/client-user-state';
import { scheduleUserPreferencesSync, cancelPendingPreferenceSync, getUserPreferences, putUserPreferences } from '@/lib/client/api/user-state';
import { SYNCED_PREFERENCE_KEYS, type SyncedPreferenceKey, type SyncedPreferencesPatch } from '@/types/user-state';
import { useAuthSession } from '@/hooks/useAuthSession';
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';

View file

@ -2,8 +2,8 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState, ReactNode } from 'react';
import type { BaseDocument } from '@/types/documents';
import { listDocuments, uploadDocuments, deleteDocuments } from '@/lib/client-documents';
import { putCachedEpub, putCachedHtml, putCachedPdf, evictCachedEpub, evictCachedHtml, evictCachedPdf } from '@/lib/document-cache';
import { listDocuments, uploadDocuments, deleteDocuments } from '@/lib/client/api/documents';
import { putCachedEpub, putCachedHtml, putCachedPdf, evictCachedEpub, evictCachedHtml, evictCachedPdf } from '@/lib/client/cache/documents';
import { useAuthSession } from '@/hooks/useAuthSession';
interface DocumentContextType {

View file

@ -16,16 +16,16 @@ import type { NavItem } from 'epubjs';
import type { SpineItem } from 'epubjs/types/section';
import type { Book, Rendition } from 'epubjs';
import { setLastDocumentLocation } from '@/lib/dexie';
import { scheduleDocumentProgressSync } from '@/lib/client-user-state';
import { getDocumentMetadata } from '@/lib/client-documents';
import { ensureCachedDocument } from '@/lib/document-cache';
import { setLastDocumentLocation } from '@/lib/client/dexie';
import { scheduleDocumentProgressSync } from '@/lib/client/api/user-state';
import { getDocumentMetadata } from '@/lib/client/api/documents';
import { ensureCachedDocument } from '@/lib/client/cache/documents';
import { useTTS } from '@/contexts/TTSContext';
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
import { createRangeCfi } from '@/lib/epub';
import { createRangeCfi } from '@/lib/client/epub';
import { useParams } from 'next/navigation';
import { useConfig } from './ConfigContext';
import { withRetry, getAudiobookStatus, generateTTS, createAudiobookChapter } from '@/lib/client';
import { withRetry, getAudiobookStatus, generateTTS, createAudiobookChapter } from '@/lib/client/api/audiobooks';
import { CmpStr } from 'cmpstr';
import type {
TTSSentenceAlignment,

View file

@ -10,8 +10,8 @@ import {
useEffect,
useRef,
} from 'react';
import { getDocumentMetadata } from '@/lib/client-documents';
import { ensureCachedDocument } from '@/lib/document-cache';
import { getDocumentMetadata } from '@/lib/client/api/documents';
import { ensureCachedDocument } from '@/lib/client/cache/documents';
import { useTTS } from '@/contexts/TTSContext';
interface HTMLContextType {

View file

@ -27,19 +27,19 @@ import {
import type { PDFDocumentProxy } from 'pdfjs-dist';
import { getDocumentMetadata } from '@/lib/client-documents';
import { ensureCachedDocument } from '@/lib/document-cache';
import { getDocumentMetadata } from '@/lib/client/api/documents';
import { ensureCachedDocument } from '@/lib/client/cache/documents';
import { useTTS } from '@/contexts/TTSContext';
import { useConfig } from '@/contexts/ConfigContext';
import { normalizeTextForTts } from '@/lib/nlp';
import { withRetry, getAudiobookStatus, generateTTS, createAudiobookChapter } from '@/lib/client';
import { normalizeTextForTts } from '@/lib/shared/nlp';
import { withRetry, getAudiobookStatus, generateTTS, createAudiobookChapter } from '@/lib/client/api/audiobooks';
import {
extractTextFromPDF,
highlightPattern,
clearHighlights,
clearWordHighlights,
highlightWordIndex,
} from '@/lib/pdf';
} from '@/lib/client/pdf';
import type {
TTSSentenceAlignment,

View file

@ -34,12 +34,12 @@ import { useAudioCache } from '@/hooks/audio/useAudioCache';
import { useVoiceManagement } from '@/hooks/audio/useVoiceManagement';
import { useMediaSession } from '@/hooks/audio/useMediaSession';
import { useAudioContext } from '@/hooks/audio/useAudioContext';
import { getLastDocumentLocation, setLastDocumentLocation } from '@/lib/dexie';
import { getDocumentProgress, scheduleDocumentProgressSync } from '@/lib/client-user-state';
import { getLastDocumentLocation, setLastDocumentLocation } from '@/lib/client/dexie';
import { getDocumentProgress, scheduleDocumentProgressSync } from '@/lib/client/api/user-state';
import { useBackgroundState } from '@/hooks/audio/useBackgroundState';
import { withRetry, generateTTS, alignAudio } from '@/lib/client';
import { preprocessSentenceForAudio, splitTextToTtsBlocks, splitTextToTtsBlocksEPUB } from '@/lib/nlp';
import { isKokoroModel } from '@/lib/kokoro';
import { withRetry, generateTTS, alignAudio } from '@/lib/client/api/audiobooks';
import { preprocessSentenceForAudio, splitTextToTtsBlocks, splitTextToTtsBlocksEPUB } from '@/lib/shared/nlp';
import { isKokoroModel } from '@/lib/shared/kokoro';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import type {
TTSLocation,

View file

@ -10,11 +10,11 @@ export const documents = pgTable('documents', {
lastModified: bigint('last_modified', { mode: 'number' }).notNull(),
filePath: text('file_path').notNull(),
createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
pk: primaryKey({ columns: [table.id, table.userId] }),
userIdIdx: index('idx_documents_user_id').on(table.userId),
userIdLastModifiedIdx: index('idx_documents_user_id_last_modified').on(table.userId, table.lastModified),
}));
}, (table) => [
primaryKey({ columns: [table.id, table.userId] }),
index('idx_documents_user_id').on(table.userId),
index('idx_documents_user_id_last_modified').on(table.userId, table.lastModified),
]);
export const audiobooks = pgTable('audiobooks', {
id: text('id').notNull(),
@ -25,9 +25,9 @@ export const audiobooks = pgTable('audiobooks', {
coverPath: text('cover_path'),
duration: real('duration').default(0),
createdAt: timestamp('created_at').defaultNow(),
}, (table) => ({
pk: primaryKey({ columns: [table.id, table.userId] }),
}));
}, (table) => [
primaryKey({ columns: [table.id, table.userId] }),
]);
export const audiobookChapters = pgTable('audiobook_chapters', {
id: text('id').notNull(),
@ -38,13 +38,13 @@ export const audiobookChapters = pgTable('audiobook_chapters', {
duration: real('duration').default(0),
filePath: text('file_path').notNull(),
format: text('format').notNull(), // mp3, m4b
}, (table) => ({
pk: primaryKey({ columns: [table.id, table.userId] }),
bookFk: foreignKey({
}, (table) => [
primaryKey({ columns: [table.id, table.userId] }),
foreignKey({
columns: [table.bookId, table.userId],
foreignColumns: [audiobooks.id, audiobooks.userId],
}).onDelete('cascade'),
}));
]);
// Auth tables (user, session, account, verification) are managed by Better Auth.
// They are created/migrated via `@better-auth/cli migrate` and should NOT be
@ -56,10 +56,10 @@ export const userTtsChars = pgTable("user_tts_chars", {
charCount: bigint('char_count', { mode: 'number' }).default(0),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
}, (table) => ({
pk: primaryKey({ columns: [table.userId, table.date] }),
dateIdx: index('idx_user_tts_chars_date').on(table.date),
}));
}, (table) => [
primaryKey({ columns: [table.userId, table.date] }),
index('idx_user_tts_chars_date').on(table.date),
]);
export const userPreferences = pgTable('user_preferences', {
userId: text('user_id').primaryKey().references(() => user.id, { onDelete: 'cascade' }),
@ -78,10 +78,10 @@ export const userDocumentProgress = pgTable('user_document_progress', {
clientUpdatedAtMs: bigint('client_updated_at_ms', { mode: 'number' }).notNull().default(0),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
}, (table) => ({
pk: primaryKey({ columns: [table.userId, table.documentId] }),
userUpdatedIdx: index('idx_user_document_progress_user_id_updated_at').on(table.userId, table.updatedAt),
}));
}, (table) => [
primaryKey({ columns: [table.userId, table.documentId] }),
index('idx_user_document_progress_user_id_updated_at').on(table.userId, table.updatedAt),
]);
export const documentPreviews = pgTable('document_previews', {
documentId: text('document_id').notNull(),
@ -101,7 +101,7 @@ export const documentPreviews = pgTable('document_previews', {
lastError: text('last_error'),
createdAtMs: bigint('created_at_ms', { mode: 'number' }).notNull().default(0),
updatedAtMs: bigint('updated_at_ms', { mode: 'number' }).notNull().default(0),
}, (table) => ({
pk: primaryKey({ columns: [table.documentId, table.namespace, table.variant] }),
statusLeaseIdx: index('idx_document_previews_status_lease').on(table.status, table.leaseUntilMs),
}));
}, (table) => [
primaryKey({ columns: [table.documentId, table.namespace, table.variant] }),
index('idx_document_previews_status_lease').on(table.status, table.leaseUntilMs),
]);

View file

@ -11,11 +11,11 @@ export const documents = sqliteTable('documents', {
lastModified: integer('last_modified').notNull(),
filePath: text('file_path').notNull(),
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
}, (table) => ({
pk: primaryKey({ columns: [table.id, table.userId] }),
userIdIdx: index('idx_documents_user_id').on(table.userId),
userIdLastModifiedIdx: index('idx_documents_user_id_last_modified').on(table.userId, table.lastModified),
}));
}, (table) => [
primaryKey({ columns: [table.id, table.userId] }),
index('idx_documents_user_id').on(table.userId),
index('idx_documents_user_id_last_modified').on(table.userId, table.lastModified),
]);
export const audiobooks = sqliteTable('audiobooks', {
id: text('id').notNull(),
@ -26,9 +26,9 @@ export const audiobooks = sqliteTable('audiobooks', {
coverPath: text('cover_path'),
duration: real('duration').default(0),
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
}, (table) => ({
pk: primaryKey({ columns: [table.id, table.userId] }),
}));
}, (table) => [
primaryKey({ columns: [table.id, table.userId] }),
]);
export const audiobookChapters = sqliteTable('audiobook_chapters', {
id: text('id').notNull(),
@ -39,13 +39,13 @@ export const audiobookChapters = sqliteTable('audiobook_chapters', {
duration: real('duration').default(0),
filePath: text('file_path').notNull(),
format: text('format').notNull(), // mp3, m4b
}, (table) => ({
pk: primaryKey({ columns: [table.id, table.userId] }),
bookFk: foreignKey({
}, (table) => [
primaryKey({ columns: [table.id, table.userId] }),
foreignKey({
columns: [table.bookId, table.userId],
foreignColumns: [audiobooks.id, audiobooks.userId],
}).onDelete('cascade'),
}));
]);
// Auth tables (user, session, account, verification) are managed by Better Auth.
// They are created/migrated via `@better-auth/cli migrate` and should NOT be
@ -57,10 +57,10 @@ export const userTtsChars = sqliteTable("user_tts_chars", {
charCount: integer('char_count').default(0),
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
updatedAt: integer('updated_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
}, (table) => ({
pk: primaryKey({ columns: [table.userId, table.date] }),
dateIdx: index('idx_user_tts_chars_date').on(table.date),
}));
}, (table) => [
primaryKey({ columns: [table.userId, table.date] }),
index('idx_user_tts_chars_date').on(table.date),
]);
export const userPreferences = sqliteTable('user_preferences', {
userId: text('user_id').primaryKey().references(() => user.id, { onDelete: 'cascade' }),
@ -79,10 +79,10 @@ export const userDocumentProgress = sqliteTable('user_document_progress', {
clientUpdatedAtMs: integer('client_updated_at_ms').notNull().default(0),
createdAt: integer('created_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
updatedAt: integer('updated_at').default(sql`(cast(strftime('%s','now') as int) * 1000)`),
}, (table) => ({
pk: primaryKey({ columns: [table.userId, table.documentId] }),
userUpdatedIdx: index('idx_user_document_progress_user_id_updated_at').on(table.userId, table.updatedAt),
}));
}, (table) => [
primaryKey({ columns: [table.userId, table.documentId] }),
index('idx_user_document_progress_user_id_updated_at').on(table.userId, table.updatedAt),
]);
export const documentPreviews = sqliteTable('document_previews', {
documentId: text('document_id').notNull(),
@ -102,7 +102,7 @@ export const documentPreviews = sqliteTable('document_previews', {
lastError: text('last_error'),
createdAtMs: integer('created_at_ms').notNull().default(0),
updatedAtMs: integer('updated_at_ms').notNull().default(0),
}, (table) => ({
pk: primaryKey({ columns: [table.documentId, table.namespace, table.variant] }),
statusLeaseIdx: index('idx_document_previews_status_lease').on(table.status, table.leaseUntilMs),
}));
}, (table) => [
primaryKey({ columns: [table.documentId, table.namespace, table.variant] }),
index('idx_document_previews_status_lease').on(table.status, table.leaseUntilMs),
]);

View file

@ -1,7 +1,7 @@
'use client';
import { useState, useCallback } from 'react';
import { getVoices } from '@/lib/client';
import { getVoices } from '@/lib/client/api/audiobooks';
const DEFAULT_VOICES = ['alloy', 'ash', 'coral', 'echo', 'fable', 'onyx', 'nova', 'sage', 'shimmer'];

View file

@ -2,9 +2,9 @@
import { useCallback } from 'react';
import { useLiveQuery } from 'dexie-react-hooks';
import { db } from '@/lib/dexie';
import { db } from '@/lib/client/dexie';
import type { EPUBDocument } from '@/types/documents';
import { sha256HexFromArrayBuffer } from '@/lib/sha256';
import { sha256HexFromArrayBuffer } from '@/lib/client/sha256';
export function useEPUBDocuments() {
const documents = useLiveQuery(

View file

@ -1,5 +1,5 @@
import { useEffect, RefObject, useState } from 'react';
import { debounce } from '@/lib/pdf';
import { debounce } from '@/lib/client/pdf';
export function useEPUBResize(containerRef: RefObject<HTMLDivElement | null>) {
const [isResizing, setIsResizing] = useState(false);

View file

@ -2,9 +2,9 @@
import { useCallback } from 'react';
import { useLiveQuery } from 'dexie-react-hooks';
import { db } from '@/lib/dexie';
import { db } from '@/lib/client/dexie';
import type { HTMLDocument } from '@/types/documents';
import { sha256HexFromString } from '@/lib/sha256';
import { sha256HexFromString } from '@/lib/client/sha256';
export function useHTMLDocuments() {
const documents = useLiveQuery(

View file

@ -2,9 +2,9 @@
import { useCallback } from 'react';
import { useLiveQuery } from 'dexie-react-hooks';
import { db } from '@/lib/dexie';
import { db } from '@/lib/client/dexie';
import type { PDFDocument } from '@/types/documents';
import { sha256HexFromArrayBuffer } from '@/lib/sha256';
import { sha256HexFromArrayBuffer } from '@/lib/client/sha256';
export function usePDFDocuments() {
const documents = useLiveQuery(

View file

@ -1,5 +1,5 @@
import { RefObject, useState, useEffect } from 'react';
import { debounce } from '@/lib/pdf';
import { debounce } from '@/lib/client/pdf';
interface UsePDFResizeResult {
containerWidth: number;

View file

@ -2,7 +2,7 @@
import { useMemo } from 'react';
import { useAuthConfig } from '@/contexts/AuthRateLimitContext';
import { getAuthClient } from '@/lib/auth-client';
import { getAuthClient } from '@/lib/client/auth-client';
type SessionHookResult = ReturnType<ReturnType<typeof getAuthClient>['useSession']>;

View file

@ -1,4 +1,4 @@
import { sha256HexFromArrayBuffer } from '@/lib/sha256';
import { sha256HexFromArrayBuffer } from '@/lib/client/sha256';
import type { BaseDocument, DocumentType } from '@/types/documents';
export type UploadSource = {
@ -243,11 +243,11 @@ export async function getDocumentContentSnippet(
): Promise<string> {
const params = new URLSearchParams();
params.set('id', id);
params.set('format', 'snippet');
params.set('snippet', '1');
if (typeof options?.maxChars === 'number') params.set('maxChars', String(options.maxChars));
if (typeof options?.maxBytes === 'number') params.set('maxBytes', String(options.maxBytes));
const res = await fetch(`/api/documents/blob?${params.toString()}`, { signal: options?.signal });
const res = await fetch(`/api/documents/blob/preview/fallback?${params.toString()}`, { signal: options?.signal });
if (!res.ok) {
const data = (await res.json().catch(() => null)) as { error?: string } | null;
throw new Error(data?.error || `Failed to load content snippet (status ${res.status})`);

View file

@ -1,5 +1,5 @@
import type { BaseDocument, EPUBDocument, HTMLDocument, PDFDocument } from '@/types/documents';
import { downloadDocumentContent } from '@/lib/client-documents';
import { downloadDocumentContent } from '@/lib/client/api/documents';
export type DocumentCacheBackend = {
get: (meta: BaseDocument) => Promise<PDFDocument | EPUBDocument | HTMLDocument | null>;
@ -42,12 +42,12 @@ export async function ensureCachedDocumentCore(
}
export async function getCachedPdf(id: string): Promise<PDFDocument | null> {
const { getPdfDocument } = await import('@/lib/dexie');
const { getPdfDocument } = await import('@/lib/client/dexie');
return (await getPdfDocument(id)) ?? null;
}
export async function putCachedPdf(meta: BaseDocument, data: ArrayBuffer): Promise<void> {
const { addPdfDocument } = await import('@/lib/dexie');
const { addPdfDocument } = await import('@/lib/client/dexie');
await addPdfDocument({
id: meta.id,
type: 'pdf',
@ -59,17 +59,17 @@ export async function putCachedPdf(meta: BaseDocument, data: ArrayBuffer): Promi
}
export async function evictCachedPdf(id: string): Promise<void> {
const { removePdfDocument } = await import('@/lib/dexie');
const { removePdfDocument } = await import('@/lib/client/dexie');
await removePdfDocument(id);
}
export async function getCachedEpub(id: string): Promise<EPUBDocument | null> {
const { getEpubDocument } = await import('@/lib/dexie');
const { getEpubDocument } = await import('@/lib/client/dexie');
return (await getEpubDocument(id)) ?? null;
}
export async function putCachedEpub(meta: BaseDocument, data: ArrayBuffer): Promise<void> {
const { addEpubDocument } = await import('@/lib/dexie');
const { addEpubDocument } = await import('@/lib/client/dexie');
await addEpubDocument({
id: meta.id,
type: 'epub',
@ -81,17 +81,17 @@ export async function putCachedEpub(meta: BaseDocument, data: ArrayBuffer): Prom
}
export async function evictCachedEpub(id: string): Promise<void> {
const { removeEpubDocument } = await import('@/lib/dexie');
const { removeEpubDocument } = await import('@/lib/client/dexie');
await removeEpubDocument(id);
}
export async function getCachedHtml(id: string): Promise<HTMLDocument | null> {
const { getHtmlDocument } = await import('@/lib/dexie');
const { getHtmlDocument } = await import('@/lib/client/dexie');
return (await getHtmlDocument(id)) ?? null;
}
export async function putCachedHtml(meta: BaseDocument, data: string): Promise<void> {
const { addHtmlDocument } = await import('@/lib/dexie');
const { addHtmlDocument } = await import('@/lib/client/dexie');
await addHtmlDocument({
id: meta.id,
type: 'html',
@ -103,12 +103,12 @@ export async function putCachedHtml(meta: BaseDocument, data: string): Promise<v
}
export async function evictCachedHtml(id: string): Promise<void> {
const { removeHtmlDocument } = await import('@/lib/dexie');
const { removeHtmlDocument } = await import('@/lib/client/dexie');
await removeHtmlDocument(id);
}
export async function clearDocumentCache(): Promise<void> {
const { clearPdfDocuments, clearEpubDocuments, clearHtmlDocuments } = await import('@/lib/dexie');
const { clearPdfDocuments, clearEpubDocuments, clearHtmlDocuments } = await import('@/lib/client/dexie');
await Promise.all([clearPdfDocuments(), clearEpubDocuments(), clearHtmlDocuments()]);
}
@ -132,7 +132,7 @@ export async function ensureCachedDocument(meta: BaseDocument, options?: { signa
meta,
{
get: async (m) => {
const { getPdfDocument, getEpubDocument, getHtmlDocument } = await import('@/lib/dexie');
const { getPdfDocument, getEpubDocument, getHtmlDocument } = await import('@/lib/client/dexie');
if (m.type === 'pdf') return (await getPdfDocument(m.id)) ?? null;
if (m.type === 'epub') return (await getEpubDocument(m.id)) ?? null;
return (await getHtmlDocument(m.id)) ?? null;

View file

@ -3,8 +3,8 @@ import {
getDocumentPreviewCache,
putDocumentPreviewCache,
removeDocumentPreviewCache,
} from '@/lib/dexie';
import { documentPreviewFallbackUrl, documentPreviewPresignUrl } from '@/lib/client-documents';
} from '@/lib/client/dexie';
import { documentPreviewFallbackUrl, documentPreviewPresignUrl } from '@/lib/client/api/documents';
const inMemoryPreviewUrlCache = new Map<string, string>();
const inFlightPreviewPrime = new Map<string, Promise<string | null>>();

View file

@ -8,9 +8,9 @@ import {
BaseDocument,
DocumentListDocument,
} from '@/types/documents';
import { sha256HexFromBytes, sha256HexFromString } from '@/lib/sha256';
import { downloadDocumentContent, listDocuments, uploadDocumentSources, type UploadSource } from '@/lib/client-documents';
import { cacheStoredDocumentFromBytes } from '@/lib/document-cache';
import { sha256HexFromBytes, sha256HexFromString } from '@/lib/client/sha256';
import { downloadDocumentContent, listDocuments, uploadDocumentSources, type UploadSource } from '@/lib/client/api/documents';
import { cacheStoredDocumentFromBytes } from '@/lib/client/cache/documents';
const DB_NAME = 'openreader-db';
// Managed via Dexie (version bumped from the original manual IndexedDB)

View file

@ -32,7 +32,7 @@ function getHighlightWorker(): Worker | null {
try {
highlightWorker = new Worker(
new URL('pdfHighlightWorker.ts', import.meta.url),
new URL('pdf-highlight-worker.ts', import.meta.url),
{ type: 'module' }
);
return highlightWorker;

View file

@ -1,150 +0,0 @@
import { pdfjs } from 'react-pdf';
import ePub from 'epubjs';
export { extractRawTextSnippet, extractTextSnippet } from '@/lib/text-snippets';
function shouldUseLegacyPdfWorker(): boolean {
if (typeof window === 'undefined') return false;
const ua = window.navigator.userAgent;
const isSafari = /^((?!chrome|android).)*safari/i.test(ua);
if (!isSafari) return false;
const match = ua.match(/Version\/(\d+)/i);
if (!match?.[1]) return true;
const version = Number.parseInt(match[1], 10);
return Number.isFinite(version) ? version < 18 : true;
}
export function ensurePdfWorker(): void {
if (typeof window === 'undefined') return;
if (pdfjs.GlobalWorkerOptions.workerSrc) return;
const useLegacy = shouldUseLegacyPdfWorker();
const workerSrc = useLegacy
? new URL('pdfjs-dist/legacy/build/pdf.worker.min.mjs', import.meta.url).href
: new URL('pdfjs-dist/build/pdf.worker.min.mjs', import.meta.url).href;
pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
pdfjs.GlobalWorkerOptions.workerPort = null;
}
async function blobToDataUrl(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(new Error('Failed to read blob'));
reader.onload = () => resolve(String(reader.result));
reader.readAsDataURL(blob);
});
}
async function scaleImageBlobToDataUrl(blob: Blob, targetWidth: number): Promise<string> {
if (typeof window === 'undefined') {
return blobToDataUrl(blob);
}
if (typeof createImageBitmap !== 'function') {
return blobToDataUrl(blob);
}
const bitmap = await createImageBitmap(blob);
try {
const scale = targetWidth > 0 ? targetWidth / bitmap.width : 1;
const width = Math.max(1, Math.round(bitmap.width * scale));
const height = Math.max(1, Math.round(bitmap.height * scale));
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) {
return blobToDataUrl(blob);
}
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(bitmap, 0, 0, width, height);
const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
return dataUrl;
} finally {
bitmap.close();
}
}
export async function renderPdfFirstPageToDataUrl(
data: ArrayBuffer,
targetWidth: number,
): Promise<string> {
if (typeof window === 'undefined') {
throw new Error('PDF thumbnail rendering must run in the browser');
}
ensurePdfWorker();
const loadingTask = pdfjs.getDocument({ data });
const pdf = await loadingTask.promise;
try {
const page = await pdf.getPage(1);
const viewport = page.getViewport({ scale: 1 });
const scale = targetWidth > 0 ? targetWidth / viewport.width : 1;
const scaledViewport = page.getViewport({ scale });
const canvas = document.createElement('canvas');
canvas.width = Math.max(1, Math.floor(scaledViewport.width));
canvas.height = Math.max(1, Math.floor(scaledViewport.height));
const ctx = canvas.getContext('2d', { alpha: false });
if (!ctx) {
throw new Error('Failed to create canvas context');
}
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const renderTask = page.render({
canvasContext: ctx,
viewport: scaledViewport,
intent: 'display',
});
await renderTask.promise;
return canvas.toDataURL('image/jpeg', 0.82);
} finally {
await pdf.destroy().catch(() => undefined);
await loadingTask.destroy().catch(() => undefined);
}
}
export async function extractEpubCoverToDataUrl(
data: ArrayBuffer,
targetWidth: number,
): Promise<string | null> {
if (typeof window === 'undefined') {
return null;
}
const book = ePub(data);
const opened = book.opened.catch(() => undefined);
try {
const coverObjectUrl = await book.coverUrl();
if (!coverObjectUrl) return null;
const res = await fetch(coverObjectUrl);
const blob = await res.blob();
if (coverObjectUrl.startsWith('blob:')) {
URL.revokeObjectURL(coverObjectUrl);
}
return await scaleImageBlobToDataUrl(blob, targetWidth);
} catch {
return null;
} finally {
void opened.finally(() => {
try {
book.destroy();
} catch {
// ignore
}
});
}
}
// Note: text snippet helpers live in `src/lib/text-snippets.ts` to stay server-safe.

View file

@ -6,7 +6,7 @@ import {
ListObjectsV2Command,
PutObjectCommand,
} from '@aws-sdk/client-s3';
import { getS3Client, getS3Config } from '@/lib/server/s3';
import { getS3Client, getS3Config } from '@/lib/server/storage/s3';
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
const SAFE_BOOK_ID_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;

View file

@ -102,7 +102,7 @@ function parseTitleFromFFMetadata(stdout: string): string | undefined {
}
export async function ffprobeAudio(filePath: string, signal?: AbortSignal): Promise<ProbeResult> {
const { getFFmpegPath } = await import('@/lib/server/ffmpeg-bin');
const { getFFmpegPath } = await import('@/lib/server/audiobooks/ffmpeg-bin');
return new Promise<ProbeResult>((resolve, reject) => {
const ffmpeg = spawn(getFFmpegPath(), [

View file

@ -5,7 +5,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { db } from "@/db";
import { isAuthEnabled, isAnonymousAuthSessionsEnabled } from "@/lib/server/auth-config";
import { isAuthEnabled, isAnonymousAuthSessionsEnabled } from "@/lib/server/auth/config";
import * as authSchemaSqlite from "@/db/schema_auth_sqlite";
import * as authSchemaPostgres from "@/db/schema_auth_postgres";
@ -76,7 +76,7 @@ const createAuth = () => betterAuth({
enabled: true,
beforeDelete: async (user) => {
try {
const { deleteUserStorageData } = await import('@/lib/server/user-data-cleanup');
const { deleteUserStorageData } = await import('@/lib/server/user/data-cleanup');
await deleteUserStorageData(user.id, null);
} catch (error) {
console.error('[auth] Failed to clean up user storage before deletion:', error);
@ -122,8 +122,8 @@ const createAuth = () => betterAuth({
// Lazy-load heavy modules only when account linking actually happens
const [{ rateLimiter }, claimData] = await Promise.all([
import('@/lib/server/rate-limiter'),
import('@/lib/server/claim-data'),
import('@/lib/server/rate-limit/rate-limiter'),
import('@/lib/server/user/claim-data'),
]);
// Transfer rate limiting data (TTS char counts) from anonymous user to authenticated user

View file

@ -7,7 +7,7 @@ import {
PutObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { getS3Client, getS3Config } from '@/lib/server/s3';
import { getS3Client, getS3Config } from '@/lib/server/storage/s3';
const DOCUMENT_ID_REGEX = /^[a-f0-9]{64}$/i;
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;

View file

@ -4,8 +4,8 @@ import {
PutObjectCommand,
} from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { deleteDocumentPrefix, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents-blobstore';
import { getS3Client, getS3Config } from '@/lib/server/s3';
import { deleteDocumentPrefix, isMissingBlobError, isValidDocumentId } from '@/lib/server/documents/blobstore';
import { getS3Client, getS3Config } from '@/lib/server/storage/s3';
const SAFE_NAMESPACE_REGEX = /^[a-zA-Z0-9._-]{1,128}$/;
const DEFAULT_NAMESPACE_SEGMENT = '_default';

View file

@ -14,9 +14,9 @@ import {
headDocumentPreview,
isMissingBlobError,
putDocumentPreviewBuffer,
} from '@/lib/server/document-previews-blobstore';
import { getDocumentBlob } from '@/lib/server/documents-blobstore';
import { renderEpubCoverToJpeg, renderPdfFirstPageToJpeg } from '@/lib/server/document-previews-render';
} from '@/lib/server/documents/previews-blobstore';
import { getDocumentBlob } from '@/lib/server/documents/blobstore';
import { renderEpubCoverToJpeg, renderPdfFirstPageToJpeg } from '@/lib/server/documents/previews-render';
const LEASE_MS = 45_000;
const RETRY_AFTER_MS = 1_500;

View file

@ -1,6 +1,6 @@
import { db } from '@/db';
import { userTtsChars } from '@/db/schema';
import { isAuthEnabled } from '@/lib/server/auth-config';
import { isAuthEnabled } from '@/lib/server/auth/config';
import { eq, and, lt, sql } from 'drizzle-orm';
function readPositiveIntEnv(name: string, fallback: number): number {

View file

@ -1,5 +1,5 @@
import path from 'path';
import { UNCLAIMED_USER_ID } from '@/lib/server/docstore';
import { UNCLAIMED_USER_ID } from '@/lib/server/storage/docstore-legacy';
import { ensureSystemUserExists } from '@/db';
const TEST_NAMESPACE_HEADER = 'x-openreader-test-namespace';

View file

@ -1,16 +1,16 @@
import { db } from '@/db';
import { documents, audiobooks, audiobookChapters, userPreferences, userDocumentProgress } from '@/db/schema';
import { eq, and, inArray } from 'drizzle-orm';
import { UNCLAIMED_USER_ID } from './docstore';
import { UNCLAIMED_USER_ID } from '../storage/docstore-legacy';
import {
deleteAudiobookObject,
getAudiobookObjectBuffer,
listAudiobookObjects,
putAudiobookObject,
} from './audiobooks-blobstore';
import { isS3Configured } from './s3';
} from '../audiobooks/blobstore';
import { isS3Configured } from '../storage/s3';
import { isAuthEnabled } from '@/lib/server/auth-config';
import { isAuthEnabled } from '@/lib/server/auth/config';
type AudiobookRow = {
id: string;

View file

@ -7,10 +7,10 @@
import { db } from '@/db';
import { documents, audiobooks } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { isS3Configured } from '@/lib/server/s3';
import { deleteDocumentBlob } from '@/lib/server/documents-blobstore';
import { deleteDocumentPreviewArtifacts } from '@/lib/server/document-previews-blobstore';
import { audiobookPrefix, deleteAudiobookPrefix } from '@/lib/server/audiobooks-blobstore';
import { isS3Configured } from '@/lib/server/storage/s3';
import { deleteDocumentBlob } from '@/lib/server/documents/blobstore';
import { deleteDocumentPreviewArtifacts } from '@/lib/server/documents/previews-blobstore';
import { audiobookPrefix, deleteAudiobookPrefix } from '@/lib/server/audiobooks/blobstore';
type DocumentRow = { id: string };
type AudiobookRow = { id: string };

View file

@ -1,7 +1,7 @@
import type { Archiver } from 'archiver';
import { Readable } from 'stream';
import type { ReadableStream as NodeReadableStream } from 'stream/web';
import { decodeChapterFileName } from '@/lib/server/audiobook';
import { decodeChapterFileName } from '@/lib/server/audiobooks/chapters';
export type ExportBlobBody =
| NodeJS.ReadableStream

View file

@ -1,7 +1,7 @@
import type { NextRequest } from 'next/server';
import type { AuthContext } from '@/lib/server/auth';
import { requireAuthContext } from '@/lib/server/auth';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/test-namespace';
import type { AuthContext } from '@/lib/server/auth/auth';
import { requireAuthContext } from '@/lib/server/auth/auth';
import { getOpenReaderTestNamespace, getUnclaimedUserIdForNamespace } from '@/lib/server/testing/test-namespace';
export type ResolvedUserStateScope = {
auth: AuthContext;

View file

@ -2,9 +2,9 @@ import { ListObjectsV2Command } from '@aws-sdk/client-s3';
import { and, eq, inArray, like } from 'drizzle-orm';
import { db } from '../src/db';
import { audiobooks, audiobookChapters, documents } from '../src/db/schema';
import { deleteDocumentPrefix } from '../src/lib/server/documents-blobstore';
import { deleteAudiobookPrefix } from '../src/lib/server/audiobooks-blobstore';
import { getS3Client, getS3Config, isS3Configured } from '../src/lib/server/s3';
import { deleteDocumentPrefix } from '../src/lib/server/documents/blobstore';
import { deleteAudiobookPrefix } from '../src/lib/server/audiobooks/blobstore';
import { getS3Client, getS3Config, isS3Configured } from '../src/lib/server/storage/s3';
function chunk<T>(items: T[], size: number): T[][] {
if (items.length === 0) return [];

Some files were not shown because too many files have changed in this diff Show more