import * as fs from 'fs'; import * as path from 'path'; import { supabase } from './client'; import { config } from '../config'; import { logger } from '../utils/logger'; /** * Upload a user's input file to the turnitin-inputs bucket. * Returns the storage path within the bucket. */ export async function uploadInputFile( userId: string, storageKey: string, fileName: string, fileBuffer: Buffer, upsert = false, ): Promise { const ext = path.extname(fileName); const storagePath = `${userId}/${storageKey}/input${ext}`; const { error } = await supabase.storage .from(config.inputBucket) .upload(storagePath, fileBuffer, { contentType: getMimeType(ext), upsert, }); if (error) { logger.error('Failed to upload input file', { storagePath, error: error.message }); throw error; } return storagePath; } /** * Download a file from Supabase Storage to a local path. */ export async function downloadInputFile(storagePath: string, localPath: string): Promise { const { data, error } = await supabase.storage .from(config.inputBucket) .download(storagePath); if (error) { logger.error('Failed to download input file', { storagePath, error: error.message }); throw error; } const buffer = Buffer.from(await data.arrayBuffer()); const dir = path.dirname(localPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(localPath, buffer); } /** * Upload a generated report PDF to the turnitin-reports bucket. * Returns the storage path and expiry timestamp. */ export async function uploadReportPdf( userId: string, jobId: string, localPdfPath: string, ): Promise<{ storagePath: string; expiresAt: string }> { const storagePath = `${userId}/${jobId}/report.pdf`; const fileBuffer = fs.readFileSync(localPdfPath); const { error } = await supabase.storage .from(config.reportBucket) .upload(storagePath, fileBuffer, { contentType: 'application/pdf', upsert: true, }); if (error) { logger.error('Failed to upload report PDF', { storagePath, error: error.message }); throw error; } const expiresAt = new Date( Date.now() + config.reportRetentionHours * 60 * 60 * 1000, ).toISOString(); return { storagePath, expiresAt }; } /** * Upload a legacy Turnitin Digital Receipt PDF with the same retention policy * as the similarity report. */ export async function uploadReceiptPdf( userId: string, jobId: string, localPdfPath: string, ): Promise<{ storagePath: string; expiresAt: string }> { const storagePath = `${userId}/${jobId}/receipt.pdf`; const fileBuffer = fs.readFileSync(localPdfPath); const { error } = await supabase.storage .from(config.reportBucket) .upload(storagePath, fileBuffer, { contentType: 'application/pdf', upsert: true, }); if (error) { logger.error('Failed to upload Digital Receipt PDF', { storagePath, error: error.message, }); throw error; } const expiresAt = new Date( Date.now() + config.reportRetentionHours * 60 * 60 * 1000, ).toISOString(); return { storagePath, expiresAt }; } /** * Delete a report PDF from Supabase Storage. */ export async function deleteReportPdf(storagePath: string): Promise { const { error } = await supabase.storage .from(config.reportBucket) .remove([storagePath]); if (error) { logger.error('Failed to delete report PDF', { storagePath, error: error.message }); throw error; } } /** * Upload a Playwright browser storage state to the sessions bucket. * Returns the storage path. */ export async function uploadStorageState( accountId: string, stateJson: string, ): Promise { const storagePath = `${accountId}/state.json`; const { error } = await supabase.storage .from(config.sessionBucket) .upload(storagePath, Buffer.from(stateJson, 'utf-8'), { contentType: 'application/json', upsert: true, }); if (error) { logger.error('Failed to upload storage state', { accountId, error: error.message }); throw error; } return storagePath; } /** * Download a previously saved storage state. * Returns the JSON string, or null if not found. */ export async function downloadStorageState(storagePath: string): Promise { const { data, error } = await supabase.storage .from(config.sessionBucket) .download(storagePath); if (error) { // Not found is not fatal — the account may not have a saved session if (error.message?.includes('not found') || error.message?.includes('Object not found')) { return null; } logger.error('Failed to download storage state', { storagePath, error: error.message }); throw error; } return await data.text(); } /** * Create a time-limited signed URL for a file in any bucket. */ export async function createSignedUrl( bucket: string, filePath: string, expiresInSeconds: number, downloadFileName?: string, ): Promise { const { data, error } = await supabase.storage .from(bucket) .createSignedUrl( filePath, expiresInSeconds, downloadFileName ? { download: downloadFileName } : undefined, ); if (error) { logger.error('Failed to create signed URL', { bucket, filePath, error: error.message }); throw error; } return data.signedUrl; } /** Map file extensions to MIME types */ function getMimeType(ext: string): string { const mimeTypes: Record = { '.pdf': 'application/pdf', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', '.ps': 'application/postscript', '.html': 'text/html', '.txt': 'text/plain', '.rtf': 'application/rtf', '.odt': 'application/vnd.oasis.opendocument.text', '.hwp': 'application/x-hwp', }; return mimeTypes[ext.toLowerCase()] || 'application/octet-stream'; }