mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-10 05:26:44 +03:00
- Implemented comprehensive GitHub API rate limit handling:
- Integrated @octokit/plugin-throttling for automatic retry with exponential backoff
- Added RateLimitManager service to track and enforce rate limits
- Store rate limit status in database for persistence across restarts
- Automatic pause and resume when limits are exceeded
- Proper user identification for 5000 req/hr authenticated limit (vs 60 unauthenticated)
- Improved rate limit UI/UX:
- Removed intrusive rate limit card from dashboard
- Toast notifications only at critical thresholds (80% and 100% usage)
- All rate limit events logged for debugging
- Optimized for GitHub's API constraints:
- Reduced default batch size from 10 to 5 repositories
- Added documentation about GitHub's 100 concurrent request limit
- Better handling of repositories with many issues/PRs
159 lines
5.4 KiB
TypeScript
159 lines
5.4 KiB
TypeScript
import type { APIRoute } from "astro";
|
|
import type { MirrorOrgRequest, MirrorOrgResponse } from "@/types/mirror";
|
|
import { db, configs, organizations } from "@/lib/db";
|
|
import { eq, inArray } from "drizzle-orm";
|
|
import { createGitHubClient } from "@/lib/github";
|
|
import { mirrorGitHubOrgToGitea } from "@/lib/gitea";
|
|
import { repoStatusEnum } from "@/types/Repository";
|
|
import { type MembershipRole } from "@/types/organizations";
|
|
import { createSecureErrorResponse } from "@/lib/utils";
|
|
import { processWithResilience } from "@/lib/utils/concurrency";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import { getDecryptedGitHubToken } from "@/lib/utils/config-encryption";
|
|
|
|
export const POST: APIRoute = async ({ request }) => {
|
|
try {
|
|
const body: MirrorOrgRequest = await request.json();
|
|
const { userId, organizationIds } = body;
|
|
|
|
if (!userId || !organizationIds || !Array.isArray(organizationIds)) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
message: "userId and organizationIds are required.",
|
|
}),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
if (organizationIds.length === 0) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
success: false,
|
|
message: "No organization IDs provided.",
|
|
}),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// Fetch config
|
|
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({ error: "Config missing for the user or token." }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// Fetch organizations
|
|
const orgs = await db
|
|
.select()
|
|
.from(organizations)
|
|
.where(inArray(organizations.id, organizationIds));
|
|
|
|
if (!orgs.length) {
|
|
return new Response(
|
|
JSON.stringify({ error: "No organizations found for the given IDs." }),
|
|
{ status: 404, headers: { "Content-Type": "application/json" } }
|
|
);
|
|
}
|
|
|
|
// Fire async mirroring without blocking response, using parallel processing with resilience
|
|
setTimeout(async () => {
|
|
if (!config.githubConfig.token) {
|
|
throw new Error("GitHub token is missing in config.");
|
|
}
|
|
|
|
// Create a single Octokit instance to be reused with rate limit tracking
|
|
const decryptedToken = getDecryptedGitHubToken(config);
|
|
const githubUsername = config.githubConfig?.owner || undefined;
|
|
const octokit = createGitHubClient(decryptedToken, userId, githubUsername);
|
|
|
|
// Define the concurrency limit - adjust based on API rate limits
|
|
// Using a lower concurrency for organizations since each org might contain many repos
|
|
const CONCURRENCY_LIMIT = 2;
|
|
|
|
// Generate a batch ID to group related organizations
|
|
const batchId = uuidv4();
|
|
|
|
// Process organizations in parallel with resilience to container restarts
|
|
await processWithResilience(
|
|
orgs,
|
|
async (org) => {
|
|
// Prepare organization data
|
|
const orgData = {
|
|
...org,
|
|
status: repoStatusEnum.parse("imported"),
|
|
membershipRole: org.membershipRole as MembershipRole,
|
|
lastMirrored: org.lastMirrored ?? undefined,
|
|
errorMessage: org.errorMessage ?? undefined,
|
|
};
|
|
|
|
// Log the start of mirroring
|
|
console.log(`Starting mirror for organization: ${org.name}`);
|
|
|
|
// Mirror the organization
|
|
await mirrorGitHubOrgToGitea({
|
|
config,
|
|
octokit,
|
|
organization: orgData,
|
|
});
|
|
|
|
return org;
|
|
},
|
|
{
|
|
userId: config.userId || "",
|
|
jobType: "mirror",
|
|
batchId,
|
|
getItemId: (org) => org.id,
|
|
getItemName: (org) => org.name,
|
|
concurrencyLimit: CONCURRENCY_LIMIT,
|
|
maxRetries: 2,
|
|
retryDelay: 3000,
|
|
checkpointInterval: 1, // Checkpoint after each organization
|
|
onProgress: (completed, total, result) => {
|
|
const percentComplete = Math.round((completed / total) * 100);
|
|
console.log(`Organization mirroring progress: ${percentComplete}% (${completed}/${total})`);
|
|
|
|
if (result) {
|
|
console.log(`Successfully mirrored organization: ${result.name}`);
|
|
}
|
|
},
|
|
onRetry: (org, error, attempt) => {
|
|
console.log(`Retrying organization ${org.name} (attempt ${attempt}): ${error.message}`);
|
|
}
|
|
}
|
|
);
|
|
|
|
console.log("All organization mirroring tasks completed");
|
|
}, 0);
|
|
|
|
const responsePayload: MirrorOrgResponse = {
|
|
success: true,
|
|
message: "Mirror job started.",
|
|
organizations: orgs.map((org) => ({
|
|
...org,
|
|
status: repoStatusEnum.parse(org.status),
|
|
membershipRole: org.membershipRole as MembershipRole,
|
|
lastMirrored: org.lastMirrored ?? undefined,
|
|
errorMessage: org.errorMessage ?? undefined,
|
|
})),
|
|
};
|
|
|
|
// Immediate response
|
|
return new Response(JSON.stringify(responsePayload), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
} catch (error) {
|
|
return createSecureErrorResponse(error, "mirror organization", 500);
|
|
}
|
|
};
|