freshyo/apps/admin-ui/hooks/useUploadToObjectStorage.ts
2026-03-21 20:59:45 +05:30

117 lines
3.2 KiB
TypeScript

import { useState } from 'react';
import { trpc } from '../src/trpc-client';
type ContextString = 'review' | 'product_info' | 'notification' | 'store';
interface UploadInput {
blob: Blob;
mimeType: string;
}
interface UploadBatchInput {
images: UploadInput[];
contextString: ContextString;
}
interface UploadResult {
keys: string[];
presignedUrls: string[];
}
export function useUploadToObjectStorage() {
const [isUploading, setIsUploading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [progress, setProgress] = useState<{ completed: number; total: number } | null>(null);
const generateUploadUrls = trpc.common.generateUploadUrls.useMutation();
const upload = async (input: UploadBatchInput): Promise<UploadResult> => {
setIsUploading(true);
setError(null);
setProgress({ completed: 0, total: input.images.length });
try {
const { images, contextString } = input;
if (images.length === 0) {
return { keys: [], presignedUrls: [] };
}
// 1. Get presigned URLs from backend (one call for all images)
const mimeTypes = images.map(img => img.mimeType);
const { uploadUrls } = await generateUploadUrls.mutateAsync({
contextString,
mimeTypes,
});
if (uploadUrls.length !== images.length) {
throw new Error(`Expected ${images.length} URLs, got ${uploadUrls.length}`);
}
// 2. Upload all images in parallel
const uploadPromises = images.map(async (image, index) => {
const presignedUrl = uploadUrls[index];
const { blob, mimeType } = image;
const response = await fetch(presignedUrl, {
method: 'PUT',
body: blob,
headers: { 'Content-Type': mimeType },
});
if (!response.ok) {
throw new Error(`Upload ${index + 1} failed with status ${response.status}`);
}
// Update progress
setProgress(prev => prev ? { ...prev, completed: prev.completed + 1 } : null);
return {
key: extractKeyFromPresignedUrl(presignedUrl),
presignedUrl,
};
});
// Use Promise.all - if any fails, entire batch fails
const results = await Promise.all(uploadPromises);
return {
keys: results.map(r => r.key),
presignedUrls: results.map(r => r.presignedUrl),
};
} catch (err) {
const uploadError = err instanceof Error ? err : new Error('Upload failed');
setError(uploadError);
throw uploadError;
} finally {
setIsUploading(false);
setProgress(null);
}
};
const uploadSingle = async (blob: Blob, mimeType: string, contextString: ContextString): Promise<{ key: string; presignedUrl: string }> => {
const result = await upload({
images: [{ blob, mimeType }],
contextString,
});
return {
key: result.keys[0],
presignedUrl: result.presignedUrls[0],
};
};
return {
upload,
uploadSingle,
isUploading,
error,
progress,
isPending: generateUploadUrls.isPending
};
}
function extractKeyFromPresignedUrl(url: string): string {
const u = new URL(url);
const rawKey = u.pathname.replace(/^\/+/, '');
return decodeURIComponent(rawKey);
}