11import { promisify } from 'util' ;
2+ import * as fs from 'fs' ;
23import _read from 'read' ;
34import { WebApi , getBasicHandler } from 'azure-devops-node-api/WebApi' ;
45import { IGalleryApi , GalleryApi } from 'azure-devops-node-api/GalleryApi' ;
@@ -184,88 +185,183 @@ export function patchOptionsWithManifest(options: any, manifest: Manifest): void
184185 }
185186}
186187
187- export function generateFileStructureTree ( rootFolder : string , filePaths : string [ ] , maxPrint : number = Number . MAX_VALUE ) : string [ ] {
188+ export function bytesToString ( bytes : number ) : string {
189+ let size = 0 ;
190+ let unit = '' ;
191+
192+ if ( bytes > 1048576 ) {
193+ size = Math . round ( bytes / 10485.76 ) / 100 ;
194+ unit = 'MB' ;
195+ } else {
196+ size = Math . round ( bytes / 10.24 ) / 100 ;
197+ unit = 'KB' ;
198+ }
199+ return `${ size } ${ unit } ` ;
200+ }
201+
202+ const FOLDER_SIZE_KEY = "/__FOlDER_SIZE__\\" ;
203+ const FOLDER_FILES_TOTAL_KEY = "/__FOLDER_CHILDREN__\\" ;
204+ const FILE_SIZE_WARNING_THRESHOLD = 0.85 ;
205+ const FILE_SIZE_LARGE_THRESHOLD = 0.2 ;
206+
207+ export async function generateFileStructureTree ( rootFolder : string , filePaths : { origin : string , tree : string } [ ] , printLinesLimit : number = Number . MAX_VALUE ) : Promise < string [ ] > {
188208 const folderTree : any = { } ;
189209 const depthCounts : number [ ] = [ ] ;
190210
191211 // Build a tree structure from the file paths
192- filePaths . forEach ( filePath => {
193- const parts = filePath . split ( '/' ) ;
212+ // Store the file size in the leaf node and the folder size in the folder node
213+ // Store the number of children in the folder node
214+ for ( const filePath of filePaths ) {
215+ const parts = filePath . tree . split ( '/' ) ;
194216 let currentLevel = folderTree ;
195217
196218 parts . forEach ( ( part , depth ) => {
219+ const isFile = depth === parts . length - 1 ;
220+
221+ // Create the node if it doesn't exist
197222 if ( ! currentLevel [ part ] ) {
198- currentLevel [ part ] = depth === parts . length - 1 ? null : { } ;
223+ if ( isFile ) {
224+ // The file size is stored in the leaf node,
225+ currentLevel [ part ] = 0 ;
226+ } else {
227+ // The folder size is stored in the folder node
228+ currentLevel [ part ] = { } ;
229+ currentLevel [ part ] [ FOLDER_SIZE_KEY ] = 0 ;
230+ currentLevel [ part ] [ FOLDER_FILES_TOTAL_KEY ] = 0 ;
231+ }
232+
233+ // Count the number of items at each depth
199234 if ( depthCounts . length <= depth ) {
200235 depthCounts . push ( 0 ) ;
201236 }
202237 depthCounts [ depth ] ++ ;
203238 }
239+
204240 currentLevel = currentLevel [ part ] ;
241+
242+ // Count the total number of children in the nested folders
243+ if ( ! isFile ) {
244+ currentLevel [ FOLDER_FILES_TOTAL_KEY ] ++ ;
245+ }
205246 } ) ;
206- } ) ;
247+ } ;
207248
208- // Get max depth
249+ // Get max depth depending on the maximum number of lines allowed to print
209250 let currentDepth = 0 ;
210- let countUpToCurrentDepth = depthCounts [ 0 ] ;
251+ let countUpToCurrentDepth = depthCounts [ 0 ] + 1 /* root folder */ ;
211252 for ( let i = 1 ; i < depthCounts . length ; i ++ ) {
212- if ( countUpToCurrentDepth + depthCounts [ i ] > maxPrint ) {
253+ if ( countUpToCurrentDepth + depthCounts [ i ] > printLinesLimit ) {
213254 break ;
214255 }
215256 currentDepth ++ ;
216257 countUpToCurrentDepth += depthCounts [ i ] ;
217258 }
218-
219259 const maxDepth = currentDepth ;
220- let message : string [ ] = [ ] ;
221260
222- // Helper function to print the tree
223- const printTree = ( tree : any , depth : number , prefix : string ) => {
261+ // Get all file sizes
262+ const fileSizes : [ number , string ] [ ] = await Promise . all ( filePaths . map ( async ( filePath ) => {
263+ try {
264+ const stats = await fs . promises . stat ( filePath . origin ) ;
265+ return [ stats . size , filePath . tree ] ;
266+ } catch ( error ) {
267+ return [ 0 , filePath . origin ] ;
268+ }
269+ } ) ) ;
270+
271+ // Store all file sizes in the tree
272+ let totalFileSizes = 0 ;
273+ fileSizes . forEach ( ( [ size , filePath ] ) => {
274+ totalFileSizes += size ;
275+
276+ const parts = filePath . split ( '/' ) ;
277+ let currentLevel = folderTree ;
278+ parts . forEach ( part => {
279+ if ( typeof currentLevel [ part ] === 'number' ) {
280+ currentLevel [ part ] = size ;
281+ } else if ( currentLevel [ part ] ) {
282+ currentLevel [ part ] [ FOLDER_SIZE_KEY ] += size ;
283+ }
284+ currentLevel = currentLevel [ part ] ;
285+ } ) ;
286+ } ) ;
287+
288+ let output : string [ ] = [ ] ;
289+ output . push ( chalk . bold ( rootFolder ) ) ;
290+ output . push ( ...createTreeOutput ( folderTree , maxDepth , totalFileSizes ) ) ;
291+
292+ for ( const [ size , filePath ] of fileSizes ) {
293+ if ( size > FILE_SIZE_WARNING_THRESHOLD * totalFileSizes ) {
294+ output . push ( `\nThe file ${ filePath } is ${ chalk . red ( 'large' ) } (${ bytesToString ( size ) } )` ) ;
295+ break ;
296+ }
297+ }
298+
299+ return output ;
300+ }
301+
302+ function createTreeOutput ( fileSystem : any , maxDepth : number , totalFileSizes : number ) : string [ ] {
303+
304+ const getColorFromSize = ( size : number ) => {
305+ if ( size > FILE_SIZE_WARNING_THRESHOLD * totalFileSizes ) {
306+ return chalk . red ;
307+ } else if ( size > FILE_SIZE_LARGE_THRESHOLD * totalFileSizes ) {
308+ return chalk . yellow ;
309+ } else {
310+ return chalk . grey ;
311+ }
312+ } ;
313+
314+ const createFileOutput = ( prefix : string , fileName : string , fileSize : number ) => {
315+ let fileSizeColored = '' ;
316+ if ( fileSize > 0 ) {
317+ const fileSizeString = `[${ bytesToString ( fileSize ) } ]` ;
318+ fileSizeColored = getColorFromSize ( fileSize ) ( fileSizeString ) ;
319+ }
320+ return `${ prefix } ${ fileName } ${ fileSizeColored } ` ;
321+ }
322+
323+ const createFolderOutput = ( prefix : string , filesCount : number , folderSize : number , folderName : string , depth : number ) => {
324+ if ( depth < maxDepth ) {
325+ // Max depth is not reached, print only the folder
326+ // as children will be printed
327+ return prefix + chalk . bold ( `${ folderName } /` ) ;
328+ }
329+
330+ // Max depth is reached, print the folder name and additional metadata
331+ // as children will not be printed
332+ const folderSizeString = bytesToString ( folderSize ) ;
333+ const folder = chalk . bold ( `${ folderName } /` ) ;
334+ const numFilesString = chalk . green ( `(${ filesCount } ${ filesCount === 1 ? 'file' : 'files' } )` ) ;
335+ const folderSizeColored = getColorFromSize ( folderSize ) ( `[${ folderSizeString } ]` ) ;
336+ return `${ prefix } ${ folder } ${ numFilesString } ${ folderSizeColored } ` ;
337+ }
338+
339+ const createTreeLayerOutput = ( tree : any , depth : number , prefix : string , path : string ) => {
224340 // Print all files before folders
225- const sortedFolderKeys = Object . keys ( tree ) . filter ( key => tree [ key ] !== null ) . sort ( ) ;
226- const sortedFileKeys = Object . keys ( tree ) . filter ( key => tree [ key ] === null ) . sort ( ) ;
227- const sortedKeys = [ ...sortedFileKeys , ...sortedFolderKeys ] ;
341+ const sortedFolderKeys = Object . keys ( tree ) . filter ( key => typeof tree [ key ] !== 'number' ) . sort ( ) ;
342+ const sortedFileKeys = Object . keys ( tree ) . filter ( key => typeof tree [ key ] === 'number' ) . sort ( ) ;
343+ const sortedKeys = [ ...sortedFileKeys , ...sortedFolderKeys ] . filter ( key => key !== FOLDER_SIZE_KEY && key !== FOLDER_FILES_TOTAL_KEY ) ;
228344
345+ const output : string [ ] = [ ] ;
229346 for ( let i = 0 ; i < sortedKeys . length ; i ++ ) {
230-
231347 const key = sortedKeys [ i ] ;
232348 const isLast = i === sortedKeys . length - 1 ;
233349 const localPrefix = prefix + ( isLast ? '└─ ' : '├─ ' ) ;
234350 const childPrefix = prefix + ( isLast ? ' ' : '│ ' ) ;
235351
236- if ( tree [ key ] === null ) {
352+ if ( typeof tree [ key ] === 'number' ) {
237353 // It's a file
238- message . push ( localPrefix + key ) ;
354+ output . push ( createFileOutput ( localPrefix , key , tree [ key ] ) ) ;
239355 } else {
240356 // It's a folder
357+ output . push ( createFolderOutput ( localPrefix , tree [ key ] [ FOLDER_FILES_TOTAL_KEY ] , tree [ key ] [ FOLDER_SIZE_KEY ] , key , depth ) ) ;
241358 if ( depth < maxDepth ) {
242- // maxdepth is not reached, print the folder and its children
243- message . push ( localPrefix + chalk . bold ( `${ key } /` ) ) ;
244- printTree ( tree [ key ] , depth + 1 , childPrefix ) ;
245- } else {
246- // max depth is reached, print the folder but not its children
247- const filesCount = countFiles ( tree [ key ] ) ;
248- message . push ( localPrefix + chalk . bold ( `${ key } /` ) + chalk . green ( ` (${ filesCount } ${ filesCount === 1 ? 'file' : 'files' } )` ) ) ;
359+ output . push ( ...createTreeLayerOutput ( tree [ key ] , depth + 1 , childPrefix , path + key + '/' ) ) ;
249360 }
250361 }
251362 }
363+ return output ;
252364 } ;
253365
254- // Helper function to count the number of files in a tree
255- const countFiles = ( tree : any ) : number => {
256- let filesCount = 0 ;
257- for ( const key in tree ) {
258- if ( tree [ key ] === null ) {
259- filesCount ++ ;
260- } else {
261- filesCount += countFiles ( tree [ key ] ) ;
262- }
263- }
264- return filesCount ;
265- } ;
266-
267- message . push ( chalk . bold ( rootFolder ) ) ;
268- printTree ( folderTree , 0 , '' ) ;
269-
270- return message ;
271- }
366+ return createTreeLayerOutput ( fileSystem , 0 , '' , '' ) ;
367+ }
0 commit comments