Spaces:
Sleeping
Sleeping
| 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<string> { | |
| 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<void> { | |
| 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<void> { | |
| 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<string> { | |
| 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<string | null> { | |
| 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<string> { | |
| 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<string, string> = { | |
| '.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'; | |
| } | |