@studious-lms/server 1.1.19 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BASE64_REMOVAL_SUMMARY.md +153 -0
- package/dist/lib/fileUpload.d.ts +45 -12
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +181 -72
- package/dist/lib/googleCloudStorage.d.ts +7 -8
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +21 -33
- package/dist/lib/thumbnailGenerator.d.ts +0 -1
- package/dist/lib/thumbnailGenerator.d.ts.map +1 -1
- package/dist/lib/thumbnailGenerator.js +3 -17
- package/dist/routers/_app.d.ts +476 -10
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +221 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +244 -16
- package/dist/routers/auth.js +1 -1
- package/dist/routers/file.d.ts +18 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/folder.d.ts +0 -1
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +12 -9
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +4 -2
- package/package.json +1 -1
- package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +57 -0
- package/prisma/schema.prisma +19 -0
- package/src/lib/fileUpload.ts +229 -83
- package/src/lib/googleCloudStorage.ts +21 -40
- package/src/lib/thumbnailGenerator.ts +3 -18
- package/src/routers/assignment.ts +292 -16
- package/src/routers/auth.ts +1 -1
- package/src/routers/folder.ts +13 -9
- package/src/routers/labChat.ts +4 -2
- package/src/routers/user.ts +1 -1
package/src/lib/fileUpload.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { TRPCError } from "@trpc/server";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { getSignedUrl, objectExists } from "./googleCloudStorage.js";
|
|
4
|
+
import { generateMediaThumbnail } from "./thumbnailGenerator.js";
|
|
5
5
|
import { prisma } from "./prisma.js";
|
|
6
|
+
import { logger } from "src/utils/logger.js";
|
|
6
7
|
|
|
7
8
|
export interface FileData {
|
|
8
9
|
name: string;
|
|
9
10
|
type: string;
|
|
10
11
|
size: number;
|
|
11
|
-
data
|
|
12
|
+
// No data field - for direct file uploads
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface DirectFileData {
|
|
@@ -27,13 +28,22 @@ export interface UploadedFile {
|
|
|
27
28
|
thumbnailId?: string;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
export interface DirectUploadFile {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
type: string;
|
|
35
|
+
size: number;
|
|
36
|
+
path: string;
|
|
37
|
+
uploadUrl: string;
|
|
38
|
+
uploadExpiresAt: Date;
|
|
39
|
+
uploadSessionId: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// DEPRECATED: These functions are no longer used - files are uploaded directly to GCS
|
|
43
|
+
// Use createDirectUploadFile() and createDirectUploadFiles() instead
|
|
44
|
+
|
|
30
45
|
/**
|
|
31
|
-
*
|
|
32
|
-
* @param file The file data to upload
|
|
33
|
-
* @param userId The ID of the user uploading the file
|
|
34
|
-
* @param directory Optional directory to store the file in
|
|
35
|
-
* @param assignmentId Optional assignment ID to associate the file with
|
|
36
|
-
* @returns The uploaded file record
|
|
46
|
+
* @deprecated Use createDirectUploadFile instead
|
|
37
47
|
*/
|
|
38
48
|
export async function uploadFile(
|
|
39
49
|
file: FileData,
|
|
@@ -41,6 +51,59 @@ export async function uploadFile(
|
|
|
41
51
|
directory?: string,
|
|
42
52
|
assignmentId?: string
|
|
43
53
|
): Promise<UploadedFile> {
|
|
54
|
+
throw new TRPCError({
|
|
55
|
+
code: 'NOT_IMPLEMENTED',
|
|
56
|
+
message: 'uploadFile is deprecated. Use createDirectUploadFile instead.',
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @deprecated Use createDirectUploadFiles instead
|
|
62
|
+
*/
|
|
63
|
+
export async function uploadFiles(
|
|
64
|
+
files: FileData[],
|
|
65
|
+
userId: string,
|
|
66
|
+
directory?: string
|
|
67
|
+
): Promise<UploadedFile[]> {
|
|
68
|
+
throw new TRPCError({
|
|
69
|
+
code: 'NOT_IMPLEMENTED',
|
|
70
|
+
message: 'uploadFiles is deprecated. Use createDirectUploadFiles instead.',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets a signed URL for a file
|
|
76
|
+
* @param filePath The path of the file in Google Cloud Storage
|
|
77
|
+
* @returns The signed URL
|
|
78
|
+
*/
|
|
79
|
+
export async function getFileUrl(filePath: string): Promise<string> {
|
|
80
|
+
try {
|
|
81
|
+
return await getSignedUrl(filePath);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('Error getting signed URL:', error);
|
|
84
|
+
throw new TRPCError({
|
|
85
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
86
|
+
message: 'Failed to get file URL',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Creates a file record for direct upload and generates signed URL
|
|
93
|
+
* @param file The file metadata (no base64 data)
|
|
94
|
+
* @param userId The ID of the user uploading the file
|
|
95
|
+
* @param directory Optional directory to store the file in
|
|
96
|
+
* @param assignmentId Optional assignment ID to associate the file with
|
|
97
|
+
* @param submissionId Optional submission ID to associate the file with
|
|
98
|
+
* @returns The direct upload file information with signed URL
|
|
99
|
+
*/
|
|
100
|
+
export async function createDirectUploadFile(
|
|
101
|
+
file: DirectFileData,
|
|
102
|
+
userId: string,
|
|
103
|
+
directory?: string,
|
|
104
|
+
assignmentId?: string,
|
|
105
|
+
submissionId?: string
|
|
106
|
+
): Promise<DirectUploadFile> {
|
|
44
107
|
try {
|
|
45
108
|
// Validate file extension matches MIME type
|
|
46
109
|
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
|
@@ -63,59 +126,30 @@ export async function uploadFile(
|
|
|
63
126
|
// Create a unique filename
|
|
64
127
|
const uniqueFilename = `${uuidv4()}.${fileExtension}`;
|
|
65
128
|
|
|
66
|
-
|
|
129
|
+
// Construct the full path
|
|
67
130
|
const filePath = directory
|
|
68
131
|
? `${directory}/${uniqueFilename}`
|
|
69
132
|
: uniqueFilename;
|
|
70
133
|
|
|
71
|
-
|
|
72
|
-
const
|
|
134
|
+
// Generate upload session ID
|
|
135
|
+
const uploadSessionId = uuidv4();
|
|
73
136
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// Handle both data URI format (data:image/jpeg;base64,...) and raw base64
|
|
79
|
-
const base64Data = file.data.includes(',') ? file.data.split(',')[1] : file.data;
|
|
80
|
-
const fileBuffer = Buffer.from(base64Data, 'base64');
|
|
81
|
-
|
|
82
|
-
// // Generate thumbnail directly from buffer
|
|
83
|
-
const thumbnailBuffer = await generateMediaThumbnail(fileBuffer, file.type);
|
|
84
|
-
if (thumbnailBuffer) {
|
|
85
|
-
// Store thumbnail in a thumbnails directory
|
|
86
|
-
const thumbnailPath = `thumbnails/${filePath}`;
|
|
87
|
-
const thumbnailBase64 = `data:image/jpeg;base64,${thumbnailBuffer.toString('base64')}`;
|
|
88
|
-
await uploadToGCS(thumbnailBase64, thumbnailPath, 'image/jpeg');
|
|
89
|
-
|
|
90
|
-
// Create thumbnail file record
|
|
91
|
-
const thumbnailFile = await prisma.file.create({
|
|
92
|
-
data: {
|
|
93
|
-
name: `${file.name}_thumb.jpg${Math.random()}`,
|
|
94
|
-
type: 'image/jpeg',
|
|
95
|
-
path: thumbnailPath,
|
|
96
|
-
// path: '/dummyPath' + Math.random().toString(36).substring(2, 15),
|
|
97
|
-
user: {
|
|
98
|
-
connect: { id: userId }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
thumbnailId = thumbnailFile.id;
|
|
104
|
-
}
|
|
105
|
-
} catch (error) {
|
|
106
|
-
console.warn('Failed to generate thumbnail:', error);
|
|
107
|
-
// Continue without thumbnail - this is not a critical failure
|
|
108
|
-
}
|
|
137
|
+
// Generate backend proxy upload URL (not direct GCS)
|
|
138
|
+
const baseUrl = process.env.BACKEND_URL || 'http://localhost:3001';
|
|
139
|
+
const uploadUrl = `${baseUrl}/api/upload/${encodeURIComponent(filePath)}`;
|
|
140
|
+
const uploadExpiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes from now
|
|
109
141
|
|
|
110
|
-
// Create file record in database
|
|
111
|
-
|
|
112
|
-
// const uploadedPath = '/dummyPath' + Math.random().toString(36).substring(2, 15);
|
|
142
|
+
// Create file record in database with PENDING status
|
|
113
143
|
const fileRecord = await prisma.file.create({
|
|
114
144
|
data: {
|
|
115
145
|
name: file.name,
|
|
116
146
|
type: file.type,
|
|
117
147
|
size: file.size,
|
|
118
|
-
path:
|
|
148
|
+
path: filePath,
|
|
149
|
+
uploadStatus: 'PENDING',
|
|
150
|
+
uploadUrl,
|
|
151
|
+
uploadExpiresAt,
|
|
152
|
+
uploadSessionId,
|
|
119
153
|
user: {
|
|
120
154
|
connect: { id: userId }
|
|
121
155
|
},
|
|
@@ -124,75 +158,187 @@ export async function uploadFile(
|
|
|
124
158
|
connect: {id: directory},
|
|
125
159
|
},
|
|
126
160
|
}),
|
|
127
|
-
...(thumbnailId && {
|
|
128
|
-
thumbnail: {
|
|
129
|
-
connect: { id: thumbnailId }
|
|
130
|
-
}
|
|
131
|
-
}),
|
|
132
161
|
...(assignmentId && {
|
|
133
162
|
assignment: {
|
|
134
163
|
connect: { id: assignmentId }
|
|
135
164
|
}
|
|
165
|
+
}),
|
|
166
|
+
...(submissionId && {
|
|
167
|
+
submission: {
|
|
168
|
+
connect: { id: submissionId }
|
|
169
|
+
}
|
|
136
170
|
})
|
|
137
171
|
},
|
|
138
172
|
});
|
|
139
173
|
|
|
140
|
-
// Return file information
|
|
141
174
|
return {
|
|
142
175
|
id: fileRecord.id,
|
|
143
176
|
name: file.name,
|
|
144
177
|
type: file.type,
|
|
145
178
|
size: file.size,
|
|
146
|
-
path:
|
|
147
|
-
|
|
179
|
+
path: filePath,
|
|
180
|
+
uploadUrl,
|
|
181
|
+
uploadExpiresAt,
|
|
182
|
+
uploadSessionId
|
|
148
183
|
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
logger.error('Error creating direct upload file:', {error: error instanceof Error ? {
|
|
186
|
+
name: error.name,
|
|
187
|
+
message: error.message,
|
|
188
|
+
stack: error.stack,
|
|
189
|
+
} : error});
|
|
190
|
+
throw new TRPCError({
|
|
191
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
192
|
+
message: 'Failed to create direct upload file',
|
|
193
|
+
});
|
|
149
194
|
}
|
|
150
|
-
|
|
151
|
-
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Confirms a direct upload was successful
|
|
199
|
+
* @param fileId The ID of the file record
|
|
200
|
+
* @param uploadSuccess Whether the upload was successful
|
|
201
|
+
* @param errorMessage Optional error message if upload failed
|
|
202
|
+
*/
|
|
203
|
+
export async function confirmDirectUpload(
|
|
204
|
+
fileId: string,
|
|
205
|
+
uploadSuccess: boolean,
|
|
206
|
+
errorMessage?: string
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
try {
|
|
209
|
+
// First fetch the file record to get the object path
|
|
210
|
+
const fileRecord = await prisma.file.findUnique({
|
|
211
|
+
where: { id: fileId },
|
|
212
|
+
select: { path: true }
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!fileRecord) {
|
|
216
|
+
throw new TRPCError({
|
|
217
|
+
code: 'NOT_FOUND',
|
|
218
|
+
message: 'File record not found',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let actualUploadSuccess = uploadSuccess;
|
|
223
|
+
let actualErrorMessage = errorMessage;
|
|
224
|
+
|
|
225
|
+
// If uploadSuccess is true, verify the object actually exists in GCS
|
|
226
|
+
if (uploadSuccess) {
|
|
227
|
+
try {
|
|
228
|
+
const exists = await objectExists(process.env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
|
|
229
|
+
if (!exists) {
|
|
230
|
+
actualUploadSuccess = false;
|
|
231
|
+
actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
|
|
232
|
+
logger.error(`File upload verification failed for ${fileId}: object ${fileRecord.path} not found in GCS`);
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
logger.error(`Error verifying file existence in GCS for ${fileId}:`, {error: error instanceof Error ? {
|
|
236
|
+
name: error.name,
|
|
237
|
+
message: error.message,
|
|
238
|
+
stack: error.stack,
|
|
239
|
+
} : error});
|
|
240
|
+
actualUploadSuccess = false;
|
|
241
|
+
actualErrorMessage = 'Failed to verify file existence in Google Cloud Storage';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const updateData: any = {
|
|
246
|
+
uploadStatus: actualUploadSuccess ? 'COMPLETED' : 'FAILED',
|
|
247
|
+
uploadProgress: actualUploadSuccess ? 100 : 0,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (!actualUploadSuccess && actualErrorMessage) {
|
|
251
|
+
updateData.uploadError = actualErrorMessage;
|
|
252
|
+
updateData.uploadRetryCount = { increment: 1 };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (actualUploadSuccess) {
|
|
256
|
+
updateData.uploadedAt = new Date();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await prisma.file.update({
|
|
260
|
+
where: { id: fileId },
|
|
261
|
+
data: updateData
|
|
262
|
+
});
|
|
263
|
+
} catch (error) {
|
|
264
|
+
logger.error('Error confirming direct upload:', {error});
|
|
152
265
|
throw new TRPCError({
|
|
153
266
|
code: 'INTERNAL_SERVER_ERROR',
|
|
154
|
-
message: 'Failed to upload
|
|
267
|
+
message: 'Failed to confirm upload',
|
|
155
268
|
});
|
|
156
269
|
}
|
|
157
270
|
}
|
|
158
271
|
|
|
159
272
|
/**
|
|
160
|
-
*
|
|
161
|
-
* @param
|
|
162
|
-
* @param
|
|
163
|
-
* @param directory Optional subdirectory to store the files in
|
|
164
|
-
* @returns Array of uploaded file information
|
|
273
|
+
* Updates upload progress for a direct upload
|
|
274
|
+
* @param fileId The ID of the file record
|
|
275
|
+
* @param progress Progress percentage (0-100)
|
|
165
276
|
*/
|
|
166
|
-
export async function
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
): Promise<UploadedFile[]> {
|
|
277
|
+
export async function updateUploadProgress(
|
|
278
|
+
fileId: string,
|
|
279
|
+
progress: number
|
|
280
|
+
): Promise<void> {
|
|
171
281
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
282
|
+
// await prisma.file.update({
|
|
283
|
+
// where: { id: fileId },
|
|
284
|
+
// data: {
|
|
285
|
+
// uploadStatus: 'UPLOADING',
|
|
286
|
+
// uploadProgress: Math.min(100, Math.max(0, progress))
|
|
287
|
+
// }
|
|
288
|
+
// });
|
|
289
|
+
const current = await prisma.file.findUnique({ where: { id: fileId }, select: { uploadStatus: true } });
|
|
290
|
+
if (!current || ['COMPLETED','FAILED','CANCELLED'].includes(current.uploadStatus as string)) return;
|
|
291
|
+
const clamped = Math.min(100, Math.max(0, progress));
|
|
292
|
+
await prisma.file.update({
|
|
293
|
+
where: { id: fileId },
|
|
294
|
+
data: {
|
|
295
|
+
uploadStatus: 'UPLOADING',
|
|
296
|
+
uploadProgress: clamped
|
|
297
|
+
}
|
|
298
|
+
});
|
|
174
299
|
} catch (error) {
|
|
175
|
-
|
|
300
|
+
logger.error('Error updating upload progress:', {error: error instanceof Error ? {
|
|
301
|
+
name: error.name,
|
|
302
|
+
message: error.message,
|
|
303
|
+
stack: error.stack,
|
|
304
|
+
} : error});
|
|
176
305
|
throw new TRPCError({
|
|
177
306
|
code: 'INTERNAL_SERVER_ERROR',
|
|
178
|
-
message: 'Failed to upload
|
|
307
|
+
message: 'Failed to update upload progress',
|
|
179
308
|
});
|
|
180
309
|
}
|
|
181
310
|
}
|
|
182
311
|
|
|
183
312
|
/**
|
|
184
|
-
*
|
|
185
|
-
* @param
|
|
186
|
-
* @
|
|
313
|
+
* Creates multiple direct upload files
|
|
314
|
+
* @param files Array of file metadata
|
|
315
|
+
* @param userId The ID of the user uploading the files
|
|
316
|
+
* @param directory Optional subdirectory to store the files in
|
|
317
|
+
* @param assignmentId Optional assignment ID to associate files with
|
|
318
|
+
* @param submissionId Optional submission ID to associate files with
|
|
319
|
+
* @returns Array of direct upload file information
|
|
187
320
|
*/
|
|
188
|
-
export async function
|
|
321
|
+
export async function createDirectUploadFiles(
|
|
322
|
+
files: DirectFileData[],
|
|
323
|
+
userId: string,
|
|
324
|
+
directory?: string,
|
|
325
|
+
assignmentId?: string,
|
|
326
|
+
submissionId?: string
|
|
327
|
+
): Promise<DirectUploadFile[]> {
|
|
189
328
|
try {
|
|
190
|
-
|
|
329
|
+
const uploadPromises = files.map(file =>
|
|
330
|
+
createDirectUploadFile(file, userId, directory, assignmentId, submissionId)
|
|
331
|
+
);
|
|
332
|
+
return await Promise.all(uploadPromises);
|
|
191
333
|
} catch (error) {
|
|
192
|
-
|
|
334
|
+
logger.error('Error creating direct upload files:', {error: error instanceof Error ? {
|
|
335
|
+
name: error.name,
|
|
336
|
+
message: error.message,
|
|
337
|
+
stack: error.stack,
|
|
338
|
+
} : error});
|
|
193
339
|
throw new TRPCError({
|
|
194
340
|
code: 'INTERNAL_SERVER_ERROR',
|
|
195
|
-
message: 'Failed to
|
|
341
|
+
message: 'Failed to create direct upload files',
|
|
196
342
|
});
|
|
197
343
|
}
|
|
198
344
|
}
|
|
@@ -16,46 +16,8 @@ export const bucket = storage.bucket(process.env.GOOGLE_CLOUD_BUCKET_NAME!);
|
|
|
16
16
|
// Short expiration time for signed URLs (5 minutes)
|
|
17
17
|
const SIGNED_URL_EXPIRATION = 5 * 60 * 1000;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* @param base64Data Base64 encoded file data
|
|
22
|
-
* @param filePath The path where the file should be stored
|
|
23
|
-
* @param contentType The MIME type of the file
|
|
24
|
-
* @returns The path of the uploaded file
|
|
25
|
-
*/
|
|
26
|
-
export async function uploadFile(
|
|
27
|
-
base64Data: string,
|
|
28
|
-
filePath: string,
|
|
29
|
-
contentType: string
|
|
30
|
-
): Promise<string> {
|
|
31
|
-
try {
|
|
32
|
-
// Remove the data URL prefix if present
|
|
33
|
-
const base64Content = base64Data.includes('base64,')
|
|
34
|
-
? base64Data.split('base64,')[1]
|
|
35
|
-
: base64Data;
|
|
36
|
-
|
|
37
|
-
// Convert base64 to buffer
|
|
38
|
-
const fileBuffer = Buffer.from(base64Content, 'base64');
|
|
39
|
-
|
|
40
|
-
// Create a new file in the bucket
|
|
41
|
-
const file = bucket.file(filePath);
|
|
42
|
-
|
|
43
|
-
// Upload the file
|
|
44
|
-
await file.save(fileBuffer, {
|
|
45
|
-
metadata: {
|
|
46
|
-
contentType,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
return filePath;
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.error('Error uploading to Google Cloud Storage:', error);
|
|
53
|
-
throw new TRPCError({
|
|
54
|
-
code: 'INTERNAL_SERVER_ERROR',
|
|
55
|
-
message: 'Failed to upload file to storage',
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
19
|
+
// DEPRECATED: This function is no longer used - files are uploaded directly to GCS
|
|
20
|
+
// The backend proxy upload endpoint in index.ts handles direct uploads
|
|
59
21
|
|
|
60
22
|
/**
|
|
61
23
|
* Gets a signed URL for a file
|
|
@@ -100,4 +62,23 @@ export async function deleteFile(filePath: string): Promise<void> {
|
|
|
100
62
|
message: 'Failed to delete file from storage',
|
|
101
63
|
});
|
|
102
64
|
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if an object exists in Google Cloud Storage
|
|
69
|
+
* @param bucketName The name of the bucket (unused, uses default bucket)
|
|
70
|
+
* @param objectPath The path of the object to check
|
|
71
|
+
* @returns Promise<boolean> True if the object exists, false otherwise
|
|
72
|
+
*/
|
|
73
|
+
export async function objectExists(bucketName: string, objectPath: string): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
const [exists] = await bucket.file(objectPath).exists();
|
|
76
|
+
return exists;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error checking if object exists in Google Cloud Storage:', error);
|
|
79
|
+
throw new TRPCError({
|
|
80
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
81
|
+
message: 'Failed to check object existence',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
103
84
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import sharp from 'sharp';
|
|
2
2
|
import { prisma } from './prisma.js';
|
|
3
|
-
import {
|
|
3
|
+
import { deleteFile, getSignedUrl } from './googleCloudStorage.js';
|
|
4
4
|
|
|
5
5
|
// Thumbnail size configuration
|
|
6
6
|
const THUMBNAIL_WIDTH = 200;
|
|
@@ -166,20 +166,5 @@ export async function generateThumbnail(fileName: string, fileType: string): Pro
|
|
|
166
166
|
* @param userId The user ID who owns the file
|
|
167
167
|
* @returns The ID of the created thumbnail File
|
|
168
168
|
*/
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const base64Data = `data:image/jpeg;base64,${thumbnailBuffer.toString('base64')}`;
|
|
172
|
-
const thumbnailFileName = await uploadFile(base64Data, `thumbnails/${originalFileName}_thumb`, 'image/jpeg');
|
|
173
|
-
|
|
174
|
-
// Create a new File entry for the thumbnail
|
|
175
|
-
const newThumbnail = await prisma.file.create({
|
|
176
|
-
data: {
|
|
177
|
-
name: `${originalFileName}_thumb.jpg`,
|
|
178
|
-
path: thumbnailFileName,
|
|
179
|
-
type: 'image/jpeg',
|
|
180
|
-
userId: userId,
|
|
181
|
-
},
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
return newThumbnail.id;
|
|
185
|
-
}
|
|
169
|
+
// DEPRECATED: This function is no longer used - thumbnails are generated during direct uploads
|
|
170
|
+
// Thumbnail generation is now handled in the direct upload flow
|