Implement secure file uploads in Google Antigravity with Supabase Storage and presigned URLs.
# File Upload Patterns for Google Antigravity
Implement secure file uploads with Supabase Storage, validation, and processing.
## Upload Component
```typescript
// components/FileUploader.tsx
"use client";
import { useState, useRef, useCallback } from "react";
import { createClient } from "@/lib/supabase/client";
interface FileUploaderProps {
bucket: string;
onUpload: (url: string) => void;
accept?: string;
maxSizeMB?: number;
}
export function FileUploader({ bucket, onUpload, accept = "image/*", maxSizeMB = 5 }: FileUploaderProps) {
const [uploading, setUploading] = useState(false);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const supabase = createClient();
const handleUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setError(null);
if (file.size > maxSizeMB * 1024 * 1024) {
setError(`File too large. Max size: ${maxSizeMB}MB`);
return;
}
setUploading(true);
setProgress(0);
try {
const ext = file.name.split(".").pop();
const path = `${Date.now()}-${Math.random().toString(36).substring(7)}.${ext}`;
const { error: uploadError } = await supabase.storage.from(bucket).upload(path, file, {
cacheControl: "3600",
upsert: false,
});
if (uploadError) throw uploadError;
const { data: { publicUrl } } = supabase.storage.from(bucket).getPublicUrl(path);
onUpload(publicUrl);
setProgress(100);
} catch (err) {
setError(err instanceof Error ? err.message : "Upload failed");
} finally {
setUploading(false);
}
}, [bucket, maxSizeMB, onUpload, supabase]);
return (
<div className="file-uploader">
<input ref={inputRef} type="file" accept={accept} onChange={handleUpload} disabled={uploading} className="hidden" />
<button onClick={() => inputRef.current?.click()} disabled={uploading}>
{uploading ? `Uploading... ${progress}%` : "Select File"}
</button>
{error && <p className="error">{error}</p>}
</div>
);
}
```
## Server-Side Upload with Validation
```typescript
// app/api/upload/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
import { v4 as uuidv4 } from "uuid";
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
export async function POST(request: NextRequest) {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) return NextResponse.json({ error: "No file" }, { status: 400 });
if (!ALLOWED_TYPES.includes(file.type)) {
return NextResponse.json({ error: "Invalid file type" }, { status: 400 });
}
if (file.size > MAX_SIZE) {
return NextResponse.json({ error: "File too large" }, { status: 400 });
}
const ext = file.name.split(".").pop();
const path = `${user.id}/${uuidv4()}.${ext}`;
const { error } = await supabase.storage.from("uploads").upload(path, file, { contentType: file.type });
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
const { data: { publicUrl } } = supabase.storage.from("uploads").getPublicUrl(path);
return NextResponse.json({ url: publicUrl });
}
```
## Presigned Upload URLs
```typescript
// app/api/upload/presigned/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createClient } from "@/lib/supabase/server";
import { v4 as uuidv4 } from "uuid";
export async function POST(request: NextRequest) {
const supabase = createClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
const { filename, contentType } = await request.json();
const ext = filename.split(".").pop();
const path = `${user.id}/${uuidv4()}.${ext}`;
const { data, error } = await supabase.storage.from("uploads").createSignedUploadUrl(path);
if (error) return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ uploadUrl: data.signedUrl, path });
}
```
## Image Processing
```typescript
// lib/image-processing.ts
import sharp from "sharp";
export async function processImage(buffer: Buffer, options: { maxWidth?: number; maxHeight?: number; quality?: number }): Promise<Buffer> {
let image = sharp(buffer);
const metadata = await image.metadata();
if (options.maxWidth || options.maxHeight) {
image = image.resize(options.maxWidth, options.maxHeight, { fit: "inside", withoutEnlargement: true });
}
return image.webp({ quality: options.quality || 80 }).toBuffer();
}
export async function generateThumbnail(buffer: Buffer, size = 200): Promise<Buffer> {
return sharp(buffer).resize(size, size, { fit: "cover" }).webp({ quality: 70 }).toBuffer();
}
```
## Drag and Drop Upload
```typescript
// components/DropZone.tsx
"use client";
import { useState, useCallback, DragEvent } from "react";
export function DropZone({ onFiles }: { onFiles: (files: File[]) => void }) {
const [dragOver, setDragOver] = useState(false);
const handleDrop = useCallback((e: DragEvent) => {
e.preventDefault();
setDragOver(false);
const files = Array.from(e.dataTransfer.files);
onFiles(files);
}, [onFiles]);
const handleDragOver = useCallback((e: DragEvent) => {
e.preventDefault();
setDragOver(true);
}, []);
const handleDragLeave = useCallback(() => setDragOver(false), []);
return (
<div className={`dropzone ${dragOver ? "active" : ""}`} onDrop={handleDrop} onDragOver={handleDragOver} onDragLeave={handleDragLeave}>
<p>Drag files here or click to upload</p>
</div>
);
}
```
## Best Practices
1. **Validation**: Validate file type and size server-side
2. **Naming**: Use unique names to prevent conflicts
3. **Presigned URLs**: Use for direct uploads to reduce server load
4. **Processing**: Process images to optimize size and format
5. **Cleanup**: Implement cleanup for orphaned filesThis file-upload prompt is ideal for developers working on:
By using this prompt, you can save hours of manual coding and ensure best practices are followed from the start. It's particularly valuable for teams looking to maintain consistency across their file-upload implementations.
Yes! All prompts on Antigravity AI Directory are free to use for both personal and commercial projects. No attribution required, though it's always appreciated.
This prompt works excellently with Claude, ChatGPT, Cursor, GitHub Copilot, and other modern AI coding assistants. For best results, use models with large context windows.
You can modify the prompt by adding specific requirements, constraints, or preferences. For file-upload projects, consider mentioning your framework version, coding style, and any specific libraries you're using.