99 OpenLocalFileParams ,
1010 OpenLocalFolderParams ,
1111 RenameLocalFileResult ,
12+ WriteLocalFileParams ,
1213} from '@lobechat/electron-client-ipc' ;
1314import { SYSTEM_FILES_TO_IGNORE , loadFile } from '@lobechat/file-loaders' ;
1415import { shell } from 'electron' ;
@@ -20,13 +21,18 @@ import { promisify } from 'node:util';
2021import FileSearchService from '@/services/fileSearchSrv' ;
2122import { FileResult , SearchOptions } from '@/types/fileSearch' ;
2223import { makeSureDirExist } from '@/utils/file-system' ;
24+ import { createLogger } from '@/utils/logger' ;
2325
2426import { ControllerModule , ipcClientEvent } from './index' ;
2527
28+ // 创建日志记录器
29+ const logger = createLogger ( 'controllers:LocalFileCtr' ) ;
30+
2631const statPromise = promisify ( fs . stat ) ;
2732const readdirPromise = promisify ( fs . readdir ) ;
2833const renamePromiseFs = promisify ( fs . rename ) ;
2934const accessPromise = promisify ( fs . access ) ;
35+ const writeFilePromise = promisify ( fs . writeFile ) ;
3036
3137export default class LocalFileCtr extends ControllerModule {
3238 private get searchService ( ) {
@@ -38,23 +44,35 @@ export default class LocalFileCtr extends ControllerModule {
3844 */
3945 @ipcClientEvent ( 'searchLocalFiles' )
4046 async handleLocalFilesSearch ( params : LocalSearchFilesParams ) : Promise < FileResult [ ] > {
47+ logger . debug ( 'Received file search request:' , { keywords : params . keywords } ) ;
48+
4149 const options : Omit < SearchOptions , 'keywords' > = {
4250 limit : 30 ,
4351 } ;
4452
45- return this . searchService . search ( params . keywords , options ) ;
53+ try {
54+ const results = await this . searchService . search ( params . keywords , options ) ;
55+ logger . debug ( 'File search completed' , { count : results . length } ) ;
56+ return results ;
57+ } catch ( error ) {
58+ logger . error ( 'File search failed:' , error ) ;
59+ return [ ] ;
60+ }
4661 }
4762
4863 @ipcClientEvent ( 'openLocalFile' )
4964 async handleOpenLocalFile ( { path : filePath } : OpenLocalFileParams ) : Promise < {
5065 error ?: string ;
5166 success : boolean ;
5267 } > {
68+ logger . debug ( 'Attempting to open file:' , { filePath } ) ;
69+
5370 try {
5471 await shell . openPath ( filePath ) ;
72+ logger . debug ( 'File opened successfully:' , { filePath } ) ;
5573 return { success : true } ;
5674 } catch ( error ) {
57- console . error ( `Failed to open file ${ filePath } :` , error ) ;
75+ logger . error ( `Failed to open file ${ filePath } :` , error ) ;
5876 return { error : ( error as Error ) . message , success : false } ;
5977 }
6078 }
@@ -64,35 +82,42 @@ export default class LocalFileCtr extends ControllerModule {
6482 error ?: string ;
6583 success : boolean ;
6684 } > {
85+ const folderPath = isDirectory ? targetPath : path . dirname ( targetPath ) ;
86+ logger . debug ( 'Attempting to open folder:' , { folderPath, isDirectory, targetPath } ) ;
87+
6788 try {
68- const folderPath = isDirectory ? targetPath : path . dirname ( targetPath ) ;
6989 await shell . openPath ( folderPath ) ;
90+ logger . debug ( 'Folder opened successfully:' , { folderPath } ) ;
7091 return { success : true } ;
7192 } catch ( error ) {
72- console . error ( `Failed to open folder for path ${ targetPath } :` , error ) ;
93+ logger . error ( `Failed to open folder ${ folderPath } :` , error ) ;
7394 return { error : ( error as Error ) . message , success : false } ;
7495 }
7596 }
7697
7798 @ipcClientEvent ( 'readLocalFiles' )
7899 async readFiles ( { paths } : LocalReadFilesParams ) : Promise < LocalReadFileResult [ ] > {
100+ logger . debug ( 'Starting batch file reading:' , { count : paths . length } ) ;
101+
79102 const results : LocalReadFileResult [ ] = [ ] ;
80103
81104 for ( const filePath of paths ) {
82105 // 初始化结果对象
106+ logger . debug ( 'Reading single file:' , { filePath } ) ;
83107 const result = await this . readFile ( { path : filePath } ) ;
84-
85108 results . push ( result ) ;
86109 }
87110
111+ logger . debug ( 'Batch file reading completed' , { count : results . length } ) ;
88112 return results ;
89113 }
90114
91115 @ipcClientEvent ( 'readLocalFile' )
92116 async readFile ( { path : filePath , loc } : LocalReadFileParams ) : Promise < LocalReadFileResult > {
93- try {
94- const effectiveLoc = loc ?? [ 0 , 200 ] ;
117+ const effectiveLoc = loc ?? [ 0 , 200 ] ;
118+ logger . debug ( 'Starting to read file:' , { filePath , loc : effectiveLoc } ) ;
95119
120+ try {
96121 const fileDocument = await loadFile ( filePath ) ;
97122
98123 const [ startLine , endLine ] = effectiveLoc ;
@@ -106,6 +131,13 @@ export default class LocalFileCtr extends ControllerModule {
106131 const charCount = content . length ;
107132 const lineCount = selectedLines . length ;
108133
134+ logger . debug ( 'File read successfully:' , {
135+ filePath,
136+ selectedLineCount : lineCount ,
137+ totalCharCount,
138+ totalLineCount,
139+ } ) ;
140+
109141 const result : LocalReadFileResult = {
110142 // Char count for the selected range
111143 charCount,
@@ -128,6 +160,7 @@ export default class LocalFileCtr extends ControllerModule {
128160 try {
129161 const stats = await statPromise ( filePath ) ;
130162 if ( stats . isDirectory ( ) ) {
163+ logger . warn ( 'Attempted to read directory content:' , { filePath } ) ;
131164 result . content = 'This is a directory and cannot be read as plain text.' ;
132165 result . charCount = 0 ;
133166 result . lineCount = 0 ;
@@ -136,12 +169,12 @@ export default class LocalFileCtr extends ControllerModule {
136169 result . totalLineCount = 0 ;
137170 }
138171 } catch ( statError ) {
139- console . error ( `Stat failed for ${ filePath } after loadFile :` , statError ) ;
172+ logger . error ( `Failed to get file status ${ filePath } :` , statError ) ;
140173 }
141174
142175 return result ;
143176 } catch ( error ) {
144- console . error ( `Error processing file ${ filePath } :` , error ) ;
177+ logger . error ( `Failed to read file ${ filePath } :` , error ) ;
145178 const errorMessage = ( error as Error ) . message ;
146179 return {
147180 charCount : 0 ,
@@ -160,13 +193,20 @@ export default class LocalFileCtr extends ControllerModule {
160193
161194 @ipcClientEvent ( 'listLocalFiles' )
162195 async listLocalFiles ( { path : dirPath } : ListLocalFileParams ) : Promise < FileResult [ ] > {
196+ logger . debug ( 'Listing directory contents:' , { dirPath } ) ;
197+
163198 const results : FileResult [ ] = [ ] ;
164199 try {
165200 const entries = await readdirPromise ( dirPath ) ;
201+ logger . debug ( 'Directory entries retrieved successfully:' , {
202+ dirPath,
203+ entriesCount : entries . length ,
204+ } ) ;
166205
167206 for ( const entry of entries ) {
168207 // Skip specific system files based on the ignore list
169208 if ( SYSTEM_FILES_TO_IGNORE . includes ( entry ) ) {
209+ logger . debug ( 'Ignoring system file:' , { fileName : entry } ) ;
170210 continue ;
171211 }
172212
@@ -186,7 +226,7 @@ export default class LocalFileCtr extends ControllerModule {
186226 } ) ;
187227 } catch ( statError ) {
188228 // Silently ignore files we can't stat (e.g. permissions)
189- console . error ( `Failed to stat ${ fullPath } :` , statError ) ;
229+ logger . error ( `Failed to get file status ${ fullPath } :` , statError ) ;
190230 }
191231 }
192232
@@ -199,9 +239,10 @@ export default class LocalFileCtr extends ControllerModule {
199239 return ( a . name || '' ) . localeCompare ( b . name || '' ) ; // Then sort by name
200240 } ) ;
201241
242+ logger . debug ( 'Directory listing successful' , { dirPath, resultCount : results . length } ) ;
202243 return results ;
203244 } catch ( error ) {
204- console . error ( `Failed to list directory ${ dirPath } :` , error ) ;
245+ logger . error ( `Failed to list directory ${ dirPath } :` , error ) ;
205246 // Rethrow or return an empty array/error object depending on desired behavior
206247 // For now, returning empty array on error listing directory itself
207248 return [ ] ;
@@ -210,16 +251,21 @@ export default class LocalFileCtr extends ControllerModule {
210251
211252 @ipcClientEvent ( 'moveLocalFiles' )
212253 async handleMoveFiles ( { items } : MoveLocalFilesParams ) : Promise < LocalMoveFilesResultItem [ ] > {
254+ logger . debug ( 'Starting batch file move:' , { itemsCount : items ?. length } ) ;
255+
213256 const results : LocalMoveFilesResultItem [ ] = [ ] ;
214257
215258 if ( ! items || items . length === 0 ) {
216- console . warn ( 'moveLocalFiles called with empty items array. ' ) ;
259+ logger . warn ( 'moveLocalFiles called with empty parameters ' ) ;
217260 return [ ] ;
218261 }
219262
220263 // 逐个处理移动请求
221264 for ( const item of items ) {
222265 const { oldPath : sourcePath , newPath } = item ;
266+ const logPrefix = `[Moving file ${ sourcePath } -> ${ newPath } ]` ;
267+ logger . debug ( `${ logPrefix } Starting process` ) ;
268+
223269 const resultItem : LocalMoveFilesResultItem = {
224270 newPath : undefined ,
225271 sourcePath,
@@ -228,6 +274,7 @@ export default class LocalFileCtr extends ControllerModule {
228274
229275 // 基本验证
230276 if ( ! sourcePath || ! newPath ) {
277+ logger . error ( `${ logPrefix } Parameter validation failed: source or target path is empty` ) ;
231278 resultItem . error = 'Both oldPath and newPath are required for each item.' ;
232279 results . push ( resultItem ) ;
233280 continue ;
@@ -237,10 +284,13 @@ export default class LocalFileCtr extends ControllerModule {
237284 // 检查源是否存在
238285 try {
239286 await accessPromise ( sourcePath , fs . constants . F_OK ) ;
287+ logger . debug ( `${ logPrefix } Source file exists` ) ;
240288 } catch ( accessError : any ) {
241289 if ( accessError . code === 'ENOENT' ) {
290+ logger . error ( `${ logPrefix } Source file does not exist` ) ;
242291 throw new Error ( `Source path not found: ${ sourcePath } ` ) ;
243292 } else {
293+ logger . error ( `${ logPrefix } Permission error accessing source file:` , accessError ) ;
244294 throw new Error (
245295 `Permission denied accessing source path: ${ sourcePath } . ${ accessError . message } ` ,
246296 ) ;
@@ -249,7 +299,7 @@ export default class LocalFileCtr extends ControllerModule {
249299
250300 // 检查目标路径是否与源路径相同
251301 if ( path . normalize ( sourcePath ) === path . normalize ( newPath ) ) {
252- console . log ( `Skipping move: source and target path are identical: ${ sourcePath } `) ;
302+ logger . info ( ` ${ logPrefix } Source and target paths are identical, skipping move `) ;
253303 resultItem . success = true ;
254304 resultItem . newPath = newPath ; // 即使未移动,也报告目标路径
255305 results . push ( resultItem ) ;
@@ -259,14 +309,15 @@ export default class LocalFileCtr extends ControllerModule {
259309 // LBYL: 确保目标目录存在
260310 const targetDir = path . dirname ( newPath ) ;
261311 makeSureDirExist ( targetDir ) ;
312+ logger . debug ( `${ logPrefix } Ensured target directory exists: ${ targetDir } ` ) ;
262313
263314 // 执行移动 (rename)
264315 await renamePromiseFs ( sourcePath , newPath ) ;
265316 resultItem . success = true ;
266317 resultItem . newPath = newPath ;
267- console . log ( `Successfully moved ${ sourcePath } to ${ newPath } `) ;
318+ logger . info ( ` ${ logPrefix } Move successful `) ;
268319 } catch ( error ) {
269- console . error ( `Error moving ${ sourcePath } to ${ newPath } :` , error ) ;
320+ logger . error ( `${ logPrefix } Move failed :` , error ) ;
270321 // 使用与 handleMoveFile 类似的错误处理逻辑
271322 let errorMessage = ( error as Error ) . message ;
272323 if ( ( error as any ) . code === 'ENOENT' )
@@ -296,6 +347,10 @@ export default class LocalFileCtr extends ControllerModule {
296347 results . push ( resultItem ) ;
297348 }
298349
350+ logger . debug ( 'Batch file move completed' , {
351+ successCount : results . filter ( ( r ) => r . success ) . length ,
352+ totalCount : results . length ,
353+ } ) ;
299354 return results ;
300355 }
301356
@@ -307,8 +362,12 @@ export default class LocalFileCtr extends ControllerModule {
307362 newName : string ;
308363 path : string ;
309364 } ) : Promise < RenameLocalFileResult > {
365+ const logPrefix = `[Renaming ${ currentPath } -> ${ newName } ]` ;
366+ logger . debug ( `${ logPrefix } Starting rename request` ) ;
367+
310368 // Basic validation (can also be done in frontend action)
311369 if ( ! currentPath || ! newName ) {
370+ logger . error ( `${ logPrefix } Parameter validation failed: path or new name is empty` ) ;
312371 return { error : 'Both path and newName are required.' , newPath : '' , success : false } ;
313372 }
314373 // Prevent path traversal or using invalid characters/names
@@ -319,6 +378,7 @@ export default class LocalFileCtr extends ControllerModule {
319378 newName === '..' ||
320379 / [ " * / : < > ? \\ | ] / . test ( newName ) // Check for typical invalid filename characters
321380 ) {
381+ logger . error ( `${ logPrefix } New filename contains illegal characters: ${ newName } ` ) ;
322382 return {
323383 error :
324384 'Invalid new name. It cannot contain path separators (/, \\), be "." or "..", or include characters like < > : " / \\ | ? *.' ,
@@ -331,18 +391,19 @@ export default class LocalFileCtr extends ControllerModule {
331391 try {
332392 const dir = path . dirname ( currentPath ) ;
333393 newPath = path . join ( dir , newName ) ;
394+ logger . debug ( `${ logPrefix } Calculated new path: ${ newPath } ` ) ;
334395
335396 // Check if paths are identical after calculation
336397 if ( path . normalize ( currentPath ) === path . normalize ( newPath ) ) {
337- console . log (
338- `Skipping rename: oldPath and calculated newPath are identical: ${ currentPath } ` ,
398+ logger . info (
399+ `${ logPrefix } Source path and calculated target path are identical, skipping rename ` ,
339400 ) ;
340401 // Consider success as no change is needed, but maybe inform the user?
341402 // Return success for now.
342403 return { newPath, success : true } ;
343404 }
344405 } catch ( error ) {
345- console . error ( `Error calculating new path for rename ${ currentPath } to ${ newName } :` , error ) ;
406+ logger . error ( `${ logPrefix } Failed to calculate new path :` , error ) ;
346407 return {
347408 error : `Internal error calculating the new path: ${ ( error as Error ) . message } ` ,
348409 newPath : '' ,
@@ -353,12 +414,12 @@ export default class LocalFileCtr extends ControllerModule {
353414 // Perform the rename operation using fs.promises.rename directly
354415 try {
355416 await renamePromise ( currentPath , newPath ) ;
356- console . log ( `Successfully renamed ${ currentPath } to ${ newPath } `) ;
417+ logger . info ( ` ${ logPrefix } Rename successful: ${ currentPath } -> ${ newPath } `) ;
357418 // Optionally return the newPath if frontend needs it
358419 // return { success: true, newPath: newPath };
359420 return { newPath, success : true } ;
360421 } catch ( error ) {
361- console . error ( `Error renaming ${ currentPath } to ${ newPath } :` , error ) ;
422+ logger . error ( `${ logPrefix } Rename failed :` , error ) ;
362423 let errorMessage = ( error as Error ) . message ;
363424 // Provide more specific error messages based on common codes
364425 if ( ( error as any ) . code === 'ENOENT' ) {
@@ -377,4 +438,44 @@ export default class LocalFileCtr extends ControllerModule {
377438 return { error : errorMessage , newPath : '' , success : false } ;
378439 }
379440 }
441+
442+ @ipcClientEvent ( 'writeLocalFile' )
443+ async handleWriteFile ( { path : filePath , content } : WriteLocalFileParams ) {
444+ const logPrefix = `[Writing file ${ filePath } ]` ;
445+ logger . debug ( `${ logPrefix } Starting to write file` , { contentLength : content ?. length } ) ;
446+
447+ // 验证参数
448+ if ( ! filePath ) {
449+ logger . error ( `${ logPrefix } Parameter validation failed: path is empty` ) ;
450+ return { error : 'Path cannot be empty' , success : false } ;
451+ }
452+
453+ if ( content === undefined ) {
454+ logger . error ( `${ logPrefix } Parameter validation failed: content is empty` ) ;
455+ return { error : 'Content cannot be empty' , success : false } ;
456+ }
457+
458+ try {
459+ // 确保目标目录存在
460+ const dirname = path . dirname ( filePath ) ;
461+ logger . debug ( `${ logPrefix } Creating directory: ${ dirname } ` ) ;
462+ fs . mkdirSync ( dirname , { recursive : true } ) ;
463+
464+ // 写入文件内容
465+ logger . debug ( `${ logPrefix } Starting to write content to file` ) ;
466+ await writeFilePromise ( filePath , content , 'utf8' ) ;
467+ logger . info ( `${ logPrefix } File written successfully` , {
468+ path : filePath ,
469+ size : content . length ,
470+ } ) ;
471+
472+ return { success : true } ;
473+ } catch ( error ) {
474+ logger . error ( `${ logPrefix } Failed to write file:` , error ) ;
475+ return {
476+ error : `Failed to write file: ${ ( error as Error ) . message } ` ,
477+ success : false ,
478+ } ;
479+ }
480+ }
380481}
0 commit comments