@@ -198,6 +198,7 @@ var options = {
198198 host : process . env . PM2_SERVE_HOST || process . argv [ 4 ] || '0.0.0.0' ,
199199 path : path . resolve ( process . env . PM2_SERVE_PATH || process . argv [ 2 ] || '.' ) ,
200200 spa : process . env . PM2_SERVE_SPA === 'true' ,
201+ ftp : process . env . PM2_SERVE_FTP === 'true' ,
201202 homepage : process . env . PM2_SERVE_HOMEPAGE || '/index.html' ,
202203 basic_auth : process . env . PM2_SERVE_BASIC_AUTH === 'true' ? {
203204 username : process . env . PM2_SERVE_BASIC_AUTH_USERNAME ,
@@ -249,10 +250,6 @@ http.createServer(function (request, response) {
249250function serveFile ( uri , request , response ) {
250251 var file = decodeURIComponent ( new URL ( uri || request . url , 'http://localhost' ) . pathname ) ;
251252
252- if ( file === '/' || file === '' ) {
253- file = options . homepage ;
254- request . wantHomepage = true ;
255- }
256253 var filePath = path . resolve ( options . path + file ) ;
257254
258255 // since we call filesystem directly so we need to verify that the
@@ -262,6 +259,136 @@ function serveFile(uri, request, response) {
262259 return response . end ( '403 Forbidden' ) ;
263260 }
264261
262+ fs . stat ( filePath , function ( err , stats ) {
263+ if ( ! err && stats . isDirectory ( ) ) {
264+ if ( options . ftp ) {
265+ // In ftp mode, try index.html first, fall back to directory listing
266+ var indexPath = path . join ( filePath , 'index.html' ) ;
267+ fs . access ( indexPath , fs . constants . R_OK , function ( err ) {
268+ if ( ! err ) {
269+ return serveStaticFile ( indexPath , file + '/index.html' , request , response ) ;
270+ }
271+ return serveDirectoryListing ( filePath , file , response ) ;
272+ } ) ;
273+ return ;
274+ }
275+ // Not ftp mode: serve homepage
276+ if ( file === '/' || file === '' ) {
277+ file = options . homepage ;
278+ request . wantHomepage = true ;
279+ filePath = path . resolve ( options . path + file ) ;
280+ }
281+ } else if ( file === '/' || file === '' ) {
282+ file = options . homepage ;
283+ request . wantHomepage = true ;
284+ filePath = path . resolve ( options . path + file ) ;
285+ }
286+ serveStaticFile ( filePath , file , request , response ) ;
287+ } ) ;
288+ }
289+
290+ function formatFileSize ( bytes ) {
291+ if ( bytes < 1024 ) return bytes + 'B' ;
292+ if ( bytes < 1024 * 1024 ) return ( bytes / 1024 ) . toFixed ( 1 ) + 'K' ;
293+ if ( bytes < 1024 * 1024 * 1024 ) return ( bytes / ( 1024 * 1024 ) ) . toFixed ( 1 ) + 'M' ;
294+ return ( bytes / ( 1024 * 1024 * 1024 ) ) . toFixed ( 1 ) + 'G' ;
295+ }
296+
297+ function serveDirectoryListing ( dirPath , urlPath , response ) {
298+ fs . readdir ( dirPath , function ( err , entries ) {
299+ if ( err ) {
300+ response . writeHead ( 500 ) ;
301+ return response . end ( 'Error reading directory' ) ;
302+ }
303+
304+ // Ensure urlPath ends with /
305+ if ( urlPath && urlPath [ urlPath . length - 1 ] !== '/' ) urlPath += '/' ;
306+ if ( ! urlPath ) urlPath = '/' ;
307+
308+ var pending = entries . length ;
309+ var items = [ ] ;
310+
311+ if ( pending === 0 ) {
312+ return renderDirectoryListing ( dirPath , urlPath , items , response ) ;
313+ }
314+
315+ entries . forEach ( function ( entry ) {
316+ // Skip dotfiles
317+ if ( entry [ 0 ] === '.' ) {
318+ if ( -- pending === 0 ) renderDirectoryListing ( dirPath , urlPath , items , response ) ;
319+ return ;
320+ }
321+ fs . stat ( path . join ( dirPath , entry ) , function ( err , stats ) {
322+ if ( ! err ) {
323+ items . push ( {
324+ name : entry ,
325+ isDir : stats . isDirectory ( ) ,
326+ size : stats . size ,
327+ mtime : stats . mtime
328+ } ) ;
329+ }
330+ if ( -- pending === 0 ) renderDirectoryListing ( dirPath , urlPath , items , response ) ;
331+ } ) ;
332+ } ) ;
333+ } ) ;
334+ }
335+
336+ function renderDirectoryListing ( dirPath , urlPath , items , response ) {
337+ // Directories first, then files, alphabetical within each group
338+ items . sort ( function ( a , b ) {
339+ if ( a . isDir !== b . isDir ) return a . isDir ? - 1 : 1 ;
340+ return a . name . localeCompare ( b . name ) ;
341+ } ) ;
342+
343+ var escHtml = function ( s ) { return s . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) . replace ( / " / g, '"' ) ; } ;
344+
345+ var rows = '' ;
346+
347+ // Parent directory link (unless at root)
348+ if ( urlPath !== '/' ) {
349+ rows += '<tr><td class="name"><a href="../">../</a></td><td class="size">-</td><td class="mtime">-</td></tr>\n' ;
350+ }
351+
352+ items . forEach ( function ( item ) {
353+ var displayName = item . isDir ? item . name + '/' : item . name ;
354+ var href = encodeURIComponent ( item . name ) + ( item . isDir ? '/' : '' ) ;
355+ var size = item . isDir ? '-' : formatFileSize ( item . size ) ;
356+ var mtime = item . mtime . toISOString ( ) . replace ( 'T' , ' ' ) . slice ( 0 , 19 ) ;
357+ rows += '<tr>'
358+ + '<td class="name"><a href="' + escHtml ( href ) + '">' + escHtml ( displayName ) + '</a></td>'
359+ + '<td class="size">' + size + '</td>'
360+ + '<td class="mtime">' + mtime + '</td>'
361+ + '</tr>\n' ;
362+ } ) ;
363+
364+ var html = '<!DOCTYPE html>\n<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">'
365+ + '<title>Index of ' + escHtml ( urlPath ) + '</title>'
366+ + '<style>'
367+ + 'body{font-family:monospace;margin:20px;background:#1e1e1e;color:#ccc}'
368+ + 'h1{font-size:1.2em;border-bottom:1px solid #444;padding-bottom:8px}'
369+ + 'table{border-collapse:collapse;width:100%}'
370+ + 'th,td{text-align:left;padding:4px 12px}'
371+ + 'th{border-bottom:2px solid #444;font-size:0.9em;color:#888}'
372+ + 'tr:hover{background:#2a2a2a}'
373+ + '.size,.mtime{white-space:nowrap}'
374+ + '.size{text-align:right}'
375+ + 'a{color:#58a6ff;text-decoration:none}'
376+ + 'a:hover{text-decoration:underline}'
377+ + '</style></head><body>'
378+ + '<h1>Index of ' + escHtml ( urlPath ) + '</h1>'
379+ + '<table><thead><tr><th>Name</th><th class="size">Size</th><th>Last Modified</th></tr></thead>'
380+ + '<tbody>' + rows + '</tbody></table>'
381+ + '</body></html>' ;
382+
383+ response . writeHead ( 200 , {
384+ 'Content-Type' : 'text/html' ,
385+ 'Access-Control-Allow-Origin' : '*' ,
386+ 'Access-Control-Allow-Methods' : 'GET'
387+ } ) ;
388+ response . end ( html , 'utf-8' ) ;
389+ }
390+
391+ function serveStaticFile ( filePath , file , request , response ) {
265392 var contentType = contentTypes [ filePath . split ( '.' ) . pop ( ) . toLowerCase ( ) ] || 'text/plain' ;
266393
267394 fs . readFile ( filePath , function ( error , content ) {
0 commit comments