| import * as fs from 'fs'; |
| import * as path from 'path'; |
| import { supabase } from './client'; |
| import { config } from '../config'; |
| import { logger } from '../utils/logger'; |
| import { |
| createR2SignedDownloadUrl, |
| deleteR2Object, |
| downloadR2File, |
| isR2ObjectRef, |
| putR2Buffer, |
| putR2File, |
| readR2Text, |
| } from './r2'; |
|
|
| |
| |
| |
| |
| 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}`; |
|
|
| if (config.storageProvider === 'r2') { |
| try { |
| return await putR2Buffer( |
| config.inputBucket, |
| storagePath, |
| fileBuffer, |
| getMimeType(ext), |
| ); |
| } catch (error) { |
| logger.error('Failed to upload input file to R2', { |
| storagePath, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| const { error } = await supabase.storage |
| .from(config.supabaseInputBucket) |
| .upload(storagePath, fileBuffer, { |
| contentType: getMimeType(ext), |
| upsert, |
| }); |
|
|
| if (error) { |
| logger.error('Failed to upload input file', { storagePath, error: error.message }); |
| throw error; |
| } |
|
|
| return storagePath; |
| } |
|
|
| |
| |
| |
| |
| export async function uploadInputFileFromPath( |
| userId: string, |
| storageKey: string, |
| fileName: string, |
| localPath: string, |
| upsert = false, |
| ): Promise<string> { |
| const ext = path.extname(fileName); |
| const storagePath = `${userId}/${storageKey}/input${ext}`; |
|
|
| if (config.storageProvider === 'r2') { |
| try { |
| return await putR2File( |
| config.inputBucket, |
| storagePath, |
| localPath, |
| getMimeType(ext), |
| ); |
| } catch (error) { |
| logger.error('Failed to upload input file to R2', { |
| storagePath, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| |
| |
| const fileBuffer = await fs.promises.readFile(localPath); |
| const { error } = await supabase.storage |
| .from(config.supabaseInputBucket) |
| .upload(storagePath, fileBuffer, { |
| contentType: getMimeType(ext), |
| upsert, |
| }); |
|
|
| if (error) { |
| logger.error('Failed to upload input file', { storagePath, error: error.message }); |
| throw error; |
| } |
|
|
| return storagePath; |
| } |
|
|
| |
| |
| |
| export async function downloadInputFile(storagePath: string, localPath: string): Promise<void> { |
| if (isR2ObjectRef(storagePath)) { |
| try { |
| await downloadR2File(storagePath, localPath); |
| return; |
| } catch (error) { |
| logger.error('Failed to download input file from R2', { |
| storagePath, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| const { data, error } = await supabase.storage |
| .from(config.supabaseInputBucket) |
| .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); |
| } |
|
|
| |
| export async function deleteInputFile(storagePath: string): Promise<void> { |
| if (isR2ObjectRef(storagePath)) { |
| await deleteR2Object(storagePath); |
| return; |
| } |
|
|
| const { error } = await supabase.storage |
| .from(config.supabaseInputBucket) |
| .remove([storagePath]); |
| if (error) { |
| logger.error('Failed to delete staged input file', { |
| storagePath, |
| error: error.message, |
| }); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function uploadReportPdf( |
| userId: string, |
| jobId: string, |
| localPdfPath: string, |
| ): Promise<{ storagePath: string; expiresAt: string }> { |
| const storagePath = `${userId}/${jobId}/report.pdf`; |
|
|
| if (config.storageProvider === 'r2') { |
| try { |
| const objectRef = await putR2File( |
| config.reportBucket, |
| storagePath, |
| localPdfPath, |
| 'application/pdf', |
| ); |
| return { |
| storagePath: objectRef, |
| expiresAt: createReportExpiry(), |
| }; |
| } catch (error) { |
| logger.error('Failed to upload report PDF to R2', { |
| storagePath, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| const fileBuffer = fs.readFileSync(localPdfPath); |
|
|
| const { error } = await supabase.storage |
| .from(config.supabaseReportBucket) |
| .upload(storagePath, fileBuffer, { |
| contentType: 'application/pdf', |
| upsert: true, |
| }); |
|
|
| if (error) { |
| logger.error('Failed to upload report PDF', { storagePath, error: error.message }); |
| throw error; |
| } |
|
|
| return { storagePath, expiresAt: createReportExpiry() }; |
| } |
|
|
| |
| |
| |
| |
| export async function uploadReceiptPdf( |
| userId: string, |
| jobId: string, |
| localPdfPath: string, |
| ): Promise<{ storagePath: string; expiresAt: string }> { |
| const storagePath = `${userId}/${jobId}/receipt.pdf`; |
|
|
| if (config.storageProvider === 'r2') { |
| try { |
| const objectRef = await putR2File( |
| config.reportBucket, |
| storagePath, |
| localPdfPath, |
| 'application/pdf', |
| ); |
| return { |
| storagePath: objectRef, |
| expiresAt: createReportExpiry(), |
| }; |
| } catch (error) { |
| logger.error('Failed to upload Digital Receipt PDF to R2', { |
| storagePath, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| const fileBuffer = fs.readFileSync(localPdfPath); |
|
|
| const { error } = await supabase.storage |
| .from(config.supabaseReportBucket) |
| .upload(storagePath, fileBuffer, { |
| contentType: 'application/pdf', |
| upsert: true, |
| }); |
|
|
| if (error) { |
| logger.error('Failed to upload Digital Receipt PDF', { |
| storagePath, |
| error: error.message, |
| }); |
| throw error; |
| } |
|
|
| return { storagePath, expiresAt: createReportExpiry() }; |
| } |
|
|
| |
| |
| |
| export async function deleteReportPdf(storagePath: string): Promise<void> { |
| if (isR2ObjectRef(storagePath)) { |
| try { |
| await deleteR2Object(storagePath); |
| return; |
| } catch (error) { |
| logger.error('Failed to delete report PDF from R2', { |
| storagePath, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| const { error } = await supabase.storage |
| .from(config.supabaseReportBucket) |
| .remove([storagePath]); |
|
|
| if (error) { |
| logger.error('Failed to delete report PDF', { storagePath, error: error.message }); |
| throw error; |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function uploadStorageState( |
| accountId: string, |
| stateJson: string, |
| ): Promise<string> { |
| const storagePath = `${accountId}/state.json`; |
|
|
| if (config.storageProvider === 'r2') { |
| try { |
| return await putR2Buffer( |
| config.sessionBucket, |
| storagePath, |
| Buffer.from(stateJson, 'utf-8'), |
| 'application/json', |
| ); |
| } catch (error) { |
| logger.error('Failed to upload storage state to R2', { |
| accountId, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| const { error } = await supabase.storage |
| .from(config.supabaseSessionBucket) |
| .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; |
| } |
|
|
| |
| |
| |
| |
| export async function downloadStorageState(storagePath: string): Promise<string | null> { |
| if (isR2ObjectRef(storagePath)) { |
| try { |
| return await readR2Text(storagePath); |
| } catch (error) { |
| logger.error('Failed to download storage state from R2', { |
| storagePath, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| const { data, error } = await supabase.storage |
| .from(config.supabaseSessionBucket) |
| .download(storagePath); |
|
|
| if (error) { |
| |
| 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(); |
| } |
|
|
| |
| |
| |
| export async function createSignedUrl( |
| bucket: string, |
| filePath: string, |
| expiresInSeconds: number, |
| downloadFileName?: string, |
| ): Promise<string> { |
| if (isR2ObjectRef(filePath)) { |
| try { |
| return await createR2SignedDownloadUrl( |
| filePath, |
| expiresInSeconds, |
| downloadFileName, |
| ); |
| } catch (error) { |
| logger.error('Failed to create R2 signed URL', { |
| bucket, |
| filePath, |
| error: error instanceof Error ? error.message : String(error), |
| }); |
| throw error; |
| } |
| } |
|
|
| const legacyBucket = |
| bucket === config.reportBucket |
| ? config.supabaseReportBucket |
| : bucket === config.inputBucket |
| ? config.supabaseInputBucket |
| : bucket === config.sessionBucket |
| ? config.supabaseSessionBucket |
| : bucket; |
| const { data, error } = await supabase.storage |
| .from(legacyBucket) |
| .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; |
| } |
|
|
| function createReportExpiry(): string { |
| return new Date( |
| Date.now() + config.reportRetentionHours * 60 * 60 * 1000, |
| ).toISOString(); |
| } |
|
|
| |
| 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'; |
| } |
|
|