mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
feat: enable S3 video uploads and update related configurations
- Added support for self-hosted S3 video uploads with new environment variables: OPENFRAME_ENABLE_S3_VIDEO_UPLOADS and OPENFRAME_MAX_VIDEO_UPLOAD_BYTES. - Updated .env.example and .env.docker.example to reflect new configuration options. - Enhanced Content Security Policy to include origins for S3-compatible storage. - Updated dependencies for AWS SDK to support new features. - Refactored upload logic to accommodate both Bunny and S3 upload providers. - Updated documentation to clarify the usage of direct uploads and S3 configurations. - Closes #11
This commit is contained in:
@@ -22,6 +22,8 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||
import { cleanupPendingR2VideoUpload, uploadVideoToR2 } from '@/lib/client/r2-video-upload';
|
||||
import type { DirectUploadProvider } from '@/components/video-page/types';
|
||||
|
||||
type ProjectOption = {
|
||||
id: string;
|
||||
@@ -35,6 +37,7 @@ interface VideoDragDropUploaderProps {
|
||||
workspaceId?: string;
|
||||
projectOptions?: ProjectOption[];
|
||||
canUpload?: boolean;
|
||||
directUploadProvider?: DirectUploadProvider;
|
||||
}
|
||||
|
||||
const VIDEO_FILE_EXTENSIONS = ['mp4', 'webm', 'ogg', 'mov', 'm4v', 'mkv'];
|
||||
@@ -68,6 +71,7 @@ export function VideoDragDropUploader({
|
||||
workspaceId,
|
||||
projectOptions,
|
||||
canUpload = false,
|
||||
directUploadProvider = 'bunny',
|
||||
}: VideoDragDropUploaderProps) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -86,11 +90,23 @@ export function VideoDragDropUploader({
|
||||
);
|
||||
|
||||
const activeTusUploadRef = useRef<ActiveTusUpload | null>(null);
|
||||
const pendingUploadRef = useRef<{
|
||||
projectId: string;
|
||||
videoId: string;
|
||||
uploadToken: string;
|
||||
} | null>(null);
|
||||
const pendingUploadRef = useRef<
|
||||
| {
|
||||
type: 'bunny';
|
||||
projectId: string;
|
||||
videoId: string;
|
||||
uploadToken: string;
|
||||
}
|
||||
| {
|
||||
type: 'r2';
|
||||
projectId: string;
|
||||
objectKey: string;
|
||||
uploadToken: string;
|
||||
reservationId: string | null;
|
||||
thumbnailObjectKey?: string;
|
||||
}
|
||||
| null
|
||||
>(null);
|
||||
const cancelRequestedRef = useRef(false);
|
||||
const dragDepthRef = useRef(0);
|
||||
const hasLoadedProjectsRef = useRef(false);
|
||||
@@ -205,11 +221,20 @@ export function VideoDragDropUploader({
|
||||
|
||||
if (pending) {
|
||||
try {
|
||||
await fetch(`/api/projects/${pending.projectId}/videos/bunny-init`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ videoId: pending.videoId, uploadToken: pending.uploadToken }),
|
||||
});
|
||||
if (pending.type === 'bunny') {
|
||||
await fetch(`/api/projects/${pending.projectId}/videos/bunny-init`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ videoId: pending.videoId, uploadToken: pending.uploadToken }),
|
||||
});
|
||||
} else {
|
||||
await cleanupPendingR2VideoUpload(pending.projectId, {
|
||||
objectKey: pending.objectKey,
|
||||
uploadToken: pending.uploadToken,
|
||||
reservationId: pending.reservationId,
|
||||
thumbnailObjectKey: pending.thumbnailObjectKey,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup cancelled upload:', error);
|
||||
}
|
||||
@@ -232,12 +257,80 @@ export function VideoDragDropUploader({
|
||||
setSelectedProjectId(projectId);
|
||||
setSelectedProjectName(projectName ?? projectsById.get(projectId) ?? null);
|
||||
|
||||
let createdVideoId: string | null = null;
|
||||
let uploadToken: string | null = null;
|
||||
let pendingCleanup:
|
||||
| { type: 'bunny'; videoId: string; uploadToken: string }
|
||||
| {
|
||||
type: 'r2';
|
||||
objectKey: string;
|
||||
uploadToken: string;
|
||||
reservationId: string | null;
|
||||
thumbnailObjectKey?: string;
|
||||
}
|
||||
| null = null;
|
||||
|
||||
try {
|
||||
const title = getDefaultTitleFromFile(file);
|
||||
|
||||
if (directUploadProvider === 'r2') {
|
||||
const uploaded = await uploadVideoToR2(projectId, file, {
|
||||
onProgress: (progress) => {
|
||||
setUploadProgress(progress);
|
||||
setUploadStatus(`Uploading... ${progress}%`);
|
||||
},
|
||||
});
|
||||
pendingCleanup = {
|
||||
type: 'r2',
|
||||
objectKey: uploaded.objectKey,
|
||||
uploadToken: uploaded.uploadToken,
|
||||
reservationId: uploaded.reservationId,
|
||||
thumbnailObjectKey: uploaded.thumbnailObjectKey,
|
||||
};
|
||||
pendingUploadRef.current = {
|
||||
type: 'r2',
|
||||
projectId,
|
||||
objectKey: uploaded.objectKey,
|
||||
uploadToken: uploaded.uploadToken,
|
||||
reservationId: uploaded.reservationId,
|
||||
thumbnailObjectKey: uploaded.thumbnailObjectKey,
|
||||
};
|
||||
|
||||
setUploadStatus('Saving video...');
|
||||
const createResponse = await fetch(`/api/projects/${projectId}/videos`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
description: null,
|
||||
videoUrl: uploaded.proxyUrl,
|
||||
providerId: 'r2',
|
||||
videoId: uploaded.objectKey,
|
||||
thumbnailUrl: uploaded.thumbnailUrl || '/placeholder-video-thumbnail.png',
|
||||
duration: uploaded.duration,
|
||||
uploadToken: uploaded.uploadToken,
|
||||
objectKey: uploaded.objectKey,
|
||||
reservationId: uploaded.reservationId,
|
||||
}),
|
||||
});
|
||||
|
||||
const createPayload = (await createResponse.json().catch(() => null)) as {
|
||||
error?: string;
|
||||
} | null;
|
||||
|
||||
if (!createResponse.ok) {
|
||||
throw new Error(createPayload?.error || 'Failed to create video');
|
||||
}
|
||||
|
||||
toast.success(
|
||||
`Video uploaded to ${projectName ?? projectsById.get(projectId) ?? 'project'}`
|
||||
);
|
||||
setDialogOpen(false);
|
||||
setDroppedFile(null);
|
||||
cleanupUploadState();
|
||||
router.push(`/projects/${projectId}`);
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const initResponse = await fetch(`/api/projects/${projectId}/videos/bunny-init`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -259,9 +352,11 @@ export function VideoDragDropUploader({
|
||||
throw new Error(initPayload?.error || 'Failed to initialize upload');
|
||||
}
|
||||
|
||||
createdVideoId = initPayload.data.videoId;
|
||||
uploadToken = initPayload.data.uploadToken;
|
||||
const createdVideoId = initPayload.data.videoId;
|
||||
const uploadToken = initPayload.data.uploadToken;
|
||||
pendingCleanup = { type: 'bunny', videoId: createdVideoId, uploadToken };
|
||||
pendingUploadRef.current = {
|
||||
type: 'bunny',
|
||||
projectId,
|
||||
videoId: createdVideoId,
|
||||
uploadToken,
|
||||
@@ -346,13 +441,20 @@ export function VideoDragDropUploader({
|
||||
return;
|
||||
}
|
||||
|
||||
if (createdVideoId && uploadToken) {
|
||||
if (pendingCleanup) {
|
||||
try {
|
||||
await fetch(`/api/projects/${projectId}/videos/bunny-init`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ videoId: createdVideoId, uploadToken }),
|
||||
});
|
||||
if (pendingCleanup.type === 'bunny') {
|
||||
await fetch(`/api/projects/${projectId}/videos/bunny-init`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
videoId: pendingCleanup.videoId,
|
||||
uploadToken: pendingCleanup.uploadToken,
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
await cleanupPendingR2VideoUpload(projectId, pendingCleanup);
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error('Failed to cleanup pending upload:', cleanupError);
|
||||
}
|
||||
@@ -364,7 +466,7 @@ export function VideoDragDropUploader({
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to upload video');
|
||||
}
|
||||
},
|
||||
[bunnyCdnHostname, cleanupUploadState, projectsById, router]
|
||||
[bunnyCdnHostname, cleanupUploadState, directUploadProvider, projectsById, router]
|
||||
);
|
||||
|
||||
const handleDropFile = useCallback(
|
||||
|
||||
Reference in New Issue
Block a user