List approved files from a directory with PHP

Build a small public download list with a fixed directory, an extension allowlist, escaped labels and no hidden files or symlink traversal.

A directory-backed download list is useful for a small collection of public PDFs or images. The important word is public: hiding a filename from the list does not stop someone requesting the file directly.

Give the list its own directory

Put only publication-approved files in downloads/, alongside this PHP page, and serve that directory at /downloads/. The code is deliberately non-recursive: it lists files from that one directory, not the surrounding project.

<?php
declare(strict_types=1);

$directory = __DIR__ . '/downloads';
$extensions = ['pdf', 'jpg', 'jpeg', 'png', 'mp3', 'docx'];
$names = [];

if (!is_dir($directory) || is_link($directory)) {
    throw new RuntimeException('The public download directory is unavailable.');
}

foreach (new FilesystemIterator($directory, FilesystemIterator::SKIP_DOTS) as $entry) {
    $name = $entry->getFilename();
    if ($entry->isLink() || !$entry->isFile() || str_starts_with($name, '.')
        || !in_array(strtolower($entry->getExtension()), $extensions, true)) {
        continue;
    }
    $names[] = $name;
    if (count($names) > 100) {
        throw new RuntimeException('Use a paginated catalogue for a larger library.');
    }
}
natcasesort($names);

echo '<ul>';
foreach ($names as $name) {
    $label = htmlspecialchars($name, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
    echo '<li><a href="/downloads/' . rawurlencode($name) . '">'
        . $label . '</a></li>';
}
echo '</ul>';

Filename encoding keeps spaces and characters such as & from breaking the URL. HTML escaping protects the visible label independently. Neither is a substitute for deciding which files belong online.

Do not turn this into a file browser

Keep the filesystem path in code or trusted configuration, never in a query parameter. Backups, invoices, source files and customer documents belong elsewhere. For private downloads, use an authenticated handler with a per-file permission check rather than direct public links. Uploaded files also need their own content validation and safe serving policy; an extension allowlist alone does not establish a file's actual type.

References: FilesystemIterator and escaping HTML.

Original version26 January 2014

Kept here for reference and earlier links. The updated guide above is the recommended starting point; older code may depend on retired services or different software versions.

I've created this simple bit of PHP to specify a directory, and then list the files grouped by folder on screen.

You can set certain files to be created as links, for easy browsing, they are: jpg,png,gif,docx,pdf,mp3. You can add any other file you want in the $files line.

I personally find this snippet very useful for document libraries and file management. As it saves you adding a file, and then having to manually list it in any index.php file. It will just automatically appear!

This example shows how it will turn a list of folders/documents etc into a clickable directory online:
PHP Dynamic Library Code Example

Just change the $path variable to point to your desired directory.

<? ob_start();

$path="example/Docs";

$directories = glob($path.'/*', GLOB_BRACE);
foreach($directories as $directory) {
 echo '
  <div class="container_16">
    <div class="grid_16"><table border="1" bordercolor="#FFFFFF" class="tables">
      <tr>
        <td colspan="2" bordercolor="#999999" bgcolor="#f4f4f4"><span class="table-title"><em><strong>';?><?=$directory; echo'</strong></em></span>
        </td>
      </tr>
      <tr>
        <td colspan="2" bordercolor="#CCCCCC"><table border="1" bordercolor="#FFFFFF">
          <tr bordercolor="#FFFFFF" class="forms">';
           $files = glob($directory. '/*.{jpg,png,gif,docx,pdf,mp3}', GLOB_BRACE);
           foreach($files as $file1) {
             $info1 = pathinfo($file1);
             $name = $info1['filename']; //index
             echo "<a href=\"$file1\">$name</a><br>";
           }
           echo '
        </table></td>
      </tr>
    </table>
    </div><!—End Grid16—>
  </div><!—End Container16—>';
}
ob_flush(); ?>

Keep exploring

A couple more useful notes.