Download src/services/supabaseService.ts from Collabos/chatapi: direct link, hf CLI and curl.
- Browser
- Download file 3.66 kB
-
https://huggingface.co/spaces/Collabos/chatapi/resolve/main/src/services/supabaseService.ts
- Command line
-
hf download hf://spaces/Collabos/chatapi/src/services/supabaseService.ts
-
curl -L -o supabaseService.ts https://huggingface.co/spaces/Collabos/chatapi/resolve/main/src/services/supabaseService.ts
3.66 kB
| import { createClient, SupabaseClient } from '@supabase/supabase-js'; | |
| import { v4 as uuidv4 } from 'uuid'; | |
| import { CONFIG } from '../config'; | |
| import { logger } from '../utils/logger'; | |
| // ββ Singleton client ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let _client: SupabaseClient | null = null; | |
| function getClient(): SupabaseClient { | |
| if (!_client) { | |
| _client = createClient(CONFIG.SUPABASE_URL, CONFIG.SUPABASE_SERVICE_KEY, { | |
| auth: { persistSession: false }, | |
| }); | |
| } | |
| return _client; | |
| } | |
| // ββ Allowed MIME types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const ALLOWED_MIME: Set<string> = new Set([ | |
| 'image/jpeg', | |
| 'image/png', | |
| 'image/gif', | |
| 'image/webp', | |
| 'video/mp4', | |
| 'video/webm', | |
| 'audio/mpeg', | |
| 'audio/ogg', | |
| 'audio/webm', | |
| 'application/pdf', | |
| ]); | |
| // ββ File validation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| export interface FileUploadRequest { | |
| fileName: string; | |
| mimeType: string; | |
| sizeBytes: number; | |
| } | |
| export interface UploadUrlResult { | |
| signedUrl: string; | |
| objectPath: string; | |
| publicUrl: string; | |
| } | |
| export function validateFileRequest(req: FileUploadRequest): string | null { | |
| if (!ALLOWED_MIME.has(req.mimeType)) { | |
| return `MIME type ${req.mimeType} is not allowed`; | |
| } | |
| if (req.sizeBytes > CONFIG.MAX_FILE_SIZE_BYTES) { | |
| return `File exceeds ${CONFIG.MAX_FILE_SIZE_BYTES / 1024 / 1024} MB limit`; | |
| } | |
| // Strip path traversal from filename | |
| const safeName = req.fileName.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 128); | |
| if (!safeName) return 'Invalid file name'; | |
| return null; | |
| } | |
| export function validateFileCount(count: number): string | null { | |
| if (count > CONFIG.MAX_FILE_COUNT) { | |
| return `Cannot upload more than ${CONFIG.MAX_FILE_COUNT} files at once`; | |
| } | |
| if (count < 1) return 'File count must be at least 1'; | |
| return null; | |
| } | |
| // ββ Signed upload URL generation βββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Generates a Supabase signed upload URL. | |
| * The client uses this to PUT the file directly to Supabase β HF server | |
| * never buffers any bytes, keeping memory usage at ~0 for file transfers. | |
| * | |
| * URL expires in 60 seconds β client must begin upload immediately. | |
| */ | |
| export async function generateUploadUrl( | |
| uploaderUid: string, | |
| req: FileUploadRequest | |
| ): Promise<UploadUrlResult> { | |
| const supabase = getClient(); | |
| // Sanitize file name | |
| const safeName = req.fileName.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 128); | |
| const ext = safeName.split('.').pop() ?? 'bin'; | |
| // Unique path per user prevents collisions and leakage | |
| const objectPath = `users/${uploaderUid}/${uuidv4()}.${ext}`; | |
| const { data, error } = await supabase.storage | |
| .from(CONFIG.SUPABASE_BUCKET) | |
| .createSignedUploadUrl(objectPath); | |
| if (error || !data) { | |
| logger.error('Supabase signed URL failed', { error: error?.message }); | |
| throw new Error('Could not generate upload URL'); | |
| } | |
| // Derive the public URL (bucket must be public, or use signed read URL) | |
| const { data: urlData } = supabase.storage | |
| .from(CONFIG.SUPABASE_BUCKET) | |
| .getPublicUrl(objectPath); | |
| return { | |
| signedUrl: data.signedUrl, | |
| objectPath, | |
| publicUrl: urlData.publicUrl, | |
| }; | |
| } | |