Files
OpenFrame/app/api/projects/[projectId]/download/route.ts
T
yusufipk 8d7d064647 feat: make asset downloads opt-in via "Include assets" toggle
Project/selected downloads now include only videos by default. Add an
"Include assets" checkbox toggle to both download dropdowns (default off)
that adds b-rolls and other attached assets to the download when enabled.

- buildProjectDownloadManifest gains an includeAssets option (default false).
- Download route reads ?assets=1 and passes it through.
2026-07-10 21:03:34 +07:00

113 lines
3.5 KiB
TypeScript

import { NextRequest } from 'next/server';
import { auth, checkProjectAccess } from '@/lib/auth';
import { apiErrors, successResponse, withCacheControl } from '@/lib/api-response';
import { db } from '@/lib/db';
import { logError } from '@/lib/logger';
import {
buildProjectDownloadManifest,
canDownloadProjectMedia,
parseRequestedVideoIds,
validateProjectDownloadManifest,
} from '@/lib/project-download';
import { rateLimit } from '@/lib/rate-limit';
type RouteParams = { params: Promise<{ projectId: string }> };
// GET /api/projects/[projectId]/download
export async function GET(request: NextRequest, { params }: RouteParams) {
try {
const limited = await rateLimit(request, 'project-download');
if (limited) return limited;
const session = await auth();
const { projectId } = await params;
const requestedVideoIds = parseRequestedVideoIds(request.nextUrl.searchParams.get('videoIds'));
const includeAllVersions = request.nextUrl.searchParams.get('versions') === 'all';
const includeAssets = request.nextUrl.searchParams.get('assets') === '1';
if (requestedVideoIds && requestedVideoIds.length === 0) {
return apiErrors.badRequest('At least one video must be selected for download');
}
const project = await db.project.findUnique({
where: { id: projectId },
select: {
id: true,
name: true,
ownerId: true,
workspaceId: true,
visibility: true,
allowDownloads: true,
},
});
if (!project) {
return apiErrors.notFound('Project');
}
const access = await checkProjectAccess(project, session?.user?.id);
if (!canDownloadProjectMedia(project, access)) {
return apiErrors.forbidden('Project downloads are disabled for viewers');
}
const videos = await db.video.findMany({
where: {
projectId,
...(requestedVideoIds ? { id: { in: requestedVideoIds } } : {}),
},
orderBy: [{ position: 'asc' }, { id: 'asc' }],
select: {
id: true,
title: true,
position: true,
versions: {
orderBy: { versionNumber: 'asc' },
select: {
id: true,
versionNumber: true,
versionLabel: true,
providerId: true,
videoId: true,
originalUrl: true,
sizeBytes: true,
},
},
assets: {
orderBy: { createdAt: 'asc' },
select: {
id: true,
provider: true,
displayName: true,
sourceUrl: true,
providerVideoId: true,
sizeBytes: true,
},
},
},
});
if (requestedVideoIds) {
const foundIds = new Set(videos.map((video) => video.id));
const missing = requestedVideoIds.filter((id) => !foundIds.has(id));
if (missing.length > 0) {
return apiErrors.badRequest('One or more selected videos do not belong to this project');
}
}
const manifest = buildProjectDownloadManifest(project.name, videos, {
includeAllVersions,
includeAssets,
});
const validationError = validateProjectDownloadManifest(manifest);
if (validationError) {
return apiErrors.badRequest(validationError);
}
const response = successResponse(manifest);
return withCacheControl(response, 'private, no-store');
} catch (error) {
logError('Error creating project download manifest:', error);
return apiErrors.internalError('Failed to prepare project download');
}
}