This commit is contained in:
+10
-2
@@ -119,7 +119,7 @@ function enforceUpdateCooldown(
|
||||
adminId: string,
|
||||
operation: "check" | "download" | "apply",
|
||||
reply: FastifyReply,
|
||||
): void {
|
||||
): number {
|
||||
const state = updateRateState(database, adminId);
|
||||
const now = Date.now();
|
||||
const previous = operation === "check" ? state.checkedAt : operation === "download" ? state.downloadedAt : state.appliedAt;
|
||||
@@ -134,6 +134,7 @@ function enforceUpdateCooldown(
|
||||
if (operation === "check") state.checkedAt = now;
|
||||
else if (operation === "download") state.downloadedAt = now;
|
||||
else state.appliedAt = now;
|
||||
return now;
|
||||
}
|
||||
|
||||
function adminSelect(alias = ""): string {
|
||||
@@ -957,12 +958,15 @@ export async function buildApp(database: DatabaseContext, config: AppConfig) {
|
||||
});
|
||||
|
||||
app.post("/api/update/check", { preHandler: guard(database, config), bodyLimit: 32 * 1024 }, async (request, reply) => {
|
||||
const rateState = updateRateState(database.sqlite, request.auth!.admin.id);
|
||||
const previousCheckedAt = rateState.checkedAt;
|
||||
let reservedCheckedAt: number | null = null;
|
||||
try {
|
||||
reconcileOrphanedUpdateJobs(database.sqlite, config);
|
||||
// Disabled/dev installs do not contact a release endpoint, so repeated
|
||||
// checks are local status reads and should remain immediately usable.
|
||||
if (config.updateStrategy !== "disabled") {
|
||||
enforceUpdateCooldown(database.sqlite, config, request.auth!.admin.id, "check", reply);
|
||||
reservedCheckedAt = enforceUpdateCooldown(database.sqlite, config, request.auth!.admin.id, "check", reply);
|
||||
}
|
||||
const result = await checkForUpdate(database.sqlite, config);
|
||||
writeAudit(database.sqlite, {
|
||||
@@ -981,6 +985,10 @@ export async function buildApp(database: DatabaseContext, config: AppConfig) {
|
||||
reply.header("Cache-Control", "no-store");
|
||||
return { ...result, strategy: config.updateStrategy };
|
||||
} catch (error) {
|
||||
// A failed upstream request is not a successful check. Release the
|
||||
// reservation only when this request still owns it, so a concurrent
|
||||
// successful check cannot have its cooldown overwritten.
|
||||
if (reservedCheckedAt !== null && rateState.checkedAt === reservedCheckedAt) rateState.checkedAt = previousCheckedAt;
|
||||
writeAudit(database.sqlite, {
|
||||
requestId: request.id,
|
||||
actorAdminId: request.auth!.admin.id,
|
||||
|
||||
Reference in New Issue
Block a user