mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat: implement storage quota management for uploads
- Added storage quota enforcement for audio and image uploads in the respective routes. - Introduced reservation system to manage concurrent uploads and prevent quota overages. - Enhanced comment creation to account for audio and image attachment sizes against user quotas. - Created new UploadReservation model to track in-flight upload reservations. - Backfilled existing video assets with size information from R2. - Added progress component for UI feedback during uploads. - Updated API responses to include reservation IDs for better quota management. - Adjusted error handling to return appropriate storage limit exceeded messages.
This commit is contained in:
@@ -33,7 +33,7 @@ function formatTime(seconds: number): string {
|
||||
}
|
||||
|
||||
type UploadAudioResponse = {
|
||||
data?: { url?: string };
|
||||
data?: { url?: string; reservationId?: string | null };
|
||||
error?: string;
|
||||
};
|
||||
|
||||
@@ -74,6 +74,7 @@ interface AssetsPaneProps {
|
||||
providerVideoId?: string;
|
||||
thumbnailUrl?: string;
|
||||
uploadToken?: string;
|
||||
reservationId?: string | null;
|
||||
}) => Promise<VideoAsset | null>;
|
||||
deleteAsset: (assetId: string) => Promise<boolean>;
|
||||
downloadAsset: (asset: VideoAsset, preference?: 'original' | 'compressed') => Promise<void>;
|
||||
@@ -303,7 +304,7 @@ export const AssetsPane = memo(function AssetsPane({
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
const uploadPayload = (await uploadRes.json().catch(() => null)) as { data?: { url?: string }; error?: string } | null;
|
||||
const uploadPayload = (await uploadRes.json().catch(() => null)) as { data?: { url?: string; reservationId?: string | null }; error?: string } | null;
|
||||
const uploadedImageUrl = uploadPayload?.data?.url;
|
||||
if (!uploadRes.ok || !uploadedImageUrl) {
|
||||
toast.error(uploadPayload?.error || 'Failed to upload image');
|
||||
@@ -314,6 +315,7 @@ export const AssetsPane = memo(function AssetsPane({
|
||||
provider: 'R2_IMAGE',
|
||||
sourceUrl: uploadedImageUrl,
|
||||
displayName: imageTitle.trim() || file.name,
|
||||
reservationId: uploadPayload?.data?.reservationId ?? null,
|
||||
});
|
||||
if (imageInputRef.current) imageInputRef.current.value = '';
|
||||
setImageTitle('');
|
||||
@@ -586,6 +588,7 @@ export const AssetsPane = memo(function AssetsPane({
|
||||
provider: 'R2_AUDIO',
|
||||
sourceUrl: uploadedUrl,
|
||||
displayName: voiceTitle.trim() || fallbackName,
|
||||
reservationId: uploadPayload?.data?.reservationId ?? null,
|
||||
});
|
||||
setVoiceTitle('');
|
||||
setAudioBlob(null);
|
||||
|
||||
@@ -207,7 +207,7 @@ export function useCommentActions({
|
||||
...(imageData && { imageUrl: imageData.url }),
|
||||
...(isGuest && normalizedGuestName && { guestName: normalizedGuestName }),
|
||||
...(selectedTagId && { tagId: selectedTagId }),
|
||||
...(serializedAnnotation && { annotationData: serializedAnnotation }),
|
||||
...(effectiveStrokes && { annotationData: effectiveStrokes }),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -830,7 +830,9 @@ export function useCommentActions({
|
||||
try {
|
||||
const body: Record<string, unknown> = { content: editText };
|
||||
if (editTagId !== undefined) body.tagId = editTagId;
|
||||
if (finalAnnotationData !== undefined) body.annotationData = finalAnnotationData;
|
||||
if (finalAnnotationData !== undefined) {
|
||||
body.annotationData = finalAnnotationData !== null ? JSON.parse(finalAnnotationData) : null;
|
||||
}
|
||||
if (isGuest && normalizedGuestName) body.guestName = normalizedGuestName;
|
||||
const res = await fetch(`/api/comments/${commentId}`, {
|
||||
method: 'PATCH',
|
||||
|
||||
@@ -13,6 +13,7 @@ type CreateAssetPayload = {
|
||||
providerVideoId?: string;
|
||||
thumbnailUrl?: string;
|
||||
uploadToken?: string;
|
||||
reservationId?: string | null;
|
||||
};
|
||||
|
||||
interface UseVideoAssetsParams {
|
||||
|
||||
Reference in New Issue
Block a user