relv-turnitin-backup / src /db /storage.ts
RelVDev2's picture
Deploy pre-receipt backup worker
d5c9d40
Raw
History Blame Contribute Delete
6.12 kB
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';
}