Refactor: Use Direct Creator Upload

This commit is contained in:
gpt-engineer-app[bot]
2025-09-20 13:21:25 +00:00
parent 2c05925724
commit 8dba5a78d1
2 changed files with 158 additions and 103 deletions

View File

@@ -72,33 +72,75 @@ export function PhotoUpload({
};
const uploadFile = async (file: File): Promise<UploadedImage> => {
const formData = new FormData();
formData.append('file', file);
formData.append('metadata', JSON.stringify({
// Step 1: Get direct upload URL from our edge function
const { data: uploadData, error: uploadError } = await supabase.functions.invoke('upload-image', {
body: {
metadata: {
filename: file.name,
size: file.size,
type: file.type,
uploadedAt: new Date().toISOString()
}));
}
}
});
const { data, error } = await supabase.functions.invoke('upload-image', {
if (uploadError) {
console.error('Upload URL error:', uploadError);
throw new Error(uploadError.message);
}
if (!uploadData?.success) {
throw new Error(uploadData?.error || 'Failed to get upload URL');
}
const { uploadURL, id } = uploadData;
// Step 2: Upload file directly to Cloudflare
const formData = new FormData();
formData.append('file', file);
const uploadResponse = await fetch(uploadURL, {
method: 'POST',
body: formData,
});
if (error) {
throw new Error(error.message || 'Upload failed');
if (!uploadResponse.ok) {
throw new Error('Direct upload to Cloudflare failed');
}
if (!data.success) {
throw new Error(data.error || 'Upload failed');
}
// Step 3: Poll for upload completion and get final URLs
const maxAttempts = 30; // 30 seconds maximum wait
let attempts = 0;
while (attempts < maxAttempts) {
const statusUrl = `https://ydvtmnrszybqnbcqbdcy.supabase.co/functions/v1/upload-image?id=${id}`;
const statusResponse = await fetch(statusUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkdnRtbnJzenlicW5iY3FiZGN5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzMjYzNTYsImV4cCI6MjA3MzkwMjM1Nn0.DM3oyapd_omP5ZzIlrT0H9qBsiQBxBRgw2tYuqgXKX4`,
'apikey': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InlkdnRtbnJzenlicW5iY3FiZGN5Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTgzMjYzNTYsImV4cCI6MjA3MzkwMjM1Nn0.DM3oyapd_omP5ZzIlrT0H9qBsiQBxBRgw2tYuqgXKX4'
}
});
if (statusResponse.ok) {
const statusData = await statusResponse.json();
if (statusData?.success && !statusData.draft && statusData.urls) {
return {
id: data.id,
url: data.urls.original,
filename: data.filename,
thumbnailUrl: data.urls.thumbnail
id: statusData.id,
url: statusData.urls.original,
filename: file.name,
thumbnailUrl: statusData.urls.thumbnail
};
}
}
// Wait 1 second before checking again
await new Promise(resolve => setTimeout(resolve, 1000));
attempts++;
}
throw new Error('Upload timeout - image processing took too long');
};
const handleFiles = async (files: FileList) => {

View File

@@ -19,83 +19,33 @@ serve(async (req) => {
throw new Error('Missing Cloudflare credentials')
}
if (req.method !== 'POST') {
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{
status: 405,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
if (req.method === 'POST') {
// Request a direct upload URL from Cloudflare
const { metadata = {} } = await req.json().catch(() => ({}))
const formData = await req.formData()
const file = formData.get('file') as File
if (!file) {
return new Response(
JSON.stringify({ error: 'No file provided' }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
// Validate file size (10MB limit)
const maxSize = 10 * 1024 * 1024 // 10MB
if (file.size > maxSize) {
return new Response(
JSON.stringify({ error: 'File size exceeds 10MB limit' }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
if (!allowedTypes.includes(file.type)) {
return new Response(
JSON.stringify({ error: 'Invalid file type. Only JPEG, PNG, and WebP are allowed.' }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
// Create FormData for Cloudflare Images API
const cloudflareFormData = new FormData()
cloudflareFormData.append('file', file)
// Optional metadata
const metadata = formData.get('metadata')
if (metadata) {
cloudflareFormData.append('metadata', metadata.toString())
}
// Upload to Cloudflare Images
const uploadResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1`,
const directUploadResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v2/direct_upload`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: cloudflareFormData,
body: JSON.stringify({
requireSignedURLs: false,
metadata: metadata
}),
}
)
const uploadResult = await uploadResponse.json()
const directUploadResult = await directUploadResponse.json()
if (!uploadResponse.ok) {
console.error('Cloudflare upload error:', uploadResult)
if (!directUploadResponse.ok) {
console.error('Cloudflare direct upload error:', directUploadResult)
return new Response(
JSON.stringify({
error: 'Failed to upload image',
details: uploadResult.errors || uploadResult.error
error: 'Failed to get upload URL',
details: directUploadResult.errors || directUploadResult.error
}),
{
status: 500,
@@ -104,26 +54,89 @@ serve(async (req) => {
)
}
// Return the upload result with image URLs
// Return the upload URL and image ID to the client
return new Response(
JSON.stringify({
success: true,
id: uploadResult.result.id,
filename: uploadResult.result.filename,
uploaded: uploadResult.result.uploaded,
variants: uploadResult.result.variants,
// Provide convenient URLs for different sizes
urls: {
original: uploadResult.result.variants[0], // First variant is usually the original
thumbnail: `${uploadResult.result.variants[0]}/w=400,h=400,fit=crop`, // 400x400 thumbnail
medium: `${uploadResult.result.variants[0]}/w=800,h=600,fit=cover`, // 800x600 medium
large: `${uploadResult.result.variants[0]}/w=1200,h=900,fit=cover`, // 1200x900 large
}
uploadURL: directUploadResult.result.uploadURL,
id: directUploadResult.result.id,
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
if (req.method === 'GET') {
// Check image status endpoint
const url = new URL(req.url)
const imageId = url.searchParams.get('id')
if (!imageId) {
return new Response(
JSON.stringify({ error: 'Image ID is required' }),
{
status: 400,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
const imageResponse = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/images/v1/${imageId}`,
{
headers: {
'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`,
},
}
)
const imageResult = await imageResponse.json()
if (!imageResponse.ok) {
console.error('Cloudflare image status error:', imageResult)
return new Response(
JSON.stringify({
error: 'Failed to get image status',
details: imageResult.errors || imageResult.error
}),
{
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
// Return the image details with convenient URLs
const result = imageResult.result
return new Response(
JSON.stringify({
success: true,
id: result.id,
uploaded: result.uploaded,
variants: result.variants,
draft: result.draft,
// Provide convenient URLs for different sizes if not draft
urls: result.variants && result.variants.length > 0 ? {
original: result.variants[0],
thumbnail: `${result.variants[0]}/w=400,h=400,fit=crop`,
medium: `${result.variants[0]}/w=800,h=600,fit=cover`,
large: `${result.variants[0]}/w=1200,h=900,fit=cover`,
} : null
}),
{
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
}
return new Response(
JSON.stringify({ error: 'Method not allowed' }),
{
status: 405,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
)
} catch (error) {
console.error('Upload error:', error)