import { Readable } from "node:stream"; import type { CaseFile } from "@prisma/client"; import { MINIO_BUCKET_NAME, MINIO_PUBLIC_BASE_URL, } from "@/lib/constants"; import { assertCaseFileWithinSizeLimit, getSafeMimeType, isAllowedCaseFileType } from "@/lib/file-utils"; import { trimOrUndefined } from "@/lib/utils"; import { requireCaseByIdForOwner } from "@/server/cases/service"; import { requireCustomerByIdForOwner } from "@/server/customers/service"; import { db } from "@/server/db"; import { AppError } from "@/server/errors"; import { buildCaseFileStorageKey, ensureMinioBucket, minioClient } from "@/server/storage/minio"; import type { CaseFileListQueryInput, CaseFileMetadataInput, } from "@/server/validation/case"; import type { CaseFileRagStatusValue } from "@/lib/constants"; export function normalizeCaseFileListFilters(input: CaseFileListQueryInput) { return { search: trimOrUndefined(input.fileSearch), selectedFileId: trimOrUndefined(input.fileId), sessionPreset: input.sessionPreset, }; } export async function listCaseFilesForOwner( ownerId: string, customerId: string, caseId: string, search?: string, ) { await requireCaseByIdForOwner(ownerId, customerId, caseId); return db.caseFile.findMany({ where: { ownerId, customerId, caseId, ...(search ? { displayName: { contains: search, mode: "insensitive", }, } : {}), }, orderBy: { createdAt: "desc", }, }); } export async function listCustomerArchiveFilesForOwner( ownerId: string, customerId: string, search?: string, ) { await requireCustomerByIdForOwner(customerId, ownerId); return db.caseFile.findMany({ where: { ownerId, customerId, ...(search ? { displayName: { contains: search, mode: "insensitive", }, } : {}), }, include: { case: true, }, orderBy: { createdAt: "desc", }, }); } export async function getCaseFileByIdForOwner( ownerId: string, customerId: string, caseId: string, fileId: string, ) { return db.caseFile.findFirst({ where: { id: fileId, ownerId, customerId, caseId, }, include: { case: { include: { customer: true, }, }, }, }); } export async function requireCaseFileByIdForOwner( ownerId: string, customerId: string, caseId: string, fileId: string, ) { const file = await getCaseFileByIdForOwner(ownerId, customerId, caseId, fileId); if (!file) { throw new AppError("الملف ده مش موجود.", 404); } return file; } export async function uploadCaseFileForOwner( ownerId: string, customerId: string, caseId: string, metadata: CaseFileMetadataInput, uploadedFile: File, ) { await requireCaseByIdForOwner(ownerId, customerId, caseId); if (!uploadedFile || uploadedFile.size <= 0) { throw new AppError("اختار ملف قبل الرفع.", 400); } if (!assertCaseFileWithinSizeLimit(uploadedFile.size)) { throw new AppError("حجم الملف أكبر من الحد المسموح 25 ميجابايت.", 400); } const mimeType = getSafeMimeType(uploadedFile.name, uploadedFile.type); if (!isAllowedCaseFileType(uploadedFile.name, mimeType)) { throw new AppError("نوع الملف ده غير مدعوم في النسخة الحالية.", 400); } const arrayBuffer = await uploadedFile.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); const storageKey = buildCaseFileStorageKey(caseId, uploadedFile.name); const ragStatus: CaseFileRagStatusValue = mimeType === "application/pdf" ? "PROCESSING" : "SKIPPED"; await ensureMinioBucket(); await minioClient.putObject(MINIO_BUCKET_NAME, storageKey, buffer, buffer.length, { "Content-Type": mimeType, }); return db.caseFile.create({ data: { ownerId, customerId, caseId, displayName: metadata.displayName.trim(), description: trimOrUndefined(metadata.description), originalFileName: uploadedFile.name, mimeType, sizeBytes: uploadedFile.size, storageBucket: MINIO_BUCKET_NAME, storageKey, ragStatus, }, }); } export async function updateCaseFileRagStatus( fileId: string, status: CaseFileRagStatusValue, options?: { error?: string | null; syncedAt?: Date | null; }, ) { return db.caseFile.update({ where: { id: fileId, }, data: { ragStatus: status, ragError: options?.error ?? null, ragSyncedAt: options?.syncedAt ?? null, }, }); } export async function listReadyPdfFilesForCase( ownerId: string, customerId: string, caseId: string, ) { await requireCaseByIdForOwner(ownerId, customerId, caseId); return db.caseFile.findMany({ where: { ownerId, customerId, caseId, mimeType: "application/pdf", ragStatus: "READY", }, orderBy: { createdAt: "desc", }, }); } export async function getCaseFileContentStream(file: CaseFile) { const stream = await minioClient.getObject(file.storageBucket, file.storageKey); return Readable.toWeb(stream) as ReadableStream; } export function buildCaseFileContentUrl(params: { customerId: string; caseId: string; fileId: string; download?: boolean; }) { const basePath = `/api/customers/${params.customerId}/cases/${params.caseId}/files/${params.fileId}/content`; return params.download ? `${basePath}?download=1` : basePath; } export function buildCaseFilePreviewUrl(params: { customerId: string; caseId: string; fileId: string; }) { const path = buildCaseFileContentUrl(params); if (!MINIO_PUBLIC_BASE_URL) { return path; } const normalizedBase = MINIO_PUBLIC_BASE_URL.replace(/\/$/, ""); return `${normalizedBase}${path}`; } export function buildInlineFileName(file: Pick) { const extension = file.originalFileName.includes(".") ? file.originalFileName.slice(file.originalFileName.lastIndexOf(".")) : ""; if (extension && file.displayName.toLowerCase().endsWith(extension.toLowerCase())) { return file.displayName; } return `${file.displayName}${extension}`; }