1
+ 'use server' ;
2
+
3
+ import { getCurrentUser } from "@cap/database/auth/session" ;
4
+ import { createS3Client , getS3Bucket , getS3Config } from "@/utils/s3" ;
5
+ import { createPresignedPost } from "@aws-sdk/s3-presigned-post" ;
6
+ import { db } from "@cap/database" ;
7
+ import { s3Buckets , videos } from "@cap/database/schema" ;
8
+ import { eq } from "drizzle-orm" ;
9
+ import { serverEnv } from "@cap/env" ;
10
+ import { nanoId } from "@cap/database/helpers" ;
11
+ import {
12
+ CloudFrontClient ,
13
+ CreateInvalidationCommand ,
14
+ } from "@aws-sdk/client-cloudfront" ;
15
+
16
+ export async function getVideoUploadPresignedUrl ( {
17
+ fileKey,
18
+ duration,
19
+ resolution,
20
+ videoCodec,
21
+ audioCodec,
22
+ } : {
23
+ fileKey : string ;
24
+ duration ?: string ;
25
+ resolution ?: string ;
26
+ videoCodec ?: string ;
27
+ audioCodec ?: string ;
28
+ } ) {
29
+ const user = await getCurrentUser ( ) ;
30
+
31
+ if ( ! user ) {
32
+ throw new Error ( "Unauthorized" ) ;
33
+ }
34
+
35
+ try {
36
+ const [ bucket ] = await db ( )
37
+ . select ( )
38
+ . from ( s3Buckets )
39
+ . where ( eq ( s3Buckets . ownerId , user . id ) ) ;
40
+
41
+ const s3Config = bucket
42
+ ? {
43
+ endpoint : bucket . endpoint || undefined ,
44
+ region : bucket . region ,
45
+ accessKeyId : bucket . accessKeyId ,
46
+ secretAccessKey : bucket . secretAccessKey ,
47
+ }
48
+ : null ;
49
+
50
+ if (
51
+ ! bucket ||
52
+ ! s3Config ||
53
+ bucket . bucketName !== serverEnv ( ) . CAP_AWS_BUCKET
54
+ ) {
55
+ const distributionId = serverEnv ( ) . CAP_CLOUDFRONT_DISTRIBUTION_ID ;
56
+ if ( distributionId ) {
57
+
58
+ const cloudfront = new CloudFrontClient ( {
59
+ region : serverEnv ( ) . CAP_AWS_REGION || "us-east-1" ,
60
+ credentials : {
61
+ accessKeyId : serverEnv ( ) . CAP_AWS_ACCESS_KEY || "" ,
62
+ secretAccessKey : serverEnv ( ) . CAP_AWS_SECRET_KEY || "" ,
63
+ } ,
64
+ } ) ;
65
+
66
+ const pathToInvalidate = "/" + fileKey ;
67
+
68
+ try {
69
+ const invalidation = await cloudfront . send (
70
+ new CreateInvalidationCommand ( {
71
+ DistributionId : distributionId ,
72
+ InvalidationBatch : {
73
+ CallerReference : `${ Date . now ( ) } ` ,
74
+ Paths : {
75
+ Quantity : 1 ,
76
+ Items : [ pathToInvalidate ] ,
77
+ } ,
78
+ } ,
79
+ } )
80
+ ) ;
81
+ } catch ( error ) {
82
+ console . error ( "Failed to create CloudFront invalidation:" , error ) ;
83
+ }
84
+ }
85
+ }
86
+
87
+ const [ s3Client ] = await createS3Client ( s3Config ) ;
88
+
89
+ const contentType = fileKey . endsWith ( ".aac" )
90
+ ? "audio/aac"
91
+ : fileKey . endsWith ( ".webm" )
92
+ ? "audio/webm"
93
+ : fileKey . endsWith ( ".mp4" )
94
+ ? "video/mp4"
95
+ : fileKey . endsWith ( ".mp3" )
96
+ ? "audio/mpeg"
97
+ : fileKey . endsWith ( ".m3u8" )
98
+ ? "application/x-mpegURL"
99
+ : "video/mp2t" ;
100
+
101
+ const Fields = {
102
+ "Content-Type" : contentType ,
103
+ "x-amz-meta-userid" : user . id ,
104
+ "x-amz-meta-duration" : duration ?? "" ,
105
+ "x-amz-meta-resolution" : resolution ?? "" ,
106
+ "x-amz-meta-videocodec" : videoCodec ?? "" ,
107
+ "x-amz-meta-audiocodec" : audioCodec ?? "" ,
108
+ } ;
109
+
110
+ const bucketName = await getS3Bucket ( bucket ) ;
111
+
112
+ const presignedPostData = await createPresignedPost ( s3Client , {
113
+ Bucket : bucketName ,
114
+ Key : fileKey ,
115
+ Fields,
116
+ Expires : 1800 ,
117
+ } ) ;
118
+
119
+ const customEndpoint = serverEnv ( ) . CAP_AWS_ENDPOINT ;
120
+ if ( customEndpoint && ! customEndpoint . includes ( "amazonaws.com" ) ) {
121
+ if ( serverEnv ( ) . S3_PATH_STYLE ) {
122
+ presignedPostData . url = `${ customEndpoint } /${ bucketName } ` ;
123
+ } else {
124
+ presignedPostData . url = customEndpoint ;
125
+ }
126
+ }
127
+
128
+ const videoId = fileKey . split ( "/" ) [ 1 ] ;
129
+ if ( videoId ) {
130
+ try {
131
+ await fetch ( `${ serverEnv ( ) . WEB_URL } /api/revalidate` , {
132
+ method : "POST" ,
133
+ headers : {
134
+ "Content-Type" : "application/json" ,
135
+ } ,
136
+ body : JSON . stringify ( { videoId } ) ,
137
+ } ) ;
138
+ } catch ( revalidateError ) {
139
+ console . error ( "Failed to revalidate page:" , revalidateError ) ;
140
+ }
141
+ }
142
+
143
+ return { presignedPostData } ;
144
+ } catch ( error ) {
145
+ console . error ( "Error getting presigned URL:" , error ) ;
146
+ throw new Error (
147
+ error instanceof Error ? error . message : "Failed to get presigned URL"
148
+ ) ;
149
+ }
150
+ }
151
+
152
+ export async function createVideoAndGetUploadUrl ( {
153
+ videoId,
154
+ duration,
155
+ resolution,
156
+ videoCodec,
157
+ audioCodec,
158
+ isScreenshot = false ,
159
+ isUpload = false ,
160
+ } : {
161
+ videoId ?: string ;
162
+ duration ?: number ;
163
+ resolution ?: string ;
164
+ videoCodec ?: string ;
165
+ audioCodec ?: string ;
166
+ isScreenshot ?: boolean ;
167
+ isUpload ?: boolean ;
168
+ } ) {
169
+ const user = await getCurrentUser ( ) ;
170
+
171
+ if ( ! user ) {
172
+ throw new Error ( "Unauthorized" ) ;
173
+ }
174
+
175
+ try {
176
+ const isUpgraded = user . stripeSubscriptionStatus === "active" ;
177
+
178
+ if ( ! isUpgraded && duration && duration > 300 ) {
179
+ throw new Error ( "upgrade_required" ) ;
180
+ }
181
+
182
+ const [ bucket ] = await db ( )
183
+ . select ( )
184
+ . from ( s3Buckets )
185
+ . where ( eq ( s3Buckets . ownerId , user . id ) ) ;
186
+
187
+ const s3Config = await getS3Config ( bucket ) ;
188
+ const bucketName = await getS3Bucket ( bucket ) ;
189
+
190
+ const date = new Date ( ) ;
191
+ const formattedDate = `${ date . getDate ( ) } ${ date . toLocaleString ( "default" , {
192
+ month : "long" ,
193
+ } ) } ${ date . getFullYear ( ) } `;
194
+
195
+ if ( videoId ) {
196
+ const [ existingVideo ] = await db ( )
197
+ . select ( )
198
+ . from ( videos )
199
+ . where ( eq ( videos . id , videoId ) ) ;
200
+
201
+ if ( existingVideo ) {
202
+ const fileKey = `${ user . id } /${ videoId } /${ isScreenshot ? "screenshot/screen-capture.jpg" : "result.mp4" } ` ;
203
+ const { presignedPostData } = await getVideoUploadPresignedUrl ( {
204
+ fileKey,
205
+ duration : duration ?. toString ( ) ,
206
+ resolution,
207
+ videoCodec,
208
+ audioCodec,
209
+ } ) ;
210
+
211
+ return {
212
+ id : existingVideo . id ,
213
+ user_id : user . id ,
214
+ aws_region : existingVideo . awsRegion ,
215
+ aws_bucket : existingVideo . awsBucket ,
216
+ presignedPostData,
217
+ } ;
218
+ }
219
+ }
220
+
221
+ const idToUse = videoId || nanoId ( ) ;
222
+
223
+ const videoData = {
224
+ id : idToUse ,
225
+ name : `Cap ${ isScreenshot ? "Screenshot" : isUpload ? "Upload" : "Recording" } - ${ formattedDate } ` ,
226
+ ownerId : user . id ,
227
+ awsRegion : s3Config . region ,
228
+ awsBucket : bucketName ,
229
+ source : { type : "desktopMP4" as const } ,
230
+ isScreenshot,
231
+ bucket : bucket ?. id ,
232
+ } ;
233
+
234
+ await db ( ) . insert ( videos ) . values ( videoData ) ;
235
+
236
+ const fileKey = `${ user . id } /${ idToUse } /${ isScreenshot ? "screenshot/screen-capture.jpg" : "result.mp4" } ` ;
237
+ const { presignedPostData } = await getVideoUploadPresignedUrl ( {
238
+ fileKey,
239
+ duration : duration ?. toString ( ) ,
240
+ resolution,
241
+ videoCodec,
242
+ audioCodec,
243
+ } ) ;
244
+
245
+ return {
246
+ id : idToUse ,
247
+ user_id : user . id ,
248
+ aws_region : s3Config . region ,
249
+ aws_bucket : bucketName ,
250
+ presignedPostData,
251
+ } ;
252
+ } catch ( error ) {
253
+ console . error ( "Error creating video and getting upload URL:" , error ) ;
254
+ throw new Error (
255
+ error instanceof Error ? error . message : "Failed to create video"
256
+ ) ;
257
+ }
258
+ }
0 commit comments