initial commit
This commit is contained in:
186
server/index.js
Normal file
186
server/index.js
Normal file
@@ -0,0 +1,186 @@
|
||||
import express from "express";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import multer from "multer";
|
||||
import { parseWord, PARSER_VERSION } from "./parser.js";
|
||||
import { scheduleFor } from "./schedules.js";
|
||||
import {
|
||||
getIndex,
|
||||
indexStatus,
|
||||
search,
|
||||
dayOf,
|
||||
freeRooms,
|
||||
examsOf,
|
||||
findGroups,
|
||||
directoryCalendar,
|
||||
todayIso,
|
||||
} from "./directory.js";
|
||||
import {
|
||||
getCatalog,
|
||||
getCalendar,
|
||||
documentFor,
|
||||
request,
|
||||
sourceStatus,
|
||||
} from "./source.js";
|
||||
const app = express();
|
||||
app.disable("x-powered-by");
|
||||
app.use("/api", (_req, res, next) => {
|
||||
res.set("Cache-Control", "no-store");
|
||||
next();
|
||||
});
|
||||
// Liveness plus what the server currently reads from the university, without network calls.
|
||||
app.get("/api/health", async (_req, res) => {
|
||||
const sources = await sourceStatus().catch((e) => ({ error: e.message }));
|
||||
res.json({
|
||||
ok: true,
|
||||
parserVersion: PARSER_VERSION,
|
||||
...sources,
|
||||
directory: indexStatus(),
|
||||
});
|
||||
});
|
||||
const isDate = (s) => /^\d{4}-\d{2}-\d{2}$/.test(s || "");
|
||||
const badRequest = (res, error) => res.status(400).json({ error });
|
||||
// University-wide lookups over every catalog document.
|
||||
app.get("/api/directory/search", async (req, res, next) => {
|
||||
try {
|
||||
res.json(search(await getIndex(), String(req.query.q || "").slice(0, 60)));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/directory/day", async (req, res, next) => {
|
||||
try {
|
||||
const date = req.query.date || todayIso();
|
||||
const teacher = String(req.query.teacher || "").slice(0, 80),
|
||||
room = String(req.query.room || "").slice(0, 20);
|
||||
if (!isDate(date)) return badRequest(res, "Дата должна быть в формате ГГГГ-ММ-ДД.");
|
||||
if (!teacher && !room) return badRequest(res, "Укажите преподавателя или аудиторию.");
|
||||
res.json(dayOf(await getIndex(), await directoryCalendar(), { teacher, room }, date));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/directory/free", async (req, res, next) => {
|
||||
try {
|
||||
const date = req.query.date || todayIso(),
|
||||
pair = Number(req.query.pair);
|
||||
if (!isDate(date)) return badRequest(res, "Дата должна быть в формате ГГГГ-ММ-ДД.");
|
||||
const calendar = await directoryCalendar();
|
||||
const result = freeRooms(await getIndex(), calendar, date, pair);
|
||||
if (!result) return badRequest(res, "Нет такой пары в расписании звонков.");
|
||||
res.json(result);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/groups", async (req, res, next) => {
|
||||
try {
|
||||
const text = (name) => String(req.query[name] || "").slice(0, 120);
|
||||
res.json(
|
||||
findGroups(await getIndex(), {
|
||||
q: text("q"),
|
||||
faculty: text("faculty"),
|
||||
course: text("course"),
|
||||
form: text("form"),
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/exams", async (req, res, next) => {
|
||||
try {
|
||||
const group = String(req.query.group || "").slice(0, 40);
|
||||
if (!group) return badRequest(res, "Укажите группу.");
|
||||
res.json({
|
||||
group,
|
||||
exams: examsOf(await getIndex(), group, String(req.query.faculty || "") || null),
|
||||
});
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/catalog", async (req, res, next) => {
|
||||
try {
|
||||
res.json(await getCatalog(req.query.refresh === "1"));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/calendar", async (req, res, next) => {
|
||||
try {
|
||||
res.json(await getCalendar(req.query.refresh === "1"));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/schedule/:id", async (req, res, next) => {
|
||||
try {
|
||||
const file = await documentFor(req.params.id);
|
||||
res.json(await scheduleFor(file, { refresh: req.query.refresh === "1" }));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
app.get("/api/original/:id", async (req, res, next) => {
|
||||
try {
|
||||
const file = await documentFor(req.params.id);
|
||||
res.set("Content-Type", "application/octet-stream");
|
||||
res.set(
|
||||
"Content-Disposition",
|
||||
`attachment; filename*=UTF-8''${encodeURIComponent(file.name)}`,
|
||||
);
|
||||
res.send(await request(file.path));
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 10 * 1024 * 1024, files: 1, fields: 0 },
|
||||
});
|
||||
app.post("/api/import", upload.single("file"), async (req, res, next) => {
|
||||
try {
|
||||
if (!req.file)
|
||||
return res.status(400).json({ error: "Выберите файл Word." });
|
||||
// Multer decodes multipart names as latin1 by default.
|
||||
const name = Buffer.from(req.file.originalname, "latin1").toString("utf8");
|
||||
if (!/\.docx?$/i.test(name))
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: "Поддерживаются файлы .docx и .doc." });
|
||||
res.json(await parseWord(req.file.buffer, { name, uploaded: true }));
|
||||
} catch (e) {
|
||||
e.status = 422;
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
const dist = fileURLToPath(new URL("../dist/", import.meta.url));
|
||||
app.use(express.static(dist, { maxAge: 0 }));
|
||||
app.get("/{*path}", (req, res, next) =>
|
||||
req.path.startsWith("/api/")
|
||||
? res.status(404).json({ error: "Адрес не найден." })
|
||||
: res.sendFile(dist + "index.html", (err) => err && next(err)),
|
||||
);
|
||||
app.use((error, _req, res, _next) => {
|
||||
console.error(error.message);
|
||||
res
|
||||
.status(error.code === "LIMIT_FILE_SIZE" ? 413 : error.status || 503)
|
||||
.json({
|
||||
error:
|
||||
error.code === "LIMIT_FILE_SIZE"
|
||||
? "Файл больше 10 МБ. Выберите документ меньшего размера."
|
||||
: error.status === 422
|
||||
? error.message
|
||||
: error.status === 404
|
||||
? error.message
|
||||
: "Не удалось загрузить расписание. Проверьте подключение и попробуйте ещё раз.",
|
||||
});
|
||||
});
|
||||
const port = Number(process.env.PORT || 3001);
|
||||
app.listen(port, process.env.HOST || "127.0.0.1", () => {
|
||||
console.log(`API и PWA: http://127.0.0.1:${port}`);
|
||||
// Read the week table and parse the whole catalog in the background,
|
||||
// so the first visit and the first search do not wait for the university.
|
||||
getCalendar().catch((e) => console.error(`Таблица недель: ${e.message}`));
|
||||
getIndex().catch((e) => console.error(`Индекс: ${e.message}`));
|
||||
});
|
||||
Reference in New Issue
Block a user