or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

directory-listing.mdindex.mdplugin-registration.mdreply-decorators.md
tile.json

directory-listing.mddocs/

Directory Listing

Optional directory browsing functionality that provides JSON and HTML formatted directory contents with extensible rendering, filtering options, and detailed file metadata.

Capabilities

Directory Listing Configuration

Enable and configure directory listing functionality with flexible output formats and rendering options.

/**
 * Directory listing configuration options
 */
type ListConfig = boolean | ListOptionsJsonFormat | ListOptionsHtmlFormat;

/**
 * Base options for directory listing
 */
interface ListOptions {
  /** Route names that trigger directory listing */
  names?: string[];
  /** Include extended folder information */
  extendedFolderInfo?: boolean;
  /** JSON output format - 'names' or 'extended' */
  jsonFormat?: 'names' | 'extended';
}

interface ListOptionsJsonFormat extends ListOptions {
  /** Output format - 'json' for structured data */
  format: 'json';
  /** Optional render function for HTML format query parameter */
  render?: ListRender;
}

interface ListOptionsHtmlFormat extends ListOptions {
  /** Output format - 'html' for web display */
  format: 'html';
  /** Required render function for HTML output */
  render: ListRender;
}

/**
 * Render function for HTML directory listings
 * @param dirs - Array of directory entries
 * @param files - Array of file entries  
 * @returns HTML string for directory display
 */
interface ListRender {
  (dirs: ListDir[], files: ListFile[]): string;
}

Usage Examples:

// Basic directory listing (JSON format)
await fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'public'),
  prefix: '/files/',
  list: true
});

// JSON format with extended information
await fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'docs'),
  prefix: '/docs/',
  list: {
    format: 'json',
    jsonFormat: 'extended', 
    extendedFolderInfo: true,
    names: ['index', 'directory', '/']
  }
});

// HTML format with custom rendering
await fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'gallery'),
  prefix: '/gallery/',
  list: {
    format: 'html',
    render: (dirs, files) => `
      <!DOCTYPE html>
      <html>
        <head><title>Gallery</title></head>
        <body>
          <h1>Gallery Directory</h1>
          <div class="folders">
            ${dirs.map(dir => `
              <a href="${dir.href}" class="folder">
                πŸ“ ${dir.name}
                ${dir.extendedInfo ? `(${dir.extendedInfo.fileCount} files)` : ''}
              </a>
            `).join('')}
          </div>
          <div class="files">
            ${files.map(file => `
              <a href="${file.href}" class="file">
                πŸ“„ ${file.name} (${formatBytes(file.stats.size)})
              </a>
            `).join('')}
          </div>
        </body>
      </html>
    `
  }
});

Directory Entry Types

Data structures for directory and file entries in listings.

/**
 * Directory entry in listing
 */
interface ListDir {
  /** URL path to directory */
  href: string;
  /** Directory name */
  name: string;
  /** File system stats */
  stats: Stats;
  /** Extended information if enabled */
  extendedInfo?: ExtendedInformation;
}

/**
 * File entry in listing
 */
interface ListFile {
  /** URL path to file */
  href: string;
  /** File name */
  name: string;
  /** File system stats */
  stats: Stats;
}

/**
 * Extended folder information
 */
interface ExtendedInformation {
  /** Number of files in this folder */
  fileCount: number;
  /** Total number of files (recursive) */
  totalFileCount: number;
  /** Number of subfolders in this folder */
  folderCount: number;
  /** Total number of subfolders (recursive) */
  totalFolderCount: number;
  /** Total size of all files (recursive) */
  totalSize: number;
  /** Most recent last modified timestamp (recursive) */
  lastModified: number;
}

JSON Output Formats

Different JSON response formats for directory listings.

Names Format:

{
  "dirs": ["folder1", "folder2"],
  "files": ["file1.txt", "file2.pdf"]
}

Extended Format:

{
  "dirs": [
    {
      "name": "folder1",
      "stats": {
        "dev": 2049,
        "mode": 16877,
        "size": 4096,
        "mtime": "2023-01-15T10:30:00.000Z"
      },
      "extendedInfo": {
        "fileCount": 5,
        "totalFileCount": 12,
        "folderCount": 2,
        "totalFolderCount": 3,
        "totalSize": 15420,
        "lastModified": 1673781000000
      }
    }
  ],
  "files": [
    {
      "name": "file1.txt",
      "stats": {
        "dev": 2049,
        "mode": 33188,
        "size": 1024,
        "mtime": "2023-01-14T15:20:00.000Z"
      }
    }
  ]
}

Route Name Configuration

Configure which route names trigger directory listing responses.

/**
 * Route names that trigger directory listing
 */
type ListNames = string[];

Usage Examples:

// Multiple route names for directory listing
{
  list: {
    format: 'json',
    names: ['index', 'index.json', 'directory', '/']
  }
}

// Directory listing responds to:
// GET /prefix/
// GET /prefix/index
// GET /prefix/index.json  
// GET /prefix/directory
// GET /prefix/subfolder/
// GET /prefix/subfolder/index
// etc.

Format Override via Query Parameter

Override configured format using URL query parameters.

Usage Examples:

// Configuration with HTML format
{
  list: {
    format: 'html',
    render: (dirs, files) => '...',
    jsonFormat: 'extended'
  }
}

// Requests:
// GET /files/          -> HTML response (default)
// GET /files/?format=json -> JSON response (override)
// GET /files/folder?format=html -> HTML response (explicit)

Extended Folder Information

Detailed folder statistics including recursive counts and sizes.

Usage Examples:

// Enable extended folder info
{
  list: {
    format: 'json',
    jsonFormat: 'extended',
    extendedFolderInfo: true
  }
}

// Custom render function using extended info
{
  list: {
    format: 'html',
    extendedFolderInfo: true,
    render: (dirs, files) => {
      return `
        <div class="directory-listing">
          ${dirs.map(dir => `
            <div class="folder">
              <h3><a href="${dir.href}">${dir.name}/</a></h3>
              ${dir.extendedInfo ? `
                <div class="stats">
                  <span>${dir.extendedInfo.fileCount} files</span>
                  <span>${dir.extendedInfo.folderCount} folders</span>
                  <span>${formatBytes(dir.extendedInfo.totalSize)} total</span>
                  <span>Modified: ${new Date(dir.extendedInfo.lastModified).toLocaleDateString()}</span>
                </div>
              ` : ''}
            </div>
          `).join('')}
        </div>
      `;
    }
  }
}

Security Considerations

Directory listing respects dotfile and security settings.

Usage Examples:

// Respect dotfiles setting
await fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'files'),
  dotfiles: 'deny', // Hidden files won't appear in listings
  list: true
});

// Custom allowedPath with directory listing
await fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'content'),
  list: {
    format: 'json',
    names: ['/']
  },
  allowedPath: (pathName, root, request) => {
    // Block listing of admin directories
    if (pathName.includes('/admin/')) {
      return false;
    }
    // Require authentication for private directories
    if (pathName.includes('/private/') && !request.user) {
      return false;
    }
    return true;
  }
});

Complete Example

Full directory listing implementation with all features.

Usage Examples:

await fastify.register(require('@fastify/static'), {
  root: path.join(__dirname, 'files'),
  prefix: '/browse/',
  dotfiles: 'ignore',
  list: {
    format: 'html',
    names: ['index', 'browse', '/'],
    extendedFolderInfo: true,
    jsonFormat: 'extended',
    render: (dirs, files) => {
      const formatBytes = (bytes) => {
        if (bytes === 0) return '0 B';
        const k = 1024;
        const sizes = ['B', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
      };
      
      const formatDate = (timestamp) => {
        return new Date(timestamp).toLocaleDateString('en-US', {
          year: 'numeric',
          month: 'short',
          day: 'numeric',
          hour: '2-digit',
          minute: '2-digit'
        });
      };
      
      return `
        <!DOCTYPE html>
        <html>
          <head>
            <title>File Browser</title>
            <style>
              body { font-family: Arial, sans-serif; margin: 20px; }
              .item { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee; }
              .folder { background: #f0f8ff; }
              .name { flex: 1; margin-left: 10px; }
              .size { width: 100px; text-align: right; color: #666; }
              .date { width: 150px; text-align: right; color: #666; }
              a { text-decoration: none; color: #0066cc; }
              a:hover { text-decoration: underline; }
            </style>
          </head>
          <body>
            <h1>πŸ“ File Browser</h1>
            <div class="listing">
              ${dirs.map(dir => `
                <div class="item folder">
                  <span>πŸ“</span>
                  <div class="name">
                    <a href="${dir.href}">${dir.name}/</a>
                    ${dir.extendedInfo ? `<br><small>${dir.extendedInfo.fileCount} files, ${dir.extendedInfo.folderCount} folders</small>` : ''}
                  </div>
                  <div class="size">${dir.extendedInfo ? formatBytes(dir.extendedInfo.totalSize) : '-'}</div>
                  <div class="date">${formatDate(dir.stats.mtime)}</div>
                </div>
              `).join('')}
              ${files.map(file => `
                <div class="item">
                  <span>πŸ“„</span>
                  <div class="name">
                    <a href="${file.href}" target="_blank">${file.name}</a>
                  </div>
                  <div class="size">${formatBytes(file.stats.size)}</div>
                  <div class="date">${formatDate(file.stats.mtime)}</div>
                </div>
              `).join('')}
            </div>
            <p><a href="?format=json">View as JSON</a></p>
          </body>
        </html>
      `;
    }
  }
});

// Accessible at:
// GET /browse/ -> HTML listing
// GET /browse/?format=json -> JSON listing  
// GET /browse/subfolder/ -> HTML listing of subfolder
// GET /browse/subfolder/index -> Same as above