1414using AgileConfig . Server . Event ;
1515using AgileConfig . Server . IService ;
1616using Microsoft . AspNetCore . Authorization ;
17+ using Microsoft . AspNetCore . Http ;
1718using Microsoft . AspNetCore . Mvc ;
1819using Newtonsoft . Json ;
1920
@@ -382,6 +383,243 @@ public async Task<IActionResult> Export([FromBody] AppExportRequest model)
382383 return File ( Encoding . UTF8 . GetBytes ( json ) , "application/json" , fileName ) ;
383384 }
384385
386+ [ TypeFilter ( typeof ( PermissionCheckAttribute ) , Arguments = new object [ ] { Functions . App_Add } ) ]
387+ [ HttpPost ]
388+ public async Task < IActionResult > PreviewImport ( IFormFile file )
389+ {
390+ var importFile = await ReadImportFileAsync ( file ) ;
391+ var preview = await BuildImportPreviewAsync ( importFile ) ;
392+ return Json ( new
393+ {
394+ success = ! preview . Errors . Any ( ) ,
395+ data = preview ,
396+ message = preview . Errors . FirstOrDefault ( )
397+ } ) ;
398+ }
399+
400+ [ TypeFilter ( typeof ( PermissionCheckAttribute ) , Arguments = new object [ ] { Functions . App_Add } ) ]
401+ [ HttpPost ]
402+ public async Task < IActionResult > Import ( [ FromBody ] AppImportRequest model )
403+ {
404+ ArgumentNullException . ThrowIfNull ( model ) ;
405+ ArgumentNullException . ThrowIfNull ( model . File ) ;
406+
407+ var preview = await BuildImportPreviewAsync ( model . File ) ;
408+ if ( preview . Errors . Any ( ) )
409+ return Json ( new
410+ {
411+ success = false ,
412+ data = preview ,
413+ message = string . Join ( Environment . NewLine , preview . Errors )
414+ } ) ;
415+
416+ var currentUserId = await this . GetCurrentUserId ( _userService ) ;
417+ var now = DateTime . Now ;
418+
419+ foreach ( var previewItem in preview . Apps . OrderBy ( x => x . Order ) )
420+ {
421+ var importItem = model . File . Apps . First ( x => string . Equals ( x . App ? . Id , previewItem . AppId , StringComparison . OrdinalIgnoreCase ) ) ;
422+ var app = new App
423+ {
424+ Id = importItem . App . Id ,
425+ Name = importItem . App . Name ,
426+ Group = importItem . App . Group ,
427+ Secret = importItem . App . Secret ,
428+ Enabled = importItem . App . Enabled ,
429+ Type = importItem . App . Inheritanced ? AppType . Inheritance : AppType . PRIVATE ,
430+ CreateTime = now ,
431+ Creator = currentUserId
432+ } ;
433+
434+ var inheritanceApps = BuildInheritanceLinks ( importItem . App . InheritancedApps , app . Id ) ;
435+ await _appService . AddAsync ( app , inheritanceApps ) ;
436+
437+ foreach ( var envConfigs in importItem . Envs )
438+ {
439+ foreach ( var configVm in envConfigs . Value ?? new List < AppExportConfigVM > ( ) )
440+ {
441+ var config = new Config
442+ {
443+ Id = Guid . NewGuid ( ) . ToString ( "N" ) ,
444+ AppId = app . Id ,
445+ Env = envConfigs . Key ,
446+ Group = configVm . Group ,
447+ Key = configVm . Key ,
448+ Value = configVm . Value ,
449+ Description = configVm . Description ,
450+ CreateTime = now ,
451+ Status = ConfigStatus . Enabled ,
452+ OnlineStatus = OnlineStatus . WaitPublish ,
453+ EditStatus = EditStatus . Add
454+ } ;
455+ await _configService . AddAsync ( config , envConfigs . Key ) ;
456+ }
457+ }
458+ }
459+
460+ return Json ( new
461+ {
462+ success = true ,
463+ data = preview
464+ } ) ;
465+ }
466+
467+ private static List < AppInheritanced > BuildInheritanceLinks ( List < string > parentIds , string appId )
468+ {
469+ var inheritanceApps = new List < AppInheritanced > ( ) ;
470+ if ( parentIds == null ) return inheritanceApps ;
471+
472+ var sort = 0 ;
473+ foreach ( var parentId in parentIds . Where ( x => ! string . IsNullOrWhiteSpace ( x ) ) . Distinct ( StringComparer . OrdinalIgnoreCase ) )
474+ inheritanceApps . Add ( new AppInheritanced
475+ {
476+ Id = Guid . NewGuid ( ) . ToString ( "N" ) ,
477+ AppId = appId ,
478+ InheritancedAppId = parentId ,
479+ Sort = sort ++
480+ } ) ;
481+
482+ return inheritanceApps ;
483+ }
484+
485+ private async Task < AppExportFileVM > ReadImportFileAsync ( IFormFile file )
486+ {
487+ if ( file == null || file . Length == 0 ) throw new ArgumentException ( "file" ) ;
488+
489+ using var stream = file . OpenReadStream ( ) ;
490+ using var reader = new System . IO . StreamReader ( stream , Encoding . UTF8 ) ;
491+ var content = await reader . ReadToEndAsync ( ) ;
492+ var importFile = JsonConvert . DeserializeObject < AppExportFileVM > ( content ) ;
493+ if ( importFile == null ) throw new ArgumentException ( "file" ) ;
494+
495+ return importFile ;
496+ }
497+
498+ private async Task < AppImportPreviewVM > BuildImportPreviewAsync ( AppExportFileVM importFile )
499+ {
500+ var preview = new AppImportPreviewVM ( ) ;
501+ if ( importFile ? . Apps == null || ! importFile . Apps . Any ( ) )
502+ {
503+ preview . Errors . Add ( "Import file does not contain any apps." ) ;
504+ return preview ;
505+ }
506+
507+ var appItems = importFile . Apps
508+ . Where ( x => x ? . App != null )
509+ . ToList ( ) ;
510+ if ( ! appItems . Any ( ) )
511+ {
512+ preview . Errors . Add ( "Import file does not contain any valid app entries." ) ;
513+ return preview ;
514+ }
515+
516+ var duplicateIds = appItems
517+ . GroupBy ( x => x . App . Id ?? string . Empty , StringComparer . OrdinalIgnoreCase )
518+ . Where ( x => ! string . IsNullOrWhiteSpace ( x . Key ) && x . Count ( ) > 1 )
519+ . Select ( x => x . Key )
520+ . OrderBy ( x => x , StringComparer . OrdinalIgnoreCase )
521+ . ToList ( ) ;
522+ preview . Errors . AddRange ( duplicateIds . Select ( x => $ "Duplicate AppId in import file: { x } .") ) ;
523+
524+ var duplicateNames = appItems
525+ . GroupBy ( x => x . App . Name ?? string . Empty , StringComparer . OrdinalIgnoreCase )
526+ . Where ( x => ! string . IsNullOrWhiteSpace ( x . Key ) && x . Count ( ) > 1 )
527+ . Select ( x => x . Key )
528+ . OrderBy ( x => x , StringComparer . OrdinalIgnoreCase )
529+ . ToList ( ) ;
530+ preview . Errors . AddRange ( duplicateNames . Select ( x => $ "Duplicate app name in import file: { x } .") ) ;
531+
532+ foreach ( var item in appItems )
533+ {
534+ if ( string . IsNullOrWhiteSpace ( item . App . Id ) ) preview . Errors . Add ( "Imported app is missing AppId." ) ;
535+ if ( string . IsNullOrWhiteSpace ( item . App . Name ) ) preview . Errors . Add ( "Imported app is missing Name." ) ;
536+ }
537+
538+ var importedAppIds = new HashSet < string > ( appItems . Select ( x => x . App . Id ) . Where ( x => ! string . IsNullOrWhiteSpace ( x ) ) , StringComparer . OrdinalIgnoreCase ) ;
539+ var existingApps = await _appService . GetAllAppsAsync ( ) ;
540+
541+ foreach ( var item in appItems . Where ( x => x . App != null && ! string . IsNullOrWhiteSpace ( x . App . Id ) ) )
542+ {
543+ if ( existingApps . Any ( x => string . Equals ( x . Id , item . App . Id , StringComparison . OrdinalIgnoreCase ) ) )
544+ preview . Errors . Add ( $ "AppId already exists: { item . App . Id } .") ;
545+ if ( ! string . IsNullOrWhiteSpace ( item . App . Name ) && existingApps . Any ( x => string . Equals ( x . Name , item . App . Name , StringComparison . OrdinalIgnoreCase ) ) )
546+ preview . Errors . Add ( $ "App name already exists: { item . App . Name } .") ;
547+ }
548+
549+ foreach ( var item in appItems )
550+ {
551+ foreach ( var parentId in item . App . InheritancedApps ? . Where ( x => ! string . IsNullOrWhiteSpace ( x ) ) . Distinct ( StringComparer . OrdinalIgnoreCase ) ?? new List < string > ( ) )
552+ {
553+ if ( importedAppIds . Contains ( parentId ) ) continue ;
554+ if ( existingApps . Any ( x => string . Equals ( x . Id , parentId , StringComparison . OrdinalIgnoreCase ) ) ) continue ;
555+ preview . Errors . Add ( $ "App '{ item . App . Id } ' references missing parent '{ parentId } '. Parent must already exist or be included in the import file.") ;
556+ }
557+ }
558+
559+ var orderLookup = TryTopologicalSort ( appItems , importedAppIds , preview . Errors ) ;
560+ if ( preview . Errors . Any ( ) ) return preview ;
561+
562+ preview . Apps = appItems
563+ . OrderBy ( x => orderLookup [ x . App . Id ] )
564+ . Select ( x => new AppImportPreviewItemVM
565+ {
566+ AppId = x . App . Id ,
567+ Name = x . App . Name ,
568+ Group = x . App . Group ,
569+ Enabled = x . App . Enabled ,
570+ Inheritanced = x . App . Inheritanced ,
571+ InheritancedApps = x . App . InheritancedApps ? . Where ( v => ! string . IsNullOrWhiteSpace ( v ) ) . Distinct ( StringComparer . OrdinalIgnoreCase ) . ToList ( ) ?? new List < string > ( ) ,
572+ EnvCount = x . Envs ? . Count ?? 0 ,
573+ ConfigCount = x . Envs ? . Sum ( env => env . Value ? . Count ?? 0 ) ?? 0 ,
574+ Order = orderLookup [ x . App . Id ]
575+ } )
576+ . ToList ( ) ;
577+
578+ return preview ;
579+ }
580+
581+ private static Dictionary < string , int > TryTopologicalSort ( List < AppExportItemVM > appItems , HashSet < string > importedAppIds , List < string > errors )
582+ {
583+ var dependencyMap = appItems . ToDictionary (
584+ x => x . App . Id ,
585+ x => ( x . App . InheritancedApps ?? new List < string > ( ) )
586+ . Where ( parentId => ! string . IsNullOrWhiteSpace ( parentId ) && importedAppIds . Contains ( parentId ) )
587+ . Distinct ( StringComparer . OrdinalIgnoreCase )
588+ . ToList ( ) ,
589+ StringComparer . OrdinalIgnoreCase ) ;
590+
591+ var inDegree = dependencyMap . ToDictionary ( x => x . Key , _ => 0 , StringComparer . OrdinalIgnoreCase ) ;
592+ var childMap = dependencyMap . Keys . ToDictionary ( x => x , _ => new List < string > ( ) , StringComparer . OrdinalIgnoreCase ) ;
593+
594+ foreach ( var entry in dependencyMap )
595+ {
596+ inDegree [ entry . Key ] = entry . Value . Count ;
597+ foreach ( var parentId in entry . Value ) childMap [ parentId ] . Add ( entry . Key ) ;
598+ }
599+
600+ var queue = new Queue < string > ( inDegree . Where ( x => x . Value == 0 ) . Select ( x => x . Key ) . OrderBy ( x => x , StringComparer . OrdinalIgnoreCase ) ) ;
601+ var ordered = new List < string > ( ) ;
602+ while ( queue . Any ( ) )
603+ {
604+ var next = queue . Dequeue ( ) ;
605+ ordered . Add ( next ) ;
606+
607+ foreach ( var child in childMap [ next ] . OrderBy ( x => x , StringComparer . OrdinalIgnoreCase ) )
608+ {
609+ inDegree [ child ] -- ;
610+ if ( inDegree [ child ] == 0 ) queue . Enqueue ( child ) ;
611+ }
612+ }
613+
614+ if ( ordered . Count != dependencyMap . Count )
615+ {
616+ var cyclicApps = inDegree . Where ( x => x . Value > 0 ) . Select ( x => x . Key ) . OrderBy ( x => x , StringComparer . OrdinalIgnoreCase ) ;
617+ errors . Add ( $ "Cyclic inheritance detected among imported apps: { string . Join ( ", " , cyclicApps ) } .") ;
618+ }
619+
620+ return ordered . Select ( ( appId , index ) => new { appId , index } ) . ToDictionary ( x => x . appId , x => x . index + 1 , StringComparer . OrdinalIgnoreCase ) ;
621+ }
622+
385623 /// <summary>
386624 /// Get all applications that can be inherited.
387625 /// </summary>
@@ -462,4 +700,4 @@ public async Task<IActionResult> GetAppGroups()
462700 data = groups . OrderBy ( x => x )
463701 } ) ;
464702 }
465- }
703+ }
0 commit comments