feat: Update Dockerfile and server configuration for improved health checks and error handling

This commit is contained in:
2026-10-19 23:04:54 +03:00
parent e38c388462
commit 7206bf9c4a
10 changed files with 324 additions and 60 deletions

View File

@@ -1,22 +1,19 @@
import { XMLParser } from "fast-xml-parser";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { createHash } from "node:crypto";
import { parseWeeksDocx, parseBellsDocx } from "./calendar.js";
import { apiError, sourceError } from "./errors.js";
import { DATA } from "./paths.js";
import {
academicStartYear,
assignSemesters,
todayIso,
weekIndexOf,
} from "../shared/weeks.js";
const run = promisify(execFile);
const ORIGIN = "https://nextcloud.psu.by";
// The university's Nextcloud; tests point PSU_SOURCE_ORIGIN at a local stand-in.
const ORIGIN = process.env.PSU_SOURCE_ORIGIN || "https://nextcloud.psu.by";
const SHARE = "/public.php/dav/files/no5zKbMrns6j5jQ/";
const YEAR_FOLDER = /^(\d{4})-(\d{4})$/;
const DATA = fileURLToPath(new URL("../.data/", import.meta.url));
const parser = new XMLParser({ removeNSPrefix: true, ignoreAttributes: false });
const arr = (value) => (value ? (Array.isArray(value) ? value : [value]) : []);
const DISCOVERY_TTL = 15 * 60 * 1000,
@@ -39,6 +36,8 @@ const status = { catalogError: null, calendarError: null };
const failedAt = { catalog: 0, calendar: 0 };
const backingOff = (what) => Date.now() - failedAt[what] < MIN_REFRESH;
const TIMEOUT = Number(process.env.PSU_SOURCE_TIMEOUT_MS) || 25000;
const MAX_SIZE = 10 * 1024 * 1024;
export async function request(path, method = "GET") {
// Only the university share is reachable; listings and catalog entries supply the paths.
if (
@@ -48,31 +47,35 @@ export async function request(path, method = "GET") {
!["GET", "PROPFIND"].includes(method)
)
throw new Error("Недопустимый путь документа.");
const url = ORIGIN + path;
// curl also works with the system trust store on macOS. Certificate checks stay enabled.
const args = [
"--fail",
"--silent",
"--show-error",
"--max-time",
"25",
"--max-filesize",
"10485760",
"--max-redirs",
"0",
"--request",
method,
];
if (method === "PROPFIND") args.push("--header", "Depth: 1");
args.push(url);
const { stdout } = await run("curl", args, {
encoding: "buffer",
maxBuffer: 11 * 1024 * 1024,
timeout: 28000,
}).catch((error) => {
// Node's fetch keeps connections to the university open between requests, so a catalog
// crawl of a hundred listings pays for one TLS handshake, not a hundred. Certificates are
// checked, redirects are refused, and the answer is cut off past 10 MB or 25 seconds.
try {
const response = await fetch(ORIGIN + path, {
method,
headers: method === "PROPFIND" ? { Depth: "1" } : {},
redirect: "manual",
signal: AbortSignal.timeout(TIMEOUT),
});
if (!response.ok) {
await response.body?.cancel();
throw new Error(`HTTP ${response.status}`);
}
if (Number(response.headers.get("content-length")) > MAX_SIZE) {
await response.body?.cancel();
throw new Error("Документ больше 10 МБ.");
}
const chunks = [];
let size = 0;
for await (const chunk of response.body || []) {
size += chunk.length;
if (size > MAX_SIZE) throw new Error("Документ больше 10 МБ.");
chunks.push(chunk);
}
return Buffer.concat(chunks);
} catch (error) {
throw sourceError(error);
});
return stdout;
}
}
export function decodeListing(xml, root = SHARE) {
const entries = arr(parser.parse(xml).multistatus?.response);