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 uploadFile = async (file: File): Promise<UploadedImage> => {
// 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()
}
}
});
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(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
formData.append('metadata', JSON.stringify({
filename: file.name,
size: file.size,
type: file.type,
uploadedAt: new Date().toISOString()
}));
const { data, error } = await supabase.functions.invoke('upload-image', { const uploadResponse = await fetch(uploadURL, {
method: 'POST',
body: formData, body: formData,
}); });
if (error) { if (!uploadResponse.ok) {
throw new Error(error.message || 'Upload failed'); throw new Error('Direct upload to Cloudflare failed');
} }
if (!data.success) { // Step 3: Poll for upload completion and get final URLs
throw new Error(data.error || 'Upload failed'); 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: 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++;
} }
return { throw new Error('Upload timeout - image processing took too long');
id: data.id,
url: data.urls.original,
filename: data.filename,
thumbnailUrl: data.urls.thumbnail
};
}; };
const handleFiles = async (files: FileList) => { const handleFiles = async (files: FileList) => {

View File

@@ -19,108 +19,121 @@ serve(async (req) => {
throw new Error('Missing Cloudflare credentials') throw new Error('Missing Cloudflare credentials')
} }
if (req.method !== 'POST') { if (req.method === 'POST') {
return new Response( // Request a direct upload URL from Cloudflare
JSON.stringify({ error: 'Method not allowed' }), const { metadata = {} } = await req.json().catch(() => ({}))
{
status: 405, const directUploadResponse = await fetch(
headers: { ...corsHeaders, 'Content-Type': 'application/json' } `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: JSON.stringify({
requireSignedURLs: false,
metadata: metadata
}),
} }
) )
}
const formData = await req.formData() const directUploadResult = await directUploadResponse.json()
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) if (!directUploadResponse.ok) {
const maxSize = 10 * 1024 * 1024 // 10MB console.error('Cloudflare direct upload error:', directUploadResult)
if (file.size > maxSize) { return new Response(
return new Response( JSON.stringify({
JSON.stringify({ error: 'File size exceeds 10MB limit' }), error: 'Failed to get upload URL',
{ details: directUploadResult.errors || directUploadResult.error
status: 400, }),
headers: { ...corsHeaders, 'Content-Type': 'application/json' } {
} status: 500,
) 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`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${CLOUDFLARE_IMAGES_API_TOKEN}`,
},
body: cloudflareFormData,
} }
)
const uploadResult = await uploadResponse.json() // Return the upload URL and image ID to the client
if (!uploadResponse.ok) {
console.error('Cloudflare upload error:', uploadResult)
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
error: 'Failed to upload image', success: true,
details: uploadResult.errors || uploadResult.error 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
}), }),
{ {
status: 500,
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: { ...corsHeaders, 'Content-Type': 'application/json' }
} }
) )
} }
// Return the upload result with image URLs
return new Response( return new Response(
JSON.stringify({ JSON.stringify({ error: 'Method not allowed' }),
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
}
}),
{ {
status: 405,
headers: { ...corsHeaders, 'Content-Type': 'application/json' } headers: { ...corsHeaders, 'Content-Type': 'application/json' }
} }
) )