@@ -6,6 +6,8 @@ import { DockgeSocket, fileExists, ValidationError } from "./util-server";
66import  path  from  "path" ; 
77import  { 
88    acceptedComposeFileNames , 
9+     acceptedComposeFileNamePattern , 
10+     ArbitrarilyNestedLooseObject , 
911    COMBINED_TERMINAL_COLS , 
1012    COMBINED_TERMINAL_ROWS , 
1113    CREATED_FILE , 
@@ -108,7 +110,7 @@ export class Stack {
108110    } 
109111
110112    get  isManagedByDockge ( )  : boolean  { 
111-         return  fs . existsSync ( this . path )  &&  fs . statSync ( this . path ) . isDirectory ( ) ; 
113+         return  ! ! this . _configFilePath  &&  this . _configFilePath . startsWith ( this . server . stacksDir ) ; 
112114    } 
113115
114116    get  status ( )  : number  { 
@@ -157,7 +159,7 @@ export class Stack {
157159    } 
158160
159161    get  path ( )  : string  { 
160-         return  path . join ( this . server . stacksDir ,   this . name ) ; 
162+         return  this . _configFilePath   ||   "" ; 
161163    } 
162164
163165    get  fullPath ( )  : string  { 
@@ -267,41 +269,12 @@ export class Stack {
267269    } 
268270
269271    static  async  getStackList ( server  : DockgeServer ,  useCacheForManaged  =  false )  : Promise < Map < string ,  Stack > >  { 
270-         let  stacksDir  =  server . stacksDir ; 
271-         let  stackList  : Map < string ,  Stack > ; 
272+         let  stackList  : Map < string ,  Stack >  =  new  Map < string ,  Stack > ( ) ; 
272273
273274        // Use cached stack list? 
274275        if  ( useCacheForManaged  &&  this . managedStackList . size  >  0 )  { 
275276            stackList  =  this . managedStackList ; 
276-         }  else  { 
277-             stackList  =  new  Map < string ,  Stack > ( ) ; 
278- 
279-             // Scan the stacks directory, and get the stack list 
280-             let  filenameList  =  await  fsAsync . readdir ( stacksDir ) ; 
281- 
282-             for  ( let  filename  of  filenameList )  { 
283-                 try  { 
284-                     // Check if it is a directory 
285-                     let  stat  =  await  fsAsync . stat ( path . join ( stacksDir ,  filename ) ) ; 
286-                     if  ( ! stat . isDirectory ( ) )  { 
287-                         continue ; 
288-                     } 
289-                     // If no compose file exists, skip it 
290-                     if  ( ! await  Stack . composeFileExists ( stacksDir ,  filename ) )  { 
291-                         continue ; 
292-                     } 
293-                     let  stack  =  await  this . getStack ( server ,  filename ) ; 
294-                     stack . _status  =  CREATED_FILE ; 
295-                     stackList . set ( filename ,  stack ) ; 
296-                 }  catch  ( e )  { 
297-                     if  ( e  instanceof  Error )  { 
298-                         log . warn ( "getStackList" ,  `Failed to get stack ${ filename }  , error: ${ e . message }  ` ) ; 
299-                     } 
300-                 } 
301-             } 
302- 
303-             // Cache by copying 
304-             this . managedStackList  =  new  Map ( stackList ) ; 
277+             return  stackList ; 
305278        } 
306279
307280        // Get status from docker compose ls 
@@ -310,28 +283,92 @@ export class Stack {
310283        } ) ; 
311284
312285        if  ( ! res . stdout )  { 
286+             log . warn ( "getStackList" ,  "No response from docker compose daemon when attempting to retrieve list of stacks" ) ; 
313287            return  stackList ; 
314288        } 
315289
316290        let  composeList  =  JSON . parse ( res . stdout . toString ( ) ) ; 
291+         let  pathSearchTree : ArbitrarilyNestedLooseObject  =  { } ;  // search structure for matching paths 
317292
318293        for  ( let  composeStack  of  composeList )  { 
319-             let  stack  =  stackList . get ( composeStack . Name ) ; 
320- 
321-             // This stack probably is not managed by Dockge, but we still want to show it 
322-             if  ( ! stack )  { 
323-                 // Skip the dockge stack if it is not managed by Dockge 
324-                 if  ( composeStack . Name  ===  "dockge" )  { 
294+             try  { 
295+                 let  stack  =  new  Stack ( server ,  composeStack . Name ) ; 
296+                 stack . _status  =  this . statusConvert ( composeStack . Status ) ; 
297+ 
298+                 let  composeFiles  =  composeStack . ConfigFiles . split ( "," ) ;  // it is possible for a project to have more than one config file 
299+                 stack . _configFilePath  =  path . dirname ( composeFiles [ 0 ] ) ; 
300+                 stack . _composeFileName  =  path . basename ( composeFiles [ 0 ] ) ; 
301+                 if  ( stack . name  ===  "dockge"  &&  ! stack . isManagedByDockge )  { 
302+                     // skip dockge if not managed by dockge 
325303                    continue ; 
326304                } 
327-                 stack  =  new  Stack ( server ,  composeStack . Name ) ; 
328305                stackList . set ( composeStack . Name ,  stack ) ; 
306+ 
307+                 // add project path to search tree so we can quickly decide if we have seen it before later 
308+                 // e.g. path "/opt/stacks" would yield the tree { opt: stacks: {}  } 
309+                 path . join ( stack . _configFilePath ,  stack . _composeFileName ) . split ( path . sep ) . reduce ( ( searchTree ,  pathComponent )  =>  { 
310+                     if  ( pathComponent  ==  "" )  { 
311+                         return  searchTree ; 
312+                     } 
313+                     if  ( ! searchTree [ pathComponent ] )  { 
314+                         searchTree [ pathComponent ]  =  { } ; 
315+                     } 
316+                     return  searchTree [ pathComponent ] ; 
317+                 } ,  pathSearchTree ) ; 
318+             }  catch  ( e )  { 
319+                 if  ( e  instanceof  Error )  { 
320+                     log . error ( "getStackList" ,  `Failed to get stack ${ composeStack . Name }  , error: ${ e . message }  ` ) ; 
321+                 } 
329322            } 
323+         } 
324+ 
325+         // Search stacks directory for compose files not associated with a running compose project (ie. never started through CLI) 
326+         try  { 
327+             // Hopefully the user has access to everything in this directory! If they don't, log the error. It is a small price to pay for fast searching. 
328+             let  rawFilesList  =  fs . readdirSync ( server . stacksDir ,  { 
329+                 recursive : true , 
330+                 withFileTypes : true 
331+             } ) ; 
332+             let  acceptedComposeFiles  =  rawFilesList . filter ( ( dirEnt : fs . Dirent )  =>  dirEnt . isFile ( )  &&  ! ! dirEnt . name . match ( acceptedComposeFileNamePattern ) ) ; 
333+             log . debug ( "getStackList" ,  `Folder scan yielded ${ acceptedComposeFiles . length }   files` ) ; 
334+             for  ( let  composeFile  of  acceptedComposeFiles )  { 
335+                 // check if we have seen this file before 
336+                 let  fullPath  =  composeFile . parentPath ; 
337+                 let  previouslySeen  =  fullPath . split ( path . sep ) . reduce ( ( searchTree : ArbitrarilyNestedLooseObject  |  boolean ,  pathComponent )  =>  { 
338+                     if  ( pathComponent  ==  "" )  { 
339+                         return  searchTree ; 
340+                     } 
330341
331-             stack . _status  =  this . statusConvert ( composeStack . Status ) ; 
332-             stack . _configFilePath  =  composeStack . ConfigFiles ; 
342+                     // end condition 
343+                     if  ( searchTree  ==  false  ||  ! ( searchTree  as  ArbitrarilyNestedLooseObject ) [ pathComponent ] )  { 
344+                         return  false ; 
345+                     } 
346+ 
347+                     // path (so far) has been previously seen 
348+                     return  ( searchTree  as  ArbitrarilyNestedLooseObject ) [ pathComponent ] ; 
349+                 } ,  pathSearchTree ) ; 
350+                 if  ( ! previouslySeen )  { 
351+                     // a file with an accepted compose filename has been found that did not appear in `docker compose ls`. Use its config file path as a temp name 
352+                     log . info ( "getStackList" ,  `Found project unknown to docker compose: ${ fullPath }  /${ composeFile . name }  ` ) ; 
353+                     let  [  configFilePath ,  configFilename ,  inferredProjectName  ]  =  [  fullPath ,  composeFile . name ,  path . basename ( fullPath )  ] ; 
354+                     if  ( stackList . get ( inferredProjectName ) )  { 
355+                         log . info ( "getStackList" ,  `... but it was ignored. A project named ${ inferredProjectName }   already exists` ) ; 
356+                     }  else  { 
357+                         let  stack  =  new  Stack ( server ,  inferredProjectName ) ; 
358+                         stack . _status  =  UNKNOWN ; 
359+                         stack . _configFilePath  =  configFilePath ; 
360+                         stack . _composeFileName  =  configFilename ; 
361+                         stackList . set ( inferredProjectName ,  stack ) ; 
362+                     } 
363+                 } 
364+             } 
365+         }  catch  ( e )  { 
366+             if  ( e  instanceof  Error )  { 
367+                 log . error ( "getStackList" ,  `Got error searching for undiscovered stacks:\n${ e . message }  ` ) ; 
368+             } 
333369        } 
334370
371+         this . managedStackList  =  stackList ; 
335372        return  stackList ; 
336373    } 
337374
@@ -379,35 +416,24 @@ export class Stack {
379416    } 
380417
381418    static  async  getStack ( server : DockgeServer ,  stackName : string ,  skipFSOperations  =  false )  : Promise < Stack >  { 
382-         let  dir  =  path . join ( server . stacksDir ,  stackName ) ; 
383- 
419+         let  stack : Stack  |  undefined ; 
384420        if  ( ! skipFSOperations )  { 
385-             if  ( ! await  fileExists ( dir )  ||  ! ( await  fsAsync . stat ( dir ) ) . isDirectory ( ) )  { 
386-                 // Maybe it is a stack managed by docker compose directly 
387-                 let  stackList  =  await  this . getStackList ( server ,  true ) ; 
388-                 let  stack  =  stackList . get ( stackName ) ; 
389- 
390-                 if  ( stack )  { 
391-                     return  stack ; 
392-                 }  else  { 
393-                     // Really not found 
394-                     throw  new  ValidationError ( "Stack not found" ) ; 
395-                 } 
421+             let  stackList  =  await  this . getStackList ( server ,  true ) ; 
422+             stack  =  stackList . get ( stackName ) ; 
423+             if  ( ! stack  ||  ! await  fileExists ( stack . path )  ||  ! ( await  fsAsync . stat ( stack . path ) ) . isDirectory ( )  )  { 
424+                 throw  new  ValidationError ( `getStack; Stack ${ stackName }   not found` ) ; 
396425            } 
397426        }  else  { 
398-             //log.debug("getStack", "Skip FS operations");  
399-         } 
400- 
401-         let   stack  :  Stack ; 
402- 
403-         if   ( ! skipFSOperations )   { 
404-             stack   =   new   Stack ( server ,   stackName ) ; 
405-         }   else   { 
406-             stack   =   new   Stack ( server ,   stackName ,   undefined ,   undefined ,   true ) ; 
427+             // search for known stack with this name  
428+              if   ( this . managedStackList )   { 
429+                  stack   =   this . managedStackList . get ( stackName ) ; 
430+              } 
431+              if   ( ! this . managedStackList   ||   ! stack )   { 
432+                  stack   =   new   Stack ( server ,   stackName ,   undefined ,   undefined ,   true ) ; 
433+                  stack . _status   =   UNKNOWN ; 
434+                  stack . _configFilePath   =   path . resolve ( server . stacksDir ,   stackName ) ; 
435+             } 
407436        } 
408- 
409-         stack . _status  =  UNKNOWN ; 
410-         stack . _configFilePath  =  path . resolve ( dir ) ; 
411437        return  stack ; 
412438    } 
413439
@@ -536,7 +562,6 @@ export class Stack {
536562                }  catch  ( e )  { 
537563                } 
538564            } 
539- 
540565            return  statusList ; 
541566        }  catch  ( e )  { 
542567            log . error ( "getServiceStatusList" ,  e ) ; 
0 commit comments