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:
parent
8e1b6b4009
commit
c2188b476f
110 changed files with 302 additions and 548 deletions
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 }) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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']>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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})`);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>>();
|
||||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
|
|
@ -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.
|
||||
|
|
@ -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}$/;
|
||||
|
|
@ -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(), [
|
||||
|
|
@ -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
|
||||
|
|
@ -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}$/;
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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';
|
||||
|
|
@ -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;
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
Loading…
Reference in a new issue