import { Injectable, NotFoundException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { WipeProfile } from '../../entities/wipe-profile.entity'; import { WipeSchedule } from '../../entities/wipe-schedule.entity'; import { WipeHistory } from '../../entities/wipe-history.entity'; import { CreateProfileDto } from './dto/create-profile.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { CreateScheduleDto } from './dto/create-schedule.dto'; import { TriggerWipeDto } from './dto/trigger-wipe.dto'; import { NatsService } from '../../services/nats.service'; @Injectable() export class WipesService { private readonly logger = new Logger(WipesService.name); constructor( @InjectRepository(WipeProfile) private readonly wipeProfileRepo: Repository, @InjectRepository(WipeSchedule) private readonly wipeScheduleRepo: Repository, @InjectRepository(WipeHistory) private readonly wipeHistoryRepo: Repository, private readonly natsService: NatsService, ) {} async getProfiles(licenseId: string): Promise { return this.wipeProfileRepo.find({ where: { license_id: licenseId }, order: { created_at: 'DESC' }, }); } async createProfile(licenseId: string, dto: CreateProfileDto): Promise { const profile = this.wipeProfileRepo.create({ license_id: licenseId, ...dto, }); return this.wipeProfileRepo.save(profile); } async updateProfile( licenseId: string, profileId: string, dto: UpdateProfileDto, ): Promise { const profile = await this.wipeProfileRepo.findOne({ where: { id: profileId, license_id: licenseId }, }); if (!profile) { throw new NotFoundException(`Wipe profile ${profileId} not found`); } Object.assign(profile, dto); profile.updated_at = new Date(); return this.wipeProfileRepo.save(profile); } async deleteProfile(licenseId: string, profileId: string): Promise { const result = await this.wipeProfileRepo.delete({ id: profileId, license_id: licenseId, }); if (result.affected === 0) { throw new NotFoundException(`Wipe profile ${profileId} not found`); } } async getSchedules(licenseId: string): Promise { return this.wipeScheduleRepo.find({ where: { license_id: licenseId }, relations: ['wipe_profile'], order: { created_at: 'DESC' }, }); } async createSchedule(licenseId: string, dto: CreateScheduleDto): Promise { const schedule = this.wipeScheduleRepo.create({ license_id: licenseId, ...dto, }); return this.wipeScheduleRepo.save(schedule); } async getHistory(licenseId: string, limit: number = 50): Promise { return this.wipeHistoryRepo.find({ where: { license_id: licenseId }, relations: ['wipe_profile', 'wipe_schedule', 'map'], order: { created_at: 'DESC' }, take: limit, }); } async triggerWipe( licenseId: string, dto: TriggerWipeDto, ): Promise<{ wipe_history_id: string }> { const history = this.wipeHistoryRepo.create({ license_id: licenseId, wipe_type: dto.wipe_type, wipe_profile_id: dto.wipe_profile_id, trigger_type: 'manual', status: 'pending', }); const saved = await this.wipeHistoryRepo.save(history); await this.natsService.publish(`corrosion.${licenseId}.cmd.wipe`, { wipe_history_id: saved.id, wipe_type: dto.wipe_type, wipe_profile_id: dto.wipe_profile_id ?? null, trigger_type: 'manual', timestamp: new Date().toISOString(), }); this.logger.log(`Wipe triggered for license ${licenseId} — history id ${saved.id}`); return { wipe_history_id: saved.id }; } async triggerDryRun( licenseId: string, dto: TriggerWipeDto, ): Promise<{ would_delete: string[]; would_preserve: string[]; estimated_duration_seconds: number; profile_name: string | null; notes: string[]; }> { // Resolve profile config if a profile ID was supplied. let profile: WipeProfile | null = null; if (dto.wipe_profile_id) { profile = await this.wipeProfileRepo.findOne({ where: { id: dto.wipe_profile_id, license_id: licenseId }, }); } if (!profile && dto.wipe_profile_id) { throw new NotFoundException(`Wipe profile ${dto.wipe_profile_id} not found`); } const notes: string[] = []; // Base files affected by all wipe types. const would_delete: string[] = ['*.map', '*.sav']; const would_preserve: string[] = [ 'oxide/', 'oxide/plugins/', 'cfg/', 'server.cfg', ]; // Blueprint wipe additions. if (dto.wipe_type === 'blueprint' || dto.wipe_type === 'full') { would_delete.push('player.blueprints.db', 'player.tech.db'); } // Full wipe: also clear player data and oxide data. if (dto.wipe_type === 'full') { would_delete.push( 'player.deaths.db', 'player.identities.db', 'player.states.db', 'player.tokens.db', 'oxide/data/*', ); would_preserve.splice(would_preserve.indexOf('oxide/'), 1); } // Factor in pre_wipe_config from the profile (if set). let estimatedSeconds = 45; if (profile) { const pre = profile.pre_wipe_config as Record; const post = profile.post_wipe_config as Record; if (pre?.backup_before_wipe) { estimatedSeconds += 60; notes.push('Pre-wipe backup will run before deletion (+60s)'); would_preserve.push('backups/'); } if (pre?.kick_players_before_wipe) { const countdownWarnings: number[] = (pre.countdown_warnings as number[]) ?? []; const maxWarning = countdownWarnings.length > 0 ? Math.max(...countdownWarnings) : 0; if (maxWarning > 0) { estimatedSeconds += maxWarning * 60; notes.push(`Players will be warned ${countdownWarnings.join(', ')} minutes before kick (+${maxWarning * 60}s)`); } } if (post?.verify_server_started) { estimatedSeconds += 30; notes.push('Post-wipe: server health check will run (+30s)'); } if (post?.rollback_on_failure) { notes.push('Rollback on failure is enabled — backup will be preserved if wipe fails'); } if (post?.max_restart_attempts) { const attempts = post.max_restart_attempts as number; if (attempts > 1) { estimatedSeconds += (attempts - 1) * 15; notes.push(`Up to ${attempts} restart attempts (+${(attempts - 1) * 15}s max)`); } } } else { notes.push('No profile selected — using default wipe behavior'); } // Account for world size in time estimate. // Larger worlds take longer to clear from disk (rough heuristic). // We don't have world_size here without querying server_config, // so apply a static estimate per wipe type. if (dto.wipe_type === 'full') { estimatedSeconds += 75; } else if (dto.wipe_type === 'blueprint') { estimatedSeconds += 10; } this.logger.log( `Dry-run for license ${licenseId}: type=${dto.wipe_type}, ` + `profile=${profile?.profile_name ?? 'none'}, estimated=${estimatedSeconds}s`, ); return { would_delete, would_preserve, estimated_duration_seconds: estimatedSeconds, profile_name: profile?.profile_name ?? null, notes, }; } }