Files
gitea-mirror/src/middleware.ts
Arunavo Ray 698eb0b507 fix: Complete Issue #72 - Fix automatic mirroring and repository cleanup
Major fixes for Docker environment variable issues and cleanup functionality:

🔧 **Duration Parser & Scheduler Fixes**
- Add comprehensive duration parser supporting "8h", "30m", "24h" formats
- Fix GITEA_MIRROR_INTERVAL environment variable mapping to scheduler
- Auto-enable scheduler when GITEA_MIRROR_INTERVAL is set
- Improve scheduler logging to clarify timing behavior (from last run, not startup)

🧹 **Repository Cleanup Service**
- Complete repository cleanup service for orphaned repos (unstarred, deleted)
- Fix cleanup configuration logic - now works with CLEANUP_DELETE_IF_NOT_IN_GITHUB=true
- Auto-enable cleanup when deleteIfNotInGitHub is enabled
- Add manual cleanup trigger API endpoint (/api/cleanup/trigger)
- Support archive/delete actions with dry-run mode and protected repos

🐛 **Environment Variable Integration**
- Fix scheduler not recognizing GITEA_MIRROR_INTERVAL=8h
- Fix cleanup requiring both CLEANUP_DELETE_FROM_GITEA and CLEANUP_DELETE_IF_NOT_IN_GITHUB
- Auto-enable services when relevant environment variables are set
- Better error logging and debugging information

📚 **Documentation Updates**
- Update .env.example with auto-enabling behavior notes
- Update ENVIRONMENT_VARIABLES.md with clarified functionality
- Add comprehensive tests for duration parsing

This resolves the core issues where:
1. GITEA_MIRROR_INTERVAL=8h was not working for automatic mirroring
2. Repository cleanup was not working despite CLEANUP_DELETE_IF_NOT_IN_GITHUB=true
3. Users had no visibility into why scheduling/cleanup wasn't working

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 11:06:21 +05:30

200 lines
7.3 KiB
TypeScript

import { defineMiddleware } from 'astro:middleware';
import { initializeRecovery, hasJobsNeedingRecovery, getRecoveryStatus } from './lib/recovery';
import { startCleanupService, stopCleanupService } from './lib/cleanup-service';
import { startSchedulerService, stopSchedulerService } from './lib/scheduler-service';
import { startRepositoryCleanupService, stopRepositoryCleanupService } from './lib/repository-cleanup-service';
import { initializeShutdownManager, registerShutdownCallback } from './lib/shutdown-manager';
import { setupSignalHandlers } from './lib/signal-handlers';
import { auth } from './lib/auth';
import { isHeaderAuthEnabled, authenticateWithHeaders } from './lib/auth-header';
import { initializeConfigFromEnv } from './lib/env-config-loader';
// Flag to track if recovery has been initialized
let recoveryInitialized = false;
let recoveryAttempted = false;
let cleanupServiceStarted = false;
let schedulerServiceStarted = false;
let repositoryCleanupServiceStarted = false;
let shutdownManagerInitialized = false;
let envConfigInitialized = false;
export const onRequest = defineMiddleware(async (context, next) => {
// First, try Better Auth session (cookie-based)
try {
const session = await auth.api.getSession({
headers: context.request.headers,
});
if (session) {
context.locals.user = session.user;
context.locals.session = session.session;
} else {
// No cookie session, check for header authentication
if (isHeaderAuthEnabled()) {
const headerUser = await authenticateWithHeaders(context.request.headers);
if (headerUser) {
// Create a session-like object for header auth
context.locals.user = {
id: headerUser.id,
email: headerUser.email,
emailVerified: headerUser.emailVerified,
name: headerUser.name || headerUser.username,
username: headerUser.username,
createdAt: headerUser.createdAt,
updatedAt: headerUser.updatedAt,
};
context.locals.session = {
id: `header-${headerUser.id}`,
userId: headerUser.id,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 1 day
ipAddress: context.request.headers.get('x-forwarded-for') || context.clientAddress,
userAgent: context.request.headers.get('user-agent'),
};
} else {
context.locals.user = null;
context.locals.session = null;
}
} else {
context.locals.user = null;
context.locals.session = null;
}
}
} catch (error) {
// If there's an error getting the session, set to null
context.locals.user = null;
context.locals.session = null;
}
// Initialize shutdown manager and signal handlers first
if (!shutdownManagerInitialized) {
try {
console.log('🔧 Initializing shutdown manager and signal handlers...');
initializeShutdownManager();
setupSignalHandlers();
shutdownManagerInitialized = true;
console.log('✅ Shutdown manager and signal handlers initialized');
} catch (error) {
console.error('❌ Failed to initialize shutdown manager:', error);
// Continue anyway - this shouldn't block the application
}
}
// Initialize configuration from environment variables (only once)
if (!envConfigInitialized) {
envConfigInitialized = true;
try {
await initializeConfigFromEnv();
} catch (error) {
console.error('⚠️ Failed to initialize configuration from environment:', error);
// Continue anyway - environment config is optional
}
}
// Initialize recovery system only once when the server starts
// This is a fallback in case the startup script didn't run
if (!recoveryInitialized && !recoveryAttempted) {
recoveryAttempted = true;
try {
// Check if recovery is actually needed before attempting
const needsRecovery = await hasJobsNeedingRecovery();
if (needsRecovery) {
console.log('⚠️ Middleware detected jobs needing recovery (startup script may not have run)');
console.log('Attempting recovery from middleware...');
// Run recovery with a shorter timeout since this is during request handling
const recoveryResult = await Promise.race([
initializeRecovery({
skipIfRecentAttempt: true,
maxRetries: 2,
retryDelay: 3000,
}),
new Promise<boolean>((_, reject) => {
setTimeout(() => reject(new Error('Middleware recovery timeout')), 15000);
})
]);
if (recoveryResult) {
console.log('✅ Middleware recovery completed successfully');
} else {
console.log('⚠️ Middleware recovery completed with some issues');
}
} else {
console.log('✅ No recovery needed (startup script likely handled it)');
}
recoveryInitialized = true;
} catch (error) {
console.error('⚠️ Middleware recovery failed or timed out:', error);
console.log('Application will continue, but some jobs may remain interrupted');
// Log recovery status for debugging
const status = getRecoveryStatus();
console.log('Recovery status:', status);
recoveryInitialized = true; // Mark as attempted to avoid retries
}
}
// Start cleanup service only once after recovery is complete
if (recoveryInitialized && !cleanupServiceStarted) {
try {
console.log('Starting automatic database cleanup service...');
startCleanupService();
// Register cleanup service shutdown callback
registerShutdownCallback(async () => {
console.log('🛑 Shutting down cleanup service...');
stopCleanupService();
});
cleanupServiceStarted = true;
} catch (error) {
console.error('Failed to start cleanup service:', error);
// Don't fail the request if cleanup service fails to start
}
}
// Start scheduler service only once after recovery is complete
if (recoveryInitialized && !schedulerServiceStarted) {
try {
console.log('Starting automatic mirror scheduler service...');
startSchedulerService();
// Register scheduler service shutdown callback
registerShutdownCallback(async () => {
console.log('🛑 Shutting down scheduler service...');
stopSchedulerService();
});
schedulerServiceStarted = true;
} catch (error) {
console.error('Failed to start scheduler service:', error);
// Don't fail the request if scheduler service fails to start
}
}
// Start repository cleanup service only once after recovery is complete
if (recoveryInitialized && !repositoryCleanupServiceStarted) {
try {
console.log('Starting repository cleanup service...');
startRepositoryCleanupService();
// Register repository cleanup service shutdown callback
registerShutdownCallback(async () => {
console.log('🛑 Shutting down repository cleanup service...');
stopRepositoryCleanupService();
});
repositoryCleanupServiceStarted = true;
} catch (error) {
console.error('Failed to start repository cleanup service:', error);
// Don't fail the request if repository cleanup service fails to start
}
}
// Continue with the request
return next();
});