@@ -24,6 +24,8 @@ public sealed class TenantsController(
2424 TenantProvisioningService provisioningService ,
2525 TenantEncryptionKeyRotationService keyRotationService ,
2626 TenantQuotaEnforcementService quotaEnforcementService ,
27+ TenantBackupRestoreService backupRestoreService ,
28+ TenantMigrationPlanningService migrationPlanningService ,
2729 TenantCatalogRepository catalogRepository ,
2830 ILogger < TenantsController > logger ) : ControllerBase
2931{
@@ -454,6 +456,160 @@ public async Task<ActionResult<TenantQuotaApiResponse>> UpsertTenantQuotas(
454456 UpdatedAt = effective . UpdatedAt ,
455457 } ) ;
456458 }
459+
460+ /// <summary>
461+ /// Creates a tenant-scoped backup export.
462+ /// POST /api/v1/tenants/{tenantId}/databases/{databaseName}/backup
463+ /// </summary>
464+ [ HttpPost ( "{tenantId}/databases/{databaseName}/backup" ) ]
465+ [ ProducesResponseType ( 200 ) ]
466+ [ ProducesResponseType ( 400 ) ]
467+ [ ProducesResponseType ( 404 ) ]
468+ public async Task < ActionResult < TenantBackupApiResponse > > CreateTenantBackup (
469+ string tenantId ,
470+ string databaseName ,
471+ [ FromBody ] CreateTenantBackupApiRequest request ,
472+ CancellationToken cancellationToken = default )
473+ {
474+ if ( string . IsNullOrWhiteSpace ( request . BackupDirectory ) )
475+ {
476+ return BadRequest ( new ErrorResponse { Message = "BackupDirectory is required" } ) ;
477+ }
478+
479+ var tenant = await catalogRepository . GetTenantByIdAsync ( tenantId , cancellationToken ) ;
480+ if ( tenant is null )
481+ {
482+ return NotFound ( new ErrorResponse { Message = "Tenant not found" } ) ;
483+ }
484+
485+ var operation = await backupRestoreService . CreateBackupAsync (
486+ tenantId ,
487+ databaseName ,
488+ request . BackupDirectory ,
489+ request . IdempotencyKey ?? Guid . NewGuid ( ) . ToString ( "N" ) ,
490+ cancellationToken ) ;
491+
492+ if ( operation . Status == TenantDataOperationStatus . Failed )
493+ {
494+ return StatusCode ( 500 , new ErrorResponse { Message = operation . ErrorMessage ?? "Backup failed" } ) ;
495+ }
496+
497+ return Ok ( new TenantBackupApiResponse
498+ {
499+ OperationId = operation . OperationId ,
500+ TenantId = operation . TenantId ,
501+ DatabaseName = operation . DatabaseName ,
502+ BackupPath = operation . BackupPath ,
503+ BackupSizeBytes = operation . BackupSizeBytes ,
504+ Status = operation . Status . ToString ( ) ,
505+ StartedAt = operation . StartedAt ,
506+ CompletedAt = operation . CompletedAt ,
507+ } ) ;
508+ }
509+
510+ /// <summary>
511+ /// Restores a tenant database from a tenant-scoped backup export.
512+ /// POST /api/v1/tenants/{tenantId}/databases/{databaseName}/restore
513+ /// </summary>
514+ [ HttpPost ( "{tenantId}/databases/{databaseName}/restore" ) ]
515+ [ ProducesResponseType ( 200 ) ]
516+ [ ProducesResponseType ( 400 ) ]
517+ [ ProducesResponseType ( 404 ) ]
518+ public async Task < ActionResult < TenantRestoreApiResponse > > RestoreTenantBackup (
519+ string tenantId ,
520+ string databaseName ,
521+ [ FromBody ] RestoreTenantBackupApiRequest request ,
522+ CancellationToken cancellationToken = default )
523+ {
524+ if ( string . IsNullOrWhiteSpace ( request . BackupPath ) )
525+ {
526+ return BadRequest ( new ErrorResponse { Message = "BackupPath is required" } ) ;
527+ }
528+
529+ var tenant = await catalogRepository . GetTenantByIdAsync ( tenantId , cancellationToken ) ;
530+ if ( tenant is null )
531+ {
532+ return NotFound ( new ErrorResponse { Message = "Tenant not found" } ) ;
533+ }
534+
535+ var operation = await backupRestoreService . RestoreBackupAsync (
536+ tenantId ,
537+ databaseName ,
538+ request . BackupPath ,
539+ request . TargetDatabasePath ,
540+ request . IdempotencyKey ?? Guid . NewGuid ( ) . ToString ( "N" ) ,
541+ cancellationToken ) ;
542+
543+ if ( operation . Status == TenantDataOperationStatus . Failed )
544+ {
545+ return StatusCode ( 500 , new ErrorResponse { Message = operation . ErrorMessage ?? "Restore failed" } ) ;
546+ }
547+
548+ return Ok ( new TenantRestoreApiResponse
549+ {
550+ OperationId = operation . OperationId ,
551+ TenantId = operation . TenantId ,
552+ DatabaseName = operation . DatabaseName ,
553+ SourceBackupPath = operation . SourceBackupPath ,
554+ RestoredDatabasePath = operation . RestoredDatabasePath ,
555+ ValidationPassed = operation . ValidationPassed ,
556+ RollbackApplied = operation . RollbackApplied ,
557+ Status = operation . Status . ToString ( ) ,
558+ StartedAt = operation . StartedAt ,
559+ CompletedAt = operation . CompletedAt ,
560+ } ) ;
561+ }
562+
563+ /// <summary>
564+ /// Creates a migration plan for moving a tenant database to another server instance.
565+ /// POST /api/v1/tenants/{tenantId}/databases/{databaseName}/migration-plan
566+ /// </summary>
567+ [ HttpPost ( "{tenantId}/databases/{databaseName}/migration-plan" ) ]
568+ [ ProducesResponseType ( 200 ) ]
569+ [ ProducesResponseType ( 400 ) ]
570+ [ ProducesResponseType ( 404 ) ]
571+ public async Task < ActionResult < TenantMigrationPlanApiResponse > > CreateTenantMigrationPlan (
572+ string tenantId ,
573+ string databaseName ,
574+ [ FromBody ] CreateTenantMigrationPlanApiRequest request ,
575+ CancellationToken cancellationToken = default )
576+ {
577+ if ( string . IsNullOrWhiteSpace ( request . TargetServer ) || string . IsNullOrWhiteSpace ( request . ExportDirectory ) )
578+ {
579+ return BadRequest ( new ErrorResponse { Message = "TargetServer and ExportDirectory are required" } ) ;
580+ }
581+
582+ var tenant = await catalogRepository . GetTenantByIdAsync ( tenantId , cancellationToken ) ;
583+ if ( tenant is null )
584+ {
585+ return NotFound ( new ErrorResponse { Message = "Tenant not found" } ) ;
586+ }
587+
588+ var plan = await migrationPlanningService . CreateMigrationPlanAsync (
589+ tenantId ,
590+ databaseName ,
591+ request . TargetServer ,
592+ request . ExportDirectory ,
593+ cancellationToken ) ;
594+
595+ return Ok ( new TenantMigrationPlanApiResponse
596+ {
597+ PlanId = plan . PlanId ,
598+ TenantId = plan . TenantId ,
599+ DatabaseName = plan . DatabaseName ,
600+ SourceDatabasePath = plan . SourceDatabasePath ,
601+ TargetServer = plan . TargetServer ,
602+ ExportArtifactPath = plan . ExportArtifactPath ,
603+ DatabaseSizeBytes = plan . DatabaseSizeBytes ,
604+ Steps = plan . Steps ,
605+ ExportHook = plan . ExecutionHooks . ExportHook ,
606+ RestoreHook = plan . ExecutionHooks . RestoreHook ,
607+ ValidationHook = plan . ExecutionHooks . ValidationHook ,
608+ CreatedAt = plan . CreatedAt ,
609+ } ) ;
610+ }
611+
612+ // ...existing code...
457613}
458614
459615// API Request/Response DTOs
@@ -629,3 +785,82 @@ public sealed class TenantQuotaApiResponse
629785 public required int MaxBatchSize { get ; init ; }
630786 public required DateTime ? UpdatedAt { get ; init ; }
631787}
788+
789+ /// <summary>
790+ /// Request to create a tenant backup.
791+ /// </summary>
792+ public sealed class CreateTenantBackupApiRequest
793+ {
794+ public required string BackupDirectory { get ; init ; }
795+ public string ? IdempotencyKey { get ; init ; }
796+ }
797+
798+ /// <summary>
799+ /// Response from tenant backup request.
800+ /// </summary>
801+ public sealed class TenantBackupApiResponse
802+ {
803+ public required string OperationId { get ; init ; }
804+ public required string TenantId { get ; init ; }
805+ public required string DatabaseName { get ; init ; }
806+ public required string BackupPath { get ; init ; }
807+ public required long BackupSizeBytes { get ; init ; }
808+ public required string Status { get ; init ; }
809+ public required DateTime StartedAt { get ; init ; }
810+ public required DateTime ? CompletedAt { get ; init ; }
811+ }
812+
813+ /// <summary>
814+ /// Request to restore a tenant backup.
815+ /// </summary>
816+ public sealed class RestoreTenantBackupApiRequest
817+ {
818+ public required string BackupPath { get ; init ; }
819+ public string ? TargetDatabasePath { get ; init ; }
820+ public string ? IdempotencyKey { get ; init ; }
821+ }
822+
823+ /// <summary>
824+ /// Response from tenant restore request.
825+ /// </summary>
826+ public sealed class TenantRestoreApiResponse
827+ {
828+ public required string OperationId { get ; init ; }
829+ public required string TenantId { get ; init ; }
830+ public required string DatabaseName { get ; init ; }
831+ public required string SourceBackupPath { get ; init ; }
832+ public required string RestoredDatabasePath { get ; init ; }
833+ public required bool ValidationPassed { get ; init ; }
834+ public required bool RollbackApplied { get ; init ; }
835+ public required string Status { get ; init ; }
836+ public required DateTime StartedAt { get ; init ; }
837+ public required DateTime ? CompletedAt { get ; init ; }
838+ }
839+
840+ /// <summary>
841+ /// Request to create a tenant migration plan.
842+ /// </summary>
843+ public sealed class CreateTenantMigrationPlanApiRequest
844+ {
845+ public required string TargetServer { get ; init ; }
846+ public required string ExportDirectory { get ; init ; }
847+ }
848+
849+ /// <summary>
850+ /// Response from tenant migration plan request.
851+ /// </summary>
852+ public sealed class TenantMigrationPlanApiResponse
853+ {
854+ public required string PlanId { get ; init ; }
855+ public required string TenantId { get ; init ; }
856+ public required string DatabaseName { get ; init ; }
857+ public required string SourceDatabasePath { get ; init ; }
858+ public required string TargetServer { get ; init ; }
859+ public required string ExportArtifactPath { get ; init ; }
860+ public required long DatabaseSizeBytes { get ; init ; }
861+ public required string [ ] Steps { get ; init ; }
862+ public required string ExportHook { get ; init ; }
863+ public required string RestoreHook { get ; init ; }
864+ public required string ValidationHook { get ; init ; }
865+ public required DateTime CreatedAt { get ; init ; }
866+ }
0 commit comments