スマホでも見やすいDirectoryIndexを作る

nginxやapacheに標準でついているDirectoryIndex（いわゆる Index of）がスマホ画面ではとてもとても使いずらいので、簡易的なファイルブラウザを作ってみた。

要件

* スマホでも操作がしやすいこと
* 必要最低限の認証機能はつける
* ファイル名ソート・新しい順ソートの切り替えは欲しい
* ファイルサイズは表示させたい
* ファイル一覧画面のURIはDocumentRootからのパスに合わせる必要はないが、ファイル自体のURIはサーバのファイルに直リする
  * 動画ファイルのランダムアクセスがしたいため 
  * HTTP HEADERに Range:を含めたいんだけどコードで自前対応するのは面倒なので、これはウェブサーバにおまかせしたいのです
* 実装に時間をかけない

最低限の見栄えと、とりあえず最低限の認証機能込みで、実装は２時間くらい。

## 出来上がったもの

こんな感じです。
DocumentRoot以下のディレクトリ、ファイルがウェブブラウザから参照できます。
スマホの画面から動画を選んで再生するような使い方が主目的のため、スマホでの操作が苦にならないような見た目にしています。

![Screenshot_2021-02-11-21-12-06-49_df198e732186825c8df26e3c5a10d7cd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78337/315fdce5-ad05-65d5-eb85-807a2a1d7a3a.png)

![Screenshot_2021-02-11-21-12-59-82_df198e732186825c8df26e3c5a10d7cd.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/78337/dcc10107-67f9-2724-607f-241cc69119d9.png)


## ソースコード

```/script/index.php
<?php

const BASE_DIR = '/mnt/disk';
const SECRET = '**********';
const PASSWORD = '**********';

const DIRECTORY_KEY = 'cd';

function setAuth($password) {
    if ($password === PASSWORD) {
        setcookie('auth', hash('sha256', PASSWORD.SECRET), time() + 30 * 24 * 60 * 60);
    } else {
        setcookie('auth', '', -1);
    }
}

function checkAuth() {
    if($_COOKIE['auth'] === hash('sha256', PASSWORD.SECRET)) {
        setcookie('auth', hash('sha256', PASSWORD.SECRET), time() + 30 * 24 * 60 * 60);
        return true;
    } else {
        return false;
    };
}

function displayFileSize($size) {
    $unit = ['B', 'KB', 'MB', 'GB', 'TB'];
    for($i = 0; $i < count($unit); $i ++) {
        if (round($size / 1024, 1) < 1.0) {
            break;
        }
        $size = $size / 1024;
    }
    return strval(round($size, 1)).$unit[$i];
}

$directory = BASE_DIR.($_GET[DIRECTORY_KEY] ?? '');
$pos = strpos($directory, '/..');
if ($pos !== false && $pos >= 0) {
    exit;
}
$upperDirectory = preg_replace('@/[^/]+$@', '', $directory);
$sort = $_GET['sort'] ?? 'time';

if ($_SERVER['REQUEST_URI'] === '/auth') {
    if ($_SERVER['REQUEST_METHOD'] === 'GET') {
        echo '<html><head>    <meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex" /></head><body><form method="post"><input type="password" name="password"/><input type="submit"></form></body></html>';
    } else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        setAuth($_POST['password']);
        header('Location: /');
    }
    exit;
}
if (!checkAuth()) {
    exit;
}
?>

<html>
<head>
    <style>
    .container {
        margin: 0 auto;
        width: 100%;
        max-width: 1200px;
    }
    .filelist .filesystem-item:nth-child(odd) {  
    background-color: #FFFFFF;  
    }
    .filelist .filesystem-item:nth-child(even) {  
    background-color: #EEEEEE;  
    }  
    .filesystem-item {
        padding: 0.75rem;
        display: flex;
    }
    .filesystem-item .directory, .filesystem-item .file {
        display: inline-blo;
        flex-grow: 1;
        word-break : break-all;
    }
    .filesystem-item .filesize {
        flex-basis: 70px;
        flex-grow: 0;
        flex-shrink: 0;
        padding-left: 10px;
    }
    .options {
        display: inline-block;
    }
    .option {
        display: inline-block;
        padding: 5px 10px;
    }
    </style>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <meta name="robots" content="noindex" />
</head>

<body>
    <div class="container">
<?php
        echo '<h1>'.preg_replace('@.+/@', '', $directory).'</h1>';
        if (strpos($upperDirectory, BASE_DIR) === 0) {
            echo '<h2><a href="/?'.DIRECTORY_KEY.'='.urlencode(preg_replace('@^'.BASE_DIR.'@', '', $upperDirectory)).'&sort='.$sort.'">'.preg_replace('@.+\/@', '', $upperDirectory).'</a>に戻る</h2>';
        }
?>
        <div class="options">
            <?php echo '<a class="option" href="/?'.DIRECTORY_KEY.'='.urlencode($_GET[DIRECTORY_KEY]).'&sort=name">名前順</a>' ?>
            <?php echo '<a class="option" href="/?'.DIRECTORY_KEY.'='.urlencode($_GET[DIRECTORY_KEY]).'&sort=time">新しい順</a>' ?>
        </div>

        <div class="filelist">
            <?php
                $files = [];
                foreach (glob($directory.'/*') as $file) {
                    $files []= $file;
                }
                usort($files, function($a, $b) use($sort) {
                    if (is_dir($a) && is_file($b)) {
                        return -1;
                    } else if (is_file($a) && is_dir($b)) {
                        return 1;
                    }
                    if ($sort == 'time') {
                        if (filemtime($a) < filemtime($b)) {
                            return 1;
                        } else if (filemtime($a) > filemtime($b)) {
                            return -1;
                        }
                    } else if ($sort == 'name') {
                        if ($a < $b) {
                            return -1;
                        } else if ($a > $b) {
                            return 1;
                        }
                    }
                    return 0;
                });
                
                foreach($files as $file) {
                    $file = preg_replace('@^'.BASE_DIR.'@', '', $file);    
                    echo '<div class="filesystem-item">';
                    $sizeFile = '';
                    if(is_dir(BASE_DIR.$file)) {
                        echo '<a class="directory" href="/?'.DIRECTORY_KEY.'='.urlencode($file).'&sort='.$sort.'">'.preg_replace('@.+/@', '', $file).'/</a>'."\n";
                        $sizeFile = date('Y/m/d', filemtime(BASE_DIR.$file));
                    } else if (is_file(BASE_DIR.$file)) {
                        echo '<a class="file" href="'.str_replace('#', '%23', $file).'">'.preg_replace('@.+/@', '', $file).'</a>'."\n";
                        $sizeFile = displayFileSize(filesize(BASE_DIR.$file));
                    }
                    echo '<div class="filesize">'.$sizeFile.'</div>';
                    echo '</div>';
                }
            ?>
        </div>
    </div>
</body>
</html>
```

```nginx.conf
server {
        listen 12345 default_server;
        root /mnt/disk;
        location / {
                try_files $uri /script/index.php?$args;
        }

        location ~ \.php$ {
            fastcgi_split_path_info ^(.+\.php)(/.+)$;
            if (!-f $document_root$fastcgi_script_name) {
                return 404;
            }
            fastcgi_pass unix:/var/run/php/php7.3-fpm.sock;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param PATH_INFO $fastcgi_path_info;
            fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
       }
}
```

細かいところはソース見てください。
初回アクセス時に、 /auth にアクセスするとパスワード入力のinputが表示されます。
パスワード入力後は、https?://<server_name>/ から document_root以下のディレクトリをブラウズできます。