openreader/src/lib/server/user/data-cleanup.ts
Richard R 59d0101c0c feat(server): implement unified structured logging with pino and enforce usage
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.
2026-05-27 04:20:17 -06:00

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)`,
);
}
}