1- use std:: path:: PathBuf ;
1+ use std:: ffi:: OsStr ;
2+ use std:: fs;
3+ use std:: path:: { Path , PathBuf } ;
24
35use chrono:: Utc ;
4- use serde:: Serialize ;
6+ use serde:: { Deserialize , Serialize } ;
57
68use crate :: db:: RuleType ;
79use crate :: error:: AppError ;
8- use crate :: server:: ProjectLayout ;
910use crate :: server:: sync:: sync_to_gitea_all;
1011use crate :: server:: {
11- OperationLogAction , OperationLogBiz , OperationLogParams , Setting ,
12+ OperationLogAction , OperationLogBiz , OperationLogParams , ProjectLayout , Setting ,
1213 write_operation_log_for_result,
1314} ;
1415use crate :: utils:: knowledge:: reload_knowledge;
15- use crate :: utils:: project_check:: check_component ;
16+ use crate :: utils:: project_check:: check_component_in_dir ;
1617use crate :: utils:: { ProjectSnapshot , load_project_snapshot_from_layout} ;
1718
19+ const LEGACY_REQUIRED_DIRS : [ & str ; 4 ] = [ "conf" , "connectors" , "topology" , "models" ] ;
20+
21+ #[ derive( Debug , Deserialize ) ]
22+ pub struct ProjectImportRequest {
23+ pub source_dir : String ,
24+ }
25+
1826#[ derive( Serialize ) ]
1927pub struct ProjectImportResponse {
2028 pub summary : ProjectImportSummary ,
@@ -30,7 +38,9 @@ pub struct ProjectImportSummary {
3038 pub rule_breakdown : Vec < ProjectImportBreakdown > ,
3139 pub warnings : Vec < String > ,
3240 pub failed_files : usize ,
33- pub project_root : String ,
41+ pub source_dir : String ,
42+ pub project_models : String ,
43+ pub project_infra : String ,
3444}
3545
3646#[ derive( Serialize ) ]
@@ -47,22 +57,24 @@ pub struct ProjectImportValidation {
4757
4858pub async fn import_project_from_files_logic (
4959 operator : Option < String > ,
60+ req : ProjectImportRequest ,
5061) -> Result < ProjectImportResponse , AppError > {
5162 let operator_for_log = operator. clone ( ) ;
5263 let setting = Setting :: load ( ) ;
5364 let layout = setting. project_layout ( ) ;
54- let project_path = resolve_project_path ( & layout) ;
65+ let source_dir = normalize_source_dir ( & req. source_dir ) ?;
66+
67+ validate_legacy_project_dir ( & source_dir) ?;
68+ check_component_in_dir ( & source_dir, RuleType :: All . to_check_component ( ) ) ?;
69+ overwrite_project_layout_from_legacy_dir ( & source_dir, & layout) ?;
5570
5671 let snapshot = load_project_snapshot_from_layout ( & layout) ?;
5772 if snapshot. rules . is_empty ( ) && snapshot. knowledge . is_empty ( ) {
5873 return Err ( AppError :: validation (
59- "项目目录中未找到可导入的规则或知识库 " . to_string ( ) ,
74+ "导入后的项目目录中未找到可导入的规则或知识库 " . to_string ( ) ,
6075 ) ) ;
6176 }
6277
63- // 文件已经是主数据源;这里先执行组件校验,避免坏配置进入后续同步/发布链路。
64- check_component ( RuleType :: All . to_check_component ( ) ) ?;
65-
6678 let result = async {
6779 let ProjectSnapshot {
6880 rules,
@@ -99,12 +111,14 @@ pub async fn import_project_from_files_logic(
99111 rule_breakdown : breakdown,
100112 warnings,
101113 failed_files,
102- project_root : project_path. to_string_lossy ( ) . to_string ( ) ,
114+ source_dir : source_dir. to_string_lossy ( ) . to_string ( ) ,
115+ project_models : layout. models_root . to_string_lossy ( ) . to_string ( ) ,
116+ project_infra : layout. infra_root . to_string_lossy ( ) . to_string ( ) ,
103117 } ;
104118
105119 let validation = ProjectImportValidation {
106120 passed : true ,
107- message : "项目组件校验通过 " . to_string ( ) ,
121+ message : "目录拆分覆盖并校验通过 " . to_string ( ) ,
108122 } ;
109123
110124 Ok :: < _ , AppError > ( ProjectImportResponse {
@@ -118,6 +132,7 @@ pub async fn import_project_from_files_logic(
118132 if let Some ( op) = operator_for_log {
119133 log_params = log_params. with_operator ( op) ;
120134 }
135+ log_params = log_params. with_field ( "source_dir" , source_dir. to_string_lossy ( ) . to_string ( ) ) ;
121136 if let Ok ( ref resp) = result {
122137 log_params = log_params
123138 . with_field ( "rules_deleted" , resp. summary . rules_deleted . to_string ( ) )
@@ -126,7 +141,8 @@ pub async fn import_project_from_files_logic(
126141 "knowledge_imported" ,
127142 resp. summary . knowledge_imported . to_string ( ) ,
128143 )
129- . with_field ( "project_root" , resp. summary . project_root . clone ( ) ) ;
144+ . with_field ( "project_models" , resp. summary . project_models . clone ( ) )
145+ . with_field ( "project_infra" , resp. summary . project_infra . clone ( ) ) ;
130146 }
131147
132148 write_operation_log_for_result (
@@ -140,14 +156,147 @@ pub async fn import_project_from_files_logic(
140156 result
141157}
142158
143- fn resolve_project_path ( layout : & ProjectLayout ) -> PathBuf {
144- let base = Setting :: workspace_root ( )
145- . join ( "tmp" )
146- . join ( "project-import-view" ) ;
147- let summary = format ! (
148- "{} + {}" ,
159+ fn normalize_source_dir ( raw : & str ) -> Result < PathBuf , AppError > {
160+ let trimmed = raw. trim ( ) ;
161+ if trimmed. is_empty ( ) {
162+ return Err ( AppError :: validation ( "请输入待导入的源目录" . to_string ( ) ) ) ;
163+ }
164+
165+ let path = PathBuf :: from ( trimmed) ;
166+ let normalized = if path. is_absolute ( ) {
167+ path
168+ } else {
169+ Setting :: workspace_root ( ) . join ( path)
170+ } ;
171+
172+ if !normalized. is_dir ( ) {
173+ return Err ( AppError :: validation ( format ! (
174+ "源目录不存在或不是目录: {}" ,
175+ normalized. display( )
176+ ) ) ) ;
177+ }
178+
179+ Ok ( normalized)
180+ }
181+
182+ fn validate_legacy_project_dir ( source_dir : & Path ) -> Result < ( ) , AppError > {
183+ let missing_dirs: Vec < String > = LEGACY_REQUIRED_DIRS
184+ . iter ( )
185+ . filter_map ( |name| {
186+ let path = source_dir. join ( name) ;
187+ if path. is_dir ( ) {
188+ None
189+ } else {
190+ Some ( ( * name) . to_string ( ) )
191+ }
192+ } )
193+ . collect ( ) ;
194+
195+ if !missing_dirs. is_empty ( ) {
196+ return Err ( AppError :: validation ( format ! (
197+ "源目录结构不完整,缺少必要目录: {}" ,
198+ missing_dirs. join( ", " )
199+ ) ) ) ;
200+ }
201+
202+ Ok ( ( ) )
203+ }
204+
205+ fn overwrite_project_layout_from_legacy_dir (
206+ source_dir : & Path ,
207+ layout : & ProjectLayout ,
208+ ) -> Result < ( ) , AppError > {
209+ info ! (
210+ "开始按旧目录结构拆分覆盖双仓库: source_dir={}, models_dir={}, infra_dir={}" ,
211+ source_dir. display( ) ,
212+ layout. models_root. display( ) ,
213+ layout. infra_root. display( )
214+ ) ;
215+
216+ recreate_dir_preserving_git ( & layout. models_root ) ?;
217+ recreate_dir_preserving_git ( & layout. infra_root ) ?;
218+
219+ copy_named_entry ( source_dir, & layout. infra_root , "conf" ) ?;
220+ copy_named_entry ( source_dir, & layout. infra_root , "connectors" ) ?;
221+ copy_named_entry ( source_dir, & layout. infra_root , "topology" ) ?;
222+ copy_named_entry ( source_dir, & layout. models_root , "models" ) ?;
223+
224+ info ! (
225+ "旧目录拆分覆盖完成: source_dir={}, models_dir={}, infra_dir={}" ,
226+ source_dir. display( ) ,
149227 layout. models_root. display( ) ,
150228 layout. infra_root. display( )
151229 ) ;
152- PathBuf :: from ( format ! ( "{} ({})" , base. display( ) , summary) )
230+ Ok ( ( ) )
231+ }
232+
233+ fn recreate_dir_preserving_git ( target_dir : & Path ) -> Result < ( ) , AppError > {
234+ fs:: create_dir_all ( target_dir) . map_err ( AppError :: internal) ?;
235+
236+ for entry in fs:: read_dir ( target_dir) . map_err ( AppError :: internal) ? {
237+ let entry = entry. map_err ( AppError :: internal) ?;
238+ let path = entry. path ( ) ;
239+ if entry. file_name ( ) == OsStr :: new ( ".git" ) {
240+ continue ;
241+ }
242+
243+ if path. is_dir ( ) {
244+ fs:: remove_dir_all ( & path) . map_err ( AppError :: internal) ?;
245+ } else {
246+ fs:: remove_file ( & path) . map_err ( AppError :: internal) ?;
247+ }
248+ }
249+
250+ Ok ( ( ) )
251+ }
252+
253+ fn copy_named_entry ( source_root : & Path , target_root : & Path , name : & str ) -> Result < ( ) , AppError > {
254+ let source = source_root. join ( name) ;
255+ if !source. exists ( ) {
256+ return Err ( AppError :: validation ( format ! (
257+ "源目录缺少必要内容: {}" ,
258+ source. display( )
259+ ) ) ) ;
260+ }
261+
262+ let target = target_root. join ( name) ;
263+ if target. exists ( ) {
264+ if target. is_dir ( ) {
265+ fs:: remove_dir_all ( & target) . map_err ( AppError :: internal) ?;
266+ } else {
267+ fs:: remove_file ( & target) . map_err ( AppError :: internal) ?;
268+ }
269+ }
270+
271+ if source. is_dir ( ) {
272+ copy_dir_recursive ( & source, & target) ?;
273+ } else {
274+ if let Some ( parent) = target. parent ( ) {
275+ fs:: create_dir_all ( parent) . map_err ( AppError :: internal) ?;
276+ }
277+ fs:: copy ( & source, & target) . map_err ( AppError :: internal) ?;
278+ }
279+
280+ Ok ( ( ) )
281+ }
282+
283+ fn copy_dir_recursive ( source_dir : & Path , target_dir : & Path ) -> Result < ( ) , AppError > {
284+ fs:: create_dir_all ( target_dir) . map_err ( AppError :: internal) ?;
285+
286+ for entry in fs:: read_dir ( source_dir) . map_err ( AppError :: internal) ? {
287+ let entry = entry. map_err ( AppError :: internal) ?;
288+ let source_path = entry. path ( ) ;
289+ let target_path = target_dir. join ( entry. file_name ( ) ) ;
290+
291+ if source_path. is_dir ( ) {
292+ copy_dir_recursive ( & source_path, & target_path) ?;
293+ } else if source_path. is_file ( ) {
294+ if let Some ( parent) = target_path. parent ( ) {
295+ fs:: create_dir_all ( parent) . map_err ( AppError :: internal) ?;
296+ }
297+ fs:: copy ( & source_path, & target_path) . map_err ( AppError :: internal) ?;
298+ }
299+ }
300+
301+ Ok ( ( ) )
153302}
0 commit comments