Spaces:
Sleeping
Sleeping
| /** services/storageService.js V2.0 - GCP Direct Bucket Upload */ | |
| const { Storage } = require('@google-cloud/storage'); | |
| const path = require('path'); | |
| // Initialize GCP Storage using credentials parsed directly from ENV | |
| let storage; | |
| try { | |
| const creds = process.env.GCP_SERVICE_ACCOUNT_KEY; | |
| if (!creds) throw new Error("GCP_SERVICE_ACCOUNT_KEY is missing from environment"); | |
| // ULTRA-ROBUST FIX: Handle missing quotes, double escaping, and literal newlines. | |
| let cleanCreds = creds.trim(); | |
| if (cleanCreds.startsWith("'") && cleanCreds.endsWith("'")) cleanCreds = cleanCreds.slice(1, -1); | |
| if (cleanCreds.startsWith('"') && cleanCreds.endsWith('"')) cleanCreds = cleanCreds.slice(1, -1); | |
| // Replace REAL newlines with escaped ones for JSON.parse, then fix double-escaping of backslashes if present | |
| cleanCreds = cleanCreds.replace(/\r?\n/g, '\\n').replace(/\\\\n/g, '\\n'); | |
| let credentials; | |
| let tryContent = cleanCreds; | |
| // --- STEP 1: Aggressive Unquoting --- | |
| // Handle cases where the key is wrapped in multiple layers of escaped quotes | |
| for (let i = 0; i < 3; i++) { | |
| tryContent = tryContent.trim(); | |
| if (tryContent.startsWith('\\"') && tryContent.endsWith('\\"')) { | |
| tryContent = tryContent.slice(2, -2); | |
| } else if (tryContent.startsWith('"') && tryContent.endsWith('"')) { | |
| tryContent = tryContent.slice(1, -1); | |
| } else if (tryContent.startsWith("'") && tryContent.endsWith("'")) { | |
| tryContent = tryContent.slice(1, -1); | |
| } else { | |
| break; | |
| } | |
| } | |
| // --- STEP 2: Multi-Stage Parsing --- | |
| try { | |
| credentials = JSON.parse(tryContent); | |
| } catch (parseError) { | |
| console.warn(`[STORAGE] JSON.parse failed (${parseError.message}), using ULTRA SURGICAL extraction...`); | |
| // --- STEP 3: ULTRA SURGICAL Extraction --- | |
| // Pluck fields using greedy matching to ignore escaping mess | |
| const pluck = (key) => { | |
| const regex = new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`); | |
| const match = tryContent.match(regex); | |
| return match ? match[1] : null; | |
| }; | |
| let project_id = pluck('project_id'); | |
| let client_email = pluck('client_email'); | |
| // Private Key is special: It's long and full of \n | |
| // We look for everything between the known PEM markers | |
| const pkRegex = /-----BEGIN PRIVATE KEY-----[\s\S]+?-----END PRIVATE KEY-----/; | |
| let rawKeyMatch = tryContent.match(pkRegex); | |
| let private_key; | |
| if (rawKeyMatch) { | |
| private_key = rawKeyMatch[0] | |
| .replace(/\\n/g, '\n') | |
| .replace(/\\\\n/g, '\n') | |
| .replace(/\\r/g, '') | |
| .replace(/\\/g, ''); // Clear residual shell slashes | |
| } else { | |
| // Fallback for non-headered key string | |
| private_key = pluck('private_key'); | |
| if (private_key) { | |
| private_key = `-----BEGIN PRIVATE KEY-----\n${private_key.replace(/\\n/g, '\n').replace(/\\\\n/g, '\n')}\n-----END PRIVATE KEY-----\n`; | |
| } | |
| } | |
| if (private_key && client_email && project_id) { | |
| credentials = { | |
| type: 'service_account', | |
| project_id, | |
| client_email, | |
| private_key: private_key.trim() + '\n' | |
| }; | |
| console.log(`[STORAGE] SUCCESS via ULTRA SURGICAL extraction for ${project_id}`); | |
| } else { | |
| console.error(`🛑 [STORAGE] ULTRA SURGICAL FAILED.`, { | |
| pid: !!project_id, | |
| ce: !!client_email, | |
| pk: !!private_key | |
| }); | |
| throw new Error("Unable to reconstruct GCP credentials from corrupted environment string."); | |
| } | |
| } | |
| storage = new Storage({ | |
| credentials, | |
| projectId: credentials.project_id || 'pini-print-bot' | |
| }); | |
| console.log(`[STORAGE] SUCCESSFULLY initialized GCP client for ${credentials.project_id}`); | |
| } catch (e) { | |
| console.error("🛑 [STORAGE] CRITICAL INITIALIZATION ERROR:", e.message); | |
| storage = new Storage(); | |
| } | |
| const BUCKET_NAME = process.env.GCP_STORAGE_BUCKET || 'pini-print-uploads'; | |
| /** | |
| * Generates a signed URL for direct client upload to GCP. | |
| * Strictly limited to 5 minutes (300 seconds). | |
| * Files are path-isolated by session ID. | |
| */ | |
| async function generateSignedUploadUrl(sessionId, fileName) { | |
| if (!sessionId) throw new Error("SessionID required for storage isolation."); | |
| // Naming Policy: /uploads/sessions/{sessionId}/{fileName} | |
| const destination = `uploads/sessions/${sessionId}/${Date.now()}_${fileName}`; | |
| const file = storage.bucket(BUCKET_NAME).file(destination); | |
| const options = { | |
| version: 'v4', | |
| action: 'write', | |
| expires: Date.now() + 5 * 60 * 1000, // 5 minutes TTL | |
| contentType: 'application/octet-stream', | |
| }; | |
| try { | |
| const [url] = await file.getSignedUrl(options); | |
| console.log(`[STORAGE] Generated Signed URL (Expires 5m): ${destination}`); | |
| return { | |
| uploadUrl: url, | |
| fileName: fileName, | |
| remotePath: destination | |
| }; | |
| } catch (error) { | |
| console.error("Error generating signed URL:", error); | |
| throw error; | |
| } | |
| } | |
| /** | |
| * Downloads a file from GCP bucket as a Buffer. | |
| */ | |
| async function downloadFile(remotePath) { | |
| try { | |
| const file = storage.bucket(BUCKET_NAME).file(remotePath); | |
| const [buffer] = await file.download(); | |
| console.log(`[STORAGE] Downloaded file buffer: ${remotePath}`); | |
| return buffer; | |
| } catch (error) { | |
| console.error("Error downloading file from GCP:", error); | |
| throw error; | |
| } | |
| } | |
| module.exports = { | |
| generateSignedUploadUrl, | |
| downloadFile | |
| }; | |