chore: move backend to hono + typescript (#558)
* feat: setup secure presigned uploads and downloads * clean up * update lock * Get presigned url finally working * 557: Migrate backend to hono + ts * chore: update Dockerfile and package.json for backend configuration * add dev command * remove unused packages * Enhance backend API and frontend components for course management - Updated CORS configuration in the backend to allow specific methods and headers. - Refactored lesson download endpoint to use a new route for PDF downloads. - Removed unused certificate download functionality from the lesson router. - Updated frontend components to reflect changes in API endpoints and improve video upload functionality. - Introduced new video upload component and adjusted video handling in lesson materials. - Cleaned up unnecessary imports and optimized component structure for better performance. * PR cleanups * Enhance email configuration by adding SMTP_SENDER environment variable - Updated docker-compose.yaml to include SMTP_SENDER. - Modified Dockerfile to accept SMTP_SENDER as an argument. - Updated environment schema in env.ts to define SMTP_SENDER as optional. - Adjusted mail service to use SMTP_SENDER for the email 'from' field. - Updated documentation to reflect the new SMTP_SENDER variable. * Remove debug log statement from sendEmail function --------- Co-authored-by: Iniubong Obonguko <[email protected]>
This commit is contained in:
co-authored by
Iniubong Obonguko
parent
9eb7b01587
commit
82be557a42
@@ -0,0 +1,91 @@
|
||||
import { Context, Hono } from 'hono';
|
||||
import { ZCertificateDownload, ZCourseDownloadContent } from '$src/types/course';
|
||||
|
||||
import { generateCertificate } from '$src/utils/certificate';
|
||||
import { generateCoursePdf } from '$src/utils/course';
|
||||
import { katexRouter } from '$src/routes/course/katex';
|
||||
import { lessonRouter } from '$src/routes/course/lesson';
|
||||
import { presignRouter } from '$src/routes/course/presign';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const courseRouter = new Hono();
|
||||
|
||||
courseRouter.route('/katex', katexRouter);
|
||||
courseRouter.route('/lesson', lessonRouter);
|
||||
courseRouter.route('/presign', presignRouter);
|
||||
|
||||
courseRouter.post('/download/certificate', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
|
||||
const validatedData = ZCertificateDownload.parse(data);
|
||||
|
||||
const result = await generateCertificate(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
c.header(
|
||||
'Content-Disposition',
|
||||
`attachment; filename="certificate-${validatedData.courseName}.pdf"`
|
||||
);
|
||||
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(result);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: error.errors
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Error generating certificate:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
courseRouter.post('/download/content', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
const validatedData = ZCourseDownloadContent.parse(data);
|
||||
|
||||
const pdfBuffer = await generateCoursePdf(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(pdfBuffer);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error generating course PDF:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Context, Hono } from 'hono';
|
||||
|
||||
import katex from 'katex';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const katexRouter = new Hono();
|
||||
|
||||
const katexSchema = z.object({
|
||||
latex: z.string().min(1),
|
||||
displayMode: z.boolean().optional()
|
||||
});
|
||||
|
||||
katexRouter.post('/', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
const validatedData = katexSchema.parse(data);
|
||||
|
||||
const html = katex.renderToString(validatedData.latex, {
|
||||
displayMode: validatedData.displayMode ?? false,
|
||||
throwOnError: false
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
html
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: error.errors
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Error rendering LaTeX:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Context, Hono } from 'hono';
|
||||
import { ZCertificateDownload, ZLessonDownloadContent } from '$src/types/course/lesson';
|
||||
|
||||
import { generateLessonPdf } from '$src/utils/lesson';
|
||||
|
||||
export const lessonRouter = new Hono();
|
||||
|
||||
lessonRouter.post('/download/pdf', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
const validatedData = ZLessonDownloadContent.parse(data);
|
||||
|
||||
const pdfBuffer = await generateLessonPdf(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(pdfBuffer);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error generating lesson PDF:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||
import { ZCourseDownloadPresignedUrl, ZCoursePresignUrlUpload } from '$src/types/course';
|
||||
|
||||
import { CLOUDFLARE } from '$src/constants';
|
||||
import type { GetSignedUrlParameters } from '$src/utils/s3';
|
||||
import { Hono } from 'hono';
|
||||
import { authMiddleware } from '$src/middlewares/auth';
|
||||
import { generateFileKey } from '$src/utils/upload';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { s3Client } from '$src/utils/s3';
|
||||
|
||||
export const presignRouter = new Hono();
|
||||
|
||||
presignRouter.post('/upload', authMiddleware, async (c) => {
|
||||
const body = await c.req.json();
|
||||
const result = ZCoursePresignUrlUpload.parse(body);
|
||||
|
||||
const { fileName, fileType } = result;
|
||||
const fileKey = generateFileKey(fileName);
|
||||
|
||||
try {
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: CLOUDFLARE.R2.BUCKET,
|
||||
Key: fileKey,
|
||||
ContentType: fileType
|
||||
}) as GetSignedUrlParameters[1];
|
||||
|
||||
const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, {
|
||||
expiresIn: CLOUDFLARE.R2.PRESIGN_EXPIRATION_TIME
|
||||
});
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
url: presignedUrl,
|
||||
fileKey,
|
||||
message: 'Pre-signed URL generated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating pre-signed URL:', error);
|
||||
|
||||
if (error instanceof Error && error.name === 'SignatureDoesNotMatch') {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
type: 'SIGNATURE_ERROR',
|
||||
message: 'Invalid signature for the request'
|
||||
},
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
type: 'INTERNAL_ERROR',
|
||||
message: 'Error generating pre-signed URL'
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
presignRouter.post('/download', authMiddleware, async (c) => {
|
||||
const body = await c.req.json();
|
||||
const result = ZCourseDownloadPresignedUrl.parse(body);
|
||||
|
||||
const { keys } = result;
|
||||
|
||||
try {
|
||||
const signedUrls: Record<string, string> = {};
|
||||
|
||||
await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: CLOUDFLARE.R2.BUCKET,
|
||||
Key: key
|
||||
}) as GetSignedUrlParameters[1];
|
||||
|
||||
const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, {
|
||||
expiresIn: CLOUDFLARE.R2.DOWNLOAD_EXPIRATION_TIME
|
||||
});
|
||||
|
||||
signedUrls[key] = presignedUrl;
|
||||
})
|
||||
);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
urls: signedUrls,
|
||||
message: 'Video URLs retrieved successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error Retrieving Video:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
type: 'INTERNAL_ERROR',
|
||||
message: 'Error Retrieving Video'
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,35 +0,0 @@
|
||||
const express = require('express');
|
||||
const zod = require('zod');
|
||||
const { generateCertificate } = require('../utils/certificate');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// Validate
|
||||
const mySchema = zod.object({
|
||||
theme: zod.string(),
|
||||
studentName: zod.string(),
|
||||
courseName: zod.string(),
|
||||
courseDescription: zod.string(),
|
||||
orgLogoUrl: zod.string(),
|
||||
orgName: zod.string(),
|
||||
});
|
||||
console.log('req.body', req.body);
|
||||
|
||||
mySchema.parse(req.body);
|
||||
|
||||
const pdfBuffer = await generateCertificate(req.body);
|
||||
|
||||
res.contentType('application/pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Error', error);
|
||||
res.json({
|
||||
success: false,
|
||||
error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,42 +0,0 @@
|
||||
const express = require('express');
|
||||
const zod = require('zod');
|
||||
const { generateCoursePdf } = require('../utils/course.js');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// localhost:3002/downloadCourse
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// Validate
|
||||
const mySchema = zod.object({
|
||||
courseTitle: zod.string(),
|
||||
orgName: zod.string(),
|
||||
orgTheme: zod.string(),
|
||||
lessons: zod.array(
|
||||
zod.object({
|
||||
lessonTitle: zod.string(),
|
||||
lessonNumber: zod.string(),
|
||||
lessonNote: zod.string(),
|
||||
slideUrl: zod.string().optional(),
|
||||
video: zod.string().array().optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
console.log('req.body', req.body);
|
||||
|
||||
mySchema.parse(req.body);
|
||||
|
||||
const pdfBuffer = await generateCoursePdf(req.body);
|
||||
|
||||
res.contentType('application/pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Error', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,47 +0,0 @@
|
||||
const express = require('express');
|
||||
const zod = require('zod');
|
||||
const { generateLessonPdf } = require('../utils/lesson');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const lesson = {
|
||||
title: '',
|
||||
number: '01',
|
||||
orgName: '',
|
||||
note: '',
|
||||
slideUrl: '',
|
||||
video: [],
|
||||
courseTitle: '',
|
||||
};
|
||||
|
||||
// localhost:3002/downloadLesson
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
// Validate
|
||||
const mySchema = zod.object({
|
||||
title: zod.string(),
|
||||
number: zod.string(),
|
||||
orgName: zod.string(),
|
||||
note: zod.string(),
|
||||
slideUrl: zod.string().optional(),
|
||||
video: zod.string().array().optional(),
|
||||
courseTitle: zod.string(),
|
||||
});
|
||||
console.log('req.body', req.body);
|
||||
|
||||
mySchema.parse(req.body);
|
||||
|
||||
const pdfBuffer = await generateLessonPdf(req.body);
|
||||
|
||||
res.contentType('application/pdf');
|
||||
res.send(pdfBuffer);
|
||||
} catch (error) {
|
||||
console.error('Error', error);
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,31 +0,0 @@
|
||||
const express = require('express');
|
||||
// https://ben.page/node-latex-to-html
|
||||
const katex = require('katex');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// localhost:3002/katex
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const original = req._parsedUrl.search;
|
||||
|
||||
const latexString = original
|
||||
.replace('?', '')
|
||||
.replaceAll('+', '+')
|
||||
.replaceAll('&space;', '');
|
||||
console.log({ latexString });
|
||||
|
||||
const html = katex.renderToString(latexString, {
|
||||
throwOnError: false,
|
||||
output: 'mathml'
|
||||
});
|
||||
|
||||
console.log({ html });
|
||||
|
||||
res.send(html);
|
||||
} catch (error) {
|
||||
res.send('failed');
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Context, Hono } from 'hono';
|
||||
import { sendWithNodemailer, sendWithZoho } from '$src/services/mail';
|
||||
|
||||
import { ZSendEmailValidation } from '$src/types/mail';
|
||||
import { env } from '$src/config/env';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const mailRouter = new Hono();
|
||||
|
||||
mailRouter.post('/send', async (c: Context) => {
|
||||
try {
|
||||
const data = await c.req.json();
|
||||
const validatedData = ZSendEmailValidation.parse(data);
|
||||
|
||||
const results = await Promise.all(
|
||||
validatedData.map(async (emailData) => {
|
||||
try {
|
||||
const res = env.ZOHO_TOKEN
|
||||
? await sendWithZoho(emailData)
|
||||
: await sendWithNodemailer(emailData);
|
||||
|
||||
console.log('Email status:', res);
|
||||
return res;
|
||||
} catch (error) {
|
||||
console.error('Error sending email:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const hasErrors = results.some((result) => !result.success);
|
||||
if (hasErrors) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Some emails failed to send',
|
||||
details: results
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
return c.json({ success: true, details: results });
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Validation error',
|
||||
details: error.errors
|
||||
},
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
console.error('Error sending emails:', error);
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error occurred',
|
||||
details: error
|
||||
},
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
const express = require('express');
|
||||
const zod = require('zod');
|
||||
const { nodemailerTransporter, zohoClient } = require('../utils/email');
|
||||
const { withEmailTemplate } = require('../utils/withEmailTemplate');
|
||||
const { ZOHO_TOKEN } = process.env;
|
||||
|
||||
const emailRouter = express.Router();
|
||||
|
||||
let transporter;
|
||||
|
||||
nodemailerTransporter().then((t) => {
|
||||
transporter = t;
|
||||
});
|
||||
|
||||
async function sendWithNodemailer(emailData) {
|
||||
const { from, to, subject, content, replyTo } = emailData;
|
||||
|
||||
if (!transporter) {
|
||||
return;
|
||||
}
|
||||
|
||||
return await transporter.sendMail({
|
||||
from: from || '"Best from ClassroomIO" <[email protected]>',
|
||||
to,
|
||||
subject,
|
||||
replyTo,
|
||||
html: withEmailTemplate(content)
|
||||
});
|
||||
}
|
||||
|
||||
// format: "ClassroomIO Developers (via ClassroomIO.com)" <[email protected]>
|
||||
function extractNameAndEmail(str) {
|
||||
// Use regular expressions to match the name and email
|
||||
const regex = /"(.*?)"\s+<\s*(.*?)@(.*?)\s*>/;
|
||||
const match = str.match(regex);
|
||||
|
||||
if (match) {
|
||||
// Extract the name and email from the match groups
|
||||
const name = match[1];
|
||||
const email = match[2] + '@' + match[3];
|
||||
return { name, email };
|
||||
} else {
|
||||
// Return undefined if the format doesn't match
|
||||
return { name: from, email: from };
|
||||
}
|
||||
}
|
||||
|
||||
async function sendWithZoho(emailData) {
|
||||
const { from, to, subject, content } = emailData;
|
||||
|
||||
const fromData = extractNameAndEmail(from);
|
||||
|
||||
return zohoClient.sendMail({
|
||||
from: {
|
||||
address: fromData.email,
|
||||
name: fromData.name
|
||||
},
|
||||
to: [
|
||||
{
|
||||
email_address: {
|
||||
address: to
|
||||
}
|
||||
}
|
||||
],
|
||||
subject,
|
||||
htmlbody: content
|
||||
});
|
||||
}
|
||||
|
||||
emailRouter.post('/', async (req, res) => {
|
||||
try {
|
||||
const mySchema = zod.array(
|
||||
zod.object({
|
||||
from: zod.string().optional(),
|
||||
to: zod.string(),
|
||||
subject: zod.string(),
|
||||
content: zod.string(),
|
||||
replyTo: zod.string().optional()
|
||||
})
|
||||
);
|
||||
|
||||
mySchema.parse(req.body);
|
||||
|
||||
const emailDataArray = req.body;
|
||||
|
||||
Promise.all(
|
||||
emailDataArray.map(async (emailData) => {
|
||||
let res;
|
||||
|
||||
try {
|
||||
if (!ZOHO_TOKEN) {
|
||||
res = await sendWithNodemailer(emailData);
|
||||
} else {
|
||||
res = await sendWithZoho(emailData);
|
||||
}
|
||||
|
||||
console.log('Email status', res);
|
||||
} catch (error) {
|
||||
console.error('Error sending email', error, error?.error?.details?.[0]);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
console.log('Email sent');
|
||||
return res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error sending emails:', error);
|
||||
return res.status(400).json({ error: `Bad request - ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = emailRouter;
|
||||
@@ -1,278 +0,0 @@
|
||||
const express = require('express');
|
||||
const FormData = require('form-data');
|
||||
const axios = require('axios');
|
||||
|
||||
const { promisify } = require('util');
|
||||
const fs = require('fs');
|
||||
const unlinkAsync = promisify(fs.unlink);
|
||||
|
||||
const { Upload } = require('@aws-sdk/lib-storage');
|
||||
const {
|
||||
S3Client
|
||||
// PutObjectCommand
|
||||
// GetObjectCommand,
|
||||
} = require('@aws-sdk/client-s3');
|
||||
|
||||
const { getSupabase } = require('../utils/supabase');
|
||||
|
||||
const genUniqueId = require('../utils/genUniqueId');
|
||||
const upload = require('../utils/upload');
|
||||
|
||||
const {
|
||||
CLOUDFLARE_BUCKET_DOMAIN,
|
||||
CLOUDFLARE_ACCESS_KEY,
|
||||
CLOUDFLARE_SECRET_ACCESS_KEY,
|
||||
CLOUDFLARE_ACCOUNT_ID,
|
||||
CLOUDFLARE_BUCKET_ID,
|
||||
MUSE_API_KEY
|
||||
} = process.env;
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const S3 = new S3Client({
|
||||
region: 'auto',
|
||||
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
||||
credentials: {
|
||||
accessKeyId: CLOUDFLARE_ACCESS_KEY,
|
||||
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY
|
||||
}
|
||||
});
|
||||
|
||||
const supabase = getSupabase();
|
||||
|
||||
// const museUploadQueue = new Queue('video', process.env.REDIS_API_KEY, {
|
||||
// redis: { tls: { rejectUnauthorized: false } },
|
||||
// });
|
||||
|
||||
router.post('/', upload.single('videoFile'), async (req, res) => {
|
||||
// Set timeout for this request to 15 minutes
|
||||
req.setTimeout(900000);
|
||||
|
||||
const {
|
||||
file,
|
||||
query: { lessonId }
|
||||
} = req;
|
||||
console.log('lessonId', lessonId);
|
||||
console.log('file', file);
|
||||
|
||||
if (!file?.fieldname || !file?.fieldname) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'You must provide a file to upload'
|
||||
});
|
||||
}
|
||||
|
||||
// File upload limit is 500MB
|
||||
const fileSizeInMegabytes = file?.size / (1024 * 1024);
|
||||
|
||||
if (fileSizeInMegabytes > 500) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
type: 'FILE_TOO_LARGE',
|
||||
message: 'File is too large'
|
||||
});
|
||||
}
|
||||
const fileData = fs.readFileSync(file.path);
|
||||
|
||||
const fileName = `${genUniqueId()}-${file.originalname.split(' ').join('-')}`;
|
||||
|
||||
const fileOrigin = CLOUDFLARE_BUCKET_DOMAIN ?? `https://pub-${CLOUDFLARE_BUCKET_ID}.r2.dev`;
|
||||
|
||||
const fileUrl = `${fileOrigin}/${fileName}`;
|
||||
let metadata = {
|
||||
sizeInMb: fileSizeInMegabytes
|
||||
};
|
||||
|
||||
try {
|
||||
console.log('Upload starting');
|
||||
|
||||
console.time('upload');
|
||||
const parallelUploads3 = new Upload({
|
||||
client: S3,
|
||||
queueSize: 5,
|
||||
partSize: 1024 * 1024 * 20,
|
||||
leavePartsOnError: false, // optional manually handle dropped parts
|
||||
params: {
|
||||
Bucket: 'videos',
|
||||
Key: fileName,
|
||||
Body: fileData
|
||||
}
|
||||
});
|
||||
|
||||
parallelUploads3.on('httpUploadProgress', (progress) => {
|
||||
const uploadProgress = Math.round(((progress.loaded / progress.total) * 100) / 2);
|
||||
console.log({ uploadProgress });
|
||||
const channel = supabase.channel('upload-progress');
|
||||
channel.subscribe((status) => {
|
||||
if (status === 'SUBSCRIBED') {
|
||||
channel.send({
|
||||
type: 'broadcast',
|
||||
event: lessonId,
|
||||
payload: uploadProgress
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await parallelUploads3.done();
|
||||
|
||||
console.log('Upload to done');
|
||||
console.timeEnd('upload');
|
||||
|
||||
unlinkAsync(file.path);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
url: fileUrl,
|
||||
metadata,
|
||||
message: 'Uploaded successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
// error handling.
|
||||
const { requestId, cfId, extendedRequestId } = error.$$metadata || {};
|
||||
|
||||
console.error('Upload failed', error);
|
||||
console.log({ requestId, cfId, extendedRequestId });
|
||||
|
||||
res.json({
|
||||
success: false,
|
||||
message: 'Uploaded failed: ' + error
|
||||
});
|
||||
} finally {
|
||||
// finally.
|
||||
console.log('Upload complete');
|
||||
|
||||
// console.log('Add muse queue');
|
||||
// museUploadQueue.add({
|
||||
// fileName,
|
||||
// originalFileName: file.originalname,
|
||||
// lessonId,
|
||||
// fileUrl,
|
||||
// unlinkPath: file.path,
|
||||
// });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', (req, res) => {
|
||||
res.send('Get upload video');
|
||||
});
|
||||
|
||||
async function uploadToMuseNow(fileData, fileName) {
|
||||
try {
|
||||
// Add file data to form
|
||||
const form = new FormData();
|
||||
form.append('file', fileData, fileName);
|
||||
|
||||
// Send to muse through axios - fetch is a pain in the ass
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
url: 'https://api.muse.ai/api/files/upload',
|
||||
data: form,
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
headers: {
|
||||
Key: MUSE_API_KEY
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
metadata: response.data
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('Upload to Muse failed', error?.response?.data);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Uploads video to muse.ai and store response in DB
|
||||
// *
|
||||
// * @param {Blod} s3Data AWS s3 data
|
||||
// * @param {string} fileName Name of the file
|
||||
// * @param {string} lessonId Associated lesson this video was uploaded to
|
||||
// * @param {string} fileUrl URL to the video in s3
|
||||
// * @param {string} unlinkPath Path to mutler file to delete
|
||||
// */
|
||||
// async function uploadToMuse(s3Key, fileName, lessonId, fileUrl, unlinkPath) {
|
||||
// try {
|
||||
// await unlinkAsync(unlinkPath);
|
||||
|
||||
// const s3Data = await S3.send(
|
||||
// new GetObjectCommand({
|
||||
// Bucket: 'videos',
|
||||
// Key: s3Key,
|
||||
// })
|
||||
// );
|
||||
// // Add file data to form
|
||||
// const form = new FormData();
|
||||
// form.append('file', s3Data.Body, fileName);
|
||||
|
||||
// // Send to muse through axios - fetch is a pain in the ass
|
||||
// const response = await axios({
|
||||
// method: 'POST',
|
||||
// url: 'https://api.muse.ai/api/files/upload',
|
||||
// data: form,
|
||||
// maxContentLength: Infinity,
|
||||
// maxBodyLength: Infinity,
|
||||
// headers: {
|
||||
// Key: MUSE_API_KEY,
|
||||
// },
|
||||
// });
|
||||
|
||||
// console.log('Upload to muse complete', response.data);
|
||||
// const { data: lesson } = await supabase
|
||||
// .from('lesson')
|
||||
// .select(`*`)
|
||||
// .eq('id', lessonId)
|
||||
// .single();
|
||||
|
||||
// if (lesson) {
|
||||
// console.log('Found lesson');
|
||||
// // Add Muse response to metadata
|
||||
// const videos = lesson.videos.map((v) => {
|
||||
// if (v.link === fileUrl) {
|
||||
// console.log('matches');
|
||||
// v.metadata = JSON.parse(JSON.stringify(response.data));
|
||||
// }
|
||||
// return v;
|
||||
// });
|
||||
|
||||
// // Save uploaded data to DB
|
||||
// const { error } = await supabase
|
||||
// .from('lesson')
|
||||
// .update({ videos })
|
||||
// .eq('id', lessonId)
|
||||
// .select();
|
||||
// if (error) {
|
||||
// console.log('Error updating lesson', error);
|
||||
// } else {
|
||||
// console.log('Update DB complete');
|
||||
// }
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.log('Upload to Muse failed', error?.response?.data);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Queue to Upload to muse.ai
|
||||
// museUploadQueue.process(async function (job, done) {
|
||||
// console.log('Initilizing Muse Request\n\n', job.data);
|
||||
// const { fileName, originalFileName, lessonId, fileUrl, unlinkPath } =
|
||||
// job.data;
|
||||
|
||||
// await uploadToMuse(fileName, originalFileName, lessonId, fileUrl, unlinkPath);
|
||||
|
||||
// done();
|
||||
// });
|
||||
|
||||
// museUploadQueue.on('completed', function (job) {
|
||||
// // Job completed with output result!
|
||||
// console.log('Completed all jobs in uploading to muse', job.data);
|
||||
// });
|
||||
|
||||
// museUploadQueue.on('error', function (error) {
|
||||
// // Job completed with output result!
|
||||
// console.log('error on queue', error);
|
||||
// });
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user