import * as path from 'node:path' import * as fsp from 'node:fs/promises' import { html } from '@remix-run/html-template' import { createHtmlResponse } from '@remix-run/response/html' import { detectMimeType } from '@remix-run/mime' interface DirectoryEntry { name: string isDirectory: boolean size: number type: string } export async function generateDirectoryListing( dirPath: string, pathname: string, ): Promise { let entries: DirectoryEntry[] = [] try { let dirents = await fsp.readdir(dirPath, { withFileTypes: true }) for (let dirent of dirents) { let fullPath = path.join(dirPath, dirent.name) let isDirectory = dirent.isDirectory() let size = 0 let type = '' if (isDirectory) { size = await calculateDirectorySize(fullPath) } else { try { let stats = await fsp.stat(fullPath) size = stats.size let mimeType = detectMimeType(dirent.name) type = mimeType || 'application/octet-stream' } catch { // Unable to stat file, use defaults } } entries.push({ name: dirent.name, isDirectory, size, type, }) } } catch { return new Response('Error reading directory', { status: 500 }) } // Sort: directories first, then alphabetically entries.sort((a, b) => { if (a.isDirectory && !b.isDirectory) return -1 if (!a.isDirectory && b.isDirectory) return 1 return a.name.localeCompare(b.name, undefined, { numeric: true }) }) // Build table rows let tableRows = [] let folderIcon = html.raw` ` let fileIcon = html.raw` ` // Add parent directory link if not at root if (pathname !== '/' && pathname !== '') { let parentPath = pathname.replace(/\/$/, '').split('/').slice(0, -1).join('/') || '/' tableRows.push(html` ${folderIcon} .. `) } for (let entry of entries) { let icon = entry.isDirectory ? folderIcon : fileIcon let href = pathname.endsWith('/') ? pathname + entry.name : pathname + '/' + entry.name let sizeDisplay = formatFileSize(entry.size) let typeDisplay = entry.isDirectory ? 'Folder' : entry.type tableRows.push(html` ${icon} ${entry.name} ${sizeDisplay} ${typeDisplay} `) } return createHtmlResponse(html` Index of ${pathname}

Index of ${pathname}

${tableRows}
Name Size Type
`) } async function calculateDirectorySize(dirPath: string): Promise { let totalSize = 0 try { let dirents = await fsp.readdir(dirPath, { withFileTypes: true }) for (let dirent of dirents) { let fullPath = path.join(dirPath, dirent.name) try { if (dirent.isDirectory()) { totalSize += await calculateDirectorySize(fullPath) } else if (dirent.isFile()) { let stats = await fsp.stat(fullPath) totalSize += stats.size } } catch { // Skip files/folders we can't access } } } catch { // If we can't read the directory, return 0 } return totalSize } function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B' let units = ['B', 'kB', 'MB', 'GB', 'TB'] let i = Math.floor(Math.log(bytes) / Math.log(1024)) let size = bytes / Math.pow(1024, i) return size.toFixed(i === 0 ? 0 : 1) + ' ' + units[i] }