diff --git a/src/exams/pdf/group.test.report.tsx b/src/exams/pdf/group.test.report.tsx
index 69791db3..7e91694f 100644
--- a/src/exams/pdf/group.test.report.tsx
+++ b/src/exams/pdf/group.test.report.tsx
@@ -1,30 +1,252 @@
/* eslint-disable jsx-a11y/alt-text */
import React from "react";
-import { Document, Page, View, Text, Image } from "@react-pdf/renderer";
+import {
+ Document,
+ Page,
+ View,
+ Text,
+ Image,
+ StyleSheet,
+} from "@react-pdf/renderer";
import { styles } from "./styles";
+import TestReportFooter from "./test.report.footer";
+import { ModuleScore } from "@/interfaces/module.scores";
+import ProgressBar from "./progress.bar";
+// import { Font } from "@react-pdf/renderer";
-import { StyleSheet } from "@react-pdf/renderer";
-
+// Font.registerHyphenationCallback((word) => [word]);
interface Props {
-// date: string;
-// name: string;
-// email: string;
-// id: string;
-// gender?: string;
-// testDetails: ModuleScore[];
-// summary: string;
-// logo: string;
-// qrcode: string;
-// renderDetails: React.ReactNode;
-// title: string;
+ date: string;
+ name: string;
+ email: string;
+ id: string;
+ gender?: string;
+ testDetails: ModuleScore[];
+ summary: string;
+ logo: string;
+ qrcode: string;
+ renderDetails: React.ReactNode;
+ title: string;
+ numberOfStudents: number;
+ institution: string;
+ studentsData: any[];
}
-const GroupTestReport = ({}: Props) => {
+const customStyles = StyleSheet.create({
+ tableCellHighlight: {
+ backgroundColor: "#4f4969",
+ color: "#bc9970",
+ },
+ table: {
+ display: "flex",
+ flexDirection: "column",
+ // maxWidth: "600px",
+ // margin: "0 auto",
+ border: "1px solid #ccc",
+ // borderCollapse: 'collapse',
+ },
+ tableRow: {
+ display: "flex",
+ flexDirection: "row",
+ borderBottom: "1px solid #ccc",
+ },
+ tableHeader: {
+ fontWeight: "bold",
+ backgroundColor: "#f2f2f2",
+ },
+ tableCell: {
+ flex: 1,
+ padding: "8px",
+ textAlign: "left",
+ wordBreak: "break-all",
+ },
+});
+
+const GroupTestReport = ({
+ title,
+ date,
+ name,
+ email,
+ id,
+ gender,
+ testDetails,
+ summary,
+ logo,
+ qrcode,
+ renderDetails,
+ numberOfStudents,
+ institution,
+ studentsData,
+}: Props) => {
+ const defaultTextStyle = [styles.textFont, { fontSize: 8 }];
+ const defaultSkillsTextStyle = [styles.textFont, { fontSize: 8 }];
+ const defaultSkillsTitleStyle = [
+ styles.textFont,
+ styles.textColor,
+ styles.textBold,
+ { fontSize: 7 },
+ ];
+
return (
-
+
+
+
+
+
+ {title}
+
+
+
+ Date of Test: {date}
+
+
+ Candidate Information:
+
+
+ Name: {name}
+ ID: {id}
+ Email: {email}
+ Gender: {gender}
+
+ Total Number of Students: {numberOfStudents}
+
+ Institution: {institution}
+
+
+
+ Group Test Details:
+
+ {renderDetails}
+
+
+
+ Group Overall Performance Summary
+
+
+ {summary}
+
+
+
+
+
+ Group Score Summary
+
+
+ {testDetails
+ .filter(
+ ({ suggestions, evaluation }) => suggestions || evaluation
+ )
+ .map(({ module, suggestions, evaluation }) => (
+ TODO
+ ))}
+
+
+
+
+
+
+ {false && (
+
+
+
+ )}
+
+
+
+
+
+
+ Sr
+ Candidate Name
+ Email ID
+ Gender
+ Date of test
+ Result
+ Level
+ ID
+
+ {studentsData.map(
+ ({ id, name, email, gender, date, result, level }, index) => (
+
+
+ {index + 1}
+
+ {name}
+ {email}
+ {gender}
+ {date}
+ {result}
+ {level}
+ {id}
+
+ )
+ )}
+
+
+
+
);
diff --git a/src/exams/pdf/styles.ts b/src/exams/pdf/styles.ts
index de55c6da..00bbc484 100644
--- a/src/exams/pdf/styles.ts
+++ b/src/exams/pdf/styles.ts
@@ -47,4 +47,8 @@ export const styles = StyleSheet.create({
height: "64px",
width: "64px",
},
+ qrcode: {
+ width: "80px",
+ height: "80px",
+ },
});
diff --git a/src/exams/pdf/test.report.footer.tsx b/src/exams/pdf/test.report.footer.tsx
new file mode 100644
index 00000000..750f2e8c
--- /dev/null
+++ b/src/exams/pdf/test.report.footer.tsx
@@ -0,0 +1,55 @@
+import React from "react";
+import { styles } from "./styles";
+import { View, Text } from "@react-pdf/renderer";
+
+const TestReportFooter = () => (
+
+
+
+ Validity
+
+ This report remains valid for a duration of three months from the test
+ date.
+
+
+
+ Confidential – circulated for concern people
+
+
+
+ Declaration
+
+ We hereby declare that exam results on our platform, assessed by AI, are
+ not the sole determinants of candidates' English proficiency
+ levels. While EnCoach provides feedback based on assessments, we
+ recognize that language proficiency encompasses practical application,
+ cultural understanding, and real-life communication. We urge users to
+ consider exam results as a measure of progress and improvement, and we
+ continuously enhance our system to ensure accuracy and reliability.
+
+
+
+ info@encoach.com
+ https://encoach.com
+
+ Group ID: TRI64BNBOIU5043
+
+ `${pageNumber} / ${totalPages}`
+ }
+ fixed
+ />
+
+
+
+);
+
+export default TestReportFooter;
\ No newline at end of file
diff --git a/src/exams/pdf/test.report.tsx b/src/exams/pdf/test.report.tsx
index 978dcc2f..f0e5ebc8 100644
--- a/src/exams/pdf/test.report.tsx
+++ b/src/exams/pdf/test.report.tsx
@@ -1,21 +1,16 @@
/* eslint-disable jsx-a11y/alt-text */
import React from "react";
import { Document, Page, View, Text, Image } from "@react-pdf/renderer";
-import ProgressBar from "./progress.bar";
import { ModuleScore } from "@/interfaces/module.scores";
import { styles } from "./styles";
import { StyleSheet } from "@react-pdf/renderer";
-
+import TestReportFooter from "./test.report.footer";
const customStyles = StyleSheet.create({
testDetails: {
display: "flex",
gap: 4,
},
- qrcode: {
- width: "80px",
- height: "80px",
- },
});
interface Props {
@@ -148,71 +143,13 @@ const TestReport = ({
- {false && (
-
-
-
- )}
-
-
-
- Validity
-
- This report remains valid for a duration of three months from
- the test date.
-
-
-
- Confidential – circulated for concern people
-
-
-
- Declaration
-
- We hereby declare that exam results on our platform, assessed by
- AI, are not the sole determinants of candidates' English
- proficiency levels. While EnCoach provides feedback based on
- assessments, we recognize that language proficiency encompasses
- practical application, cultural understanding, and real-life
- communication. We urge users to consider exam results as a measure
- of progress and improvement, and we continuously enhance our
- system to ensure accuracy and reliability.
-
-
-
- info@encoach.com
- https://encoach.com
-
- Group ID: TRI64BNBOIU5043
-
- `${pageNumber} / ${totalPages}`
- }
- fixed
- />
-
-
-
+
);
diff --git a/src/pages/api/assignments/[id]/export.tsx b/src/pages/api/assignments/[id]/export.tsx
new file mode 100644
index 00000000..2d4ed674
--- /dev/null
+++ b/src/pages/api/assignments/[id]/export.tsx
@@ -0,0 +1,333 @@
+import type { NextApiRequest, NextApiResponse } from "next";
+import { app, storage } from "@/firebase";
+import {
+ getFirestore,
+ doc,
+ getDoc,
+ updateDoc,
+ getDocs,
+ query,
+ collection,
+ where,
+ documentId,
+} from "firebase/firestore";
+import { withIronSessionApiRoute } from "iron-session/next";
+import { sessionOptions } from "@/lib/session";
+import ReactPDF from "@react-pdf/renderer";
+import GroupTestReport from "@/exams/pdf/group.test.report";
+import { ref, uploadBytes } from "firebase/storage";
+import { Stat } from "@/interfaces/user";
+import { User } from "@/interfaces/user";
+import { Module } from "@/interfaces";
+import { ModuleScore } from "@/interfaces/module.scores";
+import qrcode from "qrcode";
+import { SkillExamDetails } from "@/exams/pdf/details/skill.exam";
+import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
+import { calculateBandScore } from "@/utils/score";
+import axios from "axios";
+import { moduleLabels } from "@/utils/moduleUtils";
+import {
+ generateQRCode,
+ getRadialProgressPNG,
+ streamToBuffer,
+} from "@/utils/pdf";
+
+const db = getFirestore(app);
+
+export default withIronSessionApiRoute(handler, sessionOptions);
+
+async function handler(req: NextApiRequest, res: NextApiResponse) {
+ if (req.method === "GET") return get(req, res);
+ if (req.method === "POST") return post(req, res);
+}
+
+const getExamSummary = (score: number) => {
+ if (score > 0.8) {
+ return "Scoring between 81% and 100% on the English exam collectively demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading for this group of students. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. The group is encouraged to continue challenging themselves with advanced material in writing, speaking, listening, and reading to further refine their impressive command of the English language.";
+ }
+
+ if (score > 0.6) {
+ return "The group's average scores between 61% and 80% on the English exam, encompassing writing, speaking, listening, and reading, reflect a commendable level of proficiency. There's evidence of a solid grasp of key concepts collectively, and effective application of skills. Room for refinement and deeper exploration in writing, speaking, listening, and reading remains, presenting an opportunity for the entire group to further their mastery.";
+ }
+
+ if (score > 0.4) {
+ return "Scoring between 41% and 60% on the English exam across writing, speaking, listening, and reading indicates a moderate level of understanding for the group. While there's a commendable grasp of key concepts collectively, refining fundamental skills in writing, speaking, listening, and reading can lead to notable improvement. The group is encouraged to work together with consistent effort and targeted focus on weaker areas.";
+ }
+
+ if (score > 0.2) {
+ return "The group's average scores between 21% and 40% on the English exam, encompassing writing, speaking, listening, and reading, show some understanding of key concepts in each domain. However, there's room for improvement in fundamental skills for the entire group. Strengthening writing, speaking, listening, and reading abilities collectively through consistent effort and focused group study will contribute to overall proficiency.";
+ }
+
+ return "The average performance of this group of students in English, covering writing, speaking, listening, and reading, indicates a collective need for improvement, with scores falling between 0% and 20%. Across all language domains, there's a significant gap in understanding key concepts. Strengthening fundamental skills in writing, speaking, listening, and reading is crucial for the entire group. Implementing a shared, consistent study routine and seeking group support in each area can contribute to substantial progress.";
+};
+
+const getLevelSummary = (score: number) => {
+ if (score > 0.8) {
+ return "Scoring between 81% and 100% on the English exam collectively demonstrates an outstanding level of proficiency for this group of students, showcasing a mastery of key concepts related to vocabulary and grammar. There's evidence of not only a high level of skill but also a dedication to excellence. The group is encouraged to continue challenging themselves with advanced material in vocabulary and grammar to further refine their impressive command of the English language.";
+ }
+
+ if (score > 0.6) {
+ return "The group's average scores between 61% and 80% on the English exam reflect a commendable level of proficiency with solid grasp of key concepts related to vocabulary and grammar. Room for refinement and deeper exploration in these language skills remains, presenting an opportunity for the entire group to further their mastery. Consistent effort in honing nuanced aspects of vocabulary and grammar will contribute to even greater proficiency.";
+ }
+
+ if (score > 0.4) {
+ return "Scoring between 41% and 60% on the English exam indicates a moderate level of understanding for the group, with commendable grasp of key concepts related to vocabulary and grammar. Refining these fundamental language skills can lead to notable improvement. The group is encouraged to work together with consistent effort and targeted focus on enhancing their vocabulary and grammar.";
+ }
+
+ if (score > 0.2) {
+ return "The group's average scores between 21% and 40% on the English exam show some understanding of key concepts in vocabulary and grammar. However, there's room for improvement in these fundamental language skills for the entire group. Strengthening vocabulary and grammar collectively through consistent effort and focused group study will contribute to overall proficiency.";
+ }
+
+ return "The average performance of this group of students in English suggests a collective need for improvement, with scores falling between 0% and 20%. There's a significant gap in understanding key concepts related to vocabulary and grammar. Strengthening fundamental language skills, such as vocabulary and grammar, is crucial for the entire group. Implementing a shared, consistent study routine and seeking group support in these areas can contribute to substantial progress.";
+};
+
+const getPerformanceSummary = (module: Module, score: number) => {
+ if (module === "level") return getLevelSummary(score);
+ return getExamSummary(score);
+};
+
+const getScoreAndTotal = (stats: Stat[]) => {
+ return stats.reduce(
+ (acc, { score }) => {
+ return {
+ ...acc,
+ correct: acc.correct + score.correct,
+ total: acc.total + score.total,
+ };
+ },
+ { correct: 0, total: 0 }
+ );
+};
+
+async function post(req: NextApiRequest, res: NextApiResponse) {
+ // verify if it's a logged user that is trying to export
+ if (req.session.user) {
+ const { id } = req.query as { id: string };
+
+ const docSnap = await getDoc(doc(db, "assignments", id));
+ const data = docSnap.data() as {
+ assigner: string;
+ assignees: string[];
+ results: any;
+ exams: { module: Module }[];
+ startDate: string;
+ };
+ if (!data) {
+ res.status(400).end();
+ return;
+ }
+
+ // TODO: Reenable this
+ // if (data.assigner !== req.session.user.id) {
+ // res.status(401).json({ ok: false });
+ // return;
+ // }
+
+ try {
+ const docUser = await getDoc(doc(db, "users", req.session.user.id));
+ if (docUser.exists()) {
+ // we'll need the user in order to get the user data (name, email, focus, etc);
+ const user = docUser.data() as User;
+
+ // generate the QR code for the report
+ const qrcode = await generateQRCode(
+ (req.headers.origin || "") + req.url
+ );
+
+ if (!qrcode) {
+ res.status(500).json({ ok: false });
+ return;
+ }
+
+ const flattenResults = data.results.reduce(
+ (accm: Stat[], entry: any) => {
+ const stats = entry.stats as Stat[];
+ return [...accm, ...stats];
+ },
+ []
+ ) as Stat[];
+
+ const moduleResults = data.exams.map(({ module }) => {
+ const moduleResults = flattenResults.filter(
+ (e) => e.module === module
+ );
+
+ const { correct, total } = getScoreAndTotal(moduleResults);
+ const score = calculateBandScore(correct, total, module, "academic");
+ const png = getRadialProgressPNG("azul", score, total);
+
+ return {
+ bandScore: score,
+ png,
+ module: module[0].toUpperCase() + module.substring(1),
+ score,
+ total,
+ code: module,
+ };
+ }) as ModuleScore[];
+
+ const { correct: overallCorrect, total: overallTotal } =
+ getScoreAndTotal(flattenResults);
+ const overallResult = overallCorrect / overallTotal;
+
+ // generate the overall detail report
+ const overallDetail = {
+ module: "Overall",
+ score: overallCorrect,
+ total: overallTotal,
+ png: getRadialProgressPNG("laranja", overallCorrect, overallTotal),
+ } as ModuleScore;
+
+ const testDetails = [overallDetail, ...moduleResults];
+ // generate the performance summary based on the overall result
+ const baseStat = data.exams[0];
+ const performanceSummary = getPerformanceSummary(
+ // from what I noticed, exams is either an array with the level module
+ // or X modules, either way
+ // as long as I verify the first entry I should be fine
+ baseStat.module,
+ overallResult
+ );
+
+ // level exams have a different report structure than the skill exams
+ const getCustomData = () => {
+ if (baseStat.module === "level") {
+ return {
+ title: "GROUP ENGLISH LEVEL TEST RESULT REPORT ",
+ details: ,
+ };
+ }
+
+ return {
+ title: "GROUP ENGLISH SKILLS TEST RESULT REPORT",
+ details: ,
+ };
+ };
+
+ const { title, details } = getCustomData();
+
+ const numberOfStudents = data.assignees.length;
+
+ const getStudentsData = async () => {
+ // const usersCol = collection(db, "users");
+ const docsSnap = await getDocs(
+ query(
+ collection(db, "users"),
+ where(documentId(), "in", data.assignees)
+ )
+ );
+ const users = docsSnap.docs.map((d) => ({
+ ...d.data(),
+ id: d.id,
+ })) as User[];
+
+ return data.assignees.map((id) => {
+ const user = users.find((u) => u.id === id);
+
+ if (user) {
+ const exams = flattenResults.filter((e) => e.user === id);
+ return {
+ id,
+ name: user.name,
+ email: user.email,
+ gender: user.demographicInformation?.gender,
+ date:
+ exams.length === 0
+ ? "N/A"
+ : new Date(exams[0].date).toLocaleDateString(undefined, {
+ year: "numeric",
+ month: "numeric",
+ day: "numeric",
+ }),
+ result:
+ exams.length === 0
+ ? "N/A"
+ : `${exams[0].score.correct}/${exams[0].score.total}`,
+ };
+ }
+
+ return {
+ id: "N/A",
+ name: "N/A",
+ email: "N/A",
+ gender: "N/A",
+ date: "N/A",
+ result: "N/A",
+ };
+ });
+ };
+
+ const studentsData = await getStudentsData();
+
+ const pdfStream = await ReactPDF.renderToStream(
+
+ );
+
+ // generate the file ref for storage
+ const fileName = `${Date.now().toString()}.pdf`;
+ const fileRef = ref(storage, `assignment_report/${fileName}`);
+
+ // upload the pdf to storage
+ const pdfBuffer = await streamToBuffer(pdfStream);
+ const snapshot = await uploadBytes(fileRef, pdfBuffer, {
+ contentType: "application/pdf",
+ });
+
+ // update the stats entries with the pdf url to prevent duplication
+ await updateDoc(docSnap.ref, {
+ pdf: snapshot.ref.fullPath,
+ });
+ res.status(200).end(snapshot.ref.fullPath);
+ return;
+ }
+
+ res.status(401).json({ ok: false });
+ return;
+ } catch (err) {
+ res.status(500).json({ ok: false });
+ return;
+ }
+ }
+}
+async function get(req: NextApiRequest, res: NextApiResponse) {
+ if (req.session.user) {
+ const { id } = req.query as { id: string };
+
+ const docSnap = await getDoc(doc(db, "assignments", id));
+ const data = docSnap.data();
+ if (!data) {
+ res.status(400).end();
+ return;
+ }
+
+ if (data.assigner !== req.session.user.id) {
+ res.status(401).json({ ok: false });
+ return;
+ }
+
+ if (data.pdf) {
+ return res.end(data.pdf);
+ }
+
+ res.status(404).end();
+ return;
+ }
+
+ res.status(401).json({ ok: false });
+ return;
+}
diff --git a/src/pages/api/assignments/[id].tsx b/src/pages/api/assignments/[id]/index.ts
similarity index 100%
rename from src/pages/api/assignments/[id].tsx
rename to src/pages/api/assignments/[id]/index.ts
diff --git a/src/pages/api/stats/[id]/export.tsx b/src/pages/api/stats/[id]/export.tsx
index 0e8b7e46..bb465278 100644
--- a/src/pages/api/stats/[id]/export.tsx
+++ b/src/pages/api/stats/[id]/export.tsx
@@ -25,6 +25,11 @@ import { LevelExamDetails } from "@/exams/pdf/details/level.exam";
import { calculateBandScore } from "@/utils/score";
import axios from "axios";
import { moduleLabels } from "@/utils/moduleUtils";
+import {
+ generateQRCode,
+ getRadialProgressPNG,
+ streamToBuffer,
+} from "@/utils/pdf";
const db = getFirestore(app);
@@ -35,21 +40,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === "POST") return post(req, res);
}
-export const streamToBuffer = async (
- stream: NodeJS.ReadableStream
-): Promise => {
- return new Promise((resolve, reject) => {
- const chunks: Buffer[] = [];
- stream.on("data", (data) => {
- chunks.push(data);
- });
- stream.on("end", () => {
- resolve(Buffer.concat(chunks));
- });
- stream.on("error", reject);
- });
-};
-
const getExamSummary = (score: number) => {
if (score > 0.8) {
return "Scoring between 81% and 100% on the English exam demonstrates an outstanding level of proficiency in writing, speaking, listening, and reading. Mastery of key concepts is evident across all language domains, showcasing not only a high level of skill but also a dedication to excellence. Continuing to challenge oneself with advanced material in writing, speaking, listening, and reading will further refine the already impressive command of the English language.";
@@ -137,33 +127,6 @@ const handleSkillsFeedbackRequest = async (
}
};
-const generateQRCode = async (link: string) => {
- try {
- const qrCodeDataURL = await qrcode.toDataURL(link);
- return qrCodeDataURL;
- } catch (error) {
- console.error("Error generating QR code:", error);
- return null;
- }
-};
-
-// Radial Progress PNGs were generated with only two colors
-// and they use some baseline score (10%, 20%, 30%..)
-type RADIAL_PROGRESS_COLOR = "laranja" | "azul";
-
-const getRadialProgressPNG = (
- color: RADIAL_PROGRESS_COLOR,
- score: number,
- total: number
-) => {
- // calculate the percentage of the score
- // and round it to the closest available image
- const percent = (score / total) * 100;
- const remainder = percent % 10;
- const roundedPercent = percent - remainder;
- return `public/radial_progress/${color}_${roundedPercent}.png`;
-};
-
async function post(req: NextApiRequest, res: NextApiResponse) {
// verify if it's a logged user that is trying to export
if (req.session.user) {
@@ -178,7 +141,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
);
if (docsSnap.empty) {
- res.status(404).end();
+ res.status(400).end();
return;
}
@@ -288,10 +251,6 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
return result;
});
- // generate the file ref for storage
- const fileName = `${Date.now().toString()}.pdf`;
- const fileRef = ref(storage, `exam_report/${fileName}`);
-
// calculate the overall score out of all the aggregated results
const overallScore = results.reduce(
(accm, { score }) => accm + score,
@@ -352,6 +311,10 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
/>
);
+ // generate the file ref for storage
+ const fileName = `${Date.now().toString()}.pdf`;
+ const fileRef = ref(storage, `exam_report/${fileName}`);
+
// upload the pdf to storage
const pdfBuffer = await streamToBuffer(pdfStream);
const snapshot = await uploadBytes(fileRef, pdfBuffer, {
@@ -376,7 +339,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) {
}
}
- res.status(500).json({ ok: false });
+ res.status(401).json({ ok: false });
return;
}
diff --git a/src/utils/pdf.ts b/src/utils/pdf.ts
new file mode 100644
index 00000000..16c0f471
--- /dev/null
+++ b/src/utils/pdf.ts
@@ -0,0 +1,43 @@
+import qrcode from "qrcode";
+
+export const generateQRCode = async (link: string) => {
+ try {
+ const qrCodeDataURL = await qrcode.toDataURL(link);
+ return qrCodeDataURL;
+ } catch (error) {
+ console.error("Error generating QR code:", error);
+ return null;
+ }
+};
+
+// Radial Progress PNGs were generated with only two colors
+// and they use some baseline score (10%, 20%, 30%..)
+type RADIAL_PROGRESS_COLOR = "laranja" | "azul";
+
+export const getRadialProgressPNG = (
+ color: RADIAL_PROGRESS_COLOR,
+ score: number,
+ total: number
+) => {
+ // calculate the percentage of the score
+ // and round it to the closest available image
+ const percent = (score / total) * 100;
+ const remainder = percent % 10;
+ const roundedPercent = percent - remainder;
+ return `public/radial_progress/${color}_${roundedPercent}.png`;
+};
+
+export const streamToBuffer = async (
+ stream: NodeJS.ReadableStream
+ ): Promise => {
+ return new Promise((resolve, reject) => {
+ const chunks: Buffer[] = [];
+ stream.on("data", (data) => {
+ chunks.push(data);
+ });
+ stream.on("end", () => {
+ resolve(Buffer.concat(chunks));
+ });
+ stream.on("error", reject);
+ });
+ };
\ No newline at end of file