mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
139 lines
5.5 KiB
TypeScript
139 lines
5.5 KiB
TypeScript
import { supabase } from '@/integrations/supabase/client';
|
|
import type { UploadedImage } from '@/components/upload/EntityMultiImageUploader';
|
|
|
|
export interface CloudflareUploadResponse {
|
|
result: {
|
|
id: string;
|
|
variants: string[];
|
|
};
|
|
success: boolean;
|
|
}
|
|
|
|
// Internal type to track upload status
|
|
interface UploadedImageWithFlag extends UploadedImage {
|
|
wasNewlyUploaded?: boolean;
|
|
}
|
|
|
|
/**
|
|
* Uploads pending local images to Cloudflare via Supabase Edge Function
|
|
* @param images Array of UploadedImage objects (mix of local and already uploaded)
|
|
* @returns Array of UploadedImage objects with all images uploaded
|
|
*/
|
|
export async function uploadPendingImages(images: UploadedImage[]): Promise<UploadedImage[]> {
|
|
// Process all images in parallel for better performance using allSettled
|
|
const uploadPromises = images.map(async (image, index): Promise<UploadedImageWithFlag> => {
|
|
if (image.isLocal && image.file) {
|
|
const fileName = image.file.name;
|
|
|
|
// Step 1: Get upload URL from our Supabase Edge Function
|
|
const { data: uploadUrlData, error: urlError } = await supabase.functions.invoke('upload-image', {
|
|
body: { action: 'get-upload-url' }
|
|
});
|
|
|
|
if (urlError || !uploadUrlData?.uploadURL) {
|
|
console.error(`imageUploadHelper.uploadPendingImages: Failed to get upload URL for "${fileName}":`, urlError);
|
|
throw new Error(`Failed to get upload URL for "${fileName}": ${urlError?.message || 'Unknown error'}`);
|
|
}
|
|
|
|
// Step 2: Upload file directly to Cloudflare
|
|
const formData = new FormData();
|
|
formData.append('file', image.file);
|
|
|
|
const uploadResponse = await fetch(uploadUrlData.uploadURL, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!uploadResponse.ok) {
|
|
const errorText = await uploadResponse.text();
|
|
console.error(`imageUploadHelper.uploadPendingImages: Upload failed for "${fileName}" (status ${uploadResponse.status}):`, errorText);
|
|
throw new Error(`Upload failed for "${fileName}" (status ${uploadResponse.status}): ${errorText}`);
|
|
}
|
|
|
|
const result: CloudflareUploadResponse = await uploadResponse.json();
|
|
|
|
if (!result.success || !result.result) {
|
|
console.error(`imageUploadHelper.uploadPendingImages: Cloudflare upload unsuccessful for "${fileName}"`);
|
|
throw new Error(`Cloudflare upload returned unsuccessful response for "${fileName}"`);
|
|
}
|
|
|
|
// Clean up object URL
|
|
URL.revokeObjectURL(image.url);
|
|
|
|
const CLOUDFLARE_ACCOUNT_HASH = import.meta.env.VITE_CLOUDFLARE_ACCOUNT_HASH;
|
|
|
|
// Step 3: Return uploaded image metadata with wasNewlyUploaded flag
|
|
return {
|
|
url: `https://imagedelivery.net/${CLOUDFLARE_ACCOUNT_HASH}/${result.result.id}/public`,
|
|
cloudflare_id: result.result.id,
|
|
caption: image.caption,
|
|
isLocal: false,
|
|
wasNewlyUploaded: true // Flag to track newly uploaded images
|
|
};
|
|
} else {
|
|
// Already uploaded, keep as is
|
|
return {
|
|
url: image.url,
|
|
cloudflare_id: image.cloudflare_id,
|
|
caption: image.caption,
|
|
isLocal: false,
|
|
wasNewlyUploaded: false // Pre-existing image
|
|
};
|
|
}
|
|
});
|
|
|
|
// Wait for all uploads to settle (succeed or fail)
|
|
const results = await Promise.allSettled(uploadPromises);
|
|
|
|
// Separate successful and failed uploads
|
|
const successfulUploads: UploadedImageWithFlag[] = [];
|
|
const newlyUploadedImageIds: string[] = []; // Track ONLY newly uploaded images for cleanup
|
|
const errors: string[] = [];
|
|
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
const uploadedImage = result.value;
|
|
successfulUploads.push(uploadedImage);
|
|
// Only track newly uploaded images for potential cleanup
|
|
if (uploadedImage.wasNewlyUploaded && uploadedImage.cloudflare_id) {
|
|
newlyUploadedImageIds.push(uploadedImage.cloudflare_id);
|
|
}
|
|
} else {
|
|
errors.push(result.reason?.message || `Upload ${index + 1} failed`);
|
|
}
|
|
});
|
|
|
|
// If any uploads failed, clean up ONLY newly uploaded images and throw error
|
|
if (errors.length > 0) {
|
|
if (newlyUploadedImageIds.length > 0) {
|
|
console.error(`imageUploadHelper.uploadPendingImages: Some uploads failed. Cleaning up ${newlyUploadedImageIds.length} newly uploaded images...`);
|
|
|
|
// Attempt cleanup in parallel with detailed error tracking
|
|
const cleanupResults = await Promise.allSettled(
|
|
newlyUploadedImageIds.map(imageId =>
|
|
supabase.functions.invoke('upload-image', {
|
|
body: { action: 'delete', imageId }
|
|
})
|
|
)
|
|
);
|
|
|
|
// Track cleanup failures for better debugging
|
|
const cleanupFailures = cleanupResults.filter(r => r.status === 'rejected');
|
|
if (cleanupFailures.length > 0) {
|
|
console.error(
|
|
`imageUploadHelper.uploadPendingImages: Failed to cleanup ${cleanupFailures.length} of ${newlyUploadedImageIds.length} images.`,
|
|
'These images may remain orphaned in Cloudflare:',
|
|
newlyUploadedImageIds.filter((_, i) => cleanupResults[i].status === 'rejected')
|
|
);
|
|
} else {
|
|
console.log(`imageUploadHelper.uploadPendingImages: Successfully cleaned up ${newlyUploadedImageIds.length} images.`);
|
|
}
|
|
}
|
|
|
|
throw new Error(`Failed to upload ${errors.length} of ${images.length} images: ${errors.join('; ')}`);
|
|
}
|
|
|
|
// Remove the wasNewlyUploaded flag before returning
|
|
return successfulUploads.map(({ wasNewlyUploaded, ...image }) => image as UploadedImage);
|
|
}
|