Introduce `serverLogger` utility based on pino for consistent, structured logging across all server and API modules. Replace direct console logging with `serverLogger` and add request-scoped logging helpers. Update environment variable handling, documentation, and deployment guides to reflect new logging configuration (`LOG_FORMAT`, `LOG_LEVEL`, `COMPUTE_LOG_LEVEL`). Enforce no-console in server code via ESLint and add pino/pino-pretty dependencies. This change standardizes log output, improves observability, and prepares the codebase for ingestion by log platforms.
113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
/**
|
|
* Cleans up all S3 storage artifacts belonging to a user.
|
|
* Called from Better Auth's `beforeDelete` hook so that blobs are removed
|
|
* before the DB cascade wipes the metadata rows we query against.
|
|
*/
|
|
|
|
import { db } from '@/db';
|
|
import { documents, audiobooks } from '@/db/schema';
|
|
import { eq } from 'drizzle-orm';
|
|
import { getS3Config, isS3Configured } from '@/lib/server/storage/s3';
|
|
import { deleteDocumentBlob } from '@/lib/server/documents/blobstore';
|
|
import { deleteDocumentPreviewArtifacts } from '@/lib/server/documents/previews-blobstore';
|
|
import { deleteDocumentPreviewRows } from '@/lib/server/documents/previews';
|
|
import { audiobookPrefix, deleteAudiobookPrefix } from '@/lib/server/audiobooks/blobstore';
|
|
import { deleteTtsSegmentPrefix } from '@/lib/server/tts/segments-blobstore';
|
|
import { serverLogger } from '@/lib/server/logger';
|
|
|
|
type DocumentRow = { id: string };
|
|
type AudiobookRow = { id: string };
|
|
|
|
/**
|
|
* Delete all S3 blobs owned by `userId`.
|
|
*
|
|
* This covers:
|
|
* - Document file blobs
|
|
* - Document preview images
|
|
* - Audiobook audio files (chapter mp3s, metadata json, etc.)
|
|
*
|
|
* Each item is cleaned up independently; a failure on one does not block the rest.
|
|
*/
|
|
export async function deleteUserStorageData(
|
|
userId: string,
|
|
namespace: string | null,
|
|
): Promise<void> {
|
|
const s3Enabled = isS3Configured();
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const database = db as any;
|
|
|
|
// --- Documents & previews ---
|
|
const userDocs: DocumentRow[] = await database
|
|
.select({ id: documents.id })
|
|
.from(documents)
|
|
.where(eq(documents.userId, userId));
|
|
|
|
let docsDeleted = 0;
|
|
for (const doc of userDocs) {
|
|
if (s3Enabled) {
|
|
try {
|
|
await deleteDocumentBlob(doc.id, namespace);
|
|
docsDeleted++;
|
|
} catch (error) {
|
|
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete document blob ${doc.id}:`);
|
|
}
|
|
|
|
try {
|
|
await deleteDocumentPreviewArtifacts(doc.id, namespace);
|
|
} catch (error) {
|
|
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete preview for ${doc.id}:`);
|
|
}
|
|
}
|
|
|
|
// Always clean up DB rows — documentPreviews has no FK cascade on user.
|
|
try {
|
|
await deleteDocumentPreviewRows(doc.id, namespace);
|
|
} catch (error) {
|
|
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete preview rows for ${doc.id}:`);
|
|
}
|
|
}
|
|
|
|
// --- Audiobooks ---
|
|
const userBooks: AudiobookRow[] = s3Enabled
|
|
? await database
|
|
.select({ id: audiobooks.id })
|
|
.from(audiobooks)
|
|
.where(eq(audiobooks.userId, userId))
|
|
: [];
|
|
|
|
let booksDeleted = 0;
|
|
for (const book of userBooks) {
|
|
try {
|
|
const prefix = audiobookPrefix(book.id, userId, namespace);
|
|
await deleteAudiobookPrefix(prefix);
|
|
booksDeleted++;
|
|
} catch (error) {
|
|
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete audiobook blobs ${book.id}:`);
|
|
}
|
|
}
|
|
|
|
// --- TTS segments ---
|
|
let segmentsDeleted = 0;
|
|
if (s3Enabled) {
|
|
try {
|
|
const cfg = getS3Config();
|
|
const nsSegment = namespace ? `ns/${namespace}/` : '';
|
|
const ttsPrefixV1 = `${cfg.prefix}/tts_segments_v1/${nsSegment}users/${encodeURIComponent(userId)}/`;
|
|
const ttsPrefixV2 = `${cfg.prefix}/tts_segments_v2/${nsSegment}users/${encodeURIComponent(userId)}/`;
|
|
segmentsDeleted += await deleteTtsSegmentPrefix(ttsPrefixV1);
|
|
segmentsDeleted += await deleteTtsSegmentPrefix(ttsPrefixV2);
|
|
} catch (error) {
|
|
serverLogger.error({ err: error }, `[user-data-cleanup] Failed to delete TTS segment blobs for user ${userId}:`);
|
|
}
|
|
}
|
|
|
|
if (docsDeleted > 0 || booksDeleted > 0 || segmentsDeleted > 0) {
|
|
serverLogger.info(
|
|
`[user-data-cleanup] Cleaned up S3 data for user ${userId}: ` +
|
|
`${docsDeleted}/${userDocs.length} document(s), ` +
|
|
`${booksDeleted}/${userBooks.length} audiobook(s), ` +
|
|
`${segmentsDeleted} tts segment object(s)`,
|
|
);
|
|
}
|
|
}
|