Files
gitea-mirror/src/pages/api/job/schedule-sync-repo.ts
Arunavo Ray 5add8766a4 fix(scheduler,config): preserve ENV schedule; add AUTO_MIRROR_REPOS auto-mirroring
- Prevent Automation UI from overriding schedule:
      - mapDbScheduleToUi now parses intervals robustly (cron/duration/seconds) via parseInterval
      - mapUiScheduleToDb merges with existing config and stores interval as seconds (no lossy cron conversion)
      - /api/config passes existing scheduleConfig to preserve ENV-sourced values
      - schedule-sync endpoint uses parseInterval for nextRun calculation
  - Add AUTO_MIRROR_REPOS support and scheduled auto-mirror phase:
      - scheduleConfig schema includes autoImport and autoMirror
      - env-config-loader reads AUTO_MIRROR_REPOS and carries through to DB
      - scheduler auto-mirrors imported/pending/failed repos when autoMirror is enabled before regular sync
      - docker-compose and ENV docs updated with AUTO_MIRROR_REPOS
  - Tests pass and build succeeds
2025-09-14 08:31:31 +05:30

159 lines
4.9 KiB
TypeScript

import type { APIRoute } from "astro";
import { db, configs, repositories } from "@/lib/db";
import { eq, or } from "drizzle-orm";
import { repoStatusEnum, repositoryVisibilityEnum } from "@/types/Repository";
import { isRepoPresentInGitea, syncGiteaRepo } from "@/lib/gitea";
import type {
ScheduleSyncRepoRequest,
ScheduleSyncRepoResponse,
} from "@/types/sync";
import { createSecureErrorResponse } from "@/lib/utils";
import { parseInterval } from "@/lib/utils/duration-parser";
export const POST: APIRoute = async ({ request }) => {
try {
const body: ScheduleSyncRepoRequest = await request.json();
const { userId } = body;
if (!userId) {
return new Response(
JSON.stringify({
success: false,
error: "Missing userId in request body.",
repositories: [],
} satisfies ScheduleSyncRepoResponse),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch config for the user
const configResult = await db
.select()
.from(configs)
.where(eq(configs.userId, userId))
.limit(1);
const config = configResult[0];
if (!config || !config.githubConfig.token) {
return new Response(
JSON.stringify({
success: false,
error: "Config missing for the user or GitHub token not found.",
repositories: [],
} satisfies ScheduleSyncRepoResponse),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
// Fetch repositories with status 'mirrored' or 'synced'
const repos = await db
.select()
.from(repositories)
.where(
eq(repositories.userId, userId) &&
or(
eq(repositories.status, "mirrored"),
eq(repositories.status, "synced"),
eq(repositories.status, "failed")
)
);
if (!repos.length) {
return new Response(
JSON.stringify({
success: false,
error:
"No repositories found with status mirrored, synced or failed.",
repositories: [],
} satisfies ScheduleSyncRepoResponse),
{ status: 404, headers: { "Content-Type": "application/json" } }
);
}
// Calculate nextRun and update lastRun and nextRun in the config
const currentTime = new Date();
let intervalMs = 3600 * 1000;
try {
intervalMs = parseInterval(
typeof config.scheduleConfig?.interval === 'number'
? config.scheduleConfig.interval
: (config.scheduleConfig?.interval as unknown as string) || '3600'
);
} catch {
intervalMs = 3600 * 1000;
}
const nextRun = new Date(currentTime.getTime() + intervalMs);
// Update the full giteaConfig object
await db
.update(configs)
.set({
scheduleConfig: {
...config.scheduleConfig,
lastRun: currentTime,
nextRun: nextRun,
},
})
.where(eq(configs.userId, userId));
// Start async sync in background
setTimeout(async () => {
for (const repo of repos) {
try {
// Only check Gitea presence if the repo failed previously
if (repo.status === "failed") {
const isPresent = await isRepoPresentInGitea({
config,
owner: repo.owner,
repoName: repo.name,
});
if (!isPresent) {
continue; //silently skip if repo is not present in Gitea
}
}
await syncGiteaRepo({
config,
repository: {
...repo,
status: repoStatusEnum.parse(repo.status),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
mirroredLocation: repo.mirroredLocation || "",
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
},
});
} catch (error) {
console.error(`Sync failed for repo ${repo.name}:`, error);
}
}
}, 0);
const resPayload: ScheduleSyncRepoResponse = {
success: true,
message: "Sync job scheduled for eligible repositories.",
repositories: repos.map((repo) => ({
...repo,
status: repoStatusEnum.parse(repo.status),
organization: repo.organization ?? undefined,
lastMirrored: repo.lastMirrored ?? undefined,
errorMessage: repo.errorMessage ?? undefined,
forkedFrom: repo.forkedFrom ?? undefined,
visibility: repositoryVisibilityEnum.parse(repo.visibility),
mirroredLocation: repo.mirroredLocation || "",
})),
};
return new Response(JSON.stringify(resPayload), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
return createSecureErrorResponse(error, "schedule sync", 500);
}
};