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:
@@ -0,0 +1,20 @@
|
||||
import 'dotenv/config';
|
||||
import { ensureR2UploadCors, R2_BUCKET_NAME, r2Client } from '@/lib/r2';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
async function main() {
|
||||
const origins = await ensureR2UploadCors();
|
||||
console.log(`Configured upload CORS on bucket "${R2_BUCKET_NAME}" for origins:`);
|
||||
for (const origin of origins) {
|
||||
console.log(` - ${origin}`);
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
logError('Failed to configure R2 upload CORS:', error);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(() => {
|
||||
r2Client.destroy();
|
||||
});
|
||||
@@ -82,6 +82,16 @@ async function getPublicTables(client: Client) {
|
||||
return result.rows.map((row) => row.table_name);
|
||||
}
|
||||
|
||||
async function ensureRateLimitCleanupFunction(client: Client) {
|
||||
await client.query(`
|
||||
CREATE OR REPLACE FUNCTION cleanup_rate_limits() RETURNS void AS $$
|
||||
BEGIN
|
||||
DELETE FROM rate_limits WHERE window_start < NOW() - INTERVAL '1 hour';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (!process.env.DATABASE_URL) {
|
||||
throw new Error('DATABASE_URL is required');
|
||||
@@ -132,6 +142,7 @@ async function main() {
|
||||
await runPrisma(['migrate', 'resolve', '--applied', migrationName]);
|
||||
}
|
||||
|
||||
await ensureRateLimitCleanupFunction(client);
|
||||
console.log('Fresh database bootstrap complete');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import { logError } from '@/lib/logger';
|
||||
|
||||
const UNATTACHED_UPLOAD_TTL_MS = 15 * 60 * 1000;
|
||||
const CHUNK_SIZE = 500;
|
||||
const PREFIXES = ['images/', 'voice/'] as const;
|
||||
const PREFIXES = ['images/', 'voice/', 'videos/'] as const;
|
||||
|
||||
type CleanupCandidate = {
|
||||
key: string;
|
||||
@@ -33,6 +33,10 @@ function keyToProxyUrl(key: string): string | null {
|
||||
const filename = key.slice('voice/'.length);
|
||||
return filename ? `/api/upload/audio/${filename}` : null;
|
||||
}
|
||||
if (key.startsWith('videos/')) {
|
||||
const filename = key.slice('videos/'.length);
|
||||
return filename ? `/api/upload/video/${filename}` : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -93,31 +97,38 @@ async function findReferencedUrls(urls: string[]): Promise<Set<string>> {
|
||||
).userFeedbackScreenshot;
|
||||
|
||||
for (const group of chunk(urls, CHUNK_SIZE)) {
|
||||
const [commentRows, feedbackRows, feedbackAttachmentRows, assetRows] = await Promise.all([
|
||||
db.comment.findMany({
|
||||
where: {
|
||||
OR: [{ voiceUrl: { in: group } }, { imageUrl: { in: group } }],
|
||||
},
|
||||
select: {
|
||||
voiceUrl: true,
|
||||
imageUrl: true,
|
||||
},
|
||||
}),
|
||||
db.userFeedback.findMany({
|
||||
where: { screenshotUrl: { in: group } },
|
||||
select: { screenshotUrl: true },
|
||||
}),
|
||||
userFeedbackScreenshotDelegate
|
||||
? userFeedbackScreenshotDelegate.findMany({
|
||||
where: { url: { in: group } },
|
||||
select: { url: true },
|
||||
})
|
||||
: Promise.resolve([] as Array<{ url: string }>),
|
||||
db.videoAsset.findMany({
|
||||
where: { sourceUrl: { in: group } },
|
||||
select: { sourceUrl: true },
|
||||
}),
|
||||
]);
|
||||
const [commentRows, feedbackRows, feedbackAttachmentRows, assetRows, versionRows] =
|
||||
await Promise.all([
|
||||
db.comment.findMany({
|
||||
where: {
|
||||
OR: [{ voiceUrl: { in: group } }, { imageUrl: { in: group } }],
|
||||
},
|
||||
select: {
|
||||
voiceUrl: true,
|
||||
imageUrl: true,
|
||||
},
|
||||
}),
|
||||
db.userFeedback.findMany({
|
||||
where: { screenshotUrl: { in: group } },
|
||||
select: { screenshotUrl: true },
|
||||
}),
|
||||
userFeedbackScreenshotDelegate
|
||||
? userFeedbackScreenshotDelegate.findMany({
|
||||
where: { url: { in: group } },
|
||||
select: { url: true },
|
||||
})
|
||||
: Promise.resolve([] as Array<{ url: string }>),
|
||||
db.videoAsset.findMany({
|
||||
where: { sourceUrl: { in: group } },
|
||||
select: { sourceUrl: true },
|
||||
}),
|
||||
db.videoVersion.findMany({
|
||||
where: {
|
||||
OR: [{ originalUrl: { in: group } }, { thumbnailUrl: { in: group } }],
|
||||
},
|
||||
select: { originalUrl: true, thumbnailUrl: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
for (const row of commentRows) {
|
||||
if (row.voiceUrl) referenced.add(row.voiceUrl);
|
||||
@@ -132,6 +143,10 @@ async function findReferencedUrls(urls: string[]): Promise<Set<string>> {
|
||||
for (const row of assetRows) {
|
||||
if (row.sourceUrl) referenced.add(row.sourceUrl);
|
||||
}
|
||||
for (const row of versionRows) {
|
||||
if (row.originalUrl) referenced.add(row.originalUrl);
|
||||
if (row.thumbnailUrl) referenced.add(row.thumbnailUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return referenced;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dotenv/config';
|
||||
import { ensureR2BucketExists, R2_BUCKET_NAME } from '@/lib/r2';
|
||||
import { ensureR2BucketExists, ensureR2UploadCors, R2_BUCKET_NAME, r2Client } from '@/lib/r2';
|
||||
import { isS3VideoUploadsEnabled } from '@/lib/feature-flags';
|
||||
import { logError } from '@/lib/logger';
|
||||
|
||||
const shouldCreateBucket = /^(1|true|yes|on)$/i.test(
|
||||
@@ -15,9 +16,26 @@ async function main() {
|
||||
console.log(`Ensuring object storage bucket exists: ${R2_BUCKET_NAME}`);
|
||||
await ensureR2BucketExists();
|
||||
console.log(`Bucket is ready: ${R2_BUCKET_NAME}`);
|
||||
|
||||
if (isS3VideoUploadsEnabled()) {
|
||||
try {
|
||||
const origins = await ensureR2UploadCors();
|
||||
console.log(`Configured upload CORS for origins: ${origins.join(', ')}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.warn(
|
||||
`Skipping automatic bucket CORS setup (${message}). ` +
|
||||
'If direct S3 uploads fail in the browser, configure bucket CORS manually.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
logError('Self-host bootstrap failed:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
main()
|
||||
.catch((error) => {
|
||||
logError('Self-host bootstrap failed:', error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => {
|
||||
r2Client.destroy();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user